在 C 语言中,我们经常需要描述由不同类型数据组成的复杂对象 —— 比如一个学生的信息包含字符串(姓名)、整数(年龄)和浮点数(成绩);一个点的坐标需要两个整数(x 和 y)。如果用零散的变量分别表示这些属性,不仅难以管理,还会失去数据之间的关联性。
这时候,结构体(struct) 就成了绝佳工具!它允许我们将多个不同类型的数据 “打包” 成一个整体,形成一个新的自定义类型,让代码更贴近现实世界的实体描述。
一、什么是结构体?
结构体(struct)是 C 语言中一种复合数据类型,它的核心作用是:将多个不同类型的数据成员(member)组合在一起,形成一个有逻辑关联的整体。
✅ 定义一个结构体
struct Student {
char name[20]; // 姓名(字符数组类型)
int age; // 年龄(整型)
float score; // 成绩(浮点型)
};
上面的代码定义了一个名为Student的结构体类型,它包含三个不同类型的成员:
name:用于存储姓名的字符数组(长度 20)age:用于存储年龄的整数score:用于存储成绩的浮点数
🔔 注意:
结构体定义本身只是创建了一个 “数据模板”,不会分配内存空间。只有当我们用这个模板创建变量时,才会真正占用内存。
二、结构体变量的创建与初始化
定义结构体类型后,我们可以像使用内置类型(如int、float)一样创建变量,并为其成员赋值。
1. 创建结构体变量
最基本的创建方式是在结构体类型后直接声明变量:
struct Student s1; // 定义结构体变量s1
struct Student s2, s3; // 同时定义多个变量
如果觉得struct Student的写法繁琐,可以用typedef为结构体起一个别名(推荐用法):
typedef struct Student {
char name[20];
int age;
float score;
} Student; // 为结构体起别名Student
Student s4; // 直接用别名创建变量,更简洁
2. 初始化结构体变量
初始化即创建变量时为其成员赋值,有三种常用方式:
方法一:按成员顺序初始化(C89 标准支持)
按结构体中成员的定义顺序依次赋值,用大括号包裹:
Student s1 = {"Alice", 18, 95.5f};
- 字符串
"Alice"赋值给name - 整数
18赋值给age - 浮点数
95.5f赋值给score
方法二:指定成员初始化(C99 及以上标准支持,推荐)
通过.成员名明确指定赋值的成员,顺序可以任意调整,可读性更强:
Student s2 = {
.age = 19,
.name = "Bob",
.score = 88.0f
};
方法三:先定义后逐个赋值
如果创建变量时未初始化,后续可以通过变量名.成员名的方式访问并赋值:
Student s3;
// 字符数组不能直接用=赋值,需用strcpy函数
strcpy(s3.name, "Charlie");
s3.age = 20; // 直接用.访问成员并赋值
s3.score = 90.0f;
🔔 注意:
- 访问结构体成员必须用
.操作符(如s3.age) - 字符数组
name不能直接用=赋值(如s3.name = "Charlie"是错误的),需用字符串复制函数strcpy(包含在<string.h>头文件中)
三、结构体的内存布局:为什么需要内存对齐?
结构体的内存占用是初学者最容易困惑的点。以Student结构体为例:
name[20]占 20 字节age占 4 字节score占 4 字节
按常理计算总大小应该是20+4+4=28字节,但实际用sizeof计算时,结果可能是32 字节!
#include <stdio.h>
int main() {
printf("Student大小:%zu字节\n", sizeof(Student)); // 输出可能为32
return 0;
}
这多出的 4 字节从何而来?答案是内存对齐(Memory Alignment)。
四、深入理解内存对齐
💡 什么是内存对齐?
现代 CPU 访问内存时,并不是逐个字节读取,而是按 “固定大小的块”(如 4 字节、8 字节)读取。结构体成员相对于结构体起始地址的偏移量(即距离起始位置的字节数)必须是其 “对齐数” 的整数倍,才能保证 CPU 高效读取。如果偏移量不满足要求,编译器会自动插入填充字节(padding) 来调整,这就是内存对齐。
偏移量:成员地址与结构体起始地址的差值(例如,结构体从地址
0x1000开始,某成员地址为0x1004,则偏移量为4)。
📐 对齐规则(以 x86_64 平台为例)
-
基本类型对齐数:每个基本类型有默认对齐数(由编译器和平台决定):
char:1 字节对齐(偏移量可为任意整数)short:2 字节对齐(偏移量必须是 2 的倍数)int、float:4 字节对齐(偏移量必须是 4 的倍数)double:8 字节对齐(偏移量必须是 8 的倍数)
-
结构体成员对齐:每个成员的偏移量必须是其自身对齐数的整数倍,不足则插入填充字节。
-
结构体整体对齐:结构体总大小必须是其最大成员对齐数的整数倍,不足则在末尾插入填充字节(确保结构体数组中每个元素都能正确对齐)。
🎯 示例分析 1:struct Student的内存布局
我们以Student结构体为例,逐步分析其成员的偏移量(假设结构体起始地址为0x00,偏移量从 0 开始计算):
struct Student {
char name[20]; // 1字节对齐
int age; // 4字节对齐
float score; // 4字节对齐
};
-
name成员(20 字节):
char类型 1 字节对齐,偏移量从 0 开始,占用偏移量0~19(共 20 字节)。
结束时偏移量为 19,下一个可用偏移量为 20。 -
age成员(4 字节):
int类型 4 字节对齐,偏移量 20 是 4 的倍数(20=4×5),满足对齐要求。
从偏移量 20 开始,占用20~23(4 字节),结束时偏移量为 23,下一个可用偏移量为 24。 -
score成员(4 字节):
float类型 4 字节对齐,偏移量 24 是 4 的倍数(24=4×6),满足对齐要求。
占用偏移量24~27(4 字节),结束时偏移量为 27。 -
结构体整体检查:
总大小为20+4+4=28字节,最大成员对齐数是 4,28%4=0,满足整体对齐要求。
→sizeof(Student) = 28字节。
🎯 示例分析 2:有填充的struct Mixed
struct Mixed {
char a; // 1字节对齐
int b; // 4字节对齐
char c; // 1字节对齐
};
-
a成员(1 字节):
偏移量 0(1 字节对齐),占用偏移量0,下一个可用偏移量 1。 -
填充字节(3 字节):
b是int类型(4 字节对齐),当前偏移量 1 不是 4 的倍数。
需填充 3 字节(偏移量 1~3),使下一个偏移量为 4(4 是 4 的倍数)。 -
b成员(4 字节):
从偏移量 4 开始,占用4~7,下一个可用偏移量 8。 -
c成员(1 字节):
1 字节对齐,从偏移量 8 开始,占用8,下一个可用偏移量 9。 -
整体填充(3 字节):
最大成员对齐数是 4,当前总大小 9 字节,需填充 3 字节(偏移量 9~11),使总大小为 12 字节(12 是 4 的倍数)。
→sizeof(struct Mixed) = 12字节。
🖼 图解struct Mixed内存布局
+-----------------+
| a (1字节) | 偏移量0
+-----------------+
| 填充(3字节) | 偏移量1~3(为b对齐)
+-----------------+
| b (4字节) | 偏移量4~7
+-----------------+
| c (1字节) | 偏移量8
+-----------------+
| 填充(3字节) | 偏移量9~11(为整体对齐)
+-----------------+
总大小:12字节
五、如何减少内存浪费?优化结构体设计!
内存对齐导致的填充字节是可以通过调整成员顺序优化的。基本原则是:将对齐数大的成员放在前面,对齐数小的成员放在后面,减少成员之间的填充。
反例:不合理的成员顺序(浪费 6 字节)
struct Bad {
char a; // 1字节对齐
int b; // 4字节对齐(需3字节填充)
char c; // 1字节对齐(整体需3字节填充)
};
// 总大小:1 + 3(填充) + 4 + 1 + 3(填充)= 12字节
正例:优化后的成员顺序(仅浪费 2 字节)
struct Good {
int b; // 4字节对齐(放最前,无填充)
char a; // 1字节对齐(紧跟b,偏移量4)
char c; // 1字节对齐(紧跟a,偏移量5)
// 整体需填充2字节(总大小8,是4的倍数)
};
// 总大小:4 + 1 + 1 + 2(填充)= 8字节
✅ 最佳实践:按成员的对齐数从大到小排列(如double→int→short→char),可最大限度减少填充。
六、位结构体:按位分配内存的特殊结构体
在嵌入式开发或需要节省内存的场景中,有时我们只需要用一个整数的几个二进制位来存储数据(例如表示开关状态的 “0” 和 “1” 只需 1 位)。这时可以使用位结构体(Bit Structure),它允许我们为结构体成员按二进制位指定大小,实现内存的极致利用。
✅ 定义位结构体
通过成员名: 位数的语法指定每个成员占用的二进制位数:
struct Flags {
unsigned int is_active : 1; // 占用1位(0或1)
unsigned int mode : 2; // 占用2位(0~3)
unsigned int status : 3; // 占用3位(0~7)
};
is_active:1 位,可表示0(未激活)或1(激活)mode:2 位,可表示0~3(共 4 种状态)status:3 位,可表示0~7(共 8 种状态)
🔍 位结构体的内存占用
位结构体的总大小由成员占用的总位数决定,且遵循内存对齐规则:
- 上面的
struct Flags总位数为1+2+3=6位,不足 1 字节(8 位),因此总大小为1 字节(满足unsigned int的对齐数 1 字节)。 - 如果总位数超过 8 位,会自动占用下一个字节,例如总位数 10 位时,大小为 2 字节。
#include <stdio.h>
int main() {
printf("Flags大小:%zu字节\n", sizeof(struct Flags)); // 输出1
return 0;
}
🛠 位结构体的使用
位结构体的成员访问与普通结构体相同,用.操作符:
struct Flags f;
f.is_active = 1; // 激活状态(1位,只能赋值0或1)
f.mode = 2; // 模式2(2位,值不能超过3)
f.status = 5; // 状态5(3位,值不能超过7)
🔔 注意:
- 成员的取值范围不能超过位数限制(例如 2 位成员最大值为
3,即0b11),否则会发生溢出(高位被截断)。 - 通常用
unsigned int作为成员类型,避免符号位带来的问题。 - 位结构体的成员不能取地址(
&f.is_active是错误的),因为它们不占用完整字节。
💡 位结构体的应用场景
- 嵌入式设备:存储硬件寄存器的状态位(如传感器开关、LED 状态)。
- 网络协议:压缩数据传输格式,减少带宽占用。
- 状态标记:用少量位存储多个布尔值(如 “是否登录”“是否管理员” 等)。
七、总结
| 要点 | 说明 |
|---|---|
| 🧱 结构体定义 | 用struct 标签 { 成员列表 }定义,本质是 “数据模板” |
| 🛠 变量创建 | 用struct 标签 变量名或typedef别名创建,创建时才分配内存 |
| 🔋 初始化方式 | 支持按顺序初始化、指定成员初始化,字符数组成员需用strcpy赋值 |
| 🔍 内存对齐核心 | 成员相对于结构体起始地址的偏移量必须是其对齐数的整数倍 |
| 📏 对齐规则 | 成员按自身对齐数对齐,结构体整体按最大成员对齐数对齐 |
| 🚀 优化建议 | 成员按对齐数从大到小排列,减少填充字节浪费 |
| 🔢 位结构体 | 用成员: 位数语法按位分配内存,适合存储小范围值,节省空间 |
结构体是 C 语言描述复杂数据的核心工具,而内存对齐和位结构体则体现了 C 语言对内存的精细化控制能力。掌握这些知识,不仅能写出更高效的代码,还能为嵌入式开发、底层编程等领域打下基础。
11万+

被折叠的 条评论
为什么被折叠?



