【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 类的移动构造函数初始化 firsecond(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;
}

在这里插入图片描述

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

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

相关文章

不再手动处理繁琐任务!Python自动化方案梳理

文章目录 📖 介绍 📖🏡 演示环境 🏡📒 文章内容 📒📝 文件和文件夹操作📝 Web自动化📝 自动化办公任务📝 网络请求和API调用📝 任务调度📝 桌面自动化📝 邮件自动化⚓️ 相关链接 ⚓️📖 介绍 📖 想象一下,只需一个Python程序,就能让你的电脑自…

深度学习--CNN实现猫狗识别二分类(附带下载链接, 长期有效)

1. 代码实现(包含流程解释) 样本量: 8005 # # 1.导入数据集(加载图片)数据预处理# 进行图像增强, 通过对图像的旋转 ,缩放,剪切变换, 翻转, 平移等一系列操作来生成新样本, 进而增加样本容量, # 同时对图片数值进行归一化[0:1] from tensorflow.keras.preprocessing.image …

Mysql 和MongoDB用户访问权限问题

Mysql 刚给二线运维排查了一个问题&#xff0c;Mysql安装完可用&#xff0c;且可用navicat连接&#xff0c;项目中通过127.0.0.1去连数据库报错了。错误是access denied for user ‘root’localhost,排查思路 1. 密码是否正确 &#xff08;不需要重置。到Mysql的安装目录下找…

软件I2C的代码

I2C的函数 GPIO的配置——scl和sda都配置为开漏输出 void MyI2C_Init(void) {RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);GPIO_InitTypeDef GPIO_InitStruture;GPIO_InitStruture.GPIO_Mode GPIO_Mode_Out_OD;GPIO_InitStruture.GPIO_PinGPIO_Pin_10 | GPIO_Pin_…

Linux服务器前后端项目部署vue+springboot—搭建服务器上的运行环境(JDK、Redis、MySQL、Nginx)

Linux服务器前后端项目部署—①搭建服务器上的运行环境 一、系统参数信息和使用工具 1、服务器信息 华为云 CenteOS7.8 64 配置信息&#xff1a;2核4G 2、使用工具 Xshell6 二、环境安装和配置 &#xff08;一&#xff09;JDK的下载和安装 1、创建一个新目录或者进入目…

Spring event实战

什么是spring event&#xff1f; Spring Event 是 Spring 框架提供的一种事件驱动编程模型。它允许应用程序中的组件通过发布和监听事件来进行松耦合的交互。这种机制基于观察者设计模式&#xff0c;其中组件可以扮演事件发布者的角色&#xff0c;而其他组件则作为事件监听器来…

UEFI BIOSAPP编程开发查询手册.pdf

UEFI BIOS&APP编程开发查询手册.pdf 独家整理推荐。 享受&#xff0c; 半年免费更新服务&#xff0c; 一年免费咨询服务。

django5入门【03】新建一个hello界面

注意 ⭐前提&#xff1a;将上节的项目导入到pycharm中操作步骤总结&#xff1a; 1、HelloDjango/HelloDjango目录下&#xff0c;新建一个views.py 2、HelloDjango/HelloDjango/urls.py 文件中&#xff0c;配置url路由信息 3、新建终端&#xff0c;执行运行命令python manag…

RuoYi-Vue若依 环境搭建 速成

一、若依简介 RuoYi-Vue 是一个开源的后台管理系统&#xff0c;适用于快速开发企业级应用。该平台由两部分组成&#xff1a;前端和后端。 &#xff08;1&#xff09;技术框架 前端技术&#xff1a; Vue.js: 前端框架使用 Vue.js&#xff0c;这是一种流行的JavaScript框架&a…

[实时计算flink]基于Paimon的数据库实时入湖快速入门

Apache Paimon是一种流批统一的湖存储格式&#xff0c;支持高吞吐的写入和低延迟的查询。本文通过Paimon Catalog和MySQL连接器&#xff0c;将云数据库RDS中的订单数据和表结构变更导入Paimon表中&#xff0c;并使用Flink对Paimon表进行简单分析。 背景信息 Apache Paimon是一…

