C语言:自定义类型——结构体
在C语言的类型体系中,除了char、int、float、double等基本数据类型之外,我们还可以根据实际需求自己创造数据类型,这就是自定义类型。而结构体(struct)是C语言里最基础、最常用、最重要的自定义类型,是描述复杂对象、构建数据结构、开发底层程序的核心工具。
一、结构体类型的介绍
1.结构体的官方定义
结构体(Structure):是一种由多个不同的类型(也可相同)变量组合而成的复合数据类型。这些组成结构体的变量,被称为成员变量(Member)。
他的核心价值在于:可以把描述同一个对象的多个属性,封装成一个逻辑上的整体。举例如下:
- 描述一个人,我们需要姓名、年龄、身高、性别、身份证号、体重等多个属性。
- 如果用独立变量存储,数据分散、管理混乱;用结构体就可以把这些属性打包在一起,形成一个完整的“人”的模型,既方便理解,也便于传递与维护。
2.为什么需要结构体?
在实际开发中,结构体的优势非常明显。
- 逻辑封装:把同一个对象的所有属性封装在一起,代码可读性和可维护性大幅度提升。
- 数据传递高效:作为函数传递参数时,通过传递结构体指针,就能一次性传递整个对象的所有信息,而不用传递多个零散变量。
- 自定义数据模型:可以根据业务需求,灵活构建各种复杂的数据结构,比如:链表节点、树节点、网络数据包、坐标点、学生信息、员工档案等。
3.结构体的核心特点
- 成员类型不固定:结构体成员可以是任意类型,包括基础类型(int、char、float)、数组、指针,甚至结构体。
- 内存布局可定制:通过内存对齐规则,结构体的大小可以优化,在性能和空间占用之间找到平衡感。
- 支持自引用:结构体可以包含指向自身类型的指针,这是实现链表、树等数据结构的基础。
二、结构体类型的声明
1.标准声明格式
struct 结构体标签名
{
类型1 成员名1;
类型2 成员名2;
....
类型n 成员名n;
};
- struct:关键字,声明结构体必写。
- 标签名:结构体的主名。
- {}:成员列表,描述结构体包含哪些属性。
- ;:结束符,必须有。
示例:声明学生结构体
struct Student
{
char name[20];
int age;
char sex[5];
char id[20];
};
2.struct 和 typedef struct的区别
(1)普通struct声明
普通结构体的声明,只是定义了一个带标签的结构体类型,使用时必须带struct关键字。
struct Student
{
char name[20];
int age;
}s1;//创建全局变量s1
//使用时,必须写struct Student
struct Student s2;//创建全局变量s2
//Student s2是错误的写法
int main()
{
struct Student s3;//创建局域变量s3
return 0;
}
- 标签名Student只是结构体的标识,不是完整的类型名。
- 每次定义变量,都要写struct 标签名,比较麻烦。
(2)普通typedef struct声明
typedef 的作用是给已知类型起别名,用在结构体上,可以把struct 标签名 简化成一个新的类型名,使用时不用再写struct。
typedef struct Student
{
char name[20];
int age;
}Stu;//Stu是别名
//使用时,直接写别名即可
Stu s1;//创建全局变量s1
struct Student s2;//创建全局变量s2
- 使用更简洁,不用每次都写struct。
- 是工程开发中最常用的写法,尤其是链表、树节点等复杂结构。
3.匿名结构体
匿名结构体:声明时省略标签名。
特点:只能使用一次,不能重复使用,编译器会把两个成员相同的匿名结构体视为不同类型。
struct
{
int a;
char b;
float c;
}a;//创建变量a
struct
{
int a;
char b;
float c;
}*p;//创建指针p
p=&a;//将a的地址赋值给p,但编译器认为a和p类型不同,无法赋值,错误写法
- p=&a:因为匿名结构体没有标签名,编译器无法判断他们是同一种类型,只能按定义的先后顺序视为不同类型,所以编译错误。
4.结构体自引用
定义:在结构体内部,包含一个指向自身类型的指针成员。
思考:在结构体中包含类型为该结构体本身的成员是否可以呢?
struct Node
{
int data;
struct Node next;//创建结构体变量next
};
如图所示,我们在结构体中创建结构体类型变量,那么struct Node 的大小=int 的大小+struct Node next 的大小,而struct Node 的大小又依赖自身的大小,会无限递归,无法计算,所以,这种方式是错误的。
//正确写法:用指针引用自身
struct Node
{
int data;
struct Node *next;//指针,指向同类型节点
};
总结:结构体自引用,只能用指针,不能用自身变量。
三、结构体变量的创建与初始化
1.定义结构体变量
//方法1:先声明类型,再定义变量(struct)
struct Student s1;
//方法2:先声明类型,再定义变量(typedef struct)
Stu s2;
//方法3:声明时直接定义变量
struct Student
{
char name[20];
int age;
char sex[5];
char id[20];
}s3;
2.两种初始化方式
(1)按顺序初始化
struct Student s1={"张三",20,"男","2025001"};
(2)按指定成员初始化
struct Student s2={
.age=18,
.name="李四",
.sex="女",
.id="2025002"
};
注意:结构体变量.成员名,结构体指针->成员名。
四、结构体内存对齐(重点)
1.什么是结构体内存对齐?
- 定义:结构体在内存中存储时,成员并不是紧凑挨着存放,而是按照一定规则对齐到特定地址,编译器会自动插入填充字节,这种规则叫做内存对齐。
- offsetof 函数:会返回结构体成员相对于结构体起始地址的偏移量。
//在VS环境下,对齐数默认为8
#include<stdio.h>
#include<stddef.h>//offsetof需要包含的头文件
struct S
{
char c1;
int n;
char c2;
};
int main()
{
struct S s1;
printf("%zu\n",sizeof(struct S));12//计算结构体所占总字节数
printf("%zu\n",offsetof(struct S,c1));0//计算c1相较于起始地址的偏移量
printf("%zu\n",offsetof(struct S,n));4//计算n相较于起始地址的偏移量
printf("%zu\n",offsetof(struct S,c2));8//计算c2相较于起始地址的偏移量
return 0;
}
由代码结果可知,结构体在内存中的存储所占字节数,并不是我们所认为的sizeof(char)+sizeof(int)+sizeof(char)=6B,而是12B。同时,offsetof 显示int n的偏移量为4,char c2 的偏移量为8,说明编译器在c1和n之间,n和c2之间都插入了填充字节,佐证了结构体在内存中不是以连续字节的方式存储。这就是我们接下来要详细讲解的内存对齐对齐规则。
2.对齐规则!!!
- 结构体的第一个成员对齐到和结构体变量起始位置偏移量为0的地址处。
- 从第二个成员变量开始,都要对齐到某个对齐数的整数倍地址处。对齐数 = 编译器默认的一个对齐数 与 该成员变量大小的较小值。在VS中默认的值为8;Linux中gcc没有默认对齐数,对齐数为成员自身的大小。
- 结构体总大小为最大对齐数(结构体中每个成员变量都有一个对齐数,所有数中最大的)的整数倍,即最大对齐数 * n (0、1、2、3、4...)。
- 如果嵌套了结构体,则嵌套结构体对齐到自己成员中最大对齐数的整数倍处,结构体的整体大小就是所有成员的最大对齐数(含嵌套结构体成员的对齐数)的整数倍。
例1:基本结构体1
struct S1
{
char c1; 对齐数=min(1,8)=1
char c2; 对齐数=min(1,8)=1
int n; 对齐数=min(4,8)=4
};
- 成员c1在偏移量0处开始,占1字节,结束于偏移1;成员c2从偏移量为1处开始,占1字节,结束于偏移2;成员n对齐数为4,需要从4的倍数地址开始,因此从偏移4开始,占4字节,结束于偏移8。
- 结构体的大小必须为最大对齐数的整数倍。当前所有成员结束于偏移8,而8是最大对齐数4的倍数,所以结构体大小为8。

