目录
左值与右值的概念
左值引用
左值引用的格式
右值引用
右值引用的格式
右值引用原理
右值引用的使用场景
移动构造
移动赋值
模板中的万能引用 &&
完美转发
总结
在讲两种引用之前,得首先介绍一下什么叫做左值,什么叫做右值。
左值是一个数据表达式,包括变量和指针的解引用。左值可以取地址,一般情况下也可以放在等式的左边,但是const类型的左值就不行了。因此,判断一个数据是否是左值,就看看它能不能进行取地址操作。
下面的都是左值:
int a;
int* ptr;
const int num=10;
const int* cptr;
int* const ptrc;
右值也是一个数据表达式,与左值刚刚相反,它不能进行取地址操作,一般情况下不能放在等式的左边。
下面的都是右值:
重点是将亡值
10;
10+20;
x+y;
fun();//函数的返回值(非左值引用返回)
fun(string str);//str作为将亡值也属于右值
左值引用的本质还是取数据的地址,只不过经过了上层转化,感觉就像是一个数据的另一个名字一样,因此左值的引用又叫作一个数据的别名。通过左值引用,我们可以方便的实现这种操作:拷贝构造的引用传参从而避免无限递归拷贝构造
总而言之,左值引用可以看成指针的封装使用。具体没啥好说的,我这里不细说了,有问题的小伙伴可以在留言区提出。
这个就比较有意思了,既然右值不能被取地址,按照引用的概念和实现底层(取地址),右值引用明显是在逻辑上是行不通的。那么右值引用是个什么奇葩东西呢?我们从汇编的角度看看是怎么回事去!
#include
using namespace std;
int main()
{
int&& rra = 10;
rra = 20;
return 0;
}
可以看出右值引用的时候并没有直接取字面量10的地址,而是重新取指向了一个地址空间,具体地址通过ebp-18h来计算,18就是偏移量(编译器不同,偏移量不同)。假设这是个匿名变量,那么右值引用的本质就是在栈上偏移一定的距离,创建新空间来存放原来的数据,形成匿名变量,并且对匿名变量进行左值引用的操作!并且得到的别名也是左值!
由此可见右值引用最终还是要转换成左值引用的操作,算是小小的套一下娃了。当然,这也是我在汇编下总结的,具体的参考资料倒是没去查看,有兴趣的小伙伴可以去看看C++官方给出的解释啦。
右值引用的应用场景主要是在将亡值上,而且会引出很多新的概念,这个不着急,我们先从移动构造讲起。
我们在接触类的时候学过六大函数,今天我们学习了右值引用就得学习移动构造了。
我们经常会遇到函数返回值要返回一个函数内的对象的情况:
std::string func1()
{
std::string str;
for(size_t i=0;i<26;++i)
{
str+=('a'+i);
}
return str;
}
类似这种肯定要进行传值返回,因为根据类的特性:出了作用域就要销毁。一旦返回了类的左值引用,函数结束后那就是返回了一片被销毁的空间。因此,这里的左值引用显然就解决不了了。在C98下,我们可以通过在函数传参上做手脚,即把要返回的类作为参数直接左值引用传进函数,之后对这个参数进行操作修改即可。但这样使用起来不是特别爽,因此C++的委员会想着能不能将返回值的空间直接转移给另一个同类型的变量,这样也就省得拷贝来拷贝去了。移动构造的概念就出来了。
但是得有一个前提:资源转移的变量用户你确定用不着了,才会进行这种资源转移般的操作。因此将亡值马上就要被销毁了,在代码的后面确实用不到了,这时进行资源的转移很安全,C++11中编译器会自动把将亡值看作右值,并在返回时进行移动构造。
和移动构造的思想一样,同样是将空间进行交换。
我们接下来简单测试一下移动构造和移动赋值
#include
#include
#include
#include
using namespace std;
class Mystring
{
public:
Mystring()
:_str(nullptr)
{
cout << "Mystring()--无参构造" << endl;
}
Mystring(const char* str)
:_str(nullptr)
{
cout << "Mystring(char* str)--传值构造" << endl;
_str = new char[strlen(str)];
strcpy(_str, str);
}
Mystring(const Mystring& s)
:_str(nullptr)
{
cout<<"Mystring(const Mystring& s)--拷贝构造" << endl;
_str = new char[strlen(s._str)];
strcpy(_str, s._str);
}
Mystring(Mystring&& s)
:_str(nullptr)
{
cout << "Mystring(Mystring&& s)--移动构造" << endl;
swap(_str,s._str);
}
Mystring& operator=(Mystring&& s)
{
cout << "Mystring& operator=(Mystring&& s)--移动赋值" << endl;
swap(_str, s._str);
return *this;
}
private:
char* _str;
};
int main()
{
Mystring s1;
Mystring s2("wuhu~");
Mystring s3(s2);
Mystring s4(move(s2));//move()可以将左值属性转化为右值属性
Mystring s5;
s5=move(s4);
return 0;
}
移动构造和移动赋值的最终结果显示也是正确的。
如果&&用在类模板中时,就不单单是右值引用的意思了,而是左值右值都能接收。因此才叫万能引用。
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 prefectforward(T&& t)
{
func(t);
}
int main()
{
int a=3;
prefectforward(a);
prefectforward(move(a));
const int b=10;
prefectforward(b);
prefectforward(move(b));
return 0;
}
运行结果却出乎意料,原因在于在万能引用之后,右值属性会退化成左值,这可能是C++的语言机制问题。这种情况不能直接进行move处理,因为原本是左值被move改成右值之后,有可能被进行移动操作,导致数据遭到破坏。所以为了解决这个问题,C++又引入了完美转发这个概念 。
将退化的引用完美还原。该是左值就是左值,该是右值就是右值。
转化格式:std::forward
template
void prefectforward(T&& t)
{
func(forward(t));
}
改完之后就正确了。
左值与右值引用的使用不是特别广泛,因此某些细节我可能考虑不到,有问题的小伙伴在评论区留言哦