【C++ 11】移动构造函数

文章目录

  • 【 1. 问题背景:深拷贝引起的内存开销问题 】
  • 【 2. 移动构造函数 】
  • 【 3. 左值的移动构造函数: move 实现 】

【 1. 问题背景:深拷贝引起的内存开销问题 】

  • 拷贝构造函数
    在 C++ 11 标准之前(C++ 98/03 标准中),如果想用其它对象初始化一个同类的新对象,只能借助类中的拷贝构造函数,拷贝构造函数的实现原理很简单,就是为新对象复制一份和其它对象一模一样的数据。
  • 深拷贝
    当类中拥有指针类型的成员变量时,拷贝构造函数中需要以深拷贝(而非浅拷贝)的方式复制该指针成员。参考【C++面向对象】2.构造函数、析构函数 一文详细了解。
  • 实例
    程序为 demo 类自定义一个拷贝构造函数,该函数在拷贝 d.num 指针成员时,必须采用深拷贝的方式(即拷贝该指针成员本身的同时,还要拷贝指针指向的内存资源。否则一旦多个对象中的指针成员指向同一块堆空间,这些对象析构时就会对该空间释放多次,这是不允许的)。
    程序中还定义了一个可返回 demo 对象的 get_demo() 函数,用于在 main() 主函数中初始化 a 对象,其整个初始化的流程包含以下几个阶段:
    1. 执行 get_demo() 函数内部的 demo() 语句,即调用 demo 类的 默认构造函数 生成一个匿名对象;
    2. 执行 return demo() 语句,会调用 拷贝构造函数 复制一份之前生成的匿名对象,并将其 作为 get_demo() 函数的 返回值(函数体执行完毕之前,匿名对象会被析构销毁);
    3. 执行 a = get_demo() 语句,再调用一次 拷贝构造函数将之前拷贝得到的临时对象复制给 a(此行代码执行完毕,get_demo() 函数返回的对象会被析构);
    4. 程序执行结束前,会自行调用 demo 类的析构函数销毁 a。
#include <iostream>
using namespace std;

class demo{
private:
   int *num;
public:
   //默认构造函数
   demo():num(new int(0)){
      cout<<"construct!"<<endl;
   }
   //拷贝构造函数
   demo(const demo &d):num(new int(*d.num)){
      cout<<"copy construct!"<<endl;
   }
   //析构函数
   ~demo(){
      cout<<"class destruct!"<<endl;
   }
};

// 外部函数,返回一个 demo 类型的对象
demo get_demo(){
    return demo();
}

int main(){
    demo a = get_demo();
    return 0;
}
  • 目前多数编译器都会对程序中发生的拷贝操作进行优化,因此如果我们使用 VS 2017、codeblocks 等这些编译器运行此程序时,看到的往往是优化后的输出结果:
    在这里插入图片描述
  • 而同样的程序,如果在 Linux 上使用g++ demo.cpp -fno-elide-constructors命令运行(其中 demo.cpp 是程序文件的名称),就可以看到完整的输出结果:
    construct! – 执行 demo()
    copy construct! – 执行return demo()
    class destruct! – 销毁 demo() 产生的匿名对象
    copy construct! – 执行 a = get_demo()
    class destruct! – 销毁 get_demo() 返回的临时对象
    class destruct! – 销毁 a
  • 问题的产生
    如上实例所示,利用拷贝构造函数实现对 a 对象的初始化,底层实际上进行了 2 次拷贝(而且是深拷贝)操作。当然,对于仅申请少量堆空间的临时对象来说,深拷贝的执行效率依旧可以接受,但如果临时对象中的指针成员申请了大量的堆空间,那么 2 次深拷贝操作势必会影响 a 对象初始化的执行效率。
  • 编译器的隐晦优化
    事实上,此问题一直存留在以 C++ 98/03 标准编写的 C++ 程序中。由于临时变量的产生、销毁以及发生的拷贝操作本身就是很隐晦的(编译器对这些过程做了专门的优化),且并不会影响程序的正确性,因此很少进入程序员的视野。
  • C++11 右值引用→移动语义→移动构造函数→解决深拷贝效率低问题
    那么当类中包含指针类型的成员变量,使用其它对象来初始化同类对象时,怎样才能避免深拷贝导致的效率问题呢?C++11 标准引入了解决方案,该标准中引入了右值引用的语法,借助它可以实现移动语义。

