要说内存对齐这玩意儿,那可真是程序界的 “规矩先生”,躲都躲不开。

搞高性能系统开发、玩低延迟交易系统,还有跟硬件打交道的时候,没它可不行。

整明白了内存对齐,能让缓存少出错、SIMD 指令跑得飞起,甚至还能避免程序直接崩掉 —— 有些平台就认对齐访问这一套,不服不行。

C++11 整了个 alignas 关键字,专门用来指定变量或者类型的对齐方式,按理说跟 new 搭配着用挺顺溜,可实际操作起来,哎,这里面门道可不少。

Part1  啥是内存对齐呢?

说白了,就是数据在内存里放的时候,不是挤挤挨挨堆一块儿的。从结构体开始的地址算起,每个元素往内存里搁的时候,都觉得内存是按自己的大小(通常它为4或8)划分好的,所以它放的位置必须是自己宽度的整数倍,这就是内存对齐的规矩。

编译器会给程序里的每个 “数据单元” 找个合适的地儿。C 语言还允许咱们插手 “内存对齐” 这事儿。要是你想摸清底层的猫腻,那 “内存对齐” 可不能再是雾里看花了。

咱从一个例子开始琢磨琢磨。按说 int 占 4 个字节,char 占 1 个字节,把它们塞进一个结构体里,按理说应该是 4+1=5 个字节。可实际上,跑程序出来的结果是 8 个字节,这就是内存对齐所导致的。

#include<stdio.h>
struct{
    int x;
    char y;
}Test;
int main()
{
    printf("%d\n",sizeof(Test)); // 输出8不是5
    return 0;
}

Part2  为啥非要内存对齐?

两大核心原因:

1、硬件兼容性(可移植性):

  • 并非所有硬件平台都能容忍任意地址上的任意数据访问。
  • 某些架构(如早期的 ARM, RISC-V 基础指令集,部分 DSP)严格要求特定数据类型(如 int, float, double, 指针)必须存储在特定边界(如 4 字节边界、8 字节边界)上。

违反后果:尝试在非对齐地址访问这些数据会触发硬件异常(如总线错误 SIGBUS),导致程序崩溃。

2、性能优化:

  • 减少内存访问次数:对齐的数据可在单次读取中完成,而未对齐的数据可能需要两次访问(如跨4字节边界)。
  • 提高缓存效率:对齐的数据更可能完整占用一个缓存行(通常64字节),减少缓存未命中。
  • SIMD指令支持:向量化指令(如SSE/AVX)要求数据严格对齐,否则无法发挥性能优势。
  • 原子性保证:对齐访问通常能保证原子性(单次总线事务),而未对齐访问可能需要多次事务,增加开销。

假设没有内存对齐这规矩,数据随便放。现在有个 int 变量,放在从地址 1 开始的连续四个字节里。处理器去拿数据的时候,得先从 0 地址开始读第一个 4 字节块,把不要的字节(0 地址那部分)扔了,再从地址 4 开始读下一个 4 字节块,同样把不要的数据(5、6、7 地址那部分)剔除,最后把剩下的两块数据拼起来放进寄存器。这活儿可够繁琐的。

有了内存对齐就不一样了,int 类型的数据只能放在按规矩对齐的内存里,比如从 0 地址开始。那处理器取数据的时候,一下就能读出来,不用额外瞎折腾,效率这不就上来了嘛。

简单说,就是方便计算机读写数据。对齐的地址一般都是 n(n=2、4、8)的倍数。

  • 1 个字节的变量,像 char 类型的,放哪儿都行,随便哪个地址都成。
  • 2 个字节的变量,比如 short 类型,得放在 2 的整数倍地址上。
  • 4 个字节的变量,像 float、int 类型,就得搁在 4 的整数倍地址上。
  • 8 个字节的变量,比如 long long、double 类型,必须放在 8 的整数倍地址上。

Part3   内存对齐规则

3.1、基本规则

成员对齐:每个数据成员的起始地址必须是其对齐值的倍数。

