指针、引用与 nullptr
难度:⭐ | 高频指数:🔥🔥🔥
面试回答
常见问法
- 指针和引用有什么区别?
- 引用的底层实现是什么?
- 为什么 C++11 引入了
nullptr?它和NULL、0有什么区别? - 引用能不能为空?能不能重新绑定?
- 什么时候用指针,什么时候用引用?
- 悬空指针和野指针是什么?怎么避免?
回答
指针和引用都能间接访问另一个对象,但它们的语义和约束完全不同:
int x = 42;
int* p = &x; // 指针:存储 x 的地址
int& r = x; // 引用:x 的别名
核心区别:
| 指针 | 引用 | |
|---|---|---|
| 是否可为空 | 可以(nullptr) | 不可以,必须绑定有效对象 |
| 是否可重新绑定 | 可以指向别的对象 | 不可以,一旦绑定终身不变 |
| 是否需要解引用 | 需要 *p | 不需要,直接用 |
| 是否有自己的地址 | 有,指针本身是个变量 | 通常没有独立存储(编译器可能优化掉) |
| sizeof | 指针大小(通常 4/8 字节) | 等于所引用对象的大小 |
| 能否做算术 | 可以(指针运算) | 不可以 |
关于 nullptr:C++11 引入 nullptr 替代 NULL 和 0,因为 NULL 本质上是整数 0,在函数重载时会产生歧义:
void f(int);
void f(int*);
f(NULL); // 歧义!NULL 是 0,可能匹配 f(int)
f(nullptr); // 明确匹配 f(int*)
nullptr 的类型是 std::nullptr_t,只能隐式转换为任意指针类型,不能转换为整数。
追问
1. 引用底层是怎么实现的?
大多数编译器把引用实现为一个不可变的指针(即 T* const),但这是实现细节,不是语言标准的保证。从语言层面看,引用不是对象,没有自己的地址和大小。
2. 有没有”空引用”?
语言标准不允许空引用。如果你强行写出来:
int* p = nullptr;
int& r = *p; // 未定义行为!
这是 UB(未定义行为),编译器不保证任何结果。所以”引用一定非空”是接口契约,不是运行时检查。
3. 悬空指针 / 悬空引用是什么?
指向已经被释放或超出作用域的对象:
int* dangling_ptr() {
int x = 10;
return &x; // x 离开作用域后被销毁,指针悬空
}
int& dangling_ref() {
int x = 10;
return x; // 同理,引用悬空
}
避免方式:
- 不返回局部变量的地址/引用
- 用智能指针管理堆对象生命周期
delete后立即置nullptr(指针场景)
4. 什么时候用指针,什么时候用引用?
一个简单准则:
能保证非空且不需要重新绑定 → 引用;需要表达”可能没有对象”或需要重新指向 → 指针。
具体来说:
- 函数参数必传且非空 → 引用
- 参数可选 / 可能为空 → 指针
- 需要多态 + 可空 → 指针(或智能指针)
- 数据结构中需要重新指向 → 指针
原理展开
1. 指针的本质
指针是一个变量,存储的是另一个对象的内存地址:
int x = 42;
int* p = &x;
// p 的值是 x 的地址,比如 0x7ffd1234
// *p 解引用得到 x 的值:42
指针本身也占内存(32 位系统 4 字节,64 位系统 8 字节),可以被取地址:
int** pp = &p; // 指向指针的指针
指针可以做的事:
- 为空(
nullptr) - 重新赋值指向别的对象
- 做算术运算(
p + 1移动到下一个元素) - 与整数比较或转换(不推荐)
声明风格的坑
int* p 和 int *p 都合法,现代 C++ 主流用 int* p。但多变量声明时有个经典陷阱:
int* a, b; // ⚠️ a 是 int*,b 只是 int!不是两个指针!
int *a, *b; // 两个都是 int*
因为 * 是跟变量名绑定的,不是跟类型绑定的。所以最安全的做法是一行只声明一个变量:
int* a;
int* b;
2. 引用的本质
引用是已有对象的别名,必须在声明时初始化,之后不能改变绑定:
int x = 10;
int& r = x; // r 就是 x 的另一个名字
r = 20; // 等价于 x = 20
int y = 30;
r = y; // 这不是重新绑定!这是把 y 的值赋给 x
引用的关键约束:
- 必须初始化:
int& r;编译错误 - 不能重新绑定:一旦绑定,终身指向同一个对象
- 不能为空:没有”空引用”的合法状态
- 没有”引用的引用”:
int&&是右值引用,不是引用的引用
3. nullptr vs NULL vs 0
nullptr | NULL | 0 | |
|---|---|---|---|
| 类型 | std::nullptr_t | 通常是 (void*)0 或 0 | int |
| 能否隐式转为指针 | ✅ | ✅ | ✅ |
| 能否隐式转为整数 | ❌ | ✅(因为本质是整数) | ✅ |
| 重载安全 | ✅ | ❌ | ❌ |
| C++11 起推荐 | ✅ | 不推荐 | 不推荐 |
void f(int);
void f(char*);
f(0); // 调用 f(int)
f(NULL); // 可能调用 f(int),取决于 NULL 的定义,有歧义
f(nullptr); // 明确调用 f(char*)
4. 指针的常见陷阱
野指针(未初始化)
int* p; // 未初始化,指向随机地址
*p = 10; // 未定义行为
风险:往一个随机内存地址写数据,可能覆盖其他变量、破坏栈帧,轻则数据错乱,重则程序崩溃(段错误)。而且这类 bug 往往不会立刻暴露,可能在很远的地方才表现出来,极难排查。
解决:声明时初始化为 nullptr。
悬空指针(对象已销毁)
int* p = new int(42);
delete p;
*p = 10; // 未定义行为,p 指向已释放内存
风险:那块内存可能已经被分配给别的对象了,写入会破坏别人的数据;也可能触发崩溃。更隐蔽的情况是”偶尔能跑通”,因为内存还没被复用,但换个环境就炸。
解决:delete 后置 nullptr,或用智能指针。
记忆区分:野 vs 悬空
| 野指针 | 悬空指针 | |
|---|---|---|
| 原因 | 从没指向过有效对象 | 曾经指向有效对象,但对象没了 |
| 时机 | 声明时 | 释放后 |
| 英文 | wild pointer | dangling pointer |
| 比喻 | 手里拿着空白纸条,瞎跑到随机地方 | 手里拿着过期地址,房子已经拆了还去敲门 |
口诀:野 = 从没有过,悬 = 曾经有过。
内存泄漏(忘记释放)
int* p = new int(42);
p = new int(100); // 原来的 42 泄漏了
风险:原来 new int(42) 分配的那块内存,没有任何指针指向它了,永远无法被 delete。这块内存在程序运行期间一直被占着却用不了。如果反复发生,内存占用会持续增长,最终可能导致程序被系统杀掉(OOM)。
解决:用智能指针自动管理。
5. const 与指针/引用的组合
int x = 10;
const int* p1 = &x; // 指向 const int 的指针(不能通过 p1 改 x)
int* const p2 = &x; // const 指针(p2 不能指向别处)
const int* const p3 = &x; // 都不能改
const int& r = x; // const 引用,不能通过 r 修改 x
记忆技巧:const 在 * 左边修饰指向的内容,在 * 右边修饰指针本身。
从这个角度看,引用类似于 T* const(不能重新绑定),但语法上更干净。
6. 引用与指针在函数参数中的选择
详细的传参方式对比见 函数传参方式与适用场景。
这里只强调选择原则:
// 必传、非空、只读 → const 引用
void print(const std::string& s);
// 必传、非空、要修改 → 引用
void reset(Buffer& buf);
// 可能为空 → 指针
void visit(Node* node);
// 可能为空、只读 → const 指针
void maybe_print(const std::string* s);
对比总结
| 概念 | 是什么 | 核心特征 | 典型用途 | 优点 | 缺点/风险 |
|---|---|---|---|---|---|
| 指针 | 存储地址的变量 | 可空、可重绑定、可运算 | 可选参数、动态分配、数据结构 | 灵活 | 需要判空、可能悬空 |
| 引用 | 对象的别名 | 非空、不可重绑定 | 函数参数、返回值 | 安全、语法简洁 | 不能表达”没有对象” |
nullptr | 空指针字面量 | 类型安全、不会匹配整数重载 | 初始化指针、判空 | 类型安全 | 只能用于指针 |
NULL | 宏,通常是 0 | 可能被当成整数 | 兼容旧代码 | 兼容 C | 重载歧义 |
| 野指针 | 未初始化的指针 | 指向随机地址 | — | — | UB |
| 悬空指针 | 指向已释放对象 | 对象已不存在 | — | — | UB |
易错点
- 以为引用可以为空或重新绑定
- 混淆
const int*(指向 const)和int* const(const 指针) - 用
NULL或0代替nullptr,导致重载歧义 - 返回局部变量的引用或指针(悬空)
- 以为
sizeof(引用)返回的是引用本身的大小(实际返回的是被引用对象的大小) - 以为”引用就是指针的语法糖”——语义上它们是不同的抽象层次
记忆技巧
- 指针 = 可以为空、可以换对象、需要解引用
- 引用 = 不能为空、不能换、直接用
nullptr= 类型安全的空指针,替代NULL- 选择口诀:非空用引用,可空用指针,初始化用
nullptr
面试速答版
指针和引用都能间接访问对象,但指针可以为空、可以重新指向别的对象、需要解引用;引用必须绑定有效对象、不能重新绑定、语法上直接当原对象用。
选择上,如果参数一定存在就用引用,如果可能为空就用指针。nullptr 是 C++11 引入的类型安全空指针字面量,替代了 NULL 和 0,避免了函数重载时的歧义问题。
面试加分版
指针和引用虽然都能间接访问对象,但它们表达的语义不同。指针是一个独立变量,存储地址,可以为空、可以重新赋值、可以做算术运算;引用是已有对象的别名,必须在声明时绑定,之后不能改变,也不能为空。
从底层看,引用通常被编译器实现为一个不可变指针,但从语言语义上它不是对象,没有自己的地址。所以 sizeof 一个引用得到的是被引用对象的大小,不是”引用本身”的大小。
选择上我遵循一个原则:能保证非空且不需要重新绑定就用引用,需要表达”可能没有对象”或需要重新指向就用指针。 比如函数参数必传且非空用引用,可选参数用指针。
关于 nullptr,它是 C++11 引入的,类型是 std::nullptr_t,只能隐式转换为指针类型,不能转换为整数。这解决了 NULL(本质是 0)在函数重载时可能匹配到整数参数的歧义问题。现代 C++ 里应该统一用 nullptr,不再用 NULL 或裸 0。