? 优质资源分享 ?
学习路线指引(点击解锁) | 知识定位 | 人群定位 |
---|---|---|
? Python实战微信订餐小程序 ? | 进阶级 | 本课程是python flask+微信小程序的完美结合,从项目搭建到腾讯云部署上线,打造一个全栈订餐系统。 |
?Python量化交易实战? | 入门级 | 手把手带你打造一个易扩展、更安全、效率更高的量化交易系统 |
一: 背景
最近在看 C++ 的右值引用和移动构造函数,感觉这东西一时半会还挺难理解的,可能是没踩过这方面的坑,所以没有那么大的深有体会,不管怎么说,这一篇我试着聊一下。
二: 右值引用
1. 它到底解决了什么问题?
在其他编程语言中,很少听到 右值引用
这个词,我个人感觉还是 C++
这个 值类型
优先的语言基因决定的,我们都知道 值类型
作为方法参数或者返回值时会生成自身的副本,如果 值类型
很大,那一来一回生成若干个深复制的 临时对象 将会产生巨大的性能开销。
总结一句话:
右值引用
就是尽可能的减少这中间临时对象
个数,尤其是关联到 heap 上的对象,仅此而已。
2. 右值引用是个什么样子?
说到 右值引用
得先说什么是 右值,左值
, 左值
一般都是带有内存地址的变量,而 右值
一般是立即数或者运算过程中的临时对象,这种对象不会有地址值,是不是很绕,我举个例子吧。
int main()
{
int i = 10;
int j = 11;
int sum = i + j;
}
- 10,11,(i+j)
属于右值,因为它本身没有内存地址,除非把它们放入到栈中或者堆中。
- i,j,sum
属于左值,因为它们是线程栈上地址的标识符。
知道了 左右值
概念,接下来理解 左右值引用
就很简单了,既然是 引用
,必然是多个变量指向同一个地址,对吧,修改下代码如下:
int main()
{
int i = 10;
int& k = i; //左值引用
int&& m = 10; //右值引用
}
接下来看下汇编代码:
33: int i = 10;
00FB182F mov dword ptr [ebp-0Ch],0Ah
34: int& k = i;
00FB182F mov dword ptr [ebp-0Ch],0Ah
00FB1836 lea eax,[ebp-0Ch]
00FB1839 mov dword ptr [ebp-18h],eax
36: int&& m = 10;
00FB183C mov dword ptr [ebp-30h],0Ah
00FB1843 lea eax,[ebp-30h]
00FB1846 mov dword ptr [ebp-24h],eax
从汇编代码看,它们是一模一样的,也就是说在汇编层面,其实并没有 右值引用
和 左值引用
一说。
有了这些基础,我们来看下更复杂的 class 结构。
三: 右值引用如何减少对象的创建
1. 简要思路
其实仔细想一想,减少临时对象的创建,无非就是在运算过程中复用一些对象,不需要每次都走赋值构造函数来进行深复制,画个图就像下面这样。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KxzPc3fC-1658769092301)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/18255fed94894da98f5a7d1e9246274e~tplv-k3u1fbpfcp-zoom-1.image)]
明白了这个思路,接下来我们举一个例子说明。
2. 一个简单的例子
C++ 最烦的地方就是有太多的构造函数
, 数不胜数,太尴尬了,这里我做一个简单的 +
操作例子。
#include
#include
using namespace std;
class StringBuidler {
public:
char* str;
int length;
public:
StringBuidler() {}
StringBuidler(int len, char c) {
this->str = new char[len];
this->str[0] = c;
this->length = len;
}
StringBuidler(const StringBuidler& s) {
printf("StringBuidler:深复制 \n");
this->length = s.length;
this->str = new char[s.length];
for (size\_t i = 0; i < length; i++)
{
this->str[i] = s.str[i];
}
}
StringBuidler operator+(const StringBuidler& p) {
StringBuidler tmp;
tmp.length = this->length + p.length;
tmp.str = new char[tmp.length];
int index = 0;
for (size\_t i = 0; i < this->length; i++)
{
tmp.str[index++] = this->str[i];
}
for (size\_t i = 0; i < p.length; i++)
{
tmp.str[index++] = p.str[i];
}
return tmp;
}
};
int main()
{
StringBuidler s1(10, 'a');
StringBuidler s2(5, 'b');
StringBuidler s3 = s1 + s2;
printf("s3.length=%d, s1.length=%d, s2.length=%d \n", s3.length, s1.length, s2.length);
}
折叠
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sRhfJKyW-1658769092306)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ce067b1c64f04190acaa99d56b1ddc8b~tplv-k3u1fbpfcp-zoom-1.image)]
从这个例子中可以看到,s1+s2
操作中出现了一次 深copy
,具体代码出现在 return
处,汇编代码如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IygVwMgC-1658769092307)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/2d6079bf88064cf699d12ea0e1e9a4a2~tplv-k3u1fbpfcp-zoom-1.image)]
因为是深复制,所以会再次生成一个 new char[]
,如果 new char[]
很大,那将会是不必要的性能开销,能不能像我画的图一样,将 s3 中的 str
指针直接指向 tmp 所持有的 heap 上的 char[]
数组来达到复用目的呢? 肯定是可以的。
3. 性能优化方案
这里需要用 右值引用
+ 移动构造函数
让 s3.str
指向 tmp.str
,从而避免复制构造函数,在 StringBuilder 类中加一个方法如下:
StringBuidler(StringBuidler&& s) {
this->str = s.str;
this->length = s.length;
s.str = nullptr;
}
然后把程序跑起来,截图如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AgrUrTT3-1658769092308)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6f6032ba261c44a5a2de6bba5b822616~tplv-k3u1fbpfcp-zoom-1.image)]
可以看到,深复制已经没有了,这个过程会在 return
处被调用,编译器会判断如果是右值的话,自动走 移动构造函数
,没有这个函数就会走 赋值构造函数
。
四: 总结
总之 右值引用
可以让你尽可能的复用一些中间对象,达到一个性能上的提升,其实对 C# 程序员来说,这么简单的引用赋值,C++ 搞出了这么多概念,真的很难理解,可能还是那句话,这是 C++ 的值类型优先的基因决定的。
转载请注明:xuhss » 聊聊 C++ 右值引用 和 移动构造函数