对齐值 = min(成员自身大小, 指定对齐值)(通过#pragma pack或alignas设置)。

结构体整体对齐:

    • 结构体大小必须是最大成员对齐值的倍数。
    • 若指定对齐值小于最大成员对齐值,则以最大成员为准。

3.2、基本变量类型所占大小

注:红色标记部分特别注意!!

数据类型

ILP32(32位Linux系统)

LP64(大部分64位Linux系统)

LLP64(64位Windows系统)

char

1 字节

1 字节

1 字节

short

2 字节

2 字节

2 字节

int

4 字节

4 字节

4 字节

float

4 字节

4 字节

4 字节

long

4 字节

8 字节

4 字节

double

8 字节

8 字节

8 字节

long long

8 字节

8 字节

8 字节

指针 point

4 字节

8 字节

8 字节

枚举 enum

4 字节

4 字节

4 字节

联合体  union

取 union 中最大一个变量类型大小

取 union 中最大一个变量类型大小

取 union 中最大一个变量类型大小

  • ILP32 指 int,long、point 是 32 位。
  • LP64 指 long、point 是 64 位。
  • LLP64 指 long long、point 是 64 位。

3.3、示例

实例 1:(没指定对齐字节,那 n 就等于最大成员(int i)的大小 4)

class node
{
    char c;   //放在位置0,位置区间[0]
    int i;      //4 = n, 那么放置起始位置应该是4的倍数,即4,位置区间为[4~7]
    short s; //2 < n,那么放置起始位置应该是2的倍数,即8,位置区间为[8~9]
}

这时候成员一共占了 [0~9] 10 个字节,还得整体对齐,大小得是 4 的倍数,所以就是 12。

实例 2:(假设指定对齐字节为 8,那 n = min (8,4) = 4)

class node
{
    int i; //放在位置0,位置区间[0~3]
    char c; //1 < n, 那么放置起始位置应该是1的倍数,即4,位置区间为[4]
    short s; //2 < n,那么放置起始位置应该是2的倍数,即6,位置区间为[6~7]
}

成员总共占了 [0~7] 8 个字节,刚好是 4 的倍数,所以大小是 8。

实例 3:(假设指定对齐字节是 2,那 n = min (2,4) = 2)

class node
{
    char c; //放在位置0,位置区间[0]
    int i; //4 > n, 那么放置起始位置应该是2的倍数,即2,位置区间为[2~5]
    short s; //2 = n,那么放置起始位置应该是2的倍数,即6,位置区间为[6~7]
}

此时成员共占用 [0~7] 8 个字节,刚好是 4 的倍数,所以大小是 8。

实例 4:(按照默认设置)

class temp
{
    char c;
    int i;
    short s1;
};

从实例 1 能知道,默认对齐情况下,temp 的大小是 12,temp 的对齐字节数是三个成员里最大的,也就是 4。

对于 node 来说,n 等于它三个成员对齐字节数里最大的,也就是 t 的对齐字节数 4。

class node
{
    char c; //放在位置0,位置区间[0]
    temp t; //4(temp的对齐字节数) = n, 那么放置起始位置应该是4的倍数,即4,位置区间为[4~15]
    short s; //2 < n,那么放置起始位置应该是2的倍数,即16,位置区间为[16~17]
}

这时候成员共占用 [0~17] 18 个字节,还得整体对齐,大小得是 4 的倍数,所以是 20。

实例 5:(默认设置)

对于 node,n 等于它三个成员对齐字节数里最大的,也就是 d 的对齐字节数 8。

class node
{
    temp t; //放在位置0,位置区间[0~11]
    double d; //8(temp的对齐字节数) = n, 那么放置起始位置应该是8的倍数,即16,位置区间为[16~23]
    short s; //2 < n,那么放置起始位置应该是2的倍数,即24,位置区间为[24~25]
}

此时成员共占用 [0~25] 26 个字节,整体对齐后大小得是 8 的倍数,所以是 32。

再说说类继承时的内存对齐。

看看下面这些类:

class A
{
    int i;
    char c1;
}
class B:public A
{
    char c2;
}
class C:public B
{
    char c3;
}

sizeof (C) 的结果是多少呢?gcc 和 vs 给出的答案不一样,分别是 8 和 16。

在 gcc 里:C 就相当于把所有成员 i、c1、c2、c3 都放一个 class 里,是先继承再对齐。

在 vs 里:对于 A,对齐后大小是 8;对于 B,c2 加上对齐后的 A 的大小是 9,对齐后就是 12;对于 C,c3 加上对齐后的 B 的大小是 13,再对齐就是 16,这是先对齐再继承。

Part4  哪些情况会影响内存对齐?

1、成员声明顺序

如前所示,将大对齐要求的成员声明在前面通常能减少内部填充。

2、编译器指令

  • #pragma pack(n)(广泛支持):设置当前编译单元的最大打包对齐值(n 通常是 1, 2, 4, 8, 16)。#pragma pack() 恢复默认。慎用,影响 AB I。
  • __attribute__((packed))(GCC/Clang):强制取消结构体/类内部的填充,成员紧密排列(大小=各成员sizeof之和)。对齐值变为 1。
  • __attribute__((aligned(n)))(GCC/Clang):显式指定类型或变量的最小对齐值(n)。
  • alignas(n) (C++11 标准):显式指定类型或变量的对齐要求(更现代、可移植的方式替代编译器扩展)。
  • 目标平台架构 (ABI):x86 相对宽松,ARM、RISC-V 等 RISC 架构通常要求更严格的对齐。

Part5   内存对齐的副作用

内存对齐也不是没副作用。

它的好处是加快内存读取效率,但也有俩缺点。

1、空间浪费。

填充字节的唯一目的就是满足对齐要求,它们不存储有效数据,导致结构体/类的大小大于其成员 sizeof 之和。成员顺序不当会显著增加这种开销(如 上面的例子里,本来只需要 5 个字节,因为对齐就变成 8 个字节了,编译器会在 char 和 int 之间加 3 个字节的 pad,来保证对齐 )。优化变量顺序是节省内存的关键。

// size 12
struct one {
char a;
int b;
short c;
};
// size 8
struct two {
int b;
char a;
short c;
};

所以保持变量的良好声明顺序能省空间,但也有额外问题,类初始化是按变量顺序来的,改了声明顺序,也得注意初始化顺序受不受影响。

2、应用程序二进制接口 (ABI) 兼容性

当使用编译器指令(如 -march=native 启用 SSE/AVX/NEON)或打包指令(如 #pragma pack)编译动态库(.so/.dll) 时,会改变其内部类型的布局和对齐。

问题:

如果调用者(主程序或其他库)使用不同的对齐设置(或不同编译器、不同 -march 设置)编译,它们对所调用库中类型的布局理解就会不一致。

后果:

传递结构体、类对象或在接口中使用它们时,会导致内存解释错误、数据损坏或崩溃。这类问题非常隐蔽且难以调试。

解决方法:

  • 明确定义并严格遵循模块间的稳定 ABI(通常规定标准的打包和对齐方式)。
  • 避免在模块接口(头文件)中使用需要特殊对齐的编译器指令或属性。如果必须使用,接口双方必须严格一致
  • 重新编译涉及冲突的模块,确保对齐设置统一。

看到这儿,请你回答:(下方评论区说出你的答案)

总结

内存对齐是计算机底层硬件高效、安全访问数据的基石。

深入理解其原理(硬件要求、性能优势)和规则(有效对齐值 n、成员放置、整体补齐),对于编写高性能、可移植且内存高效的 C/C++ 代码至关重要。

可以通过 alignas (C++11)、#pragma pack、__attribute__ 等工具干预对齐,但必须清醒认识其带来的空间开销和潜在的 ABI 兼容性风险,尤其是在跨模块或跨编译器协作时。优化结构体成员顺序是减少填充浪费最直接有效的手段。

往期推荐

为什么很多人劝退学 C++,但大厂核心岗位还是要 C++?

手撕线程池:C++程序员的能力试金石

打破认知:Linux管道到底有多快?

C++的三种参数传递机制:从底层原理到实战

顺时针螺旋移动法 | 彻底弄懂复杂C/C++嵌套声明、const常量声明!!!

阿里面试官:千万级订单表新增字段,你会怎么弄?

C++内存模型实例解析

字节跳动2面:为了性能,你会牺牲数据库三范式吗?

字节C++一面:enum和enum class的区别?

Redis分布式锁:C++高并发开发的必修课

更多推荐