【C语言】第五课 结构体与内存对齐​​

🧱 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)上甚至可能导致硬件异常。

内存对齐规则主要有三条:

  1. 起始地址对齐:结构体的第一个成员从偏移量(offset)为0的地址处开始存放。
  2. 成员地址对齐:结构体的每个成员变量,其起始地址必须是 min(编译器默认对齐数, 该成员类型大小) 的整数倍。Visual Studio默认对齐数为8,Linux/GCC下则没有默认对齐数,以其类型大小为准。
    • 例如,在VS中,int(4字节)的对齐数是 min(8, 4) = 4,故其地址必须是4的倍数。
  3. 结构体整体大小对齐:整个结构体的总大小必须是 所有成员中“最大对齐数” (每个成员的对齐数中的最大值)的整数倍。如果不是,编译器会在末尾添加填充字节(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中观察

    1. 在调试模式(按 F5)下运行程序。
    2. 在代码中设置断点。
    3. 当程序在断点处停止时,可以通过监视窗口(Watch Window)查看结构体变量,并展开观察其成员的值和地址
    4. 更强大的是内存窗口(Memory Window)(调试 > 窗口 > 内存),你可以输入结构体变量的地址(如 &s1),直接查看其内存字节,非常有助于分析填充字节和实际布局。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值