ec笔记:第二部分:12复制对象时勿忘其每一个成分

SkyFireITDIY SkyFireITDIY     2022-08-18     704

关键词:

EC笔记:第二部分:12、复制对象时勿忘其每一个成分

 

1.场景

某些时候,我们不想使用编译器提供的默认拷贝函数(包括拷贝构造函数和赋值运算符),考虑以下类定义:

 

代码1:

class Point{

    private:

        double x;

        double y;

    public:

        Point()=default;

        Point(const Point &other){

            x=other.x;

            y=other.y;

        }

        const Point& operator=(const Point &other){

            x=other.x;

            y=other.y;

            return *this;

        }

};

 

Point类重写了拷贝构造函数和赋值运算符,这并没有什么问题,但是,假设现在要新增一个成员:

代码2:

class Point{

    private:

        double x;

        double y;

        double r; //新增的成员

    public:

        Point()=default;

        Point(const Point &other){

            x=other.x;

            y=other.y;

        }

        const Point& operator=(const Point &other){

            x=other.x;

            y=other.y;

            return *this;

        }

};

 

而且我们的程序员很不巧地忘了更新拷贝构造函数和赋值运算符,那么在复制的时候,新增的1成员r就保持原值,新得到的对象与原对象不相等,与要赋值的对象也不相等。

 

考虑另外一种场景:

代码3:

class Point{

    private:

        double x;

        double y;

};

 

class MyPoint:public Point{

    private:

        double r;

    public:

        Point()=default;

        MyPoint(const MyPoint& other){

            r=other.r;

        }

        const MyPoint& operator=(const MyPoint& other){

            r=other.r;

            return *this;

        }

};

 

当MyPoint继承Point类的时候,重写了拷贝构造函数和赋值运算符,但是忘了赋值基类中的成员,也会导致和前面一样的后果。

2.解决办法

为了解决上面的问题,首先,确保拷贝构造函数和赋值运算符确实将所有的成员都进行了拷贝(除非你是真的想保留一些成员)。例如,代码2中增加了成员r,那么拷贝构造函数和赋值运算符就应该对应更新:

代码4:

class Point{

    private:

        double x;

        double y;

        double r; //新增的成员

    public:

        Point()=default;

        Point(const Point &other){

            x=other.x;

            y=other.y;

            r=other.r;//新增

        }

        const Point& operator=(const Point &other){

            x=other.x;

            y=other.y;

            r=other.r;//新增

            return *this;

        }

};

 

其次,子类中重写了赋值运算符或者拷贝构造函数,一定要将父类中隐藏的成员一起复制,通常是调用父类的赋值运算符或者拷贝构造函数。代码3中可以修改为以下代码:

代码5:

class Point{

    public:

        Point()=default;

    private:

        double x;

        double y;

};

 

class MyPoint:public Point{

    private:

        double r;

    public:

        MyPoint()=default;

        MyPoint(const MyPoint& other):Point(other){

            r=other.r;

        }

        const MyPoint& operator=(const MyPoint& other){

            Point::operator=(other);

            r=other.r;

            return *this;

        }

};

 

有些童鞋可能注意到,大多数拷贝构造函数与赋值运算符的操作类似,那么,是否可以在拷贝构造函数中调用赋值运算符呢?类似下面的代码:

 

代码6:

class Point{

    public:

        Point()=default;

    private:

        double x;

        double y;

};

 

class MyPoint:public Point{

    private:

        double r;

    public:

        MyPoint()=default;

        MyPoint(const MyPoint& other){

            operator=(other);

        }

        const MyPoint& operator=(const MyPoint& other){

            Point::operator=(other);

            r=other.r;

            return *this;

        }

};

对此,《Effective C++》一书给出的解释是:

    这段解释比较不是那么易懂,总体说来,就是在执行operator=的时候,拷贝构造函数其实已经执行结束,赋值运算符只作用于已经构造好的对象上,所以此时再使用并无意义(本质上相当于调用了默认构造函数+赋值运算符)。

    反过来的话(在赋值运算符中调用拷贝构造函数),由于根本没有类似的语法,所以不做讨论。事实上,构造函数主要目的是"构造",但是调用赋值运算符的时候,对象肯定已经构造完毕,所以,不会有这样的语法支持。

 

    对于拷贝构造函数与赋值运算符有类似代码的场景,《Effective C++》给出的建议是,将公共代码提到一个单独的函数中,各自调用,以上代码可修改为:

