07 / 80 · C++11 · 约 8 分钟
内存对齐 Alignment 与结构体填充
对齐规定对象可放置的地址条件,sizeof 包含为合法布局所需的填充。alignof 查询类型要求,alignas 提高声明的对齐要求;成员大小相加不能代替布局计算,打包结构也不能代替可移植序列化。
大小与对齐分别回答什么
大小回答一个完整对象占多少字节,对齐回答对象允许放在哪些地址上。alignof(T) 给出 T 的对齐要求;该数值由实现决定,不能把 double 的大小或对齐都固定背成八。满足更严格对齐的位置,也满足较弱的有效对齐要求。
结构体需要安置每个成员,并使整个结构体能作为数组元素连续排列。成员间可能出现内部填充,末尾也可能出现尾部填充。对于普通完整对象,大小必须支持下一个同类型数组元素仍正确对齐;因此 sizeof 不是成员大小的简单求和。
先查询,再提出要求
alignas(T) 让声明至少满足 T 的对齐要求,alignas(数值) 则请求指定的有效对齐。请求必须被目标实现支持,且不能降低对象原本需要的对齐。示例借助 double 的已知类型要求给小结构增加对齐,不假定任意平台都支持某个很大的数值。
优化成员顺序有时能减少空隙,但会改变布局、初始化顺序和可能的 ABI,不能在公开数据格式上随意调整。是否值得重排应以实际目标平台测量为准,而不是把某一种 ABI 的手算结果当成标准定理。
对齐不是对象生命期
一块足够大且对齐的原始存储,并不自动允许你把它解释为任意类对象;对象生命期和合法访问类型也必须满足。把未对齐的字节地址强转为 int* 再读取,不能靠“CPU 允许未对齐访问”洗去语言层面的错误。
处理文件或网络数据时,优先按协议读取字段,或对符合要求的平凡可复制类型使用 memcpy。不要直接写出结构体全部字节作为可移植格式:除端序外,填充、成员表示以及编译器 ABI 都可能不同,填充还可能携带不应公开的残留内容。
容易答错的地方
- #pragma pack 是实现相关机制,可能制造未对齐成员;它不保证跨编译器协议兼容。
- memcmp 比较整个结构体的字节不等于比较各成员的语义值,填充可能不同。
运行一个例子
最低标准 C++11 · 完整程序 · 下载 .cpp
#include <cassert>
#include <iostream>
struct Record {
char tag;
int value;
};
struct alignas(double) AlignedTag {
char tag;
};
int main() {
static_assert(alignof(Record) >= alignof(int), "member alignment");
static_assert(sizeof(Record) % alignof(Record) == 0, "array stride");
static_assert(alignof(AlignedTag) >= alignof(double), "requested alignment");
Record items[2] = {{'a', 10}, {'b', 20}};
assert(items[1].value == 20);
std::cout << items[0].tag << ' ' << items[1].value << '\n';
}
在本地编译
g++ -std=c++11 -Wall -Wextra -Wpedantic -pthread basics-alignment.cpp -o example && ./example预期结果
a 20
CHECK YOUR UNDERSTANDING
合上答案,试着解释。
为什么 struct S { char c; int x; }; 不能仅根据成员声明就断言 sizeof(S) == 5?需要哪些目标信息?
查看参考答案
首先 int 不被标准固定为四字节;其次 x 前面可能需要填充,整个 S 末尾也可能补齐。需要知道目标 ABI 的 sizeof(int)、alignof(int)、结构体布局规则及相关打包选项。可靠的程序用 sizeof(S) 查询,而序列化协议应显式规定字段编码,不依赖 S 的内存布局。
继续查证
标准草案链接会随工作草案更新;本文版本标记对应示例最低要求,不表示草案中的所有新规则都适用于旧标准。