🌈欢迎来到C++专栏~~右值引用和移动语义
- (꒪ꇴ꒪(꒪ꇴ꒪ )🐣,我是Scort
- 目前状态:大三非科班啃C++中
- 🌍博客主页:张小姐的猫~江湖背景
- 快上车🚘,握好方向盘跟我有一起打天下嘞!
- 送给自己的一句鸡汤🤔:
- 🔥真正的大师永远怀着一颗学徒的心
- 作者水平很有限,如果发现错误,可在评论区指正,感谢🙏
- 🎉🎉欢迎持续关注!
![]()


什么是左值?
左值是一个表示数据的表达式(如 变量名或 解引用的指针)
int main()
{
//左值:可以取地址
int a = 10;
const int b = 20;//const不能修改(例外)
int* p = &a;
*p = 10;
return 0;
}
什么是右值?
右值也是一个表示数据的表达式,如字母常量、表达式的返回值、函数的返回值(不能是左值引用返回)等等
int main()
{
double x = 1.1, y = 2.2;
//以下几个都是常见的右值,不能取地址
10;
x + y;
fmin(x, y);
//错误示例(右值不能出现在赋值符号的左边)
//10 = 1;
//x + y = 1;
//fmin(x, y) = 1;
return 0;
}
C++11中新增了右值引用的语法特性,为了进行区分,于是将C++11之前的引用就叫做左值引用。但是无论左值引用还是右值引用,本质都是给对象取别名
左值引用
左值引用就是对左值的引用,给左值取别名,通过“&”来声明
int main()
{
//以下的p、b、c、*p都是左值
int* p = new int(0);
int b = 1;
const int c = 2;
//以下几个是对上面左值的左值引用
int*& rp = p;
int& rb = b;
const int& rc = c;
int& pvalue = *p;
return 0;
}
右值引用
右值引用就是对右值的引用,给右值取别名,通过“&&”来声明
int main()
{
double x = 1.1, y = 2.2;
//以下几个都是常见的右值
10;
x + y;
fmin(x, y);
//以下几个都是对右值的右值引用
int&& rr1 = 10;
double&& rr2 = x + y;
double&& rr3 = fmin(x, y);
return 0;
}
要注意的是
左值引用可以引用右值吗?
那就是const左值引用既可以引用左值,也可以引用右值,这样的其实我们已经见多了
templatevoid func(const T& x)//x既能接收左值,也能接收右值 { cout << x<< endl; } int main() { string s("hello"); func(s); //s为左值 func("world"); //"world"为右值 return 0; }
🎃奇怪的现象
右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,这时这个右值可以被取到地址,并且可以被修改,如果不想让被引用的右值被修改,可以用const修饰右值引用
int main()
{
double x = 1.1, y = 2.2;
int&& rr1 = 10;
const double&& rr2 = x + y;
rr1 = 20;
rr2 = 5.5; //报错
return 0;
}
此处的rr1就被转化成左值了,埋下伏笔哈哈哈,后面会遇到
右值引用可以引用左值吗?
move函数是C++11标准提供的一个函数,被move后的左值能够赋值给右值引用(斯国一)
int main()
{
int a = 10;
//int&& r1 = a; //右值引用不能引用左值
int&& r2 = move(a); //右值引用可以引用move以后的左值
return 0;
}
我们先来看看左值引用的使用场景:
左值引用虽然能避免不必要的拷贝操作,但左值引用并不能完全避免
💥短板:如果函数返回的是一个局部对象,该变量出了函数作用域就被销毁了,这种情况下不能用左值引用作为返回值,只能以传值的方式返回,这就是左值引用的短板。
还好之前写了博客复习:复习传送门
举个例子:int版本的to_string函数,这个to_string函数就不能使用左值引用返回,因为to_string函数返回的是一个局部变量(出作用域销毁了)
namespace ljj
{
cl::string to_string(int value)
{
bool flag = true;
if (value < 0)
{
flag = false;
value = 0 - value;
}
cl::string str;
while (value > 0)
{
int x = value % 10;
value /= 10;
str += (x + '0');
}
if (flag == false)
{
str += '-';
}
std::reverse(str.begin(), str.end());
return str;
}
}
此时调用to_string函数返回时,就一定会调用string的拷贝构造函数
int main()
{
ljj::string ret = to_string(3465);
return 0;
}
为此C++11就出手了,提出右值引用就是为了解决左值引用的这个短板的!
那怎么样才能让编译器不优化,我们手动操作呢?那就要增加移动构造和移动赋值方法
ps:C++11对右值进行了划分
移动构造是一个构造函数,该构造函数的参数是右值引用类型的,移动构造本质就是将传入右值的资源窃取过来,占为己有,这样就避免了进行深拷贝,所以它叫做移动构造,就是窃取别人的资源来构造自己的意思
调用swap函数将传入右值的资源窃取过来,占为己有
//移动构造
string(string&& s)//右值引用
:_str(nullptr)
, _size(0)
, _capacity(0)
{
cout << "string(string&& s) -- 资源转移" << endl;
swap(s); //资源互换
}

移动构造的价值
我们来看看编译器的优化:
当一个函数在返回局部对象时,会先用这个局部对象拷贝构造出一个临时对象,然后再用这个临时对象来拷贝构造我们接收返回值的对象(深拷贝)

编译器会优化成:一步到位:只需要一次拷贝构造,还要什么临时对象,我懂你意思,直接给给ret

在C++11标准出来之前这里应该调用两次string的拷贝构造函数,但最终被编译器优化成了一次,减少了一次无意义的深拷贝。(并不是所有的编译器都做了这个优化)
C++11出来后,编译器仍然保持了这种优化方式
“将亡值”str马上就要被销毁了,那还不如把它的资源转移给别人用,因此编译器在识别这种“将亡值”时会将其识别为右值,这样就可以匹配到参数类型为右值引用的移动构造函数

可以理解成:
记住记住:右值引用swap()的是将亡值,拷贝构造中不能直接swap,因为对象不是将亡值,下面的例子中,swap完后s1就销毁了,那我们不可以这样做
int main()
{
ljj::string s1("1111111");
ljj::string s2(s1);
return 0;
}
😎移动赋值是一个赋值运算符重载函数,该函数的参数是右值引用类型的,移动赋值也是将传入右值的资源窃取过来,占为己有,这样就避免了深拷贝,所以它叫移动赋值,就是窃取别人的资源来赋值给自己的意思
编译器并没有对这种情况进行优化,因此在C++11标准出来之前,对于深拷贝的类来说这里就会存在两次深拷贝,因为深拷贝的类的赋值运算符重载函数也需要以深拷贝的方式实现
//移动赋值
string& operator=(string&& s)
{
cout << "string& operator=(const string&& s) -- 移动赋值" << endl;
swap(s);
return *this; //返回左值(支持连续赋值)
}
移动赋值的优势:

此时当to_string函数返回局部的string对象时,会先调用移动构造生成一个临时对象,然后再调用移动赋值将临时对象的资源转移给我们接收返回值的对象,这个过程虽然调用了两个函数,但这两个函数要做的只是资源的移动,而不需要进行深拷贝,大大提高了效率
延长了资源的生命周期

C++11标准出来之后,STL中的容器都增加了移动构造和移动赋值
以string类为例,这是string类增加的移动构造:

这是string类增加的移动赋值:

字面上是不可以的,但也不是完全不可以,当需要用右值引用引用一个左值时,可以通过move函数将左值转化为右值
move函数的名字具有迷惑性,move函数实际并不能搬移任何东西,该函数唯一的功能就是将一个左值强制转化为右值引用,然后实现移动语义
move定义如下:
templateinline typename remove_reference<_Ty>::type&& move(_Ty&& _Arg) _NOEXCEPT { //forward _Arg as movable return ((typename remove_reference<_Ty>::type&&)_Arg); }
右值引用版本的插入函数
C++11标准出来之后,STL容器插入接口函数也增加了右值引用版本

右值引用版本的意义:
如果vector容器当中存储的是string对象,那么在调用push_back向vector容器中插入元素
int main()
{
vector v;
ljj::string s1("hello");
v.push_back(s1);//调用string的拷贝构造
cout << "——————————————————————————————————" << endl;
v.push_back("hello");//调用string的移动构造
return 0;
}
push_back函数需要先构造一个结点(在内存池中定位new),然后将该结点插入到底层的双链表当中

模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值
templatevoid PerfectForward(T&& t) { //... }
万能引用是根据传入实参的类型进行推导,如果传入的实参是一个左值,那么这里的形参t就是左值引用,如果传入的实参是一个右值,那么这里的形参t就是右值引用
举个例子:
void Func(int& x)
{
cout << "左值引用" << endl;
}
void Func(const int& x)
{
cout << "const 左值引用" << endl;
}
void Func(int&& x)
{
cout << "右值引用" << endl;
}
void Func(const int&& x)
{
cout << "const 右值引用" << endl;
}
template
void PerfectForward(T&& t)
{
Func(t);
}
int main()
{
int a = 10;
PerfectForward(a); //左值
PerfectForward(move(a)); //右值
const int b = 20;
PerfectForward(b); //const 左值
PerfectForward(move(b)); //const 右值
return 0;
}
PerfectForward函数时传入左值、右值、const左值、const右值,结果输出的全是左值,为什么呢?

就是说,右值经过一次参数传递后其属性会退化成左值,如果想要在这个过程中保持右值的属性,就需要用到完美转发
要想在参数传递过程中保持其原有的属性,需要在传参时调用forward函数
templatevoid PerfectForward(T&& t) { //完美转发:保持t引用的属性 Func(std::forward (t)); }

一个简化版的list类,类当中分别提供了左值引用版本和右值引用版本的push_back和insert函数
namespace ljj
{
template
struct ListNode
{
T _data;
ListNode* _next = nullptr;
ListNode* _prev = nullptr;
};
template
class list
{
typedef ListNode node;
public:
//构造函数
list()
{
_head = new node;
_head->_next = _head;
_head->_prev = _head;
}
//左值引用版本的push_back
void push_back(const T& x)
{
insert(_head, x);
}
//右值引用版本的push_back
void push_back(T&& x)
{
insert(_head, std::forward(x)); //完美转发
}
//左值引用版本的insert
void insert(node* pos, const T& x)
{
node* prev = pos->_prev;
node* newnode = new node;
newnode->_data = x;
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = pos;
pos->_prev = newnode;
}
//右值引用版本的insert
void insert(node* pos, T&& x)
{
node* prev = pos->_prev;
node* newnode = new node;
newnode->_data = std::forward(x); //完美转发
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = pos;
pos->_prev = newnode;
}
private:
node* _head; //指向链表头结点的指针
};
}
只要右值每往下一层传,都要完美转发,否则统统变成左值
分别传入左值和右值调用不同版本的push_back
int main()
{
ljj::list lt;
ljj::string s("1111");
lt.push_back(s); //调用左值引用
lt.push_back("2222"); //调用右值引用
return 0;
}
ps:代码中push_back和insert函数的参数T&&是右值引用,而不是万能引用,因为在list对象创建时这个类就被实例化了,后续调用push_back和insert函数时,参数T&&中的T已经是一个确定的类型了
中国奇谭真不戳

上一篇:决策树的相关知识点