C++之右值引用和移动构造函数
提纲
1、右值引用
2、移动构造函数
3、总结
1、右值引用
什么是右值引用呢?要搞明白右值引用,必须先搞清楚什么是右值和左值,其次必须搞清楚什么是值引用。
1.1 左值和右值
左值一般都是带有内存地址的变量,而右值一般是立即数或者运算过程中的临时对象,这种对象不会有地址值。举例说明如下
int main(void) {
int i = 10;
int j = 11;
int sum = i + j;
}
上面这一段代码中10,11,(i+j),这三个是属于右值,因为它本身没有内存地址,除非把它们放入到栈中或者堆中。
而i,j,sum这三个是属于左值,因为它们是线程栈上地址的标识符。
能用取址符号 & 取出地址的皆为左值,剩下的都是右值。而且,匿名变量一律属于右值。
std::move() 能把左值强制转换为右值。
移动构造实现一节的例程我们把语句 Integer b(temp); 改为 Integer b(std::move(temp)); 后,运行结果如下。
1.2 值类型
值类型 作为方法参数或者返回值时会生成自身的副本,如果 值类型 很大,那一来一回生成若干个深复制的 临时对象 将会产生巨大的性能开销。
1.3 左值引用和右值引用
知道了左右值概念,接下来理解左右值引用就很简单了,既然是引用,必然是多个变量指向同一个地址,用代码举例如下:
int main(void) {
int i = 10;
int& k = i; //左值引用
int&& m = 10; //右值引用
}
上面代码中的k是一个引用,但是它是一个指向左值的引用,上面的代码中的m是一个引用,但是它指向的是10这个常量的引用,常量10没有明确的内存地址,不能给常量10赋值,所以它是右值,指向它的引用就是右值引用。
所以,简单来讲,右值引用就是指的是指向右值的一个引用。
2、移动构造函数:右值引用如何减少对象的创建
移动构造函数是c++11的新特性,移动构造函数传入的参数是一个右值 用&&标出。
首先讲讲拷贝构造函数:拷贝构造函数是先将传入的参数对象进行一次深拷贝,再传给新对象。这就会有一次拷贝对象的开销,拷贝的内存越大越耗费时间,并且进行了深拷贝,就需要给对象分配地址空间。而移动构造函数就是为了解决这个拷贝开销而产生的。
右值引用的好处是可以减少创建对象,从而节省内存空间,加快程序的执行性能。那么它是如何减少对象创建的呢?
减少临时对象的创建,无非就是在运算过程中复用一些对象,不需要每次都走赋值构造函数来进行深复制,图示如下:
整体的思路就如上图所示,下面我们举例说明。
#include <iostream>
#include <vector>
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);
}
从这个例子中可以看到,s1+s2 操作中出现了一次 深copy,具体代码出现在 return 处,tmp对象返回后赋值给s3的过程中,发生了tmp到s3的深度复制。
因为是深复制,所以会再次生成一个 new char[] ,如果 new char[] 很大,那将会是不必要的性能开销,能不能像我画的图一样,将 s3 中的 str 指针直接指向 tmp 所持有的 heap 上的 char[] 数组来达到复用目的呢? 肯定是可以的。
这里需要用 右值引用 + 移动构造函数 让 s3.str 指向 tmp.str,从而避免复制构造函数,在 StringBuilder 类中加一个方法如下:
StringBuidler(StringBuidler &&s)
{
this->str = s.str;
this->length = s.length;
s.str = nullptr;
}
有了这个移动构造函数后,StringBuilder的加号运算符方法在执行到return语句的时候,深复制的赋值构造函数就没有了,这个移动构造函数会在 return 处被调用,编译器会判断如果是右值的话,自动走移动构造函数,没有这个函数就会走赋值构造函数。
上面的程序中,s3=s1+s2这一条语句会调用Stringbuilder的加法运算法函数,在函数中的return语句处需要返回的是一个tmp局部变量,因此它此时就是一个tmp临时变量,因为在函数结束后它就消亡了,对应的其动态内存也会被析构掉,所以系统在执行return函数之前,需要将tmp对象复制到s3对象中,此处自然就有两种解决方法:1、调用复制构造函数进行复制;2、使用移动构造函数把即将消亡tmp对象的内存的所有权进行转移,手动延长tmp对象占用内存的生命周期。
显然,前者需要深拷贝操作依次复制全部数据,而后者只需要“变更所有权”即可。
上面的运行结果中第一次析构就是return tmp; 这个临时对象在转移完内存所用权之后就析构了。
此处更需要说明的是:遇到这种情况时,编译器会很智能帮你选择类内合适的构造函数去执行,如果没有移动构造函数,它只能默认的选择复制构造函数,而同时存在移动构造函数和复制构造函数则自然会优先选择移动构造函数。
3、总结
你有一本书,(对应一个对象A)
你不想看,(这个对象A不需要再使用了)
但我很想看,(需要新建一个一样的对象B)
那么我有哪些方法可以让我能看这本书?
有两种做法,(两种做法其实对应的就是拷贝构造函数和移动构造函数)
一种是你直接把书交给我,(对应移动构造函数,资源发生了转移)
另一种是我去买一些稿纸来,(买一些稿纸意味着重新申请一块资源)
然后照着你这本书一字一句抄到稿纸上。(把原来的对象A拷贝到对象B,这时存在两个内容一样的对象,但对象A用不到了就浪费了)
这个例子用于体现拷贝构造函数和移动构造函数的不同点非常契合。
参考资料
1、https://baijiahao.baidu.com/s?id=1739395293413891144&wfr=spider&for=pc
2、https://www.jianshu.com/p/66e511a11209
3、https://blog.csdn.net/weixin_44788542/article/details/126284429
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通