第9章 结构体与联合体 1. 结构体的深入理解 1.1 结构体的基本概念 结构体是一种复合数据类型,它可以包含不同类型的成员变量,这些成员变量被组织在一起,形成一个有意义的数据单元。结构体的设计是C语言面向对象编程思想的基础,为复杂数据模型的构建提供了灵活的手段。
1.1.1 结构体的内存布局 结构体的成员在内存中是连续存储的,每个成员的内存地址都按照其声明的顺序依次排列。编译器会根据成员的类型和对齐要求来分配内存,以优化内存访问性能。
1.1.1.1 内存对齐原理 内存对齐是指编译器为结构体成员分配内存时,使其地址满足特定的对齐要求。对齐的主要目的是提高内存访问速度,因为大多数CPU访问对齐地址的数据比非对齐地址的数据更快。
对齐规则 :
成员对齐 :每个成员的起始地址必须是其大小的整数倍结构体对齐 :整个结构体的大小必须是其最大成员大小的整数倍平台对齐 :不同平台有不同的默认对齐系数(如 4 或 8 字节)最小对齐 :成员的对齐要求不会超过平台的默认对齐系数硬件层面的影响 :
CPU 数据通路 :现代 CPU 的数据通路宽度为 64 位,对齐到 8 字节边界可以充分利用数据总线带宽缓存行 :对齐的数据更容易放入单个缓存行,减少缓存未命中内存控制器 :内存控制器通常以 64 字节(缓存行大小)为单位进行内存访问非对齐惩罚 :非对齐访问可能导致多个内存周期,甚至触发硬件异常SIMD 指令 :大多数 SIMD 指令要求数据对齐到 16 或 32 字节边界编译器实现细节 :
GCC :使用 -fpack-struct[=n] 选项控制对齐行为,__attribute__((aligned(n))) 强制对齐,__attribute__((packed)) 取消对齐Clang :类似的对齐控制选项和属性,与 GCC 兼容MSVC :使用 #pragma pack(n) 指令和 __declspec(align(n)) 属性对齐系数 :默认对齐系数通常为 sizeof(void*),即 4 字节(32位)或 8 字节(64位)内存对齐的数学模型 :
成员偏移量计算:offset = (previous_offset + previous_size + alignment - 1) & ~(alignment - 1) 结构体大小计算:size = (last_member_offset + last_member_size + max_alignment - 1) & ~(max_alignment - 1) 对齐对性能的具体影响 :
x86 架构 :非对齐访问会导致 2-3 倍的性能下降ARM 架构 :某些 ARM 架构对非对齐访问会触发数据中止异常RISC-V 架构 :非对齐访问会导致陷阱或性能下降高级对齐技巧 :
缓存行填充 :在结构体末尾添加填充,确保其大小是 64 字节的整数倍页面对齐 :对于大型数据结构,使用 __attribute__((aligned(4096))) 实现页面对齐NUMA 感知 :在 NUMA 架构上,确保相关数据结构分配在同一 NUMA 节点实际案例 :
1 2 3 4 5 6 7 8 9 10 11 12 struct alignas (64 ) CacheAlignedStruct { int x; int y; char padding[64 - 2 * sizeof (int )]; }; struct alignas (4096 ) PageAlignedStruct { char data[4096 ]; };
内存对齐示例 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 struct Point { int x; int y; }; printf ("结构体大小:%zu 字节\n" , sizeof (struct Point)); struct Data { int x; void * ptr; char c; }; printf ("Data 结构体大小:%zu 字节\n" , sizeof (struct Data)); struct AlignedData { int x; char c; } __attribute__((aligned(16 ))); printf ("AlignedData 结构体大小:%zu 字节\n" , sizeof (struct AlignedData));
性能优化策略 :
按大小排序 :将大成员放在结构体开头,减少填充缓存行对齐 :对于频繁访问的结构体,确保其大小是 64 字节的整数倍显式对齐 :使用 __attribute__((aligned(n))) 强制对齐到特定边界打包结构体 :对于内存受限场景,使用 __attribute__((packed)) 取消对齐对齐感知设计 :设计数据结构时考虑缓存行边界,避免跨缓存行访问对齐对性能的影响 :
对齐访问 :单个时钟周期完成,缓存命中率高非对齐访问 :多个时钟周期,可能跨缓存行,性能下降 30-50%极端情况 :某些架构(如 ARM)对非对齐访问会触发数据中止异常实际应用建议 :
高频访问数据 :优先考虑对齐,提高性能内存受限环境 :考虑打包结构体,节省空间网络协议 :使用打包结构体,确保数据布局与协议一致硬件寄存器 :使用位字段和打包结构体,精确映射寄存器布局1.1.1.2 内存填充和浪费 当结构体成员的类型大小不同时,编译器会在成员之间插入填充字节,以满足对齐要求。这可能会导致内存浪费,但提高了访问速度。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 struct Mixed { char c; int i; double d; }; printf ("结构体大小:%zu 字节\n" , sizeof (struct Mixed));
1.1.1.3 优化内存布局 通过合理安排结构体成员的顺序,可以减少内存填充,从而减少内存使用。
优化策略 :
按照成员大小从大到小排列 将相同类型的成员放在一起 考虑平台的对齐要求 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 struct BadLayout { char c; int i; double d; }; struct GoodLayout { double d; int i; char c; }; printf ("BadLayout 大小:%zu 字节\n" , sizeof (struct BadLayout)); printf ("GoodLayout 大小:%zu 字节\n" , sizeof (struct GoodLayout));
1.1.1.4 编译器对齐控制 可以使用编译器指令来控制结构体的对齐方式,以适应特殊需求。
GCC 编译器 :
1 2 3 4 5 6 7 8 9 10 11 #pragma pack(push, 1) struct PackedStruct { char c; int i; double d; }; #pragma pack(pop) printf ("压缩后大小:%zu 字节\n" , sizeof (struct PackedStruct));
MSVC 编译器 :
1 2 3 4 5 6 7 8 9 #pragma pack(1) struct PackedStruct { char c; int i; double d; }; #pragma pack()
1.1.1.5 不同架构下的内存布局 不同CPU架构(如x86、ARM、RISC-V)的对齐要求可能不同,这会影响结构体的内存布局和大小。
示例 :在32位和64位系统上的结构体大小差异
1 2 3 4 5 6 7 8 struct Example { char c; void * ptr; };
1.1.1.6 结构体的内存访问性能 结构体成员的内存访问性能取决于其对齐情况和访问模式:
对齐访问 :速度快,CPU可以在一个时钟周期内完成访问非对齐访问 :速度慢,可能需要多个时钟周期,甚至触发硬件异常缓存局部性 :连续访问结构体成员可以提高缓存命中率性能优化建议 :
合理安排成员顺序,提高缓存局部性 避免跨缓存行访问大型结构体成员 对于频繁访问的成员,放在结构体开头 1.1.2 结构体的底层实现 编译器在处理结构体时,会进行以下操作:
类型分析 :解析结构体成员的类型和对齐要求内存布局计算 :确定每个成员的偏移量和结构体的总大小代码生成 :为结构体访问生成适当的机器码类型信息存储 :生成调试信息和类型元数据优化分析 :识别结构体的使用模式,进行相应优化示例 :结构体成员访问的汇编代码
1 2 3 4 5 6 7 8 9 10 11 12 ; x86-64 汇编:访问 struct Point 的成员 ; struct Point { int x; int y; }; ; p 是指向 struct Point 的指针 mov rax, [rbp-8] ; 加载 p 的地址到 rax mov eax, [rax] ; 访问 p->x add eax, 10 ; x += 10 mov [rax], eax ; 存储回 p->x mov eax, [rax+4] ; 访问 p->y add eax, 20 ; y += 20 mov [rax+4], eax ; 存储回 p->y
ARM64 汇编 :
1 2 3 4 5 6 7 8 9 ; ARM64 汇编:访问 struct Point 的成员 ldr x0, [sp, #8] ; 加载 p 的地址到 x0 ldr w1, [x0] ; 访问 p->x add w1, w1, #10 ; x += 10 str w1, [x0] ; 存储回 p->x ldr w1, [x0, #4] ; 访问 p->y add w1, w1, #20 ; y += 20 str w1, [x0, #4] ; 存储回 p->y
RISC-V 汇编 :
1 2 3 4 5 6 7 8 9 10 11 12 ; RISC-V 汇编:访问 struct Point 的成员 ; struct Point { int x; int y; }; ; p 是指向 struct Point 的指针 lw a0, 0(sp) ; 加载 p 的地址到 a0 lw a1, 0(a0) ; 访问 p->x addi a1, a1, 10 ; x += 10 sw a1, 0(a0) ; 存储回 p->x lw a1, 4(a0) ; 访问 p->y addi a1, a1, 20 ; y += 20 sw a1, 4(a0) ; 存储回 p->y
编译器优化策略 :
成员重排序 :编译器可能会重新排序结构体成员以减少填充内联展开 :对于小型结构体,编译器可能会将成员直接内联到寄存器偏移量计算优化 :编译器会在编译时计算成员偏移量,避免运行时计算缓存感知优化 :编译器会考虑缓存行大小,优化结构体布局结构体访问的性能分析 :
直接访问 :p.x 生成一次内存访问指针访问 :pp->x 生成两次内存访问(先取指针,再访问成员)嵌套结构体访问 :p.a.b 生成多次内存访问,性能取决于嵌套深度数组访问 :array[i].x 生成索引计算和内存访问高级结构体实现技巧 :
结构体拆分 :将频繁访问的成员放在一个结构体中,提高缓存局部性结构体压缩 :使用位字段和打包结构体减少大小结构体继承 :通过嵌入实现类似面向对象的继承多态结构体 :使用函数指针实现运行时多态实际案例 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 typedef struct { int x, y; float velocity; } FastAccessMembers; typedef struct { FastAccessMembers fast; char name[100 ]; int health; float inventory[50 ]; } GameEntity; typedef struct { void (*draw)(void *); void (*update)(void *); int x, y; } Shape; typedef struct { Shape base; int width, height; } Rectangle; typedef struct { Shape base; int radius; } Circle; void draw_rectangle (void * self) { Rectangle* rect = (Rectangle*)self; } void draw_circle (void * self) { Circle* circle = (Circle*)self; }
1.2 结构体的声明 1.2.1 基本声明 结构体声明是创建新类型的过程,编译器会为其分配类型信息但不会分配内存。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 struct struct_name { member_type1 member_name1; member_type2 member_name2; }; struct Point { int x; int y; }; struct Student { char name[50 ]; int age; float score; };
编译期处理 :编译器在处理结构体声明时,会:
解析成员类型和顺序 计算每个成员的偏移量 确定结构体的总大小和对齐要求 生成类型信息供后续使用 1.2.2 匿名结构体 匿名结构体是没有类型名称的结构体,只能在声明时直接定义变量。
1 2 3 4 5 6 7 8 9 10 11 struct { int x; int y; } p1, p2; p1.x = 10 ; p1.y = 20 ; p2 = p1;
使用场景 :
临时数据结构,只在局部作用域使用 作为其他结构体的成员,减少命名空间污染 与typedef结合使用,创建一次性类型定义 1.2.3 嵌套结构体 结构体可以嵌套在其他结构体中,形成层次化的数据结构。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 struct Date { int year; int month; int day; }; struct Person { char name[50 ]; int age; struct Date birthday ; }; struct Person person ;person.name[0 ] = 'A' ; person.name[1 ] = 'l' ; person.name[2 ] = 'i' ; person.name[3 ] = 'c' ; person.name[4 ] = 'e' ; person.name[5 ] = '\0' ; person.age = 18 ; person.birthday.year = 2005 ; person.birthday.month = 5 ; person.birthday.day = 15 ;
内存布局 :嵌套结构体的成员在内存中是连续存储的,与外部结构体成员一起按照对齐规则排列。
高级嵌套技巧 :
自引用结构体 :包含指向自身类型的指针,用于构建链表、树等数据结构不完全类型 :在声明时使用前向声明,实现递归数据结构匿名嵌套 :在C11及以上标准中,可以使用匿名结构体作为成员1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 struct Node { int data; struct Node *next ; }; struct Person { char name[50 ]; struct { int year; int month; int day; } birthday; }; struct Person p ;p.birthday.year = 2000 ;
1.3 结构体变量的定义和初始化 1.3.1 基本初始化 结构体变量的定义会在内存中分配空间,初始化则是为这些空间设置初始值。
1 2 3 4 5 6 7 struct Point p1 ; struct Student s1 ; struct Point p2 = {10 , 20 }; struct Student s2 = {"Alice" , 18 , 95.5 };
内存行为 :
未初始化的结构体变量:成员值为不确定值(栈上)或零值(全局/静态存储区) 初始化的结构体变量:按照初始化列表的顺序为成员赋值 1.3.2 指定成员初始化(C99 及以上) 指定成员初始化允许按名称初始化结构体成员,提高代码可读性和维护性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 struct Point p3 = {.x = 5 , .y = 15 };struct Student s3 = {.name = "Bob" , .score = 88.5 , .age = 19 }; struct Person person = { .name = "Charlie" , .age = 20 , .birthday = { .year = 2003 , .month = 10 , .day = 20 } };
特性 :
未指定的成员会被初始化为零值(包括嵌套结构体成员) 初始化顺序可以任意,不影响最终结果 可以混合使用位置初始化和指定成员初始化 1.3.3 结构体的聚合初始化 聚合初始化是 C 语言中初始化数组、结构体等聚合类型的统一方式。
1 2 3 4 5 6 7 8 9 10 11 12 13 struct Point points [] = { {10 , 20 }, {30 , 40 }, {50 , 60 } }; struct Point points [] = { [0 ] = {.x = 10 , .y = 20 }, [1 ] = {.x = 30 , .y = 40 }, [2 ] = {.x = 50 , .y = 60 } };
高级初始化技巧 :
省略数组大小 :编译器会根据初始化列表自动计算数组大小指定索引初始化 :使用 [index] 语法指定特定位置的初始化值复合字面量 :在表达式中创建临时结构体值1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 struct Point points [] = { {10 , 20 }, {30 , 40 }, {50 , 60 } }; struct Point points [5] = { [0 ] = {.x = 10 , .y = 20 }, [2 ] = {.x = 50 , .y = 60 } }; void move_point (struct Point *p, int dx, int dy) { *p = (struct Point){.x = p->x + dx, .y = p->y + dy}; }
初始化规则 :
聚合初始化从第一个成员开始,按顺序进行 多余的初始化器会导致编译错误 不足的初始化器会将剩余成员初始化为零值 嵌套聚合类型会递归应用初始化规则 1.4 结构体成员的访问 1.4.1 使用点运算符访问 点运算符 . 用于直接访问结构体变量的成员,是最基本的访问方式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 struct Point p ;p.x = 10 ; p.y = 20 ; printf ("点的坐标:(%d, %d)\n" , p.x, p.y);struct Circle { struct Point center ; float radius; }; struct Circle c ;c.center.x = 0 ; c.center.y = 0 ; c.radius = 5.0 ; printf ("圆心:(%d, %d), 半径:%.2f\n" , c.center.x, c.center.y, c.radius);
编译期处理 :
编译器将 p.x 转换为 *( (char *)&p + offsetof(struct Point, x) ) offsetof 宏计算成员在结构体内的偏移量对于嵌套结构体,编译器会递归计算偏移量 1.4.2 使用箭头运算符访问 箭头运算符 -> 用于访问结构体指针的成员,是 (*ptr).member 的语法糖。
1 2 3 4 5 6 7 8 9 10 11 struct Point p = {10 , 20 };struct Point *pp = &p;printf ("点的坐标:(%d, %d)\n" , pp->x, pp->y);pp->x = 30 ; pp->y = 40 ; printf ("修改后:(%d, %d)\n" , p.x, p.y);
底层实现 :
pp->x 等价于 (*pp).x编译器会生成相同的机器码 箭头运算符提高了代码可读性,特别是在处理链表、树等数据结构时 访问性能考量 :
直接访问(点运算符):一次内存访问 指针访问(箭头运算符):两次内存访问(先取指针值,再访问成员) 但现代CPU的缓存机制会显著减少这种差异 高级访问技巧 :
使用偏移量访问 :在底层编程中,可使用 offsetof 宏计算偏移量后访问类型转换访问 :通过类型转换实现结构体成员的批量操作位操作访问 :对于位域成员,可使用位操作进行精细控制1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #include <stddef.h> struct Point p = {10 , 20 };size_t x_offset = offsetof(struct Point, x);size_t y_offset = offsetof(struct Point, y);*(int *)((char *)&p + x_offset) = 100 ; *(int *)((char *)&p + y_offset) = 200 ; struct RGB { unsigned char r, g, b; }; struct RGB color = {255 , 0 , 0 };uint32_t *color_ptr = (uint32_t *)&color;printf ("RGB 值:0x%08X\n" , *color_ptr);
1.5 结构体的赋值和比较 1.5.1 结构体赋值 结构体赋值是一种值拷贝操作,会复制源结构体的所有成员值到目标结构体。
1 2 3 4 5 6 7 8 9 10 struct Point p1 = {10 , 20 };struct Point p2 = p1; printf ("p2: (%d, %d)\n" , p2.x, p2.y);struct Point *pp1 = &p1;struct Point *pp2 = pp1; pp2->x = 100 ; printf ("p1.x: %d\n" , p1.x);
底层实现 :
编译器会生成循环或批量内存复制指令 对于小型结构体,通常生成内联的移动指令 对于大型结构体,可能调用 memcpy 函数 赋值性能考量 :
结构体大小:越大的结构体赋值开销越大 成员类型:包含指针的结构体只是复制指针值,不复制指针指向的数据 优化策略:对于大型结构体,优先使用指针传递而非值传递 深度复制 vs 浅度复制 :
浅度复制:只复制成员值,对于指针成员,只复制指针地址 深度复制:不仅复制成员值,还复制指针指向的数据 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 struct StringWrapper { char *str; }; struct StringWrapper sw1 ;sw1.str = malloc (10 ); strcpy (sw1.str, "Hello" );struct StringWrapper sw2 = sw1; sw2.str[0 ] = 'h' ; printf ("sw1.str: %s\n" , sw1.str); struct StringWrapper sw3 ;sw3.str = malloc (strlen (sw1.str) + 1 ); strcpy (sw3.str, sw1.str); free (sw1.str);free (sw3.str);
1.5.2 结构体比较 C 语言不支持直接使用 == 或 != 运算符比较结构体,需要逐个成员进行比较。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 struct Point p1 = {10 , 20 };struct Point p2 = {10 , 20 };if (p1.x == p2.x && p1.y == p2.y){ printf ("p1 和 p2 相等\n" ); } else { printf ("p1 和 p2 不相等\n" ); } int compare_points (struct Point p1, struct Point p2) { if (p1.x != p2.x) { return p1.x - p2.x; } return p1.y - p2.y; } if (compare_points(p1, p2) == 0 ){ printf ("p1 和 p2 相等\n" ); }
为什么不支持直接比较 :
结构体可能包含填充字节,这些字节的值是不确定的 不同编译器的填充策略可能不同 直接内存比较会受填充字节影响,导致结果不可靠 比较函数设计原则 :
返回类型:通常返回 int,0 表示相等,非 0 表示不等 比较顺序:先比较重要成员,提高比较效率 类型安全:确保比较的成员类型相同 深度比较:对于包含指针的结构体,需要决定是否进行深度比较 高级比较技巧 :
使用 memcmp :对于不含填充字节的结构体,可使用 memcmp 进行快速比较位级比较 :对于位域结构体,可使用位操作进行精细比较哈希比较 :先计算哈希值,再比较哈希值,提高大型结构体的比较效率1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 #pragma pack(push, 1) struct CompactPoint { int x; int y; }; #pragma pack(pop) struct CompactPoint cp1 = {10 , 20 };struct CompactPoint cp2 = {10 , 20 };if (memcmp (&cp1, &cp2, sizeof (struct CompactPoint)) == 0 ) { printf ("cp1 和 cp2 相等\n" ); } #include <stdint.h> uint32_t hash_point (const struct Point *p) { uint32_t hash = 5381 ; hash = ((hash << 5 ) + hash) ^ p->x; hash = ((hash << 5 ) + hash) ^ p->y; return hash; } if (hash_point(&p1) == hash_point(&p2) && p1.x == p2.x && p1.y == p2.y) { printf ("p1 和 p2 相等\n" ); }
1.6 结构体数组 结构体数组是存储相同结构体类型元素的连续内存块,是处理批量数据的有效方式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 struct Student students [5];struct Student students [] = { {"Alice" , 18 , 95.5 }, {"Bob" , 19 , 88.5 }, {"Charlie" , 18 , 92.0 } }; for (int i = 0 ; i < 3 ; i++){ printf ("姓名:%s, 年龄:%d, 分数:%.2f\n" , students[i].name, students[i].age, students[i].score); } struct Student *ptr = students;for (int i = 0 ; i < 3 ; i++){ printf ("姓名:%s, 年龄:%d, 分数:%.2f\n" , ptr[i].name, ptr[i].age, ptr[i].score); }
内存布局 :
结构体数组在内存中是连续存储的 每个元素的内存地址为 base_address + i * sizeof(struct Student) 数组名是指向第一个元素的常量指针 访问方式 :
使用下标访问:students[i].member 使用指针访问:(students + i)->member 或 ptr[i].member 使用指针算术:ptr++ 移动到下一个元素 高级应用技巧 :
动态结构体数组 :使用动态内存分配创建可变大小的结构体数组结构体数组排序 :实现比较函数后使用 qsort 进行排序结构体数组作为函数参数 :传递数组指针和长度二维结构体数组 :用于表示表格型数据1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 struct Student *create_students (int count) { return (struct Student *)malloc (count * sizeof (struct Student)); } int compare_students (const void *a, const void *b) { const struct Student *s1 = (const struct Student *)a; const struct Student *s2 = (const struct Student *)b; return s2->score - s1->score; } void sort_students (struct Student *students, int count) { qsort(students, count, sizeof (struct Student), compare_students); } void print_students (const struct Student *students, int count) { for (int i = 0 ; i < count; i++) { printf ("%s: %.2f\n" , students[i].name, students[i].score); } } struct Seat { int row; int col; bool occupied; }; struct Seat theater [10][20]; void init_theater (struct Seat theater[][20 ], int rows) { for (int i = 0 ; i < rows; i++) { for (int j = 0 ; j < 20 ; j++) { theater[i][j].row = i + 1 ; theater[i][j].col = j + 1 ; theater[i][j].occupied = false ; } } }
性能优化策略 :
内存局部性 :结构体数组的连续存储有利于缓存命中批量操作 :可以使用 SIMD 指令对结构体数组进行批量处理大小优化 :合理设计结构体大小,避免缓存行浪费对齐优化 :确保结构体大小是 64 字节(缓存行大小)的整数倍实际应用场景 :
学生信息管理系统 图形学中的顶点数组 网络协议中的数据包队列 数据库中的记录集合 1.7 结构体作为函数参数 1.7.1 值传递 值传递是指将结构体的完整副本传递给函数,函数内部操作的是副本,不会影响原始结构体。
1 2 3 4 5 6 7 8 9 10 11 12 void print_point (struct Point p) { printf ("(%d, %d)\n" , p.x, p.y); } int main (void ) { struct Point p = {10 , 20 }; print_point(p); return 0 ; }
优缺点分析 :
优点 :函数内部操作不会影响原始数据,安全性高缺点 :对于大型结构体,复制开销大,影响性能适用场景 :小型结构体(如 Point、RGB 等),或需要在函数内部修改结构体副本编译器优化 :
小型结构体:编译器可能会将结构体成员直接传递到寄存器 中型结构体:编译器可能会使用栈传递 大型结构体:编译器可能会自动转换为指针传递(NRVO - Named Return Value Optimization) 1.7.2 地址传递 地址传递是指将结构体的指针传递给函数,函数内部通过指针访问和修改原始结构体。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 void move_point (struct Point *pp, int dx, int dy) { pp->x += dx; pp->y += dy; } int main (void ) { struct Point p = {10 , 20 }; move_point(&p, 5 , 5 ); printf ("移动后:(%d, %d)\n" , p.x, p.y); return 0 ; }
优缺点分析 :
优点 :传递开销小,适合大型结构体,可修改原始数据缺点 :函数内部操作会影响原始数据,需要注意副作用适用场景 :大型结构体,或需要在函数内部修改原始数据指针参数的最佳实践 :
始终检查指针是否为 NULL 对于输出参数,使用指针传递 对于输入参数,考虑使用 const 修饰 1.7.3 const 修饰符 使用 const 修饰符可以防止函数修改结构体的内容,提高代码的安全性和可读性。
1 2 3 4 5 6 7 void print_student (const struct Student *s) { printf ("姓名:%s, 年龄:%d, 分数:%.2f\n" , s->name, s->age, s->score); }
const 的层级 :
const struct Student *s:指针指向的结构体内容不可修改struct Student *const s:指针本身不可修改const struct Student *const s:指针和指针指向的内容都不可修改const 的好处 :
安全性 :防止意外修改数据可读性 :明确函数的意图是只读操作优化性 :编译器可以进行更多优化兼容性 :可以接受非 const 指针的参数高级参数传递技巧 :
结构体引用传递 :在 C++ 中使用引用传递,C 中可通过指针模拟结构体数组传递 :传递数组指针和长度嵌套结构体传递 :注意指针链的解引用开销函数指针作为结构体成员 :实现回调机制1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 void process_students (struct Student *students, int count, void (*process_func)(struct Student *)) { for (int i = 0 ; i < count; i++) { process_func(&students[i]); } } struct Calculator { int (*add)(int , int ); int (*subtract)(int , int ); int (*multiply)(int , int ); int (*divide)(int , int ); }; void init_calculator (struct Calculator *calc) { calc->add = add; calc->subtract = subtract; calc->multiply = multiply; calc->divide = divide; }
1.8 结构体作为函数返回值 1.8.1 返回结构体 函数可以直接返回结构体类型的值,这会将结构体的副本传递给调用者。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 struct Point create_point (int x, int y) { struct Point p ; p.x = x; p.y = y; return p; } int main (void ) { struct Point p1 = create_point(10 , 20 ); printf ("p1: (%d, %d)\n" , p1.x, p1.y); return 0 ; }
返回机制 :
函数将结构体值复制到返回值临时存储区 调用者将临时存储区的内容复制到目标变量 现代编译器会优化这种复制操作(RVO - Return Value Optimization) 编译器优化 :
RVO (返回值优化):编译器直接在调用者的栈空间中构造返回的结构体NRVO (命名返回值优化):对于命名的局部结构体变量,编译器也能进行优化Copy Elision (复制消除):完全消除不必要的复制操作优缺点分析 :
优点 :内存管理简单,不需要手动释放内存缺点 :对于大型结构体,可能存在复制开销(但通常会被编译器优化)适用场景 :小型到中型结构体,或需要返回新创建的结构体实例1.8.2 返回结构体指针 函数可以返回结构体指针,这需要注意内存管理,确保返回的指针指向有效的内存。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 struct Point *create_point_ptr1 (int x, int y) { static struct Point p ; p.x = x; p.y = y; return &p; } struct Point *create_point_ptr2 (int x, int y) { struct Point *p = (struct Point *)malloc (sizeof (struct Point)); if (p != NULL ) { p->x = x; p->y = y; } return p; } int main (void ) { struct Point *p1 = create_point_ptr1(30 , 40 ); printf ("p1: (%d, %d)\n" , p1->x, p1->y); struct Point *p2 = create_point_ptr2(50 , 60 ); if (p2 != NULL ) { printf ("p2: (%d, %d)\n" , p2->x, p2->y); free (p2); } return 0 ; }
内存来源 :
静态存储区 :返回静态变量的地址,生命周期长,但不是线程安全的动态存储区 :使用 malloc 分配,需要调用者手动释放调用者提供的缓冲区 :函数接收指针参数,在其中构造结构体全局存储区 :返回全局变量的地址,类似静态变量返回指针的最佳实践 :
明确内存所有权:谁分配谁释放 使用智能指针或内存池管理动态内存 避免返回局部变量的地址 对于静态变量,确保线程安全性 高级返回技巧 :
工厂函数 :返回初始化好的结构体指针对象池 :预分配结构体实例,提高性能惰性初始化 :首次调用时初始化,后续调用返回缓存的实例1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 struct Student *create_student (const char *name, int age, float score) { struct Student *s = (struct Student *)malloc (sizeof (struct Student)); if (s != NULL ) { strncpy (s->name, name, sizeof (s->name) - 1 ); s->name[sizeof (s->name) - 1 ] = '\0' ; s->age = age; s->score = score; } return s; } #define MAX_OBJECTS 100 struct ObjectPool { struct Point objects [MAX_OBJECTS ]; bool in_use[MAX_OBJECTS]; int count; }; struct ObjectPool pool = {0 };struct Point *get_point_from_pool (int x, int y) { for (int i = 0 ; i < MAX_OBJECTS; i++) { if (!pool.in_use[i]) { pool.in_use[i] = true ; pool.objects[i].x = x; pool.objects[i].y = y; return &pool.objects[i]; } } return NULL ; } void return_point_to_pool (struct Point *p) { for (int i = 0 ; i < MAX_OBJECTS; i++) { if (&pool.objects[i] == p) { pool.in_use[i] = false ; break ; } } }
1.9 结构体的高级特性 1.9.1 柔性数组成员 C99 引入了柔性数组成员(Flexible Array Member),允许结构体的最后一个成员是一个未指定大小的数组,用于创建可变大小的数据结构。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 struct Buffer { int size; char data[]; }; struct Buffer *create_buffer (int size) { struct Buffer *buf = (struct Buffer *)malloc (sizeof (struct Buffer) + size * sizeof (char )); if (buf != NULL ) { buf->size = size; } return buf; } void free_buffer (struct Buffer *buf) { free (buf); } int main (void ) { struct Buffer *buf = create_buffer(100 ); if (buf != NULL ) { strcpy (buf->data, "Hello, World!" ); printf ("数据:%s\n" , buf->data); free_buffer(buf); } return 0 ; }
柔性数组成员的特性 :
必须是结构体的最后一个成员 数组大小未指定(空方括号 []) sizeof 运算符计算结构体大小时不包括柔性数组成员柔性数组成员不占用结构体的存储空间 只能通过指针访问柔性数组成员 C99 及以上标准支持柔性数组成员 不能有多个柔性数组成员 内存布局 :
1 2 3 4 5 6 7 8 +------------+ | size | // 4 字节 +------------+ | data[0] | // 柔性数组成员开始 | data[1] | | ... | | data[n-1]| +------------+
与传统方法的比较 :
传统方法 :使用指针成员 char *data,需要两次内存分配(结构体和数据)柔性数组成员 :一次内存分配,数据与结构体连续存储性能对比 :
内存分配 :柔性数组成员减少一次内存分配调用,提高性能内存访问 :数据与结构体连续存储,提高缓存局部性内存碎片 :减少内存碎片,因为只分配一次内存释放操作 :只需要一次 free 调用,简化内存管理高级应用场景 :
变长数据包 :网络协议中的可变长度数据包动态字符串 :自包含的可变长度字符串缓冲区管理 :自定义大小的缓冲区序列化数据 :将复杂数据结构序列化为连续内存内存池 :使用柔性数组成员实现高效的内存池自定义数据结构 :如链表节点、树节点等柔性数组成员的高级实现技巧 :
内存分配对齐 :确保柔性数组成员的数据对齐到适当的边界大小计算 :使用 offsetof 宏计算柔性数组成员的偏移量安全访问 :添加边界检查,防止缓冲区溢出内存重分配 :使用 realloc 调整柔性数组成员的大小实际案例 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 typedef struct { size_t length; size_t capacity; char data[]; } DynamicString; DynamicString *create_string (const char *str) { size_t len = strlen (str); size_t capacity = len + 1 ; DynamicString *ds = (DynamicString *)malloc (sizeof (DynamicString) + capacity); if (ds) { ds->length = len; ds->capacity = capacity; strcpy (ds->data, str); } return ds; } void resize_string (DynamicString **ds, size_t new_capacity) { if (new_capacity <= (*ds)->capacity) { return ; } *ds = (DynamicString *)realloc (*ds, sizeof (DynamicString) + new_capacity); if (*ds) { (*ds)->capacity = new_capacity; } } void append_string (DynamicString **ds, const char *str) { size_t str_len = strlen (str); size_t new_length = (*ds)->length + str_len; if (new_length >= (*ds)->capacity) { size_t new_capacity = (*ds)->capacity * 2 ; if (new_capacity < new_length + 1 ) { new_capacity = new_length + 1 ; } resize_string(ds, new_capacity); } strcpy ((*ds)->data + (*ds)->length, str); (*ds)->length = new_length; } void free_string (DynamicString *ds) { free (ds); } typedef struct { uint16_t type; uint16_t length; uint8_t data[]; } NetworkPacket; NetworkPacket *create_packet (uint16_t type, const void *payload, size_t payload_len) { NetworkPacket *packet = (NetworkPacket *)malloc (sizeof (NetworkPacket) + payload_len); if (packet) { packet->type = type; packet->length = sizeof (NetworkPacket) + payload_len; memcpy (packet->data, payload, payload_len); } return packet; }
柔性数组成员的最佳实践 :
始终存储柔性数组成员的大小,方便后续访问和管理 预留足够的容量,减少 realloc 的调用次数 添加边界检查,防止缓冲区溢出 对于大型数据,考虑使用内存池或其他内存管理策略 注意内存对齐,特别是对于需要对齐的类型(如整数、浮点数等) 1.9.2 结构体的位字段 位字段(Bit Fields)允许在结构体中指定成员占用的位数,用于节省内存和实现位级操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 struct Flags { unsigned int flag1 : 1 ; unsigned int flag2 : 2 ; unsigned int flag3 : 3 ; unsigned int : 2 ; unsigned int flag4 : 8 ; }; struct Flags f ;f.flag1 = 1 ; f.flag2 = 3 ; f.flag3 = 5 ; f.flag4 = 255 ; printf ("flag1: %d\n" , f.flag1);printf ("flag2: %d\n" , f.flag2);printf ("flag3: %d\n" , f.flag3);printf ("flag4: %d\n" , f.flag4);printf ("结构体大小:%zu 字节\n" , sizeof (struct Flags));
位字段的特性 :
位字段的类型必须是整数类型(int、unsigned int 等) 位字段的位数不能超过其类型的大小 未命名的位字段用于填充 位字段在内存中的布局依赖于编译器 位字段的大小不能为零 位字段的类型决定了其符号性 内存布局 :
位字段通常按照声明顺序从低位到高位排列 不同编译器可能有不同的打包策略 可以使用 #pragma pack 控制对齐方式 位字段的存储单元大小由其类型决定(如 int 为 4 字节) 位字段可以跨存储单元边界,具体取决于编译器实现 高级应用场景 :
硬件寄存器映射 :直接映射硬件寄存器的位域网络协议解析 :解析网络协议中的位级字段权限控制 :使用位字段表示权限标志状态管理 :使用位字段表示多个布尔状态压缩数据 :在有限内存环境中存储大量小整数位图操作 :实现高效的位图数据结构CPU 特性检测 :检测处理器特性标志位字段的性能考量 :
内存使用 :显著节省内存,特别是当需要存储多个小整数时访问速度 :可能比普通成员稍慢,因为需要位操作可移植性 :位字段的布局依赖于编译器,可能影响可移植性缓存局部性 :位字段通常具有较好的缓存局部性,因为它们占用空间小原子性 :位字段的访问和修改可能不是原子的,需要额外的同步措施高级位字段技巧 :
联合与位字段结合 :同时支持位级和字节级访问位字段掩码 :使用位掩码操作位字段位字段的原子操作 :确保多线程环境下的安全访问位字段的位序控制 :处理不同字节序的平台差异位字段的边界对齐 :优化位字段的存储布局位字段的实际案例 :
案例 1:硬件寄存器映射
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 union GPIO_Reg { uint32_t value; struct { uint32_t MODE0 : 2 ; uint32_t MODE1 : 2 ; uint32_t MODE2 : 2 ; uint32_t MODE3 : 2 ; uint32_t MODE4 : 2 ; uint32_t MODE5 : 2 ; uint32_t MODE6 : 2 ; uint32_t MODE7 : 2 ; uint32_t Reserved : 16 ; } bits; }; volatile union GPIO_Reg *GPIO_MODE = (volatile union GPIO_Reg *)0x40020000 ;GPIO_MODE->bits.MODE0 = 0x1 ; GPIO_MODE->bits.MODE1 = 0x0 ;
案例 2:网络协议解析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 struct IPv4Header { uint8_t version : 4 ; uint8_t ihl : 4 ; uint8_t tos; uint16_t total_length; uint16_t identification; struct { uint16_t flags : 3 ; uint16_t fragment_offset : 13 ; } frag; uint8_t ttl; uint8_t protocol; uint16_t header_checksum; uint32_t source_address; uint32_t destination_address; uint32_t options[]; }; void parse_ipv4_header (const uint8_t *packet) { struct IPv4Header *header = (struct IPv4Header *)packet; printf ("版本: %d\n" , header->version); printf ("头部长度: %d\n" , header->ihl * 4 ); printf ("总长度: %d\n" , ntohs(header->total_length)); printf ("协议: %d\n" , header->protocol); printf ("源地址: %d.%d.%d.%d\n" , (header->source_address >> 24 ) & 0xFF , (header->source_address >> 16 ) & 0xFF , (header->source_address >> 8 ) & 0xFF , header->source_address & 0xFF ); }
案例 3:权限控制
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 typedef struct { unsigned int read : 1 ; unsigned int write : 1 ; unsigned int execute : 1 ; unsigned int delete : 1 ; unsigned int modify : 1 ; unsigned int reserved : 27 ; } FilePermissions; bool has_permission (FilePermissions perm, unsigned int flag) { return (perm.read && (flag & 0x1 )) || (perm.write && (flag & 0x2 )) || (perm.execute && (flag & 0x4 )) || (perm.delete && (flag & 0x8 )) || (perm.modify && (flag & 0x10 )); } FilePermissions set_permission (FilePermissions perm, unsigned int flag, bool value) { if (flag & 0x1 ) perm.read = value; if (flag & 0x2 ) perm.write = value; if (flag & 0x4 ) perm.execute = value; if (flag & 0x8 ) perm.delete = value; if (flag & 0x10 ) perm.modify = value; return perm; }
位字段的最佳实践 :
明确类型 :始终使用 unsigned 类型来避免符号扩展问题命名规范 :为位字段使用清晰的命名,说明其用途文档化 :详细记录位字段的布局和用途,特别是在硬件映射时可移植性 :避免依赖于位字段的具体布局,使用掩码和移位操作作为替代方案性能优化 :对于频繁访问的位字段,考虑使用位操作代替位字段访问边界检查 :确保位字段的赋值不超过其位数范围内存对齐 :合理安排位字段的顺序,减少填充位字段与位操作的比较 :
特性 位字段 位操作 代码可读性 高 低 内存使用 相同 相同 访问速度 可能较慢 可能较快 可移植性 低 高 灵活性 低 高 适用场景 硬件映射、协议解析 通用位操作、性能关键代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 union Register { uint32_t value; struct { uint32_t reserved : 24 ; uint32_t status : 4 ; uint32_t enable : 1 ; uint32_t interrupt : 1 ; uint32_t error : 2 ; } bits; }; union Register reg ;reg.value = 0x12345678 ; printf ("状态:%d\n" , reg.bits.status);printf ("使能:%d\n" , reg.bits.enable);#define STATUS_MASK 0x000000F0 #define STATUS_SHIFT 4 uint32_t get_status (uint32_t reg_value) { return (reg_value & STATUS_MASK) >> STATUS_SHIFT; } void set_flag_atomic (volatile struct Flags *f, int flag_index, bool value) { if (flag_index == 0 ) { __atomic_store_n(&f->flag1, value, __ATOMIC_RELEASE); } }
2. 共用体的深入理解 2.1 共用体的基本概念 共用体(Union)是一种特殊的数据类型,它允许在同一块内存空间中存储不同类型的数据。共用体的所有成员共享同一块内存,修改一个成员会影响其他成员。
2.1.1 共用体的内存布局 共用体的大小等于其最大成员的大小,所有成员都从同一个内存地址开始存储,共享同一块内存空间。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 union Data { int i; float f; char c; }; printf ("共用体大小:%zu 字节\n" , sizeof (union Data));
内存布局的详细分析 :
所有成员的起始地址相同 共用体的总大小至少为最大成员的大小 共用体的大小会被对齐到最大成员的对齐要求 不同成员的内存表示可能重叠,修改一个成员会影响其他成员 与结构体的对比 :
特性 结构体 (struct) 共用体 (union) 内存分配 所有成员各自分配内存,总大小为各成员大小之和(含填充) 所有成员共享同一块内存,总大小为最大成员的大小 成员访问 同时可访问所有成员 同一时间只能有效访问一个成员 内存使用 内存使用较大 内存使用较小,适合节省空间 初始化 可以初始化多个成员 只能初始化第一个成员
2.2 共用体的声明和使用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 union union_name { member_type1 member_name1; member_type2 member_name2; }; union Data { int i; float f; char c; }; union Data data ;data.i = 100 ; printf ("data.i = %d\n" , data.i);data.f = 3.14 ; printf ("data.f = %.2f\n" , data.f);printf ("data.i = %d\n" , data.i);
共用体的初始化 :
只能初始化第一个成员 C99及以上支持指定成员初始化 未初始化的共用体成员值是不确定的 1 2 3 4 5 union Data d1 = {100 }; union Data d2 = {.f = 3.14 };
共用体的访问规则 :
同一时间只能有效访问一个成员 访问最近修改过的成员会得到预期结果 访问未修改过的成员会得到不确定的值 不同类型成员之间的转换需要谨慎处理 2.3 共用体的应用 2.3.1 节省内存 当不同类型的数据不需要同时使用时,使用共用体可以显著节省内存空间。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 struct Value { enum { INT, FLOAT, STRING } type; union { int i; float f; char *s; } data; }; struct Value v ;v.type = INT; v.data.i = 100 ; printf ("值:%d\n" , v.data.i);v.type = FLOAT; v.data.f = 3.14 ; printf ("值:%.2f\n" , v.data.f);v.type = STRING; v.data.s = "Hello" ; printf ("值:%s\n" , v.data.s);
内存节省分析 :
不使用共用体:需要 sizeof(enum) + sizeof(int) + sizeof(float) + sizeof(char *) 的内存 使用共用体:只需要 sizeof(enum) + max(sizeof(int), sizeof(float), sizeof(char *)) 的内存 在嵌入式系统或需要大量数据结构的场景中,内存节省效果显著 内存节省的具体计算 :
32位系统:sizeof(enum) = 4,sizeof(int) = 4,sizeof(float) = 4,sizeof(char *) = 4不使用共用体:4 + 4 + 4 + 4 = 16 字节 使用共用体:4 + 4 = 8 字节(节省 50%) 64位系统:sizeof(enum) = 4,sizeof(int) = 4,sizeof(float) = 4,sizeof(char *) = 8不使用共用体:4 + 4 + 4 + 8 = 20 字节 使用共用体:4 + 8 = 12 字节(节省 40%) 案例:嵌入式系统中的应用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 typedef struct { enum { TEMP, HUMIDITY, PRESSURE, LIGHT } type; union { float temperature; float humidity; float pressure; int light; } value; uint32_t timestamp; } SensorData; void process_sensor_data (SensorData *data) { switch (data->type) { case TEMP: printf ("温度: %.2f ℃\n" , data->value.temperature); break ; case HUMIDITY: printf ("湿度: %.2f %%\n" , data->value.humidity); break ; case PRESSURE: printf ("气压: %.2f hPa\n" , data->value.pressure); break ; case LIGHT: printf ("光照: %d lux\n" , data->value.light); break ; } }
内存使用对比 :
不使用共用体:需要 sizeof(enum) + sizeof(float) * 3 + sizeof(int) + sizeof(uint32_t) = 4 + 12 + 4 + 4 = 24 字节 使用共用体:只需要 sizeof(enum) + sizeof(float) + sizeof(uint32_t) = 4 + 4 + 4 = 12 字节(节省 50%) 2.3.2 类型转换 共用体可以用于在不同类型之间进行底层的类型转换,绕过编译器的类型检查。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 union FloatInt { float f; int i; }; void print_float_bits (float f) { union FloatInt fi ; fi.f = f; printf ("浮点数 %.2f 的二进制表示:\n" , f); for (int j = 31 ; j >= 0 ; j--) { printf ("%d" , (fi.i >> j) & 1 ); if (j == 31 || j == 23 ) printf (" " ); } printf ("\n" ); } int main (void ) { print_float_bits(3.14 ); return 0 ; }
类型转换的原理 :
共用体的所有成员共享同一块内存 修改一个成员后,以另一种类型读取该内存,实现了底层的类型转换 这种转换是基于内存表示的,不涉及数值的语义转换 注意事项 :
类型转换的结果依赖于底层的内存表示 不同编译器、不同架构可能有不同的内存表示 应谨慎使用,确保转换的安全性和可移植性 高级类型转换技巧 :
浮点数与整数转换 :分析浮点数的 IEEE 754 表示字节序转换 :在不同字节序的平台之间转换数据指针类型转换 :实现多态和类型擦除位模式提取 :从复杂数据类型中提取特定的位模式案例:浮点数位操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 union FloatBits { float f; struct { uint32_t mantissa : 23 ; uint32_t exponent : 8 ; uint32_t sign : 1 ; } bits; }; void analyze_float (float f) { union FloatBits fb ; fb.f = f; printf ("浮点数: %.6f\n" , f); printf ("符号位: %d\n" , fb.bits.sign); printf ("指数位: %d ( biased: %d)\n" , fb.bits.exponent, fb.bits.exponent - 127 ); printf ("尾数位: 0x%06X\n" , fb.bits.mantissa); if (fb.bits.exponent == 0 && fb.bits.mantissa == 0 ) { printf ("特殊值: 零\n" ); } else if (fb.bits.exponent == 0xFF && fb.bits.mantissa == 0 ) { printf ("特殊值: 无穷大\n" ); } else if (fb.bits.exponent == 0xFF && fb.bits.mantissa != 0 ) { printf ("特殊值: NaN\n" ); } else if (fb.bits.exponent == 0 ) { printf ("特殊值: 非规范化数\n" ); } else { printf ("特殊值: 规范化数\n" ); } }
案例:字节序转换
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 union EndianConverter { uint32_t value; struct { uint8_t byte0; uint8_t byte1; uint8_t byte2; uint8_t byte3; } bytes; }; uint32_t swap_endian (uint32_t value) { union EndianConverter conv ; conv.value = value; uint8_t temp = conv.bytes.byte0; conv.bytes.byte0 = conv.bytes.byte3; conv.bytes.byte3 = temp; temp = conv.bytes.byte1; conv.bytes.byte1 = conv.bytes.byte2; conv.bytes.byte2 = temp; return conv.value; } void print_endianness () { union EndianConverter test ; test.value = 0x12345678 ; printf ("字节序测试: 0x%08X\n" , test.value); printf ("Byte 0: 0x%02X\n" , test.bytes.byte0); printf ("Byte 1: 0x%02X\n" , test.bytes.byte1); printf ("Byte 2: 0x%02X\n" , test.bytes.byte2); printf ("Byte 3: 0x%02X\n" , test.bytes.byte3); if (test.bytes.byte0 == 0x78 ) { printf ("系统字节序: 小端\n" ); } else if (test.bytes.byte0 == 0x12 ) { printf ("系统字节序: 大端\n" ); } else { printf ("系统字节序: 未知\n" ); } }
2.3.3 底层编程 共用体在底层编程中非常有用,特别是在访问硬件寄存器或处理二进制数据时。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 union Register { unsigned int value; struct { unsigned int bit0 : 1 ; unsigned int bit1 : 1 ; unsigned int bit2 : 1 ; unsigned int bit3 : 1 ; unsigned int bit4_7 : 4 ; unsigned int bit8_15 : 8 ; unsigned int bit16_31 : 16 ; } bits; }; union Register reg ;reg.value = 0x12345678 ; printf ("寄存器值:0x%08X\n" , reg.value);printf ("bit0: %d\n" , reg.bits.bit0);printf ("bit1: %d\n" , reg.bits.bit1);printf ("bit2: %d\n" , reg.bits.bit2);printf ("bit3: %d\n" , reg.bits.bit3);printf ("bit4_7: %d\n" , reg.bits.bit4_7);printf ("bit8_15: %d\n" , reg.bits.bit8_15);printf ("bit16_31: %d\n" , reg.bits.bit16_31);
底层编程的应用场景 :
硬件寄存器访问 :同时支持整体访问和位级访问网络协议解析 :解析和构建网络数据包文件格式处理 :处理二进制文件格式内存映射设备 :访问内存映射的硬件设备引导加载程序 :处理启动过程中的底层数据内核编程 :操作系统内核中的数据结构驱动程序开发 :硬件驱动程序中的寄存器操作高级应用技巧 :
共用体与结构体嵌套 :结合使用实现复杂的数据结构共用体数组 :创建类型变体数组共用体作为函数参数 :传递不同类型的数据共用体与宏结合 :简化位操作共用体与内联汇编 :实现高效的底层操作共用体与内存屏障 :确保内存操作的顺序性案例:网络协议解析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 union EthernetFrame { struct { uint8_t dest_mac[6 ]; uint8_t src_mac[6 ]; uint16_t ethertype; uint8_t payload[]; } fields; uint8_t raw[14 ]; }; union IPPacket { struct { uint8_t version_ihl; uint8_t tos; uint16_t total_length; uint16_t identification; uint16_t flags_fragment; uint8_t ttl; uint8_t protocol; uint16_t checksum; uint32_t src_ip; uint32_t dest_ip; uint8_t options[]; } fields; uint8_t raw[20 ]; }; void parse_packet (const uint8_t *data, size_t len) { if (len < 14 ) return ; union EthernetFrame eth ; memcpy (ð, data, 14 ); uint16_t ethertype = ntohs(eth.fields.ethertype); if (ethertype == 0x0800 && len >= 34 ) { union IPPacket ip ; memcpy (&ip, data + 14 , 20 ); printf ("Ethernet Type: 0x%04X (IPv4)\n" , ethertype); printf ("IP Version: %d\n" , ip.fields.version_ihl >> 4 ); printf ("IP Header Length: %d bytes\n" , (ip.fields.version_ihl & 0x0F ) * 4 ); printf ("Protocol: %d\n" , ip.fields.protocol); printf ("Source IP: %d.%d.%d.%d\n" , (ip.fields.src_ip >> 24 ) & 0xFF , (ip.fields.src_ip >> 16 ) & 0xFF , (ip.fields.src_ip >> 8 ) & 0xFF , ip.fields.src_ip & 0xFF ); printf ("Destination IP: %d.%d.%d.%d\n" , (ip.fields.dest_ip >> 24 ) & 0xFF , (ip.fields.dest_ip >> 16 ) & 0xFF , (ip.fields.dest_ip >> 8 ) & 0xFF , ip.fields.dest_ip & 0xFF ); } }
案例:内存映射设备访问
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 #define GPIO_BASE 0x40020000 #define GPIO_MODE_OFFSET 0x00 #define GPIO_OUTPUT_OFFSET 0x14 #define GPIO_INPUT_OFFSET 0x10 union GPIOModeReg { uint32_t value; struct { uint32_t mode0 : 2 ; uint32_t mode1 : 2 ; uint32_t mode2 : 2 ; uint32_t mode3 : 2 ; uint32_t mode4 : 2 ; uint32_t mode5 : 2 ; uint32_t mode6 : 2 ; uint32_t mode7 : 2 ; uint32_t reserved : 16 ; } bits; }; union GPIOOutputReg { uint32_t value; struct { uint32_t pin0 : 1 ; uint32_t pin1 : 1 ; uint32_t pin2 : 1 ; uint32_t pin3 : 1 ; uint32_t pin4 : 1 ; uint32_t pin5 : 1 ; uint32_t pin6 : 1 ; uint32_t pin7 : 1 ; uint32_t reserved : 24 ; } bits; }; void gpio_init (void ) { volatile uint32_t *gpio_base = (volatile uint32_t *)GPIO_BASE; volatile union GPIOModeReg *mode_reg = (volatile union GPIOModeReg *)(gpio_base + GPIO_MODE_OFFSET / 4 ); mode_reg->bits.mode0 = 0x1 ; volatile union GPIOOutputReg *output_reg = (volatile union GPIOOutputReg *)(gpio_base + GPIO_OUTPUT_OFFSET / 4 ); output_reg->bits.pin0 = 1 ; volatile union GPIOOutputReg *input_reg = (volatile union GPIOOutputReg *)(gpio_base + GPIO_INPUT_OFFSET / 4 ); printf ("Pin 1 state: %d\n" , input_reg->bits.pin1); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 union Packet { struct { uint8_t header; uint16_t length; uint32_t checksum; } fields; uint8_t raw[7 ]; }; union Packet pkt ;pkt.fields.header = 0x01 ; pkt.fields.length = 100 ; pkt.fields.checksum = 0x12345678 ; for (int i = 0 ; i < 7 ; i++) { printf ("0x%02X " , pkt.raw[i]); } printf ("\n" );union Variant { int i; float f; char *s; }; union Variant array [10]; array [0 ].i = 42 ;array [1 ].f = 3.14 ;array [2 ].s = "Hello" ;
共用体的性能考量 :
内存访问 :共用体的成员访问速度与普通变量相同内存使用 :显著节省内存,特别是当成员类型大小差异较大时缓存局部性 :由于共用体大小较小,通常具有较好的缓存局部性类型检查 :编译器不会检查共用体成员的访问合法性,需要程序员自己保证分支预测 :使用共用体的类型标签可以改善分支预测性能原子性 :位字段的访问和修改可能不是原子的,需要额外的同步措施编译优化 :编译器可以对共用体访问进行特殊优化,如常量传播和死代码消除最佳实践 :
类型安全 :始终使用类型标签(如 enum)来跟踪共用体当前存储的类型内存管理 :避免在不同大小的成员之间频繁切换,可能导致内存访问问题字节序处理 :注意字节序问题,特别是在处理网络数据或跨平台数据时代码可读性 :合理使用共用体,不要过度使用导致代码可读性下降性能优化 :对于性能关键代码,考虑使用位操作代替共用体访问边界检查 :为共用体添加边界检查,防止访问越界调试支持 :使用断言和调试宏来验证共用体的使用正确性文档化 :详细记录共用体的使用方式和注意事项共用体与现代 C 特性 :
C11 匿名共用体 :允许在结构体中使用匿名共用体C11 类型泛型 :结合共用体和 _Generic 实现类型安全的泛型函数C17 内存屏障 :确保共用体访问的内存一致性案例:C11 匿名共用体
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 typedef struct { int type; union { int i; float f; char *s; }; } GenericValue; void print_value (GenericValue v) { switch (v.type) { case 0 : printf ("Integer: %d\n" , v.i); break ; case 1 : printf ("Float: %.2f\n" , v.f); break ; case 2 : printf ("String: %s\n" , v.s); break ; default : printf ("Unknown type\n" ); break ; } } int main (void ) { GenericValue v; v.type = 0 ; v.i = 42 ; print_value(v); v.type = 1 ; v.f = 3.14 ; print_value(v); v.type = 2 ; v.s = "Hello" ; print_value(v); return 0 ; }
案例:C11 类型泛型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 typedef struct { enum { TYPE_INT, TYPE_FLOAT, TYPE_STRING } type; union { int i; float f; const char *s; } value; } Variant; #define print_variant(v) _Generic((v), \ int: print_int, \ float: print_float, \ const char *: print_string, \ Variant: print_variant_impl \ )(v) void print_int (int v) { printf ("Integer: %d\n" , v); } void print_float (float v) { printf ("Float: %.2f\n" , v); } void print_string (const char *v) { printf ("String: %s\n" , v); } void print_variant_impl (Variant v) { switch (v.type) { case TYPE_INT: print_int(v.value.i); break ; case TYPE_FLOAT: print_float(v.value.f); break ; case TYPE_STRING: print_string(v.value.s); break ; default : printf ("Unknown type\n" ); break ; } } int main (void ) { print_variant(42 ); print_variant(3.14f ); print_variant("Hello" ); Variant v; v.type = TYPE_INT; v.value.i = 100 ; print_variant(v); return 0 ; }
3. 枚举的深入理解 3.1 枚举的基本概念 枚举(Enumeration)是一种用户定义的整数类型,它由一组命名的常量组成。枚举提供了一种将整数常量与有意义的名称关联起来的方式,提高代码的可读性和可维护性。
枚举的本质 :
枚举在底层是整数类型,通常是 int 枚举常量是编译时常量,在编译时被替换为相应的整数值 枚举类型的变量存储的是整数值,而不是枚举常量的名称 3.2 枚举的声明和使用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 enum enum_name { constant1, constant2, }; enum Weekday { MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY }; enum Weekday today = WEDNESDAY;printf ("今天是星期 %d\n" , today); enum Color { RED = 1 , GREEN = 2 , BLUE = 4 }; enum Color c = GREEN;printf ("颜色值:%d\n" , c);
枚举的默认值规则 :
第一个枚举常量默认值为 0 后续枚举常量的默认值为前一个常量的值加 1 可以显式指定枚举常量的值,后续未指定的常量会基于该值递增 枚举的类型 :
在 C 中,枚举是整数类型,通常与 int 兼容 枚举变量的大小与 int 相同,除非编译器进行了优化 C99 允许使用 typedef 为枚举创建别名,提高代码可读性 3.3 枚举的高级应用 3.3.1 枚举的位掩码 枚举可以用于定义位掩码(Bit Mask),方便进行位运算,实现多个标志的组合。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 enum FilePermissions { READ = 0x01 , WRITE = 0x02 , EXECUTE = 0x04 , ALL = READ | WRITE | EXECUTE }; void check_permissions (int permissions) { if (permissions & READ) { printf ("有读权限\n" ); } if (permissions & WRITE) { printf ("有写权限\n" ); } if (permissions & EXECUTE) { printf ("有执行权限\n" ); } } int main (void ) { int permissions = READ | WRITE; check_permissions(permissions); permissions |= EXECUTE; check_permissions(permissions); permissions &= ~WRITE; check_permissions(permissions); return 0 ; }
位掩码的高级技巧 :
位掩码验证 :确保输入的位掩码只包含有效的标志位掩码转换 :在字符串和位掩码之间进行转换位掩码迭代 :遍历位掩码中的所有设置位位掩码计数 :计算位掩码中设置位的数量案例:位掩码工具函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 typedef enum { FLAG_A = 0x01 , FLAG_B = 0x02 , FLAG_C = 0x04 , FLAG_D = 0x08 } Flags; bool validate_flags (Flags flags) { return (flags & ~(FLAG_A | FLAG_B | FLAG_C | FLAG_D)) == 0 ; } int count_flags (Flags flags) { int count = 0 ; while (flags) { flags &= flags - 1 ; count++; } return count; } void iterate_flags (Flags flags, void (*callback)(Flags)) { Flags flag = 1 ; while (flag) { if (flags & flag) { callback(flag); } flag <<= 1 ; } } const char * flags_to_string (Flags flags) { static char buffer[64 ]; buffer[0 ] = '\0' ; if (flags & FLAG_A) strcat (buffer, "A " ); if (flags & FLAG_B) strcat (buffer, "B " ); if (flags & FLAG_C) strcat (buffer, "C " ); if (flags & FLAG_D) strcat (buffer, "D " ); size_t len = strlen (buffer); if (len > 0 ) buffer[len - 1 ] = '\0' ; return buffer; }
3.3.2 枚举与状态机 枚举非常适合用于实现状态机,使状态转换更加清晰和可维护。
案例:有限状态机
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 typedef enum { STATE_IDLE, STATE_INIT, STATE_RUNNING, STATE_PAUSED, STATE_ERROR, STATE_DONE } State; typedef enum { EVENT_START, EVENT_PAUSE, EVENT_RESUME, EVENT_STOP, EVENT_ERROR, EVENT_RESET } Event; State state_transition (State current_state, Event event) { switch (current_state) { case STATE_IDLE: switch (event) { case EVENT_START: return STATE_INIT; default : return current_state; } case STATE_INIT: switch (event) { case EVENT_START: return STATE_RUNNING; case EVENT_ERROR: return STATE_ERROR; default : return current_state; } case STATE_RUNNING: switch (event) { case EVENT_PAUSE: return STATE_PAUSED; case EVENT_STOP: return STATE_DONE; case EVENT_ERROR: return STATE_ERROR; default : return current_state; } case STATE_PAUSED: switch (event) { case EVENT_RESUME: return STATE_RUNNING; case EVENT_STOP: return STATE_DONE; default : return current_state; } case STATE_ERROR: switch (event) { case EVENT_RESET: return STATE_IDLE; default : return current_state; } case STATE_DONE: switch (event) { case EVENT_RESET: return STATE_IDLE; default : return current_state; } default : return current_state; } } void run_state_machine (void ) { State current_state = STATE_IDLE; printf ("初始状态: %d\n" , current_state); current_state = state_transition(current_state, EVENT_START); printf ("启动后状态: %d\n" , current_state); current_state = state_transition(current_state, EVENT_START); printf ("初始化后状态: %d\n" , current_state); current_state = state_transition(current_state, EVENT_PAUSE); printf ("暂停后状态: %d\n" , current_state); current_state = state_transition(current_state, EVENT_RESUME); printf ("恢复后状态: %d\n" , current_state); current_state = state_transition(current_state, EVENT_STOP); printf ("停止后状态: %d\n" , current_state); }
3.3.3 枚举与错误处理 枚举可以用于定义错误码,使错误处理更加清晰和可维护。
案例:错误码定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 typedef enum { ERROR_NONE = 0 , ERROR_INVALID_PARAM = 1 , ERROR_OUT_OF_MEMORY = 2 , ERROR_FILE_NOT_FOUND = 3 , ERROR_PERMISSION_DENIED = 4 , ERROR_NETWORK = 5 , ERROR_TIMEOUT = 6 } ErrorCode; const char * error_message (ErrorCode error) { switch (error) { case ERROR_NONE: return "无错误" ; case ERROR_INVALID_PARAM: return "无效参数" ; case ERROR_OUT_OF_MEMORY: return "内存不足" ; case ERROR_FILE_NOT_FOUND: return "文件未找到" ; case ERROR_PERMISSION_DENIED: return "权限被拒绝" ; case ERROR_NETWORK: return "网络错误" ; case ERROR_TIMEOUT: return "超时" ; default : return "未知错误" ; } } ErrorCode open_file (const char * filename, FILE** file) { if (!filename) { return ERROR_INVALID_PARAM; } *file = fopen(filename, "r" ); if (!*file) { if (errno == ENOENT) { return ERROR_FILE_NOT_FOUND; } else if (errno == EACCES) { return ERROR_PERMISSION_DENIED; } else { return ERROR_INVALID_PARAM; } } return ERROR_NONE; } int main (void ) { FILE* file; ErrorCode error = open_file("test.txt" , &file); if (error != ERROR_NONE) { printf ("打开文件失败: %s\n" , error_message(error)); return 1 ; } fclose(file); return 0 ; }
3.3.4 枚举与类型安全 使用枚举可以提高代码的类型安全性,减少错误的可能性。
案例:类型安全的枚举
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 typedef enum { RED, GREEN, BLUE } Color; void set_color (Color color) { switch (color) { case RED: printf ("设置颜色为红色\n" ); break ; case GREEN: printf ("设置颜色为绿色\n" ); break ; case BLUE: printf ("设置颜色为蓝色\n" ); break ; default : printf ("无效的颜色\n" ); break ; } } void set_color_unsafe (int color) { switch (color) { case RED: printf ("设置颜色为红色\n" ); break ; case GREEN: printf ("设置颜色为绿色\n" ); break ; case BLUE: printf ("设置颜色为蓝色\n" ); break ; default : printf ("无效的颜色\n" ); break ; } } int main (void ) { set_color(RED); set_color_unsafe(3 ); return 0 ; }
枚举的性能考量 :
编译期优化 :枚举常量在编译时被替换为整数值,无运行时开销内存使用 :枚举变量与整数变量大小相同,通常为 4 字节分支预测 :使用枚举的 switch 语句通常具有较好的分支预测性能类型检查 :编译器会对枚举类型进行类型检查,减少错误枚举的最佳实践 :
命名规范 :使用大写字母和下划线命名枚举常量值定义 :为枚举常量提供明确的值,避免依赖默认值范围检查 :对用户输入的枚举值进行范围检查文档化 :为枚举类型和常量添加注释,说明其用途类型安全 :使用 typedef 为枚举创建别名,提高类型安全性错误处理 :使用枚举定义错误码,使错误处理更加清晰位掩码使用 :对于需要组合的标志,使用位掩码枚举枚举与 #define 的比较 :
特性 枚举 #define 类型安全 是 否 作用域 枚举作用域 文件作用域 调试支持 调试器可以显示枚举常量名称 调试器显示整数值 类型检查 编译器进行类型检查 无类型检查 内存使用 与整数相同 无内存使用(编译期替换) 可读性 高 低 维护性 高 低
return 0;
}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 **位掩码的设计原则**: - 每个枚举常量的值应为 2 的幂(1, 2, 4, 8, ...) - 使用位或运算符(`|`)组合多个标志 - 使用位与运算符(`&`)检查某个标志是否设置 - 提供一个 `ALL` 常量表示所有标志的组合 - 可选:提供 `NONE` 常量表示无标志(值为 0) **位掩码的高级操作**: - **设置标志**:`flags |= FLAG` - **清除标志**:`flags &= ~FLAG` - **切换标志**:`flags ^= FLAG` - **检查标志**:`if (flags & FLAG)` #### 3.3.2 枚举与字符串的转换 枚举与字符串之间的转换是实际开发中常见的需求,可以编写辅助函数实现这一功能。 ```c // 枚举与字符串的转换 enum Color { RED, GREEN, BLUE, YELLOW, PURPLE, COLOR_COUNT }; // 颜色名称数组 const char *color_names[] = { "红色", "绿色", "蓝色", "黄色", "紫色" }; // 枚举转字符串 const char *color_to_string(enum Color color) { if (color >= 0 && color < COLOR_COUNT) { return color_names[color]; } return "未知颜色"; } // 字符串转枚举 enum Color string_to_color(const char *str) { for (int i = 0; i < COLOR_COUNT; i++) { if (strcmp(str, color_names[i]) == 0) { return (enum Color)i; } } return COLOR_COUNT; } int main(void) { enum Color c = RED; printf("枚举值:%d, 字符串:%s\n", c, color_to_string(c)); const char *str = "绿色"; enum Color c2 = string_to_color(str); printf("字符串:%s, 枚举值:%d\n", str, c2); return 0; }
枚举与字符串转换的最佳实践 :
使用与枚举常量顺序对应的字符串数组 提供一个额外的枚举常量表示枚举值的数量(如 COLOR_COUNT) 进行边界检查,避免数组越界 对于字符串转枚举,考虑使用哈希表提高查找效率 可以使用宏或代码生成工具自动生成转换函数 高级转换技巧 :
使用 X-Macros :通过宏定义自动生成枚举和对应的字符串数组使用哈希表 :对于大量枚举值,使用哈希表提高字符串转枚举的速度使用反射 :在支持反射的语言中,利用反射机制实现转换(C 中需要手动实现)3.4 枚举的优点 代码可读性 - 使用有意义的名称代替数字,使代码更易理解类型安全 - 编译器会检查枚举类型的使用,减少错误维护性 - 便于修改和扩展,只需修改枚举定义代码提示 - 编辑器可以提供枚举常量的代码提示,减少拼写错误常量值管理 - 集中管理相关的常量值,避免魔法数字3.5 枚举的高级特性 3.5.1 枚举的作用域 在 C 中,枚举常量的作用域与枚举类型的声明位置相同。在文件作用域声明的枚举,其常量在整个文件中可见;在函数作用域声明的枚举,其常量只在该函数内可见。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 enum GlobalEnum { GLOBAL_CONST1, GLOBAL_CONST2 }; void func () { enum LocalEnum { LOCAL_CONST1, LOCAL_CONST2 }; enum GlobalEnum g = GLOBAL_CONST1; enum LocalEnum l = LOCAL_CONST1; } void another_func () { enum GlobalEnum g = GLOBAL_CONST1; }
3.5.2 枚举的大小和对齐 枚举的大小通常与 int 相同,但在某些情况下,编译器可能会对其进行优化。
1 2 3 4 5 6 7 8 9 10 11 12 printf ("枚举大小:%zu 字节\n" , sizeof (enum Weekday)); typedef enum { SMALL_CONST1, SMALL_CONST2 } SmallEnum;
3.5.3 枚举的前向声明 C 允许对枚举进行前向声明,但需要指定枚举的大小。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 enum Direction ; enum Direction *dir_ptr ;enum Direction { NORTH, SOUTH, EAST, WEST }; enum Direction dir = NORTH;
3.6 枚举的实际应用案例 3.6.1 状态机实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 enum State { STATE_IDLE, STATE_READING, STATE_PROCESSING, STATE_WRITING, STATE_DONE }; typedef struct { enum State current_state ; int data; int result; } StateMachine; void process_state (StateMachine *sm) { switch (sm->current_state) { case STATE_IDLE: printf ("进入空闲状态\n" ); sm->current_state = STATE_READING; break ; case STATE_READING: printf ("进入读取状态\n" ); sm->data = 42 ; sm->current_state = STATE_PROCESSING; break ; case STATE_PROCESSING: printf ("进入处理状态\n" ); sm->result = sm->data * 2 ; sm->current_state = STATE_WRITING; break ; case STATE_WRITING: printf ("进入写入状态\n" ); printf ("处理结果:%d\n" , sm->result); sm->current_state = STATE_DONE; break ; case STATE_DONE: printf ("进入完成状态\n" ); break ; default : printf ("未知状态\n" ); sm->current_state = STATE_IDLE; break ; } }
3.6.2 错误码定义 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 enum ErrorCode { ERROR_NONE = 0 , ERROR_INVALID_PARAM = 1 , ERROR_OUT_OF_MEMORY = 2 , ERROR_FILE_NOT_FOUND = 3 , ERROR_PERMISSION_DENIED = 4 }; const char *error_messages[] = { "无错误" , "无效参数" , "内存不足" , "文件未找到" , "权限被拒绝" }; const char *get_error_message (enum ErrorCode code) { if (code >= 0 && code < sizeof (error_messages) / sizeof (error_messages[0 ])) { return error_messages[code]; } return "未知错误" ; } enum ErrorCode func (int param) { if (param < 0 ) { return ERROR_INVALID_PARAM; } return ERROR_NONE; }
3.6.3 配置选项 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 enum LogLevel { LOG_LEVEL_DEBUG, LOG_LEVEL_INFO, LOG_LEVEL_WARNING, LOG_LEVEL_ERROR, LOG_LEVEL_FATAL }; typedef struct { enum LogLevel log_level ; bool enable_verbose; bool enable_timestamp; } Config; Config default_config = { .log_level = LOG_LEVEL_INFO, .enable_verbose = false , .enable_timestamp = true }; void set_log_level (Config *config, enum LogLevel level) { config->log_level = level; }
3.7 枚举的最佳实践 使用有意义的名称 :枚举类型和常量的名称应清晰表达其用途避免魔法数字 :使用枚举常量代替直接使用数字合理组织枚举 :将相关的常量组织在同一个枚举中使用 typedef :为枚举创建别名,提高代码可读性提供边界检查 :在枚举与字符串转换时进行边界检查考虑位掩码 :对于需要组合的标志,使用位掩码枚举文档化枚举 :为枚举添加注释,说明其用途和值的含义保持一致性 :在整个项目中保持枚举的命名和使用风格一致4. 类型定义 使用 typedef 关键字为现有类型创建别名,提高代码的可读性、可维护性和可移植性。
4.1 基本用法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 typedef int Integer;typedef float Real;typedef struct { int x; int y; } Point; typedef int *IntPtr;typedef char *String;typedef int IntArray[10 ];typedef char StringArray[5 ][50 ];Integer age = 20 ; Real pi = 3.14 ; Point p = {10 , 20 }; IntPtr ptr = &age; String name = "Alice" ; IntArray numbers; for (int i = 0 ; i < 10 ; i++){ numbers[i] = i; } StringArray names = { "Alice" , "Bob" , "Charlie" , "David" , "Eve" };
typedef 的工作原理 :
typedef 不会创建新类型,只是为现有类型创建一个别名typedef 声明的类型别名在编译时被替换为原始类型typedef 可以嵌套使用,创建更复杂的类型别名与 #define 的区别 :
特性 typedef #define 作用 创建类型别名 简单文本替换 作用域 遵循 C 的作用域规则 从定义处到文件结束 类型安全 编译器会检查类型 无类型检查 处理复杂类型 更适合处理复杂类型(如函数指针) 处理复杂类型时容易出错
4.2 为复杂类型创建别名 4.2.1 为函数指针创建别名 函数指针别名可以大大提高代码的可读性,特别是在处理回调函数、函数表等场景时。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 typedef int (*Operation) (int , int ) ;typedef void (*Callback) (void *data) ;int add (int a, int b) { return a + b; } int subtract (int a, int b) { return a - b; } void print_callback (void *data) { printf ("回调函数被调用,数据:%s\n" , (char *)data); } int main (void ) { Operation op1 = add; Operation op2 = subtract; printf ("5 + 3 = %d\n" , op1(5 , 3 )); printf ("5 - 3 = %d\n" , op2(5 , 3 )); Callback cb = print_callback; cb("Hello" ); return 0 ; }
函数指针别名的高级应用 :
函数表 :使用函数指针数组实现多态行为回调机制 :注册和调用回调函数状态机 :使用函数指针表示状态处理函数插件系统 :动态加载和调用插件函数4.2.2 为结构体指针创建别名 为结构体指针创建别名可以简化代码,减少重复的 struct 关键字和指针符号。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 typedef struct Node { int data; struct Node *next ; } Node, *NodePtr; NodePtr create_node (int data) { NodePtr node = (NodePtr)malloc (sizeof (Node)); if (node != NULL ) { node->data = data; node->next = NULL ; } return node; } void free_list (NodePtr head) { while (head != NULL ) { NodePtr temp = head; head = head->next; free (temp); } } int main (void ) { NodePtr head = create_node(10 ); head->next = create_node(20 ); head->next->next = create_node(30 ); NodePtr current = head; while (current != NULL ) { printf ("%d " , current->data); current = current->next; } printf ("\n" ); free_list(head); return 0 ; }
结构体指针别名的优点 :
代码更简洁,减少视觉噪音 提高代码可读性,使意图更清晰 便于后续修改,只需修改 typedef 定义 可以同时为结构体和结构体指针创建别名 4.3 高级类型定义技巧 4.3.1 为复合类型创建别名 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 typedef int (*IntArrayPtr) [10];typedef int (*IntMatrixPtr) [5][10];typedef int (*(*OperationFactory)(int )) (int , int ) ;typedef struct { int (*add)(int , int ); int (*subtract)(int , int ); int (*multiply)(int , int ); int (*divide)(int , int ); } Calculator;
4.3.2 类型定义的作用域 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 typedef int GlobalInt;void func () { typedef int LocalInt; GlobalInt g = 10 ; LocalInt l = 20 ; } void another_func () { GlobalInt g = 10 ; }
4.3.3 类型定义与宏结合 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #ifdef _WIN32 typedef __int64 int64; #else typedef long long int64; #endif #define MAKE_ARRAY_TYPE(name, type, size) typedef type name[size] MAKE_ARRAY_TYPE(IntArray10, int , 10 ); MAKE_ARRAY_TYPE(FloatArray5, float , 5 ); IntArray10 arr = {1 , 2 , 3 , 4 , 5 };
4.4 类型定义的最佳实践 使用有意义的名称 - 别名应该能够反映类型的用途和语义保持一致性 - 在整个项目中使用一致的命名约定避免过度使用 - 不要为所有类型都创建别名,只在必要时使用注意作用域 - 类型定义的作用域与变量相同,通常在头文件中声明考虑可移植性 - 使用 typedef 隔离平台相关的类型差异文档化类型定义 - 为复杂的类型定义添加注释,说明其用途使用约定俗成的命名 - 对于指针类型,可以使用 Ptr 后缀;对于句柄类型,可以使用 Handle 后缀4.5 类型定义的实际应用 4.5.1 平台无关的类型定义 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 #ifdef _WIN32 typedef signed char int8; typedef short int16; typedef int int32; typedef __int64 int64; typedef unsigned char uint8; typedef unsigned short uint16; typedef unsigned int uint32; typedef unsigned __int64 uint64; #else typedef signed char int8; typedef short int16; typedef int int32; typedef long long int64; typedef unsigned char uint8; typedef unsigned short uint16; typedef unsigned int uint32; typedef unsigned long long uint64; #endif void process_data (const uint8 *buffer, int32 length) { }
4.5.2 函数指针表 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 typedef int (*Operation) (int , int ) ;enum OperationType { OP_ADD, OP_SUBTRACT, OP_MULTIPLY, OP_DIVIDE, OP_COUNT }; int add (int a, int b) { return a + b; } int subtract (int a, int b) { return a - b; } int multiply (int a, int b) { return a * b; } int divide (int a, int b) { if (b == 0 ) return 0 ; return a / b; } Operation operation_table[OP_COUNT] = { add, subtract, multiply, divide }; int perform_operation (enum OperationType op, int a, int b) { if (op >= 0 && op < OP_COUNT) { return operation_table[op](a, b); } return 0 ; }
4.5.3 不透明类型 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 typedef struct OpaqueType OpaqueType ;OpaqueType *create_opaque_type (void ) ; void destroy_opaque_type (OpaqueType *obj) ;void do_something (OpaqueType *obj, int value) ;int get_value (const OpaqueType *obj) ;struct OpaqueType { int value; }; OpaqueType *create_opaque_type (void ) { OpaqueType *obj = (OpaqueType *)malloc (sizeof (OpaqueType)); if (obj != NULL ) { obj->value = 0 ; } return obj; } void destroy_opaque_type (OpaqueType *obj) { if (obj != NULL ) { free (obj); } } void do_something (OpaqueType *obj, int value) { if (obj != NULL ) { obj->value = value; } } int get_value (const OpaqueType *obj) { if (obj != NULL ) { return obj->value; } return 0 ; }
4.6 类型定义的陷阱和注意事项 类型遮蔽 :局部类型定义会遮蔽全局类型定义,可能导致混淆指针类型别名 :使用指针类型别名时,需要注意优先级和结合性过度复杂的类型 :过于复杂的类型定义会降低代码可读性类型不一致 :在不同文件中为同一类型创建不同的别名,可能导致类型不匹配可移植性问题 :依赖于特定编译器或平台的类型定义可能影响可移植性避免陷阱的建议 :
保持类型定义简单明了 遵循一致的命名约定 为复杂类型添加详细注释 测试类型定义在不同平台上的行为 使用静态分析工具检查类型定义的正确性 5. 位域的深入应用 5.1 位域的基本概念 位域(Bit Field)是一种特殊的结构体成员,它允许指定成员占用的位数,从而精确控制内存使用并实现位级操作。
位域的本质 :
位域是结构体成员的一种特殊形式 位域占用的位数由程序员显式指定 位域通常用于存储布尔值、枚举值或其他小范围整数 位域的大小不能超过其基础类型的大小 5.2 位域的声明和使用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 struct Flags { unsigned int flag1 : 1 ; unsigned int flag2 : 2 ; unsigned int flag3 : 3 ; unsigned int : 2 ; unsigned int flag4 : 8 ; }; struct Flags f ;f.flag1 = 1 ; f.flag2 = 3 ; f.flag3 = 5 ; f.flag4 = 255 ; printf ("flag1: %d\n" , f.flag1);printf ("flag2: %d\n" , f.flag2);printf ("flag3: %d\n" , f.flag3);printf ("flag4: %d\n" , f.flag4);printf ("结构体大小:%zu 字节\n" , sizeof (struct Flags));
位域的声明规则 :
位域的类型必须是整数类型(int、unsigned int、signed int 等) 位域的位数由冒号后的整数指定 未命名的位域用于填充和对齐 位域的位数不能为零,也不能超过其基础类型的大小 位域的访问规则 :
位域可以像普通结构体成员一样访问和修改 位域的值会被自动截断到指定的位数 对位域的赋值会进行边界检查,超出范围的值会被截断 5.3 位域的应用 5.3.1 节省内存 当需要存储多个布尔值或小整数时,使用位域可以显著节省内存空间。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 struct PersonInfo { unsigned int is_student : 1 ; unsigned int is_employee : 1 ; unsigned int is_married : 1 ; unsigned int gender : 1 ; unsigned int age : 7 ; }; printf ("结构体大小:%zu 字节\n" , sizeof (struct PersonInfo)); struct PersonInfo info ;info.is_student = 1 ; info.is_employee = 0 ; info.is_married = 0 ; info.gender = 1 ; info.age = 25 ; printf ("是否学生:%d\n" , info.is_student);printf ("是否员工:%d\n" , info.is_employee);printf ("是否已婚:%d\n" , info.is_married);printf ("性别:%s\n" , info.gender ? "女" : "男" );printf ("年龄:%d\n" , info.age);
内存节省分析 :
不使用位域:需要至少 5 个字节(每个成员 1 个字节) 使用位域:只需要 2 个字节(1+1+1+1+7 = 11 位,向上对齐到 2 字节) 对于大量数据结构(如数组或链表),内存节省效果更加显著 5.3.2 硬件编程 位域在硬件编程中非常有用,特别是在访问和控制硬件寄存器时。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 struct UART_Registers { unsigned int data : 8 ; unsigned int parity : 2 ; unsigned int stop_bits : 2 ; unsigned int baud_rate : 4 ; }; #define UART_BASE 0x1000 #define UART_REG ((struct UART_Registers *)UART_BASE) void init_uart (void ) { UART_REG->data = 0x41 ; UART_REG->parity = 0 ; UART_REG->stop_bits = 1 ; UART_REG->baud_rate = 3 ; }
硬件编程的优势 :
直接映射 :位域可以直接映射硬件寄存器的位布局可读性 :使用命名位域比使用位掩码更易读安全性 :编译器会检查位域的范围,减少错误可维护性 :位域定义清晰地表达了寄存器的结构5.3.3 协议实现 位域常用于实现网络协议或文件格式中的位级字段,简化协议解析和构建。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 struct IP_Header { unsigned int version : 4 ; unsigned int header_length : 4 ; unsigned int tos : 8 ; unsigned int total_length : 16 ; unsigned int identification : 16 ; unsigned int flags : 3 ; unsigned int fragment_offset : 13 ; unsigned int ttl : 8 ; unsigned int protocol : 8 ; unsigned int checksum : 16 ; unsigned int source_ip : 32 ; unsigned int destination_ip : 32 ; }; void parse_ip_header (const unsigned char *data) { struct IP_Header *header = (struct IP_Header *)data; printf ("IP 版本:%d\n" , header->version); printf ("头部长度:%d\n" , header->header_length); printf ("总长度:%d\n" , header->total_length); printf ("TTL:%d\n" , header->ttl); printf ("协议:%d\n" , header->protocol); }
协议实现的优势 :
结构清晰 :位域定义直观地反映了协议的位布局解析简单 :无需手动计算位偏移和掩码构建方便 :可以直接设置位域值来构建协议数据包易于维护 :协议变更时只需修改位域定义5.4 位域的高级特性 5.4.1 位域的内存布局 位域在内存中的布局依赖于编译器和平台,通常遵循以下规则:
位域打包 :位域通常被打包到同一个存储单元中,直到该单元被填满存储单元大小 :存储单元的大小由位域的基础类型决定位域顺序 :位域在存储单元中的顺序(从高位到低位或从低位到高位)依赖于编译器对齐 :位域结构体的对齐方式与普通结构体相同1 2 3 4 5 6 7 8 9 10 struct BitFieldLayout { unsigned int a : 1 ; unsigned int b : 2 ; unsigned int c : 3 ; unsigned int d : 2 ; }; printf ("BitFieldLayout 大小:%zu 字节\n" , sizeof (struct BitFieldLayout));
5.4.2 位域的类型和符号 位域的类型可以是有符号或无符号整数:
无符号位域 :所有位都用于存储值,范围是 0 到 2^n - 1有符号位域 :最高位用作符号位,范围是 -2^(n-1) 到 2^(n-1) - 11 2 3 4 5 6 7 8 9 10 11 struct SignedUnsignedBits { signed int s : 3 ; unsigned int u : 3 ; }; struct SignedUnsignedBits bits ;bits.s = 3 ; bits.u = 7 ;
5.4.3 位域与联合 位域可以与联合结合使用,实现对同一内存区域的不同位级访问方式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 union RegisterAccess { uint32_t value; struct { uint32_t reserved : 24 ; uint32_t status : 4 ; uint32_t enable : 1 ; uint32_t interrupt : 1 ; uint32_t error : 2 ; } bits; }; union RegisterAccess reg ;reg.value = 0x12345678 ; printf ("状态:%d\n" , reg.bits.status);printf ("使能:%d\n" , reg.bits.enable);reg.bits.enable = 1 ; reg.bits.status = 0x0F ; printf ("修改后的值:0x%08X\n" , reg.value);
5.5 位域的性能考量 5.5.1 内存使用 优势 :位域显著减少内存使用,特别是对于包含多个小值的结构体适用场景 :内存受限的环境(如嵌入式系统),或需要存储大量数据结构的场景5.5.2 访问速度 劣势 :位域的访问速度通常比普通成员稍慢,因为需要进行位操作原因 :访问位域时,编译器需要生成额外的位掩码和移位操作影响因素 :位域的大小、位置和基础类型都会影响访问速度5.5.3 可移植性 劣势 :位域的内存布局依赖于编译器和平台,可能影响可移植性差异 :不同编译器可能有不同的位域打包策略和字节序处理解决方案 :对于需要高度可移植的代码,考虑使用显式的位掩码和移位操作5.6 位域的最佳实践 使用无符号类型 :无符号位域避免了符号扩展问题,行为更加可预测合理安排位域顺序 :将相关的位域放在一起,减少填充使用适当的基础类型 :根据位域的总位数选择合适的基础类型添加注释 :为位域添加详细注释,说明其用途和取值范围考虑可移植性 :对于需要跨平台的代码,谨慎使用位域或提供替代实现测试边界情况 :测试位域的边界值和溢出情况权衡内存与性能 :在内存使用和访问速度之间进行权衡5.7 位域的替代方案 5.7.1 位掩码 对于需要高度可移植的代码,可以使用位掩码替代位域:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #define FLAG1_MASK 0x01 #define FLAG2_MASK 0x06 #define FLAG3_MASK 0x38 unsigned int flags = 0 ;flags |= FLAG1_MASK; flags |= (3 << 1 ); flags &= ~FLAG1_MASK; if (flags & FLAG1_MASK) { } int flag2_value = (flags & FLAG2_MASK) >> 1 ;
5.7.2 枚举和位操作 结合枚举和位操作,可以实现类型安全的位标志:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 enum Flags { FLAG_NONE = 0 , FLAG1 = 1 << 0 , FLAG2 = 1 << 1 , FLAG3 = 1 << 2 , FLAG4 = 1 << 3 , FLAG_ALL = FLAG1 | FLAG2 | FLAG3 | FLAG4 }; enum Flags flags = FLAG1 | FLAG3;if (flags & FLAG1) { } flags |= FLAG2; flags &= ~FLAG3; flags ^= FLAG4;
5.8 位域的实际应用案例 5.8.1 硬件寄存器映射 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 struct GPIO_Reg { unsigned int pin0 : 1 ; unsigned int pin1 : 1 ; unsigned int pin2 : 1 ; unsigned int pin3 : 1 ; unsigned int pin4 : 1 ; unsigned int pin5 : 1 ; unsigned int pin6 : 1 ; unsigned int pin7 : 1 ; }; #define GPIO_BASE 0x80000000 #define GPIO_PORT ((struct GPIO_Reg *)GPIO_BASE) GPIO_PORT->pin0 = 1 ; GPIO_PORT->pin1 = 0 ; if (GPIO_PORT->pin2) { printf ("pin2 为高电平\n" ); } else { printf ("pin2 为低电平\n" ); }
5.8.2 压缩数据存储 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 struct CompressedData { unsigned int type : 2 ; unsigned int length : 6 ; unsigned int value : 24 ; }; struct CompressedData data ;data.type = 0 ; data.length = 4 ; data.value = 12345 ; printf ("数据类型:%d\n" , data.type);printf ("数据长度:%d\n" , data.length);printf ("数据值:%d\n" , data.value);printf ("CompressedData 大小:%zu 字节\n" , sizeof (struct CompressedData));
5.8.3 网络协议解析 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 struct TCP_Header { unsigned int src_port : 16 ; unsigned int dst_port : 16 ; unsigned int seq_num : 32 ; unsigned int ack_num : 32 ; unsigned int data_offset : 4 ; unsigned int reserved : 3 ; unsigned int flags : 9 ; unsigned int window_size : 16 ; unsigned int checksum : 16 ; unsigned int urgent_ptr : 16 ; }; void parse_tcp_header (const unsigned char *data) { struct TCP_Header *header = (struct TCP_Header *)data; printf ("源端口:%d\n" , header->src_port); printf ("目标端口:%d\n" , header->dst_port); printf ("序列号:%u\n" , header->seq_num); printf ("确认号:%u\n" , header->ack_num); printf ("数据偏移:%d\n" , header->data_offset); printf ("窗口大小:%d\n" , header->window_size); }
6. 复合数据类型的性能考虑 6.1 结构体的性能 内存访问 - 结构体成员的访问速度与普通变量相同函数参数 - 传递大型结构体时,使用指针可以避免复制开销内存对齐 - 合理安排成员顺序可以减少内存浪费1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 struct BadStruct { char c; int i; char d; }; struct GoodStruct { int i; char c; char d; }; printf ("BadStruct 大小:%zu 字节\n" , sizeof (struct BadStruct)); printf ("GoodStruct 大小:%zu 字节\n" , sizeof (struct GoodStruct));
6.2 共用体的性能 内存访问 - 共用体成员的访问速度与普通变量相同内存使用 - 共用体可以节省内存,特别是当不同类型的数据不需要同时使用时6.3 位域的性能 内存使用 - 位域可以显著节省内存访问速度 - 位域的访问速度可能比普通成员稍慢,因为需要进行位运算7. 复合数据类型的高级应用示例 7.1 示例 1:学生管理系统 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 #include <stdio.h> #include <string.h> #include <stdlib.h> typedef struct { char name[50 ]; int age; float scores[3 ]; float average; } Student; void calculate_average (Student *s) { float sum = 0 ; for (int i = 0 ; i < 3 ; i++) { sum += s->scores[i]; } s->average = sum / 3 ; } void print_student (const Student *s) { printf ("姓名:%s\n" , s->name); printf ("年龄:%d\n" , s->age); printf ("分数:%.2f %.2f %.2f\n" , s->scores[0 ], s->scores[1 ], s->scores[2 ]); printf ("平均分:%.2f\n\n" , s->average); } void sort_students (Student *students, int count) { for (int i = 0 ; i < count - 1 ; i++) { for (int j = 0 ; j < count - i - 1 ; j++) { if (students[j].average < students[j + 1 ].average) { Student temp = students[j]; students[j] = students[j + 1 ]; students[j + 1 ] = temp; } } } } int main (void ) { Student students[] = { {"Alice" , 18 , {95.5 , 88.0 , 92.5 }, 0 }, {"Bob" , 19 , {82.0 , 76.5 , 88.5 }, 0 }, {"Charlie" , 18 , {90.0 , 94.5 , 89.0 }, 0 }, {"David" , 19 , {78.5 , 82.0 , 85.5 }, 0 }, {"Eve" , 18 , {92.0 , 88.5 , 90.5 }, 0 } }; int num_students = sizeof (students) / sizeof (students[0 ]); for (int i = 0 ; i < num_students; i++) { calculate_average(&students[i]); } sort_students(students, num_students); printf ("按平均分排序后的学生信息:\n\n" ); for (int i = 0 ; i < num_students; i++) { printf ("排名 %d:\n" , i + 1 ); print_student(&students[i]); } return 0 ; }
7.2 示例 2:使用枚举和结构体实现状态机 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 #include <stdio.h> enum State { STATE_IDLE, STATE_READING, STATE_PROCESSING, STATE_WRITING, STATE_DONE }; const char *state_names[] = { "空闲" , "读取" , "处理" , "写入" , "完成" }; typedef struct { enum State current_state ; int data; int result; } StateMachine; void init_state_machine (StateMachine *sm) { sm->current_state = STATE_IDLE; sm->data = 0 ; sm->result = 0 ; } void process_state_machine (StateMachine *sm) { switch (sm->current_state) { case STATE_IDLE: printf ("状态:%s\n" , state_names[sm->current_state]); sm->current_state = STATE_READING; break ; case STATE_READING: printf ("状态:%s\n" , state_names[sm->current_state]); sm->data = 42 ; sm->current_state = STATE_PROCESSING; break ; case STATE_PROCESSING: printf ("状态:%s\n" , state_names[sm->current_state]); sm->result = sm->data * 2 ; sm->current_state = STATE_WRITING; break ; case STATE_WRITING: printf ("状态:%s\n" , state_names[sm->current_state]); printf ("处理结果:%d\n" , sm->result); sm->current_state = STATE_DONE; break ; case STATE_DONE: printf ("状态:%s\n" , state_names[sm->current_state]); break ; default : printf ("未知状态\n" ); sm->current_state = STATE_IDLE; break ; } } int main (void ) { StateMachine sm; init_state_machine(&sm); while (sm.current_state != STATE_DONE) { process_state_machine(&sm); } return 0 ; }
7.3 示例 3:使用共用体和结构体实现变体类型 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 #include <stdio.h> #include <string.h> #include <stdlib.h> enum ValueType { TYPE_INT, TYPE_FLOAT, TYPE_STRING, TYPE_BOOL }; typedef struct { enum ValueType type ; union { int i; float f; char *s; int b; } data; } Variant; Variant create_int_variant (int value) { Variant v; v.type = TYPE_INT; v.data.i = value; return v; } Variant create_float_variant (float value) { Variant v; v.type = TYPE_FLOAT; v.data.f = value; return v; } Variant create_string_variant (const char *value) { Variant v; v.type = TYPE_STRING; v.data.s = strdup(value); return v; } Variant create_bool_variant (int value) { Variant v; v.type = TYPE_BOOL; v.data.b = value; return v; } void destroy_variant (Variant *v) { if (v->type == TYPE_STRING) { free (v->data.s); } } void print_variant (const Variant *v) { switch (v->type) { case TYPE_INT: printf ("整型:%d\n" , v->data.i); break ; case TYPE_FLOAT: printf ("浮点型:%.2f\n" , v->data.f); break ; case TYPE_STRING: printf ("字符串:%s\n" , v->data.s); break ; case TYPE_BOOL: printf ("布尔型:%s\n" , v->data.b ? "真" : "假" ); break ; default : printf ("未知类型\n" ); break ; } } int main (void ) { Variant v1 = create_int_variant(42 ); Variant v2 = create_float_variant(3.14 ); Variant v3 = create_string_variant("Hello, World!" ); Variant v4 = create_bool_variant(1 ); print_variant(&v1); print_variant(&v2); print_variant(&v3); print_variant(&v4); destroy_variant(&v3); return 0 ; }
8. 小结 本章深入介绍了 C 语言中的复合数据类型,包括结构体、共用体、枚举、类型定义和位域。这些复合数据类型允许我们创建更复杂的数据结构,以适应各种编程需求。
8.1 关键知识点 结构体 :一种复合数据类型,可以包含不同类型的成员变量,成员在内存中连续存储共用体 :一种特殊的数据类型,所有成员共享同一块内存,大小等于最大成员的大小枚举 :一种用户定义的整数类型,由一组命名的常量组成类型定义 :使用 typedef 关键字为现有类型创建别名,提高代码的可读性和可维护性位域 :一种特殊的结构体成员,允许指定成员占用的位数,从而节省内存空间8.2 学习建议 多写代码 :通过实际编程练习掌握复合数据类型的使用理解内存布局 :理解结构体、共用体和位域的内存布局,有助于编写更高效的代码注意内存管理 :使用动态内存分配时,要注意及时释放内存,避免内存泄漏遵循最佳实践 :合理使用复合数据类型,提高代码的可读性和可维护性学习设计模式 :学习如何使用复合数据类型实现常见的设计模式,如状态模式、策略模式等复合数据类型是 C 语言编程的重要组成部分,掌握好这些数据类型对于编写高质量的 C 程序至关重要。通过本章的学习,希望读者能够深入理解复合数据类型的概念,掌握它们的使用方法,以及编写更加高效、安全的 C 程序。
在后续章节中,我们将学习内存管理、文件输入/输出等高级主题,这些内容将进一步扩展你的 C 语言编程能力。