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函数不应该存在调用关系。

EC笔记:第二部分:12、复制对象时勿忘其每一个成分的更多相关文章

  1. EC读书笔记系列之7:条款12 复制对象时勿忘其每一个成分

    记住: ★copying函数应确保复制“对象内的所有成员变量”及“所有base class成分” ★不要尝试以某个copying函数实现另一个copying函数.应该将共同机能放进第三个函数中,并由两 ...

  2. Effective C++_笔记_条款12_复制对象时勿忘其每一个成分

    (整理自Effctive C++,转载请注明.整理者:华科小涛@http://www.cnblogs.com/hust-ghtao/) 编译器会在必要时候为我们的classes创建copying函数, ...

  3. Effective C++ -----条款12: 复制对象时勿忘其每一个成分

    Copying函数应该确保复制“对象内的所有成员变量”及“所有base class成分”. 不要尝试以某个copying函数实现另一个copying函数.应该将共同机能放进第三个函数中,并由两个cop ...

  4. Effective C++ 条款12:复制对象时勿忘其每一个成分

    void logCall(const std::string& funcName); class Customer { public: ... Customer (const Customer ...

  5. 条款12:复制对象时勿忘其每一个成分(Copy all parts of an object)

    NOTE: 1.Copying 函数应该确保复制“对象内的所有成员变量”及“所有base class成分”. 2.不要尝试以某个copying函数实现另一个copying函数.应该将共同机能放进第三个 ...

  6. [Effective C++ --012]复制对象时勿忘其每一个成分

    引言: 在深拷贝和浅拷贝的理解中,我们知道了“拷贝构造函数”一词,并且也了解了它的构成. A(const A& r); // 形式有多种,在这里只列出一个 因此,在值传递的应用场景里,我们可以 ...

  7. C++复制对象时勿忘每一部分

    现看这样一个程序: void logCall(const string& funcname) //标记记录 { cout <<funcname <<endl; } cl ...

  8. Effective C++(12) 复制对象时要复制每一个成员

    问题聚焦: 负责拷贝的两个操作:拷贝构造函数和重载赋值操作符. 一句话总结,确保被拷贝对象的所有成员变量都做一份拷贝. Demo   void logCall(const std::string&am ...

  9. Effective C++ 条款11,12 在operator= 中处理“自我赋值” || 复制对象时不要忘记每一个成分

    1.潜在的自我赋值     a[i] = a[j];     *px = *py; 当两个对象来自同一个继承体系时,他们甚至不需要声明为相同类型就可能造成别名. 现在担心的问题是:假如指向同一个对象, ...

随机推荐

  1. The Hacker's Guide To Python 单元测试

    The Hacker's Guide To Python 单元测试 基本方式 python中提供了非常简单的单元测试方式,利用nose包中的nosetests命令可以实现简单的批量测试. 安装nose ...

  2. Dash文档制作教程

    前言 什么是Dash 面向程序员的文档库(Mac) 代码片段管理工具 这是强烈推荐给每天在各种API文档中摸爬滚打的程序员们的神器. 为什么要自己制作文档 官方的源中没有相关文档 文档在离线下体验更好 ...

  3. 优化MySchool数据库设计之【巅峰对决】

    优化MySchool数据库设计 之独孤九剑 船舶停靠在港湾是很安全的,但这不是造船的目的 By:北大青鸟五道口原玉明老师 1.学习方法: 01.找一本好书 初始阶段不适合,可以放到第二个阶段,看到知识 ...

  4. GitHub的使用记录

    前言: 该贴为笔者在使用GItHub中的一些使用注意,及Git的基本命令,会一直记录笔者在使用GitHub中可能产生的错误及解决方法(会一直更新中),待一些Git初使用者参考,如果有说明不详细或不对的 ...

  5. Autocomplete 自动补全(Webform实战篇)

    开篇语 因为项目中需要用到一个自动补全的功能,功能描述: 需求一:新增收件人的时候,自动下拉显示出数据库中所有的收件人信息(显示的信息包括:姓名-收件地址-联系方式) 需求二:选中一个值得时候,分别赋 ...

  6. C++中public、protected及private用法

    转自:http://www.jb51.net/article/54224.htm 初学C++的朋友经常在类中看到public,protected,private以及它们在继承中表示的一些访问范围,很容 ...

  7. 【开源】OSharp框架解说系列(2.1):EasyUI的后台界面搭建及极致重构

    OSharp是什么? OSharp是个快速开发框架,但不是一个大而全的包罗万象的框架,严格的说,OSharp中什么都没有实现.与其他大而全的框架最大的不同点,就是OSharp只做抽象封装,不做实现.依 ...

  8. 谈谈基于OAuth 2.0的第三方认证 [中篇]

    虽然我们在<上篇>分别讨论了4种预定义的Authorization Grant类型以及它们各自的适用场景的获取Access Token的方式,我想很多之前没有接触过OAuth 2.0的读者 ...

  9. 创建第一个 local network(II)- 每天5分钟玩转 OpenStack(81)

    上一节通过 Web GUI 创建了 “first_local_net”,本节我们需要搞清楚底层网络结构有了哪些变化? 点击 “first_local_net” 链接,显示 network 的 subn ...

  10. 从零开始编写自己的C#框架(8)——后台管理系统功能设计

    还是老规矩先吐下槽,在规范的开发过程中,这个时候应该是编写总体设计(概要设计)的时候,不过对于中小型项目来说,过于规范的遵守软件工程,编写太多文档也会拉长进度,一般会将它与详细设计合并到一起来处理,所 ...