右值引用
左值与右值
左值:能用在赋值运算符左侧的表达式
右值:能用在赋值运算符右侧,但不能用在左侧的表达式
这里a为左值,5为右值,不能写成5=a
判断方法:
左值:能够获得某个表达式的引用或地址即为左值
1 2 3 4 5 6 7
| const int a = 5; const int& b = a;
int c; int* p = &c;
int b = &4
|
- b取到了a的引用,a是(不可修改的)左值
- p取到了c的地址,c是左值
- 4不能取地址,4是右值
C++11后的分类
glvalue:泛左值
prvalue:纯右值
xvalue:将亡值
举例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| int a = 8; double m = 1.5; int*p = new int{5}; *p = 4; int b[5]; b[1] = 10;
int& r = a;
struct s{ int id; }s1; s1.id = 3;
int& refn(){ static int n = 1; return n; } refn()=5;
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| int x,y,z; int m = 3, n = 1; double d;
x = 2;
x = m+n; y = -m; z = n+2;
d = double(z) struct s{}; s obj; obj = s{};
s func(){ return s{}; } func(); s (*f)(int) = &func;
|
左右值引用
只能绑定右值,不能绑定左值
应用
函数重载:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| #include <iostream> using namespace std; void func(int& a){ cout <<"调用左值引用重载"<<endl; } void func(int&& a){ cout <<"调用右值引用重载"<<endl; }
int main(){ int a = 5; func(a); func(5); }
|
移动语义
移动构造函数
当类中存在指针等变量,而没有设置拷贝构造,编译器会自动生成拷贝构造函数,此时为浅拷贝。我们应该自己设计深拷贝,申请新的内存,并将原来的内容复制到新分配的内存中,并实现赋值运算符的重载函数,释放原有内存,申请新的内存,并复制值
此时参数中为左值引用,称为拷贝赋值运算符重载函数
为了实现移动语义,我们需要实现移动赋值运算符重载函数
此时参数为右值引用,且没有const限定符
该方法少了内存的分配和数据的复制,修改了被移动对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
| #include <iostream> using namespace std; class CharBuffer{ public:
CharBuffer(int size):m_buff(new char[size]), m_size(size){ cout <<"普通构造函数"<<endl; } CharBuffer(const CharBuffer& other):m_size(other.m_size), m_buff(new char[m_size]){ memcpy(m_buff, other.m_buff, m_size); cout<<"拷贝构造函数"<<endl; } CharBuffer& operator=(const CharBuffer& other){ if (this==&other){ return *this; } m_size = other.m_size; delete[] m_buff; m_buff = new char[m_size]; memcpy(m_buff, other.m_buff, m_size); cout <<"拷贝赋值运算符"<<endl; }
CharBuffer(CharBuffer&& other):m_size(other.m_size), m_buff(other.m_buff){ other.m_buff = nullptr; other.m_size = 0; cout <<"移动构造函数"<<endl; } CharBuffer& operator=(CharBuffer&& other){ if (this==&other){ return *this; } delete[] m_buff; m_size = other.m_size; m_buff = other.m_buff;
other.m_size= 0; other.m_buff = nullptr; cout <<"移动赋值运算符"<<endl; } int m_size; char* m_buff;
~CharBuffer(){ delete[] m_buff; cout <<"析构函数"<<endl; } };
int main(){ CharBuffer buff2{CharBuffer(100)}; }
|
拷贝省略
当我们运行语句,构造一个未命名的对象作为右值
CharBuffer buff2{CharBuffer(100)};
按照移动语义,会先通过普通构造函数创建临时对象,然后将该临时对象赋值给buff2,但编译器会对这种代码进行优化,省略调临时对象的创建,直接在目标存储位置构造该对象即输出:
C++17后,这种优化成为语言规范
返回值优化(RVO)
类似,函数直接返回值的情况下,临时对象的创建也会省略
1 2 3 4 5 6 7
| CharBuffer generate(int n){ return CharBuffer(n); }
int main(){ CharBuffer buff2 = generate(100); }
|
具名的返回值优化(NRVO)
1 2 3 4 5 6 7 8 9 10
| CharBuffer generate_nv(int n){ CharBuffer buf(n); cout <<&buf<<endl; return buf; }
int main(){ CharBuffer buff2 = generate_nv(100); cout <<&buff2<<endl; }
|
1 2 3 4
| 普通构造函数 0x61fe30 0x61fe30 析构函数
|
buff2与buff的地址相同,说明他们是对同一个对象的引用,原理同上
std::move()
该函数将传入的参数转换为右值引用并返回
1 2 3 4
| int main(){ CharBuffer buff1(100); CharBuffer buff2(move(buff1)); }
|
此时由于传入的转换为了右值引用,故正确调用了移动构造函数
等同于static_cast<CharBuffer&&>(buff1)
xvalue
1 2 3 4 5 6 7
| int main(){ CharBuffer buff2(10); { CharBuffer buff1(100); buff2 = move(buff1); } }
|
这段代码中,buff1就是一个将亡值,我们使用move函数获得其右值引用,从而将它的资源回收再利用,转移buff2中
常见xvalue
- 函数返回的右值引用
static_cast<T&&>(v)
- 未命名的右值对象获得的非静态成员变量
移动语义的应用
大部分情况下都不需要移动语义
- 若一个类涉及到深拷贝,需要复制较多数据,可以使用移动语义
- 当转移
unique_ptr的所有权时,需要移动语义
- 编写供其他程序使用的STL,需要提供移动语义的支持