引用:比指针更安全的别名

文章目录

  • 引言
  • 一、引用的本质:别名,而非地址
    • [1.1 别名语义](#1.1 别名语义)
    • [1.2 引用与指针的内存视图](#1.2 引用与指针的内存视图)
    • [1.3 引用必须在定义时初始化](#1.3 引用必须在定义时初始化)
  • [二、`const &`:临时对象的生命线](#二、const &:临时对象的生命线)
    • [2.1 const 引用可以绑定到临时对象](#2.1 const 引用可以绑定到临时对象)
    • [2.2 临时对象生命周期延长](#2.2 临时对象生命周期延长)
  • 三、引用作为函数参数:零拷贝传递
    • [3.1 从 C 的指针传参到 C++ 的引用传参](#3.1 从 C 的指针传参到 C++ 的引用传参)
    • [3.2 传值 vs 传引用 vs 传 const 引用](#3.2 传值 vs 传引用 vs 传 const 引用)
  • 四、返回引用:踩坑天堂
    • [4.1 可以安全返回引用的情况](#4.1 可以安全返回引用的情况)
    • [4.2 绝不能返回的引用](#4.2 绝不能返回的引用)
  • 五、左值引用与右值引用的第一印象
    • [5.1 什么是左值、什么是右值](#5.1 什么是左值、什么是右值)
    • [5.2 右值引用的基本用途:移动](#5.2 右值引用的基本用途:移动)
    • [5.3 引用折叠(reference collapsing)](#5.3 引用折叠(reference collapsing))
  • 六、引用的其他细节
    • [6.1 引用成员变量](#6.1 引用成员变量)
    • [6.2 指针与引用的转换](#6.2 指针与引用的转换)
    • [6.3 不要返回函数内 lambda 捕获的引用](#6.3 不要返回函数内 lambda 捕获的引用)
  • [七、引用 vs 指针:决策指南](#七、引用 vs 指针:决策指南)
  • 总结

本系列为《C++深度修炼:基础、STL源码与多线程实战》第9篇

前置条件:理解 C 语言的指针,了解 C++ 的 const(第8篇)和函数(第4篇)

引言

C 程序员对指针了如指掌:

c 复制代码
int x = 10;
int *p = &x;   // p 存着 x 的地址
*p = 20;        // 通过 p 间接修改 x

C++ 引入了引用(reference) ------一个表面上像"自动解引用的指针",但实际上是一个更基础的语言概念:别名

cpp 复制代码
int x = 10;
int &r = x;    // r 是 x 的别名------不是指针,不是地址,就是同一个东西
r = 20;        // 等同于 x = 20

引用不只是"更安全的指针"。它引发了 C++ 中一整套与值类别、临时对象、完美转发相关的设计,这些后话会在泛型编程章节展开。本文先打好基础:引用的语义、与指针的边界、const & 的妙用、以及何时用引用、何时用指针。


一、引用的本质:别名,而非地址

1.1 别名语义

cpp 复制代码
#include <iostream>

int main() {
    int x = 42;
    int &r = x;   // r 是 x 的引用(别名)

    std::cout << "x = " << x << ", r = " << r << '\n';  // 42, 42
    std::cout << "&x = " << &x << ", &r = " << &r << '\n';  // 同一个地址!

    r = 100;      // 修改 r 就是修改 x
    std::cout << "x = " << x << '\n';  // 100
}

输出:

text 复制代码
x = 42, r = 42
&x = 0x7ffc1234, &r = 0x7ffc1234   ← 完全相同的地址
x = 100

取引用变量的地址,得到的和被引用对象的地址是同一个地址。这一点和指针截然不同------指针变量有自己的地址,其中存储的值是目标对象的地址。

1.2 引用与指针的内存视图

cpp 复制代码
int x = 42;
int *p = &x;     // p 是一个独立变量,值为 &x
int &r = x;      // r 不是独立变量,它只是 x 的另一个名字

// 内存视角:
// ┌───────┬───┐
// │   x   │42 │  ← 地址 0x1000
// ├───────┼───┤
// │   p   │0x1000 │  ← 地址 0x1008(p 有自己的地址)
// ├───────┼───┤
// │   r   │ (不存在独立存储,r 就是 0x1000) │
// └───────┴───┘

引用在语言层面不占存储空间 (底层实现通常用指针,但这不是你该依赖的细节)。sizeof(r) 返回的是被引用对象的大小,不是指针的大小。

1.3 引用必须在定义时初始化

cpp 复制代码
int &r;       // ❌ 编译错误:引用必须初始化
int &r2 = x;  // ✅ 定义时绑定,之后不能"重新绑定"到别的变量

int y = 0;
r2 = y;       // 这不是重新绑定 r2------这是把 y 的值赋给 x(通过 r2)!

与指针对比:

cpp 复制代码
int *p;       // ✅ 可以先不初始化(危险,但不报错)
p = &x;       // 后续指向 x
p = &y;       // 可以改指向 y------指针可以"重新指向"
特性 指针 引用
可以不初始化 ✅ (危险) ❌ 必须初始化
可以重新绑定 p = &y ❌ 绑定后不可改
可以为空 nullptr ❌ 没有"空引用"
有独立地址 &p != &x &r == &x
需要解引用 *p = 10 ❌ 直接使用 r = 10
编译器可能优化掉

二、const &:临时对象的生命线

2.1 const 引用可以绑定到临时对象

这是引用最常用的模式,也是 C 程序员最容易忽略的差异:

cpp 复制代码
void print(const std::string &s) {
    std::cout << s << '\n';
}

int main() {
    print("hello");  // "hello" 是 const char[6],不是 std::string
    // 但 const std::string& 可以绑定到临时对象!
    // 编译器创建一个临时 std::string("hello"),引用绑定到它
}

没有 const &,你只能传 std::string 对象本身:

cpp 复制代码
void print(std::string &s) {  // 非 const 引用------不能绑定临时对象
    std::cout << s << '\n';
}

int main() {
    // print("hello");  // ❌ 不能把 const char[6] 绑定到 std::string&
    std::string s = "hello";
    print(s);            // ✅ 可以绑定到左值
}

规则const T& 可以绑定到临时对象(右值),T& 只能绑定到左值。

2.2 临时对象生命周期延长

cpp 复制代码
#include <iostream>

class Tracer {
public:
    Tracer()  { std::cout << "Tracer()\n"; }
    ~Tracer() { std::cout << "~Tracer()\n"; }
    void hello() const { std::cout << "hello\n"; }
};

int main() {
    {
        Tracer t;        // t 在作用域结束时析构
        std::cout << "before end of scope\n";
    }  // t 在这里析构
    std::cout << "---\n";

    {
        const Tracer &ref = Tracer();  // 临时对象!生命周期延长到 ref 的作用域
        ref.hello();
        std::cout << "before end of scope\n";
    }  // 临时 Tracer 在这里析构------因为 const & 延长了它的生命
}

输出:

text 复制代码
Tracer()
before end of scope
~Tracer()
---
Tracer()
hello
before end of scope
~Tracer()

const T& 将临时对象的生命延长到了引用本身的作用域。这个规则确保了你不会在下一行访问已销毁的对象。


三、引用作为函数参数:零拷贝传递

3.1 从 C 的指针传参到 C++ 的引用传参

c 复制代码
// C 的方式:传指针
void update_temperature(double *temp) {
    if (temp) *temp += 5.0;  // 必须判空------不然解引用空指针崩掉
}

// 调用侧
update_temperature(&reading);  // 需要取地址
cpp 复制代码
// C++ 的方式:传引用
void update_temperature(double &temp) {
    temp += 5.0;  // 不需要判空------引用不能为空
}

// 调用侧
update_temperature(reading);  // 不需要取地址------和传值一样的写法,但零拷贝

3.2 传值 vs 传引用 vs 传 const 引用

cpp 复制代码
#include <iostream>
#include <string>
#include <vector>

// 传值:拷贝一份
void process_by_value(std::vector<int> v) {
    v.push_back(42);  // 修改的是副本
}  // 析构副本

// 传引用:不拷贝,可修改
void process_by_ref(std::vector<int> &v) {
    v.push_back(42);  // 修改的是原对象
}

// 传 const 引用:不拷贝,不可修改
void process_by_cref(const std::vector<int> &v) {
    // v.push_back(42);  // ❌ const,不可修改
    std::cout << v.size() << '\n';  // ✅ 只读访问
}

选择标准:

场景 传参方式
小对象(int, double, pointer) 传值
大对象,只读访问 const T&
大对象,需要修改 T&
需要所有权的转移 T&&(右值引用,后续章节)
可选参数(可能为空) 指针(T*)------引用不能表示"没有"

💡 经验法则 :默认用 const T& 传递非基本类型。需要修改时用 T&。需要所有权或可选时再考虑其他。


四、返回引用:踩坑天堂

4.1 可以安全返回引用的情况

情况一:返回成员变量的引用

cpp 复制代码
class Container {
public:
    int& at(size_t i) { return data_[i]; }            // 非 const 版本
    const int& at(size_t i) const { return data_[i]; } // const 版本
private:
    std::vector<int> data_{1, 2, 3};
};

情况二:返回静态/全局对象的引用

cpp 复制代码
const std::string& app_name() {
    static const std::string name = "MyApp v2.0";
    return name;  // 安全:静态对象生命周期 = 整个程序
}

情况三:返回传入的引用参数

cpp 复制代码
// 流操作符返回引用,支持链式调用
std::ostream& operator<<(std::ostream &os, const Point &p) {
    return os << '(' << p.x << ", " << p.y << ')';
}

4.2 绝不能返回的引用

cpp 复制代码
// ❌ 灾难一:返回局部变量的引用
const std::string& make_greeting(const std::string &name) {
    std::string result = "Hello, " + name;  // 局部变量
    return result;  // 悬垂引用!result 在函数返回时就销毁了
}

// ❌ 灾难二:返回临时对象的引用
const int& get_value() {
    return 42;  // 临时 int 在 return 后销毁------悬垂引用
}

// ❌ 灾难三:返回局部 unique_ptr 的引用
const std::string& bad_factory() {
    auto p = std::make_unique<std::string>("hello");
    return *p;  // p 在函数结束时被销毁------*p 也没了
}

编译器的警告可帮不少忙(-Wall 会警告返回局部变量的引用),但不能依赖警告------逻辑上没有编译器能判断所有情况。


五、左值引用与右值引用的第一印象

C++11 引入了右值引用(rvalue reference) ,用 && 表示。这是移动语义和完美转发的基础------这里先给第一印象,详细内容在模板章节展开。

5.1 什么是左值、什么是右值

简化版定义:

  • 左值(lvalue) :有名字、能取地址的表达式。如变量 x、解引用 *p
  • 右值(rvalue) :临时的、没有持久身份的表达式。如字面量 42、表达式结果 x + y、函数返回的临时对象
cpp 复制代码
int x = 10;         // x 是左值
int &lr = x;        // ✅ 左值引用绑定左值
// int &lr2 = 10;   // ❌ 左值引用不能绑定右值
const int &clr = 10; // ✅ const 左值引用可以绑定右值

int &&rr = 10;      // ✅ 右值引用绑定右值
int &&rr2 = x + 5;  // ✅ 右值引用绑定临时表达式结果
// int &&rr3 = x;   // ❌ 右值引用不能直接绑定左值

5.2 右值引用的基本用途:移动

cpp 复制代码
#include <iostream>
#include <string>
#include <vector>

int main() {
    std::vector<int> v1{1, 2, 3, 4, 5};

    std::vector<int> v2 = v1;              // 拷贝:v1 保持不变,v2 是副本
    std::vector<int> v3 = std::move(v1);   // 移动:v1 的数据被"掏空"并转移给 v3

    std::cout << "v1.size() = " << v1.size() << '\n';  // 0 ------ v1 被移空了
    std::cout << "v3.size() = " << v3.size() << '\n';  // 5 ------ 数据归 v3 了
}

std::move 本质上是一个 cast------它把左值转成右值引用,让编译器可以选择移动构造函数而非拷贝构造函数。移动操作通常很廉价(对 std::vector 只是交换三个指针),避免了深拷贝。

5.3 引用折叠(reference collapsing)

这是模板编程中才会频繁遇到的规则,但了解它有助于理解一些看起来"违反直觉"的行为:

cpp 复制代码
// 引用的引用在某些语境中会出现,编译器自动折叠:
// T& &   → T&
// T& &&  → T&
// T&& &  → T&
// T&& && → T&&

// 规则:只要有一个是左值引用,结果就是左值引用
//      全是右值引用,结果才是右值引用

这个规则是 std::forward(完美转发)能够工作的基础------后续模板章节详细展开。


六、引用的其他细节

6.1 引用成员变量

引用可以作为类的成员:

cpp 复制代码
class Holder {
public:
    Holder(int &ref) : ref_(ref) {}

    void set(int v) { ref_ = v; }  // 修改引用指向的外部变量

private:
    int &ref_;  // 引用成员
};

但引用成员有几个问题:

  • 必须在构造的初始化列表中初始化(引用不能"后绑定")
  • 类不能默认拷贝(编译器不会自动生成拷贝赋值运算符)
  • 通常用指针成员更好------除非你明确需要"绑定后不可改"的语义

6.2 指针与引用的转换

cpp 复制代码
// 引用 → 指针:取地址即可
void by_ref(int &r) {
    int *p = &r;  // r 是 x 的别名,&r == &x
}

// 指针 → 引用:先判空,再解引用
void by_ptr(int *p) {
    if (p) {
        int &r = *p;  // 安全:已判空
    }
}

6.3 不要返回函数内 lambda 捕获的引用

cpp 复制代码
#include <functional>

// ❌ 危险
std::function<int()> make_counter_bad() {
    int count = 0;
    return [&count]() { return ++count; };  // count 在函数返回后销毁!
}

// ✅ 安全:按值捕获或使用 shared_ptr
std::function<int()> make_counter_good() {
    auto count = std::make_shared<int>(0);
    return [count]() { return ++(*count); };
}

七、引用 vs 指针:决策指南

text 复制代码
                你是 C 程序员,遇到下面场景怎么选?
                               │
                    需要"不存在的值"(空)?
                      │            │
                     是           否
                      │            │
                    指针          需要重新绑定?
                      │            │
                                 是           否
                                  │            │
                                指针         引用
                                  │            │
                                             对大型对象优化传参?
                                              │            │
                                             是           否
                                              │            │
                                         const T&    传值即可

一句话总结:引用是"不会为空、不会换绑"的指针。当你不想要指针的灵活度时,引用是更好的约束。 反过来,当语义上需要表达"可能没有",就用指针。


总结

引用是 C++ 对 C 指针世界的最重要补丁之一------它保留了间接访问的零开销,去掉了空指针和未初始化指针的危险:

  1. 引用的本质是别名------和原变量共享同一地址,不独立占用存储(语言层面)
  2. const T& 是工程中最常用的传参方式------零拷贝 + 只读保证 + 可绑定临时对象
  3. const T& 延长临时对象生命周期------让你安全地接收函数返回的临时对象
  4. 返回引用三思------局部变量、临时对象、局部智能指针的引用都会产生悬垂引用
  5. 右值引用 T&& 是移动语义的基础------留下印象即可,后续泛型编程章节会深入
  6. 默认选择 :大型对象只读传参用 const T&,需要修改用 T&,可选参数用指针,小对象传值

第2章的4篇文章(命名空间/输入输出/const/引用)到此结束。这些是 C 程序员进入 C++ 世界必须升级的"基础设施"。下一篇开始进入第3章------动态内存与智能指针 ,从 new/delete 一直讲到 unique_ptrshared_ptr 和 RAII 的核心理念。


📝 动手练习

  1. 写一个函数 swap(int &a, int &b) 用引用交换两个整数,再写一个 swap(int *a, int *b) 用指针。对比调用侧的语法差异
  2. 写一个函数返回 const std::string&,故意返回局部变量,看编译器能给出什么警告(-Wall
  3. const T& 改写一个之前大量传值的函数,用 perf 统计拷贝次数的减少
  4. 探索:int &&rr = 10; rr = 20; 能编译吗?这意味着什么?(提示:右值引用本身是左值)
相关推荐
m0_502724951 小时前
golang 、java、c++、javascript 语言switch case异同
java·javascript·c++·golang
我命由我123451 小时前
Android Framework P1 - 低配学习 Framework 方案、开机启动 Init 进程
android·c语言·c++·学习·android jetpack·android-studio·android runtime
许长安1 小时前
互斥锁、自旋锁、读写锁使用场景以及底层实现
c++·经验分享·笔记
Season4501 小时前
C++11并发支持库(condition_variable | future全家桶)
java·jvm·c++
落羽的落羽1 小时前
【项目】C++从零实现JsonRpc框架——项目引入
linux·服务器·开发语言·c++·人工智能·算法·机器学习
Andy1 小时前
C++ 容器适配器_栈_队列_双端队列
开发语言·网络·c++
思麟呀2 小时前
在C++基础上理解Csharp-2
开发语言·jvm·c++·c#
桀人2 小时前
类和对象——上篇
开发语言·c++
智者知已应修善业2 小时前
【51单片机独立按键和定时器中断的疑惑验证】2023-11-2
c++·经验分享·笔记·算法·51单片机