C++ 野指针与悬空指针

在使用指针之前,我们通常会用 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 &num;
}

// 正确方式
int getNumber() {
    int num = 100;
    return num;
}

在上面的错误方式中,num 是 getNumber() 内部的局部变量。一旦函数执行结束,num 占用的内存就会被立即回收。

如果我们把 &num 返回出去,外部调用者拿到的就是一个指向已销毁内存的悬空指针,一旦解引用就会引发不可控的错误。同理,函数也不能返回局部变量的引用。如果我们只是想把计算结果传出函数,像上面的正确方式那样直接按值返回 num 即可。

三种指针状态对比

结合前面学习的空指针,这 3 种特殊指针状态的产生原因与处理方式如下表所示。

三种特殊指针状态
指针状态 产生原因 != nullptr 能否识别 正确处理方式
空指针 显式赋值为 nullptr 可以 解引用前先判断 p != nullptr
野指针 定义局部指针时未初始化 不可以 定义指针时同步完成初始化
悬空指针 所指变量的生命周期已结束 不可以 变量销毁后及时置空,不返回局部变量地址

从上表可以看出,!= nullptr 只能识别状态明确的空指针。大家平时编写指针代码时,只要同时做到 “定义时必初始化” 和 “目标销毁后及时置空”,就能避开野指针与悬空指针带来的崩溃风险。

给站长反馈

绿叶网正在不断完善中,小伙伴们如果发现任何问题,还望多多给站长反馈,谢谢!

邮箱:lvyenet@vip.qq.com

「绿叶网」服务号
绿叶网服务号放大
关注服务号,微信也能看教程。
绿叶网服务号