C++11重大新增特性:左值引用 & 右值引用 & 移动构造 & 移动赋值
一、右值引用和左值引用概念和区别
C++11为了支持移动操作(移动构造和移动赋值),新标准引入了新的引用类型 —— 右值引用。我们将C++11之前的引用都成为左值引用。但无论是左值引用还是右值引用,本质上都是给对象取别名!
1.1 左值 & 左值引用
左值是一个表达式(如变量、解引用后的指针),表示的是一个对象的身份。我们可以对左值进行赋值操作、取地址。左值可以出现在等号的两边。
评判一个表达式是否为左值的最根本标志就是:是否可以取地址。所以对于一个const修饰的变量
,由于该变量可以取地址,所以也是一个典型的左值。左值引用即是左值的别名。
int main()
{
//a、b、p、*p都是左值
int* p = new int(0);
int a = 10;
const int b = 12;
//rp、ra、rb、rval都是左值引用
int*& rp = p;
int& ra = a;
const int& rb = b;
int& rval = *p;
return 0;
}
1.2 右值 & 右值引用
右值也是一个表达式,和左值不同的是:右值只能出现在等号的右边(即不能被赋值),不能取地址(最根本原因),通常是字面常量、表达式返回值,函数返回值。右值引用就是对右值的引用,通过&&
来获取右值引用。
右值不能取地址。但对右值取别名后,会导致右值被存储到特定的区域,并且可以取到该区域的指针。比如:字面常量10是一个右值,不能取地址。如果10被ra引用后,我们可以对ra取地址,并且可以通过修改ra进而修改右值。如果不想该右值被修改,我们可以通过const进行修饰!!
int Add(const int x, const int y)
{
return x + y;
}
int main()
{
//10、10 + 20、Add函数返回值都是右值,无法取地址
//ra、rb、rc都是右值引用
int&& ra = 10;
int&& rb = 10 + 20;
int&& rc = Add(1, 2);
ra = 20;
return 0;
}
- 需要注意的是:上述
ra、rb、rc
虽然是右值引用,但ra、rb、rc
本身还是一个变量,并且可以取地址,是一个左值!!
二、左值引用和右值引用对比
2.1 左值引用
- 普通的左值引用只能引用左值,不能引用右值。
- const修饰的左值引用,不仅可以引用左值,还可以引用右值!!
【示例】:
int main()
{
int a = 10;
int& ra = a;
//int& rb = 10;//error,普通左值引用不能引用右值
const int& rc = a;
const int& rd = 10;
return 0;
}
2.1 右值引用
- 右值引用可以引用右值,但不能引用左值。
- 我们可以通过move函数,让右值引用引用左值!!move函数调用告诉编译器:我们有一个左值,但我们希望像一个右值一样处理它。需要注意的是,move函数的返回值是一个右值,但move函数本身不会修改左值属性!
【示例】:
int main()
{
int a = 10;
const int&& rb = 10;
//int&& ra = a;//error, 右值引用无法引用左值
//error,move本身不会修改左值属性
//move(a);
//int&& ra = a;
int&& ra = move(a);//move后的返回值是一个右值
return 0;
}
三、右值和右值引用诞生的意义
在C++11标志之前,如果在vector、list
等容器保存的是一块空间的指针。此时如果调用拷贝构造函数和拷贝赋值函数,编译器会进行一个深拷贝构造出新对象,将就对象释放。
在很多情况下拷贝对象时无法避免的。
但如果返回的是一个临时对象会发生什么呢?
【示例】:
- 我们发现如果用一个临时对象拷贝构造一个变量
s
时,编译器会先拷贝构造出一个临时对象,在用该临时对象出拷贝构造变量s
。 - 但该过程中存在一个问题:临时对象深拷贝创建后仅仅使用一次便立即销毁、原对象
ret
是一个临时对象马上就要出作用域销毁,此时依旧对ret
进行拷贝构造。 - 上述情况在实际过程中会在大量场景中频繁出现,并且意义不大。这也意味着大量的无意义的深拷贝产生,将导致性能的下降。我们是否可以不进行拷贝,直接将原始数据转移到新对象中呢?(该操作的前提是原始数据马上就要被销毁)
- 为了解决上述情况,C++11引入了移动构造函数和移动拷贝函数。移动构造函数和移动拷贝函数可以将一个待销毁的变量数据(该变量通常被编译器识别为右值)直接转移到新对象。而右值和右值引则是为实现这些函数运营而生的!!
四、移动构造 & 移动赋值
在C++中,右值分为两种:内置定义类型右值为纯右值;自定义类型右值为将亡值!!对于纯右值,移动构造函数、移动赋值函数没有太大价值,行为和拷贝构造函数、移动构造函数类型。(上述临时对象ret
虽然可以取地址是一个左值,但编译器会特殊处理将其识别为右值,即将亡值)
只有当自定义类型中存在资源的深拷贝时,此时才能移动构造和移动赋值的价值。(直接转移资源,而非深拷贝!!)
4.1 移动构造函数
类似于拷贝构造函数,移动构造函数的第一个参数是该类类型的引用,不同的是引用参数是一个右值。移动构造函数的本质是直接将右值对象的资源窃取过来,占为己有,此时不在进行深拷贝。所以该构造称为移动构造,用别人的资源来构造自己。
【示例:移动构造和拷贝构造函数实现和对比】:
namespace mystring
{
class string
{
public:
string(const char* str = "")//默认构造函数
:_size(strlen(str))
, _capacity(_size)
{
cout << "string(const char* str = "") ----- 构造函数" << endl;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
//拷贝构造函数
string(const string& s)
{
cout << "string(const string& s) ---- 拷贝构造函数 深拷贝" << endl;
string tmp(s._str);
swap(tmp);
}
//移动构造函数
string(string&& s)
{
cout << "string(string&& s) ---- 移动构造函数 移动语义" << endl;
swap(s);
}
void swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size