写 C 语言的时候,char、int、float 这些都是基础类型。但现实世界中的信息往往是成套出现的:一个学生,有姓名、年龄、性别、学号;一本书,有书名、作者、价格。总不能单独定义一堆零散变量name1、age1、sex1……,写起来乱糟糟,还容易搞混。结构体(struct)就是专门用来把多种不同类型的数据打包到一起的自定义类型。
一、结构体怎么写:声明与初始化
先来看结构体的语法模板:
struct 结构体标签名 { 成员1; 成员2; 成员3; }; //⚠️最后的分号千万不能丢!举个例子,用结构体描述一名学生:
struct Stu { char name[20]; //名字 int age; //年龄 char sex[5]; //性别 char id[20]; //学号 };定义变量和初始化有两种方式:
- 按成员顺序初始化
struct Stu s = {"张三", 20, "男", "202601001"};- 指定成员初始化,顺序可以随便写(C99 支持)
struct Stu s2 = {.age=18, .name="李四", .sex="女", .id="202601002"};访问结构体成员时,普通变量用.点操作符;如果是结构体指针,则用->箭头操作符。
printf("%s\n", s.name); //结构体变量 . 成员 struct Stu* ps = &s; printf("%d\n", ps->age); //结构体指针 -> 成员匿名结构体:用完即走
声明时不写标签名,就是匿名结构体。
struct { int a; char b; float c; } x;⚠️坑:两个长得一模一样的匿名结构体,编译器会当成完全不同的两种类型,既不能互相赋值,指针也不能互相指向。匿名结构体一般只用一次,不适合复用。
结构体自引用:写链表必会!
很多同学写链表时会犯下面这个错误:
//错误写法! struct Node { int data; struct Node next; //结构体里面放一个完整自己,大小无限递归 };结构体内部不能放自身类型的完整变量,否则大小根本算不出来。✅正确做法是存放结构体指针,因为指针大小是固定的。
struct Node { int data; struct Node* next; };用 typedef 重命名时也要注意:不要用匿名结构体做自引用,此时 typedef 还没生效,结构体内部识别不到新名字。
//不行!Node还没定义出来 typedef struct { int data; Node* next; } Node; //标准正确写法 typedef struct Node { int data; struct Node* next; } Node;二、重点难点:结构体内存对齐(笔试高频)
问:下面两个结构体,成员完全一样,
sizeof大小却不一样?
struct S1 { char c1; int i; char c2; }; struct S2 { char c1; char c2; int i; };在 VS 下:sizeof(S1)=12,sizeof(S2)=8。成员一模一样,只是顺序不同,占用的内存大小就不一样,这就是内存对齐在起作用。
四条对齐规则(记牢做题无敌)
- 结构体第一个成员,放在偏移为 0 的地址。
- 后面每一个成员,都要对齐到对齐数的整数倍地址。
对齐数 =
编译器默认对齐数和该成员自身大小中较小的那一个。VS 默认对齐数是 8;Linux GCC 没有默认对齐数,对齐数就等于成员自身大小。 - 结构体总大小必须是结构体“最大对齐数”的整数倍,不够就补填充字节(空洞)。
- 如果结构体里面嵌套了另一个结构体,嵌套的结构体要对齐到它自身最大对齐数的整数倍;整体大小则是全部最大对齐数的整数倍。
演算 S1:char c1; int i; char c2;
- c1:char 占 1 字节,偏移 0。
- i:int 大小 4,对齐数 min(8,4)=4,要放在 4 的倍数地址上。0 地址已被占用,中间补 3 个空洞,i 放在偏移 4。
- c2:char 占 1 字节,放在偏移 8。此时成员占用到 9 字节。最大对齐数是 4,总大小必须是 4 的倍数,9 向上取整到 12,所以
sizeof(S1)=12。
S2:char c1; char c2; int i;
- c1 偏移 0,c2 偏移 1;
- i 对齐数 4,放到偏移 4;占用到 7 字节;向上对齐到 8,所以
sizeof(S2)=8。
💡小技巧:写结构体想省内存,就把占用字节小的成员尽量放在一起,减少填充空洞。
修改默认对齐数#pragma pack
我们也可以手动修改编译器的默认对齐数。
#pragma pack(1) //设置默认对齐数为1 struct S { char c1; int i; char c2; }; #pragma pack() //取消设置,恢复编译器默认pack(1) 的时候,对齐数全部为 1,不再有空洞,上面这个结构体的大小就是 6。
一般业务代码不建议随便改 pack,只有驱动、协议解析这类场景才会用到。
为什么要有内存对齐?(拿空间换时间)
- 硬件平台限制:有些 CPU 硬件不支持从任意地址读取 4 字节 int,地址不对齐会直接触发硬件报错。
- 访问性能提升:如果数据没对齐,CPU 需要分两次内存读取再拼接数据;对齐之后一次就能读完。
简单讲:牺牲一点点空闲的填充字节,换取读取速度和硬件兼容性。
三、结构体传参:尽量传地址,不要传整个结构体!
struct S { int arr[1000]; int num; }; //直接传整个结构体(不推荐) void print1(struct S s) { printf("%d", s.num); } //传结构体的地址(推荐) void print2(struct S* ps) { printf("%d", ps->num); }调用方式如下:
struct S s; print1(s); //把整个结构体拷贝一份压入栈,如果结构体很大,栈开销巨大 print2(&s); //仅仅压入一个8字节/4字节指针,开销极小📌结论:结构体传参优先传递指针。如果函数内部不想修改原结构体,可以写成
const struct S* ps,保护内容不被修改。
四、结构体延伸:位段(bit-field)
位段是结构体的特殊玩法,可以直接按 bit 位分配空间。冒号后面写数字,表示这个成员占用多少 bit。
struct A { int _a :2; int _b :5; int _c :10; int _d :30; };位段要点
- 位段成员必须是整型家族:
char、int、unsigned int。 - 空间开辟:按
int(4字节)或者char(1字节)为单位开辟内存。能塞就塞,塞不下就新开一块空间。 - ❗位段跨平台,不具备可移植性!
- 到底是从高位往低位放 bit,还是从低位往高位放,C 标准没有强制规定,VS 和 GCC 的行为并不一样。
- 剩余的 bit 是丢弃还是复用,标准同样未定义。
位段适合做网络协议、硬件寄存器解析;追求跨平台的业务代码尽量避开位段。
还有一个坑:bit 位没有内存地址,不能取地址 &
struct A sa; scanf("%d", &sa._b); //❌错误,不能对位段取地址正确做法是:先读到临时变量,再赋值给位段成员。
int temp; scanf("%d", &temp); sa._b = temp;📖本篇快速总结
- struct 结构体可以把不同类型的成员打包在一起,用
.访问成员,指针则用->。 - 结构体自引用只能放同类型指针,不能放完整的结构体变量。
- 内存对齐 4 条规则是计算 sizeof 结构体的核心;小成员放在一起可以节省内存。
- 结构体传参优先传指针,减少栈拷贝开销。
- 位段可以按 bit 使用内存、节省空间,但跨平台行为不确定,也不能用 & 取地址。