左值和右值
要想了解std::move,需要先了解左值和右值,以及左值引用和右值引用。先说简单的判断左值和右值的方法:
左值:
可以取地址、可以位于等号左边。也既:可以出现在等号(赋值运算符)左边,也可以出现在等号右边(取地址或者赋值给其他变量)。例如:在
int a = 10;中,变量a即是左值。我们同样可以对变量a取地址:int* b = &a,此处a和b都是左值。再比如,可以将变量a赋值给其他变量:int c = a,此处c和a都是左值。右值:
没法取地址,只能位于等号(赋值运算符)右边。例如:在下面代码中,字符串"123"和整数100都是右值。1
2std::string str = "123";
int num = 100;因为它们都只能出现在赋值运算符右边,且不能取地址,也不能出现在赋值运算符左边。在代码中写
"123" = str是非法的,写string* p = &"123"也是非法的,编译器都是会报错的。
左值引用和右值引用
知道左值和右值之后,再来将左值引用和右值引用。回忆以下引用:给变量取别名。而引用的本质其实是指针,只是没有指针那么灵活。
左值引用:
其实就是普通的引用,又或者说:对左值的引用。同时,一般的左值引用无法指向右值,const左值引用除外。例如:
1
2
3
4
5int a = 100;
int& b = a;
int& num = 100; // 左值引用指向了右值, 会编译失败
const int& temp = 100; // 编译通过,const左值引用可以指向右值上面代码中,变量
b是左值a的引用,所以b就是左值引用。为什么
const引用可以指向右值?因为const引用不会修改指向的值,所以可以指向右值。右值引用:
顾名思义,对右值的引用,也就是给右值取别名。但是注意:右值引用有特定的语法:
int&& a=100,此时a就是右值引用。注意a前面是两个&!另外,右值引用也不能指向左值。例如:1
2
3
4
5
6int &&ref_a_right = 5; // ok
int a = 5;
int &&ref_a_left = a; // 编译不过,右值引用不可以指向左值
ref_a_right = 6; // 右值引用的用途:可以修改右值左值引用和右值引用都是左值:
为什么呢?因为左值引用和右值引用本身都可以取地址,也都可以位于赋值运算符左边。且看测试代码:
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
void change(int&& right_value) {
right_value = 8;
}
int main() {
int a = 5; // a是个左值
int &ref_a_left = a; // ref_a_left是个左值引用
int &&ref_a_right = std::move(a); // ref_a_right是个右值引用
change(a); // 编译不通过, a是左值, change参数要求右值
change(ref_a_left); // 编译不通过, 左值引用ref_a_left本身也是个左值
change(ref_a_right); // 编译不通过, 右值引用ref_a_right本身也是个左值
change(std::move(a)); // 编译通过
change(std::move(ref_a_right)); // 编译通过
change(std::move(ref_a_left)); // 编译通过
change(5); // 当然可以直接接右值,编译通过
std::cout << &a << ' '; // 输出0x7fffffffd430
std::cout << &ref_a_left << ' '; // 输出0x7fffffffd430
std::cout << &ref_a_right; // 输出0x7fffffffd430
// 打印这三个左值的地址,都是一样的
}
std::move
std::move接收左值,返回右值引用。右值引用在此处是右值。也就是说:std::move可以将一个左值强制转换为一个右值。std::move并不是将一个变量的值移动到另一个变量中。而是将资源的所有权进行了移动。同时,std::move对于基本类型(如int, char, bool等)的作用和拷贝操作相同,也就是说,对基本类型执行移动操作在效果上完全等同于拷贝操作。但对于类类型(如std::string),对其执行移动操作将会转移资源的所有权,但资源所处的内存地址不变。换句话说:对一个对象使用std::move后,对象内的资源还是存储在原来的位置,只是拥有它的对象变成了另一个右值对象。借助下面代码理解:
1 |
|
上面代码输出:
1 | After copy, str is "123" |
可见,对变量str执行std::move后,它内部的字符串变为了空串。这是因为在标准库实现的std::string的移动构造函数中,内部存储字符串的char*指针赋给了新对象,原对象str的char*指针会指向空串,以此转移资源的所有权。
std::move的返回值
上面说了:std::move接收左值,返回右值引用。**不是说将左值强制转换为右值吗?那不应该返回右值吗?怎么返回右值引用了?右值引用是右值吗?**写段代码验证一下:
1 | using std::string; |
上方代码报错:表达式必须是左值或函数指示符,什么意思?意思就是std::move返回的不是一个左值,那是什么?当然是右值。对右值取地址当然会报错。前面说了,std::move返回右值引用,从哪里看出来它返回右值引用呢?将上述代码粘贴到VSCode中,鼠标悬浮于std::move处,能看到:

也就是说,此处std::move实际上调用的是:
1 | constexpr std::string &&std::move<std::string &>(std::string &__t) noexcept |
这个函数,这个函数的返回值是std::string &&类型,这不就是右值引用吗。
结合我们前面所说:std::move返回的是右值,现在又是右值引用。所以:右值引用作为函数返回值时是右值。
右值引用与std::move的应用场景
实现移动语义
1 |
|
上面代码中,使用std::move时,调用了移动构造函数。移动构造函数更改了资源的所有权,避免了数据的拷贝。
注意:不能对const变量使用std::move期待移动!