1. 结构体怎么对齐? 为什么要进行内存对齐?

     内存对齐是指数据在内存中的起始地址必须是某个值(通常是其自身大小或平台字长)的整数倍。这并不是CPU的“任性”要求,而是为了提升性能和满足硬件要求。主要原因如下:

   (1) 硬件要求:许多计算机体系结构(尤其是RISC架构如ARM, MIPS, SPARC)的CPU在设计上就只能从对齐的地址访问数据。如果尝试进行非对齐的内存访问,处理器会抛出硬件异常,导致程序崩溃。

   例如,在32位机器上,一个32位(4字节)的int变量必须存放在地址是4的倍数的位置。

   (2) 性能优化:对于允许非对齐访问的架构(如x86/x86-64),对齐访问仍然是更快的。

   (3) 减少内存访问次数:现代CPU从内存中读取数据并非一个字节一个字节地读,而是以“块”或“字”为单位(例如,一次读8字节)。如果一个4字节的int跨越了两个8字节的内存块,CPU就需要进行两次内存读取、一些移位和合并操作才能得到这个整数的值。如果是对齐的,一次读取就能完成。

   (4) 利于缓存:缓存行是缓存和内存之间数据传输的最小单位(通常是64字节)。对齐的数据结构可以更好地利用缓存,减少缓存行被浪费的情况。

打个比方来说,就像你从书架上拿书,如果书的大小和书架格子匹配,你一次就能拿出一本完整的书(对齐访问)。如果一本书横跨两个格子,你就需要先打开两个格子,把书拼起来才能读(非对齐访问),效率自然更低。

2. 如何让结构体按照指定的对齐参数进行对齐?能否按照3、4、5即任意字节对齐?

(1)如何指定对齐方式:在C/C++中,通常使用预编译指令#pragma pack语言扩展__attribute__((aligned(n))) / _Aligna来指定结构体的对齐方式。

      //attribute 译为:属性          //align 译为:使成一条直线;使一致;使匹配,对齐

#pragma pack(n) (主要在MSVC和GCC兼容编译器)
        这会告诉编译器按照 n字节的边界进行对齐。它通常会减小结构体的对齐参数以节省空间,但可能导致非对齐访问的性能损失。

使用方式:

#pragma pack(push, 1) // 将当前对齐设置压栈,并设置为1字节对齐
struct MyStruct 
{
    char a;      // 1 byte
    int b;       // 4 bytes,原本需要4字节对齐,现在在pack(1)下只需1字节对齐
    short c;     // 2 bytes
}; // 结构体总大小现在为 1+4+2 = 7 bytes
#pragma pack(pop) // 恢复之前压栈的对齐设置

__attribute__((aligned(n))) (GCC/Clang) 或 _Alignas (C11标准) 这用于增大对齐要求,通常用于强制对齐到更大的边界(如缓存行),以避免伪共享或使用需要特殊对齐的指令(如SIMD)。

使用方式:

// GCC/Clang 语法
struct MyStruct 
{
    char a;
    int b;
    short c;
} __attribute__((aligned(64))); // 整个结构体按64字节对齐

// C11 标准语法
#include <stdalign.h>
struct alignas(64) MyStruct
{
    char a;
    int b;
    short c;
};

(2)能否按照3、4、5即任意字节对齐:

理论上可以,但有重要注意事项

  • #pragma pack(3), #pragma pack(5):编译器(如GCC, Clang, MSVC)通常允许你设置任意整数(如1, 2, 3, 4...)。但是,这只是一个“请求”,而不是一个“命令”。编译器会取n和成员自身大小中的较小值作为该成员的对齐边界。

    • 问题:对齐值通常是2的幂次方(1, 2, 4, 8, 16...),因为这是硬件和内存子系统设计的基础。使用非2的幂次方(如3, 5, 7)进行打包,虽然语法上允许,但极其罕见且可能有问题

      1. 它可能无法充分发挥优化作用。

      2. 如果结构体成员有大于n的类型(比如在pack(3)下有一个double成员),该成员仍然会按照自身大小(8字节)对齐,而不是3字节。pack(n)的含义是“最多按n字节对齐”,而不是“必须按n字节对齐”。

      3. 这几乎肯定会在支持非对齐访问的平台上导致性能下降,在不支持的平台上导致程序崩溃。

  • __attribute__((aligned(3))):GCC可能会接受这个语法,但行为是未定义的或不符合预期。C标准(C11)中的_Alignas操作数必须是2的幂次方。强行使用非2的幂次方是错误的使用方式。

结论: 你可以设置任意数字,但只应使用2的幂次方(尤其是1和平台的自然字长)。使用3、5、7等非标准对齐值是没有意义的,并且会带来兼容性和稳定性风险。

3. 什么是大小端?如何测试某台机器是大端还是小端,有没有遇到过要考虑大小端的场景

(1)大小端决定了多字节数据(如整数)在内存中的存储顺序。你可以把内存地址想象成从左(低地址)到右(高地址)的格子。小端模式像“倒着放”:数据的低位字节(如个位)存在左边的低地址,高位字节存在右边。大端模式则像“正着放”:数据的高位字节(如百位)存在左边,更符合我们书写数字的习惯。

(2) 使用联合体

#include <stdio.h>

int main()
 {
    union
   {
        int i;
        char c[sizeof(int)];
    } test;

    test.i = 0x01020304; // 设置一个int值

    if (test.c[0] == 0x01) 
    { // 检查低地址处的字节是否是最高位字节
        printf("Big-Endian\n");
    } 
    else if (test.c[0] == 0x04) 
    { // 检查低地址处的字节是否是最低位字节
        printf("Little-Endian\n");
    }
    else 
    {
        printf("Unknown\n");
    }
    return 0;
}

使用指针类型转换

#include <stdio.h>

int main()
 {
    int num = 0x01020304;
    char *byte = (char *)# // 获取int的起始地址(低地址),并将其当作char指针

    if (*byte == 0x01) 
    {
        printf("Big-Endian\n");
    } 
    else if (*byte == 0x04)
    {
        printf("Little-Endian\n");
    } else 
    {
        printf("Unknown\n");
    }
    return 0;
}

(3)只要涉及到不同字节序的平台之间进行原始二进制数据交换,就必须考虑大小端问题。

    在开发一个网络协议解析器时,需要处理从嵌入式设备(可能是大端PowerPC)发来的数据包。代码运行在x86服务器(小端)上。我们严格使用了 ntohs 等函数来转换协议头中的每一个字段(如长度、类型、序列号),确保解析出的数值是正确的。如果忘记转换,比如一个长度为500的字段(0x01F4)会被错误地解析为 0xF401(62465),导致程序逻辑完全混乱。这是一个必须考虑大小端的典型场景。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