例2:基本结构体2
struct S2
{
char c1; 对齐数=min(1,8)=1
int n; 对齐数=min(4,8)=4
char c2; 对齐数=min(1,8)=1
};
- 成员c1在偏移量0处开始,占1字节,结束于偏移1;成员n对齐数为4,需要从4的倍数地址开始,因此从偏移4开始,占4字节,结束于偏移8;成员c2对齐数为1,因此从偏移8开始,结束于偏移9。
- 结构体的大小必须为最大对齐数的整数倍。当前所有成员结束于偏移9,而9不是最大对齐数4的倍数,所以结构体大小为12。

通过分析例1和例2,struct S1{char,char,int;}大小为8B,struct S2{char int, char;}大小为12B,可以得出,把小成员集中放在一起,能有效减少填充字节,节省内存空间。
例3:基本结构体3
struct S3
{
double d; 对齐数=min(8,8)=8
char c; 对齐数=min(1,8)=1
int n; 对齐数=min(4,8)=4
};
- 成员d在偏移量0处开始,占8字节,结束于偏移8;成员c对齐数为1,需要从1的倍数地址开始,因此从偏移8开始,占1字节,结束于偏移9;成员n对齐数为4,需要从4的倍数地址开始,因此从偏移12开始,结束于偏移16。
- 结构体的大小必须为最大对齐数的整数倍。当前所有成员结束于偏移16,而16是最大对齐数8的倍数,所以结构体大小为16。

