目录
一.C语言中对左值和右值的定义
1.左值
2.右值
二.左值引用和右值引用
1.左值引用
2.右值引用
3.左值引用给右值取别名
4.右值引用给左值取别名
三.移动构造和移动赋值
1.移动赋值
2.移动拷贝
编辑编辑
四.完美转发
1.先看一道试题:
一.C语言中对左值和右值的定义
C语言-数据对象,左值,右值-CSDN博客
1.左值
定义:
左值是一个表示数据的表达式(比如变量名或解引用的指针)。
我们可以获取它的地址,一般可以对它进行赋值,左值可以出现在赋值符号的左边,右值不能出现在赋值符号的左边。
定义时const修饰后的左值,不能给他赋值,但是可以获取它的地址。
#include <stdio.h>
int main(void)
{
const int a = 10;
int b = a;
return 0;
}
对于int b = a;的解释:
我们知道a是一个左值,但是同时右值本身又不可以是左值,那么这个a放到赋值运算符右边合理吗?
其实这个是当我们将一个变量放到赋值运算符的右边时,会自动取出它的存储空间空间中的数据,而这个数据是右值。
2.右值
定义:右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引 用返回)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。
内置类型的右值 :纯右值。
自定义类型的右值:将亡值。
二.左值引用和右值引用
1.左值引用
定义:左值引用就是左值的引用,给左值取别名。
2.右值引用
定义:右值引用就是右值的引用,给右值取别名。
右值不可以被更改,但是右值引用可以被更改。
3.左值引用给右值取别名
a + b这个表达式的值属性是保存在一个临时变量中的,临时变量具有常性
4.右值引用给左值取别名
三.移动构造和移动赋值
1.移动赋值
string Func()
{
string str("xxxxxxxxxx");
return str;
}
int main(void)
{
string ret;
ret = Func();
return 0;
}
如果此时我们给的拷贝构造和赋值重载是这样的:
此时就像我们的过程图中描述的一样,数据在从str到ret中的过程中发生了多次深拷贝,拷贝到没有什么,我们的计算机对于拷贝的处理是非常的快的,但是每次深拷贝时都会开辟新的空间,这个过程效率就很低了。
解决办法:
将赋值重载函数,写一个重载函数:
此时程序的过程图就变为:
移动赋值:
内置类型的右值:将亡值。
将亡值:马上就要被清理的自定义类型的对象。
此时调用Func()产生的返回值(临时对象)就是一个将亡值。这个将亡值会被编译器处理成右值,所以此时ret=Func()会被调用
同时这个将亡值在这个表达式结束后就会被清理,但是这个临时对象中保存这我们想得到的数据,那么此时我们只需要将ret和这个临时对象中的数据进行互换就可以了。
此时经过上面的操作,最后的赋值操作中我们并没有开辟新的空间,同时又近似的实现了深拷贝的操作。
所以此时我们程序的效率提高了。
上面的代码并不会发生移动赋值的情况!因为rett并不是自定义类型的右值,如果此时发生移动赋值,如果在此之后我们在想使用rett对象时,此时就会产生错误。
但是:
这样程序是会发生移动赋值的,虽然move函数在调用完之后并不会改变rett对象本身是左值的属性,但是move函数的返回值可以让编译器认为此时rett对象是一个将亡值,可以发生移动赋值。
2.移动拷贝
在同一行代码中发生多次构造会被编译器优化为一次。
虽然就像上图说的一样,经过编译器的优化,已经提高了我们程序的效率的,但是此时可不可以先移动赋值一样发生交换呢,做到一个新空间都不开辟呢?
移动构造:
此时只需要我们对拷贝构造函数进行改造:
此时在编译器优化时会将str看成一个将亡值(右值),然后和ret进行交换。
所以当我们有了移动构造再来看移动赋值的代码:
此时1到2过程就不会在去调用深拷贝的拷贝构造了,直接调用移动拷贝就可以了。
两个疑惑:
诚然我们在处理上面这样的情况时,确实应该在return中加move,但是如果没有编译器也会给我们进行优化的。
如果将返回值改为string&的,此时会产生两个问题
a.因为str是局部对象,所以在出函数作用后会释放空间,所以此时会产生野指针的问题。
b.string&后编译器不会在将str优化为右值,也就不可以发生移动构造了。
左值引用的核心价值是减少拷贝,提高效率。
右值引用的核心价值是进一步减少拷贝,弥补左值引用没有解决的场景,如:传值返回。
四.完美转发
1.先看一道试题:
void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
template<typename T>
void PerfectForward(T&& t)
{
Fun(t);
}
int main()
{
PerfectForward(10);//右值
int a;
PerfectForward(a);// 左值
PerfectForward(std::move(a)); // 右值
const int b = 8;
PerfectForward(b);// const 左值
PerfectForward(std::move(b)); // const 右值
return 0;
}
代码的运行结果是:
a.为什么此时函数模板中的明明是右值引用但是左值右值都可以接受?
因为此时图中的引用是万能引用,因为此时函数模板的T是推导出来的。
b.为什么程序中即存在左值引用又存在右值引用但是却输出的结果都是左值引用呢?
因为在函数中Fun(t)即使是右值编译器也会将他看成左值。
解决方法:
void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
// std::forward<T>(t)在传参的过程中保持了t的原生类型属性。
template<typename T>
void PerfectForward(T&& t)
{
Fun(std::forward<T>(t));
}
int main()
{
PerfectForward(10);// 右值
int a;
PerfectForward(a);// 左值
PerfectForward(std::move(a));// 右值
const int b = 8;
PerfectForward(b);// const 左值
PerfectForward(std::move(b));// const 右值
return 0;
}
forward<T>保持此时数据的左右值属性。(完美转发)。