【 2. 移动构造函数 】

  • 移动语义以移动而非深拷贝的方式初始化含有指针成员的类对象 ,即 将其他对象(通常是临时对象)拥有的内存资源 “移为已用”
    • 以前面程序中的 demo 类为例,该类的成员都包含一个整形的指针成员,其默认指向的是容纳一个整形变量的堆空间。当使用 get_demo() 函数返回的临时对象初始化 a 时,我们只需要将临时对象的 num 指针直接浅拷贝 给 a.num,然后修改该临时对象中 num 指针的指向(通常另其指向 NULL),这样就完成了 a.num 的初始化。
    • 事实上,对于程序执行过程中产生的临时对象,往往只用于传递数据(没有其它的用处),并且会很快会被销毁。因此在使用临时对象初始化新对象时,我们可以 将临时包含的指针成员指向的内存资源直接移给新对象所有,无需再新拷贝一份,这大大提高了初始化的执行效率。
  • 实例
    在之前 demo 类的基础上,我们手动为其添加了一个移动构造函数,和其它构造函数不同,移动构造函数使用右值引用形式的参数 ,并且在此构造函数中,num 指针变量采用的是浅拷贝的复制方式 ,同时在函数内部重置 d.num 指针变量指向空 ,有效避免了“同一块对空间被释放多次”情况的发生。
    //移动构造函数
    demo(demo&& d) :num(d.num) {
    d.num = NULL;
    cout << "move construct!" << endl;
    }
#include <iostream>
using namespace std;
class demo {
private:
    int* num;
public:
    //默认构造函数
    demo() :num(new int(0)) {
        cout << "construct!" << endl;
    }
    //拷贝构造函数(深拷贝)
    demo(const demo& d) :num(new int(*d.num)) {
        cout << "copy construct!" << endl;
    }
    //移动构造函数
    demo(demo&& d) :num(d.num) {
        d.num = NULL;
        cout << "move construct!" << endl;
    }
    //析构函数
    ~demo() {
        cout << "class destruct!" << endl;
    }
};

//外部函数,返回demo对象
demo get_demo() {
    return demo();
}

int main() {
    demo a = get_demo();
    return 0;
}

在 Linux 系统中使用g++ demo.cpp -o demo.exe -std=c++0x -fno-elide-constructors命令执行此程序,输出结果为:
construct!
move construct!
class destruct!
move construct!
class destruct!
class destruct!
通过执行结果我们不难得知,当为 demo 类添加移动构造函数之后,使用临时对象初始化 a 对象过程中产生的 2 次拷贝操作,都转由移动构造函数完成。

  • 当类中同时包含拷贝构造函数和移动构造函数时,如果使用临时对象初始化当前类的对象,编译器会优先调用移动构造函数来完成此操作 ,只有当类中没有合适的移动构造函数时,编译器才会退而求其次,调用拷贝构造函数。
  • 非 const 右值引用只能操作右值,程序执行结果中产生的临时对象(例如函数返回值、lambda 表达式等)既无名称也无法获取其存储地址,所以属于右值
  • 默认情况下,左值初始化同类对象只能通过拷贝构造函数完成,如果想调用移动构造函数,则必须使用右值进行初始化。C++11 标准中为了满足用户使用左值初始化同类对象时也通过移动构造函数完成的需求,新引入了 move() 函数,它可以将左值强制转换成对应的右值,由此便可以使用移动构造函数。

【 3. 左值的移动构造函数: move 实现 】

  • 基本语法
    • arg 表示指定的左值对象。
    • 该函数会返回 arg 对象的右值形式。
move( arg )
  • 实例1
    demo 对象作为左值,直接用于初始化 demo2 对象,其底层调用的是拷贝构造函数;而通过调用 move() 函数可以得到 demo 对象的右值形式,用其初始化 demo3 对象,编译器会优先调用移动构造函数。
    注意,调用拷贝构造函数,并不影响 demo 对象,但如果 调用移动构造函数,函数内部会重置 demo.num 指针的指向为 NULL ,所以程序中第 30 行代码会导致程序运行时发生错误。
#include <iostream>
using namespace std;

class movedemo {
public:
    int* num;
public:
    //默认构造函数
    movedemo() :num(new int(0)) {
        cout << "default construct!" << endl;
    }
    //拷贝构造函数(深拷贝)
    movedemo(const movedemo& d) :num(new int(*d.num)) {
        cout << "copy construct!" << endl;
    }
    //移动构造函数
    movedemo(movedemo&& d) :num(d.num) {
        d.num = NULL;
        cout << "move construct!" << endl;
    }
};

int main() {
    movedemo demo;
    movedemo demo2 = demo;
    cout << "demo2:"<< *demo2.num << endl;   //可以执行
    
    movedemo demo3 = move(demo);
    //cout << "demo3:\n"<< *demo.num << endl;//此时 demo.num = NULL,因此此代码会报运行时错误
    
    return 0;
}

