1. 别再把它当函数了:sizeof 到底是什么?
刚学C语言那会儿,我也犯过这个错误,总是不自觉地在 sizeof 后面加上括号,把它当成 printf、malloc 那样的函数来用。直到后来在项目里踩了坑,才真正明白,sizeof 是C语言里一个非常特殊的运算符,就像 +、-、* 一样。这个认知上的转变,直接关系到你写出的代码是否高效、是否正确。
你可以把它想象成一个“尺子”,但这把尺子很特别,它是在编译阶段就拿出来量尺寸的。什么意思呢?当你写下 sizeof(int) 这行代码时,编译器在把你的源代码翻译成机器码的过程中,就已经知道了 int 在你的系统上占几个字节,然后直接把这个数字(比如4)像写死一个常数一样,填到最终生成的程序里。程序真正运行起来的时候,压根没有“测量”这个动作,不会去内存里翻找 int 有多大,所以它零运行时开销。这一点和函数调用有天壤之别,函数调用需要压栈、跳转、执行、返回,是有成本的。
正因为它是编译时运算符,所以它操作的对象必须是在编译时就能确定大小的。你不能对一个运行时才能知道大小的东西用 sizeof,比如 sizeof(一个动态分配的数组),这是行不通的。它的返回值类型是 size_t,这是一个定义在 <stddef.h> 头文件里的无符号整数类型,专门用来表示对象的大小。用 printf 打印时,格式符要用 %zu,这个 z 就是为 size_t 准备的,可别再用 %d 或者 %u 了,在跨平台时可能会出问题。
理解 sizeof 是运算符而非函数,是深入使用它的第一步。这决定了你能用它做什么,不能做什么。比如,括号在某些情况下是可以省略的。当 sizeof 的操作对象是类型名时,必须加括号,如 sizeof(int);但当操作对象是一个变量时,括号可以省略,写成 sizeof variable 也是合法的。不过在实际编码中,为了统一和清晰,我建议都加上括号,这能让代码更易读,也避免一些不必要的歧义。
2. 从基础到进阶:sizeof 的六大核心应用场景
2.1 量体裁衣:基本数据类型的大小
这是我们最熟悉的用法,用来看看各种数据类型在你的机器上到底占多大地方。写个简单的程序跑一下,心里就有数了。
#include <stdio.h>
int main() {
printf("char 的尺寸是:%zu 字节\n", sizeof(char)); // 永远是1
printf("short 的尺寸是:%zu 字节\n", sizeof(short));
printf("int 的尺寸是:%zu 字节\n", sizeof(int)); // 常见的是4
printf("long 的尺寸是:%zu 字节\n", sizeof(long)); // 可能是4或8
printf("long long 的尺寸是:%zu 字节\n", sizeof(long long));
printf("float 的尺寸是:%zu 字节\n", sizeof(float)); // 常见的是4
printf("double 的尺寸是:%zu 字节\n", sizeof(double)); // 常见的是8
printf("指针的尺寸是:%zu 字节\n", sizeof(void*)); // 32位是4,64位是8
return 0;
}
这里有个关键点必须牢记:C语言标准只规定了每种类型的最小尺寸范围,并没有规定死具体是多少字节。比如 int,标准只说它至少能表示 -32767 到 32767,所以至少是2字节。但在我们常见的现代系统(ILP32 或 LP64 数据模型)上,int 通常是4字节。long 的大小更是和操作系统位数强相关:在32位Windows/Linux上通常是4字节,在64位Linux上通常是8字节,而在64位Windows上却仍然是4字节(这是一个著名的差异点)。所以,永远不要假设 sizeof(int) 一定等于4,写跨平台代码时,如果需要确定大小的整数,请使用 <stdint.h> 里的 int32_t、uint64_t 等类型。
2.2 指针的“体型”:所有指针都一样大吗?
很多初学者会疑惑,int*、char*、struct MyStruct* 这些指针的大小一样吗?答案是:在同一个平台上,所有数据指针的大小通常是一样的。
#include <stdio.h>
int main() {
int *p_int;
char *p_char;
double *p_double;
void *p_void;
printf("int* 大小: %zu\n", sizeof(p_int));
printf("char* 大小: %zu\n", sizeof(p_char));
printf("double* 大小: %zu\n", sizeof(p_double));
printf("void* 大小: %zu\n", sizeof(p_void));
printf("int* 本身的大小: %zu\n", sizeof(int*)); // 直接对类型名使用
return 0;
}
这段代码在64位系统上运行,很可能会输出5个8。指针的大小不取决于它指向什么类型的数据,而取决于系统的寻址空间。32位系统用32位(4字节)地址寻址,所以指针是4字节;64位系统用64位(8字节)地址,所以指针是8字节。但是,请注意函数指针、成员函数指针可能和普通数据指针大小不同,这是另一个话题了。另外,对指针本身使用 sizeof,量的是这个指针变量占用的内存大小,而不是它指向的内存块的大小。想获取指针指向的数组大小?对不起,sizeof 办不到,因为指针丢失了数组的长度信息。
2.3 变量与数组:实战中最常用的技巧
对变量使用 sizeof 是最直观的,它返回该变量所属类型的大小。但真正体现 sizeof 威力的,是在处理数组的时候。
#include <stdio.h>
int main() {
int num = 100;
int arr[10] = {0};
char str[] = "Hello, World!";
// 计算变量大小
printf("变量 num 的大小: %zu\n", sizeof(num)); // 等价于 sizeof(int)
// 计算整个数组占用的总内存
printf("数组 arr 的总大小: %zu\n", sizeof(arr)); // 10 * sizeof(int) = 40
// 计算数组单个元素的大小
printf("数组 arr 单个元素的大小: %zu\n", sizeof(arr[0])); // sizeof(int)
// 神技:计算数组元素个数(经典用法,务必掌握)
size_t element_count = sizeof(arr) / sizeof(arr[0]);
printf("数组 arr 的元素个数: %zu\n", element_count); // 10
// 对于字符串数组,同样适用(包含结尾的 '\0')
printf("字符串 str 的长度(含\\0): %zu\n", sizeof(str)); // 14个字符
printf("字符串 str 的字符数(不含\\0): %zu\n", sizeof(str)/sizeof(char) - 1); // 13
return 0;
}
用 sizeof(数组名) / sizeof(数组名[0]) 来计算数组元素个数,这是一个在栈上定义的静态数组的“黄金法则”。它之所以有效,是因为 sizeof(数组名) 在数组定义的作用域内,返回的是整个数组的字节大小。但这里有一个巨大的“坑”:当数组作为函数参数传递时,它会退化成指向其首元素的指针。此时在函数内部,sizeof(数组参数) 得到的是指针的大小,而不是数组的大小。
void print_size(int arr[]) { // 等价于 int *arr
printf("函数内 sizeof(arr): %zu\n", sizeof(arr)); // 输出8(指针大小),而不是40!
}
我在这上面栽过跟头。有一次写一个处理缓冲区的函数,想用这个方法判断传入的缓冲区大小,结果总是得到8(64位系统),程序行为诡异。后来才醒悟,数组长度信息必须在函数外计算好,通过另一个参数传进来。这是C语言数组机制的一个核心特点,必须时刻警惕。
2.4 函数与表达式:sizeof 的求值策略
对函数调用使用 sizeof,这个用法有点特别,而且很容易用错。
#include <stdio.h>
int func_int() {
printf("func_int 被调用了吗?\n");
return 42;
}
double func_double() {
printf("func_double 被调用了吗?\n");
return 3.14;
}
void func_void() {
printf("func_void 被调用了吗?\n");
}
int main() {
// 情况1:sizeof(函数调用表达式)
printf("sizeof(func_int()): %zu\n", sizeof(func_int()));
printf("sizeof(func_double()): %zu\n", sizeof(func_double()));
// 情况2:试图对void表达式使用sizeof(错误!)
// printf("%zu\n", sizeof(func_void())); // 编译错误:invalid application of 'sizeof' to a void type
// 情况3:试图对函数名使用sizeof(错误!)
// printf("%zu\n", sizeof(func_int)); // 编译错误:invalid application of 'sizeof' to a function type
return 0;
}
运行这段代码,你会发现一个有趣的现象:控制台没有打印出“func_int 被调用了吗?”这句话。这证明了前面说的:sizeof 是编译时运算符。sizeof(func_int()) 并不会真正去调用 func_int 函数,编译器只是分析 func_int() 这个表达式的返回类型是 int,然后直接把 sizeof(int) 的结果(比如4)填进去。所以,函数体内的任何副作用(比如打印、修改全局变量)都不会发生。
你可以这样理解:sizeof(表达式) 中的表达式是不被求值的(除非表达式是可变长度数组VLA的类型名,那是C99的特性,另当别论)。它只关心这个表达式的最终类型是什么。因此,sizeof(func_void()) 是错的,因为 void 类型没有大小;sizeof(func_int) 也是错的,因为函数名本身(作为函数指示符)在大多数上下文中会转换为函数指针,但 sizeof 不能直接用于函数类型。
2.5 结构体与联合体:内存对齐的“魔术师”
这是 sizeof 应用中最容易让人感到意外和困惑的地方。一个结构体的大小,并不简单地等于其所有成员大小之和。编译器为了提升内存访问效率,会进行“内存对齐”。
#include <stdio.h>
struct StructA {
char a; // 1字节
int b; // 4字节
short c; // 2字节
};
struct StructB {
int b; // 4字节
char a; // 1字节
short c; // 2字节
};
union UnionExample {
int i;
char c;
double d;
};
int main() {
printf("sizeof(struct StructA): %zu\n", sizeof(struct StructA));
printf("sizeof(struct StructB): %zu\n", sizeof(struct StructB));
printf("sizeof(union UnionExample): %zu\n", sizeof(union UnionExample));
// 打印成员大小和偏移量(需要 <stddef.h>)
printf("\n成员偏移量分析(StructA):\n");
printf("偏移量 of a: %zu\n", offsetof(struct StructA, a)); // 0
printf("偏移量 of b: %zu\n", offsetof(struct StructA, b)); // 可能是4
printf("偏移量 of c: %zu\n", offsetof(struct StructA, c)); // 可能是8
return 0;
}
在我的64位Linux系统上,使用默认对齐,输出可能是:
StructA大小:12字节StructB大小:8字节UnionExample大小:8字节
为什么 StructA 和 StructB 成员一样,大小却不同?这就是内存对齐在“作怪”。简单来说,每个成员在结构体中的起始地址,必须是其自身类型大小的整数倍。StructA 中,char a 占1字节,接着是 int b(假设4字节对齐),它不能紧挨着 a 放在地址1开始,因为1不是4的整数倍。所以编译器会在 a 后面插入3字节的“填充”,让 b 从地址4开始。b 之后是 short c(2字节对齐),地址8是2的倍数,可以存放。最后,整个结构体的大小必须是其最宽基本成员对齐值的整数倍(这里最宽是 int,4字节),所以总大小12字节。
StructB 调整了顺序,int b 放在开头,自然对齐。char a 放在地址4,short c 需要2字节对齐,地址5不行,所以在 a 后填充1字节,让 c 从地址6开始。最终 b(4) + a(1) + 填充(1) + c(2) = 8字节,正好是4的倍数。调整成员顺序可以节省内存,这在定义包含大量实例的结构体(如网络数据包、文件头)时非常有用。
对于联合体 union,sizeof 返回的是其最大成员的大小,因为所有成员共享同一块内存。理解结构体和联合体的 sizeof,是进行底层内存操作、网络编程、硬件交互的基础。
3. 避坑指南:sizeof 使用中的常见陷阱与最佳实践
用了这么多年 sizeof,我总结了一些容易出错的地方和对应的处理经验。
陷阱一:在函数参数中误用 sizeof 计算数组大小。 前面已经强调过,数组作为参数会退化为指针。解决方案有两种:一是显式传递数组长度作为另一个参数;二是如果数组长度是编译时常量,可以使用宏或全局常量来定义。
// 好方法:传递长度
void process_array(int *arr, size_t count) {
for (size_t i = 0; i < count; i++) {
// 处理 arr[i]
}
}
// 调用
int my_array[100];
process_array(my_array, sizeof(my_array)/sizeof(my_array[0]));
陷阱二:对 malloc 返回的指针使用 sizeof。
这是一个经典错误。sizeof(ptr) 量的是指针变量本身的大小,而不是它指向的动态分配内存块的大小。
int *dynamic_array = (int*)malloc(100 * sizeof(int));
size_t wrong_size = sizeof(dynamic_array); // 错误!得到的是 sizeof(int*),比如8
size_t correct_element_count = 100; // 这个信息你必须自己记住!
动态分配的内存大小,程序员必须自己管理,sizeof 帮不了你。
陷阱三:忽略括号导致的优先级问题。
虽然 sizeof 是运算符,但它的优先级比较高。不过,在涉及类型转换等复杂表达式时,省略括号可能导致意外结果。为了安全,我强烈建议始终使用括号。
int *p;
size_t s1 = sizeof *p; // 正确,等价于 sizeof(*p),即 sizeof(int)
size_t s2 = sizeof(int)*p; // 危险!这可能是 (sizeof(int)) * p,把指针当整数乘了!
size_t s3 = sizeof((int)*p); // 加上括号,意图明确
陷阱四:在跨平台代码中硬编码 sizeof 结果。
永远不要写 if (sizeof(int) == 4) 这样的代码来做功能判断。如果你需要特定大小的类型,请使用 <stdint.h>。如果你需要根据类型大小来执行不同的代码路径,使用预处理指令 #if 或静态断言 static_assert(C11)。
#include <stdint.h>
#include <stdio.h>
// 使用标准类型
int32_t guaranteed_32bit_int;
uint64_t guaranteed_64bit_uint;
// 静态断言(C11)
#include <assert.h>
static_assert(sizeof(long) >= 4, "long must be at least 4 bytes on this platform.");
最佳实践:
- 统一使用括号:无论操作数是类型还是变量,都使用
sizeof(...),提高可读性。 - 与
malloc/calloc搭配使用:分配内存时,用sizeof计算单个元素大小,避免硬编码。int *arr = malloc(n * sizeof(*arr));这种写法很好,即使以后arr的类型从int改成long,这行代码也无需修改。 - 使用
size_t类型接收结果:声明变量来存储sizeof的结果时,使用size_t类型,这是最匹配的。 - 利用
sizeof进行代码自描述:像sizeof(array)/sizeof(array[0])这样的写法,本身就清晰地表达了“计算数组长度”的意图,比单独定义一个ARRAY_LEN常量有时更直观(对于栈数组)。
说到底,sizeof 是C语言赋予我们的一把窥探内存布局的尺子。用得熟,能写出更高效、更健壮的代码;用不好,就会引入隐秘的bug。多写、多试、多思考它在不同上下文中的行为,这把尺子就会成为你手中得心应手的工具。我至今还记得第一次用 sizeof 发现结构体实际大小和想象中不一样时的那种惊讶,以及后来利用对齐规则优化数据结构带来的性能提升。这些实战中的点滴,才是真正理解一个特性的关键。

323

被折叠的 条评论
为什么被折叠?



