41. 值传递、左值引用传递、右值引用传递,该如何选择?
这个问题,说实话,是每个C++开发者都会遇到的灵魂拷问。我记得刚入行那会儿,看到同事代码里各种&、&&、还有直接传值,整个人都是懵的。后来踩了不少坑,才慢慢摸清楚门道。
今天我就把这几年的经验梳理一下。说白了,选择哪种传递方式,核心就三个字:所有权。你想让谁拥有这个对象?是调用者自己留着,还是交给函数,还是借给函数用一下?想清楚这个,选择就简单了。
三种传递方式的核心区别
我们先看一张图,把三者的关系理清楚:
什么时候用值传递?
值传递,说白了就是「复制一份给你」。调用者把对象拷贝一份丢给函数,函数随便改,原对象纹丝不动。
我个人习惯:小对象用值传递。 比如int、char、bool这些内置类型,或者很小的结构体(不超过两个指针大小)。为什么?因为拷贝它们的成本几乎为零,甚至比传递指针还快——省去了间接寻址的开销。
// 小对象,值传递没问题
void setWidth(int w) {
width_ = w;
}
// 大对象,别这么干!
void processLargeData(std::vector<int> data) { // 拷贝了整个vector!
// ...
}
什么时候用左值引用?
左值引用,就是「借给你看看,别乱改」。const T& 是最安全的——只读不写。T& 则是「借给你,你可以改,但改完得还我」。
我建议:只要函数不需要修改参数,就用 const T&。这是最通用的选择,既能接受左值,也能接受右值(右值可以绑定到const引用)。
// 只读访问,用 const T&
void printName(const std::string& name) {
std::cout << name << std::endl; // 只读,不拷贝
}
// 需要修改,用 T&
void appendSuffix(std::string& str) {
str += "_suffix"; // 修改原对象
}
什么时候用右值引用?
右值引用,就是「你用完就扔的东西,让我来接手」。它的核心价值在于移动语义——把资源从临时对象里「偷」过来,避免深拷贝。
我记得有一次优化一个日志系统,每条日志都要拼接字符串。原来用的是 const std::string&,每次都要拷贝。改成 std::string&& 后,临时字符串直接移动进来,性能提升了30%多。
// 移动构造函数 - 右值引用的经典用法
class Buffer {
public:
Buffer(Buffer&& other) noexcept
: data_(other.data_), size_(other.size_) {
other.data_ = nullptr; // 窃取资源
other.size_ = 0;
}
private:
char* data_;
size_t size_;
};
// 完美转发 - 右值引用的进阶用法
template<typename T>
void wrapper(T&& arg) {
target(std::forward<T>(arg)); // 保持值类别
}
核心原则: 右值引用只用于两种情况:
- 移动构造函数/移动赋值运算符 - 实现资源转移
- 完美转发 - 保持参数的左值/右值属性
其他场景,别滥用右值引用。我见过有人把普通函数参数写成 T&&,结果调用方传左值还得 std::move,代码变得很难看。
实战选择指南
说了这么多,到底怎么选?我总结了一个简单的决策流程:
| 场景 | 推荐方式 | 原因 |
|---|---|---|
| 小对象(≤ 16字节) | 值传递 | 拷贝成本低,甚至比引用更快 |
| 大对象,只读 | const T& | 避免拷贝,安全高效 |
| 大对象,需要修改 | T& | 修改原对象,不产生副本 |
| 临时对象,资源转移 | T&& | 移动语义,零拷贝 |
| 模板函数,保持值类别 | T&& + std::forward | 完美转发,保留左值/右值属性 |
| 需要副本,但可能移动 | 值传递(然后 std::move) | 调用方传左值时拷贝,传右值时移动 |
一个常见的坑
我曾经在代码审查里看到这样的写法:
// ❌ 错误示范
void process(std::vector<int>&& data) {
// 处理 data...
}
// 调用方
std::vector<int> v = {1, 2, 3};
process(std::move(v)); // 必须显式 move
// 之后 v 的状态是未定义的!
这种写法的问题在于:它强迫调用方必须传右值。如果调用方想传左值,还得自己 std::move,而且 move 之后原对象就废了。这很不友好。
更好的做法是:
// ✅ 推荐做法
void process(std::vector<int> data) {
// 处理 data...
}
// 调用方
std::vector<int> v = {1, 2, 3};
process(v); // 拷贝
process(std::move(v)); // 移动(如果 vector 支持)
process({1, 2, 3}); // 直接构造
你看,值传递反而更灵活。调用方可以自由选择拷贝还是移动。这就是为什么我常说:「值传递是万金油,引用传递是手术刀」。小场景用万金油,大场景才动手术刀。
总结一下
嗯,说了这么多,其实就一句话:先想清楚所有权,再选传递方式。
- 调用者保留所有权,只借给你看 → const T&
- 调用者保留所有权,让你改 → T&
- 调用者放弃所有权,让你接管 → T&& 或 值传递
- 调用者不确定,让你自己决定 → 值传递
我在实际项目中,90% 的场景用 const T& 和值传递就搞定了。右值引用主要用在移动构造函数和完美转发里。别为了炫技而用右值引用,代码的可读性比性能更重要——除非你确定那真的是热点路径。