两根安全带。空指针(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,重跑——编译直接失败。"万能口袋"必须先说明装的是什么,才能掏出来用。
五、常见误区
- "
NULL和0一样"——NULL本质上是 0,但语义完全不同。写int *p = 0;能编过,但可读性差;C++11 之后统一写nullptr。 - "空指针可以解引用,大不了是 0"——会崩溃(不是返回 0)。空指针解引用是典型的运行时灾难,必须靠
if提前拦住。 - "
void *是万能指针,什么都能干"——能做的是"当容器"(装各种地址);不能做的是"直接使用"(解引用、加减)。 - "
void *可以直接赋给int *"——C++ 里必须显式转换:int *ip = (int *)vp;。C 语言允许隐式,C++ 收紧了这个口子。 - "指针用完了不用管"——用完置空是好习惯。尤其是指向动态内存的指针(后续章节),不置空很容易造成"悬空指针"事故。
自测三问(点开对答案)
1. 为什么用空指针之前要先判空?——因为解引用空指针会**直接崩溃**,判空能提前挡住。 2. `void *p` 能不能写 `*p`?为什么?——不能。`void` 没有类型信息,编译器不知道读几个字节、按什么格式解释。 3. `void *` 里存的地址想用起来要做什么?——**强制转换回具体类型**,如 `int *ip = (int *)vp;`。- ☐ 7.4.5 指向常量的指针与指针常量(下一节)
相关笔记
- 变量与指针 —— 野指针的由来
- 指向常量的指针与指针常量 —— 下一节:另一根安全带
- 从函数中返回指针 —— 悬空指针与判空
- 变量的声明及赋值 —— 未初始化=垃圾值
- 🔷 C++基础 · 索引 —— 全课路线图