在这里插入图片描述

  • 实例2
    程序中分别构建了 first 和 second 这 2 个类,其中 second 类中包含一个 first 类对象。程序中使用了 2 次 move() 函数:
    • second oth; // oth 为左值。
    • second oth2 = move(oth); // 如果想调用移动构造函数为 oth2 初始化,需先利用 move() 函数生成一个 oth 的右值版本;
      oth 对象内部还包含一个 first 类对象,对于 oth.fir 来说,其也是一个左值,所以在初始化 oth.fir 时,还需要再调用一次 move() 函数。
#include <iostream>
using namespace std;

class first {
public:
    int* num;
public:
    //默认构造函数
    first() :num(new int(0)) {
        cout << "first default construct!" << endl;
    }
    //移动构造函数
    first(first&& d) :num(d.num) {
        d.num = NULL;
        cout << "first move construct!" << endl;
    }
};

class second {
public:
    first fir;
public:
    //默认构造函数
    second() :fir() { cout << "second default construct" << endl; }
    //用 first 类的移动构造函数初始化 fir
    second(second&& sec) :fir(move(sec.fir)) {
        cout << "second move construct" << endl;
    }
};

int main() {
    second oth;
    second oth2 = move(oth);
    //cout << *oth.fir.num << endl;   //程序报运行时错误
    
    return 0;
}

在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Vue脚手架学习 vue脚手架配置代理、插槽、Vuex使用、路由、ElementUi插件库的使用

目录 1.vue脚手架配置代理 1.1 方法一 1.2 方法二 2.插槽 2.1 默认插槽 2.2 具名插槽 2.3 作用域插槽 3.Vuex 3.1 概念 3.2 何时使用&#xff1f; 3.3 搭建vuex环境 3.4 基本使用 3.5 getters的使用 3.6 四个map方法的使用 3.6.1 mapState方法 3.6.2 mapGetter…

LabVIEW中句柄与引用

在LabVIEW中&#xff0c;句柄&#xff08;Handle&#xff09; 是一种用于引用特定资源或对象的标识符。它类似于指针&#xff0c;允许程序在内存中管理和操作复杂的资源&#xff0c;而不需要直接访问资源本身。句柄用于管理动态分配的资源&#xff0c;如队列、文件、网络连接、…

[python flask 数据库ORM操作]

一、链接数据库 我们选择的框架是flask-sqlAlchemy 这个框架是对pymysql的封装。 连接数据库 #导入包 from flask_sqlalchemy import SQLAlchemy #创建flask app对象 app Flask(__name__) #设置配置信息 HOSTNAME "localhost" PORT 3306; USERNAME "root&…

在C++中比大小

关于min()函数和max()函数: min()、max()这两个函数如果需要使用&#xff0c;要在程序头文件中加上<algorithm>库就可以使用这个函数了 #include <algorithm>min()函数是比较数中哪一个数最小&#xff0c;就返回最小的数&#xff0c;而max()函数则是比较数中哪一个…

计算机毕业设计Flask+Vue.js空气质量预测 空气质量可视化 空气质量分析 空气质量爬虫 大数据毕业设计 Hadoop Spark

《FlaskVue.js空气质量预测与可视化系统》开题报告与任务书 一、研究背景与意义 随着工业化进程的加速和城市化水平的不断提高&#xff0c;空气质量问题日益成为全球关注的焦点。空气污染不仅严重影响着人们的身体健康&#xff0c;如增加呼吸系统疾病、心血管疾病等风险&…

商场楼宇室内导航系统

商场楼宇室内导航系统 本文所涉及所有资源均在传知代码平台可获取 文章目录 商场楼宇室内导航系统效果图导航效果图查看信息数据加载加载模型模型选型处理楼层模型绑定店铺创建店铺名称动态显示隐藏2d元素空气墙查看信息楼梯导航效果图 导航效果图 查看信息 数据加载 因为是一…

Java最全面试题->Java主流框架->Srping面试题

Spring面试题 下边是我自己整理的面试题,基本已经很全面了,想要的可以私信我,我会不定期去更新思维导图 哪里不会点哪里 谈谈你对 Spring 的理解? Spring 是一个开源框架,为简化企业级应用开发而生。Spring 可以是使简单的 JavaBean 实现以前只有 EJB 才能实现的功能。…

定时开关机功能实现

提示&#xff1a;本文仅仅针对MTK平台实现需求&#xff0c;对其它芯片主控平台暂无借鉴可言 文章目录 需求需求描述实际手机功能图 资料相关说明实现方案修改方案修改内容点内置App修改MtkAlarmManagerService.java 坑点解决总结 需求 实现手机一样的定时开关机功能 需求描述…

计算不停歇,百度沧海数据湖存储加速方案 2.0 设计和实践

本文整理自百度云智峰会 2024 —— 云原生论坛的同名演讲。 今天给大家介绍下百度沧海存储团队在数据湖加速方面的工作进展情况。 数据湖这个概念&#xff0c;从 2012 年产生到现在已经有十余年的时间&#xff0c;每家公司对它内涵的解读都不太一样。但是数据湖的主要存储底座…

