先说明一个很多新手都会问的入门问题:C语言里的sizeof到底是什么?它看起来像是函数,用起来也像函数,但严格来说它是一个运算符。这个区分不是抠字眼,而是理解它一系列“奇怪行为”的钥匙。这篇内容我尽量把sizeof的前前后后讲透,从它的本质、各种实际用法,到高频踩坑点和面试常客,一次性说清楚。
如果你刚开始学C语言,或者已经在代码里用过sizeof但偶尔会被它的结果吓一跳,这篇文章就是写给你的。看完之后你会发现,很多“莫名奇妙”的数值,背后原因其实非常清晰。
1. sizeof 的核心认知:不是函数,是运算符
1.1 先看它长什么样
sizeof有两种语法形式,这是很多教材都会强调但新手容易忽略的细节。
- 对变量或表达式求大小:
sizeof 变量名或sizeof(变量名),括号可加可不加。 - 对类型名求大小:
sizeof(类型名),这里的括号必须加,因为类型名本身不是表达式。
为什么会有这种区别?因为sizeof本质上是语言层面的运算符,不是库函数。编译时它需要知道“你要测量谁的大小”,而这个“谁”可以是一个变量,也可以是一个类型。变量直接写名字就能让编译器识别,但类型名在语法上需要一个边界,所以必须用括号把它包起来。
int a = 0; printf("%zu\n", sizeof a); // 合法,对变量求大小 printf("%zu\n", sizeof(int)); // 合法,对类型求大小 printf("%zu\n", sizeof int); // 编译错误:类型名必须加括号这里插一句:sizeof的返回值类型是size_t,在绝大多数现代系统上等价于unsigned long或unsigned long long。用printf打印时格式说明符应该用%zu,这是C99标准加入的专门用于size_t的占位符。很多老教程还在用%d或%lu,严格来说都不够严谨。
1.2 它在编译期求值,不是运行期
这是理解sizeof最重要的一个点。
sizeof的计算发生在编译阶段,编译器看到sizeof(某个类型或变量)时,就直接根据类型系统算出内存占用大小,把它当成一个常量替换到代码里。你的程序运行时,根本不存在“去测量内存”这个动作。
这意味着什么?意味着sizeof的结果只和静态类型有关,和运行时内存里实际存了什么值完全没有关系。经典的例子:
int arr[10]; printf("%zu\n", sizeof(arr)); // 40(假设 int 占4字节) printf("%zu\n", sizeof(arr[5])); // 4,只关心 arr[5] 的类型是 int就算arr[5]里存的数是一个天文数字,sizeof的结果依然是4。因为arr[5]的静态类型是int,和值无交易。
这个特性还能推出一个很有意思的规律:sizeof不会真正执行括号里的表达式。比如sizeof(++a),它只是看看++a这个表达式的类型,算出这个类型的大小,然后就把整个表达式丢弃了,a不会被自增。这是初学者最容易踩的坑之一,我见过不少人在代码里写sizeof(a++)指望顺便把变量加一,结果发现变量纹丝不动。
int a = 5; printf("%zu\n", sizeof(a++)); // 打印4(int大小) printf("%d\n", a); // 依然是5,a并没有自增1.3 sizeof 返回的是“字节数”,标准只保证这一点
很多人会纠结:sizeof(int)到底等于多少?是不是一定是4?
C语言标准规定的是:sizeof(char)恒等于1,sizeof的单位就是char的大小。至于int占几个char,标准只给了一个区间约束——int至少要能容纳-32767~32767这个范围,但没有强制规定必须是4字节。
在绝大多数现代桌面和服务器平台上,char1字节、short2字节、int4字节、long8字节(64位Linux)或4字节(Windows),但这只是“平台事实”,不是“语言保证”。写跨平台代码时,不要理所当然地认为int就是4字节。判断依据应该是sizeof的结果本身,或者是<stdint.h>里提供的定宽类型,比如int32_t、uint64_t,这类类型在不同平台上都会映射为相应宽度的类型。
2. 核心用法拆解:从变量到类型到表达式
2.1 对变量和普通类型求大小
先列一个最简单的参考表(以常见的32位或64位平台为例):
| 类型 | 常见大小(字节) | 备注 |
|---|---|---|
char | 1 | 标准恒为1 |
short | 2 | 至少2字节 |
int | 4 | 绝大多数现代平台 |
long | 4或8 | 平台相关,Windows是4,64位Linux是8 |
float | 4 | 单精度浮点 |
double | 8 | 双精度浮点 |
char* | 4或8 | 取决于指针宽度,和指向类型无关 |
注意指针那一行:sizeof(char*)、sizeof(int*)、sizeof(struct xxx*)在任何平台上结果都一样,因为它们都是“指针”,指针变量的内存大小只由平台地址宽度决定。所以你不用费心去记“某某类型的指针占多少字节”,记住“在当前平台所有指针大小相同”就够了。
2.2 对表达式求大小:只看类型,不算值
sizeof(表达式)的用法在实际工程里最常见的是这样两个场景:
第一个,用于确定性计算:比如你要动态分配一个结构体数组,写malloc(n * sizeof(struct Student))。这里的sizeof(struct Student)在编译期就确定好了,分配多少字节完全静态可算。
第二个,用于简化代码和增强可维护性:比如你写sizeof(a + b),如果a和b是int,那么结果就是sizeof(int);但如果你把a改成long long,结果自动变成sizeof(long long)。这时候用sizeof表达式的好处是,你不再需要关心具体类型名是什么,编译器会帮你判断表达式的最终类型。这种写法在泛型宏(C11的_Generic)或者模板式代码里非常有效。
这里有一个需要记住的点:sizeof(表达式)里的表达式虽然不会真的执行,但编译器依然会做类型推导。比如:
short s = 2; int i = 100000; printf("%zu\n", sizeof(s + i)); // 结果是4,因为 short 和 int 做运算会整型提升为 int这里s + i的静态类型是int,所以sizeof结果是4,而不是2。如果你还停留在“sizeof(s + i)等于sizeof(s)加sizeof(i)”的想象里,那就大错特错了。sizeof永远是对“一个东西”求大小,不是求和。
2.3 对字符串字面量求大小
字符串字面量在C语言里是一个char数组,只不过编译器替你在末尾加了'\0'。所以:
printf("%zu\n", sizeof("hello")); // 6,不是5因为字符串实际占用了6个字节:h、e、l、l、o、'\0'。这个是新手特别容易遗漏的点。你可以用sizeof("hello") - 1得到实际可见字符的个数,很多人会用这个技巧来避免调用strlen,尤其在编译期就能确定字符串长度、不依赖运行时的场景里。
#define STRLEN(s) (sizeof(s) - 1)但这个宏有局限性:它只对字符串字面量有效。如果你传入一个char*变量,得到的就是指针大小减1,完全没有意义。所以这类宏往往只配合字符串字面量使用。
3. 高频陷阱:数组、指针与“退化”
3.1 数组名与指针的经典区别
在C语言里,数组名在大多数表达式中会“退化”为指向首元素的指针,但在两个运算符里不会:一个是&,另一个就是sizeof。所以sizeof(数组名)得到的是整个数组占用的总字节数,而sizeof(指向数组首元素的指针)得到的只是一个指针的大小。我见过太多初学者在这里栽跟头:
int arr[10]; int *p = arr; printf("%zu\n", sizeof(arr)); // 40,整个数组 printf("%zu\n", sizeof(p)); // 8 或 4,只是指针本身这个差异在函数传参时变成了“重灾区”。当你把数组传给函数时,函数形参里写的int arr[]本质上是一个指针,它并不保留数组的长度信息:
void func(int arr[]) { printf("%zu\n", sizeof(arr)); // 永远是 8 或 4,不是数组大小 } int main() { int data[10]; func(data); }输出结果会让人怀疑人生:明明实参是一个10个元素的数组,怎么在函数里sizeof就变小了?原因就是数组参数退化:编译器把函数形参int arr[]重新解释为int *arr。所以函数内部无法通过sizeof获得原始数组的大小。要解决这个问题,最常见做法是额外传入长度参数:
void func(int arr[], size_t n) { for (size_t i = 0; i < n; i++) { // 遍历数组 } }这是写C代码的基本功:任何需要处理数组长度的函数,要么传长度,要么用结构体把数组和长度包在一起,没有第三条路。
3.2 二维数组与“行”的概念
二维数组求大小也是个高频考点。看这个例子:
int matrix[4][5]; printf("%zu\n", sizeof(matrix)); // 4*5*4 = 80 printf("%zu\n", sizeof(matrix[0])); // 5*4 = 20,这一行有5个int printf("%zu\n", sizeof(matrix[0][0]));// 4,单个int关键理解:matrix[0]的类型是int[5],是包含5个int的数组,所以sizeof(matrix[0])得到的是这一行的大小。用好这个特性可以写出更清晰的循环边界:
for (int i = 0; i < sizeof(matrix) / sizeof(matrix[0]); i++) { for (int j = 0; j < sizeof(matrix[0]) / sizeof(matrix[0][0]); j++) { // 访问 matrix[i][j] } }上面计算行数用的sizeof(matrix) / sizeof(matrix[0]),意思是总字节数除以一行字节数,就是行数。同理,列数用sizeof(matrix[0]) / sizeof(matrix[0][0])。这种写法比硬编码4和5更安全,改数组维度时循环不用手动跟着改。
但注意,一旦这个二维数组作为参数传给函数,又回到了3.1节那个退化问题:
void process(int matrix[][5], size_t rows) { printf("%zu\n", sizeof(matrix[0])); // 还是20,因为 matrix[0] 的类型依然是 int[5] printf("%zu\n", sizeof(matrix)); // 退化为指针大小,即 8 }这里有个细节很多人会误解:二维数组作为函数参数时,列数必须写出来,因为编译器要知道“每一行有多长”才能正确计算地址偏移。int matrix[][5]里的5不能省,但第一维可以用指针变量传入。
3.3 字符串数组与指针的细微差别
一个高频面试题:char s[] = "hello"和char *s = "hello"有什么区别?
用sizeof一眼就能看穿:
char s1[] = "hello"; char *s2 = "hello"; printf("%zu\n", sizeof(s1)); // 6,数组整体大小,包含 '\0' printf("%zu\n", sizeof(s2)); // 8 或 4,指针大小s1是在栈上分配了6个字节的字符数组内容可以修改;s2是一个指针,指向字符串字面量所在的内存区域,这种字面量通常位于只读区,修改它会引发未定义行为。sizeof的差异正好反映了这两者在内存布局上的本质不同。
4. 结构体与字节对齐:sizeof 的进阶实战
4.1 为什么结构体大小不是字段之和
这是个很有代表性的现象:一个结构体里各个字段的字节数加起来是10,但sizeof的结果可能是12、16甚至24。原因在于编译器会按对齐规则在字段之间插入填充字节。
struct A { char c; // 1字节 int i; // 4字节 }; printf("%zu\n", sizeof(struct A)); // 通常不是5,而是8为什么会这样?因为大多数平台要求int类型在内存中的地址是4的倍数,为了让i对齐到合适的地址,编译器在c后面填了3个字节的空洞。这个空洞是真实存在的内存占用,所以sizeof的结果包含了它们。
结构体的总大小还要满足一个规则:结构体的整体大小必须是所有字段中对齐要求最严格的那个字段的对齐值的整数倍。比如上面的结构体里最严格的是int(对齐到4),那么结构体大小必须是4的倍数,8满足,5不满足。
4.2 字段顺序会影响结构体大小
这个技巧在实际项目里很有用:把结构体字段按大小从大到小排列,往往可以减少填充字节。看两个结构体:
struct B { char c; double d; int i; }; struct C { double d; int i; char c; };假设double8字节、int4字节、char1字节,且对齐规则为自然对齐:struct B的大小往往是24,而struct C可能是16。原因在于struct B中char后面为对齐double填了7个字节,然后int后为了结构体总大小是8的倍数又填了3个字节;而struct C从double开始,后续字段依次紧密排列,几乎不需要额外填充。
如果你在写网络协议解析、文件格式解析、共享内存映射这类对内存布局敏感的程序,调整字段顺序可以显著节省内存,尤其是在你有大量结构体实例时,这种优化是白捡的收益。
4.3 与 #pragma pack 的相爱相杀
有些场景(比如解析二进制文件)需要结构体严格按1字节对齐,不能用默认对齐。这时很多人会用#pragma pack(1)或__attribute__((packed))来取消填充:
#pragma pack(1) struct Packet { char type; int len; char data; }; #pragma pack() printf("%zu\n", sizeof(struct Packet)); // 6,不再有填充字节这种写法能让结构体大小等于字段之和,方便直接映射到协议缓冲区。但代价是:访问非对齐字段时,某些平台(尤其是ARM系列)会产生性能下降,甚至触发硬件异常。x86上多数情况只是慢一点,但跨平台代码需谨慎。我的习惯是:能不用packed就不用,协议解析时优先用“偏移量+手动读取”的方式,而不是过度依赖结构体直接映射。这样既能避开对齐问题,也更容易处理大小端转换。
4.4 一个实用技巧:用 offsetof 查看字段偏移
想知道结构体某个字段相对起点的偏移量,sizeof配合<stddef.h>里的offsetof宏可以做:
#include <stddef.h> struct Data { char a; int b; char c; }; printf("offset of a: %zu\n", offsetof(struct Data, a)); printf("offset of b: %zu\n", offsetof(struct Data, b)); printf("offset of c: %zu\n", offsetof(struct Data, c)); printf("total size: %zu\n", sizeof(struct Data));你可以亲自运行看看输出:b的偏移量往往是4而不是1,c的偏移量可能是8,结构体总大小可能是12。这个宏在实现容器结构、反射机制或者手动序列化时非常有用。
5. sizeof 在动态内存与安全编码中的实战
5.1 malloc 时不要手动写错类型大小
动态内存分配是最能体现sizeof价值的场景之一。正确写法:
int *p = malloc(10 * sizeof(int));有些人喜欢写成malloc(10 * 4),这里假设了int是4字节。换一个平台就可能出错。而malloc(10 * sizeof(*p))还有一个额外好处:就算你后来把p的类型从int*改成double*,这行代码依旧正确,不需要同步修改。
double *p = malloc(n * sizeof(*p)); // 推荐我见过不少老代码用malloc(n * sizeof(int))分配内存给double*用,结果在类型宽度不同的平台上悄无声息地内存越界,这种bug非常难排查。用sizeof(*p)相当于让编译器替你把类型和分配大小绑在一起。
5.2 使用变长数组时的注意点
C99引入了变长数组,允许用变量定义数组。很多人会写出这样的代码:
int n = 10; int arr[n]; printf("%zu\n", sizeof(arr)); // 40,这里依然正确对于变长数组,sizeof的结果不再是编译期常量,而是在运行时根据n的值计算出来的。但它反过来又基于静态类型规则:整个数组大小就是元素大小乘以元素个数。
变长数组虽然方便,但栈上分配大数组容易爆栈,而且错误处理不灵活。实际工程中如果要处理不确定大小的数据,我更推荐malloc。这也是为什么C11把变长数组设为可选特性,而不是强制要求。
5.3 缓冲区清零与 sizeof 的配合
memset和sizeof的组合是C语言日常操作里出现频率极高的:
struct Message msg; memset(&msg, 0, sizeof(msg));这里的sizeof(msg)必须用结构体变量形式,因为sizeof(struct Message)也可以,但写msg的好处是如果变量类型变了,这行代码不用改。类似的还有memcpy、memmove等函数,第三个参数几乎总是sizeof。
一个小坑:如果你有一个指向结构体的指针,要初始化它指向的内存,不能写memset(p, 0, sizeof(p)),那样只会清掉指针本身占用的8字节,而结构体完全没有被初始化。正确写法是memset(p, 0, sizeof(*p))。我把这个列为C语言新手最容易犯的“看起来对但实际错”的经典错误之一。
6. 常见问题与速查表
6.1 快速排查指南
我自己排查sizeof相关问题时,经常用这类思路:
| 现象 | 可能原因 | 验证方法 |
|---|---|---|
sizeof(数组)结果比预想小 | 函数参数里的数组退化为指针 | 打印函数内外的sizeof对比 |
sizeof(表达式)没有按预期执行副作用 | sizeof不求值表达式 | 看看表达式里的变量有没有变化 |
| 结构体大小比字段之和大 | 字节对齐填充 | 用offsetof打印各字段偏移 |
字符串sizeof比strlen大1 | 字符串末尾的'\0' | 分别打印两个结果 |
| 结构体大小在不同平台不一致 | 对齐规则或int/long宽度差异 | 用sizeof打印每个字段类型大小 |
6.2 面试常客:sizeof 与 strlen 对比
sizeof和字符串函数strlen常被拿来对比,我直接给一张对照表:
| 对比维度 | sizeof | strlen |
|---|---|---|
| 类别 | 运算符,编译期求值 | 库函数,运行期求值 |
| 对数组 | 返回整个数组占用字节数 | 从起始位置扫描到'\0'为止 |
| 对指针 | 返回指针本身大小 | 沿指针所指内存扫描 |
是否包含'\0' | 字符串字面量时包含 | 不包含 |
| 有无副作用 | 无,不求值表达式 | 有,要遍历内存 |
例子:
char s[] = "hello"; printf("%zu\n", sizeof(s)); // 6 printf("%zu\n", strlen(s)); // 56.3 我的几个实际经验
第一,不要在sizeof里写带副作用的表达式。你写sizeof(i++)期待i自增,实则没有。遇到这种需求直接写两行,别玩花活。
第二,打印sizeof结果时用%zu。不少人为了省事用%d,其实在size_t是无符号64位整数的平台上,%d是隐患,大值会显示负数或截断。养成用%zu的习惯,一条输出语句而已。
第三,在结构体传参时显式传大小。别写一个接收结构体指针的函数然后在函数内部用sizeof(*p)来计算,如果那个结构体类型后续被改小了,而调用方实际传入的是一块更大的旧结构体,用sizeof(*p)处理会漏掉尾部字段。对外的API建议显式接收一个size_t size参数,代表调用方传入的缓冲区实际大小。
7. 进一步延伸:typeof 与泛型宏
如果你已经熟练掌握了sizeof,再看两个与之紧密相关的进阶手法,会让代码质量再上一个台阶。
第一个是 GNU C 扩展里的typeof或 C23 标准化的typeof,它可以提取表达式的类型,从而构造更安全更通用的宏。配合sizeof的自动推导能力,能写出非常丝滑的代码:
#define MAX(a, b) ({ \ typeof(a) _a = (a); \ typeof(b) _b = (b); \ _a > _b ? _a : _b; \ })这个宏里typeof保证_a和_b的类型与实参完全一致,避免类型不匹配。虽然是GNU扩展,但在 GCC 和 Clang 下都很常用。
第二个是C11标准中的_Generic泛型选择,它可以让宏根据参数类型选择不同的处理分支:
#define TYPE_DESC(x) _Generic((x), \ int: "int", \ double: "double", \ char*: "char*", \ default: "other" \ )这类工具和sizeof一样,都是“在编译期做类型分析”的思路。理解了sizeof的编译期求值本质,再学这些概念就会轻松很多。
从入门到进阶,sizeof看起来只是一个求大小的运算符,但它的背后牵扯出数组退化、指针大小、结构体对齐、编译期求值等等C语言的核心机制。我始终觉得,把sizeof彻底吃透,是打通C语言基本功的一个标志性节点。希望这篇文章能帮你在这一站走得顺畅。