【C++】字节对齐—alignof/alignas和#pragma pack(n)

0. 为什么要字节对齐

(1)是什么?

每个变量(结构体的成员)都有“摆放要求”:“我必须放在 编号是 N 的倍数 的货架格子上。”。N 就是它的 对齐值 alignof(T)

//字节自然对齐时
//int x 对齐到 4,放在 4~7:
[   第0排   ] [   第1排   ] 
 0 1 2 3  |  4 5 6 7  |  8 9 10 11
           ↑___________↑
           一次就能搬走(4 字节)
//叉车(CPU)一趟就把 4 5 6 7 整排端走,不横跨。

//字节没有对齐时
//把同一个 int 故意放到 3~6:
[   第0排   ] [   1 排   ] 
 0 1 2 3  |  4 5 6 7  |  8 9 10 11
       ↑_______↑
       3 4 5 6   ← 这个 int 占用两排
//第 0 排里有 3 号字节
//第 1 排里有 4 5 6 号字节

//叉车一次只能搬一排,于是:
//先搬第 0 排,把 3 号字节捡起来;
//再搬第 1 排,把 4 5 6 捡起来;
//最后还要在 CPU 内部把碎片拼成完整的 4 字节。
//CPU取两次,性能减半。

(2)为什么要有这条规则?

因为CPU一次只能搬 4、8、16……字节的作为一个单元货;
如果结构体的存储横跨两个单元,CPU要取两次,甚至直接翻车(硬件异常)。
所以需要内存对齐。

(3)自然字节对齐

没有显式通过 alignas 修改的情况下,结构体的对齐方式是由其 【最大对齐成员】(alignof) 决定的。

struct MyStruct1
{
  char a;     // 1 byte
  int b;      // 4 byte
  double c;   // 8 byte
};
//最大对齐成员是 double c,对齐为 8 字节。
//所以 alignof(MyStruct1) 是 8。
//编译器会自动插入填充字节,使得整个结构体大小为 16 字节(不是 32)。

(4)sizeof(T)最终长度

  • 无论自然对齐还是 alignas(N),最终大小一定是其对齐值的整数倍,并且是在“刚好能装下所有成员”的前提下最小的那个倍数。
  • 自然对齐同样满足这句话,只不过把 N 换成“该类型自己的自然对齐值”而已。
  • 自然对齐时,编译器在【成员之间或结构体末尾】插入若干字节,目的:使下一个成员/数组元素满足对齐要求
  • alignas(N) 时,【只在末尾】再补 0…N−1 字节,让 sizeof(T) 升为 N 的倍数。

1. alignof和alignas

alignas 和 alignof 是 C++11 引入的两个关键字,用于控制变量(如果是结构体的话,就是【整个结构体起始地址】)的内存对齐。
对齐值必须是2的幂

(1)alignof

作用:查询类型(结构体的话,是其最大成员)的自然对齐对齐值
语法:alignof(Type)
返回值:std::size_t,表示该类型必须对齐到的字节边界。

#include <iostream>
#include <cstddef>

int main() 
{
    std::cout << "alignof(int) = " << alignof(int) << '\n';  
    std::cout << "alignof(double) = " << alignof(double) << '\n';

    struct MyStruct1
    {
      char a;
      int b;
    };
    std::cout << "sizeof(MyStruct1)=" << sizeof(MyStruct1) << "\n";  //最大成员b的内存对齐为4

    struct MyStruct2
    {
      char a;
      int b;
      double c;
    };
    std::cout << "sizeof(MyStruct2)=" << sizeof(MyStruct2) << "\n";  //最大成员c的内存对齐为8
}

(2)alignas

作用:显式修改某个变量的对齐值(结构体的话,结构体整体的起始地址),按指定字节数对齐(也会影响sizeof)。
语法:alignas(N),其中 N 必须是 2 的幂(如 1, 2, 4, 8, 16...),CPU读取的小单元字节数。
用途:优化性能、满足硬件要求、与底层接口对齐等。

#include <iostream>
#include <cstddef>

//自然对齐
struct MyStruct1
{
  char a;
  int b;
  double c;
};

//MyStruct2整体必须从32字节倍数的内存开始
struct alignas(32) MyStruct2 
{
    char a;
    int b;
};

int main() 
{
    MyStruct1 s1;
    std::cout << "sizeof(MyStruct1)=" << sizeof(MyStruct1) << "\n";
    std::cout << "alignof(MyStruct1) = " << alignof(MyStruct1) << '\n';
    std::cout << "address of s1 = " << &s1 << '\n';

    MyStruct2 s2;
    std::cout << "sizeof(MyStruct2)=" << sizeof(MyStruct2) << "\n";
    std::cout << "alignof(MyStruct2) = " << alignof(MyStruct2) << '\n';
    std::cout << "address of s2 = " << &s2 << '\n';
}

注意:实际是设定小单元的字节数,变量起始地址对齐到N时,编译器会在【结构体末尾】补 0~N-1 字节,使得 sizeof(T) = ≥ 实际成员大小的最小 N 的倍数,但不是简单地就等于 N

#include <iostream>
#include <cstddef>

struct alignas(32) S { char c[48]; };

int main() 
{
    std::cout << sizeof(S) << '\n';   // 输出 64
    std::cout << alignof(S) << '\n';  // 输出 32
}
//成员实际占用:48 字节
//对齐要求:32 字节
//48 ÷ 32 = 1 余 16 → 不够整除
//下一个 32 的倍数:64
//所以编译器在末尾再补 16 字节
原大小 alignas(N) 最终 sizeof
48 32 64
31 32 32
32 32 32
33 32 64

2. pragma pack(n)

预处理器指令,显式控制编译器对齐策略。
强制将结构体成员按照指定字节数对齐
pragma pack()省内部空间,牺牲性能。网络协议、文件头常用 pack(1) 或 pack(2)

(1) 和alignof、alignas的区别:

  • alignof/alignas是C++11起的标准。而pragma pack(n)是编译器的打包控制。
  • alignas控制:整个结构体的起始地址对齐, 不改变成员间布局。
    pragma pack控制:成员间的填充(padding),会改变成员布局。

(2)举例说明

pragma pack(n):强行降低“包裹”摆放要求。
n 越小,越紧凑,空间省,可能未对齐访问。
下面把

#pragma pack(push, 1)
struct FooPacked
{
    char   c;      // 1
    int    i;      // 4
    double d;      // 8
};
#pragma pack(pop)

在内存里 逐字节 画出来,一看就懂“pack(1) 把填充全部挤掉”长什么样

  • “自然对齐”长什么样(无 pack)
偏移  0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16
      |c|x|x|x|i|i|i|i|x|x|x|x|x|x|x|x|d|...(再 8 字节)
      1  3 废      4            8  废(对齐到 8)→ 共 24 字节
  • alignof = 8
  • sizeof = 24
  • 加上 #pragma pack(push, 1) 之后
    规则:所有成员只按 1 字节对齐中间和尾部都不插废字节
偏移  0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17
      |c|i|i|i|i|d|d|d|d|d|d|d|d|
      1  4        8
  • 没有任何“x”废字节
  • 总大小 = 1 + 4 + 8 = 13 字节
  • alignof(FooPacked) = 1
  • 如果放在数组里,下一个元素直接从偏移 13 开始,不一定落在 8 的倍数
    访问 d 时可能横跨两个 8 字节缓存行,甚至触发总线错误(ARM/MIPS)。
posted @ 2025-09-06 12:14  仰望星河Leon  阅读(66)  评论(0)    收藏  举报