右值引用作用是可以减少内存拷贝次数,从而优化性能。
首先,什么是右值?右值是一个与左值相区分的概念。左值是:既能出现在等号左边也能出现在等号右边的变量或表达式,比如int a = 5,那么a就是一个左值,因为它可以出现在等号左边被赋值,也可以在等号右边给别人赋值。右值:因为声明结束后会被销毁,所以不能放在等号左边,比如上面int a = 5;这句话的5,就是一个明显的右值。
我们知道复制构造函数调用的一个条件,就是函数返回值是一个类对象的时候。比如说下面的场景:
String test()
{
String teststr = "this is a test";//调用拷贝构造函数
return teststr;//返回的时候同样调用拷贝构造函数
}
它的返回值是一个类string的对象。在返回teststr的时候,会调用拷贝构造函数,在内存空间中再次分配一块空间存放类对象。但是这种拷贝构造是完全没有必要的,因为作为一个teststr在实际中并没有派上用场。如果堆内存很大,就可能出现额外的内存消耗,所以我们能不能想个办法避免这多余的一次拷贝呢?答案就是右值引用。右值引用避免了拷贝构造函数的调用,使得新构造出来的对象直接使用原来临时右值对象的地址空间,避免了内存空间重复复制造成的浪费。
MyString& operator=(MyString&& other)//&&为右值引用
{
if (*this != other){
m_nLen = other.m_nLen;
m_pData = other.m_pData;
other.m_pData = NULL;
}
return *this;
}
利用右值引用,相当于只进行了指针的转移,并没有真正进行复制。避免无意义的复制,使得被移走资源的右值在废弃时已经成为空壳,析构的开销也会降低。
2. 移动语义
移动语义和移动构造函数相关联,同样避免的是内存重复复制带来的性能损耗。当函数返回一个类对象时,临时对象会析构掉之前我们又重新申请与其相同内存且复制内容,而移动构造函数避免了这个过程,从而减少复制次数。复制构造函数和移动构造函数对比如下图所示:
#include <iostream>
using namespace std;
class HasPtrMem
{
public:
HasPtrMem()//构造函数
{
cout << "Construct! "<< endl;
}
HasPtrMem(const HasPtrMem & h)//复制构造函数
{
cout << "Copy construct! "<< endl;
}
HasPtrMem(HasPtrMem&& h) // 移动构造函数
{
cout << "Move construct! "<< endl;
}
~ HasPtrMem()
{
cout << "Destruct! " << endl;
}
};
HasPtrMem GetTemp()
{
return HasPtrMem();
}
int main()
{
HasPtrMem a = GetTemp();
}
/*输出:
Construct!
Move construct!
Destruct!
Destruct!
*/
我们可以发现对于一个临时对象HasPtrMem()生成之后,如果调用拷贝构造函数,那么这个a对象和HasPtrMem()实际上是不同地址的,因为这是拷贝构造函数进行的一次操作;而当有了移动构造函数之后,实际上就不会调用拷贝构造函数,而会调用移动构造函数,那么用移动构造函数生成的对象a的地址,实际上就是那个临时对象HasPtrMem()的地址,这样避免新开辟内存空间,而这个临时对象的内存,也不会因为这句话的结束而被析构掉。所以移动语义实际上和右值引用相辅相成,共同完成减少内存拷贝次数,节省堆空间,优化性能的作用。
3. move()语义:
因为右值引用是通过移动构造函数直接指向临时对象的内存空间从而达到节省内存空间的作用,而临时对象往往是右值,也就是右值引用。那么左值可不可以借鉴这个方法呢?实际上也是可以的,这就是move()函数的功能:std::move()实际上把一个左值转换成右值,从而方便使用移动语义。move()只是把左值转换成右值,不涉及到内存拷贝。
MyString str1 = "hello";
MyString str2(str1);//复制构造函数,调用MyString(const MyString &str)
MyString str3 = move(str2);//移动语义,调用MyString(MyString && str);