WRY

Where Are You?
You are on the brave land,
To experience, to remember...

0%

Cpp的左值与右值

定义

左值的定义:占据内存中某个可识别的位置

  • 可修改左值是特殊的左值,不含有数组类型、不完整类型、const 修饰的类型。如果它是 structunion,它的成员都(递归地)不应含有 const 修饰的类型。
  • 除了数组、函数、不完整类型的所有左值都可以转换为右值。

右值的定义:表示内存中一个不可识别的位置(不是左值的都是右值),对右值进行赋值是没有意义的,因为赋值对应的位置是一个临时的不可识别的位置

函数返回的结果一般都是一个右值,但当函数返回的是一个引用的时候,是可以对函数的返回结果直接赋值的,例如

1
2
3
4
5
6
7
8
9
#include<iostream>
int globalvar = 20;
int& foo(){
return globalvar;
}
int main(){
foo() = 10;
std::cout<<globalvar<<std::endl; // 输出的是10
}

另外一个常见的例子是

1
2
std::map<int, float> mymap;
mymap[10] = 5.6;

左右值的相互转换

左值转右值

1
2
3
4
int a = 1;     // a 是左值
int b = 2; // b 是左值
int c = a + b; // + 需要右值,所以 a 和 b 被转换成右值
// + 返回右值

右值产生左值

右值是不能转化成左值的,但是可以通过某些方式产生一个左值

1
2
3
int arr[] = {1, 2};
int* p = &arr[0];
*(p + 1) = 10; // 正确: p + 1 是右值,但 *(p + 1) 是左值

对应的一元取址操作符号需要一个左值,返回一个右值

1
2
3
4
int var = 10; 
int* bad_addr = &(var + 1); // 错误: 一元 '&' 操作符需要左值参数
int* addr = &var; // 正确: var 是左值
&var = 40; // 错误: 赋值操作的左操作数需要是左值

取值符号还可以用于定义引用类型,引用类型又叫做“左值引用”。因此,不能将一个右值赋值给(非常量的)左值引用:

1
std::string& sref = std::string();  // 错误: 非常量的引用 'std::string&' 错误地使用右值 'std::string` 初始化

常量的 左值引用可以使用右值赋值。因为你无法通过常量的引用修改变量的值,也就不会出现修改了右值的情况。这也使得 C++ 中一个常见的习惯成为可能:函数的参数使用常量引用接收参数,避免创建不必要的临时对象。

c++ move操作

符号&&代表右值引用,c++11中增加了移动赋值函数,下面介绍一个完整的类应有的基本构造函数

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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#include<bits/stdc++.h>
using namespace std;

class Intvec {
public:
// explicit声明禁止隐形转换【普通构造函数】
explicit Intvec(size_t num = 0):m_size(num), m_data(new int[m_size]) {
log("constructor");
}
// 【析构函数】,在继承中记得使用virtual修饰
~Intvec() {
log("destructor");
if (m_data) {
delete[] m_data;
m_data = 0;
}
}

// 【拷贝构造函数】
Intvec(const Intvec& other) : m_size(other.m_size), m_data(new int[m_size]) {
log("copy constructor");
for (size_t i = 0; i < m_size; ++i)
m_data[i] = other.m_data[i];
}

// 【拷贝赋值操作符】
Intvec& operator=(const Intvec& other) {
log("copy assignment operator");
if(this==&other){
return *this;
}
Intvec tmp(other);
std::swap(m_size, tmp.m_size);
std::swap(m_data, tmp.m_data);
return *this;
}

// 【移动赋值操作】因为右值会随之释放掉,就没有必要新建一个对象进行资源倒腾了
Intvec& operator=(Intvec&& other) {
log("move assignment operator");
std::swap(m_size, other.m_size);
std::swap(m_data, other.m_data);
return *this;
}


private:
void log(const char* msg) {
cout << "[" << this << "] " << msg << "\n";
}
size_t m_size;
int* m_data;
};


int main(){
Intvec v1(20);
Intvec v2;

cout << "\nassigning lvalue...\n";
v2 = v1;
cout << "ended assigning lvalue...\n";

cout << "\nassigning rvalue...\n";
v2 = Intvec(33);
cout << "ended assigning rvalue...\n\n";
}

对应结果

没有移动赋值函数时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[0x16d96b640] constructor
[0x16d96b630] constructor

assigning lvalue...
[0x16d96b630] copy assignment operator
[0x16d96b5d0] copy constructor
[0x16d96b5d0] destructor
ended assigning lvalue...

assigning rvalue...
[0x16d96b610] constructor
[0x16d96b630] copy assignment operator
[0x16d96b5d0] copy constructor
[0x16d96b5d0] destructor
[0x16d96b610] destructor
ended assigning rvalue...

[0x16d96b630] destructor
[0x16d96b640] destructor
~/repos/jingtao/algorithm-exercises

添加了移动赋值函数之后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[0x16f3e3640] constructor
[0x16f3e3630] constructor

assigning lvalue...
[0x16f3e3630] copy assignment operator
[0x16f3e35d0] copy constructor
[0x16f3e35d0] destructor
ended assigning lvalue...

assigning rvalue... // 该步骤变得相对简单一些【右值转左值】的情况
[0x16f3e3610] constructor
[0x16f3e3630] move assignment operator
[0x16f3e3610] destructor
ended assigning rvalue...

[0x16f3e3630] destructor
[0x16f3e3640] destructor
~/repos/jingtao/algorithm-exercises

参考资料