代码7:

class Point{

    public:

        Point()=default;

    private:

        double x;

        double y;

};

 

class MyPoint:public Point{

    private:

        double r;

        void init(const MyPoint& other){

            r=other.r;

        }

    public:

        MyPoint()=default;

        MyPoint(const MyPoint& other):Point(other){

            init(other);

        }

        const MyPoint& operator=(const MyPoint& other){

            Point::operator=(other);

            init(other);

            return *this;

        }

};

 

    但是对于以上的修改方法,如果是对于这个类,我仍觉得有瑕疵。虽然拷贝构造函数中直接调用了父类的拷贝构造函数,但是在调用init函数的时候,成员r其实已经被构造,整个对象此时已经构造完毕,再对r进行复制,依然会产生额外的开销(当然,这个开销比拷贝构造函数直接调用赋值运算符小)。

    所以我建议的修改方式为直接调用每个成员的拷贝构造函数,代码如下:

 

代码8:

class Point{

    public:

        Point()=default;

    private:

        double x;

        double y;

};

 

class MyPoint:public Point{

    private:

        double r;

    public:

        MyPoint()=default;

        MyPoint(const MyPoint& other):Point(other),r(other.r){

        }

        const MyPoint& operator=(const MyPoint& other){

            Point::operator=(other);

            r=other.r;

            return *this;

        }

};

 

此时Point和r都调用了各自的拷贝构造函数完成构造,比代码7的效率要好一些。

当然,这个修改只是针对这个类,《Effective C++》中指的是拷贝构造函数与赋值运算符中有相同代码部分,此处的修改已经不属于"相同代码"了,因为r=other.r已经被优化为了r(other.r),相当于拷贝构造函数与赋值运算符中已经没有相同代码了。

 

针对代码7代码8的分析,可以知道,只有当r没有提供拷贝构造函数的时候,代码7才不能优化为代码8,通常有两种情况:

  1. r是一个类对象,而且此对象中有些成员未提供拷贝构造函数
  2. r未提供拷贝构造函数

其实总的就是一句话:r无法通过拷贝构造函数构造,这时才需要用自定义代码完成r的构造。所以为了避免效率上的损失,尽量每个成员都提供拷贝构造函数(除非确实不希望成员被复制,当然,此时对象也不应该有拷贝构造函数)。

3.建议

  1. 确保每个成员都被复制
  2. 将不同copy函数中相同部分提取出来,重新封装一个函数,各自调用,两个copy函数不应该存在调用关系。

笔记探索js的this对象(第二部分)

了解this对象之后我们明白了this对象就是指向调用函数的作用域 那么接下来我们便要清除函数究竟在哪个作用域调用 找到调用的作用域首先要了解一下几点: 1、调用栈:调用栈就是一系列的函数,表明当前函数是由哪些上层函... 查看详情

读书笔记《你不知道的javascript(上卷)》——第二部分this和对象原型(代码片段)

文章目录第6章行为委托6.1面向委托的设计6.1.1类理论6.1.2委托理论1.互相委托(禁止)2.调试6.1.3比较思维模型6.2类与对象6.2.1控件“类”ES6的class语法糖6.2.2委托控件对象6.3更简洁的设计反类6.4更好的语法反词法6... 查看详情

实现一个带有指纹加密功能的笔记本(android)第二部分(代码片段)

上文基本完成了整个笔记本的笔记功能的实现,接下来记录实现指纹识别加密以及一些小注意事项。首先判断该手机是否具备指纹识别的硬件功能和用户是否开启指纹识别。publicbooleanisFinger()//用来检查是否有指纹识别权限if(check... 查看详情

读书笔记--算法导论(第二部分排序和顺序统计学)

输入数据的结构 在实际中,待排序的数很少是孤立的值,它们通常是一个称为记录的数据集的一部分。每个记录有一个关键字key,它是待排序的值。记录的其他数据称为卫星数据,即它们通常以key为中心传送。在一个排序的... 查看详情

mediasourceextension官方文档(第二部分)(代码片段)

2.MediaSourceObjectMediaSource对象表示HTMLMediaElement元素的一个媒体数据源。它会记录源的readyState和一个可以添加媒体数据去展示的SourceBuffer对象的列表。MediaSource对象由web应用创建然后绑定到HTMLMediaElement元素上。web应用通过添加SourceB... 查看详情

