C++11 左值右值引用区别与移动语义优化传值返回
C++11 引入右值引用和移动语义解决传值返回时的对象拷贝开销问题。通过列表初始化规范对象创建,利用 const 引用延长临时对象生命周期。左值引用绑定可寻址对象,右值引用绑定临时资源。编译器优先调用移动构造和移动赋值替代拷贝操作,显著减少深拷贝类型如 vector 和 string 的性能损耗。引用折叠机制允许模板函数同时处理左值和右值参数,实现完美转发。

C++11 引入右值引用和移动语义解决传值返回时的对象拷贝开销问题。通过列表初始化规范对象创建,利用 const 引用延长临时对象生命周期。左值引用绑定可寻址对象,右值引用绑定临时资源。编译器优先调用移动构造和移动赋值替代拷贝操作,显著减少深拷贝类型如 vector 和 string 的性能损耗。引用折叠机制允许模板函数同时处理左值和右值参数,实现完美转发。

int x{2};
int x1 = 2;
// 自定义类型(类)的列表初始化
Date d1 = {2025, 11, 01};
Date d2{2025, 05, 28};
{2025, 12, 12} 会先构造一个临时对象,然后将这个临时对象的引用绑定到 const 引用上。因为临时对象的生命周期会被延长,与 const 引用的生命周期一致,所以这种写法是合法的。const Date& d3 = {2025, 12, 12};
const Date& d4{2025, 12, 12};
// Date& d4{2025, 12, 12};// 报错:非 const 引用不能绑定临时对象
问题:const Date& d4(2025, 10, 10) 为什么一定要加 const? 如果不加 const,就是费 const 引用,会报错。原因在于非 const 引用意味着可以修改内部的数据,但是受生命周期影响,被引用的内容出了作用域就会销毁,再去修改内部的数据,就会报错。加 const 一来可以防止内部的数据被修改,二来可以让引用一直坚持到生命周期结束再销毁。
总结: 临时对象匿名对象的生命周期都只在一行,const 引用可以延长临时对象 + 匿名对象的生命周期。
注意一个小点:只有 { } 初始化才可以省略 =。像 Date d{2025}; 一定会报错(如果构造函数不匹配)。
局部对象和被 const 引用延长生命周期的临时对象,析构顺序与构造顺序相反(即'先构造的后析构,后构造的先析构')。 在这段代码中,构造顺序大致是:d1 → d2 → d3 → d4;因此析构顺序是:d4 → d3 → d2 → d1。
vector<int> v1 = {1, 2, 3, 4, 5, 6};
vector<int> v2{1, 2, 3, 4, 5, 6};
const vector<int>& v3 = {9, 8, 7, 6, 5, 4};
const vector<int>& v4({9, 8, 7, 6, 5, 4});
// 构造 + 拷贝构造 + 优化
vector<int> v5({6, 7, 8});
// ({6, 7, 8}) 通过列表构造函数创建临时对象,然后用该临时对象直接构造 v5。
// 编译器会触发拷贝构造优化(返回值优化,RVO),避免临时对象的拷贝,直接在 v5 的内存地址上构造对象,最终等价于一次构造。
map<string, int> map1 = {{"apple", 5}, {"blue", 9}};
// map<string, int> & map2({ "apple", 5 }, { "blue" , 9 });报错没有匹配的构造函数的类型
左值和右值的区别:右值不可以取地址,左值可以取地址。 左值引用给左值取别名,右值引用给右值取别名。
// 左值引用给左值取别名
int& r1 = b;
int*& r2 = p;
int& r3 = *p;
string& r4 = s;
char& r5 = s[0];
// 右值引用给右值取别名
int&& rr1 = 10;
double&& rr2 = x + y;
double&& rr3 = fmin(x, y);
string&& rr4 = string("11111");
左值不能直接引用右值,但是加了 const 的左值可以引用;右值也不能直接引用左值,但是可以引用 move 左值。补充:move 的本质就是进行强制类型转换。
注意:从语义和内存本质的角度,左值引用和右值引用都是'别名',本身不额外开辟新的对象存储空间,在底层实现上通常以指针的形式存在(汇编层面会体现为指针操作)。右值引用可以给右值起别名,但右值引用变量本身是左值属性。所以变量表达式都是左值属性。
总结:右值引用可以延长被引用对象的生命周期,被引用对象可以通过非 const 的引用修改;左值引用不能延长对象生命周期,但是 const 左值引用可以延长生命周期,被引用对象不能通过 const 的引用修改。
int main() {
std::string s1 = "happy";
std::string&& a1 = std::move(s1);
const std::string& a2 = s1 + s1; // 到 const 的左值引用延长生命周期
// a2 += "happy"; // error: 没有与这些操作数匹配的 "+=" 运算符
// 错误:不能通过到 const 的引用修改
std::cout << a2 << '\n'; // print: happyhappy
std::string&& a3 = s1 + s1; // 右值引用延长生存期
a3 += "exercise"; // 能通过到非 const 的引用修改
std::cout << a3 << '\n'; // print: happyhappyexercise
return 0;
}
C++11 以后,分别重载左值引用、const 左值引用、右值引用作为形参的 f 函数,那么实参是左值会匹配 f(左值引用),实参是 const 左值会匹配 f(const 左值引用),实参是右值会匹配 f(右值引用)。
#include <iostream>
#include <string>
using namespace std;
void f(int& x) { cout << "左值引用重载调用" << endl; }
void f(const int& x) { cout << "const 左值引用重载调用" << endl; }
void f(int&& x) { cout << "右值引用重载调用" << endl; }
int main() {
int i = 1;
const int ci = 2;
int&& x = 3;
f(1); // print: 右值引用重载调用 (实际匹配 f(int&&))
f(ci); // print: const 左值引用重载调用
f(x); // print: 左值引用重载调用
f(std::move(x)); // print: 右值引用重载调用
return 0;
}
面试题插入:左值引用和右值引用的区别: 左值引用是'对象的别名',用于共享访问和避免拷贝;右值引用是'临时资源的所有权标识',用于移动语义和完美转发,是 C++11 后提升性能的关键特性。左值引用和右值引用的目的都是减少拷贝,提高效率。
优点:左值引用可以修改参数/返回值,方便使用; 缺点:但是在有些场景下,左值引用在对象函数栈帧中结束了会销毁,不能使用左值引用返回,当前函数局部对象,出了当前函数作用域生命周期到了就销毁了,不能用左值返回,只能传值返回。
问题:那么如何解决只能传值返回,但是返回对象在函数栈帧结束之后会销毁的问题?
解决场景: 方案一:不用返回值,用输出型参数。不足:一定程度上牺牲了可读性; 方案二:编译器优化; 方案三:新标准新语法处理(右值引用),而右值引用可以延长函数对象的生命周期。
编译器不优化的场景是拷贝构造 + 拷贝赋值,VS2019 debug 版本传参有优化,对于 ret 赋值的没有优化到还是有拷贝构造和拷贝赋值,二代优化只有一次拷贝赋值,没有出现临时对象,没有拷贝构造。将构造和拷贝构造合二为一。 C++11 之后彻底不优化,但是调用了移动赋值和移动构造,这样会直接调用移动构造和移动赋值,传值返回的代价约等于 0。
编译器如果不优化:先产生一个临时对象,str 函数哈哈栈帧结束后,临时对象接收 str 中存储的数据(一次拷贝构造),临时对象又会将内部存储的值拷贝构造给 main() 函数栈帧中的 ret。总体上来说是两次拷贝构造。
string addstring1(string nums1, string nums2) {
string str;
int end1 = nums1.size() - 1, end2 = nums2.size() - 1;
int next = 0;
while(end1 >= 0 || end2 >= 0) {
int sum = (end1 >= 0 ? nums1[end1--] - '0' : 0) +
(end2 >= 0 ? nums2[end2--] - '0' : 0) + next;
str += ('0' + sum % 10);
next = sum / 10;
}
if(next) str += ('0' + next);
reverse(str.begin(), str.end());
return str;
}
int main() {
string ret = addstring1("xndx", "lzdx");
cout << ret.c_str() << endl;
return 0;
}
不优化:会调用很多次构造先构造参数,每一次参数的构造完成,还会调用拷贝构造; 一代优化:合二为一,没有临时对象的产生,只产生了一次拷贝构造,构造 + 拷贝构造。 二代优化:合三为一,直接构造。
不优化版本下:产生一个临时对象,str 在函数栈帧结束之前将内部资源拷贝构造给临时对象,由临时对象拷贝赋值给 main 函数中的 ret。参数构造 + 拷贝构造,最后还有一次拷贝赋值。 一代优化:多个参数构造,一次拷贝构造 + 一次拷贝赋值; 二代优化:拷贝构造和构造合二为一,最后一次拷贝赋值。
不优化版本:str 先将内部资源移动构造给临时对象,main 函数中 ret 接收临时对象中的资源,也是移动构造,每一个参数构造 + 移动构造;
1 代优化:没有产生临时对象,编译器识别到 str 是'即将被返回的局部对象',会将其视为右值;执行 return str; 时,触发移动构造:直接将 str 的资源'转移'给临时对象(而非拷贝);进一步优化中,临时对象和 main 中的 ret 会'合二为一'(省略临时对象的构造),最终 str 的资源直接移动构造到 ret 中。此过程仅触发一次移动构造,资源转移效率极高(无额外拷贝)。构造参数 + 最终一次移动构造。
2 代优化(比如在 VS2022 编译器下)中甚至没有产生 str,只有 ret,也就是说 str 本质是 ret 对象的引用,实际上没有产生 str,一旦产生,函数栈帧销毁,局部对象 str 销毁,ret 就是野引用,所以 str 没有产生,其底层使用指针形式实现,这个时候打印出 str 和 ret 的地址会发现他们的地址是相同的。这时候会直接构造,没有移动构造。
不优化版本下:产生一个临时对象,str 在函数栈帧结束之前将内部资源拷贝构造给临时对象,由临时对象拷贝赋值给 main 函数中的 ret。参数构造 + 移动构造,最后还有一次移动赋值。 一代优化:多次构造,一次移动构造 + 一次移动赋值; 二代优化:构造和移动构造合二为一,只有构造和移动赋值。
总体来说:如果代码中有拷贝构造和拷贝赋值,也有移动构造和移动赋值,编译器一定会优先执行移动构造和移动赋值,因为选择的都是效率高的。当编译器升级 + C++ 支持移动构造和移动赋值,传值返回的效率变高。
问题:那么移动构造和移动赋值的效率这么高,需不需要在每一个类型中都实现? 对于深拷贝的自定义类型(vector/string/map…),实现移动构造和移动赋值的价值很大,一定程度上比拷贝构造和拷贝赋值的效率高很多。 对于浅拷贝的自定义类型(如 Date/pair<int,int>…) 不需要额外实现移动构造和移动赋值,因为浅拷贝的传值返回和移动构造移动赋值相比,浅拷贝没有指向资源,拷贝代价不大,效率差不多。
int main() {
typedef int& lref; // lref 是'int 的左值引用'类型
typedef int&& rref; // rref 是'int 的右值引用'类型。
int n = 0;
lref& r1 = n; // r1 的类型是 int&
lref&& r2 = n; // r2 的类型是 int&
rref& r3 = n; // r3 的类型是 int&
rref&& r4 = 1; // r4 的类型是 int&&
return 0;
}
总结:只要嵌套中存在左值引用(&),最终结果就是左值引用;只有纯右值引用(&&)嵌套时,才是右值引用。
// 由于引用折叠限定,f1 实例化以后总是一个左值引用
template<class T>
void f1(T& x) // 模板参数是左值引用,实例化的时候只能接受左值(n),无论 T 是什么类型,x 始终是左值引用(无折叠空间,因为参数已固定为&)。
{}
// 由于引用折叠限定,f2 实例化后可以是左值引用,也可以是右值引用
template<class T>
void f2(T&& x) // 模板参数是万能引用(T&&)
{}
f1(T& x):模板参数是左值引用,实例化时只能接受左值(如 n),且无论 T 推导为何,x 始终是左值引用。 f2(T&& x):模板参数是万能引用(T&&),实例化时:传入左值(如 n),T 推导为 int&,x 类型为 int& && → 折叠为 int&(左值引用)。传入右值(如 1),T 推导为 int,x 类型为 int&&(右值引用)。
结论:f1 只能处理左值引用,f2 可通过引用折叠同时处理左值和右值引用(万能引用的特性)。
总结:左值只能绑定左值引用,右值引用可以绑定右值引用和 const 左值引用。
// 由于引用折叠限定,f1 实例化以后总是一个左值引用
template<class T>
void f1(T& x)
{}
// 由于引用折叠限定,f2 实例化后可以是左值引用,也可以是右值引用
template<class T>
void f2(T&& x)
{}
int main() {
typedef int& lref;
typedef int&& rref;
int n = 0;
lref& r1 = n;
lref&& r2 = n;
rref& r3 = n;
rref&& r4 = 1;
// 没有折叠->实例化为 void f1(int& x)
f1<int>(n); // n 是左值,可绑定到 int&;
f1<int>(0); // 0 是右值,无法绑定到非 const 的 int& → 报错。
// 折叠 f1<int&>(n); // n 是左值,可以绑定到 int& &
f1<int&>(0); // 0 是右值,无法绑定,改正:f1<const int&>(0);
// 折叠 f1<int&&>(n); // T=int&& → 参数类型为 (int&&)&(引用折叠为 int&)。n(左值)可绑定到 int&;
f1<int&&>(0); // 0(右值)不可 → 后者报错。
// 折叠->实例化为 void f1(const int& x)
f1<const int&>(n); // T=const int& → 参数类型为 const int&(const 左值引用)。
f1<const int&>(0);
< &&>();
<>(n);
<>();
<&>(n);
<&>();
<&&>(n);
<&&>();
;
}
Function(T&& t) 函数模板程序中,假设实参是 int 右值,模板参数 T 的推导 int,实参是 int 左值,模板参数 T 的推导 int&,再结合引用折叠规则,就实现了实参是左值,实例化出左值引用版本形参的 Function,实参是右值,实例化出右值引用版本形参的 Function。
template<class T>
void Function(T && t) // 模板参数是万能引用(T&&)
{
int a = 0;
T x = a; // x++;// 所以 Function 内部会编译报错,x 不能++,error:不能给常量赋值
cout << &a << endl;
cout << &x << endl << endl;
}
int main() {
Function(10); // 10 是右值,推导出 T 为 int,模板实例化为 void Function(int&& t)
int a;
Function(a); // a 是左值,推导出 T 为 int&,引用折叠,模板实例化为 void Function(int& t)
Function(std::move(a)); // std::move(a) 是右值,推导出 T 为 int,模板实例化为 void Function(int&& t)
const int b = 8;
Function(b); // b 是左值,推导出 T 为 const int&, 引用折叠,模板实例化为 void Function(const int&& t)
Function(std::move(b)); // std::move(b) 右值,推导出 T 为 const int,模板实例化为 void Function(const int&&t)
return 0;
}

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online