这一章在干嘛?
结构的声明、成员访问(. 与 ->)、自引用、存储对齐,结构传参的取舍,以及位段与联合两个「贴着硬件」的特性。
10.1 结构基础:声明、访问、自引用
typedef struct Node { /* typedef 起名,后面直接用 Node */
int value;
struct Node *next; /* 自引用:只能是指针(大小未知前不能整体嵌入) */
} Node;
Node n = { 10, NULL }; /* 初始化列表按成员顺序 */
Node *p = &n;
n.value /* 直接访问:结构.成员 */
p->value /* 间接访问:指针->成员,等价于 (*p).value */
两个不同结构类型的指针不能直接互赋(类型不同),尽管它们都是「地址」——编译器在做类型检查。不完整声明 struct B; 用于解决两个结构互相引用的死锁(A 里放 B*,B 里放 A*)。
10.2 存储分配与对齐
结构成员按声明顺序排列,但**中间可能塞「填充字节」**让每个成员落在其「对齐边界」上——CPU 访问对齐数据更快,某些平台不对齐直接崩:
struct Bad { char c; double d; char e; }; /* 排布:c(1)+填7+d(8)+e(1)+填7 = 24 */
struct Good { double d; char c; char e; }; /* d(8)+c(1)+e(1)+填6 = 16 */
两条实用法则:① 成员按大小降序排列能省空间;② 不要比较结构体本身(填充字节是随机值,== 不可用,memcmp 也不可靠),逐成员比较才对。
10.3 结构作函数参数:传值还是传指针?
| 方式 | 开销 | 能否修改原结构 | 适用 |
|---|---|---|---|
| void f(struct S s) | 拷贝整个结构(大的很贵) | 否 | 小型只读结构,想要值语义隔离 |
| void f(struct S *ps) | 一个指针 | 能(const 修饰可禁写) | 大结构、需要回写、链表操作——默认选它 |
void grow(struct Rect *r, int dw) { r->w += dw; } /* 回写用指针 */
int area(const struct Rect *r) { return r->w * r->h; } /* 只读加 const */
返回结构也是传值拷贝,小结构返回值很方便;大结构返回用「调用方传入输出指针」更高效。
10.4 位段与联合
位段(bit field):把成员指定到 bit 级,硬件寄存器/协议头打包利器:
struct Flags {
unsigned ready : 1; /* 1 bit */
unsigned mode : 3; /* 0~7 */
unsigned channel: 4; /* 0~15 */
}; /* 总共 1 字节出头(对齐后通常 1~2 字节) */
联合(union):所有成员共享同一起始地址,大小 = 最大成员。同一块内存的多种解读:
union Reg {
unsigned int u32; /* 整体读 */
unsigned char b[4]; /* 按字节读:看字节序 */
};
union Reg r;
r.u32 = 0x12345678;
printf("%02X\n", r.b[0]); /* 78 = 小端;12 = 大端 —— 探测字节序的经典写法 */
变体记录套路:
结构体里放「类型标签 + 联合」,标签决定这次联合按哪个成员解读——没有运行时类型信息时,靠它区分数据种类。
1. p->value 等价于什么?为什么需要两种访问符?
等价于 (*p).value。. 用于结构变量直接访问成员,-> 用于结构指针间接访问。因为 * 的优先级低于 .,(*p).value 必须加括号,-> 让间接访问更简洁且不易写错。
2. 为什么不能直接用 == 比较两个结构体?
运算符 == 不支持结构类型(编译报错)。而 memcmp 也不可靠:结构内的填充字节内容不确定,两个成员完全相同的结构可能因填充不同而判不等。正确做法是逐成员比较。
3. 联合 union 的核心特性与典型用途?
所有成员共享同一块内存(起始地址相同),大小等于最大成员,写一个成员会覆盖其他成员的解读。典型用途:多视角解读同一数据(如探测字节序、寄存器字/字节访问)、变体记录(配合类型标签节省内存)。