左值和右值

左值:能用在赋值语句等号左侧的东西, 它能够代表一个地址
右值:不能作为左值的值就是右值,右值不能出现在赋值语句中等号的左侧

结论:
C++中的一条表达式,要么就是右值,要么就是左值,不可能两者都不是。

左值有的时候能够被当作右值使用

i = i + 1,i是个左值,不是个右值,虽然它出现在了等号右边

i用在等号右边的时候,被当作一个具体的值,我们说i有一种右值属性(不是右值)
i出现在等号左边的时候,用的是i这个变量代表的内存中的地址,这就被称为左值属性

因此一个左值,同时具有左值属性和右值属性

用到左值的运算符有哪些?

1.赋值运算符

1
2
int a;
printf("%d\n", a = 4); //4

整个赋值语句的结果仍然是左值 (a = 5) = 8;

2.取地址&

1
2
3
int a = 5;
&a;
//不可以 &5

3.容器的下标[]都需要左值,

1
2
3
string a = "fengyun"
a[0];
//不可以 fengyun[0]

4.迭代器的–,++需要左值

1
2
vector<int>::iterator iter;
iter++;

一般而言,如果运算符不能对字面量值进行操作的,这个运算符就需要用左值。

左值:代表的是一个地址,所以左值表达式的求值结果,就是得到一个对象,就得有地址。
但是求值结果为对象的表达式不代表一定是左值,需要具体分析。

引用分类

  • 左值引用:即绑定到左值。
  • const引用:常量引用,也是左值引用。不希望改变值得对象
  • 右值引用:它是一个引用,引用的对象侧重于临时变量,生命周期短。
    int &&refrightvalue = 3;//绑定到了一个常数
    refrightvalue = 5;

左值引用&

即绑定到一个左值上。

引用不存在“空引用”这个说法,所以左值引用初始化的时候必须绑定到一个左值上。

1
2
3
4
5
6
7
8
9
10
int a = 1;
int &b{ a };
int &c;//error
int &c = 1;//error,不可绑定到右值


const int &c = 1;//const引用可以绑定到右值,所以const引用特殊
等价于
int tmp = 1;
const int &c = tmp;

右值引用&&

即绑定到一个右值。必须绑定到右值的引用。

C++11右值引用是希望我们用右值引用绑定一些即将销毁的或者是一些临时对象上。
因此我们可以把右值引用理解成一个对象的名字。

能绑定到左值上的引用,一般都不能绑定到右值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
string t1("fengyun");
string &r1{ t1 };

string &r2{"fengyun"};//error,左值引用不可以绑定到临时变量。临时变量被地址当作右值

const string &r3{"fengyun"};//创建一个临时变量,绑定到左值r3上去

string &&r4{ t1 };//error

string &&r5{"fengyun"};//可以绑定到一个临时变量

int i = 10;
int&r6 = i; //正确,左值引用

int &&r7 = i * 100;//可以
int &r8 = i * 100;//error,i本来是左值,但是i*100变为了右值。

右值引用的引入目的

  1. c++11引入&&代表一种新数据类型,引入新数据类型肯定有目的。
  2. 提高程序运行效率。 把拷贝对象变成移动对象来提高程序运行效率。(假设对象A不再使用,我可以直接把对象A开辟的内存块直接转给对象B,将A的指针将置为nullptr,但内存没有回收,B也无需new一段内存,这块内存是转换了一个“主人”)
  3. 移动对象如何发生? 移动赋值运算符和移动拷贝构造函数需要参数类型是右值引用,右值引用也就是为了提高系统运行速率。

总结

返回左值引用的函数,连同赋值,下标,解引用和前置递增递减运算符(–i), 都是返回左值表达式的例子;我们可以将一个左值引用绑定到这样的例子上。

返回非引用类型的函数,连同算术,关系,位以及后置递增运算符(i–),都生成右值,不能将一个左值引用绑定到这类表达式。
但是我们可以将一个const的左值引用或者一个右值引用绑定到这类表达式

1
2
3
4
5
6
7
8
9
10
11
int i = 100;
(++i) = 199;//i被赋值成199

int &&r1 = i++;//先生成一个临时变量_tempi,记录i的值(_tempi = i)的值用于使用的目的,再给i+1;
//接着返回这个临时变量。临时变量这个东西,左值引用是不可以绑定的,右值引用却可以
//但是此后i和r1没有任何关系了,r1绑定的是临时变量。

int &r2 = i++;//error
int &r3 = ++i;//r3变成i的别名了

int &&r4 = ++i;//error

重点强调:

  • 虽然r1是右值引用(绑定到了右值),但是要把r1看成一个变量,r1本身是一个左值,在=号左边呆着。
1
2
int && r1 = 100;
int &r2 = r1;//r1是左值
  • 所有变量,看成左值。因为他们是有地址的,而且用右值引用也绑定不上。
1
int &&r3 = r1;
  • 任何函数里边的形参都是左值。void f(int &&w); w是右值引用,但w本身是左值。

  • 临时对象都是右值。

std::move

C++11标准库里的新函数。

虽然move的中文意思是“移动”,但是这个函数本身没有做任何移动的操作。
move只有一个能力:把一个左值强制转换为一个右值。右值引用就可以绑定到原本是左值的变量上。

1
2
3
4
5
6
7
8
9
10
11
12
int i = 10;
int &&ri = i;//error;

//以下绑定成功
int i = 10;
int &&ri = std::move(i);//将左值转换成一个右值
i = 20;//i变为20,ri也会变为20
ri = 15;//ri代表i了,ri和i都变为15

//
int && ri2 = 100;
int && ri3 = std::move(ri2);//绑定成功

一个有趣现象:

1
2
string st = "fengyun";
string st2 = std::move(st);//执行完这一句之后st变为了"";

