移动语义

左值和右值

左值是指表达式执行结束后仍然存在的持久对象,右值是指表达式执行结束后就不再存在的临时对象,如何区分左值和右值?

左值可以取地址

右值不能取地址

字符串常量 例如 “word” 是左值,因为

1
cout << &"word" << endl;

是可以进行取值的

关于左值和右值的存储位置

关于右值的存储位置他们可能存储在内存中,也可以存在寄存器,这具体取决于实现和上下文

在内存中存储:当他们是较大的对象或者编译器决定这样做更高效时,就可能存在内存中

存在寄存器中:对于简单的右值,编译器就可能存在寄存器中

总而言之,右值就是马上要销毁的

引用绑定左值

1
2
int a = 10;
int &ref = a;

引用虽然不能绑定右值,但是可以使用const &来绑定

1
const int & ref = 1;

可以通过右值引用来分辨左值和右值

右值引用

1
2
3
4
5
int & r1 = a;//非const左值引用
const int &r1 = a;//const左值引用

int && ref1 = 1;//非const右值引用
int && ref1 = a;//error 右值引用只能绑定右值

并且右值引用本身也可以取值,

1
2
int && ref1 = 1;//非const右值引用
&ref1;//successful

但右值引用一定是左值吗?

不一定

拷贝构造传入的是一个右值的时候,会先创建临时对象,在进行复制,再销毁,很浪费性能,对该过程进行优化,直接让
(1)调用拷贝构造的那个对象的数据成员指针指向匿名对象新开辟的空间,
(2)然后让匿名对象的指针置为null,但这个过程不能再拷贝构造中修改,因为拷贝构造传入的参数不一定是右值,所以引出了移动构造函数

移动构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class String{
public:
String(const char * pstr)//普通拷贝构造
:_data(new char[strlen(pstr)+1])
{
strcpy(_data,psrt);
}
//移动构造
String(String && rhs)
:_data(rhs._pstr)//第(1)步
{
rhs._pstr = nullptr;//第(2)步
}

private:
char * _data;

}

加上编译器的去优化参数 –fno-dlide-constructors

传入右值 会发现再没有调用拷贝构造,而是调用了移动构造

移动构造函数的特点:

1.如果没有显式定义构造函数、拷贝构造、赋值运算符函数、析构函数,编译器会自动生成移动构造,对右值的复制会调用移动构造。

2.如果显式定义了拷贝构造,而没有显式定义移动构造,那么对右值的复制会调用拷贝构造。

3.如果显式定义了拷贝构造和移动构造,那么对右值的复制会调用移动构造。

总结:移动构造函数优先级高于拷贝构造函数。

可以理解为:如果显式定义了拷贝构造和移动构造,利用一个已存在的对象创建一个新对象时,会先尝试调用移动构造,如果这个对象是右值,就使用移动构造函数创建出新对象,如果这个对象是左值,移动构造使用不了,就会调用拷贝构造。


同拷贝构造,复制运算符传入右值也会出现繁琐冗余的创建销毁的操作,同样可以优化

再写一个移动赋值函数

1
2
3
4
5
6
7
8
STring & operator(String && rhs){
if(this != &rhs){
delete [] _pstr;
_pstr = rhs._pshr;
rhs._pshr = nullptr;
}
return *this;
}

移动赋值函数的特点:

1.如果没有显式定义构造函数、拷贝构造、赋值运算符函数、析构函数,编译器会自动生成移动赋值函数。使用右值的内容进行赋值会调用移动赋值函数。

2.如果显式定义了赋值运算符函数,而没有显式定义移动赋值函数,那么使用右值的内容进行赋值会调用赋值运算符函数。

3.如果显式定义了移动赋值函数和赋值运算符函数,那么使用右值的内容进行赋值会调用移动赋值函数。

移动赋值函数优先级也是高于赋值运算符函数

总结:

将拷贝构造函数和赋值运算符函数称为具有复制控制语义的函数;

将移动构造函数和移动赋值函数称为具有移动语义的函数(移交控制权)

具有移动语义的函数优于具有复制控制语义的函数执行。

那移动赋值函数中的自复制判断是否还有必要?

有必要

因为有一个函数std::move()函数可以将左值转为右值,因为有一些使用移动语义的场景下,需要将左值转为右值

1
2
3
int a = 1;
&(std::move(a));//error 左值转为了右值
int && ref = std:move(a);//可以绑定
1
2
3
4
String s1("hello");
String s2 = move(s1);
//这样的操作会影响s1对象本体的内容
//最终的效果是s1申请的堆空间的管理权被移交给了s2

自复制判断的必要性

1
2
3
String s1("hello");
s1 = move(s1);//这种情况就需要判断自赋值
//如果没有自复制判断的话,移动复制运算符函数一上来就把s1给置空了,然后s1就变成空指针了

右值引用本身的性质

匿名的右值引用本身也是右值属性

1
2
3
4
5
6
7
8
9
int && func4(){
return 10;
}

void test(){
//有名字的是左值
int && ref = func4();
&ref;
}

但是有名字的是左值

1
2
3
4
5
6
7
8
9
10
11
//按道理来说,这里返回值这里发生拷贝构造,返回的是一个副本
//但是其实return调用的是移动构造,将str1对”wangdao“的管理权给了匿名对象(跟上面正好相反了 倒反天罡)
String func2(){
String str1["wangdao"];
return str1;
}
void test(){
&func2();//error 右值
String && ref = func2();
&ref;
}

str1对象再func2执行完时就会销毁(将亡对象)

此时不会调用拷贝构造,而是调用移动构造,将str1申请的堆空间的管理权移交给一个匿名的对象(func2的返回值)

如果返回值的生命周期比函数的生命周期长,那就不会调用拷贝构造而是调用移动构造

1
2
3
4
5
6
7
String str1["wangdao"];
String func2(){
return str1;//返回值为str1的副本,调用拷贝构造
}
void test(){
func2();
}

总结:当类中同时定义移动构造函数和拷贝构造函数,需要对以前的规则进行补充,调用哪个函数还需要取决于返回的对象本体的生命周期