
1. 从一段“诡异”的代码说起为什么sizeof不是函数几年前我在为一个嵌入式项目调试一段内存拷贝的代码时遇到了一个让我至今记忆犹新的“坑”。代码片段大致如下void copy_data(char *dest, const char *src, size_t size) { // 假设这里需要根据数据类型大小做一些预处理 int data_type_size sizeof(src); // ... 其他逻辑 memcpy(dest, src, size); }当时我的意图是想获取源数据指针src所指向内容的大小比如一个int或一个结构体。我理所当然地写下了sizeof(src)心想这应该返回指针指向的数据类型大小。然而程序运行起来后内存拷贝总是出错数据对不上。排查了很久最后才发现sizeof(src)返回的并不是char的大小1字节也不是src指向的字符串长度而是char*这个指针变量本身在内存中占用的字节数在我的开发环境64位系统上这个值是8。那一刻我才彻底明白sizeof是一个运算符它在编译时就确定了结果它“看”的是变量或类型的声明而不是运行时的值。对于指针src它的类型就是char*所以sizeof(src)就是sizeof(char*)。这个教训让我意识到很多C语言初学者甚至是有一定经验的开发者都可能对sizeof存在误解仅仅把它当作一个“获取大小”的工具而忽略了它作为编译时运算符的本质。所以今天我们就来彻底拆解sizeof运算符。这不是一个简单的“查手册”就能完全掌握的知识点它涉及C语言编译原理、内存对齐、类型系统等底层概念。无论你是正在啃《C Primer Plus》的新手还是工作中需要与内存、硬件打交道的嵌入式或系统程序员深入理解sizeof都至关重要。它能帮你写出更高效、更安全的代码也能让你在调试一些“灵异”内存问题时快速定位到根源。2.sizeof的本质编译时求值的运算符首先我们必须从最根本的概念上纠正一个常见的错误认知sizeof是C语言的一个单目运算符不是函数。这个区别至关重要它直接决定了sizeof的行为特性。2.1 运算符与函数的根本区别为什么强调它是运算符因为函数调用发生在程序运行时而sizeof的求值发生在程序编译时。编译器在生成最终的可执行文件之前就已经计算出了所有sizeof表达式的结果并将这个结果作为一个常量替换到代码中。我们可以用一段简单的代码来验证#include stdio.h int main() { int a 10; size_t size_int sizeof(a); // 等价于 sizeof(int) size_t size_type sizeof(int); printf(sizeof(a) %zu\n, size_int); printf(sizeof(int) %zu\n, size_type); // 关键验证sizeof 内的表达式不会被执行 int i 0; size_t size_expr sizeof(i); // i 这个操作不会被执行 printf(sizeof(i) %zu, i %d\n, size_expr, i); // i 的值仍然是 0 return 0; }运行这段代码你会发现i的值在sizeof(i)之后依然是0。因为编译器只关心i这个表达式的类型这里是int并计算该类型的大小。它根本不会去生成执行i的机器指令。如果sizeof是函数那么参数i会先被求值然后传递进去i就会变成1。这个特性也意味着sizeof可以用于一些函数无法使用的场合比如求一个不完整数组的大小在函数参数中数组会退化为指针但sizeof在定义数组的作用域内可以获取其总大小。2.2sizeof的两种使用形式作为运算符sizeof有两种语法形式sizeof(类型) 例如sizeof(int),sizeof(char*)。sizeof 表达式 例如sizeof a,sizeof(a b)。注意当表达式是单个变量时括号有时可以省略但为了清晰和避免优先级问题我强烈建议始终使用括号。例如sizeof a b会被解析为(sizeof a) b这可能不是你的本意。而sizeof(a b)则明确表示求表达式ab结果类型的大小。2.3 返回类型size_tsizeof的返回值类型是size_t。这是一个定义在stddef.h等头文件中的无符号整数类型。它的具体大小位数是平台相关的但保证足够大能够表示该平台上任何对象在内存中占用的字节数。在打印size_t类型的值时应该使用%zu格式说明符C99标准引入。使用错误的格式符如%d,%u可能导致未定义行为在64位系统上尤其容易出错。// 正确的打印方式 printf(Size is: %zu bytes\n, sizeof(int)); // 错误的打印方式可能在64位系统产生警告或错误输出 printf(Size is: %d bytes\n, sizeof(int)); // 不推荐3. 深入核心sizeof如何计算各种类型的大小理解了sizeof是编译时运算符后我们来看看它面对不同操作数时的具体计算规则。这里的水比想象中要深。3.1 基本数据类型并非理所当然很多人认为sizeof(char)一定是1sizeof(int)一定是4。前半句是对的C标准明确规定sizeof(char)等于1字节。并且sizeof返回的“1字节”在标准中被称为char的大小其他类型的大小都是它的倍数。但sizeof(int)等于4这只是一种常见情况并非C语言标准的规定。C标准只规定了int的最小范围至少能表示 -32767 到 32767并没有规定其具体的字节数。它的大小由编译器和目标平台CPU架构、操作系统共同决定遵循所谓的“数据模型”。常见的几种数据模型LP32 / ILP32 在32位系统如x86 Linux/Windows上常见。int,long,pointer都是32位4字节。LP64 在64位Unix-like系统如x86-64 Linux/macOS上主流。int是32位4字节long和pointer是64位8字节。LLP64 在64位Windows系统上使用。int和long都是32位4字节long long和pointer是64位8字节。所以编写跨平台代码时绝不能对int,long等类型的大小做硬编码假设。如果需要确定大小的整数请使用stdint.h中定义的int8_t,uint32_t,int64_t等类型。3.2 数组类型sizeof的“高光时刻”这是sizeof最有用也最容易产生困惑的地方之一。当操作数是一个数组名时sizeof返回的是整个数组在内存中占用的总字节数。int arr[10]; printf(sizeof(arr) %zu\n, sizeof(arr)); // 输出 40 (假设 int 为4字节 4 * 10 40) // 计算数组元素个数的一个经典惯用法 size_t element_count sizeof(arr) / sizeof(arr[0]); printf(Number of elements: %zu\n, element_count); // 输出 10sizeof(arr) / sizeof(arr[0])这个公式是C语言中计算静态数组元素个数的“标准答案”它是在编译时计算的常量表达式非常高效安全。但是这里有一个巨大的“坑”当数组名作为函数参数传递时它会退化为指向其首元素的指针。此时在函数内部使用sizeof获取到的便是指针的大小而不是原数组的大小。void print_size(int array_param[]) { // 等价于 void print_size(int *array_param) // 这里 array_param 已经是一个指针了 printf(Inside function - sizeof(array_param): %zu\n, sizeof(array_param)); // 输出指针大小 (如 8) printf(Inside function - sizeof(*array_param): %zu\n, sizeof(*array_param)); // 输出 int 大小 (如 4) } int main() { int my_array[20]; printf(In main - sizeof(my_array): %zu\n, sizeof(my_array)); // 输出整个数组大小 (如 80) print_size(my_array); return 0; }这是无数C程序员踩过的坑。如果你需要在函数内部知道传入数组的大小必须额外传递一个表示数组长度的参数。3.3 结构体与联合体内存对齐的“魔术”sizeof应用于结构体struct或联合体union时结果往往不等于其各成员大小简单相加。这背后是“内存对齐”在起作用。内存对齐 现代计算机的CPU并非可以随意从任意地址读取数据。为了提升访问效率CPU通常要求数据的地址是其自身大小的整数倍。编译器为了满足这个要求会在结构体成员之间插入“填充字节”。看一个例子struct example1 { char a; // 1字节 // 编译器插入3字节填充 (padding)因为下一个int需要4字节对齐 int b; // 4字节 char c; // 1字节 // 编译器可能再插入3字节填充使得整个结构体大小是最大成员对齐值的整数倍 }; printf(sizeof(struct example1): %zu\n, sizeof(struct example1)); // 很可能输出 12 struct example2 { int b; // 4字节 char a; // 1字节 char c; // 1字节 // 为了满足int对齐a和c后面只需要2字节填充即可 }; printf(sizeof(struct example2): %zu\n, sizeof(struct example2)); // 很可能输出 8example1和example2的成员一模一样只是顺序不同sizeof的结果却相差了4个字节这就是内存对齐导致的。合理的成员排列可以节省内存这在内存受限的嵌入式系统中尤为重要。注意 对齐规则和填充策略是编译器相关的但遵循ABI应用程序二进制接口规范。你可以使用#pragma pack(n)或__attribute__((packed))等编译器扩展来修改对齐方式但这可能以牺牲性能为代价并可能导致一些平台相关的兼容性问题。对于联合体unionsizeof返回的是其最大成员的大小同时也会考虑对齐。union my_union { int i; char c[10]; double d; }; // sizeof(union my_union) 至少是10char数组大小还要向上对齐到double的对齐要求通常是8。 // 最终大小可能是16。3.4 指针类型永远的大小无论指针指向什么类型的数据sizeof对任何指针类型int*,char*,void*, 函数指针等的求值结果都是该指针变量本身在内存中占用的字节数。这取决于目标平台的寻址能力32位系统 指针通常是4字节。64位系统 指针通常是8字节。printf(sizeof(int*) %zu\n, sizeof(int*)); printf(sizeof(char*) %zu\n, sizeof(char*)); printf(sizeof(void*) %zu\n, sizeof(void*)); printf(sizeof(void (*)()) %zu\n, sizeof(void (*)())); // 函数指针 // 在同一个平台上这些输出值通常是一样的。3.5 函数类型与voidsizeof不能用于函数类型或不完整的类型如未指定长度的数组、前置声明的结构体。// sizeof(void); // 错误无效应用于 void 类型 // sizeof(main); // 错误无效应用于函数名函数类型但是sizeof可以用于void表达式这在GNU C扩展中允许结果为1。不过依赖这种非标准特性会损害代码的可移植性。4. 实战进阶sizeof在工程中的典型应用与陷阱了解了基本规则我们来看看在实际项目中sizeof如何被正确或错误地使用。4.1 动态内存分配malloc的好搭档这是sizeof最经典、最重要的应用场景之一。在调用malloc,calloc,realloc等动态内存分配函数时必须准确计算需要分配的字节数。// 为10个int分配空间 int *dynamic_array (int*)malloc(10 * sizeof(int)); // 注意不是 malloc(10 * sizeof(int*)) 那是分配10个指针的空间。 // 为一个结构体分配空间 struct MyStruct *p (struct MyStruct*)malloc(sizeof(struct MyStruct)); // 更安全、更推荐的写法避免类型名写错 struct MyStruct *p_safe malloc(sizeof(*p_safe)); // 如果 p_safe 的类型未来改变了这行代码无需修改。使用sizeof(*pointer)作为malloc的参数是一种最佳实践它让代码与类型声明紧密绑定减少了因类型修改而忘记更新malloc参数的风险。4.2 内存操作函数安全与效率的基石memcpy,memset,memcmp等函数都需要一个表示字节数的参数。sizeof在这里不可或缺。struct Data src, dest; // 复制整个结构体 memcpy(dest, src, sizeof(struct Data)); // 初始化数组为0 int arr[100]; memset(arr, 0, sizeof(arr)); // 使用整个数组的大小 // 比较两个结构体是否二进制相等 if (memcmp(obj1, obj2, sizeof(struct Data)) 0) { // 相等 }4.3 泛型编程的雏形offsetof宏在C语言中我们可以利用sizeof和指针运算来实现一些简单的泛型操作。标准库stddef.h中的offsetof宏就是一个典范它用于计算结构体成员相对于结构体起始地址的偏移量。它的一个典型实现就依赖于sizeof// offsetof 宏的一个可能实现 (编译器通常内置) #define OFFSETOF(type, member) ((size_t)(((type *)0)-member))理解这个宏需要一些技巧它先将地址0强制转换为type*类型然后访问其member成员并取地址。由于起始地址是0这个地址值就是偏移量。虽然不直接使用sizeof但sizeof在理解结构体布局和指针运算时是基础。4.4 常见陷阱与避坑指南对字符串使用sizeofchar str1[] Hello; char *str2 World; printf(sizeof(str1)%zu\n, sizeof(str1)); // 输出 6 (包含结尾的 \0) printf(sizeof(str2)%zu\n, sizeof(str2)); // 输出 8 (指针大小) printf(strlen(str1)%zu\n, strlen(str1)); // 输出 5 (字符串长度不含\0)记住sizeof(数组)返回总分配大小包括\0sizeof(指针)返回指针大小strlen返回字符串长度。在函数参数中使用sizeof获取数组大小 如前所述这是无效的。必须显式传递大小参数。误用sizeof计算动态分配数组的大小int *p malloc(100 * sizeof(int)); size_t wrong_size sizeof(p); // 错误这是指针大小不是分配的内存大小。 // C语言没有标准方法在运行时获取 malloc 分配块的大小。动态分配的内存块大小需要程序员自己管理。忽略括号导致的优先级问题int a 10; double b 20.0; size_t s1 sizeof a b; // 等价于 (sizeof a) b结果是 double 类型。 size_t s2 sizeof (a b); // 计算表达式 ab (结果为double) 的大小。始终使用括号是避免此类问题最简单的方法。5. 编译器视角与底层探索要真正吃透sizeof我们需要稍微了解一下编译器在背后做了什么。5.1 编译时求值的实现当编译器遇到sizeof表达式时它会确定操作数的类型。如果是表达式则确定其结果的类型。根据目标平台的ABI应用程序二进制接口规范查询该类型的大小。这个信息在编译器设计时就已经确定并内置在编译器中。将sizeof表达式替换为一个整型常量。这个常量会被直接写入生成的目标代码汇编或机器码中程序运行时没有任何计算开销。你可以通过查看编译器生成的汇编代码来验证这一点。使用-S选项在GCC/Clang中可以让编译器生成汇编文件你会看到sizeof的结果直接以立即数如$4,$8的形式出现。5.2sizeof与_Alignof(C11)C11标准引入了_Alignof运算符用于查询类型的对齐要求。它和sizeof是兄弟都是在编译时工作。理解对齐对于深入理解sizeof在结构体上的行为非常有帮助。#include stdalign.h // 为了使用 alignof 宏通常定义为 _Alignof struct S { char c; int i; }; printf(Alignment of char: %zu\n, alignof(char)); // 通常是1 printf(Alignment of int: %zu\n, alignof(int)); // 通常是4 printf(Alignment of struct S: %zu\n, alignof(struct S)); // 通常是其成员最大对齐值即4 printf(Size of struct S: %zu\n, sizeof(struct S)); // 考虑对齐后的大小如8_Alignof返回的值就是该类型在内存中分配起始地址需要满足的“整数倍”条件。结构体的对齐要求通常是其成员中最大的那个_Alignof值。5.3 可变长度数组VLA与sizeof的运行时行为C99标准引入了可变长度数组VLA这给sizeof带来了一个特例。当sizeof的操作数是一个VLA时它的求值可能发生在运行时而不再是纯粹的编译时常量。void func(int n) { int vla[n]; // VLA size_t size sizeof(vla); // 这行代码在运行时计算 size n * sizeof(int) printf(VLA size: %zu\n, size); }这是因为VLA的大小在编译时无法确定取决于运行时传入的参数n。这是一个非常重要的例外情况在编写需要常量表达式的场合如静态数组大小、case标签、位域长度等时不能使用涉及VLA的sizeof。6. 举一反三从sizeof延伸出的编程思维深入理解sizeof不仅仅是掌握一个运算符更是培养一种严谨的、贴近机器模型的编程思维。数据抽象与内存具象 C语言给了程序员直接操作内存的能力sizeof就是连接高级语言类型系统和底层内存布局的桥梁。当你写sizeof(struct PacketHeader)时你心里应该能大致勾勒出这个结构体在内存中是如何“摆放”的哪里可能有填充字节。这种能力对于网络编程、协议解析、硬件寄存器映射等场景至关重要。可移植性意识 硬编码数字如int size 4;是代码可移植性的天敌。sizeof提供了一种与平台无关的方式来描述数据大小。任何与内存尺寸相关的操作都应该优先考虑使用sizeof来计算。编译时与运行时 清晰地区分哪些事情发生在编译时如sizeof(类型)哪些发生在运行时如strlen是写出高效、正确代码的关键。编译时能解决的问题绝不留到运行时。防御性编程 在使用memcpy,malloc等函数时养成使用sizeof计算大小的习惯并优先使用sizeof(*ptr)这种形式可以避免许多因类型修改而引入的隐蔽bug。这是一种低成本、高收益的防御性编程实践。最后回到我开头提到的那个“坑”。那次经历让我养成了一个习惯每当我要使用sizeof时我都会停下来问自己两个问题“我现在操作的对象它的类型到底是什么”以及“我期望sizeof在这里给我什么是整个对象的大小还是指针的大小或者是其他什么东西” 多问这两个问题能帮你避开sizeof绝大多数常见的陷阱。