37、空指针:NULL与nullptr的区别及解引用风险
空指针,这玩意儿是C语言里最经典的坑之一。我入行那会儿,第一次因为解引用空指针导致程序崩溃,debug了一整个下午。后来发现,其实就是少了个判空。嗯,从那以后,我对空指针就格外敏感。
今天咱们聊聊两个东西:NULL 和 nullptr。很多人觉得它们差不多,其实差别不小。再加上解引用空指针的风险,这几乎是每个嵌入式工程师都会遇到的噩梦。
NULL 是什么?
NULL 在C语言里,其实就是一个宏定义。通常是这样:
#define NULL ((void*)0)
说白了,它就是 0 的一个别名。只不过编译器把它当作指针来看待。但这里有个问题——NULL 的类型是 void*,不是具体的指针类型。这在C++里会引发一些麻烦。
举个例子:
void func(int* p);
void func(int n);
func(NULL); // 到底调用哪个?
在C++里,NULL 会被当作整数 0 来处理,于是编译器会优先调用 func(int)。这显然不是我们想要的。我在项目中遇到过这种重载歧义,排查起来特别头疼。
nullptr 的诞生
nullptr 是C++11引入的关键字。它的类型是 std::nullptr_t,可以隐式转换为任何指针类型,但不会转换为整数。这就解决了上面的歧义问题。
func(nullptr); // 明确调用 func(int*)
我个人习惯在C++项目里一律用 nullptr,不再碰 NULL。你想想看,既然语言提供了更安全的工具,为什么不用呢?
C语言里怎么办?
如果你还在写纯C代码,那只能用 NULL。不过别担心,C语言里没有函数重载,所以 NULL 的歧义问题基本不存在。但要注意一点:NULL 和 0 在C里是等价的,所以有些编译器可能会给出警告。
我建议在C代码里,指针初始化统一用 NULL,不要用 0。这样代码可读性更好,别人一看就知道这是个指针。
解引用空指针的风险
这才是重点。解引用空指针,说白了就是访问地址为 0 的内存。操作系统通常不允许这么做,于是程序就会崩溃——段错误(Segmentation Fault)。
为什么会这样?因为地址 0 被操作系统保留,用来捕获空指针错误。你一旦访问,内核就会发信号干掉你的进程。
在嵌入式环境里,情况更复杂。有些单片机没有MMU,地址 0 可能是合法的内存区域。这时候解引用空指针不会崩溃,但会读写到不该碰的地方,导致各种诡异的问题。我曾经在一个ARM Cortex-M3的项目里遇到过,空指针赋值后系统没死,但某个全局变量莫名其妙变了。查了两天才发现是空指针的锅。
如何避免空指针解引用?
方法其实很简单,就两条:
- 初始化指针时赋空:
int* p = NULL;或int* p = nullptr; - 使用前判空:
if (p != NULL) { *p = 42; }
听起来像废话对吧?但我在代码审查里,几乎每次都能看到有人忘了判空。尤其是函数参数,传进来的指针是不是空,你根本不知道。
void set_value(int* p, int val) {
if (p == NULL) {
return; // 或者打印错误日志
}
*p = val;
}
嗯,这里要注意:判空之后,要么返回错误码,要么直接 return。千万别继续往下走。
NULL 与 nullptr 的对比
| 特性 | NULL | nullptr |
|---|---|---|
| 语言 | C / C++ | C++11 及以上 |
| 本质 | 宏,通常为 ((void*)0) 或 0 | 关键字,类型为 std::nullptr_t |
| 类型安全 | 差,可被当作整数 | 好,只能转为指针 |
| 重载场景 | 可能引发歧义 | 无歧义 |
| 推荐用法 | C 代码 | C++ 代码 |
知识结构图
下面这张图帮你理清空指针的核心知识点:
避坑指南
我曾经在一个通信协议栈里,因为没判空,导致野指针覆盖了关键配置数据。设备运行了三天才出问题,排查起来简直要命。从那以后,我给自己定了几条铁律:
- 所有指针变量定义时,必须初始化(NULL 或 nullptr)。
- 所有函数参数如果是指针,必须判空(除非文档明确说不会传空)。
- 释放内存后,立即把指针置空:
free(p); p = NULL; - 代码审查时,空指针相关逻辑必须逐行过。
说白了,空指针问题不是技术难题,而是习惯问题。你只要养成每次用指针前都问自己一句“它会不会是空?”的习惯,就能避开90%的坑。
嗯,今天就聊到这儿。记住:NULL 和 nullptr 的区别要搞清楚,但更重要的是——别解引用空指针。