C/C++ sizeof 完全指南:编译期求值、内存对齐与常见陷阱

发布时间:2026/9/30 5:50:24
C/C++ sizeof 完全指南:编译期求值、内存对齐与常见陷阱 1. sizeof是编译期算出来的关键字不是函数1.1 为什么必须摆脱sizeof是一个函数的错觉先说结论sizeof是C/C语言里的运算符也有人叫操作符、关键字不是函数也根本不存在sizeof函数需要头文件这一说。你不需要#include任何头文件就能用它这跟strlen、memcpy完全是两码事。怎么验证它是运算符而不是函数最直观的一点** sizeof 加不加括号都能用 **。对变量操作时完全可以写sizeof x像sizeof(x)这样带括号只是为了保证在复杂表达式或类型名场景下不会产生歧义。函数能有这种不带括号的写法吗显然不能。另一个关键区别是求值时机。sizeof是编译期求值的也就是说编译器在编译阶段就把结果算出来了生成的是常量后面会提到一个C99变长数组的例外运行时不产生任何计算开销。而strlen是运行时函数它必须拿着你的指针去内存里一个字符一个字符地数直到遇到\0为止。这个差异在实际开发中影响非常大。我见过不少人写代码时拿sizeof当求字符串长度用结果字符串一长、数据一变程序就出各种莫名其妙的bug。看一段典型的错误代码#include stdio.h #include string.h void print_str(char *s) { printf(sizeof result: %zu\n, sizeof(s)); // 编译期就算完结果是8 printf(strlen result: %zu\n, strlen(s)); // 运行时数出来的长度 } int main() { char msg[] hello world; print_str(msg); return 0; }msg本身是有11个字符加一个\0的12字节数组但一旦作为参数传进print_str形参char *s就是一个指针sizeof(s)在64位平台上一律返回8。这是C/C面试题里的老面孔也是实际开发中传数组丢尺寸问题的根源。要拿到数组的真实尺寸得在数组还没退化成指针的地方下手具体方法后面章节详说。1.2 编译期求值带来的实用推论因为sizeof是编译期常量它在代码里能干一些运行时函数绝对干不了的事。第一可以作为数组维度。C标准里要求数组长度必须是编译期常量所以这样写在很多编译模式下是合法的int arr[sizeof(int) * 4]; // 16个int的数组如果你试图用strlen的返回值去定数组维度那编译器基本不可能让你通过因为strlen是运行时函数返回值不是常量表达式。第二可以配合宏或模板做编译期断言。比如古老经典的ASSERT_SIZEOF宏#define STATIC_ASSERT(cond) typedef char static_assertion[(cond) ? 1 : -1] STATIC_ASSERT(sizeof(int) 4);如果sizeof(int) ! 4编译器会直接报错这就把检查从运行期提前到了编译期。C11以后有了static_assert关键字写法更干净但本质一样——依赖的都是sizeof是编译期常量这一特性。第三** 对空指针解引用是安全的 **。注意这里的解引用只是形式上解引用实际上根本没有访问内存struct Packet { int len; char payload[128]; }; void process(struct Packet *p) { if (p NULL) { // 这里不会崩溃因为sizeof不产生运行时访问 size_t size sizeof(*p); printf(packet size: %zu\n, size); } }很多人第一次看到sizeof(*p)且p NULL时心里发怵觉得这是空指针解引用会崩。其实完全不会因为sizeof根本不读p指向的内存它只关心*p这个表达式的类型然后算类型大小。这类代码在写分配器、协议解析时很常见省去了你为了拿类型大小而临时定义一个变量的麻烦。2. 基本类型和指针的大小32位与64位平台差异一表看懂2.1 各平台类型大小对照表基本数据类型的大小并不是固定不变的它取决于平台的数据模型。x86 32位Linux用的是ILP32模型x86_64 64位Linux下常用的是LP64模型而64位Windows用的是LLP64模型。这里头最容易踩的坑就是long。先上64位LinuxLP64下常见的类型大小类型32位Linux64位Linux64位Windowschar111short222int444long484long long888float444double888void*488注意long这一行。在64位Linux下long是8字节在64位Windows下long仍然是4字节。如果你的代码在一个平台上sizeof(long)等于8跑到另一个平台变成4而你把这个值写进了文件或通过网络协议传给对端那基本就是一场灾难。我实际碰到过一个案例某服务端程序用long存时间戳在Linux上写完文件Windows客户端去解析结果所有字段整体错位。排查了大半天最后发现就是long的平台差异。结论是** 涉及跨平台数据交换时别用int、long、long long这种模糊类型直接上stdint.h/cstdint里的大小明确类型**比如int32_t、uint64_t它们的sizeof是精确固定的。2.2 指针统一大小与const不影响大小的真相不管什么类型的指针在同一平台上的大小总是相同的。32位平台上所有指针都是4字节64位平台上所有指针都是8字节。char*、int*、struct XXX*、函数指针、以及两层三层的int**在64位平台上一律是8字节。因为指针存的是内存地址地址线的宽度决定了指针的宽度和它指向的目标类型无关。有人会问void*是8那int*呢也是8。int**呢还是8。这个规则一记就能用一辈子。还有个容易被忽略的点const不改变sizeof的结果。const char*和char*大小一样char * const也一样const char * const还是一样。const是编译期约束作用是限制代码能不能改这个值而不是在内存里给指针多加什么空间。顺便说一句热搜词里提到的顶层指针和底层指针可以相互赋值吗——这个问题和sizeof没有直接关系它是C里关于const修饰指针语法的问题。顶层const如int * const p表示指针本身不可变底层const如const int *p表示指向的对象不可变。底层const约束的是能不能通过这个指针修改目标两个指针之间能不能赋值取决于底层const是否兼容但这跟sizeof的结论无关这两类指针在内存里占的空间完全一样。3. 数组和字符串sizeof最容易被绕进去的场景3.1 能拿到数组大小却传不进函数sizeof作用于数组名时返回的是整个数组占用的字节数这是一个让很多人最开始感到兴奋、后来又反复吃亏的性质。经典操作是求元素个数int arr[] {1, 2, 3, 4, 5}; size_t n sizeof(arr) / sizeof(arr[0]); // 5sizeof(arr)是整个数组20字节5个int × 4字节sizeof(arr[0])是一个int的4字节相除得到5。这个写法成了C/C里的惯用法。但是注意千万不要写一个获取个数的通用函数size_t get_len(int arr[]) { return sizeof(arr) / sizeof(arr[0]); // 错arr是int*结果是8/4264位 }问题就出在我开头说的数组参数退化函数形参里的int arr[]和int *arr是等价的arr在这时就是一个指针sizeof(arr)是864位平台除出来自然不对。所以数组大小必须在数组还是数组的地方计算一旦跨了函数边界尺寸信息就丢了。解决思路通常是三种在调用方算好sizeof(arr)/sizeof(arr[0])连同数组一起传给函数函数参数里多传一个size_t len参数C里用模板引用推导保住数组类型后面第6章会详细写。3.2 字符数组、字符串字面量与常见的字符串误区字符串场景是sizeof重灾区。关键是理解C风格字符串本质是字符数组且必须以\0结尾。sizeof会把这个结尾符也算进去strlen不会它数到\0就停。char s[] hello; printf(sizeof: %zu\n, sizeof(s)); // 6包括结尾的 \0 printf(strlen: %zu\n, strlen(s)); // 5这里s是数组sizeof(s)返回6。但如果你写的是指针const char *p hello; printf(sizeof(p): %zu\n, sizeof(p)); // 8指针大小与字符串无关这个差异非常具有迷惑性。更迷惑的是sizeof(hello)——字符串字面量本身是char[6]类型的数组所以sizeof(hello)返回6包括\0。同样是hello当它是数组实体时sizeof是6当它被赋值给指针时再sizeof就变成8了。这导致了一个经典bug有人写代码把字符串从一个地方拷贝到另一个地方用错大小导致拷贝不完整或越界char src[] hello; char *dst (char *)malloc(sizeof(src)); // 正确6字节 // 错误做法malloc(sizeof(src) - 1) 少了结尾符strcpy虽然会写完但严格说越界了热搜词里有个数组转字符串说一下我的常用思路如果是字符数组要转成字符串往往就是确保\0存在的问题。比如char buf[10]只填了3个有效字符想把它当字符串用就得手动在buf[3]处置\0。此时sizeof(buf)这个编译期常量就能帮你判断剩余空间if (sizeof(buf) - len - 1 0)之类。sizeof在这里的价值是它知道整个缓冲区的容量而strlen只知道现有内容的长度两者搭配才能写出安全的边界判断。3.3 二维数组和结构体数组怎么算二维数组的sizeof其实不难只要记住多维数组就是嵌套的一维数组这一条。int a[3][4];sizeof(a)整个二维数组3 * 4 * sizeof(int) 4864位下其实也是48因为int是4字节sizeof(a[0])第0行也就是一个int[4]大小是16sizeof(a[0][0])一个int4求行数sizeof(a) / sizeof(a[0])即48/163求列数sizeof(a[0]) / sizeof(a[0][0])即16/44注意a[0]这个表达式的类型是int[4]它本身是数组名所以在它上面用sizeof不会退化能拿到16。这也是数组名作为sizeof操作数时不退化这条规则的一个体现。结构体数组同理struct Point pts[10]的sizeof(pts)就是10乘以单个结构体的大小。热搜里结构数组与普通数组的区别严格说从sizeof角度看不出差别——都是一段连续内存大小等于元素个数乘单个元素大小。区别在语义层面结构体数组的每个元素是多个字段的聚合排序、比较、序列化时的行为不一样但对sizeof来说规则完全一致。常见用途就是批量初始化、批量拷贝和遍历struct Point pts[3] {{1, 2}, {3, 4}, {5, 6}}; size_t count sizeof(pts) / sizeof(pts[0]); // 34. 结构体的字节对齐算不对就不是8的倍数这么简单4.1 为什么要对齐CPU的性子和内存布局的真相很多初学者第一次算结构体大小时都会翻车因为他们天真地把所有成员大小加起来。比如struct A { char c; int i; };直觉上char占1字节、int占4字节总大小应该是5。但实际sizeof(struct A)在绝大多数编译器和平台上是8。为什么因为要内存对齐。CPU从内存读数据不是一字节一字节随便读的它按照字宽读取。64位CPU一次能从内存拿8字节但访问的地址实际上往往需要对齐到8字节边界效率才会高。说得通俗一点CPU去银行柜台取钱柜台是按窗口批量发钱的你如果非让它在两个窗口之间拼凑一次取款它得多跑几个来回才能凑齐。内存对齐就是让每个成员尽量落在整块窗口里减少拼接次数。标准并没有强制规定编译器必须怎么对齐但实际操作上使用了一套自然对齐规则每个成员的对齐数等于它自身大小或者说min(自身大小, 编译器默认最大对齐值)结构体的总大小需要是最大成员对齐数的整数倍。4.2 一步步推算结构体大小例子、offsetof验证、数组拆分回到struct A { char c; int i; }的例子第0字节放cc的对齐数是1放哪都行i的对齐数是4所以它的起始偏移必须是4的倍数。不能紧挨着c放在第1字节必须挪到第4字节中间第1~3字节空出来填充padding结构体总大小现在是4488是最大对齐数4的整数倍所以sizeof等于8可以用标准库里提供的offsetof宏需要stddef.h或cstddef来验证每个成员的偏移#include stddef.h #include stdio.h struct A { char c; int i; }; int main() { printf(offsetof(c) %zu\n, offsetof(struct A, c)); // 0 printf(offsetof(i) %zu\n, offsetof(struct A, i)); // 4 printf(sizeof(struct A) %zu\n, sizeof(struct A)); // 8 return 0; }输出的偏移值能很清楚地看到c后面那3个填充字节。offsetof宏是配合结构体内存布局分析最常用的工具它本身也是编译期求值不会产生运行时开销。再看一个带数组成员的结构体。这个例子能帮你快速理解数组成员的对齐值struct B { char s[5]; int x; };char s[5]本质上是5个char每个char的对齐数是1数组整体的类型是char[5]但数组的对齐数是从其元素类型来的所以还是1当然数组本身有5字节的空间。于是x的对齐数是4从偏移4开始放s占0~4共5字节第5~7字节填充x占8~11字节总大小12。到这里为止结构体大小12也是4的倍数。如果把char s[5]改成char s[3]那就是8因为x从偏移4开始总大小8如果改成char s[7]x从偏移8开始总大小12。规律很清晰** 先把每个成员放到自身对齐数的倍数位置最后把总大小提升到最大成员对齐数的倍数 **。4.3 编译器如何算最大对齐数以及#pragma pack的影响在上面的规则里关键在于最大成员对齐数。GCC还提供了一个查看类型对齐数的操作符_AlignofC11C里对应alignof它返回一个类型的对齐要求。例如struct C { char a; double b; char c; };按默认规则double的对齐数是8所以b从偏移8开始结构体总大小是24而不是1817填充817。这个结果很多人会觉得反直觉明明double8字节 两个char 2字节一共才10字节怎么算出24原因就是对齐——b前面的7个填充和最后面的7个收尾填充都是对齐规则的代价。但对齐规则是可以人为干预的。#pragma pack(n)可以强制编译器把对齐数压到min(自然对齐数, n)。比如#pragma pack(push, 1) struct PackedA { char c; int i; }; #pragma pack(pop)pack(1)下所有成员按1字节对齐sizeof(PackedA)变为5。GCC和Clang还有个更灵活的写法__attribute__((packed))struct __attribute__((packed)) PackedB { char c; int i; };效果一样。但要注意** 强制紧凑对齐会带来性能损失和可移植性问题 **。压掉填充字节之后CPU可能需要多次内存访问才能拼出一个未对齐的int在某些架构上甚至会直接触发运行时错误。所以pack只在特定场景下推荐使用比如网络协议报文结构、磁盘文件格式、以及和硬件寄存器交互的结构体。普通业务结构体不要为了省那几个字节去 pack省下的空间和引入的麻烦通常不成正比。字节对齐还牵扯到跨平台结构体序列化即使你计算好了本平台的结构体sizeof如果代码要跨编译器和跨架构跑默认对齐规则可能不一样同一份内存 dump 到文件里在另一个平台上解析就是另一套偏移。业内通用方案是** 序列化时显式按字段顺序写而不是直接把整个结构体memcpy到缓冲区 **。如果非要用整体拷贝那就必须 pack 到1字节对齐并且用固定大小类型。谈到结构体还有热搜词里的结构体链表基本语法这里顺手带一笔。链表节点在malloc分配内存时一定要用sizeof而不是手写大小struct Node { int data; struct Node *next; }; struct Node *n (struct Node *)malloc(sizeof(struct Node));如果写成malloc(sizeof(struct Node*))那就只分配了一个指针的大小后续访问n-data必然越界。我在Code Review里见过不止一次这种错误新手很容易把节点大小和指针大小混在一起。5. 类与联合体的sizeof虚表指针、空类、智能指针都算在哪儿5.1 类的sizeof哪些成员算在内哪些不算进入C之后sizeof的规则在结构体对齐基础上又叠加了一层面向对象逻辑。核心准则如下成员变量参与计算所有非静态成员变量的字节数 中间填充 尾部填充。成员函数不参与计算成员函数是代码存在代码段不占对象实例的大小。静态成员变量不参与计算静态变量存在全局/静态区属于类而非对象。虚函数会额外增加一个虚表指针vptr只要类里有一个虚函数对象里就会隐藏一个指向虚函数表的指针64位下就是8字节。空类的大小是1C标准规定不同的对象必须有不同的地址所以空类不能真的是0字节编译器给它分配1字节占位。看几个例子#include cstdio class Empty {}; // sizeof 1 class NoVtable { public: int a; void print() {} // 不占空间 private: static int s_static; // 不占空间静态成员 }; // sizeof 4只有a class WithVtable { public: virtual void update() {} // 引入vptr int a; }; // sizeof 168字节vptr 4字节int 4字节对齐填充WithVtable为什么是16不是12因为8字节vptr的对齐数是8结构体总大小必须按最大对齐数8对齐4字节的a占用第8字节开始的位置第12~15字节是填充。注意成员声明的顺序会影响最终大小把int a放在vptr前面或者后面不改变vptr本身占位但成员的填充方式会有差异。继承场景下派生类的大小大体等于基类大小 派生类自己的成员大小再按对齐规则调整。空类继承的时候有空基类优化EBO空基类可以不再多占1字节。比如class Base {}; // sizeof 1 class Derived : public Base { public: int x; }; // sizeof 4Base被优化掉没有额外加1但如果基类里有虚函数派生类中也存在vptr相关的布局叠加计算时就要多留一份心眼。个人建议日常开发不要试图手算太复杂的继承层次结构体的sizeof而是用static_assert或者单元测试来锁定预期值防止有人改动成员声明顺序导致二进制布局突变。5.2 联合体的sizeof和典型应用联合体union的规则很简单** 大小等于所有成员中最大的那个再考虑整个联合体对齐到最大成员对齐数的倍数 **。因为联合体的所有成员共享同一块内存同一时刻只有一个成员是活跃的。union Data { char c; int i; double d; }; // 最大成员是double8字节所以sizeof(union Data) 8这里有个细节值得展开如果你有一个联合体里包含char buf[13]和一个double dchar buf[13]是13字节但联合体本身的大小不会是13也不会是18而会向上取到8的倍数因为double对齐数是8即16。联合体最有价值的场景是** 类型双关type punning**。比如网络协议解析时收到的数据包是一个字节流你可以定义一个联合体让同一块内存既能按字节数组读又能按结构体字段读union Packet { unsigned char raw[64]; struct { unsigned short id; unsigned char type; unsigned char len; unsigned char payload[60]; } fields; };不过写这类代码时要注意编译器优化下的严格别名规则strict aliasing可能产生未定义行为稳妥做法是用memcpy或者C的std::bit_cast。联合体在这里的价值是提供一个内存复用 多视图解释的模型sizeof则是帮你确认整个联合体到底占多大防止缓冲区开小。热搜词里还有qt5信号槽传递结构体和结构体变量的定义这两个和sizeof的交集在于跨模块或跨线程传结构体时接收方分配接收缓冲区的长度需要根据结构体sizeof来定。Qt信号槽本身并不关心结构体内部对齐但如果你用自定义类型做队列连接qRegisterMetaType内部注册时也会用类似sizeof(T)的逻辑来管理对象存储。不管什么框架容器要把对象装进去就意味着必须知道对象大小。5.3 智能指针和标准库容器的大小智能指针的sizeof也经常被拿到面试题里讲。我的实测结论std::unique_ptrT一般等于一个裸指针64位下是8字节。它内部只持有一个T*因为独占语义不需要额外的引用计数。std::shared_ptrT一般是16字节。因为它内部有T*指向对象和指向控制块的指针存放引用计数等在主流实现里就是两个指针。std::weak_ptrT同样16字节因为它也要访问控制块。std::vectorT、std::string这类容器大小取决于实现典型vector是24字节三指针begin、end、capacitystd::string在GCC libstdc中通常32字节SSO优化在MSVC中可能是40字节。这属于实现细节标准不保证跨平台时不要依赖具体值。这里给个实际提醒当你把std::shared_ptrT放进std::vector时vector的每个元素占16字节不能想当然认为智能指针就是指针、8字节。标准库容器的sizeof还有一个进阶应用在写对象池、内存池时你要按sizeof(T)分配块但如果T是STL容器它本身大小只是控制结构的大小真正数据在堆上所以容器对象的拷贝和移动跟深层数据无关不能因为sizeof(std::vectorint)是24就直接memcpy它。6. 函数、函数指针与VLAsizeof的最后几个边界情况6.1 为什么不能对函数本身sizeof但函数指针完全可以C标准规定sizeof不能用于函数类型incomplete type所以sizeof(func)是编译错误。原因在于函数没有对象大小这种概念它是一段代码不是一个可以拷贝、存储、移动的数据对象。但函数指针是对象它占一个指针的大小64位下就是8。区分两个概念函数指针指向函数的指针变量int (*p)(int, int)sizeof(p)是8。指针函数返回指针的函数int *f(int, int)它是函数本身sizeof(f)编译不过。这里还有一个GCC扩展需要说明。标准C里sizeof(func)不合法但GCC为了兼容老代码做了扩展允许对函数名使用sizeof并按1处理。所以你如果在Linux下用gcc编译以下代码void f() {} int main() { printf(%zu\n, sizeof(f)); // GCC扩展输出1严格模式下报错 return 0; }可能得到1而不是报错。但这是编译器扩展不是标准行为。我在项目里通常把代码开成-stdc99 -pedantic-errors之类的严格模式宁可编译期报错也不依赖这种能运行但没标准保障的行为。函数指针的sizeof恒等于普通指针大小这也启发我们函数指针和void*并不能保证互相转换。POSIX标准里有dlsym这类API它返回void*你拿它去转函数指针在标准C里是未定义行为很多编译器允许但严格模式下有隐患。理解了函数指针是8字节、函数本身无法sizeof以后你对这类问题的边界感就更清楚了。6.2 C里怎么保住数组的尺寸前面反复提到数组传参后sizeof会退化。C里最优雅的解法是用模板推导保留数组类型常见写法如下#include iostream template typename T, size_t N constexpr size_t array_size(const T ()[N]) noexcept { return N; } int main() { int arr[10]; std::cout array_size(arr) std::endl; // 10 return 0; }这里的精髓是形参const T ()[N]是数组引用引用不会引发数组到指针的退化因此N被推导成精确的数组长度。C17开始标准库直接给了std::size(arr)对原生数组和STL容器统一适用建议直接用#include iterator #include iostream int main() { int arr[10]; std::cout std::size(arr) std::endl; // 10 return 0; }我在工程上给出的建议是新代码直接用std::size老代码或者需要兼容C11时可以自己写模板。总之不要写sizeof(arr)/sizeof(arr[0])这种宏因为宏在arr是指针时照样给你一个错误结果而模板或std::size会在编译期就拦住你。6.3 VLAsizeof的少数运行时求值场景C99引入了变长数组Variable Length Array这时sizeof的编译期求值就不再成立了#include stdio.h #include stdlib.h void process(int n) { int arr[n]; // VLAn在运行时才知道 printf(sizeof(arr) %zu\n, sizeof(arr)); // 运行时求值输出 n * sizeof(int) } int main() { process(5); process(10); return 0; }arr的长度是运行时变量所以sizeof(arr)也必须运行时求值。这在C99标准里是明确允许的也因此C99的sizeof不再是严格的编译期常量表达式不能用来定义全局数组维度全局VLA在C99里也不被普遍支持。C11把它降级为可选特性很多项目比如Linux内核至今有意不启用VLA就是因为它在栈上分配不确定大小容易引发栈溢出。C一直不支持VLA相关需求你可以用std::vector或std::array替代。注意C的std::arrayT, N的N必须是编译期常量所以std::array的sizeof仍然是编译期可得的——它是固定的、带边界信息、且可以被sizeof正确算出总大小的容器算是C风格数组在C里的正统平替。最后分享一个我常用的收尾技巧当定义了跨模块交互的结构体或类时我会在头文件底部加几行游static_assert和offsetof组成的布局快照#include cstddef #include type_traits struct Header { uint32_t magic; uint16_t version; uint16_t flags; uint32_t length; }; static_assert(sizeof(Header) 12, Header layout unexpectedly changed); static_assert(offsetof(Header, magic) 0, magic offset changed); static_assert(offsetof(Header, length) 8, length offset changed);这样一来任何人后续往Header里加字段或调整声明顺序只要布局变了编译期直接报错不会等到线上运行时才出现解析错乱。这是我做序列化、网络协议和硬件通信代码时的习惯动作sizeof在这里不只是算大小而是成了一道降低风险的安全网。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询