ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

C/C++ sizeof运算符完全指南:类型大小、内存对齐与工程避坑

2026/9/29 9:56:05 拓冰建站 浏览量
C/C++ sizeof运算符完全指南:类型大小、内存对齐与工程避坑 先问一个问题你多久没用对 sizeof 了我面试别人的时候基本只要问一句“sizeof 是函数还是运算符”就能过滤掉一大批简历上写着“精通 C/C”的人。再问一句“结构体 sizeof 等于成员之和吗”又能过滤掉一大半。不是问题多刁钻而是 sizeof 这东西看起来太简单了简单到很多人拿到它就用根本没想过背后的规则。这篇就把 sizeof 从头到尾捋一遍。从基本数据类型到指针、数组、字符串再到函数、结构体、类、联合体每个类型到底怎么算、为什么这么算、实际工程里怎么踩坑全讲清楚。适合刚入门的 C/C 新手也很适合准备面试、或者写了好几年代码但一直靠“试”来蒙 sizeof 结果的老手。1. sizeof 的本质它是运算符不是函数先说最基础也最关键的一件事sizeof 是运算符不是函数。虽然你写的时候经常写成sizeof(int)、sizeof(struct xxx)这种带括号的形式看起来就像函数调用但它本质上和、-、*一样是语言层面内置的操作符。区别在哪函数在运行时被调用有压栈、跳转、返回这一套流程sizeof 的求值发生在编译期编译器在生成目标代码之前就把结果算出来了运行时不产生任何额外开销。所以你不能对运行期间才确定大小的东西求 sizeof比如函数参数中退化为指针的数组也不要想当然地认为表达式会被执行。1.1 编译期求值带来的几个“怪现象”因为 sizeof 在编译期就算完了所以有几个现象很反直觉。第一sizeof(i)不会让 i 加 1。你在代码里写int i 0; size_t sz sizeof(i); printf(%d %zu\n, i, sz);打印结果是0 4。i 的值根本没变因为 sizeof 压根不会真正执行 i 这个表达式它只需要知道 i 这个表达式的类型是 int而 int 在当前平台的字节数是 4结果就是 4。编译器甚至会直接把这个表达式当“哑操作”优化掉连警告都不给你。第二sizeof只关心类型不关心值。sizeof(100)、sizeof(0)、sizeof(-1)都是同一个结果因为它们的类型都是 int。同理sizeof(NULL)在 C 里一般是sizeof(long)或sizeof(int)取决于__nullptr的表示但sizeof(nullptr)是sizeof(std::nullptr_t)也就是 864 位平台。如果你想确认某个变量的类型大小直接 sizeof(变量) 就行没必要纠结值。第三在使用变长数组VLA的 C99 环境下sizeof 是少数几个可以在运行时求值的例外。比如int n 10; int arr[n]; printf(%zu\n, sizeof(arr)); // 40在运行时算这种场景下编译器没法在编译期确定大小只能生成运行时求值的代码。但这属于 C99 的可选特性而且 C 标准不支持 VLA有些编译器如 GCC 在 C 里做了扩展支持所以大多数人用不到这个例外知道有这回事就行。1.2 为什么总有人问 sizeof 要不要头文件搜“sizeof 需要头文件吗”的人多半是编译时遇到了类似error: size_t does not name a type这种报错。问题不出在 sizeof 本身而出在 size_t 上。sizeof 的返回值类型是 size_t而 size_t 是在标准头文件里通过 typedef 定义的。在 C 里它通常定义在stddef.h、stdlib.h、stdio.h等多个头文件中你只要包含了任意一个就可以用。在 C 里cstddef、cstdlib、cstdio等都有定义。所以正确说法是sizeof 本身不需要包含任何头文件就能用它是关键字但如果你要声明一个 size_t 变量或者用printf(%zu, sizeof(x))这种格式打印那就得保证 size_t 可见。很多老教材用的%lu在 64 位 Linux 下其实是不严谨的应该用 C99 的%zu才是跨平台安全的写法。这里顺便说个冷知识sizeof 的结果类型 size_t 在 32 位平台是 unsigned int在 64 位平台是 unsigned long对应到printf的格式说明符一个用%u一个用%lu跨平台对齐很麻烦。标准委员会后来专门加了%zu就是解决这个问题的。你自己写日志代码的时候老老实实用%zu别再用%lu糊弄了。2. 基本数据类型的 sizeof别再背“int 固定 4 字节”了很多教材为了方便考试直接告诉你 int 是 4 字节、char 是 1 字节、float 是 4 字节、double 是 8 字节。这话在大多数 PC 平台上没错但它不是 C/C 标准强制规定的。标准只规定了最小范围char 至少 8 位short 至少 16 位int 至少 16 位long 至少 32 位整个系统按 8 比特字节实现。也就是说在某些罕见的嵌入式平台或 DSP 上int 可以是 2 字节sizeof(int) 就等于 2。所以严谨的做法是用sizeof(类型)来获取而不是硬编码。特别是你写网络协议解析、二进制文件读写、跨平台通信库这类代码时一旦假设 int 就是 4换个平台就全崩。2.1 不同平台下基本类型的真实大小以最常见的 x86/x64 平台为例各基本类型的大小如下表类型32 位平台64 位平台标准保证的最小值char111short222int442long48Linux/ 4Windows4long long888float44-double88-long double8/10/1616Linux/ 8Windows-bool11-指针48-注意表格里 long 在 64 位平台上是分叉的Linux 的 LP64 模型里 long 是 8 字节Windows 的 LLP64 模型里 long 还是 4 字节。这就是很多人把代码从 Linux 移植到 Windows 发现结构体大小对不上的根本原因。你如果写跨平台代码要么别用 long直接用 long long 或 int32_t/int64_t 这种固定宽度类型要么就老老实实接受 sizeof 的差异。还有 long double这个更坑。x86 上它可能占 10 字节80 位扩展精度但编译器往往按 12 或 16 字节对齐存储在 MSVC 下 long double 干脆和 double 一样就是 8 字节。你要是序列化这种东西请直接放弃——它没有跨平台二进制表示的标准。2.2 size_t、指针大小与顶层/底层指针指针的 sizeof 规律比较简单但有几个值得说的点。第一同一平台下不同类型的指针大小通常相同比如sizeof(char*)、sizeof(int*)、sizeof(struct xxx*)在 64 位平台上都是 8。因为它们存的都是内存地址地址的宽度由系统总线位数决定。但函数指针不一定有些嵌入式平台的函数指针和数据指针大小不同MSVC 在 x86 下还支持__near、__far等修饰符会让指针大小产生差异。所以函数指针的 sizeof 别想当然。第二指针变量和被指向的内容大小完全不同。sizeof(ptr)是指针自身的大小sizeof(*ptr)是它指向的那个对象的大小。比如struct BigStruct { long long data[128]; }; BigStruct* p new BigStruct(); printf(%zu %zu\n, sizeof(p), sizeof(*p));打印结果是8 1024。这里sizeof(p)就是 864 位平台sizeof(*p)是整个结构体的大小。很多人写 malloc 的时候容易栽在这上面malloc(sizeof(BigStruct))分配了正确大小但要是误写成malloc(sizeof(p))那就只分配 8 字节后面一访问就崩溃。第三顶层/底层指针也叫顶层 const 和底层 const的 sizeof 完全一样因为它只和“指针本身是指针”这个事实有关不关心所指对象是 const 还是非 const。const int* p、int* const q、const int* const r这三种在 64 位下全都是 8。你搜“顶层指针和底层指针可以相互赋值吗”的时候大概也查过 const 修饰符的转换规则这里顺带说一句它们之间的赋值转换影响的是类型检查不影响存储大小。3. 数组与字符串的 sizeof数组名不是指针这是 sizeof 最常见的误解区。无数人在函数参数里写sizeof(arr)以为能得到整个数组的大小结果拿到的却是 8一个指针的大小。核心问题就一句话数组名在大多数表达式中会退化为指针但在 sizeof 中不退化。C/C 标准规定得非常明确当数组名作为 sizeof 的操作数时它代表整个数组对象不是第一个元素的地址。所以sizeof(数组名)得到的是“整个数组占用的字节数”而sizeof(指针)得到的是地址本身的大小。这两者的结果往往不一样。3.1 一维、二维数组的 sizeof 计算一维数组最简单int a[10]; printf(%zu\n, sizeof(a)); // 404 * 10 printf(%zu\n, sizeof(a[0])); // 4一个 int 的大小要算元素个数标准写法是sizeof(a) / sizeof(a[0])结果是 10。这里注意sizeof(a[0])其实可以换成sizeof(*a)因为*a就是 a[0]。在 C 里更推荐用 std::size(a)C17 起它在编译期直接返回数组元素个数语义更明确。二维数组稍微绕一点。设int b[3][4]那么sizeof(b)整个二维数组大小等于 3*4*4 48。sizeof(b[0])第一行“整个数组”的大小等于 4*4 16。sizeof(b[0][0])单个元素的大小等于 4。注意b[0] 也是一个数组名在 sizeof 里同样不退化。所以sizeof(b) / sizeof(b[0])得到的是 3第一维长度sizeof(b[0]) / sizeof(b[0][0])得到的是 4第二维长度。这种嵌套写法在遍历二维数组时非常实用改成 3 行 5 列、4 行 7 列都不用改循环边界直接按 sizeof 算。3.2 字符串与 \0 的恩怨字符串和 sizeof 的纠葛多半是 \0 导致的。看这段char str1[] hello; const char* str2 hello; printf(%zu %zu\n, sizeof(str1), sizeof(str2)); printf(%zu %zu\n, sizeof(str1), strlen(str1));sizeof(str1)是 6因为字符串字面量 hello 在内存里实际是{h,e,l,l,o,\0}数组长度必须包含结尾的 \0。sizeof(str2)是 8因为 str2 是个指针存的是那个字符串常量的地址。strlen(str1)是 5它数到 \0 就停不包含结束符。所以当你写char buf[64]; strcpy(buf, hello);要判断缓冲区够不够应该用sizeof(buf)和 strlen 的结果去比而不是sizeof(strlen(...))。sizeof(strlen(...))是 size_t 类型的大小一般是 8这意味着你永远只能判断字符串长度是否小于“size_t 的宽度”而不是缓冲区剩余空间这显然是个逻辑错误。字符串数组还有个细节如果你用char str[][16] {...}二维数组存多个字符串每行必须以 \0 结尾否则后续的 printf(%s) 会越界读到下一行甚至其他内存区域。sizeof 计算行数的时候用sizeof(str) / sizeof(str[0])很可靠因为sizeof(str[0])是 16包含为 \0 预留的空间不管实际字符串多短。3.3 数组作为函数参数时的退化陷阱把一个数组传进函数函数参数里写int arr[]还是int arr[10]其实都是一回事——编译器自动把它调整成int* arr。所以在函数内部sizeof(arr)就是 8不是 40。这个规则害人不浅很多人没意识就写错了。void foo(int arr[]) { printf(%zu\n, sizeof(arr)); // 8不是 40 }解决方案有两种。一是同时传入数组长度这是 C 里的传统做法。二是把参数定义成“指向数组的指针”void foo2(int (arr)[10]) { printf(%zu\n, sizeof(arr)); // 40引用保留数组类型 }但这种写法限定了长度为 10不够通用。真正的工程推荐是无论哪种方案调用方有义务把数组长度一并传过去函数内部不要依赖 sizeof。这算 C/C 里最容易踩的坑之一我见过不少线上崩溃是因为一个工具函数里用 sizeof(arr) 去计算输入缓冲区的容量。4. 函数与函数指针为什么 sizeof(函数名) 会编译报错函数能不能用 sizeof直接给你结论标准 C/C 不允许对函数名求 sizeof编译器会报error: invalid application of sizeof to a function type之类的错误。原因是 C/C 的对象模型里函数不是“对象”它没有地址空间意义上的“大小”概念。虽然它在内存里有代码段但代码段的长度不是类型系统关心的事编译器不维护函数占多少字节这个元信息。4.1 函数不是对象函数指针才是函数本身不能 sizeof但函数指针完全可以。函数指针存的是函数的入口地址它就是个普通的指针变量在 64 位平台上占 8 字节。#include stdio.h int add(int a, int b) { return a b; } int main() { int (*pfn)(int, int) add; printf(%zu\n, sizeof(pfn)); // 8 printf(%zu\n, sizeof(*pfn)); // 错误没法对函数类型求 sizeof return 0; }sizeof(*pfn)会编译失败因为*pfn的类型是int(int, int)一个函数类型。你唯一能做的大概是sizeof(*pfn)但*pfn和pfn在语义上是同一个地址结果还是 8。想通过 sizeof 拿到“函数代码占多少字节”是做不到的。4.2 两种允许 sizeof(函数) 的“奇技淫巧”准确说标准里有一条特殊例外C11 和 C11 开始sizeof(函数名)在“不执行函数”的前提下结果被定义为 1。这个 1 不是函数的大小而是标准规定的一个“占位值”主要是为了支持泛型编程场景里 sizeof 可以作用于任何类型表达式。但实际编译器基本都不推荐你依赖这个值因为它没意义。GCC 在 GNU 模式下还有一个扩展sizeof(函数名)会按照函数的机器码长度给出一个估计值。这个值在不同优化级别下不一样比如开启 -O2 后内联函数可能膨胀行内汇编的函数可能更长。你要是好奇可以写个空函数试一下但千万别拿它来做内存估计或反汇编分析因为结果既不可移植也不稳定。4.3 类成员函数对对象大小的影响类里的普通成员函数不占对象空间。对象的大小只由数据成员决定以及后面要说的虚表指针。所以一个类有 100 个成员函数和只有 1 个成员函数对象大小一模一样。但是虚函数不同——只要类里有虚函数无论是自己声明的还是继承来的对象里就会多一个指向虚函数表vtable的指针 vptr在 64 位平台这 vptr 占 8 字节。虚函数本身不直接占对象空间但 vptr 间接地代表了虚函数机制的存在。这个放到“类的 sizeof”一节重点说。5. 结构体的 sizeof内存对齐是重头戏结构体的 sizeof 是所有类型里最值得花心思的。很多人以为结构体大小就是成员大小之和但实际编程里你会发现计算结果和预期不一致原因就是内存对齐alignment和补白padding。内存对齐是什么一句话解释CPU 读取内存时不是按字节取而是按字4 字节或 8 字节取的。如果你的 int 变量地址不是 4 的倍数CPU 就要访问两次内存才能读全性能损失很大。所以编译器会给每个成员分配一个合理的对齐边界让它们的偏移量满足对齐要求。5.1 从两个结构体实例说起看两个内容一样、顺序不一样的例子struct A { char a; // 偏移 0 int b; // 需要 4 字节对齐偏移 4~7 char c; // 偏移 8 }; // 总大小对齐到 4 的倍数12 struct B { int b; // 偏移 0~3 char a; // 偏移 4 char c; // 偏移 5 }; // 总大小6 对齐到 4 的倍数8struct A 里a 占了偏移 0b 不能从偏移 1 开始因为 int 对齐数是 4所以编译器在 a 后面补了 3 个填充字节paddingb 从偏移 4 开始。c 在偏移 8结构体结束当前大小是 9但整个结构体的对齐数是成员里最大的对齐数也就是 4所以必须被 4 整除于是尾部又补了 3 个字节最终大小 12。struct B 就聪明多了把 int 放前面两个 char 排后面总大小 6对齐到 8。同样一组成员差距整整 4 个字节。如果你写的是需要存储几百万个结构体的内存池或分布式消息队列这 4 字节的差别就是几 MB 内存的差别。5.2 对齐规则详解与成员重排结构体的对齐规则可以总结为三条每个成员变量在结构体内部的偏移量必须是“该成员类型对齐数”的整数倍。char 对齐数是 1short 是 2int 是 4double 在 64 位平台是 8。结构体自身的大小必须是“结构体内部最大对齐数”的整数倍不足要在尾部补齐。嵌套结构体的情况下内嵌结构体相当于一个“成员”它的偏移量需要按照内嵌结构体自身的最大对齐数来对齐。知道了规则就能主动优化成员顺序了。基本原则把对齐要求高的成员double、long long、指针放在前面把对齐要求低的char、bool放在后面能够有效减少填充字节。上面的 struct A 排成 struct B 的顺序就是从 12 字节降到 8 字节的经典操作。但也不要走极端。有些代码风格要求按语义分组排列成员不适合强行重排尤其是涉及序列化或协议映射时成员顺序本身就是协议的一部分不可能随意调整。5.3 嵌套结构体、数组成员与位域嵌套结构体先看例子struct Inner { char a; int b; // 自身大小 8对齐数 4 }; struct Outer { char c; // 偏移 0 Inner in; // 偏移 4因为 Inner 的对齐数是 4 char d; // 偏移 12 }; // 总大小 16尾部补齐到 16有人以为 Outer 大小是 1 8 1 10但实际是 16。因为 Inner 的最大对齐数是 4所以偏移量必须是 4 的倍数c 后面被塞了 3 个填充字节。这里的关键是内嵌结构体的“对齐数”就是它内部最大成员的对齐数不是它自身的大小。数组成员的规则更简单数组的对齐数是“元素类型的对齐数”不是数组总长度。比如char buf[10]的对齐数是 1int buf[10]的对齐数是 4。结构体里定义数组成员后偏移规则和单元素一样只是数组本身是一个连续占用空间的对象。位域bit-field是另一个坑。位域是按位分配的编译器可以把多个位域塞进同一个字节或同一个字里也可以不塞具体行为是 implementation-defined 的。用 #pragma pack 控制对齐时位域的存储规则还会随编译器和目标平台改变序列化含位域的结构体到外部设备时务必核对目标平台的对位方式。空结构体还有个小知识点在 C 语言里struct Empty {}是不合法的编译器会报错因为 C 标准要求结构体至少有一个命名成员。但在 C 里struct Empty {}是合法的sizeof 结果是 1原因后面在类里详细说。5.4 用 #pragma pack 控制对齐的真实场景默认对齐规则在多数情况下是友好的但写网络协议、嵌入式寄存器映射、音视频编解码结构体时你往往希望结构体严格按协议规定排列不塞任何填充字节。这时候就用#pragma pack#pragma pack(push, 1) struct ProtocolHeader { uint8_t version; uint16_t length; uint32_t crc; }; #pragma pack(pop)pack(1) 表示所有成员对齐到 1 字节即不填充最终大小为 1 2 4 7不是默认的 8。这个 1 字节偏移在你解析 Socket 缓冲区、直接强转一个指向接收缓冲区的结构体指针时特别重要否则结构体成员之间多出来的填充字节会直接把数据解错。注意 pack 是预处理器指令不是标准 C/CMSVC 和 GCC/Clang 都支持但他们支持的语法略有差异。别在公共头文件里到处乱放 pack要用的时候局部开启用完马上 pop 恢复。另外 pack(1) 虽然压掉了大小但牺牲了对齐性能大量分配这类结构体时可能引发未对齐访问在 x86 上问题不大但在 ARM 上可能直接触发总线错误。要不要用 pack取决于你是“省内存”还是“访问速度”优先。6. 类的 sizeof空类、虚函数与继承C 的类在内存布局上比结构体复杂得多但 sizeof 的核心规则仍然基于同一套对齐思想。进入 C 之前先记住三个现行标准下的默认事实非静态成员变量占对象空间静态成员变量不占对象空间成员函数不占对象空间。6.1 静态成员、成员函数不占对象空间class Widget { public: void func(int a, int b) {}; // 不占对象空间 static int s_value; // 不占对象空间 int m_value; // 占 4 字节 };sizeof(Widget) 的结果是 4。函数和静态变量都不影响这个对象的大小因为它们不属于任何“单个对象”函数在代码段共享静态变量存在全局数据区。很多游戏引擎为了在对象池里记录每个组件的类型会把类做成空类或只含一个枚举的类此时它的大小就很干净。不过 static 成员如果不占空间那Widget::s_value的地址是什么呢它指向全局区的变量和对象地址没有关系但你可以把 static 成员变量的类型和大小用 decltype 推导出来再 sizeof 它这是可以的因为 sizeof 作用于类型。6.2 空类为什么是 1 字节C 里只要类的大小编译器不能为 0否则两个不同的对象在内存中的地址就是同一个这会直接破坏指针比较和容器算法的前提。所以编译器给空类安排 1 字节的占位保证不同的空类对象地址不同。但这里有几个例外一个空类被继承时如果基类部分是空的经过空基类优化EBO, Empty Base Optimization基类那 1 字节往往会被“吃掉”派生类的大小可以还是 1或者干脆是 0 优化后的 1。另一个例外是空类作为成员变量时和普通成员一样占 1 字节还要考虑对齐填充。比如class Empty {}; class Derived : public Empty { int m_value; }; // 大小通常还是 4不是 8因为基类的 1 字节被优化掉了 class Holder { Empty e; // 占 1 字节 int m; // 对齐到 4偏移 4 }; // 大小 8因为有填充EBO 在实现标准库的容器适配器、函数对象、智能指针等元编程设施中极其常见因为很多类内部会组合一个空类型只有通过 EBO 才能保证不额外膨胀。6.3 虚函数带来的 vptr只要类里声明了虚函数编译器就会为对象增加一个隐藏成员——虚表指针 vptr指向这个类的虚函数表 vtable。vptr 的大小等于指针大小64 位平台是 8 字节。虚函数本身不占对象空间它们的实现代码放在代码段里但安排虚函数机制的这个 vptr 会显著影响类对象的大小和对齐。class Base { public: virtual ~Base() {} int m_a; };这个类的大小在 64 位平台是 16vptr 占 8 字节int 占 4 字节尾部填充 4 字节凑成 16对齐到 8。如果写成int m_a; virtual ~Base() {}当然了成员顺序和虚函数声明顺序无关vptr 一般放在对象的起始位置或末尾视编译器 ABI 而定。在 Itanium C ABILinux/GCC/Clang上vptr 通常在对象偏移 0 处MSVC 上vptr 也可能在偏移 0 处但多继承时会有多个 vptr 交叉分布。所以“一个虚函数加 8 字节”这个说法成立但只在这个类只有一个 vptr 的前提下成立。多继承更绕如果派生类从两个各带虚函数的基类继承那么派生类对象里通常有两个 vptr分别对应两个基类子对象的虚表。如果继承链上某个基类只有一个 vptr但派生类自己又新增虚函数那么通常复用第一个基类的 vptr不会额外增加第二根 vptr——除非必要编译器会尽可能复用已有的 vptr 来减小对象大小。这里规则比较繁琐建议是理解“vptr 数量和虚基类/多继承有关”不要凭感觉猜 sizeof建议直接打印验证。6.4 继承与虚继承对大小的影响单继承不引入虚函数时派生类对象大小约等于“基类子对象大小 派生类新增成员大小”再按新的对齐数补齐。举一个例子struct Base { int a; }; // 4 struct Derived : Base { char c; }; // 先 5对齐到 4结果 8为什么不是 5因为 Derived 的最大对齐数是 4来自 Base 的 int尾巴要补到 4 的倍数所以是 8。如果你不希望这个填充可以重排 Base 的成员struct Base { char c; int a; }在 Base 自己内部大小为 8Derived 再加 char 到 9对齐到 4 变成 12反而变大。可见成员排列至少要在基类这一层动手不能只看派生类。虚继承就更复杂了。C 标准规定虚基类子对象在派生类中只存在一份即使多条继承路径都派生自同一个虚基类也只会有一个副本。为了实现这一点编译器会在每个虚继承的路径上插入一个指向虚基类子对象的指针vbptrvirtual base pointer。于是 sizeof 会额外增加指针的大小而且不同继承路径上插入指针的位置不一样总体大小非常可能大于普通继承。有个经典例子class A { public: int x; }; // 4无虚函数 class B : virtual public A { public: int y; }; // vbptr 8 y 4 A 4 对齐 16 class C : virtual public A { public: int z; }; // 同样 16 class D : public B, public C { public: int w; }; // 通常 32 或 40D 里两个虚基类子对象路径分别有 vbptrB 和 C 各带一份 A但最终只保留一个 A。到底多大取决于编译器实现和成员顺序。这就是为什么大型继承体系里不能靠直觉算 sizeof必须用 static_assert 或运行时打印验证并且在跨平台移植后回归测试。7. 联合体的 sizeof最大成员说了算但没这么简单联合体union的内存规则和结构体相反所有成员从同一地址开始因此联合体对象占用的存储空间必须能放下“最大”的那个成员。计算公式是联合体大小等于最大成员大小且对齐到所有成员对齐数的最大值。union Data { char buf[13]; uint32_t num; double d; };成员最大是 buf[13]13 字节但 d 的对齐数是 8所以联合体对齐到 8容量也要补到 8 的整数倍最终 sizeof(Data) 是 16。奇怪吗因为 union 的起始地址必须按所有成员的最大对齐数对齐所以实际占用空间中可能有尾部填充保证数组下一个元素也能对齐。7.1 联合体大小的计算规则规则就是两条对齐数取最大大小取成员最大值然后向上取整到对齐数的倍数。把它和结构体对比一下数据类型计算方式示例结构体所有成员偏移按对齐规则排布可能存在填充char int 可能是 8联合体所有成员从偏移 0 开始大小 最大成员大小尾部补到对齐数的倍数char[13] double 是 16如果联合体成员里有结构体计算过程也一样先算出各结构体成员自己的大小和对齐数再比较谁最大、谁的对齐要求最高最后得到联合体大小。注意联合体自身不能包含非平凡类型C11 之前像 std::string、std::vector 这类带非平凡构造函数/析构函数的类型C11 之后才允许出现在 union 里但你必须手动调用它们的构造和析构很容易出错。除非万不得已别在生产代码里往 union 塞 STL 容器。7.2 联合体在协议解析和类型转换中的应用联合体最经典的应用场景是“同一块内存按不同方式解释”。比如解析一个二进制协议的头部可以同时定义一个 uint32 版本和四个 uint8 的字节数组版本union ProtocolVersion { uint32_t as_uint; uint8_t bytes[4]; };因为你没法跨平台保证字节序所以写入方用小端序写成 bytes[0]、bytes[1] 这种字段时as_uint 在大小端机器上读出来的值不同。Union 方便调试时选择哪种视角看数据但不能用来做跨平台字节序转换——那应该用移位运算符而不是依赖内存布局。另一种常用是 C 风格的类型双关。比如把 float 的二进制位原样读成 uint32_tunion FloatBits { float f; uint32_t bits; }; FloatBits fb; fb.f 3.14f; printf(0x%08x\n, fb.bits);但 C 标准对此的行为是未定义的虽然大多数编译器实测可用所以写 C 新代码时更推荐用 memcpy 或 C20 的 std::bit_cast。union 适合的是阅读协议、压缩存储而不是投机取巧做类型转换。联合体还有个实际好处是省内存。如果你处理的是事件消息消息结构因类型不同而字段不同就可以用“公共头 union 载荷”的方式来建模比让所有事件都包含所有字段省很多内存。这种模式在网络框架、GUI 系统里特别常见。配合结构体使用时务必记好“一次只能激活一个成员”的原则——你往一个成员写了值再用另一个成员去读除了上面说过的位模式重新解释场景外其他情况下结果都不可预期。8. 实际开发中 sizeof 的常用姿势与避坑清单前面把各种类型的计算规则讲完了这里收拢一下工程里最常见的 sizeof 用法和容易踩的坑都是实打实的经验不是面试题。8.1 数组元素个数、动态分配与缓冲区最常用的姿势是计算数组元素个数。C 语言里没有 std::size所以传统写法就是#define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0]))这个宏在 C 和 C 里都能用但你得保证传入的是真正的数组不是指针。如果传入int*表达式就变成了sizeof(int*) / sizeof(int)在 64 位平台结果是 2错误隐蔽得很。C 里如果想让编译器帮你检查可以写成模板templatetypename T, size_t N constexpr size_t array_size(T ()[N]) { return N; }传指针进来直接编译失败算是 C 的一个安全改进。动态分配的核心原则是“sizeof 对象类型不 sizeof 指针”。写int* p (int*)malloc(sizeof(int) * n); // 对 int* p (int*)malloc(sizeof(p) * n); // 错这是指针大小C 中用 new 申请数组时也有类似误区new int[n]编译器自动按 int 大小分配了你只需要在释放时写delete[] p不要再去算总字节数。缓冲区场景里要区分“缓冲区总大小”和“字符串长度”。比如接收网络数据后拼接字符串snprintf(buf, sizeof(buf), %.*s, tmp)里的 sizeof 用于指定缓冲区容量这是正确的但如果你算的是 strlen(tmp)就限制不了整个缓冲区的长度要小心。8.2 通信协议中的对齐问题通信协议是把结构体直接映射到字节流的最典型场景。你从 socket 收到一段数据最粗暴的做法是把指针强转成结构体指针char recv_buf[1024]; recv(sock, recv_buf, sizeof(recv_buf), 0); ProtoMsg* msg (ProtoMsg*)recv_buf;这种做法有用但要小心两点一是结构体的对齐方式要和服务端、协议定义一致最好显式#pragma pack(1)二是字节序问题x86 是小端若协议规定大端直接强转后数值是反的必须手动交换字节序。用 sizeof(ProtoMsg) 来校验接收缓冲区长度的时候如果接收缓冲区含可变长度字段比如长度头 可变 payloadsizeof 只能算固定头部实际消息总长要用头部里的长度字段算。另外直接在强转后访问结构体成员如果结构体里的 int 成员在 unaligned 地址上某些平台会崩溃。x86 能在硬件层面容忍未对齐访问只是慢ARM 上可能直接 SIGBUS。所以与其在字节流上硬套结构体不如用 memcpy 把字段逐段拷出来再按需转换字节序。8.3 sizeof 在模板和静态断言中的运用C 里 sizeof 最强大的一点是它可以和编译期机制配合在编译期检查某些条件。写一个static_assert(sizeof(int) 4, int must be 4 bytes!);这个静态断言在编译期就检查不同平台编译时就能发现平台假设不对。类似的还有检查结构体布局是否符合预期static_assert(sizeof(ProtocolHeader) 8, ProtocolHeader size mismatch!);如果你的代码对某个结构体做了序列化或反序列化这些断言能阻止你在改了成员后悄悄破坏协议格式。在模板元编程里sizeof 还可以用来做类型分发。比如通过sizeof(类型) sizeof(void*)判断能否把对象按值传给函数或者判断类型是否“小到可以放进寄存器”。更典型的应用是实现 SFINAE 里的“找出最大成员”式 trait不过那些场景偏进阶。对普通项目而言static_assert 和数组元素计数就是 sizeof 最有价值的两大工程用法。8.4 典型面试题速查最后整理几个高频 sizeof 题大家可以直接自测。答案和解析写在后面建议先自己想一遍题目 1在 64 位 Linux 下struct S { char c; int i; double d; };的 sizeof 是多少正确答案是 16。c 在偏移 0i 对齐到 4 所以从 4 开始到 7d 对齐到 8 所以从 8 开始到 15总大小 16。如果换一种顺序struct S2 { double d; int i; char c; };这个大小是 16 还是 8d 0~7i 8~11c 12总大小 13对齐到 8 的倍数就是 16。这两个在 64 位 Linux 下结果一样但我曾经试过在 ARM 平台、某些老嵌入式编译器下对齐数按照 4 处理 double结果会变成 12 或 16 的不确定性。所以面试题只是在固定平台下成立真正的工程问题要用 sizeof 实测。题目 2sizeof(hello)、sizeof(char[5])、sizeof(char*)、strlen(hello)分别是什么sizeof(hello)是 6字符串字面量包含结尾 \0。sizeof(char[5])是 5字符数组容量就是 5不强制要求最后一个字符是 \0但若是当作字符串用必须自己保证。sizeof(char*)是 864 位strlen(hello)是 5。这四者的差异就是“数组类型大小”“指针大小”“字符串运行时长度”三个概念的区别。题目 3一个空类为什么 sizeof 是 1一个类有虚函数后sizeof 怎么变化加一个静态成员变量呢空类占 1 字节保证地址唯一。有虚函数后8 位平台上加 8 字节的 vptr原始的空类本身仍可能占 1 字节最后大小通常 8 或 16取决于成员。静态成员不占对象空间所以加多少个静态成员都不影响 sizeof。题目 4联合体里放一个int和一个char[7]sizeof 多少成员最大是 7int 对齐数 4最终对齐到 4 的倍数所以是 8。如果把 char[7] 改成 char[9]、加一个 double那大小变成 16double 对齐 8容量补齐到 16 的倍数。题目 5函数指针的 sizeof 是多少取决于平台64 位环境下通常是 8和普通指针一致。但如果目标平台有分段内存模型x86 实模式、旧式嵌入式函数指针可能是 4 或更特殊的结构。在现代 PC 编程里可以安全地当作 8但跨平台校验时别写死。这些题说穿了就是“对齐 类型退化 虚指针 静态成员不占空间”的组合。明白规则之后其实没什么玄学。写代码这么多年我自己的体会是sizeof 是那种“平时不觉得有用错一次才长记性”的关键字。尤其是处理网络协议、嵌入式寄存器映射、跨语言通信缓冲的时候一个结构体的对齐方式错了线上数据就会烂得悄无声息。所以我现在的习惯是凡是被序列化或跨模块传输的结构体在定义旁边一定加一行 static_assert(sizeof(结构体) 预期值)并写个单测打印每个成员的 offset。每次有人在结构体里新增字段测试立刻就会告诉你大小变了协议要不要升版本、其他模块要不要同步改一目了然。最后再分享一个小技巧你要是实在记不住某个类型在当前平台的大小直接编译一个小程序跑一下比查任何文档都快。C/C 的命令行工具链里随便敲一个临时源文件printf 一行 sizeof答案就在眼前。别凭直觉写硬编码这是 sizeof 教给我的第一课也是最后一课。