C/C++ 入门:引用
学完指针之后,引用就是 C++ 给“给变量起别名”这件事提供的更直接写法。
📚 基本概念速读
| 名称 | 定义 | 省流 |
|---|---|---|
| 引用 | 已有对象的别名 | 同一个变量的另一个名字 |
| 左值引用 | 绑定到可定位对象的引用,写作 T& |
常见引用 |
| 绑定 | 让引用和某个对象关联起来 | 起别名 |
| 实参 | 函数调用时传入的真实对象 | 真正的数据 |
| 形参 | 函数定义里接收参数的名字 | 函数里的入口 |
| const 引用 | 只读引用,写作 const T& |
不改对象,还能少拷贝 |
| 临时对象 | 表达式产生的短生命周期对象 | 临时值 |
🧩 引用的基本模型
引用不是新建一个独立变量,而是给已有对象再起一个名字。
1 | int x = 10; |
可以这样理解:
1 | x ----+ |
x 和 ref
是两个名字,但它们访问的是同一个对象。修改
ref,本质上就是修改 x。
引用最常见的用途是做函数参数:
flowchart LR
A[实参对象] --> B[引用形参]
B --> C[函数内部直接操作原对象]
🔗 左值引用
定义
左值引用的一般形式:
1 | 类型& 引用名 = 已有变量; |
示例:
1 | int value = 10; |
alias 是 value 的引用,也就是
value 的别名。
&在这里不是取地址,而是引用声明的一部分。
绑定规则
普通左值引用只能绑定到可以长期存在、可以定位的对象。
1 | int x = 10; |
初学阶段可以先把左值粗略理解成:有名字、能取地址、表达式结束后还存在的对象。
| 表达式 | 能否绑定到 int& |
原因 |
|---|---|---|
x |
可以 | 普通变量,有稳定位置 |
arr[0] |
可以 | 数组元素,有稳定位置 |
10 |
不可以 | 字面量不是可修改对象 |
x + 1 |
不可以 | 表达式结果是临时值 |
必须初始化
引用定义时必须立刻绑定到一个对象。
1 | int x = 10; |
原因很简单:引用的意义就是“某个对象的别名”。如果没有对象可以绑定,这个别名就没有意义。
不能重新绑定
引用一旦绑定到某个对象,就不能再改成另一个对象的别名。
1 | int a = 10; |
r = b; 不是让 r 改为引用
b,而是把 b 的值赋给 r
当前绑定的对象,也就是 a。
🛠️ 引用做函数参数
值传递会拷贝
先看普通值传递:
1 | void addOne(int x) |
addOne 里的 x 是 number
的副本。函数内部改的是副本,不会影响外面的 number。
引用参数可以修改实参
如果希望函数直接修改外面的变量,可以使用引用参数:
1 | void addOne(int& x) |
这里的 x 是 number 的引用。函数内部修改
x,就是修改 number。
调用过程可以理解为:
1 | number = 10 |
避免大对象拷贝
引用参数还可以避免拷贝大对象。
1 |
|
上面这种写法会把传入的 string
拷贝一份。对于很短的字符串影响不大,但如果对象很大,拷贝成本就会变高。
如果函数只是读取,不需要修改,可以先写成引用:
1 | void printName(string& name) |
但这还不够好,因为 string& 暗示函数可能会修改
name。更好的写法是
const string&,下一节展开。
🧊 const 引用
只读传参
const 引用表示:通过这个引用不能修改绑定的对象。
1 |
|
const string& name 同时表达了两层意思:
| 目的 | 说明 |
|---|---|
| 避免拷贝 | 不复制整个 string 对象 |
| 保持只读 | 函数不会通过 name 修改实参 |
这也是 C++ 里非常常见的参数写法。
绑定临时对象
普通左值引用不能绑定临时对象,但 const 引用可以。
1 | const int& a = 10; // 正确 |
这条规则让下面这种调用变得自然:
1 |
|
如果参数写成 string&,后两种调用就不适合;写成
const string&,既能接收已有对象,也能接收临时对象。
什么时候用 const 引用
初学阶段可以按这个规则判断:
| 参数类型 | 适合场景 |
|---|---|
int x |
小的基础类型,直接拷贝即可 |
double x |
小的基础类型,直接拷贝即可 |
string x |
函数需要自己保留一份副本 |
const string& x |
只读取字符串,避免拷贝 |
vector<int>& x |
需要修改传入的容器 |
const vector<int>& x |
只读取容器,避免拷贝 |
不是所有参数都要写引用。像
int、double、char、bool
这种小类型,值传递通常更简单。
🆚 引用与指针对比
引用和指针都可以间接访问对象,但它们的语义不同。
| 对比项 | 引用 | 指针 |
|---|---|---|
| 核心语义 | 对象的别名 | 保存地址的变量 |
| 是否必须初始化 | 必须初始化 | 可以先不指向有效对象 |
| 是否可空 | 通常不能表示空 | 可以是 nullptr |
| 是否能重新绑定 | 不能重新绑定 | 可以改为指向别的对象 |
| 使用方式 | 像普通变量一样用 | 常用 * 解引用、-> 访问成员 |
| 常见场景 | 函数参数、返回已有对象别名 | 可选对象、动态内存、底层地址操作 |
语义差异
引用强调“这个参数就是那个对象本身”。
1 | void reset(int& x) |
指针强调“我手里有一个地址,需要沿着地址去访问对象”。
1 | void reset(int* p) |
两者都能修改外部变量:
1 | int value = 10; |
引用版本调用更像普通函数;指针版本调用时显式写
&value,更能提醒读者“这里传的是地址”。
可空性
指针可以是空指针:
1 | int* p = nullptr; |
所以使用指针前经常要检查:
1 | if (p != nullptr) { |
引用一般不表达“没有对象”这个状态:
1 | void print(const int& x) |
如果函数要求一定有一个有效对象,引用通常更合适;如果“可能没有对象”是合法状态,指针更直接。
重新绑定
指针可以改变指向:
1 | int a = 10; |
引用不能重新绑定:
1 | int a = 10; |
这一点是引用和指针最重要的区别之一。
使用场景
可以先按下面的方式选择:
| 需求 | 推荐 |
|---|---|
| 函数需要修改实参 | T& |
| 函数只读大对象 | const T& |
| 参数可能为空 | T* |
| 需要动态申请和释放内存 | 指针或智能指针 |
| 需要换成指向另一个对象 | 指针 |
| 只是给已有对象起别名 | 引用 |
现代 C++ 里,普通业务代码会大量使用引用和 const
引用;裸指针更多出现在底层接口、和 C
交互、动态内存或“可空参数”语义里。
⚠️ 常见误区
| 误区 | 正解 |
|---|---|
| 引用是对象的副本 | 引用是别名,修改引用就是修改原对象 |
| 引用可以先声明后绑定 | 引用必须定义时初始化 |
r = b 会让引用改绑到 b |
这是赋值,会修改 r 当前绑定的对象 |
| 所有函数参数都应该用引用 | 小的基础类型直接值传递更简单 |
const T& 只能绑定普通变量 |
const T& 也能绑定临时对象 |
| 引用和指针只是语法不同 | 两者在可空性、重新绑定和语义表达上不同 |
✅ 总结
引用就是对象的别名,最常用在函数参数里:要修改实参用
T&,只读大对象用const T&。
学到这里,可以把函数、指针和引用连起来看:值传递适合简单副本,引用适合直接操作已有对象,指针适合表达地址、可空和更底层的内存操作。
Happy Hacking! 🎉