C++ 指针与引用的区别

在 C++ 中,引用和指针都可以用来间接访问或修改一个已经存在的变量,但它们的使用规则与适用场景却有很大不同。

平时编写代码或设计函数时,什么时候应该使用引用,什么时候又应该使用指针呢?只要把两者的底层差异放在一起对比一下,判断标准其实非常清晰。

C++ 指针与引用的区别

引用本质上是原变量的别名,而指针是一个独立存放内存地址的变量。两者最明显的共同点,就是都能在不复制原数据的前提下操作目标变量。

虽然都能读写原变量,但它们在初始化要求、空状态、能否更换目标以及内存占用上有着本质区别,如下表所示。

引用与指针的区别
对比维度 引用(reference) 指针(pointer)
定义时初始化 必须初始化并绑定已有变量 可指向变量,也可设为 nullptr
能否为空 不能为空,必须代表有效变量 可以设为 nullptr 空指针
能否更换目标 一旦绑定,中途不能改绑其他变量 可随时赋新地址,改为指向其他变量
访问目标写法 直接使用引用名 ref 需要使用星号解引用 *p
取地址的结果 &ref 得到原变量的地址 p 保存原变量地址,&p 是指针自身的地址
sizeof 测量结果 原变量类型的大小 指针变量自身的大小(64 位下占 8 字节)

从上表可以看出,访问时的符号差异只是表面现象,更关键的是它们在内存中的存在形式与绑定规则。下面我们通过两个例子来分别验证一下。

示例 1:引用与指针操作同一变量

#include <iostream>

int main() {
    int score = 80;
    int& ref = score;    // 定义引用
    int* p = &score;     // 定义指针

    std::cout << "&score = " << &score << '\n';
    std::cout << "&ref = " << &ref << '\n';
    std::cout << "p = " << p << '\n';
    std::cout << "&p = " << &p << '\n';

    ref = 90;            // 通过引用修改 score
    std::cout << "score = " << score << '\n';

    *p = 100;            // 通过指针修改 score
    std::cout << "score = " << score << '\n';
    return 0;
}

运行结果如下。

&score = 0x7ffdf3a37cc4
&ref = 0x7ffdf3a37cc4
p = 0x7ffdf3a37cc4
&p = 0x7ffdf3a37cc8
score = 90
score = 100

分析:

从运行结果可以看出,&score、&ref 和 p 打印出来的内存地址完全相同,都是 0x7ffdf3a37cc4。而 &p 打印出来的则是另一个独立的地址 0x7ffdf3a37cc8。

这说明引用 ref 只是原变量 score 的别名,本身不会额外开辟新的内存空间。因此,我们对 ref 取地址或使用 sizeof(ref) 测量大小时,拿到的都是 score 自身的地址和大小(4 个字节)。

而指针 p 拥有自己独立的内存空间 &p(64 位下占 8 个字节),只不过它里面保存的内容恰好是 score 的地址。此外,定义时如果暂时还没有明确目标,引用不能悬空不初始化,而指针可以先初始化为空指针 nullptr。

// 错误方式
int& ref;

// 正确方式
int* p = nullptr;

另一个小伙伴们容易混淆的地方,是 “能不能中途更换目标”。引用一旦绑定某个变量,就不能再改绑其他变量。而指针可以随时修改自己保存的地址。我们把这两种操作放在同一个例子里对比看看。

示例 2:给引用赋值与修改指针指向

#include <iostream>

int main() {
    int a = 10;
    int b = 20;

    int& ref = a;    // ref 绑定变量 a
    int* p = &a;     // p 指向变量 a

    ref = b;         // 把 b 的值赋给 a
    p = &b;          // 将 p 改为指向变量 b

    b = 30;
    std::cout << "a = " << a << '\n';
    std::cout << "ref = " << ref << '\n';
    std::cout << "*p = " << *p << '\n';
    return 0;
}

运行结果如下。

a = 20
ref = 20
*p = 30

分析:

在这个例子中,ref = b 和 p = &b 虽然都使用了赋值号,但实际执行的操作完全不同。

因为引用一旦绑定就不能改绑,所以 ref = b 并没有让 ref 改绑到变量 b 身上,而是把 b 当前的值 20 赋给了 a。对于这个例子来说,下面 2 种方式是等价的。

