
写 C/C 一定会撞上sizeof但很多人把它当函数用甚至为了它特地去查要不要引头文件。实际上sizeof是 C/C 语言内置的运算符不需要任何头文件只要你写了代码它就一直在那儿。这篇小结不是给新手背结论的而是把sizeof的本质、常见用法、以及我在项目里真正踩过的坑一次性讲透。无论你是刚入门的学生还是整天跟指针、结构体、内存打交道的嵌入式或服务端开发这篇文章都能让你重新审视这个熟悉又陌生的老朋友。1. sizeof是什么以及为什么每个C/C程序员都要吃透它1.1 sizeof的本质一个运算符不是一个函数很多人第一次见sizeof是在书上的示例里写法多半是sizeof(int)于是下意识以为它是标准库提供的函数。这是一个流传很广的误解。sizeof是 C 和 C 里的一元运算符和、*、!是同一类东西。它最大的特点是在编译期处理不是运行期函数调用。编译器拿到sizeof后面的操作数会直接算出这个类型或表达式所占的字节数然后把它当成一个整数常量替换到代码里。看个最简单的例子#include stdio.h int main(void) { int a 10; size_t s1 sizeof(int); size_t s2 sizeof a; size_t s3 sizeof(a); printf(%zu %zu %zu\n, s1, s2, s3); return 0; }这段代码能说明两件事sizeof int这种写法在语法上其实是允许的因为sizeof是运算符不是函数后面不一定要加括号但碰到类型名比如sizeof int编译器会要求必须写成sizeof(int)不然语法分析会出错。所以业界习惯一律加括号主要是为了统一和可读性。我建议你写代码时统一采用sizeof(类型)或sizeof(表达式)的格式不要为了秀操作写sizeof a。真到维护的时候别人看得省心你自己也少踩坑。1.2 sizeof需要头文件吗和size_t要分清网络热搜里经常有人问“sizeof函数需要头文件吗”答案非常明确不需要。因为sizeof是语言核心的一部分只要编译器能识别 C/C 语法它就能工作。就算你写一个空文件里面只有int x; unsigned long long y sizeof(x);编译器也完全认识sizeof。不过有一个特别容易被混淆的点sizeof的运算结果类型是size_t而size_t是一个类型别名定义在stddef.h、stdio.h、stdlib.h这些头文件里。所以如果你写了size_t s sizeof(int);那么编译器需要看到size_t的声明这时代码里必须包含某个提供size_t的头文件或者用unsigned int、unsigned long去接。实际开发里我几乎不会单独为了size_t去引stddef.h因为项目里大概率已经引了stdio.h或stdlib.h。但在一些极简嵌入式代码里你可能会看到#include stddef.h unsigned int size sizeof(int);这不是因为sizeof需要头文件而是size_t需要。这个区别如果你能跟别人讲清楚面试官基本就认定你不是背书的。1.3 sizeof是在编译期求值的既然说sizeof是编译期运算符那它必然有一个很反直觉的特性不会真的去计算表达式。#include stdio.h int main(void) { int i 0; size_t sz sizeof(i); printf(i %d, sz %zu\n, i, sz); return 0; }这段代码输出结果是i 0, sz 432 位int环境。也就是说i根本没执行sizeof只看i这个表达式的类型是int然后就返回了sizeof(int)。这给了我们一个很实用的小技巧在 C 里你完全可以在不构造对象的情况下获取某个类型实例的大小。比如struct MyClass { int x; double y; char name[64]; }; size_t size sizeof(MyClass{}.name); // 这里并没有真正生成一个完整的 MyClass 对象留在栈上当然正常写sizeof(MyClass::name)在 C 里不一定合法因为非静态成员不能单独取偏移但sizeof(MyClass{}.name)这种写法偶尔能在泛型代码里派上用场。1.4 sizeof返回的类型size_t而不是int还有一个细节点sizeof的结果类型是size_t是无符号类型不是int。很多人打印时用%d在某些编译器上会导致格式字符串和参数类型不匹配轻则打印出乱码重则触发警告甚至未定义行为。正确打印方式是%zuC99 起#include stdio.h int main(void) { printf(%zu\n, sizeof(int)); return 0; }如果你的编译器比较老不支持%zu可以强转成unsigned long再打印printf(%lu\n, (unsigned long)sizeof(int));这点在写跨平台日志时很重要别问我怎么知道的都是被报警告烦出来的。2. 基础类型的sizeof结果与平台差异2.1 典型32/64位平台上的基础类型大小很多初学者会问int到底占 4 字节还是 8 字节答案是不一定。C 标准只规定了最小范围和相对关系没有强制规定每种整数类型必须是多少字节。以最常见的 x86 和 x86-64 平台为例我标一下常见的字节数类型32位平台常见值64位平台常见值备注char11标准恒定为1short22至少16位int44至少16位通常32位long4在 Linux 上8在 Windows 上4历史差异最大long long88C99标准引入float44通常 IEEE 754 单精度double88通常 IEEE 754 双精度指针48取决于地址空间所以如果你在做网络协议解析、文件格式读写、或者跨进程通信不要假设long一定是 8 字节或 4 字节。正确做法是用stdint.h/cstdint里的int32_t、uint64_t这些类型在不同平台上都有固定的位宽。顺便说一个亲测的场景之前我在 Windows 上写了一个共享内存的结构体里面有个long字段本机测试一切正常。后来把同一份代码放到 Linux 上发现共享内存里的字段偏移全部错位排查了半天最后发现就是long在 Windows 是 4 字节、在 Linux 是 8 字节。从那以后我二进制布局里的整数类型全部改成int32_t/uint64_t再也没出过这种问题。2.2 char永远是1但不等于一定是8位C 标准规定sizeof(char)永远等于 1。这里的关键点是sizeof得到的数值是以char大小为单位的所以sizeof(char)必然等于 1。但这不代表char一定是 8 位。在一些极其小众的 DSP 平台或者老式嵌入式处理器上char可能是 16 位甚至 32 位。C 标准只保证CHAR_BIT 8具体多少由平台决定。你可以在limits.h里查看CHAR_BIT。这个特性对绝大多数开发者来说没有实际影响但理解它能帮你明白一件事sizeof的“1”是语言定义的抽象字节不是物理上的 8 bit。真要写底层的位运算、编解码协议该用CHAR_BIT的地方不要偷懒。2.3 用sizeof在编译期保护平台假设既然平台大小有差异那怎么防止代码被错误地移植到别的架构C11 提供了_Static_assertC11 提供了static_assert可以在编译期检查sizeof结果#include stdint.h _Static_assert(sizeof(int32_t) 4, int32_t must be 4 bytes); _Static_assert(sizeof(uint64_t) 8, uint64_t must be 8 bytes);在 C 里可以写成static_assert(sizeof(int) 4, This code assumes int is 32-bit);这种断言在写跨平台网络库时特别有用。你可以在公共头文件里放一组static_assert一旦有人把代码拖到某些奇怪平台上编译编译器会直接报错而不是等程序跑到某个协议解析的地方才崩溃。3. 数组、字符串与sizeof最容易踩坑的重灾区3.1 数组名与sizeof不退化原则数组名在绝大多数表达式中会“退化”成指向首元素的指针但有一个非常重要的例外当sizeof的操作数是数组名时不会发生退化。#include stdio.h int main(void) { int arr[10]; printf(sizeof(arr) %zu\n, sizeof(arr)); printf(sizeof(arr[0]) %zu\n, sizeof(arr[0])); return 0; }siziof(arr)得到的是整个数组占用的字节数比如10 * 4 40。而sizeof(arr[0])是单个元素的字节数。两者一除就是数组元素个数size_t count sizeof(arr) / sizeof(arr[0]);这是 C 语言里最经典、也最常用的数组长度计算方式。注意这种方法只能用于“真正的数组”不能用于指针也不能用于已经退化成指针的函数参数。3.2 字符串字面量与sizeof别漏了末尾的\0字符串是数组的另一种表现形式所以在sizeof面前同样有坑。#include stdio.h #include string.h int main(void) { char str[] hello; const char *p hello; printf(sizeof(str) %zu\n, sizeof(str)); // 6 printf(strlen(str) %zu\n, strlen(str)); // 5 printf(sizeof(p) %zu\n, sizeof(p)); // 864位平台指针大小 printf(sizeof(\hello\) %zu\n, sizeof(hello)); // 6 return 0; }hello字面量实际是char[6]因为末尾还藏着一个\0。所以sizeof(hello)等于 6而strlen(hello)只数到结尾之前的 5 个字符。很多人写序列化或网络包时喜欢用sizeof(hello)来算字符串常量长度这是可行的因为它包含\0可以直接用来memcpy或fwrite。但如果你用char *p指向同一个字符串常量sizeof(p)只是指针大小永远拿不到字符串长度。这是面试里高频出现的区别。3.3 sizeof和strlen的区别一个编译期一个运行期sizeof和strlen虽然都能和字符串扯上关系但完全是两回事对比项sizeofstrlen性质运算符编译期求值库函数运行期遍历是否需要头文件不需要需要string.h统计对象类型或表达式的存储大小字符串的字符个数是否包含\0如果对象是数组会包含不包含操作数类型或表达式只能传指针实际开发中我经常看到有人在缓冲区管理时用错char buf[64]; strcpy(buf, hello); size_t a sizeof(buf); // 64缓冲区容量 size_t b strlen(buf); // 5当前字符串长度这两个都很有用但用途完全不同。复制或写入时要限制的是缓冲区容量应该用sizeof(buf)来算剩余空间打印或发送时要确定内容长度才用strlen(buf)。搞反了轻则浪费空间重则缓冲区溢出。3.4 二维数组和多维数组的sizeof多维数组的sizeof规律和普通数组类似只是要一层一层剥。#include stdio.h int main(void) { int matrix[3][4]; printf(sizeof(matrix) %zu\n, sizeof(matrix)); // 3*4*4 printf(sizeof(matrix[0]) %zu\n, sizeof(matrix[0])); // 4*4 printf(sizeof(matrix[0][0]) %zu\n, sizeof(matrix[0][0])); // 4 return 0; }用sizeof可以安全地计算行数和列数size_t rows sizeof(matrix) / sizeof(matrix[0]); size_t cols sizeof(matrix[0]) / sizeof(matrix[0][0]);有一个和函数参数相关的经典问题如果把二维数组传给函数比如void process(int m[][4], size_t rows);在函数内部sizeof(m)并不是整个二维数组的大小因为形参m已经退化成“指向数组的指针”也就是int (*)[4]。所以函数里算列数没问题编译器知道第二维是 4但算行数就不再用sizeof(m) / sizeof(m[0])了必须把rows显式传进来。4. 指针和动态内存为什么sizeof(指针)总让你意外4.1 指针的sizeof是固定的吗在绝大多数主流平台上所有类型的指针大小都一样32 位系统是 4 字节64 位系统是 8 字节。它指向的是char、int还是某个结构体不影响指针本身的大小。#include stdio.h int main(void) { char *pc; int *pi; void *pv; double *pd; printf(%zu %zu %zu %zu\n, sizeof(pc), sizeof(pi), sizeof(pv), sizeof(pd)); return 0; }这段代码在主流 64 位平台上一律打印8。这里的本质原因是指针保存的是地址地址宽度由平台决定和指向对象的类型无关。不过“主流”不等于“所有”。在一些特殊架构上函数指针可能和数据指针大小不同某些 Harvard 架构的嵌入式芯片上还有更奇怪的情况。所以在写极度可移植的代码时不要把“所有指针大小相同”当成绝对真理。但在日常服务器开发和桌面开发中你可以放心用sizeof(指针)做序列化估算。4.2 sizeof不能探测动态分配内存的大小这是sizeof被误解最深的一个点。int *p malloc(100 * sizeof(int)); size_t n sizeof(p); // 结果不是 400而是 864位平台malloc返回的是一块连续内存的首地址当你把它赋值给指针变量后sizeof(p)只能得到指针变量的存储大小得不到这块内存的大小。C 标准并没有规定库函数要记录你分配了多少字节更不存在标准接口去查询malloc分配的大小。所以实际开发里所有动态分配的内存长度都必须由程序员自己保存。我习惯用一个轻量结构体来封装#include stdlib.h typedef struct { int *data; size_t len; } IntArray; IntArray create_int_array(size_t n) { IntArray arr; arr.data malloc(n * sizeof(int)); arr.len n; return arr; }这才是真正可靠的长度管理方式。不要幻想sizeof能帮你记住malloc的分配结果。4.3 数组作为函数参数时的退化问题C 语言里写函数参数时void f(int arr[])和void f(int *arr)是完全等价的。数组参数被“调整”成了指针参数所以函数内部对arr使用sizeof得到的永远是指针大小。#include stdio.h void print_size(int arr[]) { printf(inside function: %zu\n, sizeof(arr)); } int main(void) { int data[10]; printf(outside: %zu\n, sizeof(data)); // 40 print_size(data); // 8 return 0; }这个问题的根源是效率如果直接把整个数组拷贝进函数参数传递开销太大所以 C 设计成传首地址。要正确处理数组长度最常用的办法是调用点算好长度再传进去void print_size(int arr[], size_t n) { printf(array bytes maybe: %zu\n, n * sizeof(arr[0])); }这里sizeof(arr[0])还是安全的因为arr退化成指针后arr[0]的类型是int不是数组。C 开发者可以用模板引用捕获数组维度做到更省心template typename T, size_t N void print_size(T (arr)[N]) { std::cout sizeof(arr) \n; // 这里是真正的数组大小 }但在 C 里老老实实传长度是最稳的做法。4.4 malloc时用sizeof *p可以避免类型重复在malloc分配指针数组时很多人会写int *p malloc(10 * sizeof(int));这没有错但如果你把类型从int改成long这里要改两处一处是p的声明一处是sizeof(int)。我习惯写成int *p malloc(10 * sizeof(*p));*p的类型就是int所以sizeof(*p)等价于sizeof(int)。这样以后如果把p改成long *malloc 这一行不用动。这算是我个人很喜欢的一个小习惯能少改一行是一行还能避免类型不同步造成的 bug。5. 结构体、联合体与对齐sizeof背后的对齐规则5.1 结构体sizeof不等于成员之和任何一个在 C 语言里写过结构体的人迟早都会遇到这个经典的“意外”#include stdio.h struct S { char c; int i; }; int main(void) { printf(%zu\n, sizeof(struct S)); return 0; }看起来char占 1 字节int占 4 字节加起来应该是 5。但实际输出往往是 8。原因是“对齐”alignment。很多处理器访问特定类型数据时要求地址必须是 2、4 或 8 的倍数否则轻则性能下降重则直接触发硬件异常。编译器为了让结构体成员满足各自的自然对齐会在成员之间插入填充字节padding。在上面的例子里结构体布局是偏移 0char c偏移 1~3填充偏移 4~7int i结构体总大小还要满足结构体自身对齐要求而结构体对齐要求通常等于最大成员对齐要求也就是int的 4 字节因此总大小是 8而不是 5。5.2 联合体sizeof的规则联合体union和结构体的规则相反所有成员共享同一块内存所以sizeof(union)等于最大成员的大小同时还要满足最大成员的对齐要求。#include stdio.h union U { char c; int i; double d; }; int main(void) { printf(%zu\n, sizeof(union U)); // 通常 8 return 0; }这里double是 8 字节所以联合体大小至少是 8。如果换成char和long long结果也会跟着最大成员走。联合体常用于协议解析、类型双关、或者需要在同一块内存里复用不同布局的场景。但在 C 里用带非平凡构造函数的对象作为union成员会有限制更安全的替代方案是std::variant。C 语言里倒是直接很多我常在嵌入式通信协议里用union把字节数组映射到不同字段视图。5.3 手动优化结构体布局的实践因为sizeof受对齐影响所以结构体成员顺序不同占用的空间可能差别很大struct BadLayout { char c1; int i; char c2; }; // 常见大小 12 struct GoodLayout { int i; char c1; char c2; }; // 常见大小 8第一个结构体里char c1后面填充 3 字节int i放在偏移 4 的位置之后char c2再占 1 字节最后还要补齐到 4 的倍数所以变成 12。第二个结构体里int放最前面占 4 字节两个char占 2 字节总大小 6对齐到 4 倍数是 8。所以一个非常实用的经验是把结构体成员按占用空间从大到小排列通常能减少填充。不过这只是经验不是绝对定理。因为有的时候你还要考虑成员顺序的语义可读性。比如一个网络协议头字段顺序必须和字节流完全一致这时候为了保持布局你宁愿牺牲一些填充甚至用#pragma pack强制紧凑排列#pragma pack(push, 1) struct ProtocolHeader { uint8_t version; uint16_t length; uint32_t seq; }; #pragma pack(pop)#pragma pack(1)会让所有成员按 1 字节对齐结构体大小等于成员大小之和。代价是某些平台可能产生非对齐访问性能下降甚至崩溃所以只能在明确需要紧凑二进制布局时使用。如果你想看每个成员的偏移量可以用offsetof宏#include stdio.h #include stddef.h struct S { char c; int i; }; int main(void) { printf(offset c %zu\n, offsetof(struct S, c)); printf(offset i %zu\n, offsetof(struct S, i)); return 0; }注意offsetof定义在stddef.h中sizeof不需要头文件但offsetof需要。很多人把这两者搞混实际上它们是不同层面的东西。6. sizeof与表达式求值不执行表达式的特性6.1 sizeof(表达式)不计算表达式这是sizeof最容易被忽略的语义它只关心类型不关心值所以表达式里的副作用不会发生。#include stdio.h int foo(void) { printf(foo called\n); return 0; } int main(void) { int i 0; printf(sizeof(i) %zu\n, sizeof(i)); printf(sizeof(foo()) %zu\n, sizeof(foo())); printf(i %d\n, i); return 0; }上面这段代码foo called不会打印i仍然等于 0。因为sizeof根本不需要执行foo()或i它只要知道foo()的返回类型是int、i的类型是int就能返回大小。这个特性在泛型代码里很有用。比如 C 的模板元编程里经常可以看到template typename T struct TypeSize { static constexpr size_t value sizeof(T); };它不用创建任何T类型的对象也不会真的去构造对象就能拿出sizeof(T)。6.2 VLA的sizeof例外运行期求值C99 引入了变长数组VLA允许在运行时用变量指定数组大小int n 20; int arr[n]; printf(%zu\n, sizeof(arr)); // 运行期计算等于 20 * sizeof(int)这里的sizeof(arr)不再是一个编译期常量因为编译器在编译时不知道n的值。所以 VLA 的sizeof是 C 标准里一个难得的例外它会在运行期被计算。VLA 虽然在 C 里合法但我不建议在需要长期维护的代码里滥用原因有两个一是 VLA 在栈上分配数组长度不可控时容易爆栈二是 C 标准并不支持 VLA写出的代码没法在 C 编译器里直接复用。如果确实需要动态长度数组优先考虑malloc或 C 的std::vector。6.3 sizeof与运算符优先级的坑sizeof的优先级和一元运算符同级从右往左结合。所以sizeof a 1会被解析成(sizeof a) 1而不是sizeof(a 1)。如果想让sizeof作用于整个表达式括号不能省int a 10; size_t s1 sizeof a 1; // 等价于 sizeof(int) 1 size_t s2 sizeof(a 1); // 求 a1 的类型大小通常还是 sizeof(int)类似的还有sizeof *p 1很多人本意可能是sizeof(*p) 1但如果你写成了sizeof *p 1结果其实也是sizeof(*p) 1因为*的一元优先级高于。这里真正容易踩的是下面这种int *p; size_t s sizeof p 1; // 指针大小 1 size_t t sizeof(p 1); // 表达式 p1 的类型仍然是 int*所以还是指针大小最后一条稍稍有点反直觉无论 p 有没有实际指向有效内存sizeof(p 1)都不会解引用 p也不会做加法。C 里这样写甚至不能为未定义行为负责因为加法本身没执行。所以别试着用sizeof去“试探”表达式会不会崩它不干活。7. sizeof的常见面试题与实战排查7.1 常见面试题速查表我整理了实际面试和被问时最高频的一组例子假设环境是 64 位平台、int4 字节、指针 8 字节代码结果考察点sizeof(char)1char恒定为1sizeof(hello)6字符串字面量含\0char buf[20]; sizeof(buf)20数组名不退化char *p buf; sizeof(p)8指针大小与指向类型无关void f(int a[]) { sizeof(a); }8数组参数退化为指针int a[10]; sizeof(a)/sizeof(a[0])10经典数组长度公式struct { char c; int i; };8常见结构体对齐填充union { char c; int i; double d; };8常见联合体取最大成员int i0; sizeof(i)4i仍为0表达式不求值如果你能完全说清每一行的来龙去脉sizeof这一关基本就算过了。7.2 实际项目中的避坑清单结合这些年做项目和看别人代码的经验我把最容易踩的坑整理成一份清单不要用sizeof判断动态内存长度。malloc出来的内存没有长度元信息必须自己维护长度字段。不要在函数参数里用sizeof(arr)/sizeof(arr[0])。参数已经退化成指针结果只会是指针大小除以元素大小完全是错的。跨平台的时候不要写死sizeof(int)和sizeof(long)。用stdint.h里的固定宽度类型更稳妥。结构体序列化别图方便直接fwrite(obj, sizeof(obj), 1, fp)。填充字节和对齐在不同平台上不一致除非你用#pragma pack或者明确知道布局。C 多态类上不要用sizeof(*basePtr)去拿动态类型大小。sizeof是编译期机制只能得到静态类型大小无法知道虚函数背后真正的派生类大小。sizeof不需要头文件但size_t需要。这是两个问题别在面试时漏嘴。malloc时用sizeof(*p)而不是sizeof(type)改类型时少改一处也不容易出错。C 里还有个常见误区很多人问sizeof能不能代替std::size来拿数组大小。C17 提供了std::size(arr)返回元素个数而sizeof(arr)返回字节数。两者用途不同一般建议在 C 里直接写std::size(arr)更语义化C 代码里再用sizeof(arr)/sizeof(arr[0])。7.3 什么时候不要用sizeof不是所有场景都该第一时间想到sizeof。我个人的习惯是这么区分的如果我要知道“这块缓冲区有多大”用sizeof如果我要知道“这个字符串有几个字符”用strlen如果我要知道“这个std::vector里存了几个元素”用.size()如果我要知道“这个指针指向的堆内存有多大”对不起标准字节没有这种能力靠自己的长度字段。还有一个容易被忽视的场景不要拿sizeof去比较表达式类型是否相同。比如if (sizeof(int) sizeof(long)) { ... }这在类型宽度相同的平台上是成立的但你其实无法确定int和long是同一个类型。如果代码后续要做重载或模板特化这种判断会误导人。正确的做法是用类型特征比如 C 里的std::is_same_vint, long。7.4 我最后想分享的一个习惯我在项目里通常会把数组元素个数计算封装成宏或函数模板而不是散落在业务代码里到处写sizeof(arr) / sizeof(arr[0])。C 语言里可以这样#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0]))然后要求团队规则这个宏只能用在“当前作用域里能看到的真实数组”上绝不能用在指针上。配合编译器的-Wsizeof-pointer-div警告在 GCC 上还能提前拦截一部分问题。用 C 的话直接写template typename T, size_t N constexpr size_t ArraySize(T ()[N]) noexcept { return N; }这样数组引用不会退化成指针比宏更安全。函数参数传数组进去也能正确得到元素个数。说到底sizeof不是难懂的天书它只是特别讲究“场景”是数组还是指针是编译期还是运行期有没有对齐填充把这些边界想清楚这个运算符就能成为你写安全代码的利器而不是面试背完就忘的知识点。