# 12 结构体 ## 12.1 完整概念讲解 结构体是 C 语言中将多个不同类型变量组合在一起的复合数据类型。结构体允许你创建自定义的数据结构,是面向对象编程中"类"概念的前身。 核心特性: - 结构体成员在内存中按声明顺序排列 - 每个成员占用独立的内存空间 - 结构体变量可以整体赋值(浅拷贝) - 结构体的大小受内存对齐规则影响 ## 12.2 核心API/语法 | 语法 | 说明 | | --------------------------------- | ------------------------ | | `struct Tag { ... };` | 结构体类型声明 | | `struct Tag var;` | 声明结构体变量 | | `struct Tag var = { .x = 1 };` | C99 指定初始化器 | | `var.member` | 用 `.` 访问成员 | | `ptr->member` | 用 `->` 通过指针访问成员 | | `sizeof(struct Tag)` | 结构体大小(含对齐填充) | | `offsetof(type, member)` | 成员偏移量 | | `container_of(ptr, type, member)` | 由成员指针反推结构体指针 | ## 12.3 代码示例(完整可编译,附gcc命令) ### 示例1:结构体定义、初始化与访问 ```c // 12_struct_basics.c #include #include struct Person { char name[32]; int age; double salary; }; int main(void) { // C89 风格初始化 struct Person p1 = {"Alice", 30, 7500.50}; // C99 指定初始化器(可乱序、可省略) struct Person p2 = { .salary = 8200.00, .name = "Bob", .age = 25 }; // . 和 -> 操作符 printf("p1: %s, %d, %.2f\n", p1.name, p1.age, p1.salary); struct Person *pp = &p2; printf("p2: %s, %d, %.2f\n", pp->name, pp->age, pp->salary); // 整体赋值(浅拷贝) struct Person p3 = p1; printf("p3: %s, %d, %.2f\n", p3.name, p3.age, p3.salary); return 0; } ``` **编译运行:** ```bash gcc -Wall -Wextra -std=c99 -o 12_struct_basics 12_struct_basics.c ./12_struct_basics ``` ### 示例2:内存对齐与 sizeof 计算 ```c // 12_struct_align.c #include #include struct Aligned { char a; // 1 byte + 3 padding int b; // 4 bytes char c; // 1 byte + 3 padding }; struct Packed { char a; char c; int b; }; #pragma pack(push, 1) struct PackedAttribute { char a; // 1 byte int b; // 4 bytes, no padding char c; // 1 byte }; #pragma pack(pop) struct Nested { int id; struct { double x; double y; } point; char tag; }; int main(void) { printf("sizeof(struct Aligned) = %zu\n", sizeof(struct Aligned)); printf(" offset of a = %zu\n", offsetof(struct Aligned, a)); printf(" offset of b = %zu\n", offsetof(struct Aligned, b)); printf(" offset of c = %zu\n", offsetof(struct Aligned, c)); printf("\n"); printf("sizeof(struct Packed) = %zu\n", sizeof(struct Packed)); printf(" offset of a = %zu\n", offsetof(struct Packed, a)); printf(" offset of c = %zu\n", offsetof(struct Packed, c)); printf(" offset of b = %zu\n", offsetof(struct Packed, b)); printf("\n"); printf("sizeof(struct PackedAttribute) = %zu\n", sizeof(struct PackedAttribute)); printf("\n"); printf("sizeof(struct Nested) = %zu\n", sizeof(struct Nested)); printf(" offset of id = %zu\n", offsetof(struct Nested, id)); printf(" offset of point = %zu\n", offsetof(struct Nested, point)); printf(" offset of tag = %zu\n", offsetof(struct Nested, tag)); return 0; } ``` **编译运行:** ```bash gcc -Wall -Wextra -std=c99 -o 12_struct_align 12_struct_align.c ./12_struct_align ``` ### 示例3:结构体传参与 container_of ```c // 12_struct_pass.c #include #include // container_of 宏原理演示 #define offsetof(TYPE, MEMBER) ((size_t)&((TYPE *)0)->MEMBER) #define container_of(ptr, type, member) \ ((type *)((char *)(ptr) - offsetof(type, member))) struct Node { int data; int priority; struct Node *next; }; // 值传递:拷贝整个结构体,开销大 void print_by_value(struct Node n) { printf("[by value] data=%d, pri=%d\n", n.data, n.priority); } // 指针传递:只传地址,高效(推荐) void print_by_ptr(const struct Node *n) { printf("[by ptr] data=%d, pri=%d\n", n->data, n->priority); } // 柔性数组成员 struct Buffer { int len; char data[]; // C99 柔性数组,必须是最后一个成员 }; int main(void) { // 结构体传参 struct Node n = {42, 10, NULL}; print_by_value(n); print_by_ptr(&n); // container_of 演示:已知成员指针,反推结构体指针 struct Node node = {100, 5, NULL}; struct Node *prio_ptr = &node.priority; // 假设外部只拿到了 priority 的指针 struct Node *base = container_of(prio_ptr, struct Node, priority); printf("container_of: recovered data = %d\n", base->data); // 柔性数组:动态分配 size_t msg_len = 12; struct Buffer *buf = malloc(sizeof(struct Buffer) + msg_len); buf->len = msg_len; memcpy(buf->data, "Hello World!", msg_len); printf("Buffer len=%d, data=%.*s\n", buf->len, buf->len, buf->data); free(buf); return 0; } ``` **编译运行:** ```bash gcc -Wall -Wextra -std=c99 -o 12_struct_pass 12_struct_pass.c ./12_struct_pass ``` ## 12.4 注意事项与易错点 1. **整体赋值是浅拷贝**:指针成员共享同一块内存,修改一个会影响另一个。深拷贝需要手动复制指针指向的内容。 2. **`#pragma pack` 只影响对齐,不影响成员排列顺序**:成员在内存中的相对顺序不变。 3. **柔性数组要求**:必须是结构体的最后一个成员;包含柔性数组的结构体不能直接赋值(无固定大小);分配时 `malloc(sizeof(struct) + extra)`。 4. **`sizeof` 与实际数据大小不同**:`sizeof` 包含填充字节,`strlen` 只看字符串内容。 5. **结构体嵌套时对齐以最大成员为基准**:内嵌结构体的对齐要求取其最大成员的对齐值。 ## 12.5 面试要点(5个Q&A) **Q1:结构体内存对齐的规则是什么?** > 1. 每个成员的偏移量必须是自身对齐值的整数倍(不足则填充)。 > 2. 结构体整体大小必须是最大对齐值的整数倍。 **Q2:`#pragma pack(1)` 的作用和副作用?** > 强制 1 字节对齐,消除填充。副作用:CPU 访问非对齐地址可能性能下降甚至崩溃(取决于硬件)。 **Q3:结构体值传递和指针传递的区别?** > 值传递复制整个结构体(大结构体开销大),指针传递只传 4/8 字节地址。注意值传递时修改不影响原数据。 **Q4:什么是柔性数组成员?** > C99 允许结构体最后一个成员是未知长度的数组 `char data[]`。必须 `malloc(sizeof(struct) + length)` 分配,用于变长数据包。 **Q5:`container_of` 宏的原理?** > 已知成员指针 `ptr`,通过 `ptr - offsetof(type, member)` 计算出结构体起始地址。Linux 内核广泛使用此宏实现链表等数据结构。