C/C++ 入门:引用

学完指针之后,引用就是 C++ 给“给变量起别名”这件事提供的更直接写法。

📚 基本概念速读

名称 定义 省流
引用 已有对象的别名 同一个变量的另一个名字
左值引用 绑定到可定位对象的引用,写作 T& 常见引用
绑定 让引用和某个对象关联起来 起别名
实参 函数调用时传入的真实对象 真正的数据
形参 函数定义里接收参数的名字 函数里的入口
const 引用 只读引用,写作 const T& 不改对象,还能少拷贝
临时对象 表达式产生的短生命周期对象 临时值

🧩 引用的基本模型

引用不是新建一个独立变量,而是给已有对象再起一个名字。

1
2
3
4
5
6
7
int x = 10;
int& ref = x;

ref = 20;

cout << x << endl; // 20
cout << ref << endl; // 20

可以这样理解:

1
2
3
x   ----+
| 同一个 int 对象
ref ----+

xref 是两个名字,但它们访问的是同一个对象。修改 ref,本质上就是修改 x

引用最常见的用途是做函数参数:



flowchart LR
    A[实参对象] --> B[引用形参]
    B --> C[函数内部直接操作原对象]

🔗 左值引用

定义

左值引用的一般形式:

1
类型& 引用名 = 已有变量;

示例:

1
2
int value = 10;
int& alias = value;

aliasvalue 的引用,也就是 value 的别名。

& 在这里不是取地址,而是引用声明的一部分。

绑定规则

普通左值引用只能绑定到可以长期存在、可以定位的对象。

1
2
3
4
5
int x = 10;
int& r = x; // 正确:x 是一个普通变量

// int& a = 10; // 错误:普通左值引用不能绑定到字面量
// int& b = x + 1;// 错误:x + 1 是临时结果

初学阶段可以先把左值粗略理解成:有名字、能取地址、表达式结束后还存在的对象

表达式 能否绑定到 int& 原因
x 可以 普通变量,有稳定位置
arr[0] 可以 数组元素,有稳定位置
10 不可以 字面量不是可修改对象
x + 1 不可以 表达式结果是临时值

必须初始化

引用定义时必须立刻绑定到一个对象。

1
2
3
4
int x = 10;
int& r = x; // 正确

// int& bad; // 错误:引用必须初始化

原因很简单:引用的意义就是“某个对象的别名”。如果没有对象可以绑定,这个别名就没有意义。

不能重新绑定

引用一旦绑定到某个对象,就不能再改成另一个对象的别名。

1
2
3
4
5
6
7
8
int a = 10;
int b = 20;

int& r = a;
r = b;

cout << a << endl; // 20
cout << b << endl; // 20

r = b; 不是让 r 改为引用 b,而是把 b 的值赋给 r 当前绑定的对象,也就是 a

🛠️ 引用做函数参数

值传递会拷贝

先看普通值传递:

1
2
3
4
5
6
7
8
9
10
11
12
13
void addOne(int x)
{
x = x + 1;
}

int main()
{
int number = 10;
addOne(number);

cout << number << endl; // 10
return 0;
}

addOne 里的 xnumber 的副本。函数内部改的是副本,不会影响外面的 number

引用参数可以修改实参

如果希望函数直接修改外面的变量,可以使用引用参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
void addOne(int& x)
{
x = x + 1;
}

int main()
{
int number = 10;
addOne(number);

cout << number << endl; // 11
return 0;
}

这里的 xnumber 的引用。函数内部修改 x,就是修改 number

调用过程可以理解为:

1
2
3
4
5
6
7
8
9
10
number = 10
|
v
addOne(int& x)
|
v
x 是 number 的别名
|
v
x = x + 1 等价于 number = number + 1

避免大对象拷贝

引用参数还可以避免拷贝大对象。

1
2
3
4
5
6
7
8
#include <iostream>
#include <string>
using namespace std;

void printName(string name)
{
cout << name << endl;
}

上面这种写法会把传入的 string 拷贝一份。对于很短的字符串影响不大,但如果对象很大,拷贝成本就会变高。

如果函数只是读取,不需要修改,可以先写成引用:

1
2
3
4
void printName(string& name)
{
cout << name << endl;
}

但这还不够好,因为 string& 暗示函数可能会修改 name。更好的写法是 const string&,下一节展开。

🧊 const 引用

只读传参

const 引用表示:通过这个引用不能修改绑定的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
#include <string>
using namespace std;

void printName(const string& name)
{
cout << name << endl;
// name = "new name"; // 错误:const 引用不能修改对象
}

