楔子
不知道大家编码时有没有遇到过这样的情况:在一个函数中定义了一个局部的结构体变量,这个结构体里包含了很多不同类型的成员,导致在初始化该结构体成员时,每个成员都要赋对应的初值。就像下面这样:
c 代码:struct { char a; // 字符 int b; // 整型 int* c; // 指针 int d[3]; // 数组 } str_value = {.a = '\0', .b = 0, .c = NULL, .d = {0,0,0}};
“有考虑过直接使用
={0}
来对结构体变量赋初始值,但又害怕产生不可预料的行为。”
有上面 👆 这样的想法是因为 C 语言中的整型变量使用 0 来初始化是显而易见的,就是整数 0,但字符、指针和数组这些看起来与整型数不那么匹配的变量类型,使用 0 真的可行吗?我想这是很多初学者甚至工作多年的老鸟都疑惑的问题。那么要解开这个困惑,我们就先从编译器的行为开始分析。
编译器行为
还是以上面的 str_value
变量为例,我们在定义这个变量时会有两种情况,可能定义为静态变量(全局变量),或是定义为非静态变量(局部变量) 。当定义为静态变量时,往往不需要考虑初始化的问题。
尽管我们知道变量是在定义时就应初始化,否则其值会是内存中的任意“垃圾值”。但我们往往相信编译器与我们多年的“共事”已经心生情愫,绝不会坑我们,它一定会引导所定义的变量自动赋予正确的初始值 😶。
事实也的确如此~ 那编译器在赋值时到底赋了什么呢?
答:当静态变量定义时,编译器会给未显式初始化的成员自动分配 0 值。
其实通过窥探编译器这一行为,基本就认定了开发者在进行初始化时可以使用 ={0}
来操作,这相当于是在模仿编译器的行为。
不同类型处理 0 值
到这里,我们仅仅知道用 0 值初始化行为没有错,但我们还不了解为什么可以用 0 来对不同类型的变量进行初始化。这就涉及到每种类型在处理 0 值时的底层逻辑了。
字符
字符与整数值的转换需要参考 ASCII 码,其中数值 0 对应的字符是 NUL(空字符)。在 C 语言中,没有专门的“空字符”类型,而是直接用整数值 0 或转义字符 '\0'
来表示。所以 0 等于 '\0'
,在 C 语言中 '\0'
就是字符类型的默认初始值,因此同样也可以用 0 来表示。
指针
通常指针的默认初始值为 NULL
(空指针),这个和字符 NUL 很像,但我们不去从这个角度考虑它和 0 值的关系,而是看看 C 语言标准库中是如何定义 NULL
这个宏的。
以 GCC 为例,在 <stddef.h>
这个标准头文件中,有这么一行代码:#define NULL ((void*)0)
,它的含义是『定义一个宏 NULL,它代表地址 0x00』,更直白地说,就是如果我强行把电脑内存地址 0x00 处的值改为 n,那么 NULL
解引用后的值就是 n。因此,代码 str_value.c = NULL;
就相当于 str_value.c = ((void*)0);
。
在机器中,地址 0x00 是绝对的禁区,操作系统会通过内存管理单元(MMU)硬性保护这片区域,任何程序都无权对其进行读写操作。从操作系统内核的角度看,地址 0x00 的值通常是 0。但这其实也不是 0 值与地址 0x00 混用的根本原因,实际上 C 标准为了类型安全和程序员便利而引入的一条特殊规则:An integer constant expression with the value 0, or such an expression cast to type void*,因此,在使用 0 值对指针赋值时,其实会自动转换为 (void*)0
,代码 str_value.c = 0;
等同于 str_value.c = ((void*)0);
等同于 str_value.c = NULL;
。
数组
数组本质上是整型、字符或指针等类型的集合,处理 0 值的原理与单变量处理是没有区别的,只要认真看完上面的内容基本就能理解。
但需要注意的是,数组和结构体初始化时的 ={0}
操作和赋值时 =0
操作是不同的,在 C 语言中进行 ={0} 初始化是一种聚合赋值的行为,会把所有成员都赋 0 值。
int
、字符型 char
还是指针型 int*
,都是可以使用 0 值作为默认初始值的。变量初始化问题延申
- 静态变量在未确定初始化值前,推荐不进行手动初始化(可依赖编译器自动零初始化);
- 非静态局部变量和常量一定要进行显性初始化;
- 数组和结构体在初始化赋 0 值时可以使用
={0}
的方式,而非必须 memset 方式,但这个数组必须是大小确定的。