我们在一开始学习c++时就学习过引用的语法,当时我们将引用这一语法理解为给变量起别名。在c++11当中新增了右值引用语法特性,无论是左值引用还是右值引用,都是给对象起别名。注意,要摒弃一个误区,不能简单的认为在赋值号左边的就叫左值,右边的就叫右值,实际上左值和右值的界定需要参照以下定义:
左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址,一般可以对它赋值,左值可以出现在赋值符号的左边,右值不能出现在赋值符号的左边.定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址,因此还是左值.
左值引用就是给左值的引用,给左值取别名.如:
//左值引用
int a = 0;
int& r1 = a; //给a取别名为r1
右值是一个表示数据的表达式,如:字面常量, 表达式返回值, 函数返回值(这个不能是左值引用返回)等等,右值可以出现在赋值符号的右边,但是不能出现在赋值符号的左边,右值不能取地址.
右值引用就是对右值的引用,给右值取别名.如:
//右值引用
int&& r5 = 10; //给10取别名为r5
double x = 1.1, y = 2.2;
double&& r6 = x + y; //给表达式x+y取别名为r6
/左值引用引用右值
double x = 2.2;
double y = 3.3;
const int& r2 = 10;
const double& r3 = x + y;//这里x和y都是左值,但是x+y表达式返回的结果5是一个临时变量是右值
//右值引用引用左值
int a = 10;
int&& r7 = move(a);
也就是说,正常情况下左值只能引用左值, 右值只能引用右值, 但是const左值可以引用右值,右值可以引用move后的左值。
左值引用使用场景:
做参数
void swap(int& a,int&b) //左值引用可以直接修改原对象,减少参数传递时的拷贝
{
int tmp = a;
a = b;
b = tmp;
}
int main()
{
int x = 2;
int y = 3;
swap(x,y);
return 0;
}
做返回值
//左值引用可以直接修改返回值,同时减少了函数传值返回的拷贝
int& get(size_t pos)
{
return data[pos];
}
左值引用意义: 减少拷贝,并可以直接修改原对象
左值引用的缺点:但是当函数返回对象是一个局部变量,出了函数作用域就不存在了,就不能使用左值引用返回,只能传值返回。例如:
函数中可以看到,这里只能使用传值返回,传值返回会导致至少1次拷贝构造(如果是一些旧一点的编译器可能是两次拷贝构造)。
通过上面我们对左值引用使用场景和意义的分析,我们得知了左值引用的短板。因此C++的大佬们就引入了右值引用和移动语义来解决这个问题:移动语义包括移动构造和移动赋值,我们先来看移动构造:
移动构造本质是将参数右值的资源窃取过来,占为已有,那么就不用做深拷贝了,所以它叫做移动构造,就是窃取别人的资源来构造自己:
而移动赋值也是将赋值运算符右边的右值资源窃取过来,占为己有,也就不用再做深拷贝了:
基于上面的概念,实现的string类移动构造和移动赋值函数如下:
//移动构造
string(string&& s)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
swap(s);
}
//移动赋值
string& operator=(string&& s)
{
swap(s);
return *this;
}
有些场景下,我们可能需要用右值去引用左值实现移动语义。当需要用右值引用引用一个左值时,可以通过move函数将左值转化为右值。
int main()
{
string s1("hello world");
// 这里s1是左值,调用的是拷贝构造
string s2(s1);
// 这里我们把s1 move处理以后, 会被当成右值,调用移动构造
// 但是这里要注意,一般是不要这样用的,因为我们会发现s1的
// 资源被转移给了s3,s1被置空了。
string s3(std::move(s1));
return 0;
}
完美转发(Perfect Forwarding) 是 C++11 引入的核心特性之一,用于在泛型编程中精确传递参数的左值/右值属性,避免不必要的拷贝或类型损失。它结合了 右值引用、万能引用(Universal Reference) 和
std::forward
实现。
假设有一个泛型函数 wrapper
,需要将参数转发给另一个函数 target
:
template<typename T>
void wrapper(T arg)
{
target(arg); // 直接传递参数
}
问题:
arg
是左值还是右值,target(arg)
接收的始终是左值(因为右值引用本身是左值, 如果右值引用本身是右值那么就没法移动语义了)所以左值引用和右值引用传递到下层都变成了左值引用。
arg
是临时对象(右值),无法触发移动语义,可能导致深拷贝。
右值引用默认是左值,我们才能基于此实现移动语义:
但是如果不支持完美转发的话,右值引用无法保持右值属性,那么我们遇到嵌套容器深拷贝的情况就没法用移动语义:
1. 万能引用(Universal Reference)
T&&
,且 T
需要被推导(如函数模板或 auto
)。
template<typename T>
void wrapper(T&& arg) { // arg 是万能引用
// 模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。
// 模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力,
// 但是引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值,
// 我们希望能够在传递过程中保持它的左值或者右值的属性, 就需要用我们下面学习的完美转发
}
2. std::forward<T>
T
的原始类型(左值或右值),将参数有条件地转换回原始类型。
T
是左值引用,返回左值;否则返回右值引用(触发移动语义)。
#include <iostream>
#include <utility> // std::forward
// 目标函数
void target(int& x) { std::cout << "左值: " << x << std::endl; }
void target(int&& x) { std::cout << "右值: " << x << std::endl; }
// 完美转发的包装函数
template<typename T>
void wrapper(T&& arg)
{
target(std::forward<T>(arg)); // 关键:保留参数的原始类型
}
int main()
{
int a = 10;
wrapper(a); // 传递左值 → 调用 target(int&)
wrapper(20); // 传递右值 → 调用 target(int&&)
wrapper(std::move(a)); // 显式转为右值 → 调用 target(int&&)
return 0;
}
希望这篇关于 C++11之左值引用,右值引用和移动语义 的博客能对大家有所帮助,欢迎大佬们留言或私信与我交流.
学海漫浩浩,我亦苦作舟!关注我,大家一起学习,一起进步!