C++ 引用
引用(reference)是 C++ 区别于 C 的第一项重大改进。简单说,引用就是"变量的别名"。它和指针有相似的作用(都能间接操作变量),但更安全、更优雅。这一章我们看引用的所有用法,以及它和指针的本质区别。
一、引用的基本概念
引用就是给已存在的变量起个别名。语法是在类型后加 &:
#include <iostream>
using namespace std;
int main() {
int x = 10;
// 引用:给变量起别名,& 在类型后
int& ref = x; // ref 是 x 的别名
cout << x << endl; // 10
cout << ref << endl; // 10(同一份数据)
ref = 20; // 通过 ref 修改,实际改的是 x
cout << x << endl; // 20
// 关键:引用必须初始化,且不能再绑定到别的变量
// int& bad; // 编译错误:未初始化
return 0;
}引用的三大铁律(牢记):
- 必须初始化:声明时必须绑定到一个变量,不能"先声明再绑定"。
- 不可改绑定:一旦绑定到 A,终生就是 A 的别名;后续的
=都是赋值,不是重新绑定。 - 不为空:不存在"空引用",引用必须绑到一个真实存在的对象。
这三条和指针恰好相反,所以引用比指针安全——不存在空指针解引用、不存在悬空指针重新赋值的问题。
二、引用 vs 指针
引用和指针都能"间接操作变量",但本质完全不同:
#include <iostream>
using namespace std;
int main() {
int a = 10, b = 20;
// 指针
int* p = &a; // p 存储 a 的地址
cout << *p << endl; // 10(解引用)
p = &b; // 可以让 p 指向另一个变量
cout << *p << endl; // 20
if (p != nullptr) { // 指针可以为空,使用前要检查
cout << *p << endl;
}
// 引用
int& r = a; // r 是 a 的别名
cout << r << endl; // 10
r = b; // 注意!这是赋值,不是重新绑定
cout << a << endl; // 20(a 被改成 20,r 仍然引用 a)
// 没有"引用为空"的概念
return 0;
}对比表:
- 初始化:引用必须初始化;指针可以不初始化(危险)。
- 重新绑定:引用不能;指针可以反复改指向。
- 空状态:引用没有;指针有 nullptr。
- 语法:引用直接用,无需解引用
*;指针要*p。 - 多级:指针有
int**(指向指针的指针);引用没有"引用的引用"。 - 算术:指针可以做
p+1(移动到下一个元素);引用不能。
何时用引用:函数参数(避免拷贝、要修改)、返回值。 何时用指针:数组遍历、可能为空、需要重新指向。能引用就不指针。
三、引用的经典应用:swap
对比指针和引用两种 swap 实现,引用明显更优雅:
#include <iostream>
using namespace std;
// 用指针实现的 swap
void swapP(int* a, int* b) {
int tmp = *a;
*a = *b;
*b = tmp;
}
// 用引用实现的 swap(更优雅)
void swapR(int& a, int& b) {
int tmp = a;
a = b;
b = tmp;
}
int main() {
int x = 1, y = 2;
swapP(&x, &y); // 调用时要传地址
cout << x << " " << y << endl; // 2 1
int m = 5, n = 6;
swapR(m, n); // 调用时直接传变量
cout << m << " " << n << endl; // 6 5
return 0;
}引用的优势一目了然:
- 调用更简洁:直接传变量,不需要取地址
&。 - 函数体内更清晰:不用
*解引用,代码读起来像普通变量。 - 类型安全:不会被传空指针崩溃。
四、const 引用:避免拷贝的利器
这是引用最常见的工程用途:
#include <iostream>
#include <string>
using namespace std;
// const 引用最常见的用途:避免拷贝大对象
// 比如传 string/vector/自定义类
void print(const string& s) {
// s = "modified"; // 编译错误:const 不能修改
cout << s << endl;
}
// const 引用还能绑定到临时对象(右值)
// 非 const 引用不能绑定右值
int main() {
string big = "very long string ..........................";
print(big); // 传引用,零拷贝
const int& r = 42; // OK:const 引用绑定字面量
cout << r << endl; // 42
const string& s = "hi"; // OK:const 引用绑定字符串字面量
cout << s << endl; // hi
return 0;
}const 引用的两个关键点:
- 函数参数"只读不拷贝":这是 C++ 性能优化的第一原则。大型对象(string、vector、自定义类)按值传会拷贝,改用
const T&后零拷贝。 - 能绑定右值:const 引用是唯一能绑定到临时对象(右值)的引用类型。这就是为什么
void f(const string& s)可以接受f("hello")。
记住口诀:小类型(int/double)按值,大类型(string/vector)按 const 引用。
五、函数返回引用:能当左值
返回引用可以让函数调用结果当左值用,但不能返回局部变量的引用:
#include <iostream>
#include <vector>
using namespace std;
// 危险:返回局部变量的引用(悬空引用)
int& bad() {
int x = 42;
return x; // 函数返回后 x 被销毁,引用指向无效内存!
}
// 安全:返回静态/全局变量的引用
int& counter() {
static int c = 0;
c++;
return c;
}
// 安全:返回参数中传入的引用
int& select(int& a, int& b) {
return a > b ? a : b;
}
int main() {
// int& r = bad(); // 编译警告,运行时未定义行为
// cout << r << endl;
counter() = 100; // 返回引用可以当左值!把 counter() 改成 100
cout << counter() << endl; // 101
int x = 10, y = 20;
select(x, y) = 0; // 把较大的 y 改成 0
cout << y << endl; // 0
return 0;
}悬空引用(dangling reference)是 C++ 的经典坑:
- 安全:返回静态/全局变量、传入的引用、堆上对象的引用。
- 危险:返回局部变量、临时对象。函数返回后这些就被销毁了,引用指向无效内存。
- 编译器会警告(
-Wreturn-stack-address),不要忽略!
六、右值引用与 std::move(C++11)
这是 C++11 引入的重要概念,主要用于移动语义(性能优化)。简单理解:
#include <iostream>
#include <string>
#include <utility> // std::move
using namespace std;
// C++11 引入右值引用(&&),用于实现"移动语义"
// - 左值(lvalue):有名字、有地址,可以取地址。如变量名 x
// - 右值(rvalue):没名字、临时存在。如字面量 42、表达式 x+y
void takeLvalue(int& x) {
cout << "lvalue ref: " << x << endl;
}
void takeRvalue(int&& x) {
cout << "rvalue ref: " << x << endl;
}
int main() {
int a = 10;
takeLvalue(a); // OK:a 是左值
// takeLvalue(42); // 错误:42 是右值,不能绑定到非 const 左值引用
// takeRvalue(a); // 错误:a 是左值,不能绑定到右值引用
takeRvalue(42); // OK:42 是右值
takeRvalue(a + 1); // OK:a+1 是右值
// std::move:把左值"强转"成右值,允许移动
string s1 = "hello";
string s2 = move(s1); // s1 的内容被"搬走"给 s2
cout << s2 << endl; // hello
cout << s1.size() << endl; // 0(s1 进入"有效但未指定"状态)
return 0;
}左值和右值的区分:
- 左值(lvalue):有名字、有地址、可以反复取用的对象。如
int a中的 a。 - 右值(rvalue):临时的、即将销毁的值。如字面量
42、表达式a + 1、函数返回的非引用值。 - 左值引用
T&:绑定左值。 - 右值引用
T&&:绑定右值(C++11 新增)。 - std::move(x):把左值强转为右值,表示"我不再需要 x 了,你可以搬空它"。
七、为什么要有移动语义?
这是 C++11 最重要的性能优化:
#include <iostream>
#include <vector>
#include <string>
using namespace std;
// 假设 vector 内部存了一个大缓冲区(比如 1MB)
// 拷贝:重新分配 + 复制 1MB 数据,很慢
// 移动:只把指针挪一下,几乎零成本
vector<string> makeData() {
vector<string> data(100000, "long string ...");
return data; // 返回值:编译器会用移动语义(RVO/NRVO 优化)
}
int main() {
vector<string> v1 = {string(1000000, 'x')}; // 1MB 字符串
vector<string> v2 = v1; // 拷贝:复制 1MB,慢
vector<string> v3 = move(v1); // 移动:只搬指针,飞快,v1 变空
return 0;
}
// 总结:右值引用 + move 语义是 C++11 的核心性能优化
// 让你写"返回大对象"的代码无需担心性能代价对比拷贝和移动:
- 拷贝:复制整个内部数据(比如 1MB 字符串),慢。
- 移动:只把内部指针"搬"过去,几乎零成本。
移动语义的实际用途:
- 返回大对象:函数返回 vector、string 时,编译器自动用移动语义。
- 容器存储:把对象 push_back 到 vector,可以用
move(obj)避免拷贝。 - 智能指针转移所有权:unique_ptr 只能移动,不能拷贝。
新手不用一上来就精通移动语义,先记住:函数返回大对象时直接 return,编译器会自动优化。
八、引用总结
- 左值引用
T&:普通引用,绑定左值,可以修改原对象。 - const 引用
const T&:只读引用,能绑定左值和右值,函数参数首选。 - 右值引用
T&&:C++11,绑定右值,用于移动语义。 - 引用必须初始化、不可改绑定、不为空——这让它比指针安全。
- 能引用就不指针,但数组遍历、可能为空、需要重新指向时用指针。
小结
这一章你认识了 C++ 的引用,知道了它和指针的本质区别(必须初始化/不可改绑定/不为空),学会了用 const 引用做高效参数,接触了右值引用和移动语义。下一篇进入 C++ 的核心——类与对象,开始面向对象编程。
← 上一篇 C++ 函数
下一篇 C++ 类与对象 →