右值引用
左值与右值
左值:能用在赋值运算符左侧的表达式
右值:能用在赋值运算符右侧,但不能用在左侧的表达式
1
2
int a = 0;
a = 5;这里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; // 函数地址
// 非静态类成员函数、枚举、数组、this指针、lambda表达式、一些内置运算符表达式左右值引用
- 左值引用:普通的引用
1
2
int a = 5;
int& b = a;- 右值引用:使用两个&进行引用
1
int &&c = 5;只能绑定右值,不能绑定左值
应用
函数重载:
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);
}1
2
调用左值引用重载
调用右值引用重载移动语义
移动构造函数
当类中存在指针等变量,而没有设置拷贝构造,编译器会自动生成拷贝构造函数,此时为浅拷贝。我们应该自己设计深拷贝,申请新的内存,并将原来的内容复制到新分配的内存中,并实现赋值运算符的重载函数,释放原有内存,申请新的内存,并复制值
此时参数中为左值引用,称为拷贝赋值运算符重载函数
为了实现移动语义,我们需要实现移动赋值运算符重载函数
此时参数为右值引用,且没有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,但编译器会对这种代码进行优化,省略调临时对象的创建,直接在目标存储位置构造该对象即输出:
1
2
普通构造函数
析构函数C++17后,这种优化成为语言规范
返回值优化(RVO)
类似,函数直接返回值的情况下,临时对象的创建也会省略
1
2
3
4
5
6
7
CharBuffer generate(int n){
return CharBuffer(n);
}
int main(){
CharBuffer buff2 = generate(100);
}1
2
普通构造函数
析构函数具名的返回值优化(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));
}1
2
3
4
普通构造函数
移动构造函数
析构函数
析构函数此时由于传入的转换为了右值引用,故正确调用了移动构造函数
等同于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,需要提供移动语义的支持