🔷 第七章 数组、指针和引用
📄 一句话概括

两根安全带。空指针(NULL / nullptr)表示"这个指针现在不指向任何地方",用来表达"还没接上"或"失败了";空类型指针 void * 是"万能口袋",能装任何类型的地址,但不能直接解引用——因为编译器不知道它指的是什么类型、该读几个字节,得先转换回具体类型再用。

📝 本节和前面几节的关系

7.4.1 提醒过"未初始化的指针是野指针,极其危险"。这一节给出两个标准答案:① 暂时没目标就明确写成"空"(不是留垃圾值);② 需要装"什么类型都可能"的地址时用 void *。它还回用了 2.3.2 的"未初始化=垃圾值"、7.4.3 的"步长由类型决定"(void * 不能做加减,正因为没有类型就没有步长)。

一、空指针:明确地"不指向任何地方"

int *p = NULL;        // C 风格写法
int *p = nullptr;     // C++11 推荐写法

两者都表示"空":指针里存的是一个特殊值,不指向任何有效内存。nullptr 是 C++11 引进的关键字(回看 1.1.1 的版本演进),比 NULL 更安全、更推荐——NULL 本质上就是整数 0(来自 <cstddef>),在某些重载场景下会被当成"整数"而不是"指针"。

三条使用规矩

规矩 1:空指针不能解引用

int *p = nullptr;
cout << *p;           // ❌ 程序崩溃(段错误 / access violation)

因为"顺着一个空门牌号找上门",当然什么都找不到。

规矩 2:用之前先判空(最重要)

if (p != nullptr) {       // 或写成 if (p)
    cout << *p;
} else {
    cout << "指针为空,不能取值";
}

规矩 3:用完置空

int *p = &a;
// ... 用了一阵 ...
p = nullptr;              // 明确表示"我不再指向 a 了"

这条是防止"悬空指针"(指向已经消失的变量)的好习惯——7.6.3 讲"从函数返回指针"时会看到它的重要性。

二、为什么需要空指针

场景 空指针的作用
声明了暂时还不知道指向谁 int *p = nullptr; 比留垃圾值安全一万倍
函数返回指针表示"失败" 找到了就返回地址,没找到返回 nullptr(调用方一判就知道)
标记"到此为止" 链表、树的最后一个节点后面接一个空指针
表示"这个资源已释放" 释放之后立刻置空,避免误用

三、空类型指针 void *:万能口袋

void *p;              // 能装"任何类型的地址"

它是"类型擦除"的指针——不记自己指向什么类型:

int    a = 10;
double b = 3.14;

void *p = &a;         // ✅ 装 int 的地址
p = &b;               // ✅ 再装 double 的地址,随便换

但代价是不能直接用:

能做 不能做
存任何类型的地址 不能直接解引用:*p ❌
赋给别的 void *、比较是否为空 不能做指针运算:p + 1 ❌(不知道步长)
转换回具体类型后用 不能直接赋给 int *(C++ 要求显式转换)

为什么不能直接解引用?因为编译器要靠类型回答两个问题:从这地址读几个字节?按什么格式解释? void 什么都没说,所以必须先"告诉它":

void *p = &a;
int *ip = (int *)p;         // 转换回 int*("这地址里装的是 int")
cout << *ip << endl;        // ✅ 现在可以取值了

真实用途:标准库里的 malloc 返回的就是 void *——它只负责给一块内存,"这块内存当什么用"由你自己说。

四、实例08:空类型指针的使用

#include <iostream>
using namespace std;

int main() {
    int    a = 100;
    double b = 3.14;

    // ① 空指针:判空再用
    int *p = nullptr;
    cout << "p 是空的吗?" << (p == nullptr ? "是" : "不是") << endl;   // 是

    p = &a;                                    // 接上目标
    if (p != nullptr) cout << "接上后 *p = " << *p << endl;           // 100

    // ② 空类型指针:装什么类型的地址都行
    void *vp;
    vp = &a;
    cout << "vp 装 int 的地址,转回来 = " << *(int *)vp << endl;      // 100

    vp = &b;
    cout << "vp 装 double 的地址,转回来 = " << *(double *)vp << endl; // 3.14

    // cout << *vp << endl;      // ❌ 编译错误:void* 不能直接解引用
    // cout << vp + 1 << endl;   // ❌ 编译错误:void* 不能做指针运算
    return 0;
}

看到什么算成功:p 是空的吗?是、接上后 *p = 100、vp 装 int 的地址,转回来 = 100、vp 装 double 的地址,转回来 = 3.14。

最值钱的是被注释掉的两行:把它们打开试试,编译器会明确报错——这就是"空类型指针不能直接用"的现场证据。编译器在替你挡刀。

30 秒实验:把 *(int *)vp 里的 (int *) 去掉,写成 *vp,重跑——编译直接失败。"万能口袋"必须先说明装的是什么,才能掏出来用。

五、常见误区

  1. "NULL 和 0 一样"——NULL 本质上是 0,但语义完全不同。写 int *p = 0; 能编过,但可读性差;C++11 之后统一写 nullptr。
  2. "空指针可以解引用,大不了是 0"——会崩溃(不是返回 0)。空指针解引用是典型的运行时灾难,必须靠 if 提前拦住。
  3. "void * 是万能指针,什么都能干"——能做的是"当容器"(装各种地址);不能做的是"直接使用"(解引用、加减)。
  4. "void * 可以直接赋给 int *"——C++ 里必须显式转换:int *ip = (int *)vp;。C 语言允许隐式,C++ 收紧了这个口子。
  5. "指针用完了不用管"——用完置空是好习惯。尤其是指向动态内存的指针(后续章节),不置空很容易造成"悬空指针"事故。
自测三问(点开对答案) 1. 为什么用空指针之前要先判空?——因为解引用空指针会**直接崩溃**,判空能提前挡住。 2. `void *p` 能不能写 `*p`?为什么?——不能。`void` 没有类型信息,编译器不知道读几个字节、按什么格式解释。 3. `void *` 里存的地址想用起来要做什么?——**强制转换回具体类型**,如 `int *ip = (int *)vp;`。

相关笔记