一、结构体类型的声明
1.1 结构的声明
结构体是一种自定义的数据类型,允许将不同类型的数据组合成一个整体。声明语法如下:
struct 结构体名 {
数据类型 成员1;
数据类型 成员2;
// ...
};
示例:
struct Student {
char name[20];
int age;
float score;
};
1.2 结构体变量的创建和初始化
- 创建方式:
struct Student stu1; // 先声明类型,后定义变量
struct { int x; int y; } point; // 匿名结构体
初始化方式:
struct Student stu2 = {"张三", 18, 90.5};
struct Student stu3 = { .age = 20, .name = "李四",.score = 85.0}; // C99指定初始化器
1.3 结构的特殊声明
匿名结构体可以直接定义变量,但无法重复使用:
struct {
int a;
char b;
} anon_var;
1.4 结构的自引用
用于构建链表等复杂结构:
struct Node {
int data;
struct Node* next; // 正确方式
};
错误示例:
struct Node {
int data;
Node* next; // 错误:未定义类型Node
};
二、结构体内存对齐
2.1 对齐规则
规则 1:结构体的第一个成员对齐到和结构体变量起始位置偏移量为 0 的地址处
可以把结构体想象成一个大箱子,这个箱子从地址 0 开始摆放。结构体的第一个成员就像是第一个要放进箱子的物品,它会直接放在箱子的最开始位置,也就是地址 0 处,不需要考虑其他对齐因素。
struct Example1 {
char c; // 第一个成员,直接放在起始地址 0 处
int i;
};
规则 2:其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。对齐数 = 编译器默认的一个对齐数与该成员变量大小的较小值
- 对齐数的计算:对于结构体中除第一个成员之外的其他成员,需要先计算它们的对齐数。对齐数是由编译器默认的对齐数和成员自身大小这两个值中较小的那个决定的。不同的编译器默认对齐数可能不同,例如在 Visual Studio(VS)中默认值为 8,而在 Linux 的 gcc 编译器中没有默认对齐数,此时对齐数就是成员自身的大小。
- 成员放置位置:计算出对齐数后,该成员就要放在这个对齐数的整数倍的地址处。如果当前地址不是对齐数的整数倍,就需要在前面填充一些字节,直到达到对齐数的整数倍。
示例:
#include <stdio.h>
struct Example2 {
char c; // 第一个成员,放在地址 0 处
int i; // 成员大小为 4 字节,VS 中默认对齐数为 8,对齐数取较小值 4
// 由于 char 占 1 字节,当前地址 1 不是 4 的整数倍,需要填充 3 字节
// 所以 i 从地址 4 开始存放
};
int main() {
printf("Size of Example2: %zu\n", sizeof(struct Example2));
return 0;
}
在这个例子中,char
类型的 c
放在地址 0 处,int
类型的 i
对齐数为 4,因为当前地址 1 不是 4 的整数倍,所以要在 c
后面填充 3 个字节,i
从地址 4 开始存放。
规则 3:结构体总大小为最大对齐数(结构体中每个成员变量都有一个对齐数,所有对齐数中最大的)的整数倍
结构体的所有成员都按照前面的规则放置好后,结构体整体占用的内存大小并不是成员实际占用字节数的简单相加,而是要保证结构体的总大小是所有成员对齐数中最大那个对齐数的整数倍。如果实际占用的内存大小不是最大对齐数的整数倍,就需要在结构体的末尾填充一些字节,使其达到最大对齐数的整数倍。
示例:
#include <stdio.h>
struct Example3 {
char c; // 对齐数为 1,放在地址 0 处
short s; // 对齐数为 2,由于 c 占 1 字节,当前地址 1 不是 2 的整数倍,填充 1 字节,s 从地址 2 开始存放
int i; // 对齐数为 4,s 占 2 字节,当前地址 4 是 4 的整数倍,i 从地址 4 开始存放
};
int main() {
printf("Size of Example3: %zu\n", sizeof(struct Example3));
return 0;
}
在这个结构体中,char
的对齐数是 1,short
的对齐数是 2,int
的对齐数是 4,最大对齐数是 4。成员实际占用的字节数是 1(c
) + 1(填充)+ 2(s
) + 4(i
) = 8 字节,8 是 4 的整数倍,所以结构体的总大小就是 8 字节。
规则 4:如果嵌套了结构体的情况,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍
当结构体中嵌套了另一个结构体时,嵌套的结构体就像一个 “小箱子”,这个 “小箱子” 要放在合适的位置。它的起始地址要对齐到它自己内部成员中最大对齐数的整数倍处。计算整个结构体的总大小的时候,要把嵌套结构体内部成员的对齐数也考虑进来,最终结构体的整体大小要保证是所有最大对齐数(包括嵌套结构体成员的对齐数)的整数倍。
示例:
#include <stdio.h>
struct Inner {
char c; // 对齐数为 1
int i; // 对齐数为 4,最大对齐数是 4
};
struct Example4 {
char c1; // 对齐数为 1,放在地址 0 处
struct Inner in; // 嵌套结构体,最大对齐数是 4,当前地址 1 不是 4 的整数倍,填充 3 字节,in 从地址 4 开始存放
short s; // 对齐数为 2,in 占 8 字节(1 + 3 填充 + 4),当前地址 12 是 2 的整数倍,s 从地址 12 开始存放
};
int main() {
printf("Size of Example4: %zu\n", sizeof(struct Example4));
return 0;
}
在这个例子中,嵌套结构体 Inner
的最大对齐数是 4,所以 Inner
要对齐到 4 的整数倍地址处。整个结构体 Example4
的所有成员最大对齐数也是 4,最终结构体的总大小要保证是 4 的整数倍。经过计算和填充,结构体 Example4
的总大小是 14 字节(1 + 3 填充 + 8 + 2),刚好是 4 的整数倍,所以最终大小就是 14 字节。
2.2 为什么存在内存对齐?
1. 硬件效率:按块读取内存
现代 CPU 读取内存是一块一块来的,就像从书架上拿书,一次拿一摞(比如 4 本或 8 本)。要是数据没对齐,就像书没摆好,CPU 得多次伸手去拿,才能凑齐想要的数据,效率低。而数据对齐后,CPU 一次就能拿到完整的数据,速度快多了。
2. 兼容性:不同平台要求不同
不同的硬件平台,就像不同的书架,对书的摆放要求不一样。有些书架要求书必须按顺序一本本对齐放,要是放乱了,就拿不出来或者拿错。所以程序在不同硬件上跑,数据对齐得符合人家的要求,不然就可能出错。
3. 性能优化:减少访问次数
内存访问就像去书架找书,次数多了很麻烦。合理对齐数据,能让 CPU 一次拿到更多有用的数据,就像一次能拿一摞需要的书,不用来回跑好几趟,程序自然就跑得快啦。
2.3 修改默认对齐数
使用#pragma pack()
指令:
#pragma pack(2) // 设置对齐数为2
struct Test {
char a; // 1字节,起始地址0
int b; // 4字节 → 按2对齐,起始地址2
}; // 总大小:6字节(2+4=6)
#pragma pack() // 恢复默认对齐
三、结构体传参
- 值传递:
void print_stu(struct Student s) { ... } // 效率低,复制整个结构体
-
指针(地址)传递:
void print_stu(const struct Student* s) { ... } // 推荐方式
注意:指针传递需确保指针有效,避免野指针问题
四、结构体实现位段
4.1 什么是位段
在 C 语言里,有些数据所需存储空间极小,像表示开关状态(开或关),1 位二进制数(0 或 1)就足够;表示 8 种不同等级,用 3 位二进制数(能表示 0 - 7)就行。但基本数据类型如 char
占 1 字节(8 位),int
一般占 4 字节(32 位),用它们存储这类数据会浪费内存。
位段可解决此问题,它允许在结构体中精准指定每个成员使用的二进制位数,从而高效利用内存。示例如下:
#include <stdio.h>
struct MyFlags {
unsigned int is_open : 1; // 1 位表示开关状态
unsigned int level : 3; // 3 位表示 8 种等级
unsigned int mode : 2; // 2 位表示 4 种模式
};
int main() {
struct MyFlags flags;
flags.is_open = 1;
flags.level = 5;
flags.mode = 2;
printf("is_open: %u\n", flags.is_open);
printf("level: %u\n", flags.level);
printf("mode: %u\n", flags.mode);
return 0;
}
4.2 位段的内存分配
按类型分配
位段通常按 int
、unsigned int
或 signed int
类型分配内存,常见系统中 int
占 4 字节(32 位)。
依次存放规则
位段成员在内存中依次存放。若前面位段成员占用位数与当前位段成员要占用的位数之和,未超过当前分配的内存块(通常 32 位),则当前位段成员接着分配;若超过,则从下一个内存块开始分配。
未命名位段用途
未命名位段可作 “占位符”,调整后续位段成员的起始位置,灵活控制内存使用。示例:
#include <stdio.h>
struct BitFieldExample {
unsigned int part1 : 5;
unsigned int part2 : 3;
unsigned int : 2;
unsigned int part3 : 4;
};
int main() {
printf("Size of BitFieldExample: %zu bytes\n", sizeof(struct BitFieldExample));
return 0;
}
该结构体中,各成员位段总和未超 32 位,所以通常占 4 字节。
4.3 位段的跨平台问题
存储顺序差异
不同编译器处理位段时,位段在内存中的存储顺序可能不同,有的从低位开始分配,有的从高位开始,这会使相同代码在不同编译器下,位段成员存储位置有差异。
长度限制不同
不同平台和编译器对位段成员长度限制有别,部分编译器不允许位段成员位数超特定值。
负数处理不同
有符号位段在不同编译器处理负数的方式可能不同,导致代码在不同平台运行结果有差异。
4.4 位段的应用
网络协议解析
网络协议头部包含众多标志位和状态信息,用很少位数就能表示,位段可方便解析处理这些头部信息。例如 IPv4 协议头部的 4 位版本号和 4 位首部长度:
#include <stdio.h>
struct IPv4Header {
unsigned int version : 4;
unsigned int header_length : 4;
};
int main() {
unsigned char header_data = 0x45;
struct IPv4Header *ip_header = (struct IPv4Header *)&header_data;
printf("Version: %u\n", ip_header->version);
printf("Header Length: %u\n", ip_header->header_length);
return 0;
}
设备寄存器控制
嵌入式系统开发中,常与硬件设备寄存器交互,寄存器很多位有特定含义,用于控制设备功能,位段可方便操作寄存器各位。
节省内存
在嵌入式系统或资源受限设备中,位段能精确控制数据占用位数,节省大量内存,提升程序运行效率。
4.5 位段使用的注意事项
类型要求
位段类型必须是 int
、unsigned int
或 signed int
,其他类型(如 char
、float
)不可用。
不能取地址
不能使用 &
运算符获取位段成员地址,因为位段成员可能只占字节部分位,无独立内存地址。
跨平台兼容性
不同编译器和平台处理位段有差异,编写代码时要留意跨平台兼容性,充分测试。
避免位数溢出
给位段成员赋值时,不能超出其表示范围,否则会溢出,导致不可预期结果。例如:
#include <stdio.h>
struct WrongUsage {
unsigned int small_num : 2;
};
int main() {
struct WrongUsage wu;
wu.small_num = 5; // 2 位位段只能表示 0 - 3,赋值 5 会溢出
printf("small_num: %u\n", wu.small_num);
return 0;
}
五、扩展知识
5.1 柔性数组成员
用于动态数组:
struct Array {
int len;
int data[]; // 柔性数组成员
};
// 动态分配:
struct Array* arr = malloc(sizeof(struct Array) + 10*sizeof(int));
5.2 结构体与枚举的结合
typedef enum { MALE, FEMALE } Gender;
struct Person {
char name[20];
Gender sex;
};
5.3 结构体常用操作
- 结构体比较:逐个成员比较
- 结构体复制:使用
memcpy()
或直接赋值(C99 支持) - 结构体打印:自定义格式化输出函数
六、常见问题解答
-
结构体可以包含自身类型吗?
- 不能直接包含,但可以包含指针(自引用)
-
内存对齐会浪费空间吗?
- 是的,但这是空间与时间的权衡,现代编译器会优化
-
位段能跨字节边界吗?
- 取决于编译器,可能导致不可移植性
七、总结
结构体是 C 语言中最重要的复合数据类型之一,掌握内存对齐规则和位段技术能显著提升程序性能。在实际开发中,应根据场景选择合适的结构体设计方式,同时注意跨平台兼容性问题。