(46)MATLAB仿真从正弦波转换为方波

文章目录 前言一、MATLAB代码二、仿真结果画图三、吉布斯效应 前言 本文使用MATLAB仿真的方法&#xff0c;给出从正弦波转换为方波的过程&#xff0c;说明方波的傅里叶级数展开式是如何由奇次谐波的和构成的。另外&#xff0c;说明了在此过程中的吉布斯效应。 一、MATLAB代码 …

pm2 部署vue

1、为什么要使用pm2运行vue项目 为什么&#xff01;&#xff01;&#xff01;我们一般是将打出来的DIST目录上传到服务器发布即可&#xff0c;为啥我会使用PM2来运行部署呢&#xff1f; 前提&#xff1a;vue2mysqlexpress不使用中间服务器&#xff0c;即不要后端人员开发接口服…

Bands Page 乐队页面

“带区”页面提供了用于添加和删除带区、自定义带区设置以及更改带区和列布局的设计时工具。此页面如下图所示。 该页面说明了一个预览部分、一个用于访问所选频段设置的属性网格以及一组按钮&#xff0c;这些按钮提供了下面列表中描述的功能。 添加新乐队…- 创建新带。创建新…

Elasticsearch使用实战以及代码详解

Elasticsearch 是一个使用 Java 语言编写、遵守 Apache 协议、支持 RESTful 风格的分布式全文搜索和分析引擎&#xff0c;它基于 Lucene 库构建&#xff0c;并提供多种语言的 API。Elasticsearch 可以对任何类型的数据进行索引、查询和聚合分析&#xff0c;无论是文本、数字、地…

【论文学习与撰写】,论文word文档中出现乱码的情况,文档中显示的乱码,都是英文字母之类的,但打印预览是正常的

目录 1、问题 2、解决方法 1、问题 写论文的时候&#xff0c;有时会出现乱码的情况&#xff0c; 如下图&#xff0c;这种情况&#xff0c; 可是 在打印预览的时候&#xff0c;就显示的正常 如下图&#xff0c; 2、解决方法 既然是文档正文显示错误&#xff0c;显示乱码&…

【HarmonyOS NEXT】服务端向终端推送消息——获取Push Token

【需求】 获取Push Token 【文档】 https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/push-get-token-V5 【代码】 // EntryAbility.ets 文件 import { pushService } from kit.PushKit; export default class EntryAbility extends UIAbility {onCreat…

【详解】下载MySql安装教程(帮助数据库下载)

此版本是我下载的版本&#xff0c;其他版本均可以。 1.官网下载相应的版本&#xff1a;MYSQL&#xff1a;8.0.33 https://www.mysql.com/ 2.点击DOWNLOADS进入 3.在上述界面当中往下翻&#xff0c;找到社区版的下载界面 4.点进社区版的界面 前三个是Linux系统下的安装&a…

1.centos 镜像

centos 它有官网的下载地址&#xff1a;https://vault.centos.org/ 选择想要的版本&#xff0c;我选择 centos7.8 进入到镜像目录 isos 选择 x86_64 选择想要的版本&#xff0c;我选择 CentOS-7-x86_64-DVD-2003.iso 安装就正常安装就行。我选择虚拟机安装。这个参考&…

git的安装以及入门使用

文章目录 git的安装以及入门使用什么是git&#xff1f;git安装git官网 git初始化配置使用方式初始化配置&#xff1a; git的安装以及入门使用 什么是git&#xff1f; Git 是一个免费开源的分布式版本控制系统&#xff0c;使用特殊的仓库数据库记录文件变化。它记录每个文件的…

前端开发设计模式——状态模式

目录 一、状态模式的定义和特点 二、状态模式的结构与原理 1.结构&#xff1a; 2.原理&#xff1a; 三、状态模式的实现方式 四、状态模式的使用场景 1.按钮的不同状态&#xff1a; 2.页面加载状态&#xff1a; 3.用户登录状态&#xff1a; 五、状态模式的优点 1.提…