一、右值引用简介
C++11增加了一个新的类型,称为右值引用(R-value reference),标记为 &&。在介绍右值引用类型之前要先了解什么是左值和右值:
- lvalue是locator value的缩写,rvalue是 read value的缩写。
- 左值是指存储在内存中、有明确存储地址(可取地址)的数据。
- 右值是指可以提供数据值的数据(不可取地址)。
通过描述可以看出,区分左值与右值的便捷方法是:可以对表达式取地址(&)就是左值,否则为右值。
所有有名字的变量或者对象都是左值,而右值是匿名的。
int a = 520;
int b = 1314;
a = b;
一般情况下,位于 = 前面的表达式为左值,位于 = 后边的表达式为右值。也就是说例子中的a,b为左值,520,1314为右值。a=b是一种特殊情况,在这个表达式中a,b都是左值,因为变量b是可以被取地址的,不能视为右值。
int value = 520;
上面语句中的value为左值,520为字面量也就是右值。其中value可以被引用,但是520就不行了,因为字面量都是右值。
二、右值引用的使用
右值引用就是对一个右值进行引用的类型。因为右值是匿名的,所以我们只能通过引用它的方式找到它。无论声明左值引用还是右值引用都必须立即进行初始化,因为引用类型本身并不拥有所绑定对象的内存,只是该对象的一个别名。通过右值引用的声明,该右值又”重获新生”。
其生命周期与右值引用类型变量的声明周期一样,只要该变量还活着,该右值临时量将会一直存活下去。
关于右值引用的使用,代码如下:
#include <iostream>
using namespace std;
int&& value = 520;
class Test
{
public:
Test()
{
cout << "construct: my name is jerry" << endl;
}
Test(const Test& a)
{
cout << "copy construct: my name is tom" << endl;
}
};
Test getObj()
{
return Test();
}
int main()
{
int a1;
//int&& a2 = a1; // error
//Test& t = getObj(); // error
Test&& t = getObj();
const Test& t1 = getObj();
return 0;
}
- 在上面的例子中int &&value=520;里面的520为纯右值,value是字面量520这个右值的引用。
- 在int &&a2=a1中;a1虽然写在=的右边,但是它仍然是一个左值,使用左值初始化一个右值引用是不合法的。
- Test & t=getObj()这段代码语法是错误的,右值不能给普通的左值引用赋值。
- Test && t=getObj()中getObj()返回的临时对象称为将亡值,t是这个将亡值的右值引用。
- const Test & t1=getObj()这段代码语法是正确的,常量左值引用是一个万能的引用类型,它可以接受左值、右值、常量左值和常量右值。
测试代码的结果:
三、性能优化
在C++中在进行对象赋值操作的时候,很多情况下会发生对象之间的深拷贝,如果堆内存很大,这个拷贝的代价也就非常大,在某些情况下,如果想要避免对象的深拷贝,就可以使用右值引用进行性能优化。
修改上述代码:
#include <iostream>
using namespace std;
class Test
{
public:
Test() : m_num(new int(100))
{
cout << "construct: my name is jerry" << endl;
}
Test(const Test& a) : m_num(new int(*a.m_num))
{
cout << "copy construct: my name is tom" << endl;
}
~Test()
{
delete m_num;
}
int* m_num;
};
Test getObj()
{
Test t;
return t;
}
int main()
{
Test t = getObj();
cout << "t.m_num: " << *t.m_num << endl;
return 0;
};
测试代码的输出结果:
通过输出结果我们可以看到调用Test t=getObj()的时候调用拷贝构造函数对返回的临时对象进行了深拷贝得到对象t,在getObj()函数中创建的对象虽然进行了内存申请操作,但是没有使用就释放掉了。如果能够使用临时对象已经申请的资源,既能节省资源,还能节省申请和释放的时间,如果要执行这样的操作就需要使用右值引用了。
右值引用具有移动语义,移动语义可将资源(堆、系统对象等)通过浅拷贝从一个对象转义到另一个对象这样就能减少不必要的临时对象的创建、拷贝及销毁,可以大幅度提高C++应用程序的性能。
修改上述代码:
#include <iostream>
using namespace std;
class Test
{
public:
Test() : m_num(new int(100))
{
cout << "construct: my name is jerry" << endl;
}
Test(const Test& a) : m_num(new int(*a.m_num))
{
cout << "copy construct: my name is tom" << endl;
}
// 添加移动构造函数
Test(Test&& a) : m_num(a.m_num)
{
a.m_num = nullptr;
cout << "move construct: my name is sunny" << endl;
}
~Test()
{
delete m_num;
cout << "destruct Test class ..." << endl;
}
int* m_num;
};
Test getObj()
{
Test t;
return t;
}
int main()
{
Test t = getObj();
cout << "t.m_num: " << *t.m_num << endl;
return 0;
};
测试代码输出的结果:
通过修改,在上面的代码给Test类添加了移动构造函数(参数为右值引用类型),这样在进行Test t=getObj()操作时候并没有调用拷贝构造函数进行深拷贝,而是调用移动构造函数,这个函数中只是进行了浅拷贝,没有对临时对象进行深拷贝,提高性能。
如果不使用移动构造,在执行Test t=getObj()时候也是进行浅拷贝,但是临时对象被析构的时候,类成员指针int *m_num指向的内存也就被析构了,对象t也就无法访问这块内存地址了。
在测试程序中getObj()的返回值就是一个将亡值,也就是一个右值,在进行赋值操作的时候如果右边是一个右值,那么移动构造就会被调用。移动构造中使用了右值引用,会将临时对象的堆内存地址的所有权转移给对象t,这块内存被成功续命,因此在t对象中还可以继续使用这块内存。
注意:对于需要动态申请大量资源的类,应该设计移动构造函数,以提高程序效率。需要注意的是,我们一般在提供移动构造函数的同时,也会提供常量左值引用的拷贝构造函数,以保证移动不成还可以使用拷贝构造函数。