例4:基本结构体4(含嵌套体)
struct S4
{
char c1; 对齐数=min(1,8)=1
struct S3 s; 对齐数=min(结构体成员中的最大对齐数,8)=min(8,8)=8
double d; 对齐数=min(8,8)=8
};
- 成员c1在偏移量0处开始,占1字节,结束于偏移1;成员s对齐数为8,需要从8的倍数地址开始,因此从偏移8开始,占16字节,结束于偏移24;成员d对齐数为8,需要从8的倍数地址开始,因此从偏移开始24,结束于偏移32。
- 结构体的大小必须为最大对齐数的整数倍。当前所有成员结束于偏移32,而32是最大对齐数8的倍数,所以结构体大小为32。

3.为什么存在内存对齐?
- 平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
- 性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要1次访问。假设一个处理器总是从内存中取8字节,则地址必须是8的倍数。如果我们保证将所有的double类型的数据的地址都对齐成8的倍数,那么就可以用一个内存操作来读或者写值了。否则,我们可能需要执行两次内存访问,因为对象可能被分放在两个8字节内存块中。
总体来说:结构体的内存是拿空间来换取时间的做法。
4.修改默认对齐数
在实际开发中,有时候我们需要手动控制结构体的对齐规则,比如为了极致节省内存,或者为了和某些硬件 / 网络协议的格式匹配。这时候就可以使用 C 语言提供的预处理指令 #pragma pack() 来修改编译器的默认对齐数。
#pragma pack(n):设置当前文件默认对齐数为n,n必须是2的幂(1、2、4、8.....).
#pragma pack(1)//设置为1字节对齐
struct S1
{
char c1; 对齐数=min(1,1)=1
char n; 对齐数=min(1,1)=1
char c2; 对齐数=min(4,1)=1
};
#pragma pack()//恢复默认
struct S2
{
char c1; 对齐数=min(1,8)=1
char c2; 对齐数=min(1,8)=1
int n; 对齐数=min(4,8)=4
};


