int a = 1;
a = 2;
a = 3;
其中a是左值。
2. 右值描述着一个临时生成的数值,它在一系列运算之后将会消失于最后一次浅拷贝。或者直接丢弃。
int a = 1+1;
其中1是右值,1+1是运算,随后生成右值2。右值2被拷贝入a后被销毁。
2+3;
其中2+3生成右值5,在没有任何赋值运算后值被丢弃。
区分左值与右值最好的方式是对值取地址,取地址符不报编译错则则是左值。因为取地址运算符只支持左值。
3. 引用表示与其他值产生引用关系,对引用的修改会牵连其对应的值。(这里是左值引用)
int a = 1;
int&b = a;
b = 2;
执行后b与a同时变为2
4. 实际上引用是指针常量的语法糖
如上代码与如下代码产生近乎一样的汇编。
int a = 1;
int* const b = &a;
*b = 2;
5. 引用可以减少拷贝次数,因为它可以和指针一样传递内存地址,而非拷贝值。
6. 右值引用与左值引用类似,右值引用其引用是右值。
但由于右值大多时候是没有内存地址的,比如
int a = 1 + 1;
此时1 + 1的结果2是右值,但汇编中并没有为2分配内存。
那么引用是如何做到像指针一样引用右值的?
编译器选择给右值分配了个地址并且存放右值。
int&& a = 1 ; //右值引用
a = a + 1;
int b = a;
其中[ebp-18h]就是右值的地址,而[a]存放的就是右值的地址。
是如下代码的语法糖,它们生成一样的汇编指令。
int _tmp = 1;
int* const a = &_tmp;
*a = *a + 1;
int b = *a;
7. 深拷贝与浅拷贝,都是拷贝对象的一种方式,区别于指针。
通常情况下:
深拷贝会为对象内的指针指向的对象也进行拷贝动作,也就是真正意义上的完整拷贝。
浅拷贝只会拷贝指针本身,拷贝后的对象内的指针与源对象的指针指向同一个对象。
具体的深拷贝与浅拷贝的实现由拷贝构造函数、移动构造函数、拷贝赋值运算符重载、移动赋值运算符重载完成。
8. 拷贝构造函数与移动构造函数是两种特殊的构造函数,他们以一个自身类的实例作为参数,来构造近乎一样的实例。
拷贝构造函数接受一个左值引用,其作用一般是深拷贝的方式对传入的对象进行拷贝。此时拷贝的对象与被拷贝的对象可以共存。
参考左值描述与如下代码
移动构造函数接受一个右值引用,其作用一般是浅拷贝的方式对传入的对象进行拷贝。此时被拷贝的对象内容被销毁 ,其内容被移动到自身。
参考右值描述与如下代码
9. 赋值运算符重载
赋值运算符重载也可以存在左值与右值两种不同的赋值方式。与拷贝构造函数、移动构造函数类似。
不过需要注意拷贝时检查是不是自己给自己赋值。避免UAF(释放后重用错误)与不必要的开销。
10. std::move的作用是将左值引用转换为右值引用,常用于拷贝前。
左值拷贝时使用深拷贝,右值拷贝时是浅拷贝,并且销毁源对象。参考左值描述、右值描述。
使用std::move可以把左值转换为右值,所以紧跟着的拷贝会从深拷贝变成浅拷贝。同时销毁被转换的左值。
std::move的实现:他只是将参数通过static_cast转换成了右值引用。
当不再使用某个左值的时候,并且想把它放入一个地方存储起来。std::move可能是比较不错的选择。
当然,这也同时需要对应对象的拷贝支持移动赋值与移动构造。参考深拷贝与浅拷贝
11. std::move滥用导致额外的浅拷贝
正常返回一个字符串类
本意是避免返回时候的深拷贝,写的std::move。
实际上返回一个类时,并不会产生深拷贝。这是编译器最基本的优化措施。
实际上编译器会对返回值进行优化。(更多资料参考 返回值优化、RVO)
它优化后的结果像是将return 后面的表达式直接与外部的语句进行拼接。
如上图11-1产生的效果类似于:
String data = String("1.0.0 beta")
变成了一个构造函数。没有任何拷贝。
如下是编译器真正在代码层面做的优化:
它和以下代码生成的汇编类似:
从图中可以看出,实际上是将返回的对象当作参数传入。除了一个字符指针的深拷贝以外,不存在任何其他拷贝行为
[tip]:有一点区别是此处调用的是赋值运算符重载,而编译器优化的是构造函数,由于构造函数只能在初始化使用,所以无法完全模拟。。
但在错误的写法中,返回时添加std::move将导致返回的String类变成了右值。
此时在函数返回时无法优化,而会进行浅拷贝到函数外。
这反而导致了一次不必要的浅拷贝。
第二张图中std::move的代码与如下代码生成的汇编近乎一致:
output = std::move(_tmp)处产生的一次浅拷贝。
参考文章:
https://github.com/isocpp/CppCoreGuidelines/blob/036324/CppCoreGuidelines.md#Rf-return-move-local
12. 右值引用变量本身也是左值。但使用右值引用类型作返回值的函数,返回的是右值。
如下代码能够编译通过:
int&& func(int& a)
{
return std::move(a);
}
int main()
{
int&& a = 1;
int* c = &a;
int b = func(a);
return 0;
}
但如下代码会产生错误:
int&& func(int& a)
{
return std::move(a);
}
int main()
{
int&& a = 1;
&func(a);
return 0;
}
13. C的赋值运算符返回右值,C++的赋值运算符返回左值?为什么要改。
C语言:
C++:
如果C++赋值运算返回右值则如下代码可能得到非预期结果:
由于赋值运算是右结合(即同优先级运算先算右边)
拆解后会变成:
右值引用在最后一次浅拷贝被销毁。参考右值描述。
此时b因被作为右值处理而被销毁。这一般不是我们想要的。
如果当做左值看待:
则a=b会执行深拷贝,则a和b都可以被多次使用。参考左值描述
C语言为什么不使用左值?一方面可能是当时没想到,另一方面可能是设计思想的不同。
C++采用面向对象的设计思想,赋值运算在C++里面是对象与对象之间的复制行为,这种行为应该由对象自行定义。
C语言的设计思想是面向过程和面向底层,赋值运算在C中的概念是对内存的一个修改行为。 变量表示的是一块内存的地址,返回的应当是内存被修改后的值。
更多相关知识:std::forward(完美转发)、引用折叠