具备哪些特质的内外网文件交换系统 才是高科技企业需要的?

高科技企业是指涉及对国家产生深远和积极影响的先进技术的产业集群&#xff0c;它们以持续的创新和高研发投入为核心&#xff0c;推动科技进步和产业升级。高科技企业是市场经济的重要组成&#xff0c;为经济发展和技术进步提供充足动力&#xff0c;因此&#xff0c;高科技企业…

【南开X上海交大】OPUS:效率显著提升的OCC网络

1. 摘要 占据预测任务旨在预测体素化的3D环境中的占据状态&#xff0c;在自动驾驶领域中迅速获得了关注。主流的占据预测方法首先将3D环境离散化为体素网格&#xff0c;然后在这些密集网格上执行分类。然而&#xff0c;样本数据分析显示&#xff0c;大多数体素实际上是未占据的…

《15分钟轻松学Go》教程目录

在AI快速发展的时代&#xff0c;学习Go语言依然很有用。Go语言擅长处理高并发任务&#xff0c;也就是说可以同时处理很多请求&#xff0c;这对于需要快速响应的AI服务非常重要。另外&#xff0c;Go适合用来处理和传输大量数据&#xff0c;非常适合机器学习模型的数据预处理。 …

并查集 --- Java通用模版

什么是并查集 并查集可以解决什么问题&#xff1a;判断两个节点是否在一个集合&#xff0c;也可以将两个节点添加到一个集合中。 并查集常用于处理大规模数据下的元素分组问题&#xff0c;特别是在数据量极大时&#xff0c;使用正常的数据结构可能会导致空间或时间复杂度过高…

2024年10月21日计算机网络,乌蒙第一部分

【互联网数据传输原理 &#xff5c;OSI七层网络参考模型】 https://www.bilibili.com/video/BV1EU4y1v7ju/?share_sourcecopy_web&vd_source476fcb3b552dae37b7e82015a682a972 mac地址相当于是名字&#xff0c;ip地址相当于是住址&#xff0c;端口相当于是发送的东西拿什…

推荐一款功能强大的数据备份工具:Iperius Backup Full

Iperius Backup是一款非常灵活而且功能强大的数据备份工具&#xff0c;程序可以非常好的保护您的文件和数据的安全。支持DAT备份、LTO备份、NAS备份、磁带备份、RDX驱动器、USB备份、并且支持zip压缩和军事级别的AES 256位数据加密技术! 主要特色 云备份 Iperius可以自动地发…

STM32F1+HAL库+FreeTOTS学习18——任务通知

STM32F1HAL库FreeTOTS学习18——任务通知 1. 任务通知1.1 任务通知的引入1.2 任务通知简介1.3 任务通知的优缺点 2. 任务相关API函数2.1 发送任务通知2.1.1 xTaskGenericNotify()2.1.2 xTaskNotifyGive()和xTaskNotifyGiveIndexed()2.1.2 xTaskNotify()和xTaskNotifyIndexed()2…

【LeetCode:910. 最小差值 II + 模拟 + 思维】

&#x1f680; 算法题 &#x1f680; &#x1f332; 算法刷题专栏 | 面试必备算法 | 面试高频算法 &#x1f340; &#x1f332; 越难的东西,越要努力坚持&#xff0c;因为它具有很高的价值&#xff0c;算法就是这样✨ &#x1f332; 作者简介&#xff1a;硕风和炜&#xff0c;…

低功耗4G模组的小秘密:RSA算法示例驾到,通通闪开...

在实际应用中&#xff0c;低功耗4G模组的RSA算法示例具有重要的价值&#xff0c;所以今天我们学习合宙低功耗4G模组Air780EP_LuatOS_rsa示例&#xff1a; 1.简介 RSA算法的安全性基于&#xff1a;将两个大质数相乘很容易&#xff0c;但是想要将其乘积分解成原始的质数因子却非…

微信小程序广告组件被驳回之后怎么重新提交广告组件?

有时候遇到广告组件被退回的问题 这时需要重新提交一次程序代码&#xff0c;然后提交审核然后发布新版本之后&#xff0c;找到广告管理&#xff0c;即可看到广告组件是在正在审核状态中

CANoe_数据回放功能功能介绍_时间段(区间)选择

CANoe的日志回放功能&#xff0c;可以选择时间段回放&#xff0c;这样可以在数据量很大的时候快速定位分析数据问题点 CANoe日志回放功能概述 CANoe的日志回放功能允许用户重现和分析已记录的CAN总线或其他网络总线数据。这些日志文件通常以CANoe自己的日志格式&#xff08;.b…