int main()
{
string userName = "Alice";
printName(userName);
return 0;
}

const string& name 同时表达了两层意思:

目的 说明
避免拷贝 不复制整个 string 对象
保持只读 函数不会通过 name 修改实参

这也是 C++ 里非常常见的参数写法。

绑定临时对象

普通左值引用不能绑定临时对象,但 const 引用可以。

1
2
3
4
5
const int& a = 10;      // 正确
const int& b = 3 + 4; // 正确

cout << a << endl; // 10
cout << b << endl; // 7

这条规则让下面这种调用变得自然:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
#include <string>
using namespace std;

void printText(const string& text)
{
cout << text << endl;
}

int main()
{
string name = "Alice";

printText(name); // 绑定已有 string 对象
printText("hello"); // 绑定临时 string 对象
printText(name + " Smith"); // 绑定表达式产生的临时对象

return 0;
}

如果参数写成 string&,后两种调用就不适合;写成 const string&,既能接收已有对象,也能接收临时对象。

什么时候用 const 引用

初学阶段可以按这个规则判断:

参数类型 适合场景
int x 小的基础类型,直接拷贝即可
double x 小的基础类型,直接拷贝即可
string x 函数需要自己保留一份副本
const string& x 只读取字符串,避免拷贝
vector<int>& x 需要修改传入的容器
const vector<int>& x 只读取容器,避免拷贝

不是所有参数都要写引用。像 intdoublecharbool 这种小类型,值传递通常更简单。

🆚 引用与指针对比

引用和指针都可以间接访问对象,但它们的语义不同。

对比项 引用 指针
核心语义 对象的别名 保存地址的变量
是否必须初始化 必须初始化 可以先不指向有效对象
是否可空 通常不能表示空 可以是 nullptr
是否能重新绑定 不能重新绑定 可以改为指向别的对象
使用方式 像普通变量一样用 常用 * 解引用、-> 访问成员
常见场景 函数参数、返回已有对象别名 可选对象、动态内存、底层地址操作

语义差异

引用强调“这个参数就是那个对象本身”。

1
2
3
4
void reset(int& x)
{
x = 0;
}

指针强调“我手里有一个地址,需要沿着地址去访问对象”。

1
2
3
4
5
6
void reset(int* p)
{
if (p != nullptr) {
*p = 0;
}
}

两者都能修改外部变量:

1
2
3
4
int value = 10;

reset(value); // 引用版本
reset(&value); // 指针版本

引用版本调用更像普通函数;指针版本调用时显式写 &value,更能提醒读者“这里传的是地址”。

可空性

指针可以是空指针:

1
int* p = nullptr;

所以使用指针前经常要检查:

1
2
3
if (p != nullptr) {
cout << *p << endl;
}

引用一般不表达“没有对象”这个状态:

1
2
3
4
void print(const int& x)
{
cout << x << endl;
}

如果函数要求一定有一个有效对象,引用通常更合适;如果“可能没有对象”是合法状态,指针更直接。

重新绑定

指针可以改变指向:

1
2
3
4
5
6
7
int a = 10;
int b = 20;

int* p = &a;
p = &b;

cout << *p << endl; // 20

引用不能重新绑定:

1
2
3
4
5
int a = 10;
int b = 20;

int& r = a;
r = b; // 赋值给 a,不是改绑到 b

这一点是引用和指针最重要的区别之一。

使用场景

可以先按下面的方式选择:

需求 推荐
函数需要修改实参 T&
函数只读大对象 const T&
参数可能为空 T*
需要动态申请和释放内存 指针或智能指针
需要换成指向另一个对象 指针
只是给已有对象起别名 引用

现代 C++ 里,普通业务代码会大量使用引用和 const 引用;裸指针更多出现在底层接口、和 C 交互、动态内存或“可空参数”语义里。

⚠️ 常见误区

误区 正解
引用是对象的副本 引用是别名,修改引用就是修改原对象
引用可以先声明后绑定 引用必须定义时初始化
r = b 会让引用改绑到 b 这是赋值,会修改 r 当前绑定的对象
所有函数参数都应该用引用 小的基础类型直接值传递更简单
const T& 只能绑定普通变量 const T& 也能绑定临时对象
引用和指针只是语法不同 两者在可空性、重新绑定和语义表达上不同

✅ 总结

引用就是对象的别名,最常用在函数参数里:要修改实参用 T&,只读大对象用 const T&

学到这里,可以把函数、指针和引用连起来看:值传递适合简单副本,引用适合直接操作已有对象,指针适合表达地址、可空和更底层的内存操作。

Happy Hacking! 🎉