在使用指针之前,我们通常会用 p != nullptr 来检查它是否为空。那么只要加上这句判断,是不是就可以放心地执行 *p 了呢?实际上并非如此。
赋值为 nullptr 的空指针状态明确可控,通过 != nullptr 就能安全避开风险。但在 C++ 开发中,还有两种连 != nullptr 也检查不出来、极易导致程序崩溃的失效指针:一种是 “野指针”,另一种是 “悬空指针”。
C++ 野指针
所谓的 “野指针(Wild Pointer)”,并不是 C++ 官方标准里的语法关键字,而是开发者用来形容 “从一开始就没有可靠指向” 的指针的俗称。
最典型的野指针,就是在函数内部声明了一个局部指针变量,却没有给它赋初值。声明普通整型变量 int a; 时如果不赋初值,里面装的是随机的垃圾值。同样地,未初始化的局部指针里面装的也是一个随机的 “垃圾内存地址”。下面这种写法就会产生野指针。
int* p; // 未初始化的野指针
std::cout << *p << '\n'; // 访问未知内存这里小伙伴们要特别注意:面对未初始化的野指针,写 if (p != nullptr) 是起不到任何保护作用的。
因为野指针 p 里面本来就是一串非零的随机垃圾地址,并不等于 nullptr。此时 p != nullptr 条件依然成立,程序还是会进入 if 分支去读写那个非法地址,最终导致运行异常或直接崩溃。
要想避开野指针,做法其实非常简单:我们在定义局部指针变量的同时,直接完成初始化即可。
示例 1:定义指针时同步初始化
#include <iostream>
int main() {
int* p1 = nullptr; // 暂无目标时设为 nullptr
int age = 20;
int* p2 = &age; // 已有目标时指向变量
if (p1 == nullptr) {
std::cout << "p1 当前是空指针" << '\n';
}
if (p2 != nullptr) {
std::cout << "*p2 = " << *p2 << '\n';
}
return 0;
}运行结果如下。
p1 当前是空指针
*p2 = 20分析:
在这个例子中,p1 和 p2 都在定义的同时赋予了明确的值。暂时没有目标变量时直接初始化为 nullptr,已有目标变量 age 时直接初始化为 &age。只有让每一个指针在定义时就处于明确状态,后面的非空检查才真正管用。
注意 平时编写 C++ 代码时,我们要养成 “定义局部指针即初始化” 的习惯。如果定义时暂时还没有明确的目标变量,一定要先写成 int* p = nullptr,不要留下悬空未赋值的局部指针。
C++ 悬空指针
悬空指针(Dangling Pointer)也常叫做 “迷途指针”。它和野指针的区别在于:悬空指针在一开始确实指向过一个真实有效的变量。只是后来那个变量的生命周期结束了,内存已被系统收回,而指针里依然保存着已经作废的旧地址。
这就好比朋友原本租住在某个房间,后来退租搬走了,房间已被房东收回,但我们手里还记着旧门牌号。如果再顺着这个旧地址找过去,就会出问题。

在日常编写基础代码时,初学者最容易在以下 2 种场景中写出悬空指针。
1. 局部代码块变量销毁
在函数或 if、for 等花括号代码块内部定义的局部变量,一旦程序执行离开所在的花括号,就会自动销毁,占用的内存也会被系统收回。如果外层的指针还保存着它的旧地址,这个指针就会变成悬空指针。
示例 2:局部变量销毁后及时置空
#include <iostream>
int main() {
int* p = nullptr;
int age = 20;
if (age >= 18) {
int bonus = 100; // if 块内的局部变量
p = &bonus;
std::cout << "if 块内,*p = " << *p << '\n';
}
p = nullptr; // bonus 销毁后及时置空
if (p == nullptr) {
std::cout << "已将失效指针重置为 nullptr" << '\n';
}
return 0;
}运行结果如下。
if 块内,*p = 100
已将失效指针重置为 nullptr分析:
在 if (age >= 18) 的花括号内部,局部变量 bonus 被创建,指针 p 保存了 bonus 的地址,此时通过 *p 可以正常读取到 100。
当程序执行完 if 代码块的右花括号 } 之后,局部变量 bonus 就会被销毁。需要特别注意的是,变量销毁时 C++ 不会自动把指向它的指针改成 nullptr。刚走出右花括号时,p 里面依然记着 bonus 的旧地址,p != nullptr 仍然成立,此时的 p 就是一个悬空指针。
因此,离开 if 代码块后千万不能再去执行 *p,而应该立即执行 p = nullptr 手动置空。更稳妥的做法是,平时尽量不要使用外层指针去保存内层局部块变量的地址。
2. 函数返回局部变量的地址
另一种更隐蔽的踩坑场景,是在函数中把局部变量的地址作为函数返回值传给外部调用者。我们可以对比一下错误写法与正确写法。
// 错误方式
int* getNumber() {
int num = 100;
return #
}
// 正确方式
int getNumber() {
int num = 100;
return num;
}在上面的错误方式中,num 是 getNumber() 内部的局部变量。一旦函数执行结束,num 占用的内存就会被立即回收。
如果我们把 &num 返回出去,外部调用者拿到的就是一个指向已销毁内存的悬空指针,一旦解引用就会引发不可控的错误。同理,函数也不能返回局部变量的引用。如果我们只是想把计算结果传出函数,像上面的正确方式那样直接按值返回 num 即可。
三种指针状态对比
结合前面学习的空指针,这 3 种特殊指针状态的产生原因与处理方式如下表所示。
| 指针状态 | 产生原因 | != nullptr 能否识别 | 正确处理方式 |
|---|---|---|---|
| 空指针 | 显式赋值为 nullptr | 可以 | 解引用前先判断 p != nullptr |
| 野指针 | 定义局部指针时未初始化 | 不可以 | 定义指针时同步完成初始化 |
| 悬空指针 | 所指变量的生命周期已结束 | 不可以 | 变量销毁后及时置空,不返回局部变量地址 |
从上表可以看出,!= nullptr 只能识别状态明确的空指针。大家平时编写指针代码时,只要同时做到 “定义时必初始化” 和 “目标销毁后及时置空”,就能避开野指针与悬空指针带来的崩溃风险。
