关于左值和右值的定义
- lvalue(locator value), 即左值,就是有名字的变量(对象),可以被赋值,可以在多条语句中使用。代表一个在内存中占有确定位置的对象,换句话说就是有一个地址。
- 而右值呢,就是临时变量(对象),没有名字,只能在一条语句中出现,不能被赋值。在内存中没有确切地址,它们只是在计算的周期临时储存在寄存器中。
简单的例子
1 | int foo(){ |
我们编译时会得到报错:
1
error: lvalue required as left operand of assignment
意思是赋值的左操作数需要是一个左值。
不是说foo() = 2这句一定是错的。当foo()返回的是左值的时候,这个赋值语句便是合法的。
1 | int g_var = 1; |
- 上面的代码就是合法的。foo()返回一个引用,这是一个左值。
- C++的vector、map等容器重载的运算符[] 返回的也是左值,比如我们可以对map[“10086”]进行赋值。
左值引用
1 | int a = 1; |
- 在上面的代码,第二行是正确的,b是一个左值引用,一个左值赋给左值引用,合法。第三行错误,因为2是右值,右值赋给左值引用非法。第四行同样。
右值引用(C++11)
我们有时会看到这样的函数声明:
1
void Function(std::vector<int>&& nums);
在参数前面有’&&’符号。这表明nums是一个右值引用。
我们看一段代码。
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
68
class Intvec{
public:
explicit Intvec(size_t num = 0):
m_size(num), m_data(new int[m_size]){
log("构造函数");
}
~Intvec(){
log("析构函数");
if(m_data){
delete []m_data;
m_data = NULL;
}
}
Intvec(const Intvec& other):
m_size(other.m_size), m_data(new int[m_size]){
log("拷贝构造函数");
for(int i = 0; i < m_size; ++i)
m_data[i] = other.m_data[i];
}
Intvec& operator=(const Intvec& other){
log("赋值运算");
Intvec tmp(other);
std::swap(m_size, tmp.m_size);
std::swap(m_data, tmp.m_data);
return *this;
}
private:
void log(const char* msg){
std::cout << "[" << this << "] " << msg << std::endl;
}
size_t m_size;
int* m_data;
};
int main(){
Intvec v1(20);
Intvec v2;
printf("++++++++++++++++++\n");
v2 = v1;
printf("*******************\n");
v2 = Intvec(33);
printf("#####################\n");
}
```
- 运行后,我们会看到如下结果:
```c++
[0x7fffffffe510] 构造函数
[0x7fffffffe520] 构造函数
++++++++++++++++++
[0x7fffffffe520] 赋值运算
[0x7fffffffe4e0] 拷贝构造函数
[0x7fffffffe4e0] 析构函数
*******************
[0x7fffffffe530] 构造函数
[0x7fffffffe520] 赋值运算
[0x7fffffffe4e0] 拷贝构造函数
[0x7fffffffe4e0] 析构函数
[0x7fffffffe530] 析构函数
#####################
[0x7fffffffe520] 析构函数
[0x7fffffffe510] 析构函数可以看到,在第二次赋值时,执行了很多工作。尤其是,它有一对额外的构造/析构调用。不幸的是,这是个额外工作,没有任何用,因为在拷贝赋值运算符的内部,另一个临时拷贝的对象在被创建和析构。
在这里还有一个问题,左值引用的赋值函数,被一个右值作为参数也成功运行了。因为重载operator=的参数是一个常量左值引用。
左值引用只能绑定左值,右值引用只能绑定右值,如果绑定的不对,编译就会失败。但是,常量左值引用例外,它可以算是一个“万能”的引用类型,它可以绑定非常量左值、常量左值、右值,而且在绑定右值的时候,常量左值引用还可以像右值引用一样将右值的生命期延长,缺点是,只能读不能改。
我们添加另一个operator=到类中去:
1
2
3
4
5
6
7Intvec& operator=(Intvec&& other)
{
log("右值引用的赋值运算");
std::swap(m_size, other.m_size);
std::swap(m_data, other.m_data);
return *this;
}然后再执行相同的main函数,会得到
1
2
3
4
5
6
7
8
9
10
11
12
13[0x7fffffffe510] 构造函数
[0x7fffffffe520] 构造函数
++++++++++++++++++
[0x7fffffffe520] 赋值运算
[0x7fffffffe4e0] 拷贝构造函数
[0x7fffffffe4e0] 析构函数
*******************
[0x7fffffffe530] 构造函数
[0x7fffffffe520] 右值引用的赋值运算
[0x7fffffffe530] 析构函数
#####################
[0x7fffffffe520] 析构函数
[0x7fffffffe510] 析构函数&& 语法是新的右值引用。的确如它名字一样-给我们一个右值的引用,在调用之后将被析构。
我们再来尝试点有趣的事情,把左值引用的operator=注释掉,再编译,会得到:
1
error: use of deleted function ‘Intvec& Intvec::operator=(const Intvec&)’
错误指向的是”v2 = v1;”这是因为右值引用的operator=的参数只能是右值,不能是左值。
然后,我们将报错的这一行改成:
1
v2 = std::move(v1);
编译运行成功,得到结果:
1
2
3
4
5
6
7
8
9
10
11[0x7fffffffe510] 构造函数
[0x7fffffffe520] 构造函数
++++++++++++++++++
[0x7fffffffe520] 右值引用的赋值运算
*******************
[0x7fffffffe530] 构造函数
[0x7fffffffe520] 右值引用的赋值运算
[0x7fffffffe530] 析构函数
#####################
[0x7fffffffe520] 析构函数
[0x7fffffffe510] 析构函数因为我们在operator=中是使用std::swap来实现的,我们重写一个main函数,来看看右值引用对对象内容的变化:
1
2
3
4
5
6
7
8
9
10
11
12
13int main(){
Intvec v1(20);
Intvec v2;
printf("++++++++++++++++++\n");
v2 = Intvec(33);//在右值引用运算符重载函数中,对象内容进行了交换。临时创建的Intvec(33)本来作为右值,是不能改变的,但是作为右值引用后,内容发生变化。
v1.m_data[0] = 5;
v2.m_data[0] = 9;
printf("#####################\n");
v2 = std::move(v1);//std::move作用:将左值转成右值。因为函数内部使用std::swap,这样做会导致v1与v2内容进行了交换
printf("-------------------\n");
printf("v1 size: %d v1 data[0]: %d v2 size: %d v2 data[0]: %d\n",
v1.m_size, v1.m_data[0], v2.m_size, v2.m_data[0]);
}结果:
1
2
3
4
5
6
7
8
9
10
11[0x7fffffffe510] 构造函数
[0x7fffffffe520] 构造函数
++++++++++++++++++
[0x7fffffffe530] 构造函数
[0x7fffffffe520] 右值引用的赋值运算
[0x7fffffffe530] 析构函数
#####################
[0x7fffffffe520] 右值引用的赋值运算
-------------------
v1 size: 33 v1 data[0]: 9 v2 size: 20 v2 data[0]: 5
[0x7fffffffe520] 析构函数可以看到,右值引用可以对右值更改。
我们将右值引用写成swap而不是复制,是考虑到右值是本就该消亡的值,所以为提升性能,我们没有使用复制,而是将右值的内容”偷”了过来。
当一个左值马上要结束它的生命周期,我们又想将其赋给另一对象时,可以用std::move将其转成右值,再进行赋值,这时会优先调用右值引用的赋值函数,能够有效减少内存的复制。
右值引用和相关的移动语义是C++11标准中引入的最强大的特性之一。这里只是管中窥豹,只可见其一斑。要更深入理解,需要多去阅读源码,查阅资料。
右值引用的意义
- 直观意义:为临时变量续命,也就是为右值续命,因为右值在表达式结束后就消亡了,如果想继续使用右值,那就会动用昂贵的拷贝构造函数。(关于这部分,推荐一本书《深入理解C++11》)
- 右值引用是用来支持转移语义的。转移语义可以将资源 ( 堆,系统对象等 ) 从一个对象转移到另一个对象,这样能够减少不必要的临时对象的创建、拷贝以及销毁,能够大幅度提高 C++ 应用程序的性能。临时对象的维护 ( 创建和销毁 ) 对性能有严重影响。
- 转移语义是和拷贝语义相对的,可以类比文件的剪切与拷贝,当我们将文件从一个目录拷贝到另一个目录时,速度比剪切慢很多。
- 通过转移语义,临时对象中的资源能够转移其它的对象里。
- 在现有的 C++ 机制中,我们可以定义拷贝构造函数和赋值函数。要实现转移语义,需要定义转移构造函数,还可以定义转移赋值操作符。对于右值的拷贝和赋值会调用转移构造函数和转移赋值操作符。如果转移构造函数和转移拷贝操作符没有定义,那么就遵循现有的机制,拷贝构造函数和赋值操作符会被调用。
- 普通的函数和操作符也可以利用右值引用操作符实现转移语义。
但是这几点总结的不错
- std::move执行一个无条件的转化到右值。它本身并不移动任何东西;
- std::forward把其参数转换为右值,仅仅在那个参数被绑定到一个右值时;
- std::move和std::forward在运行时(runtime)都不做任何事。