// 方式 1
ref = b;

// 方式 2
a = b;

而 p = &b 修改的是指针 p 自身保存的内存地址,将指向的目标从变量 a 换成了变量 b。因此后面执行 b = 30 时,已经指向 b 的 *p 读到了最新的 30,而始终绑定在 a 身上的 ref 读到的依然是 20。

如何选择指针与引用?

弄清楚两者的区别后,大家在平时写代码以及设计函数参数时只需要问自己两个问题:① 目标变量有没有可能不存在?② 中途需不需要更换指向的目标?根据答案,我们可以按照以下 3 条标准来选择:

  • 目标必须存在且不换目标:如果函数要求调用者必须传入一个真实有效的变量,并且内部不需要中途更换目标,我们优先使用引用。采用引用传参不仅写法直观,调用时直接传变量名即可,函数内部也省去了检查 nullptr 的步骤。
  • 目标允许为空:有时某个函数参数是可选的,允许 “有目标变量就处理,传入 nullptr 就跳过”。由于引用不能为空,遇到这种场景我们就应该使用指针来实现。
  • 中途需要更换指向目标:如果我们在代码中需要先关联变量 a,执行到后面又改为关联变量 b,就只能使用指针,因为引用一旦绑定就无法更改目标。

下面我们通过一个例子来对比函数参数中引用与指针的具体用法。

示例 3:函数参数中的引用与指针

#include <iostream>

// 使用引用接收必传目标
void addOne(int& value) {
    value++;
}

// 使用指针接收可选目标
void resetIfPresent(int* value) {
    if (value != nullptr) {
        *value = 0;
    }
}

int main() {
    int score = 10;

    addOne(score);
    std::cout << "score = " << score << '\n';

    resetIfPresent(nullptr);    // 传入空指针
    std::cout << "score = " << score << '\n';

    resetIfPresent(&score);     // 传入变量地址
    std::cout << "score = " << score << '\n';
    return 0;
}

运行结果如下。

score = 11
score = 11
score = 0

分析:

addOne(int& value) 要求调用者必须传入一个有效的整型变量来加 1,因此我们将形参定义为引用。调用时直接写 addOne(score),score 的值就从 10 变成了 11。

而 resetIfPresent(int* value) 允许调用者传入 nullptr 表示不提供目标,因此我们将形参定义为指针。

第 1 次调用 resetIfPresent(nullptr) 时,我们传入了空指针,函数检测到 value != nullptr 不成立就会直接跳过,因此 score 保持 11 不变。第 2 次调用传入 &score 后,条件成立,函数执行 *value = 0 将 score 重置为 0。

对于指针与引用的选型,小伙伴们只需要记住这么一句话就可以了:在 C++ 实际开发中,能使用引用就优先使用引用。只有当目标可能为空,或者中途需要更换指向时,才使用指针。

常见问题

1. 既然引用比指针更安全、写起来也更简单,为什么 C++ 不直接取消指针,只保留引用呢?

这主要有以下两方面的原因:

兼容 C 语言与系统接口:C++ 需要兼容 C 语言以及大量操作系统底层接口,而 C 语言里只有指针,没有引用。

满足空状态与灵活指向的需求:引用定义时必须初始化、不能为空且不能改绑。当我们需要表示 “可选参数为空”、计算数组地址偏移,或者在后续章节管理动态内存时,都必须依靠能设为 nullptr 且能随时改指向的指针来实现。

2. 引用在编译器底层是怎么实现的?

在语法层面上,我们可以把引用单纯看作原变量的 “别名”。而在编译器底层,引用通常是借助指针常量来实现的,它的指向一旦确定就固定不变。只不过编译器在背后自动帮我们完成了取地址和解引用的操作,并在编译阶段加上了 “必须初始化且不能改绑” 的语法检查。

上一篇:

下一篇:

给站长反馈

绿叶网正在不断完善中,小伙伴们如果发现任何问题,还望多多给站长反馈,谢谢!

邮箱:lvyenet@vip.qq.com

「绿叶网」服务号
绿叶网服务号放大
关注服务号,微信也能看教程。
绿叶网服务号