js第二部分--dom文档对象模型

DOM一、DOM的概念 DocumentObjectModel文档对象模型 万事万物皆对象: (DOM树中一切皆节点,节点是一个标签) document对象 | html对象 || head对象body对象 |||||||| ...... DOM对象产生是为了让js操纵文档 对象有属性和方法 二、D... 查看详情

参考手册目录__第二部分

参考手册目录__第一部分参考手册目录__第二部分参考手册目录__第三部分 第二部分:python库------164 第12章:内置函数和异常------164 12.1内置函数和类型------17212.2内置异常------172  12.2.1异常基类------173  12.2.2预定义的... 查看详情

gridview 问题...第二部分

】gridview问题...第二部分【英文标题】:gridviewproblem...parttwo【发布时间】:2010-12-3110:57:29【问题描述】:抱歉再次问这个问题,但我没有得到最后一个太大的帮助(可能是由于假期或者是因为我的问题是有点混乱)。即时通讯从... 查看详情

图深度学习(deeplearningongraphs)第四章第二部分(4.2)阅读笔记

说明之前写的文章的序号混乱,目前作规定标题优先级定位如下:一、1、(1)、<1>。二、preservingstructuralrolestruc2vec是一种保持结构相似度的图嵌入方法,与deepwalk的mappingfunction以及reconstructorfunction一致... 查看详情

迅为4418开发板qt移植移动4g模块第二部分

第一部分:http://www.cnblogs.com/topeet/p/6509248.html 第二部分:5.ping不通域名一般是DNS没有设置对造成的。在etc下有一个文件resolv.conf 。这个文件是用来放DNS的。就是这个里面的域名没有设置对。然而在eth0的运行脚本里强制性... 查看详情

string类总结第二部分实战练习(代码片段)

第二部门:实战练习  昨天由于时间原因,这个部分应该在同一个文章中的,无奈只能今天再开一个了,今天主要是讲一些面试题一:equals和==的区别  最简单的面试题,也是最基础的,我估计每个学习java的人都在网上看到... 查看详情

scrypt合约中的椭圆曲线算法:第二部分(代码片段)

我们在脚本中实现了椭圆曲线(EC)算法。在之前的实现中,我们进行链下计算并在脚本中验证结果。我们这里直接用脚本计算。基于EC的应用非常多,特别是在密码学领域,如数字签名、加密、承诺方案等。作为具体示... 查看详情

核心数据删除问题:第二部分

】核心数据删除问题:第二部分【英文标题】:coredatadeleteissues:parttwo【发布时间】:2011-01-2111:10:04【问题描述】:亲爱的社区。Bellow是代码的一部分,它可以工作,但对我来说有两个问题(代码在AppDelegate主线程中工作并从选择... 查看详情

深入理解java虚拟机第二部分.内存自动管理机制.2.hotspot虚拟机对象探秘

2、HotSpot虚拟机对象探秘 对象的创建过程1、加载类虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果... 查看详情

origin使用手册/笔记第二部分:数据的录入

目录1:手动输入数据2:直接拖动数据文件到Origin3:通过剪贴板传送数据4:由数据文件导入数据(1)导入单个ASCII数据文件(2)导入多个ASCII码文件数据(3)导入excel文件数据4:通过列运算... 查看详情

origin使用手册/笔记第二部分:数据的录入

目录1:手动输入数据2:直接拖动数据文件到Origin3:通过剪贴板传送数据4:由数据文件导入数据(1)导入单个ASCII数据文件(2)导入多个ASCII码文件数据(3)导入excel文件数据4:通过列运算... 查看详情

我将如何做第一部分和第二部分?

】我将如何做第一部分和第二部分?【英文标题】:Howwouldidofirstandsecondparts?【发布时间】:2022-01-1620:19:46【问题描述】:考虑我们必须从n个人中选择一个领导者。为此,我们创建了一个大小为n的数组。我们为每个候选人分配一... 查看详情

独立成分分析(ica)的模拟实验(r语言)

本笔记是ESL14.7节图14.42的模拟过程。第一部分将以ProDenICA法为例试图介绍ICA的整个计算过程;第二部分将比较ProDenICA、FastICA以及KernelICA这种方法,试图重现图14.42。ICA的模拟过程生成数据首先我们得有一组独立(ICA的前提条件)... 查看详情