Appearance
结构体、位段、联合与枚举
⏱️ 本讲 L1 正课约 80 分钟,建议拆成两段学习:结构体与位段一段,联合与枚举一段。折叠的 L2 内容可先跳过。
🎯 学习目标
学完本讲,你应当能够:
- 独立声明结构体类型,用顺序方式和指定成员方式初始化,并能用
.和->访问成员。 - 按内存对齐四条规则手算任意结构体(含嵌套结构体)的大小,再写程序验证。
- 使用
#pragma pack改变对齐数,并说出它对结构体大小的影响。 - 说清位段如何以 bit 为单位省空间,以及它的四个跨平台风险。
- 定义联合体、计算联合体大小,并用联合体写程序判断机器的大小端。
- 定义枚举类型,说出枚举相对
#define的五个优点。
🔗 先修知识
自检三问(答不上来先复习):
- 指针的
->和普通变量的.分别什么时候用? int *p做p + 1时,地址实际增加多少?qsort的比较函数参数为什么是const void *?
一、结构体类型
1.1 结构体的声明
前面的课程里我们已经接触过结构体。结构体是若干个值的集合,这些值称为成员变量,每个成员可以是不同类型。
声明的一般形式:
🧩 语法模板(示意片段) · 仅展示声明形式
c
struct 标签名
{
成员列表;
} 变量列表; // 变量列表可写可不写例如描述一名学生:
🧩 类型定义片段 · 之后可直接用 struct Stu 声明变量
c
struct Stu
{
char name[20]; // 名字
int age; // 年龄
char sex[5]; // 性别
char id[20]; // 学号
}; // 这个分号绝对不能丢!struct Stu 合起来才是一个完整的类型名,和 int、double 地位相同。
1.2 结构体变量的创建与初始化
📄 01_struct_init.c · ✅ 完整程序(可直接复制编译)
c
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
struct Stu
{
char name[20];
int age;
char sex[5];
char id[20];
};
int main(void)
{
/* 按成员顺序初始化 */
struct Stu s = { "张三", 20, "男", "20230818001" };
printf("name: %s\n", s.name);
printf("age : %d\n", s.age);
printf("sex : %s\n", s.sex);
printf("id : %s\n", s.id);
/* 按指定成员初始化(C99 起的指定初始化器) */
struct Stu s2 = { .age = 18, .name = "李四", .id = "20230818002", .sex = "女" };
printf("name: %s, age: %d\n", s2.name, s2.age);
return 0;
}未显式初始化的成员会被自动置零(数值成员为 0,字符数组成员为 '\0'),所以 struct Stu s3 = {0}; 是常用的整体清零写法。
1.3 匿名结构体
声明时可以省略标签:
🧩 类型定义片段 · 匿名类型只能在声明处使用
c
struct
{
int a;
char b;
float c;
} x; // 只能在声明的同时定义变量
struct
{
int a;
char b;
float c;
} arr[20], *p; // 也可以定义数组和指针这种类型没有名字,不配合 typedef 基本只能用一次。更关键的坑在下面:
🧩 待改错片段 · 接在上面两段匿名声明之后
c
p = &x; // 合法吗?编译器会把上面两个匿名声明视为两个完全不同的类型,即使成员一模一样。gcc 报 incompatible pointer types,MSVC 也会报错。需要复用的类型,老老实实起标签或用 typedef。
1.4 结构体的自引用
链表的节点需要"指向另一个同类节点",这叫自引用。先看错误写法:
🧩 待改错片段 · 包含完整自身导致类型递归
c
struct Node
{
int data;
struct Node next; // 错!结构体里放一个完整的自己,大小无穷递归
};一个结构体里包含一个同类型的完整结构体,编译器永远算不完它的大小。正确做法是放一个指向自己的指针:
🧩 类型定义片段 · 指针大小固定,不要求知道完整布局
c
struct Node
{
int data;
struct Node *next; // 对!指针大小固定,不要求知道完整布局
};如果还想用 typedef 缩短类型名,下面这种"匿名 + 提前用新名字"的写法也不行:
🧩 待改错片段 · Node 尚未诞生就被使用
c
typedef struct
{
int data;
Node *next; // 错!此时 Node 这个名字还没诞生
} Node;推荐写法:保留 struct 标签,让成员能引用到它。
🧩 类型定义片段 · 之后 Node 与 struct Node 等价
c
typedef struct Node
{
int data;
struct Node *next;
} Node;之后既能写 struct Node,也能直接写 Node。
1.5 结构体内存对齐(热门考点)
结构体的大小不等于成员大小简单相加。先记住四条对齐规则:
- 第 1 个成员对齐到结构体起始位置(偏移量 0)。
- 从第 2 个成员起,对齐到"对齐数"的整数倍偏移处;对齐数 = 编译器默认对齐数与该成员自身大小中的较小值。
- Visual Studio 的默认对齐数为 8。
- Linux/Windows 上的 gcc 对基本类型按 ABI 自然对齐,效果上对齐数就是成员自身大小。
- 结构体总大小必须是最大对齐数的整数倍,不够就补尾部填充。
- 嵌套结构体时,内层结构体成员要对齐到它自己最大对齐数的整数倍;外层总大小对所有对齐数(含内层)的最大值取整。
下面用四个结构体一次性验证:
📄 02_align_rules.c · ✅ 完整程序(可直接复制编译)
c
#include <stdio.h>
struct S1
{
char c1; /* 偏移 0 */
int i; /* 对齐 4:偏移 4 */
char c2; /* 偏移 8;有效内容到 9,补到 12 */
};
struct S2
{
char c1; /* 偏移 0 */
char c2; /* 偏移 1 */
int i; /* 对齐 4:偏移 4;结束于 8 */
};
struct S3
{
double d; /* 偏移 0,占 8 */
char c; /* 偏移 8 */
int i; /* 对齐 4:跳过 3 字节填充,偏移 12;结束于 16 */
};
struct S4
{
char c1; /* 偏移 0 */
struct S3 s3; /* S3 内含 double,对齐 8:偏移 8,占 16 */
double d; /* 偏移 24,占 8;结束于 32 */
};
int main(void)
{
printf("S1 = %zu\n", sizeof(struct S1));
printf("S2 = %zu\n", sizeof(struct S2));
printf("S3 = %zu\n", sizeof(struct S3));
printf("S4 = %zu\n", sizeof(struct S4));
return 0;
}输出:
text
S1 = 12
S2 = 8
S3 = 16
S4 = 32S4 特别注意:嵌套的 S3 不是放在偏移 1,而是为了满足内部 double 的 8 字节对齐,整体挪到偏移 8。
📜 标准卡(L2 选读):对齐是谁规定的?
- C11 §6.2.8:定义了对象的对齐(alignment),对齐是实现定义的整数值,表示连续对象可放置位置的约束。
- C11 §6.7.2.1:规定结构体/联合体成员按声明顺序分配、非位段成员对齐由实现按适当边界安排。
- gcc(x86-64 ABI):基本类型自然对齐,
double/long long为 8,结构体内按上述规则填充。 - MSVC:默认最大对齐数
/Zp8,即默认对齐数 8,可用#pragma pack或/Zp修改。
1.6 为什么要有内存对齐
- 移植性:不是所有硬件都能访问任意地址上的任意数据,个别平台对未对齐访问直接抛硬件异常。
- 性能:CPU 按自然边界取数据。假设一次取 8 字节,对齐良好的
double一次就读完;未对齐时可能横跨两个 8 字节块,要读两次再拼接。
一句话:对齐是拿空间换时间。
既要对齐又想省空间的技巧:让小成员尽量集中。S1 和 S2 成员完全相同,只是顺序不同,S1 = 12、S2 = 8。设计结构体时,习惯上把大对齐数的成员(double、指针)放前面或集中排列。
1.7 修改默认对齐数:#pragma pack
📄 03_pragma_pack.c · ✅ 完整程序(可直接复制编译)
c
#include <stdio.h>
#pragma pack(1) /* 把默认对齐数改成 1 */
struct S
{
char c1; /* 偏移 0 */
int i; /* pack(1) 下对齐数 = min(4,1) = 1:偏移 1 */
char c2; /* 偏移 5;总大小 6 */
};
#pragma pack() /* 恢复默认对齐,影响范围到此为止 */
int main(void)
{
printf("%zu\n", sizeof(struct S));
return 0;
}输出 6——填充全部消失,结构体压到最紧,代价是访问 i 可能变慢,个别平台甚至出问题,所以只在确实需要(如文件格式、网络协议)时局部使用。
#pragma 本身是标准保留的预处理指令(C11 §6.10.6),但具体 pragma 的行为是实现定义的;pack 是 MSVC 首创、gcc/Clang 兼容的通用扩展。🟡
1.8 结构体传参
📄 04_struct_param.c · ✅ 完整程序(可直接复制编译)
c
#include <stdio.h>
struct S
{
int data[1000];
int num;
};
/* 方式 1:传值,整个结构体压栈拷贝一份 */
void print1(struct S s)
{
printf("%d\n", s.num);
}
/* 方式 2:传址,只压一个 8 字节指针 */
void print2(const struct S *ps)
{
printf("%d\n", ps->num);
}
int main(void)
{
struct S s = { {1, 2, 3, 4}, 1000 };
print1(s);
print2(&s);
return 0;
}两种都打印 1000,但 print1 要在栈上复制 4004 字节,print2 只传 8 字节。结论:结构体传参优先传地址;函数内不修改它就加 const。
二、位段
2.1 什么是位段
位段的声明和结构体几乎一样,区别有两个:成员必须是整型家族;成员名后用冒号加一个位数:
📄 05_bitfield_int.c · ✅ 完整程序(可直接复制编译)
c
#include <stdio.h>
struct A
{
int _a : 2; /* 2 个 bit */
int _b : 5; /* 5 个 bit */
int _c : 10; /* 10 个 bit */
int _d : 30; /* 30 个 bit */
};
int main(void)
{
printf("%zu\n", sizeof(struct A));
return 0;
}输出 8。2+5+10+30 = 47 bit,第一个 4 字节装不下,开第二个 4 字节。位段空间以 4 字节(int 类)或 1 字节(char 类)为单位开辟。
2.2 char 位段的实测布局
📄 06_bitfield_char.c · ✅ 完整程序(可直接复制编译)
c
#include <stdio.h>
struct S
{
char a : 3;
char b : 4;
char c : 5;
char d : 4;
};
int main(void)
{
struct S s = {0};
s.a = 2; /* 3 bit 可放 */
s.b = 12; /* 4 bit:位模式 1100 */
s.c = 3;
s.d = 4;
printf("sizeof = %zu\n", sizeof s);
unsigned char *p = (unsigned char *)&s;
for (size_t i = 0; i < sizeof s; i++)
printf("%02x ", p[i]);
printf("\n");
return 0;
}输出:
text
sizeof = 3
62 03 04在 gcc 上(低位向高位分配):第 1 字节里 a 占低 3 位、b 占接下来 4 位,拼成 62;c 的 5 位放不下剩余 1 位,开第 2 字节存为 03;d 的 4 位放不下剩余 3 位,开第 3 字节存为 04。如果给 3 位字段写 10,会被截断成 2(gcc 有 -Woverflow 警告)——位段装不下就溢出,没有任何提示式的保护。
2.3 位段的跨平台问题
- 普通
int位段被当成有符号还是无符号,实现定义。🟡 - 单个位段允许的最大位数随机器字长变化(16 位机上写 27 就失败)。
- 位在字节内从左还是从右分配,标准未规定。🟠
- 剩余位装不下下一个成员时,舍弃还是跨字节利用,也未规定。🟠
所以位段省空间但不跨平台;同一道位段题在 gcc 和 MSVC 上布局可能不同,注重移植的软件应避免。
2.4 位段不能取地址
同一个字节里挤着好几个成员,bit 没有独立地址,因此位段成员不能用 &,也不能直接 scanf:
📄 07_bitfield_input.c · ✅ 完整程序(可直接复制编译)
c
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
struct A
{
int _a : 2;
int _b : 5;
};
int main(void)
{
struct A sa = {0};
/* scanf("%d", &sa._b); */ /* 错误:cannot take address of bit-field */
int b = 0; /* 先读到普通变量 */
scanf("%d", &b);
sa._b = b; /* 再赋值给位段成员 */
printf("_b = %d\n", sa._b);
return 0;
}位段的典型应用是网络协议头:IP 数据报里很多属性只需几个 bit(版本号 4 bit、首部长度 4 bit 等),用位段能把包头压到最小,线上传输的字节更少。
三、联合体(共用体)
3.1 联合体的声明
联合体用 union 声明,语法和结构体一样,但所有成员共用同一块内存,同一时刻只有一个成员的值有效:
📄 08_union_basic.c · ✅ 完整程序(可直接复制编译)
c
#include <stdio.h>
union Un
{
char c;
int i;
};
int main(void)
{
union Un un = {0};
printf("%zu\n", sizeof un);
return 0;
}输出 4。编译器只保证能装下最大的成员(这里是 int),而不是 1+4 = 5。
3.2 联合体成员互相重叠
📄 09_union_overlap.c · ✅ 完整程序(可直接复制编译)
c
#include <stdio.h>
union Un
{
char c;
int i;
};
int main(void)
{
union Un un = {0};
printf("%p\n", (void *)&un.i);
printf("%p\n", (void *)&un.c);
printf("%p\n", (void *)&un);
un.i = 0x11223344;
un.c = 0x55; /* 只改第 1 个字节 */
printf("%x\n", un.i);
return 0;
}三个地址完全相同;最后输出 11223355(小端机器上 c 与 i 的最低字节重叠,44 被改成 55)。所以联合体能用,但写入一个成员后,其他成员的值就不再可信。
作为对比:同样两个成员写成结构体是 8 字节(char 后填充到 int),写成联合体只有 4 字节。
3.3 联合体大小的计算
规则两条:至少等于最大成员的大小;总大小还要对齐到最大对齐数的整数倍。
📄 10_union_size.c · ✅ 完整程序(可直接复制编译)
c
#include <stdio.h>
union Un1
{
char c[5]; /* 5 字节,对齐数 1 */
int i; /* 对齐数 4 */
};
union Un2
{
short c[7]; /* 14 字节,对齐数 2 */
int i; /* 对齐数 4 */
};
int main(void)
{
printf("%zu\n", sizeof(union Un1));
printf("%zu\n", sizeof(union Un2));
return 0;
}输出 8 和 16:Un1 最大内容 5,向最大对齐数 4 取整到 8;Un2 最大内容 14,向 4 取整到 16。
3.4 联合体的应用:礼品兑换单
📘 提高(L2):用联合体给互斥数据省空间
三种礼品(图书、杯子、衬衫)有公共属性(库存、价格、类型),但特殊属性互斥:图书要书名/作者/页数,杯子要设计,衬衫要设计/颜色/尺寸。若把全部属性平铺进一个结构体,每条记录都带着一堆用不上的字段;把特殊属性放进联合体,同一时刻只占一块:
📄 11_gift_list.c · ✅ 完整程序(可直接复制编译)
c
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#define BOOK 1
#define MUG 2
#define SHIRT 3
struct gift_list
{
int stock_number; /* 库存量 */
double price; /* 定价 */
int item_type; /* 商品类型 */
union {
struct {
char title[20];
char author[20];
int num_pages;
} book;
struct {
char design[30];
} mug;
struct {
char design[30];
int colors;
int sizes;
} shirt;
} item;
};
int main(void)
{
struct gift_list g = {0};
g.stock_number = 100;
g.price = 59.9;
g.item_type = BOOK;
snprintf(g.item.book.title, sizeof g.item.book.title, "C 语言入门");
snprintf(g.item.book.author, sizeof g.item.book.author, "张三");
g.item.book.num_pages = 320;
printf("库存 %d,价格 %.1f,类型 %d\n", g.stock_number, g.price, g.item_type);
if (g.item_type == BOOK)
printf("《%s》 作者:%s,%d 页\n",
g.item.book.title, g.item.book.author, g.item.book.num_pages);
return 0;
}访问联合成员前先看 item_type,确保用的是当初写入的那个成员——这是使用联合体数据结构的通用纪律。
3.5 联合体练习:判断大小端
📄 12_check_endian.c · ✅ 完整程序(可直接复制编译)
c
#include <stdio.h>
int check_sys(void)
{
union
{
int i;
char c;
} un;
un.i = 1;
return un.c; /* 小端返回 1,大端返回 0 */
}
int main(void)
{
if (1 == check_sys())
printf("小端\n");
else
printf("大端\n");
return 0;
}原理:1 在小端机器上最低字节是 01、存在最低地址;c 与 i 同起始地址,读到 01 就是小端。常见的 x86/ x64、ARM 手机(默认模式)都是小端。
四、枚举
4.1 枚举类型的声明
枚举就是把可能的取值一一列举出来:
🧩 类型定义片段 · 花括号内为枚举常量
c
enum Day /* 星期 */
{
Mon, Tues, Wed, Thur, Fri, Sat, Sun
};
enum Sex /* 性别 */
{
MALE, FEMALE, SECRET
};
enum Color
{
RED = 2, /* 可以显式赋值 */
GREEN = 4,
BLUE = 8
};花括号里的名字叫枚举常量。默认从 0 开始依次 +1;中途赋值的,后续成员在该值基础上继续 +1(例如 RED=2 后若不写 GREEN,GREEN 就是 3)。
4.2 枚举的优点
既然 #define 也能定义常量,为什么还要枚举?
- 成组的相关常量放在一起,可读性、可维护性好。
- 枚举是真正的类型,编译器能做类型检查,比散落的
#define严谨(C++ 中尤其明显)。 - 便于调试:
#define在预处理阶段就被替换掉,调试器里看不到名字;枚举常量保留到调试信息。 - 一次声明一组常量,使用方便。
- 遵循作用域规则:枚举声明在函数内就只在函数内可见。
4.3 枚举的使用
📄 13_enum_use.c · ✅ 完整程序(可直接复制编译)
c
#include <stdio.h>
enum Color
{
RED = 1,
GREEN = 2,
BLUE = 4
};
int main(void)
{
enum Color clr = GREEN;
printf("%d\n", clr);
clr = (enum Color)2; /* C 语言允许整数赋给枚举变量;加强制转换表意更清楚 */
printf("%d\n", clr);
return 0;
}输出两行 2。C 中整数与枚举互通(不建议滥用),C++ 则不允许直接赋整数。底层实现上枚举通常就是一个 int。🟡
📜 标准卡(L2 选读):枚举常量的取值
- C11 §6.7.2.2:枚举常量是
int类型;未显式赋值的常量取值为上一个枚举常量加 1,首个未赋值常量为 0;枚举列表中的值允许重复。 - 枚举变量的具体整型由实现选择(C11 §6.7.2.2p4),只要装得下所有取值即可。
⚠️ 常见坑与报错表
| 坑 | 现象 | 对策 |
|---|---|---|
| 结构体声明结尾忘分号 | error: expected ';' ... | }; 是类型声明的一部分,不能省 |
| 给两个匿名结构体互赋值 | gcc:incompatible pointer types | 需要复用就起标签或 typedef |
| 结构体里放完整的自己 | 递归类型,编译失败 | 放"指向自己的指针" |
| typedef 匿名体里提前用新名 | unknown type name 'Node' | 先保留 struct 标签再 typedef |
| 成员大小直接相加算结构体 | 与 sizeof 结果不符 | 按对齐规则算偏移,注意填充 |
| 对 bit-field 取地址 | cannot take address of bit-field | 先存入普通变量再赋值 |
| 给位段塞超范围的值 | 静默截断(可能有 -Woverflow) | 确认值在位数表示范围内 |
| 联合体写一个成员后读另一个 | 值对不上(字节重叠) | 用类型标记记住当前有效成员 |
| 以为成员相同大小就相同 | S1=12、S2=8 | 小成员集中排列可省空间 |
🛠️ 动手练习
📊 本讲网页练习进度0 / 8(0%)
进度自动保存在本浏览器;编程题不计数,请在编辑器中完成。
第 1 题 · 知识点:对齐规则 · 难度:⭐
两个结构体成员完全相同:
c
struct T1 { char c; int i; char d; };
struct T2 { char c; char d; int i; };第 2 题 · 知识点:含 double 的对齐 · 难度:⭐
c
struct S3 { double d; char c; int i; };S3 的大小是多少?(只填数字)
空①
第 3 题 · 知识点:嵌套结构体 · 难度:⭐⭐
c
struct S3 { double d; char c; int i; };
struct S4 { char c1; struct S3 s3; double d; };S4 的大小是多少?(只填数字)
空①
第 4 题 · 知识点:#pragma pack · 难度:⭐
c
#pragma pack(1)
struct P { char c1; int i; char c2; };
#pragma pack()第 5 题 · 知识点:位段特性 · 难度:⭐⭐
关于位段,下列说法正确的有哪些?
第 6 题 · 知识点:联合体大小 · 难度:⭐⭐
c
union Un2 { short c[7]; int i; };Un2 的大小是多少?(只填数字)
空①
第 7 题 · 知识点:枚举取值 · 难度:⭐
c
enum E { A, B, C = 5, D };第 8 题 · 知识点:结构体 + qsort 综合 · 难度:⭐⭐⭐
写一个程序:让用户输入若干名学生的姓名、年龄、成绩,存入结构体数组,用 qsort 按成绩降序排序后打印。
🛠 动手编程题 · 请在 VS2026(或你的编辑器)中完成
请独立完成上述程序,再对照参考答案。
✅ 过关标准:输入 3 名学生成绩 85、92、78,打印顺序必须是 92、85、78;排序必须由 qsort 完成。
💡 思路
- 定义
struct Student,含name、age、score。 - 比较函数把两个
const void *转回const struct Student *。 - 降序:p2 分数高时返回正数。
🧭 步骤
- 先读人数 n,再循环读 n 条记录。
- 调用
qsort(students, n, sizeof(struct Student), cmp)。 - 遍历打印排序结果。
🔑 参考答案与解释
c
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#define MAX 10
struct Student
{
char name[30];
int age;
float score;
};
int cmp_by_score_desc(const void *p1, const void *p2)
{
const struct Student *s1 = (const struct Student *)p1;
const struct Student *s2 = (const struct Student *)p2;
if (s2->score > s1->score) return 1;
if (s2->score < s1->score) return -1;
return 0;
}
int main(void)
{
struct Student students[MAX];
int n;
printf("请输入学生人数(最多 %d):", MAX);
scanf("%d", &n);
for (int i = 0; i < n; i++)
{
printf("学生 %d(姓名 年龄 成绩):", i + 1);
scanf("%s %d %f", students[i].name, &students[i].age, &students[i].score);
}
qsort(students, n, sizeof(struct Student), cmp_by_score_desc);
printf("\n按成绩降序:\n");
for (int i = 0; i < n; i++)
printf("%-10s %d岁 %.1f分\n",
students[i].name, students[i].age, students[i].score);
return 0;
}比较函数返回正数时 qsort 交换两者顺序,所以让"分数高的排前面"就在 s2 更大时返回 1。
网页内无法练习写代码,亲手敲、亲手编译才能真正学会。下方按顺序展开 思路 → 步骤 → 答案。
📝 小结与自测
本讲你掌握了三种自定义类型:
- 结构体:成员各有独立存储;大小按对齐规则计算(默认对齐数、取整、嵌套对齐);传参优先传地址。
- 位段:以 bit 计的结构体成员,省空间但有四类跨平台不确定性,且不能取地址。
- 联合体:成员共用一块内存,大小取最大成员并按最大对齐数取整;典型用途是互斥数据与判断大小端。
- 枚举:成组的整型常量,默认从 0 递增,比
#define可读、可调试、有类型检查。
自测三问:
- 为什么
struct { double d; char c; }和struct { char c; double d; }大小可能不同? - 联合体为什么不能同时当两个成员都有有效值来用?
- 工程代码里表示"错误码/状态码",为什么推荐枚举而不是一堆
#define?
📖 点击查看答案
- 前者:d 在 0–7、c 在 8,补到 16;后者:c 在 0,d 为 8 字节对齐被挪到偏移 8,总大小也是 16——本例恰好相同;若换成 char+int+char 与 char+char+int,区别就是 12 与 8。关键在于成员顺序决定填充多少。
- 所有成员从同一偏移开始,写一个成员会覆盖其他成员的字节,读出来的值不再有保证;必须用一个额外标记(如 item_type)记录当前有效成员。
- 枚举把相关常量成组声明、有作用域、有类型、调试器可见;
#define不受作用域约束、预处理后消失、容易散落冲突。
🔬 选学拓展(L3)
WARNING
以下内容不影响本讲学习目标,供学有余力或准备笔试的读者使用。位段与对齐的具体结果在不同编译器上可能不同,下面数值均为 gcc 16(64 位 Windows)实测。
拓展 1:用 offsetof 查看每个成员的真实偏移
offsetof(类型, 成员) 是 <stddef.h> 提供的标准宏(C11 §7.19),直接给出成员相对结构体起点的字节偏移,比手工推演更可靠:
📄 L3_offsetof.c · ✅ 完整程序(可直接复制编译)
c
#include <stdio.h>
#include <stddef.h>
struct S3
{
double d;
char c;
int i;
};
struct S4
{
char c1;
struct S3 s3;
double d;
};
int main(void)
{
printf("S3: d=%zu c=%zu i=%zu, size=%zu\n",
offsetof(struct S3, d), offsetof(struct S3, c),
offsetof(struct S3, i), sizeof(struct S3));
printf("S4: c1=%zu s3=%zu d=%zu, size=%zu\n",
offsetof(struct S4, c1), offsetof(struct S4, s3),
offsetof(struct S4, d), sizeof(struct S4));
return 0;
}实测输出:
text
S3: d=0 c=8 i=12, size=16
S4: c1=0 s3=8 d=24, size=32以后遇到复杂结构体,与其空想,不如写 5 行 offsetof 让编译器告诉你答案。
拓展 2:gcc 的紧凑属性
gcc/Clang 还支持用属性取消填充:
🧩 示意片段(编译器扩展,非标准) · 仅 gcc/Clang 支持
c
struct __attribute__((packed)) X
{
char c;
int i;
};packed 强制成员紧挨排列,sizeof(struct X) = 5。它是编译器扩展(非标准),且未对齐访问在部分平台上会变慢甚至出错,仅用于硬件寄存器映射、协议报文等场景。MSVC 上对应的做法是 #pragma pack(1)。
拓展 3:位段有符号性的小实验
🧩 示意片段 · 对比同一位数下有符号/无符号的解释
c
struct B { int x : 3; };
struct C { unsigned int x : 3; };同样写入 x = 6:有符号 3 位字段(范围 −4~3)会把 6 解释成 −2,无符号字段仍是 6。而"int 位段到底有没有符号"标准交给实现决定——gcc 在常见平台上按有符号处理。这也是协议头位段通常显式写 unsigned int 的原因。