详解c++移动构造函数和移动赋值运算符在代码性能中的作用

对象移动

对象移动,就是把一个不想用了的对象A中的一些有用的数据提取出来,在构建新对象B的时候就不需要重新构建对象中的所有数据——从不想用了的对象A中提取出来的有用数据在构建对象B时都可以拿来使用。 

我们知道,拷贝构造函数、拷贝赋值运算符等,对对象复制的成本是很高的,尤其是容器,里面如有几千个元素,那么如果对这个容器对象进行复制,里面的元素都要逐个复制,非常影响程序运行效率。

为此,提出了移动构造函数和移动赋值运算符的概念,显然,移动这件事效率会很高,比复制效率高得多,如果源对象A不再使用,那么,直接把源对象A中的某些new出来的数据移动给目标对象B,那就相当于数据还是这一堆数据,只是属主换了另外一个人,这种数据移动的效率,显然比数据复制就高,甚至某些情况下会高很多。

如果复制数据,如要把对象A复制给对象B,那对象A里面的数据还能使用,但如果把对象A(实际上是对象A中部分数据)移动给对象B对象A的数据就会出现残缺),那显然对象A就不能再被使用,否则因为数据的残缺可能会导致出现问题

移动构造函数的语法格式

我们知道,拷贝构造函数的语法格式如下,与普通构造函数的区别仅在于其参数的类型是const引用类型

	tempVal(const tempVal& t) :v1(t.v1), v2(t.v2) {cout << "调用拷贝构造函数" << endl;}

与之相似,移动构造函数的语法格式也仅仅是在参数类型上做了修改,将const引用更换为右值引用即可

也就是

	tempVal(const tempVal&& t) :v1(t.v1), v2(t.v2) {};

有关右值引用及其与临时对象的关系可参考

c++临时对象的探讨及右值引用在临时对象中的作用-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/qq_58158950/article/details/135490225?spm=1001.2014.3001.5502

左值、右值、左值引用与右值引用-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/qq_58158950/article/details/134299061?spm=1001.2014.3001.5502

移动构造函数作用演示

接下来我们通过代码来清晰的看到移动构造函数是如何实现代码性能提升的

我们首先定义一个类B

  • 成员变量:int型的m_pm
  • 成员函数:一个构造函数一个拷贝构造一个析构函数
class B
{
public:int m_pm;B(int m=0):m_pm(m){cout<<"类B的构造函数调用了"<<endl;}B(const B& b):m_pm(b.m_pm){cout<<"类B的拷贝构造函数调用了"<<endl;}virtual ~B(){cout<<"类B的析构函数调用了"<<endl;}
};

接着我们定义一个类A

  • 成员变量:类B的指针成员m_pb
  • 成员函数:构造函数、深拷贝构造函数和析构函数

最后我们再定义一个简单的函数

static A getA()
{A a;return a;
}

