ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

C语言sizeof深度解析:运算符本质、常见陷阱与工程实践

C语言sizeof深度解析:运算符本质、常见陷阱与工程实践 1. 从一道经典C语言题说起sizeof到底是函数还是运算符?C语言学习者几乎都会遇到这个经典的困惑sizeof后面有时候跟着一对括号看上去和函数调用一模一样。int a 10; printf(%zu\n, sizeof(int)); // 带括号 printf(%zu\n, sizeof a); // 不带括号也能编译通过第一眼看过去sizeof(int)和函数调用长得太像了但关键在于第二行代码——sizeof a没有括号照样能编译运行。C语言里没有任何一个函数能在调用时省略括号所以这个细节本身就说明了问题sizeof不是函数它是一个运算符而且是编译时求值的单目运算符和!、~、这些同级别。既然它是运算符那么“sizeof函数需要头文件”这个说法就根本不成立。标准库里的memcpy、strlen才需要头文件sizeof作为语言内置的运算符编译器在语法分析阶段就直接处理掉了不依赖任何头文件。你写一个空文件只敲一行int x sizeof(int);不需要#include任何东西编译器就能认识它。那为什么我们平时写sizeof(int)时看起来带着括号这其实是语法要求当操作数是类型名时类型名必须用括号括起来因为类型名本身不是一个表达式不括起来语法上说不通。而当操作数是变量或表达式时括号可加可不加加括号纯粹是为了让代码看着统一、清晰。所以sizeof(int)和sizeof i都是合法的而sizeof int在C语言标准里其实也可以编译C里则明确要求类型名必须加括号。我在实际项目里见过不少新人在代码评审时被问到这个点把sizeof当成函数去调用还有人试图对它取地址——sizeof(a)——这在编译阶段就直接报错因为sizeof的结果是编译期常量C99变长数组除外后面细说常量没有地址可言。理解sizeof是运算符这件事还有个很实际的好处你不需要再纠结sizeof函数需要包含什么头文件这种问题。它就是语言自带的能力和 - * /一样摆在任何编译单元里。真正需要关心的是它运算时的规则、它什么时候会在编译期算完、什么时候会塌陷成运行时计算以及最常见的数组指针退化问题。下面一步步拆开讲。2. sizeof的求值时机与不执行表达式特性2.1 为什么sizeof在大多数情况下是编译期常量sizeof的核心特点是大多数情况下它在编译期就已经完成计算结果以常量形式直接嵌入生成的代码里。比如char buffer[100]; size_t len sizeof(buffer);编译器看到sizeof(buffer)后能直接数出buffer数组占了100个字节char是1字节于是把len初始化为100运行时不发生任何计算。这和函数调用完全不同函数调用必须到运行时才能拿结果。也正因如此某些看似危险的表达式放到sizeof里反而完全没问题int func_never_called(void) { printf(never printed\n); return 42; } int main(void) { size_t s sizeof(func_never_called()); // 不会真的调用func_never_called printf(%zu\n, s); // 任何普通函数返回值在对应平台上都是4或8 return 0; }这里sizeof(func_never_called())的结果是函数返回类型int的大小整个表达式在编译期就被处理完了函数体根本不会执行因此never printed不会被打出来。这个特性在写代码时很值钱。比如你想用宏判断一个类型是否适合通过值传递或者想在结构体里预留对齐空间都可以放心地利用sizeof在编译期完成计算而不必担心运行时开销。前几年有团队用C语言写嵌入式通信协议解析在协议报文结构里算偏移量时sizeof和offsetof配合使用所有字段偏移都是编译期常量生成的代码里直接就是立即数偏移零运行时开销。2.2 C99变长数组(VLA)的例外不过有一个例外必须单独拎出来讲C99引入的变长数组Variable Length ArrayVLA。当你对VLA取sizeof时结果不再是编译期常量因为数组长度要到运行时才知道#include stdio.h size_t vla_size(int n) { char buffer[n]; // 变长数组长度由运行时的n决定 return sizeof(buffer); // 运行时计算 n * sizeof(char) } int main(void) { printf(%zu\n, vla_size(10)); // 输出10 printf(%zu\n, vla_size(100)); // 输出100 return 0; }这段代码里同一个函数每次调用返回的sizeof结果都可能不同。这意味着对于VLA来说sizeof确实变成了运行时运算符只要把VLA视为运行时才确定尺寸的对象即可。我在几个嵌入式项目里看到有人利用VLAsizeof做栈上动态缓冲但也有团队明确禁止VLA因为VLA分配失败时的处理很麻烦而且栈溢出风险难以预估。如果你在项目规范里看到禁止VLA这类条目多半是经历过现场事故后总结出来的教训。GCC在默认情况下是支持VLA的如果用-Wvla编译选项打开警告代码里出现VLA时编编译器会提示你。3. 五花八门的sizeof用法从基础类型到表达式陷阱3.1 基础数据类型与表达式的sizeofsizeof最常见的用途是获取基础类型的大小但不同机器、不同编译器对基本类型的大小定义并不完全一致。C语言标准只保证sizeof(char) 1以及short int long long long这一条大小关系并没有规定int必须是4字节。在实际开发中我们通常关注的是当前平台的具体情况。比如在常见的x86-64 Linux环境下char1字节short2字节int4字节long8字节float4字节double8字节long long8字节指针任意类型8字节但换到Windows上long就变成了4字节这是Windows LL64和Linux LP64两种数据模型之间的差异。跨平台开发如果直接写死long是8字节到了Windows上就是典型的隐藏bug来源。对表达式取sizeof时规则是只考虑表达式最终结果的类型不实际求值也不做任何运行时计算。需要注意的坑是表达式中的类型提升和转换会影响sizeof结果。char a 1; char b 2; size_t s sizeof(a b);a b会发生整型提升char先转成int再相加所以结果是sizeof(int)也就是4而不是你认为的两个char相加占了1字节。再看一个经典例子int arr[10]; size_t s sizeof(arr[0] 1);arr[0]类型是int加1还是ints等于sizeof(int)——这里有个容易迷惑的点arr[0]明明是数组元素为什么不是sizeof(char)这种元素大小因为表达式求值之前类型已经被确定了这里没有任何类型转换。3.2 sizeof在指针身上最常见的坑数组与指针的貌合神离数组和指针是C语言中最大的拦路虎。sizeof这个运算符在数组和指针上的行为完全不同很多线上bug正是从这里产生的。// 数组场景 char buffer[64]; size_t s1 sizeof(buffer); // 64数组占用总字节数 size_t s2 sizeof(buffer[0]); // 1单个元素大小 // 指针场景 char *p buffer; size_t s3 sizeof(p); // 864位平台指针本身的大小这里的关键在于**数组名在大多数表达式中会退化为指向首元素的指针。**这个规则是C语言故意设计的数组名代表整个数组对象但当它作为表达式参与运算时比如传参、与指针做算术或者直接赋给指针变量编译器会悄悄地把它转换成指向首元素的指针。只不过sizeof是个例外——数组名作为sizeof的操作数时不会退化sizeof直接作用于整个数组对象。这个特性与数组名就是指针这种民间说法完全相悖。网上很多教程说数组名和指针是一回事这句话在传参的意义上大体说得过去但在sizeof这里彻底翻车。为了让你秒懂可以这样记数组变量是一整块内存区域的身份标签指针是一块单独用来存放地址的内存区域。当数组名出现在表达式中时大多数场景下编译器偷懒把它换成了首元素地址但sizeof直接问你占多大空间这时候数组标签代表的是整块内存指针变量代表的是那个8字节的地址变量本身。两者完全不同。在项目里最常见的错误写法是这样的void print_size(char buffer[]) { // 注意这里的 buffer 表面上写着数组实际它就是个指针 printf(%zu\n, sizeof(buffer)); // 打印的是指针大小不是传入数组的大小 } int main(void) { char data[100]; printf(%zu\n, sizeof(data)); // 100正确 print_size(data); // 8期望100实际输出指针大小 }函数参数列表里的char buffer[]编译成什么编译器把它视为char *buffer。这是C设计的底层机制数组作为参数传递时必须退化为指针。也就是说任何函数都不可能在运行时知道调用方传进来的数组有多长。所以实际开发中处理数组的函数几乎都需要同时传入长度void process_buffer(char buffer[], size_t size) { for (size_t i 0; i size; i) { // ... } }如果你见过那种在函数里用了sizeof而不是显式传长度、并且程序还能正常跑的老代码八成是因为缓冲区里的特殊数据碰巧让程序没炸而不是因为sizeof真的拿到了数组长度。一旦代码被重构、输入数据变化这类问题立刻现出原形。3.3 二维数组与多维数组的sizeof二维数组取sizeof时也很有迷惑性int matrix[3][4]; size_t a sizeof(matrix); // 3*4*sizeof(int)48 size_t b sizeof(matrix[0]); // 第二维4*sizeof(int)16 size_t c sizeof(matrix[0][0]); // 单个int4这里matrix是包含3个一维数组的数组每个一维数组有4个int元素所以sizeof(matrix)是48字节。而matrix[0]是把第一行取出来它是一个4个int的一维数组大小16字节。多维数组和函数传参搭配起来更容易出事void process_matrix(int grid[][4], size_t rows) { size_t a sizeof(grid); // 8还是指针 size_t b sizeof(grid[0]); // 16 }注意grid退化为指针后grid[0]的类型是int[4]大小仍然是16所以对于传入的二维数组你至少还能从sizeof(grid[0])算出每行宽度但行数必须显式传进来。4. 结构体的sizeof为什么不是简单相加4.1 对齐规则与内存布局结构体是C语言里组织数据的重要工具但它的sizeof计算比想象中复杂得多。看一个最常见的例子struct Data { char c; // 1字节 int i; // 4字节 };直觉上145但sizeof(struct Data)出来的结果通常是8。为什么因为硬件层面有内存对齐的需求某些平台比如x86和ARM对int类型的内存访问要求地址必须对齐到4字节边界否则访问效率下降甚至在某些体系结构上直接触发异常。编译器为了满足对齐要求会在结构体成员之间插入填充字节padding把c后面的3个字节留给padding让i落在偏移量4的位置。C语言的地址对齐计算规则可以简化成这么几点每个类型有一个对齐要求通常是该类型大小的整数次幂。x86-64 Linux下char对齐值是1short是2int是4double在i386下是4但在x86-64下是8。结构体每个成员的偏移量必须是该成员对齐值的整数倍。结构体总大小必须是结构体最大对齐值的整数倍末尾也会补padding。再来一个例子struct Example { char a; // 偏移0 double b; // 对齐8偏移必须补到8 int c; // 对齐4偏移16 }; // sizeof(struct Example) 24从0开始a占偏移0b的对齐值是8必须从偏移8开始于是1到7的7个字节全是paddingc从偏移16开始占4字节到偏移19结束。结构体总对齐值是8当前偏移20不是8的倍数所以末尾再补4个padding字节最终大小是24。注意如果编译选项里指定了#pragma pack(4)或者用了__attribute__((packed))结果会完全不同#pragma pack(1) struct PackedExample { char a; double b; int c; }; #pragma pack() // sizeof(struct PackedExample) 13pack(1)的意思是对齐值强制为1所有成员紧挨着排列没有padding。这在跨协议传输、文件格式解析等领域经常用到——但代价是CPU读取未对齐数据时性能会下降有些平台甚至不能直接访问未对齐的double需要编译器生成多个访问指令再拼接。4.2 结构体成员顺序对大小的影响由于对齐padding的存在结构体的sizeof受成员声明顺序影响很大。同一个成员组合顺序不同尺寸可能相差不少struct OrderA { char a; int i; char b; }; // char(1) padding(3) int(4) char(1) padding(3) 12 struct OrderB { char a; char b; int i; }; // char(1) char(1) padding(2) int(4) 8两个结构体拥有完全相同的成员OrderA大小12OrderB大小8。这多出来的4个字节纯粹是排列顺序造成的。在平时写程序时如果想省内存把相同类型的成员尽量放在一起尤其是把大的类型放在前面往往能整理出更紧凑的布局。我在做嵌入式设备端结构体优化时把几个状态结构体重新排了成员顺序总大小直接下降了百分之十五到二十。在内存只有几KB的MCU上这种优化是实打实的收益。4.3 offsetof与sizeof搭配计算字段偏移除了sizeofC标准还提供了offsetof(type, member)宏定义在stddef.h中可以在编译期获取某个成员在结构体中的偏移量#include stddef.h #include stdio.h struct Packet { uint16_t header; uint32_t len; uint8_t flags; uint8_t payload[32]; }; int main(void) { printf(header offset: %zu\n, offsetof(struct Packet, header)); printf(len offset: %zu\n, offsetof(struct Packet, len)); printf(flags offset: %zu\n, offsetof(struct Packet, flags)); printf(payload offset: %zu\n, offsetof(struct Packet, payload)); printf(total size: %zu\n, sizeof(struct Packet)); return 0; }这套sizeof offsetof组合在网络协议解析、序列化、共享内存管理里非常常用。你不需要手动硬编码偏移量编译器会在编译期算好代码既清晰又不会出错。相比于硬编码数值用offsetof时如果结构体调整了成员顺序代码仍然自动匹配。5. 数组长度计算与sizeof相关的高频使用场景5.1 用sizeof获取数组元素个数经典宏的来龙去脉数组场景下sizeof最典型的使用是计算数组元素个数int numbers[] {2, 4, 6, 8, 10}; size_t count sizeof(numbers) / sizeof(numbers[0]);原理不复杂sizeof(numbers)是数组总共占用的字节数sizeof(numbers[0])是单个元素占用的字节数两者相除就得到元素个数。很多项目会封装成宏#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0]))使用示例int table[100] {0}; for (size_t i 0; i ARRAY_SIZE(table); i) { // ... }这个宏在Linux内核里也随处可见内核里写作ARRAY_SIZE(x)。不过注意这个宏只适用于真正的数组。如果你不小心把指针传进去计算出来的就是一串完全没意义的大数字size_t bad_count ARRAY_SIZE(some_pointer); // 8 / 8 1或 8 / 4 2这也是我在团队代码评审时最喜欢挑的问题之一。针对这种情况改进版的宏利用GNU扩展可以在编译期报错或警告比如#define ARRAY_SIZE(arr) \ (sizeof(arr) / sizeof((arr)[0]) \ sizeof(typeof(int[sizeof(arr) ... ])))这种写法比较复杂实际项目中看团队取舍。很多项目的编码规范直接规定数组传参必须额外传递长度禁止在函数体内对数组参数使用ARRAY_SIZE。核心原因就是数组参数在函数内已经退化成了指针这行宏拿不到任何有用信息。5.2 为什么字符串拷贝和缓冲区管理离不开sizeof日常开发中复制字符串、申请内存时经常要决定到底拷贝多少字节。这里有一个典型的对比char src[] hello; char dst[10]; strcpy(dst, src); // 拷贝6个字节含结尾的\0 memcpy(dst, src, sizeof(dst)); // 拷贝10个字节可能越界读src用strcpy是为了确保src完整复制过来且带上终止符如果用memcpy且大小填sizeof(dst)由于src实际只有6个有效字节这个memcpy会多读4个字节如果src字符串后面紧跟的恰好是其他数据就会造成未定义行为。这里正确的做法是memcpy(dst, src, strlen(src) 1); // 加上结尾空字符或者你明确知道src的字节数直接memcpy(dst, src, sizeof(src)); // src是数组sizeof得到的是整个数组大小但要注意dst的空间必须足够。用sizeof管理缓冲区的另一个常见场景是malloc分配大小char *p malloc(sizeof(char) * 100); // 可以这样写但没必要char就是1字节 int *nums malloc(sizeof(int) * 20); // 标准写法避免硬编码4 struct Data *d malloc(sizeof(struct Data)); // 必须用sizeof你不知道结构体实际大小很多新手会写int *nums malloc(80);这种硬编码字节数的代码。一旦换成64位平台int变成4字节还能碰巧对上但如果哪天你改成long *nums这个80就彻底错了。所以业界习惯是malloc的size参数必须用sizeof计算不写魔法数字。5.3 sizeof与strlen的终极对比一个数内存一个数内容很多人会把sizeof和strlen混在一起尤其是处理字符串时。一图流可以这么理解sizeof回答的是这个变量/类型占了多少字节strlen回答的是这个字符串里有多少个有效字符不包含结尾的空字符。char str[] hello; size_t a sizeof(str); // 6数组总大小包含结尾的\0 size_t b strlen(str); // 5只算5个字符不含\0严格来说strlen是函数声明在string.h里它在运行时遍历字符串直到遇到\0因此要求字符串必须以空字符结尾。sizeof则不关心字符串内容它只看类型和内存大小。这两个东西在实际代码里互为补充但绝不替换。嵌入式协议解析时我经常看到有人图省事用sizeof(recv_buf)当收到的实际字节数用这通常会导致把缓冲区里的垃圾数据全部当成了有效载荷发出去。正确做法是sizeof设置缓冲区上限实际接收字节数用返回值。6. 面试常问的sizeof陷阱sizeof永远不会执行表达式6.1 sizeof与自增自减的坑中坑网上流传最广的C语言面试题八成都有这道int i 0; size_t s sizeof(i); printf(i %d, s %zu\n, i, s);问i最后的值是多少答案是i 0, s 4。因为sizeof只关心表达式结果的类型i的类型是int在编译期就算出4整个表达式根本不会执行i不会被自增。很多人第一次见到这个结果都非常惊讶觉得这居然不执行。这个特性在项目里偶尔会变成令人迷惑的bug。试想代码里有人写了while (sizeof(cnt) 0) { ... }如果cnt是int那这个循环条件永远是4大于0cnt永远不增长循环变成死循环。所以实际写代码时务必记住不要把带副作用的表达式放进sizeof里因为副作用不生效几乎必然不是你期望的逻辑。6.2 sizeof与三目运算符、函数调用的不评估特性同样的逻辑推广开来int a 1, b 2; size_t s sizeof(a b ? a : b); // 不真的比较a和b只判断表达式最终类型是int double func(void); size_t s2 sizeof(func()); // 不调用func只看返回类型是doubleC语言标准注释里有sizeof操作数不被求值这一条但有三个例外VLA类型表达式、以及_Alignof和_Generic中的某些子表达式。这些细节其实在你写代码时影响不大重点还是记住除非操作数是VLA否则绝不要认为sizeof里的代码会执行。6.3 位域与sizeofC语言里的位域bit-field也经常在面试题里出现struct Bits { unsigned int a : 3; unsigned int b : 5; unsigned int c : 9; };这个结构体的大小并不等于35917位3字节。编译器会按底层类型这里是unsigned int分配内存并考虑对齐结果往往是sizeof(struct Bits) 4。如果位域跨越了底层存储单元的边界编译器还可能再补填充字节。实际项目中位域常见于节省存储空间的场景但它有几个坑位域的布局和字节序不是C标准强制的不同编译器可能不同位域不能取地址位域的底层类型最小可能是char也可能是int。跨平台或跨编译器时如果依赖具体位布局最好用掩码移位操作手工设定packed结构体别用位域。7. C中的sizeof类、虚函数与引用7.1 类的sizeof非静态数据成员才是大头C里sizeof同样适用但对class/struct求大小时的规则有所延伸。核心点类的size只统计非静态数据成员、虚函数表指针如果有虚函数、以及对齐填充不统计成员函数、静态数据成员。比如class Empty { }; // sizeof(Empty) 1C标准要求所有对象地址唯一空类占1字节 class WithFunc { public: void hello() {} }; // sizeof(WithFunc) 1成员函数不占对象空间 class WithVirtual { public: virtual void hello() {} }; // sizeof(WithVirtual) 864位平台有一个虚表指针你写一个空类对象希望它的地址和别的对象区分开编译器就给它分配1个字节占位。成员函数编译后是普通代码存在于代码段不占对象空间所以有函数的类还是1字节。有虚函数的类编译器会往对象里插入一个虚表指针指向该类自己的虚函数表。这个指针在64位平台上占8字节所以在x86-64下sizeof(WithVirtual)等于8如果类里有多个虚函数数量变化不影响size因为只有一根指针。7.2 继承与虚继承下sizeof会膨胀C继承场景下的sizeof更是面试经典class Base { int a; }; class Derived : public Base { int b; }; // sizeof(Derived) 8 class VBase { virtual void f(); }; class VDerived : public VBase { int c; }; // 64位平台VBase占8虚表指针VDerived占16虚表指针 int 对齐基类子对象会被完整嵌入到派生类对象中所以Derived的大小是Base(4) b(4) 8。虚继承则更复杂——子类里可能需要保存一个指向虚基类子对象的指针或偏移量尺寸会进一步膨胀。这在复杂的C框架里很常见比如多继承和虚继承混用时理论上一个对象的size会比简单加总所有成员大不少就是因为额外指针的开销。实际项目里如果你看到某个类的sizeof大得离谱排查思路是检查是否有虚函数虚表指针、是否有多重继承或虚继承、是否包含std::string等带指针的复杂成员它们本身内部还有堆内存指针。7.3 引用类型与数组在C中的sizeof差异C里引用reference本质上是对一个已存在对象的别名sizeof引用返回的是被引用对象的类型大小而不是“指针大小”double value 3.14; double ref value; // sizeof(ref) 8等同于sizeof(double)这一点容易搞混引用在机器层面实现时往往就是指针但语言层面规定引用不是独立对象所以sizeof(引用)返回被引用对象的size而不是8。数组方面C对数组名的sizeof行为和C语言完全一致求数组整个内存大小传参退化为指针。C17标准库提供的std::size定义在iterator可以在编译期获取数组元素个数也可以安全地用于std::vector等容器这个API比手写sizeof(arr)/sizeof(arr[0])更安全推荐在C17及以后的项目里优先使用。#include iterator #include iostream int main() { int arr[20]; std::cout std::size(arr) std::endl; // 20 }8. sizeof实操中的常见问题与避坑清单8.1 sizeof(字符串常量)到底是多少字符串常量的sizeof也是一个高频坑点const char *p hello; char arr[] hello; size_t a sizeof(hello); // 6包含结尾\0 size_t b sizeof(arr); // 6 size_t c sizeof(p); // 8指针变量大小 size_t d strlen(hello); // 5不包含结尾\0很多人在处理协议数据时想发送hello这个字符串却直接send(fd, p, sizeof(p))——这会把8字节的指针值本身发出去而不是把hello\0发出去。正确写法是send(fd, hello, 6)或send(fd, p, strlen(p) 1)这类。8.2 打印sizeof的格式控制符sizeof的返回值类型是size_t它是无符号整数类型。打印时要特别留意格式控制符在C99及以后标准写法是%zu如果写%d会因为符号不匹配而得到奇怪的输出甚至未定义行为在Windows上有些旧编译器对%zu支持不完善可以用%Iu不过现在主流MSVC版本已经支持%zu#include stdio.h int main(void) { int a 0; printf(%zu\n, sizeof(a)); // 推荐 return 0; }我见过一个真实事故某团队的同学用printf(%d\n, sizeof(buf))把缓冲区大小打印出来结果在64位平台上size_t是64位无符号整数%d只读取低32位一旦缓冲区超过2GB或者刚好高位有值输出就完全错乱。这类问题在排查时极其隐蔽最后用调试器看寄存器才发现是打印格式不匹配。8.3 动态内存与sizeof无关还有一个常见的误解malloc(sizeof(ptr))是不是就足够分配了不是。malloc的参数是字节数你需要的是这个指针指向的对象需要多少个字节而sizeof(ptr)是指针变量本身占了多少字节。比如int *arr malloc(sizeof(arr)); // 错误这分配了指针大小的内存8字节 int *arr2 malloc(sizeof(int) * n); // 正确n个int元素 struct Node *node malloc(sizeof(struct Node)); // 正确正确写法是先确定你要分配对象类型然后用sizeof(类型)或sizeof(*指针)计算大小。业界常见的建议是用sizeof(*指针)而不是sizeof(类型)好处是如果指针类型变了这行代码不用跟着改struct Node *node malloc(sizeof(*node)); // 比 sizeof(struct Node) 更抗重构这种做法在多个团队的项目里推广过评审时大家也都认同减少对类型的直接依赖随类型而变化。8.4 一个完整示例常见sizeof场景运行结果参考下面整理一份我在Linux x86-64环境实测过的常见sizeof结果方便参考。不同平台可能不同务必以当前平台为准表达式结果Linux x86-64说明sizeof(char)1标准保证一定为1sizeof(int)4当前平台常见的int宽度sizeof(long)8LP64数据模型下long为8字节sizeof(double)8double通常与long同宽sizeof(int*)864位平台指针大小sizeof(int[10])40数组总字节数sizeof(hello)6字符串常量含末尾\0sizeof(struct {char c; int i;})8含对齐填充sizeof(空类)1C标准保证空类至少占1字节注意Windows 64位下sizeof(long)通常是4这正好说明跨平台代码不要依赖long的宽度。如果你的代码里出现了long类型并假设它一定是8字节那么这个假设在Windows上会被打脸。8.5 常见问题速查表在实际开发中关于sizeof的问题基本集中在下面几种遇到对号入座即可现象原因解决方案sizeof(数组)在函数里变成8数组参数退化为指针显式传入数组长度sizeof(i)没有让i增加sizeof不求值编译期完成不要写带副作用的表达式sizeof(结构体)比成员相加更大对齐填充padding按对齐规律排列成员必要时packsizeof(hello)等于6不是5字符串常量包含结尾\0记住这个特性malloc(sizeof(ptr))内存不够sizeof(ptr)算的是指针大小改用sizeof(*ptr)或sizeof(类型)空类sizeof等于1C标准保证对象地址唯一正常现象不需要修改打印size_t用%d输出负数格式控制符与类型不匹配使用%zu9. 我的一些实践经验与最终提醒把sizeof彻底吃透说难不难说简单也不简单。我在实际项目里总结了几条经验第一写代码时尽量让结构体成员的顺序符合从大到小排列这样能最大限度减少对齐padding的浪费。如果结构体里都是字节数组和整数混排优先把大类型放前面小类型放后面再把相同类型的成员挤在一起。第二所有取数组长度的场景只要拿到的不是真正的数组比如函数参数一律放弃sizeof改成显式传长度。排序、遍历、拷贝缓冲区都要严格遵守这条规则。现代编译器会警告sizeof on array function parameter will return size of pointer——比如GCC的-Wsizeof-array-argument打开这类警告选项对排查问题很有帮助。第三在处理网络字节序、文件格式、共享内存这些需要明确二进制布局的场景不要依赖默认对齐显式用#pragma pack(1)或__attribute__((packed))并配合offsetof做静态断言。同时用static_assert检验sizeof(struct) 预期值如果哪天成员被改乱了编译期就会报警而不是运行到线上才出事故。第四sizeof的结果是编译期常量还是运行时值直接决定了你能否拿它定义数组尺寸、做编译期分支。C里用if constexpr (sizeof(T) 4)这种编译期条件再正常不过C里可以用它定义局部数组长度struct Header { uint32_t magic; uint32_t version; }; char buffer[sizeof(struct Header)]; // 编译期常量数组尺寸合法这里如果结构体带有VLA或者非固定成员C场景就不能这样用了。最后再分享一个我踩过的坑有段时间我在写一个通用的序列化工具需要把一个嵌套结构体里的每个字段做偏移计算和大小检查。最开始我写了一堆手工偏移量后来发现一旦有人往结构体中间加字段所有偏移全部错位排查花了大半天。后来我改成全链路使用offsetofsizeof并加了一组static_assert做编译期校验从此这类问题再也不出现了。总结下来sizeof这块的许多坑根本原因都是没有把它当成一个编译期运算符来看待而是下意识地当成运行时的类型大小查询函数。当你真正理解它在编译器眼里是什么角色写出来的代码就会稳得多。
返回列表