这是因为``std::move(st)是一个右值引用,string st2 = std::move(st); `自动触发了string的移动构造函数,将st的内容转移到了st2中。而不是std::move转移的。

1
2
3
string st = "fengyun";
string &&st2 = std::move(st);//这是一个右值绑定
//绑定到一块去了,st和st2

一般而言,我们std::move(st)后,我们写代码的不应该再使用st了。(这是系统不希望我们这么使用st,非要继续用st系统也拿我们没办法)。

临时变量

1
2
3
int &&r1 = i++;//先生成一个临时变量_tempi,记录i的值(_tempi = i)的值用于使用的目的,再给i+1;
//接着返回这个临时变量。临时变量这个东西,左值引用是不可以绑定的,右值引用却可以
//但是此后i和r1没有任何关系了,r1绑定的是临时变量。

另外一些临时对象(临时变量)是因为我们代码书写问题而产生的。

产生临时对象的情况与解决

1.以传值的方式给函数传递参数

1
2
3
4
5
6
int CTempValue:: Add (CTempValue tobj);

int main(){
CTempValue t1;
t1.Add(t1);//会执行拷贝构造函数和析构函数
}

2.类型转换生成的临时对象/隐式类型转换以保证

1
2
3
4
5
6
CTempValue sum;

sum = 1000;//这里会产生一个真正的临时对象
//1.调用构造函数和析构函数先生成一个临时对象
//2.调用拷贝赋值运算符将这个对象里边的各个成员赋给了sum对象
//3.销毁这个临时对象

优化:

1
CTempValue sum = 1000;//避免不必要的拷贝构造函数

3.函数返回对象的时候。

对象移动

  • A移动到B后,那么A对象我们就不能再使用了
  • 移动:并不是把内存中的数据从一个地址移到另一个地址。是“所有者”变更。

拷贝构造函数:Test::Test(const Test& tmpTest){......}const左值引用

移动构造函数:Test::Test(const Test &&tmpTest){......}右值引用

移动构造函数和移动赋值运算符应该完成的功能
(1)完成必要的内存移动,斩断原对象和内存的关系。
(2)确保移动后源对象处于一种“即便被销毁也没有什么问题”的一种状态。B <- A

image-20220302135802618

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
69
70
71
72
73
74
75
#include <iostream>
#include <vector>

using namespace std;

class B
{
public:
int m_bm;
};

class A
{
private:
B* m_pb;
public:
A() :m_pb(new B())
{
cout << "A() :m_pb(new B())" << endl;
}
A(const A& tmpa) :m_pb(new B(*(tmpa.m_pb)))
{
cout << "A(const A& tmpa) :m_pb(new B(*(tmpa.m_pb)))" << endl;
}
A(A&& tmpa):m_pb(tmpa.m_pb)//指针所有者变化
{
tmpa.m_pb = nullptr;
cout << "A(A&& tmpa):m_pb(tmpa.m_pb)" << endl;
}

A& operator =(const A& src)
{
if (this != &src) {
delete m_pb;
m_pb = new B(*(src.m_pb));//重新分配一块内存
std::cout << "A& operator =(const A& src)" << endl;
}
return *this;
}

A& operator =(A&& src)noexcept
{
if (this != &src) {
delete m_pb; //清空字节内存
m_pb = src.m_pb; //对方内存直接拿来
src.m_pb = nullptr; //斩断源头
std::cout << "A& operator =(A&& src)noexcept" << endl;
}
return *this;
}

virtual ~A()
{
delete m_pb;
cout << "~A()" << endl;
}
};

static A getA()
{
A a;
return a;
}

int main() {

A a = getA();

A a1(a); //1个拷贝构造函数:
A a2(std::move(a)); //std::move() ,建立了新对象,调用新对象a2的移动构造函数
A &&a3 (std::move(a)); //这里没有建立新对象,根本不会调用什么移动构造函数
//等同于把对象a有了一个新别名a3;建议后续用a3来操作

return 0;
}

注意我们定义了移动构造函数,那么A a = getA();先调用构造函数然后调用移动构造函数,析构函数,如下

1
2
3
A() :m_pb(new B())
A(A&& tmpa):m_pb(tmpa.m_pb)
~A()

如果未定义移动构造函数,那么A a = getA();先调用构造函数,再调用拷贝构造函数,析构函数:如下:

1
2
3
A() :m_pb(new B())
A(A&& tmpa):m_pb(tmpa.m_pb)
~A()

如果定义了移动构造函数,且A &&a = getA()从getA()返回的临时对象被a接管了。1个构造函数,1个移动构造函数,1个析构函数

main函数中

1
2
3
A a = getA();
A a2;
a2 = std::move(a);

运行结果:

1
2
3
4
5
6
7
A() :m_pb(new B())
A(A&& tmpa):m_pb(tmpa.m_pb)
~A()
A() :m_pb(new B())
A& operator =(A&& src)noexcept
~A()
~A()

合成的移动操作

某些条件下,编译器能合成移动构造函数,移动赋值运算符

  • 但是如果有自己的拷贝构造函数,自己的拷贝赋值运算符,或者自己的析构,那么编译器就不会为它合成移动构造函数和移动赋值运算符
    所以有一些类是没有移动构造函数和移动赋值运算符。
  • 如果我们没有自己的移动构造函数和移动赋值运算符,那么系统会调用我们自己写的拷贝构造函数和拷贝赋值运算符来代替。
  • 只有一个类没定义任何自己版本的拷贝构造成员,且类的每个非静态成员都可以移动时,编译器才会为该类合成移动构造函数或者移动赋值运算符

什么叫成员可以移动?

  • 内置类型是可以移动的
  • 类类型的成员,则这个类要有对应的移动操作相关的函数,就可以移动。