在测试函数中调用该函数,并运行观察结果(注意使用-fno-elide-constructors关闭编译器的优化选项

void test()
{A a=getA();
}

观察运行结果可以看到,系统一共执行了一次普通构造函数和两次拷贝构造函数的调用,其中

  • 普通构造函数:是为了创建getA函数中的局部对象A
  • 第一个拷贝构造函数:是getA函数返回时生成的临时对象
    (有关临时对象的生成参考c++临时对象的探讨及相关性能提升-CSDN博客)
  • 第二个拷贝构造函数:测试函数test中getA返回的临时对象拷贝给test中的a引起

实验一

接下来,我们定义一个移动构造函数,再次实验观察运行结果

	A(A&& tmpa) noexcept:m_pb(tmpa.m_pb) {tmpa.m_pb=nullptr;cout<<"类A的移动构造函数调用了"<<endl;}

注意,该移动构造函数与普通构造函数的区别

  • 参数类型
    • 移动构造函数的参数类型为右值引用
    • 拷贝构造函数的参数类型为常量引用(左值引用)
  • 具体实现
    • 移动构造函数的实现是直接将指针所指向的地址进行交接(浅拷贝),无需重新开辟一块新的内存
    • 拷贝构造函数的实现则是深拷贝,需要首先申请一块新的内存,之后再把待拷贝对象的内容复制到新申请的内存中

因此,单从实现上看,移动构造函数就比拷贝构造函数节省了内存资源的使用,而之所以移动构造的参数为右值引用,也是为了实现移动构造函数这种特性而产生的

接下来我们再次关闭编译器的优化选项,运行查看结果

可以看到,与之前没有添加移动构造函数相比,这次实验编译器使用移动构造函数替换了拷贝构造函数,并且没有对类B进行拷贝构造,类A的析构函数也少了很多,具体而言

  • 前两行的普通构造仍旧是getA函数的局部对象a引起的
  • 第三行的移动构造则是getA函数返回临时对象时引起的,因为编译器发现这是一个临时对象,而当移动构造函数存在的时候,临时对象被右值引用类型的参数所接受,也就是被移动构造函数的参数接受
  • 第四行的析构则是getA函数运行结束,其内的局部对象a被销毁引起的
  • 第五行的移动构造则是由测试函数对getA函数的调用引起的

实验二

而如果我们再将测试函数中代码稍作改变,如下,将测试函数中的a类型修改为右值引用

void test()
{A&& a=getA();
}

 再次运行观察实验结果

会发现编译器只进行了一次移动构造函数的调用,除此以外再没有其他多余的调用!

这是因为getA返回的临时对象引起了移动构造函数的调用,而这个临时对象返回到测试函数后被测试函数中右值引用类型的a直接接管

并且从此刻开始,这个由getA返回临时对象的生命周期将同test函数中a的声明周期一样(可以认为此时的a就是这个返回的临时对象),因此当测试函数结束时只进行了一次析构函数的调用倒数第二行和倒数第三行的析构是getA函数结束时引起的对局部对象a的销毁

实验三

接下来我们探讨在右值引用文章中提到的std::move()函数的作用

左值、右值、左值引用与右值引用-CSDN博客

在测试函数中追加一行代码

void test()
{A&& a=getA();A a1(a);
}

编译运行观察输出结果

可以看到,追加的代码引起了拷贝构造函数的调用

接下来,我们修改代码如下:

void test()
{A&& a=getA();A a1(std::move(a));
}

再次观察结果发现,原来的拷贝构造函数的现在被移动构造函数替换了,原因就是因为std::move()函数将对象a从左值类型强制转换成了右值类型,而右值类型的变量会被具有右值引用类型形参的一点构造函数所接收 

因此,我们再次看到,所谓std::move()函数只是一个类型转换函数,其作用就是将一个左值对象强制转换为右值

如果我们继续修改代码

void test()
{A&& a=getA();A &&a1(std::move(a));
}

运行观察结果

会发现编译器只进行了一次移动构造函数的调用,但此时需要注意,这行代码根本不产生新对象,当然也不会调用类A的移动构造函数,可以通过跟踪调试观察,这行代码的效果等同于把对象a的名修改为a1,或者说对象a和对象a1代表同一个对象

移动赋值运算符

在原有的基础上对类A增加拷贝赋值运算符和移动赋值运算符

//拷贝赋值运算符A operator=(const A& src){if(this==&src)return *this;delete this->m_pb;this->m_pb=new B(*src.m_pb);//值的赋值cout<<"类A的拷贝赋值运算符调用了"<<endl;return *this;}//移动赋值运算符A operator=(A&& a1){if(this==&a1)return *this;delete this->m_pb;this->m_pb=a1.m_pb;//指针的接管a1.m_pb=nullptr;return *this;}

测试函数如下:

void test()
{A a=getA();A a2;a2=std::move(a);
}

合成的移动操作

如果不生成自己的拷贝构造函数和拷贝赋值运算符,那么,在某些情况下,编译器会合成拷贝构造函数和拷贝赋值运算符,同样道理,在某些情况下,编译器会合成移动构造函数和移动赋值运算符。针对合成问题有一些说法,总结如下:

  • 如果一个类定义了自己的拷贝构造函数、拷贝赋值运算符或者析构函数(这三者之一,表示程序员要自己处理对象的复制或者释放问题),编译器就不会为它合成移动构造函数和移动赋值运算符。这说明只要程序员有自己复制对象和释放对象的倾向,编译器就不会帮助程序员生成移动动作的相关函数(所以有一些类是没有移动构造函数和移动赋值运算符的),这样就可以防止编译器合成出一个完全不是程序员自己想要的移动构造函数或者移动赋值运算符。
  • 只有一个类没定义任何自己版本的拷贝构造函数、拷贝赋值运算符、析构函数,且类的每个非静态成员都可以移动时,编译器才会为该类合成移动构造函数或者移动赋值运算符。
    • 可以移动的成员有
      1. 内置类型(如整型、实型等)的成员变量可以移动
      2. 如果成员变量是一个类类型,如果这个类有对应的移动操作相关的函数,则该成员变量可以移动。

总结

  1. 在有必要的情况下,应该考虑尽量给类添加移动构造函数和移动赋值运算符,达到减少拷贝构造函数和拷贝赋值运算符调用的目的,尤其是需要频繁调用拷贝构造函数和拷贝赋值运算符的场合。
  2. 不抛出异常的移动构造函数、移动赋值运算符都应该加上noexcept,用于通知编译器该函数本身不抛出异常。否则有可能因为系统内部的一些运作机制原本程序员认为可能会调用移动构造函数的地方却调用了拷贝构造函数。此外,此举还可以提高编译器的工作效率。
  3. 一个对象移动完数据后当然不会自主销毁,但是,程序员有责任使这种数据被移走的对象处于一种可以被释放(析构)的状态
  4. 一个本该由系统调用移动构造函数和移动赋值运算符的地方,如果类中没有提供移动构造函数和移动赋值运算符,则系统会调用拷贝构造函数和拷贝赋值运算符代替。

参考:
《c++新经典》

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/337723.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

openGauss学习笔记-193 openGauss 数据库运维-常见故障定位案例-备机卡住-数据库只读

文章目录 openGauss学习笔记-193 openGauss 数据库运维-常见故障定位案例-备机卡住-数据库只读193.1 switchover操作时&#xff0c;主机降备卡住193.1.1 问题现象193.1.2 原因分析193.1.3 处理办法 193.2 磁盘空间达到阈值&#xff0c;数据库只读193.2.1 问题现象193.2.2 原因分…

基于深度学习的果蔬检测识别系统(含UI界面、yolov5、Python代码、数据集)

项目介绍 项目中所用到的算法模型和数据集等信息如下&#xff1a; 算法模型&#xff1a;     yolov5 yolov5主要包含以下几种创新&#xff1a;         1. 添加注意力机制&#xff08;SE、CBAM、CA等&#xff09;         2. 修改可变形卷积&#xff08;DySnake-主…

rocketmq查看消息堆积

第一种方式&#xff1a;命令行方式&#xff1a; //查看消费者情况&#xff0c;192.168.2.210为自己mq的地址&#xff0c;回显的Diff Total参数就是堆积的消息数量 ./mqadmin consumerProgress -n 192.168.2.210:9876第二种方式&#xff0c;通过控制台&#xff1a; 回显中的Di…

一点一点,照亮你的美

一、实验要求 当鼠标点击屏幕时&#xff0c;随机出现大大小小的星星闪烁&#xff0c;犹如夜晚的星空 二、实验思路 设置图片的大小 设置事件&#xff08;当鼠标点一下&#xff0c;获取一张图片&#xff09; 设置图片的位置 设置鼠标的位置和图片的相对位置 设置随机大小 …

AI老照片修复-Bringing-Old-Photos-Back-to-Life

&#x1f3e1; 个人主页&#xff1a;IT贫道-CSDN博客 &#x1f6a9; 私聊博主&#xff1a;私聊博主加WX好友&#xff0c;获取更多资料哦~ &#x1f514; 博主个人B栈地址&#xff1a;豹哥教你学编程的个人空间-豹哥教你学编程个人主页-哔哩哔哩视频 目录 1. AI老照片修复原理-…

leetcode 动态规划(最后一块石头的重量II、目标和、一和零)

1049.最后一块石头的重量II 力扣题目链接(opens new window) 题目难度&#xff1a;中等 有一堆石头&#xff0c;每块石头的重量都是正整数。 每一回合&#xff0c;从中选出任意两块石头&#xff0c;然后将它们一起粉碎。假设石头的重量分别为 x 和 y&#xff0c;且 x < …

Low Poly Cartoon House Interiors

400个独特的低多边形预制件的集合,可以轻松创建高质量的室内场景。所有模型都已准备好放入场景中,并使用一个纹理创建,以提高性能!包含演示场景! 模型分类: - 墙壁(79件) - 地板(28块) - 浴室(33个) - 厨房(36件) - 厨房道具(68件) - 房间道具(85件) - 灯具(…

持续领跑云安全赛道!安全狗多项安全能力获认可

近日&#xff0c;以“数字安全 未来可期”为主题的“2024安全市场年度大会”在北京成功举行。 作为国内云原生安全领导厂商&#xff0c;安全狗也受邀出席此次活动。 厦门服云信息科技有限公司&#xff08;品牌名&#xff1a;安全狗&#xff09;创办于2013年&#xff0c;是国内…

【局域网window10系统搭建共享文件夹或与手机共享】

局域网window10系统搭建共享文件夹或与手机共享 1、Window 10之间搭建共享文件夹1.1 ping通两台window 10 电脑1.2 创建共享账号&#xff08;window 10专业版&#xff09;1.3 创建共享文件夹以及配置1.4访问共享文件夹 2、手机访问window10 共享文件夹&#xff08;结合步骤一&a…

OpenMv颜色识别

本文旨在分享OpenMv实现数字识别并通过串口打印出来的工程源码。如果大家想将识别的结果传给单片机&#xff0c;即OpenMv与单片机之间的通信&#xff0c;可以参考以下文章&#xff1a; OpenMV与STM32之间的通信&#xff08;附源码&#xff09;_openmv与stm32串口-CSDN博客 ​​…

Java建筑工程建设智慧工地源码

智慧工地管理平台依托物联网、互联网&#xff0c;建立云端大数据管理平台&#xff0c;形成“端云大数据”的业务体系和新的管理模式&#xff0c;从施工现场源头抓起&#xff0c;最大程度的收集人员、安全、环境、材料等关键业务数据&#xff0c;打通从一线操作与远程监管的数据…

【云计算】云计算概述

1. 云计算概述 1.1 云计算的定义 美国国家标准与技术研究院(NIST)定义 云计算是一种按使用量付费的模式&#xff0c;这种模式提供可用的、便捷的、按需的网络访问&#xff0c;进入可配置的计算资源共享池(资源包括网络&#xff0c;服务器&#xff0c;存储&#xff0c;应用软件…