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