⚡C++ 语言基础

指针、引用与 nullptr

难度:⭐ | 高频指数:🔥🔥🔥

面试回答

常见问法

  • 指针和引用有什么区别?
  • 引用的底层实现是什么?
  • 为什么 C++11 引入了 nullptr?它和 NULL0 有什么区别?
  • 引用能不能为空?能不能重新绑定?
  • 什么时候用指针,什么时候用引用?
  • 悬空指针和野指针是什么?怎么避免?

回答

指针和引用都能间接访问另一个对象,但它们的语义和约束完全不同:

int x = 42;
int* p = &x;   // 指针:存储 x 的地址
int& r = x;    // 引用:x 的别名

核心区别:

指针引用
是否可为空可以(nullptr不可以,必须绑定有效对象
是否可重新绑定可以指向别的对象不可以,一旦绑定终身不变
是否需要解引用需要 *p不需要,直接用
是否有自己的地址有,指针本身是个变量通常没有独立存储(编译器可能优化掉)
sizeof指针大小(通常 4/8 字节)等于所引用对象的大小
能否做算术可以(指针运算)不可以

关于 nullptr:C++11 引入 nullptr 替代 NULL0,因为 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* pint *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

nullptrNULL0
类型std::nullptr_t通常是 (void*)00int
能否隐式转为指针
能否隐式转为整数✅(因为本质是整数)
重载安全
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 pointerdangling 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 指针)
  • NULL0 代替 nullptr,导致重载歧义
  • 返回局部变量的引用或指针(悬空)
  • 以为 sizeof(引用) 返回的是引用本身的大小(实际返回的是被引用对象的大小)
  • 以为”引用就是指针的语法糖”——语义上它们是不同的抽象层次

记忆技巧

  • 指针 = 可以为空、可以换对象、需要解引用
  • 引用 = 不能为空、不能换、直接用
  • nullptr = 类型安全的空指针,替代 NULL
  • 选择口诀:非空用引用,可空用指针,初始化用 nullptr

面试速答版

指针和引用都能间接访问对象,但指针可以为空、可以重新指向别的对象、需要解引用;引用必须绑定有效对象、不能重新绑定、语法上直接当原对象用。

选择上,如果参数一定存在就用引用,如果可能为空就用指针。nullptr 是 C++11 引入的类型安全空指针字面量,替代了 NULL0,避免了函数重载时的歧义问题。


面试加分版

指针和引用虽然都能间接访问对象,但它们表达的语义不同。指针是一个独立变量,存储地址,可以为空、可以重新赋值、可以做算术运算;引用是已有对象的别名,必须在声明时绑定,之后不能改变,也不能为空。

从底层看,引用通常被编译器实现为一个不可变指针,但从语言语义上它不是对象,没有自己的地址。所以 sizeof 一个引用得到的是被引用对象的大小,不是”引用本身”的大小。

选择上我遵循一个原则:能保证非空且不需要重新绑定就用引用,需要表达”可能没有对象”或需要重新指向就用指针。 比如函数参数必传且非空用引用,可选参数用指针。

关于 nullptr,它是 C++11 引入的,类型是 std::nullptr_t,只能隐式转换为指针类型,不能转换为整数。这解决了 NULL(本质是 0)在函数重载时可能匹配到整数参数的歧义问题。现代 C++ 里应该统一用 nullptr,不再用 NULL 或裸 0

Related · 语言基础