C++左值、右值、引用与拷贝

1. 左值描述着一个持久的数值,它在之后会被多次使用。
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(完美转发)、引用折叠

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值