🧱 1. 结构体的定义与访问
结构体(struct)允许你将多个不同类型的变量组合成一个单一的复合类型,这在表示现实世界的实体(如学生、商品等)时非常有用。
-
定义结构体:使用
struct关键字来定义一个新的结构体类型。/* 定义一个名为Student的结构体类型 */ struct Student { char name[20]; // 姓名 int age; // 年龄 double height; // 身高 };定义结构体时,可以同时声明变量,也可以先定义类型再声明变量:
/* 方式1: 定义类型的同时声明变量 */ struct Point { int x; int y; } p1, p2; // 变量p1和p2 /* 方式2: 先定义类型,再声明变量 */ struct Point p3; // 另一个Point变量还可以使用
typedef简化类型名:typedef struct Employee { int id; char dept[10]; } Emp; // 现在可以用`Emp`代替`struct Employee` Emp e1; // 变量声明 -
初始化结构体:可以在声明时使用大括号
{}初始化。struct Student stu1 = {"Alice", 20, 1.65}; -
访问结构体成员:使用点号
.操作符访问结构体变量的成员。如果通过指针访问,则使用箭头->操作符。struct Student s; s.age = 21; // 直接访问赋值 struct Student *ptr = &s; ptr->age = 22; // 通过指针访问赋值 printf("Age: %d\n", ptr->age);
🧠 2. 内存对齐原则
CPU并非逐字节访问内存,而是以特定字节数(如2、4、8字节)为块(称为内存访问粒度)进行读取。如果数据未在自然对齐的边界上,CPU可能需要进行多次内存访问并拼接数据,这会降低效率,在某些架构(如ARM)上甚至可能导致硬件异常。
内存对齐规则主要有三条:
- 起始地址对齐:结构体的第一个成员从偏移量(offset)为0的地址处开始存放。
- 成员地址对齐:结构体的每个成员变量,其起始地址必须是
min(编译器默认对齐数, 该成员类型大小)的整数倍。Visual Studio默认对齐数为8,Linux/GCC下则没有默认对齐数,以其类型大小为准。- 例如,在VS中,
int(4字节)的对齐数是min(8, 4) = 4,故其地址必须是4的倍数。
- 例如,在VS中,
- 结构体整体大小对齐:整个结构体的总大小必须是 所有成员中“最大对齐数” (每个成员的对齐数中的最大值)的整数倍。如果不是,编译器会在末尾添加填充字节(Padding)以满足此条件。
为什么需要内存对齐?
主要是为了性能和可移植性。对齐的数据访问速度更快,且某些硬件平台根本不支持非对齐访问。
📐 3. 计算结构体大小
理解规则后,我们可以手动计算结构体大小。
-
示例1:简单结构体
struct S1 { char a; // 大小1字节,对齐数1 (min(8,1)) int b; // 大小4字节,对齐数4 (min(8,4)) short c; // 大小2字节,对齐数2 (min(8,2)) };假设起始地址为0:
a放在偏移量0。b需放在4的倍数处,故偏移量1-3填充,b从偏移量4开始存放(4-7)。c对齐数为2,可从偏移量8开始存放(8-9)。- 当前总大小:0-9 = 10字节。
- 结构体最大对齐数是
max(1,4,2)=4,10不是4的倍数,需在末尾填充2字节(偏移量10-11)。 - 最终
sizeof(S1) = 12字节。
-
示例2:调整成员顺序优化
struct S2 { int b; // 4字节,对齐数4,偏移量0-3 short c; // 2字节,对齐数2,偏移量4-5 char a; // 1字节,对齐数1,偏移量6 // 填充1字节使总大小为最大对齐数(4)的倍数 (7->8) };最终
sizeof(S2) = 8字节。
优化技巧:将占用空间大的成员(和对齐数大的成员)放在前面,通常可减少填充字节,节省内存。 -
示例3:嵌套结构体
struct Inner { double d; // 8字节,对齐数8 char e; // 1字节,对齐数1 // 编译器可能在e后填充若干字节,使Inner自身大小是8的倍数 (例如16或24,取决于规则) }; struct Outer { int a; // 4字节,对齐数4 struct Inner s; // Inner的对齐数取其自身最大对齐数(8) // 因此s的起始地址必须是8的倍数 short b; // 2字节,对齐数2 };计算嵌套结构体大小时,规则类似,但需注意内部结构体的自身对齐要求。
-
使用
#pragma pack修改默认对齐数
可以使用#pragma pack(n)指令指定新的默认对齐数(n通常是1, 2, 4, 8…)。#pragma pack(push, 1) // 将当前对齐设置压栈,并设置对齐数为1 struct PackedStruct { char a; int b; short c; }; // 理论上 sizeof(PackedStruct) == 1+4+2 = 7 #pragma pack(pop) // 恢复之前的对齐设置注意:慎用
#pragma pack。虽然节省内存,但可能导致性能下降,且在某些硬件平台上访问未对齐数据会引发崩溃。常用于网络协议包、硬件寄存器映射等需要精确控制内存布局的场景。
🔍 4. 观察内存布局(逆向重点)
在逆向工程中,分析二进制文件常需推断数据结构布局。调试器是观察内存布局的利器。
-
使用
offsetof宏:
offsetof宏(定义于stddef.h)可获取结构体成员在其中的偏移量。#include <stddef.h> printf("Offset of 'b' in S1: %zu\n", offsetof(struct S1, b)); // 输出4 -
在Visual Studio中观察:
- 在调试模式(按 F5)下运行程序。
- 在代码中设置断点。
- 当程序在断点处停止时,可以通过监视窗口(Watch Window)查看结构体变量,并展开观察其成员的值和地址。
- 更强大的是内存窗口(Memory Window)(
调试 > 窗口 > 内存),你可以输入结构体变量的地址(如&s1),直接查看其内存字节,非常有助于分析填充字节和实际布局。
551

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



