基于本文回答
0
评论

C++ 中指针和引用的主要区别是什么?

知识点图片

在 C++ 中,指针(Pointer)引用(Reference)都可以用来间接访问内存中的对象,但它们在语法、底层机制以及使用场景上有非常显著的区别。

为了方便记忆,你可以将核心区别总结为一句话:指针是一个实体变量,存储着内存地址;而引用只是一个已经存在的变量的“别名”。

以下是它们的详细区别:

1. 核心区别对照表

特性 指针 (Pointer) 引用 (Reference)
本质 存放某个对象地址的实体变量,有自己的内存空间。 某个已知变量的别名,不占用额外的逻辑内存空间(底层实现通常是常量指针)。
初始化 可以在定义时不初始化(虽不推荐,但合法)。 必须在定义时初始化。
重新赋值 (Reassignment) 可以随时改变指向,指向不同的对象。 一旦绑定到一个对象,就不能再改变为其他对象的引用。
空值 (Nullability) 可以为空(nullptrNULL)。 不能为空。必须总是引用一个有效的对象。
多级使用 支持多级指针(如 int**,指向指针的指针)。 不支持多级引用(没有引用的引用 int& &,不过 C++11 引入了右值引用 &&)。
算术运算 支持指针算术运算(如 p++,移动到下一个内存位置)。 不支持算术运算。对引用进行 ++ 操作,实际上是对它引用的原始变量进行 ++
sizeof 的结果 sizeof(指针) 得到的是指针本身的大小(通常是 4 或 8 字节)。 sizeof(引用) 得到的是被引用对象的大小。
解引用 需要显式使用 * 操作符来解引用(如 *p)。 不需要解引用,直接使用引用名即可(编译器在底层自动处理)。

2. 代码示例对比

指针的行为

cpp
int a = 10;
int b = 20;

int* p;        // 合法:可以不初始化(此时是野指针)
p = &a;        // 指向 a
*p = 15;       // a 的值变成了 15

p = &b;        // 合法:指针可以改变指向,现在指向 b
*p = 25;       // b 的值变成了 25

p = nullptr;   // 合法:指针可以为空

引用的行为

cpp
int a = 10;
int b = 20;

// int& r;     // 错误:引用必须初始化
int& r = a;    // 合法:r 是 a 的别名
r = 15;        // a 的值变成了 15

r = b;         // 注意!这不是改变引用的绑定,而是把 b 的值(20)赋值给 r 所引用的对象(a)
               // 此时 a 的值变成了 20,r 依然是 a 的别名。

3. 深层细节区别

1. 内存占用

  • 指针本身是一个变量,在栈(或堆)上确确实实占用 4 个字节(32位系统)或 8 个字节(64位系统)。
  • 引用在 C++ 语法层面上被描述为别名,没有自己的地址(你对引用取地址 &r,得到的是原始变量 a 的地址)。但在编译器底层,引用通常是通过**指针(常量指针 Type* const)**来实现的,因此在底层它实际也会占用内存,只是 C++ 语言层面对程序员隐藏了这一点。

2. 常量(Const)修饰符

指针和 const 的结合有多种形式,容易混淆;而引用比较简单:

  • 指针:
    • const int* p (或 int const* p):底层 const,指针指向的值不能通过指针修改,但指针的指向可以变。
    • int* const p:顶层 const,指针的指向不能变,但指向的值可以修改。
    • const int* const p:指向和值都不能变。
  • 引用:
    • const int& r:常引用,不能通过引用修改原始变量的值。(没有 int& const r 这种写法,因为引用本身就天生自带“指向不能变”的属性)。

4. 最佳实践:什么时候用哪个?

在 C++ 社区中有一个著名的经验法则:“能用引用时尽量用引用,必须用指针时才用指针。”

推荐使用【引用】的场景:

  1. 函数参数传递: 为了避免对象拷贝带来的性能损耗(尤其是 std::string, std::vector 等大对象),通常使用常引用 const T&
  2. 函数返回值: 比如重载运算符 operator=operator[] 时,返回引用以支持链式操作或修改容器内部元素。
  3. 确定对象一定存在: 如果你需要传递一个变量,并且这个变量在逻辑上绝对不可能为空,用引用可以省去判空的麻烦和开销。

必须使用【指针】的场景:

  1. 需要表示“可选”或“空(Null)”语义: 如果一个函数参数是可选的,传入 nullptr 代表没有传入该参数,此时必须用指针。
  2. 动态内存管理: 使用 new 分配在堆上的内存,必须使用指针来接收(如现代 C++ 中的智能指针 std::unique_ptr, std::shared_ptr)。
  3. 底层数据结构实现: 在实现链表、树、图等数据结构时,节点之间的连接需要改变指向,必须使用指针。
  4. C 语言接口兼容: 当你需要调用 C 语言编写的库(API)时,C 语言没有引用,只能传递指针。
  5. 数组操作和指针算术: 遍历 C 风格数组等需要指针偏移的场景。
右滑查看面试常问