- struct S1:将默认对齐数改为1。成员c1在偏移量0处开始,占1字节,结束于偏移1;成员c2从偏移量为1处开始,占1字节,结束于偏移2;成员n对齐数为1,需要从1的倍数地址开始,因此从偏移2开始,占4字节,结束于偏移6。结构体的大小必须为最大对齐数的整数倍。当前所有成员结束于偏移6,而6是最大对齐数1的倍数,所以结构体大小为6。
- struct S2:成员c1在偏移量0处开始,占1字节,结束于偏移1;成员c2从偏移量为1处开始,占1字节,结束于偏移2;成员n对齐数为4,需要从4的倍数地址开始,因此从偏移4开始,占4字节,结束于偏移8。结构体的大小必须为最大对齐数的整数倍。当前所有成员结束于偏移8,而8是最大对齐数4的倍数,所以结构体大小为8。
五、结构体传参
strcr S
{
int data[1000];
int num;
};
struct S s={{1,2,3,4},1000};//创建变量s
//结构体传参
void printf1(struct S t)
{
printf("%d\n",t,num);
}
//结构体地址传参
void printf2(struct S*ps)
{
printf("%d\n",ps->num);
}
int main()
{
printf1(s);//传值调用
printf2(&s);//传址调用
return 0;
}
- 结构体传值:函数参数是结构体变量,调用时会完整拷贝整个结构体的数据,在栈上开辟副本。当结构体包含大数组时,拷贝开销极大,严重影响性能。
- 结构体传址:函数参数是结构体指针,调用时只传递一个 4/8 字节的地址值,函数通过指针直接访问原结构体的数据,无需拷贝,效率极高。
总结:结构体传参,优先传地址,不仅性能更好,还能在函数内修改原结构体的数据,比传值更灵活。
六、结构体实现位段
1.什么是位段?
普通结构体的成员必须以char/int等完整类型为单位分配内存,比如一个
int占 4 字节(32 位),哪怕你只需要存 0-15 的数字,也得用掉 32 位。而位段允许你指定成员占用的 bit 数,比如用 4 位就能存 0-15,剩下的 bit 可以给其他成员共用,从而大幅减少内存占用。
- 位段的成员必须是int、unsigned int 或signed int ,在C99中位段成员的类型也可以选择其他整型家族,比如:char。
- 位段成员名后边有一个冒号和一个数字。
struct 结构体名
{
整型类型 成员名:占用位数(b);
....
};
例:
struct A
{
int _a:2; 占2 bit
int _b:5; 占5 bit
int _c:10; 占10 bit
int _d:30; 占30 bit
};
2.位段的内存分配
- 位段的成员可以是int、unsigneg int、signed int或者是char等类型。
- 位段的空间上是按照需要以4个字节(int)者1个字节(char)的方式来开辟的。
- 位段涉及很多不确定因素,位段是不跨平台的,注重可移植性的程序应该避免使用位段。
//创建结构体实现位段
struct A
{
int a:2;
int b:5;
int c:10;
int d:30;
};
//创建相同类型结构体
struct B
{
int a;
int b;
int c;
int d;
};
int main()
{
printf("%zu\n",sizeof(struct A));8//计算位段的大小
printf("%zu\n",sizeof(struct B));16//计算结构体的大小
return 0;
}
- struct A 中所占总位数为2+5+10+30=47=6B,但结果显示为8B。这是因为位段以 int(4 字节= 32bit)为单元分配内存:前 17bit(a+b+c)放在第一个单元,d:30 无法放入剩余的 15bit,因此开辟了第二个单元,总共占 8 字节。
- struct B 是普通结构体,每个
int占 4 字节,4 个int共占 16 字节。
例:结构体位段在内存中的存储
struct S
{
char a:3;
char b:4;
char c:5;
char d:4;
};
int main()
{
struct S s={0};//将结构体初始化为0
S.a=10;//给成员a赋值10
S.b=12;//给成员b赋值12
S.c=3;//给成员c赋值3
S.d=4;//给成员d赋值4
return 0;
}

一个字节(整型)的内存中,到底是从左向右使用,还是从右向左使用是不确定的。这里假设从右向左使用。
剩余空间不能满足下一个成员的时候,是否浪费,不确定。这里假设浪费。
- 第 1 个字节:
a(3bit) + b(4bit)占 7bit,剩余 1bit 无法放下c(5bit)。 - 第 2 个字节:
c(5bit)占 5bit,剩余 3bit 无法放下d(4bit)。 - 第 3 个字节:
d(4bit)占 4bit,剩余 4bit 浪费。
因此结构体实际占 3 字节。这也再次验证了:位段的大小不是简单的 bit 数相加,而是按基础类型为单元分配的。
3.位段的跨平台问题
- int 位段被当成有符号数还是无符号数是不确定的。
- 位段中最大为的数目是不能确定的(16位机器最大16,32位机器最大32,写成27,在16位机器中会出现问题)。
- 位段中的成员在内存中从左向右分配,还是从右向左分配,标准尚未定义。
- 当一个结构体包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余位时,是舍弃还是利用,尚未确定。
总结:跟结构体相比,位段可以达到同样的效果,并且可以很好的节省空间,但是有跨平台的问题存在。
4.位段使用注意事项
位段的多个成员可能共享同一个字节,因此它们的起始位置不一定在字节的边界上。内存地址是按字节分配的,单个 bit 位没有独立的内存地址,这就带来了以下限制:
- 不能对位段成员使用
&取地址运算符,也无法直接用scanf给位段成员输入值,只能先将数据读入普通变量,再赋值给位段成员。 - 不能定义位段数组,也不能用指针指向位段成员。
- 不能用
sizeof直接获取单个位段成员的大小。
struct S
{
char a:3;//占1个字节里的低3位
char b:4;//占1个字节里的低4位
};
struct S s;
&s.a;❌位段成员不能取地址
scanf("%d",&s.a);❌
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)