sizeof是C语言里一个很特别的存在:它长得像函数,实际却是运算符;它出现在表达式里,却在编译阶段就能算出结果。不少初学者把它当成“求数组长度”的函数来用,结果在数组作为参数传递的函数里拿到了意想不到的值,或者被结构体的内存对齐搞得一头雾水。这篇文章就来把sizeof的作用、求值规则和常见坑点讲透。

一、sizeof到底是什么:运算符而非函数
首先纠正一个普遍的误解:sizeof不是函数,而是C语言的关键字、一个编译期运算符。它的作用是求出一个操作数在内存中占用的字节数,结果类型是size_t(无符号整数类型,定义在stddef.h中)。写成sizeof(x)带括号只是习惯写法,当操作数是类型名时括号是必须的,比如sizeof(int);当操作数是变量时括号可以省略,比如int a; sizeof a;也是合法的。
sizeof最关键的特性是:它几乎总在编译期完成求值(C99引入变长数组VLA后是唯一的例外)。也就是说,编译器在生成目标代码时就已经把sizeof替换成一个常数值了,运行时不会执行任何计算,更不会对表达式产生副作用。看下面这个经典例子:
#include <stdio.h>
int main(void)
{
int a = 10;
printf("before: %d\n", a);
printf("sizeof size: %zu\n", sizeof(a++)); /* a++不会被执行 */
printf("after: %d\n", a);
return 0;
}
这段代码里sizeof(a++)并不会让a自增,输出的after值仍然是10。因为sizeof只关心操作数的类型,不去真正求值表达式,编译器直接用sizeof(int)也就是4替代了整个表达式。这个特性意味着sizeof几乎零运行时开销,非常适合用在数组遍历、内存分配等场景。
二、sizeof的各种求值场景与代码示例
sizeof可以作用于基本类型、变量、数组、指针、结构体甚至函数返回值类型。不同对象的结果差异很大,需要逐个理解。先看一组常见类型的字节数:
#include <stdio.h>
int main(void)
{
printf("char : %zu\n", sizeof(char)); /* 恒为1 */
printf("int : %zu\n", sizeof(int)); /* 常见为4 */
printf("double : %zu\n", sizeof(double)); /* 常见为8 */
printf("指针 : %zu\n", sizeof(int*)); /* 64位系统为8 */
int arr[10];
printf("数组 : %zu\n", sizeof(arr)); /* 40 = 10 * 4 */
printf("元素个数 : %zu\n", sizeof(arr) / sizeof(arr[0])); /* 10 */
return 0;
}
这里最实用的技巧就是sizeof(arr) / sizeof(arr[0]),即用数组总字节数除以单个元素的字节数,得到元素个数。这是C语言里遍历数组的标准写法,比硬编码数字更安全,以后修改数组长度时循环代码完全不用动。
需要注意的是,sizeof对数组求值得到的是整个数组占用的字节数,对指针求值得到的却是指针变量本身的字节数。在64位平台上无论指针指向什么,sizeof(指针)都是8;而32位平台上是4。这个区别是无数bug的源头,下一节会展开讨论。
三、数组传参后sizeof失效:最经典的坑
很多初学者写过这样的代码:在main里用sizeof求数组长度没问题,把数组传给函数后sizeof却变成了固定值。比如下面这段:
#include <stdio.h>
void print_size(int arr[])
{
/* 数组作为参数退化为指针,sizeof得到的是指针大小 */
printf("in function: %zu\n", sizeof(arr)); /* 64位系统输出8 */
}
int main(void)
{
int arr[10];
printf("in main : %zu\n", sizeof(arr)); /* 输出40 */
print_size(arr);
return 0;
}
原因在于C语言的数组退化规则:数组作为函数参数传递时,会自动退化为指向首元素的指针。函数参数列表里写int arr[]或者int arr[10],编译器实际上都按int *arr处理。所以函数内部对arr做sizeof,得到的只是指针本身的大小。
正确的做法是把长度作为额外参数一起传进去,例如void func(int arr[], int len),调用时用func(arr, sizeof(arr) / sizeof(arr[0]))。这也是为什么标准库的很多函数(比如memcpy、qsort)都要求调用者显式传入长度或元素个数。理解了数组退化,就能明白为什么sizeof只能在数组定义所在的作用域内正确工作。
四、结构体与内存对齐:sizeof结果为何比想象中大
对结构体使用sizeof时,结果常常比所有成员大小之和还要大,这是内存对齐造成的。编译器为了提高CPU访问内存的效率,会在成员之间插入填充字节,让每个成员的地址落在其自身对齐要求的边界上。看这个例子:
#include <stdio.h>
struct A {
char c; /* 1字节,但后面填充3字节 */
int i; /* 4字节 */
char d; /* 1字节,结构体尾部填充3字节 */
};
struct B {
int i; /* 4字节 */
char c; /* 1字节 */
char d; /* 1字节,尾部填充2字节 */
};
int main(void)
{
printf("sizeof(struct A) = %zu\n", sizeof(struct A)); /* 12 */
printf("sizeof(struct B) = %zu\n", sizeof(struct B)); /* 8 */
return 0;
}
结构体A和B的成员完全相同,只是顺序不同,大小却分别是12和8。A中char c后面要填充3字节让int i对齐到4的倍数,结构体总大小还必须是最大成员对齐值的倍数,所以尾部又补了3字节。这个现象说明:合理安排结构体成员顺序(把大成员放前面)可以有效节省内存。在做网络传输或文件存储时,如果需要紧凑布局,可以使用#pragma pack(1)取消填充,但要清楚这会牺牲一部分访问效率。
五、使用sizeof的其他常见误区
除了数组退化,还有几个坑值得留意。第一个是符号问题:sizeof返回的是无符号的size_t,如果拿它和负数int比较,int会被隐式转换成无符号数,导致判断逻辑完全颠倒。经典死循环写法:
#include <stdio.h>
int main(void)
{
int i = -1;
/* i被提升为无符号数,变成一个巨大的正数,条件恒成立 */
if (i < sizeof(int)) {
printf("这行会执行,逻辑出错\n");
}
return 0;
}
第二个坑是对字符串求sizeof和strlen分不清。sizeof("hello")得到6,因为它算的是字符数组包括结尾'\0'的总大小;而strlen("hello")得到5,它数的是'\0'之前的字符个数。一个算内存,一个算长度,语义完全不同。此外,sizeof对函数调用表达式求值时,得到的是函数返回类型的大小,函数本身并不会被调用。
总结一下,sizeof的核心作用是编译期计算类型或对象占用的字节数,它是实现可移植代码的重要工具。配合sizeof(arr) / sizeof(arr[0])求数组元素个数、用malloc(sizeof(struct X))动态分配内存,都是日常开发的标准操作。只要记住数组退化、内存对齐和无符号比较这三个坑,就能把sizeof用得又稳又准。