读书笔记 effective c++ Item 12 拷贝对象的所有部分

时间:2022-07-31 01:10:18

1.默认构造函数介绍

在设计良好的面向对象系统中,会将对象的内部进行封装,只有两个函数可以拷贝对象:拷贝构造函数和拷贝赋值运算符。我们把这两个函数统一叫做拷贝函数。从Item5中,我们得知,如果需要的话编译器会为你生成这两个拷贝函数,并且编译器生成的版本能够精确的做到你想做的:它们拷贝了对象的所有数据。

2.自己实现构造函数有可能出现问题

当你声明自己的拷贝函数的时候,你就会向编译器表示,你对编译器生成版本的拷贝函数有些地方不是很喜欢。你的这种做法会让编译器以一种奇怪的方式进行报复:如果你自己实现的拷贝函数出现了问题,编译器不会告诉你

2.1 问题出现场景一

考虑一个表示消费者的类,类中的拷贝函数已经被手动实现了,所以调用它们会被记入日志:

 void logCall(const std::string& funcName); // make a log entry

 class Customer {

 public:

 ...

 Customer(const Customer& rhs);

 Customer& operator=(const Customer& rhs);

 ...

 private:

 std::string name;

 };

 Customer::Customer(const Customer& rhs)

 : name(rhs.name) // copy rhs’s data

 {

 logCall("Customer copy constructor");

 }

 Customer& Customer::operator=(const Customer& rhs)

 {

 logCall("Customer copy assignment operator");

 name = rhs.name; // copy rhs’s data

 return *this; // see Item 10

 }

这里的一切看上去都是好的,也确实如此,直到另外一个数据成员加到Customer类中:、

 class Date { ... }; // for dates in time

 class Customer {

 public:

 ... // as before

 private:

 std::string name;

 Date lastTransaction;

 };

这时候,当前的拷贝函数就会执行一个部分拷贝,它们拷贝了Customer的name成员变量,却没有拷贝lastTransaction.但大多数编译器会对这种实现默不发声,甚至一个警告级别的信息也不会发出来(看Item 53)。编译器对你自己写的拷贝函数进行了报复。你拒绝使用它们提供的拷贝函数,于是它们不会告诉你代码是否是完整的。结论很明显:如果你向类中添加一个数据成员,你需要确保同时对拷贝函数进行更新。(你同时需要更新类中所有的构造函数(Item4和Item45)和任何非标准形式的operator=(Item 10给出了一个例子)),如果你忘记了,编译器不会提醒你。

2.2 更加阴险的方式-场景二

使这个问题出现的最阴险的方式是通过继承。看下面的例子:

 class PriorityCustomer: public Customer { // a derived class

 public:

 ...

 PriorityCustomer(const PriorityCustomer& rhs);

 PriorityCustomer& operator=(const PriorityCustomer& rhs);

 ...

 private:

 int priority;

 };

 PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs)

 : priority(rhs.priority)

 {

 logCall("PriorityCustomer copy constructor");

 }

 PriorityCustomer&

 PriorityCustomer::operator=(const PriorityCustomer& rhs)

 {

 logCall("PriorityCustomer copy assignment operator");

 priority = rhs.priority;

 return *this;

 }

PriorityCustomer的拷贝函数看上去拷贝了类中的所有东西,但请再看一遍。是的,它们拷贝了PriorityCustomer的所有数据成员,但是PriorityCustomer的每个对象同时包含了从Customer继承过来的数据成员,这部分数据没有被拷贝!PriorityCustomer的拷贝构造函数没有指定传到基类构造函数的参数(也就是说没有在成员初始化列表中列出Customer),所以PriorityCustomer对象的Customer部分会被Customer的无参构造函数进行初始化。(肯定会有一个,不然编译会出错。)这个构造函数会为name 和 lastTransaction执行一个默认初始化。

对于PriorityCustomer的拷贝构造运算符来说情形有些不同。它并没有以任何方式去尝试修改基类的数据成员,因此它们可以保持不变。

3.如何才能避免上面的问题

在任何时候你自己去为一个派生类实现拷贝构造函数的时候,你必须注意需要同时拷贝基类部分。这些部分当然有可能是Private的(见Item22),所以你不能直接访问它们。但是,派生类的拷贝函数必须调用对应的基类构造函数:

 PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs)

 : Customer(rhs), // invoke base class copy ctor

 priority(rhs.priority)

 {

 logCall("PriorityCustomer copy constructor");

 }

 PriorityCustomer&

 PriorityCustomer::operator=(const PriorityCustomer& rhs)

 {

 logCall("PriorityCustomer copy assignment operator");

 Customer::operator=(rhs); // assign base class parts

 priority = rhs.priority;

 return *this;

 }

在这个条款的标题中,“拷贝所有部分”的意思现在应该明了了。当你实现一个拷贝函数的时候,确保1)拷贝所有本地的数据成员。(2)同时调用所有基类的合适的拷贝函数

4.如何才能解决构造函数中的代码重复问题

在实际应用中,这两个拷贝函数通常有着类似的函数体,这会让你尝试通过一个函数调用另一个函数以达到避免代码重复的目的。你的这种想避免代码重复的愿望是值得赞赏的,但为了达到避免代码重复用一个拷贝函数调用另外一个是错误的方法。

4.1 用赋值运算符调用拷贝构造函数-错误!

用拷贝赋值运算符来调用拷贝构造函数是没有意义的,因为你正在尝试构建一个已经存在的对象。这是荒谬的,也没有这样的语法。看上去有一些能够到达你要求的语法,但实际上不是。有一些语法确实能够做到,但在一些情况下会破坏你的对象。所以我不会向你展示这些语法的任何部分。你不想通过拷贝赋值运算符去调用拷贝构造函数,接受这个想法就可以了。

4.2 用拷贝构造函数调用赋值运算符-错误!

相反,使用拷贝函数调用拷贝赋值运算符也同样是没有意义的。一个拷贝构造函数是初始化新的对象的,但是一个赋值运算符只能够应用在已经被初始化的对象上面。在一个对象上通过构造函数来执行赋值就意味着,你正在对一个未初始化的对象做某些事情,但这件事情对初始化的对象才有意义。没有意义,不要去尝试!

4.3 正确的做法-将相同代码提炼成第三个函数

想反,如果你发现你的拷贝构造函数和拷贝赋值运算符有着看上去类似的函数体,通过创建可以同时被两个构造函数调用的第三个成员函数来消除代码重复。这样的函数应该被声明为Private并且通常叫做Init.这个策略是安全的,可以达到消除重复的目的。

读书笔记 effective c++ Item 12 拷贝对象的所有部分的更多相关文章

  1. 读书笔记 effective c++ Item 13 用对象来管理资源

    1.不要手动释放从函数返回的堆资源 假设你正在处理一个模拟Investment的程序库,不同的Investmetn类型从Investment基类继承而来, class Investment { ... ...

  2. 读书笔记 effective c++ Item 4 确保对象被使用前进行初始化

    C++在对象的初始化上是变化无常的,例如看下面的例子: int x; 在一些上下文中,x保证会被初始化成0,在其他一些情况下却不能够保证.看下面的例子: class Point { int x,y; ...

  3. 读书笔记 effective c++ Item 14 对资源管理类的拷贝行为要谨慎

    1. 自己实现一个资源管理类 Item 13中介绍了 “资源获取之时也是初始化之时(RAII)”的概念,这个概念被当作资源管理类的“脊柱“,也描述了auto_ptr和tr1::shared_ptr是如 ...

  4. 读书笔记 effective c++ Item 25 实现一个不抛出异常的swap

    1. swap如此重要 Swap是一个非常有趣的函数,最初作为STL的一部分来介绍,它已然变成了异常安全编程的中流砥柱(Item 29),也是在拷贝中应对自我赋值的一种普通机制(Item 11).Sw ...

  5. 读书笔记 effective c++ Item 17 使用单独语句将new出来的对象放入智能指针

    1. 可能会出现资源泄漏的一种用法 假设我们有一个获取进程优先权的函数,还有一个在动态分类的Widget对象上根据进程优先权进行一些操作的函数: int priority(); void proces ...

  6. 读书笔记 effective c++ Item 28 不要返回指向对象内部数据(internals)的句柄(handles)

    假设你正在操作一个Rectangle类.每个矩形可以通过左上角的点和右下角的点来表示.为了保证一个Rectangle对象尽可能小,你可能决定不把定义矩形范围的点存储在Rectangle类中,而是把它放 ...

  7. 读书笔记 effective c++ Item 5 了解c++默认生成并调用的函数

    1 编译器会默认生成哪些函数  什么时候空类不再是一个空类?答案是用c++处理的空类.如果你自己不声明,编译器会为你声明它们自己版本的拷贝构造函数,拷贝赋值运算符和析构函数,如果你一个构造函数都没有声 ...

  8. 读书笔记 effective c++ Item 6 如果你不想使用编译器自动生成的函数,你需要明确拒绝

    问题描述-阻止对象的拷贝 现实生活中的房产中介卖房子,一个服务于这个中介的软件系统很自然的会有一个表示要被销售的房屋的类: class HomeForSale { ... }; 每个房产中介会立刻指出 ...

  9. 读书笔记 effective c++ Item 11 在operator=中处理自我赋值

    1.自我赋值是如何发生的 当一个对象委派给自己的时候,自我赋值就会发生: class Widget { ... }; Widget w; ... w = w; // assignment to sel ...

随机推荐

  1. <img>标签链接地址失效如何自动显示默认图片

    <img src="errurl" onerror="this.src='default.jpg'">

  2. linux创建动态库

    [1]新建源程序sharelib.c /************************************************************************* > F ...

  3. Linux Linux程序练习六

    题目:实现一个so库文件名称为listupper.so,so文件中实现一个函数,函数名为void upper(const char *src, char *desc),调用update后将参数src所 ...

  4. 朋友的发展---&gt&semi;对自己深深地激励。

    从4月5号来厦门开始实习到现在,也断断续续的跟着大佬开始实现需求了,就记录下自己这一段时间的想法吧,可能未来的自己看来会觉得挺可笑的,这个春招,说实话,自己挺失败的,为了求稳,来厦门这边面试美团,以至 ...

  5. IIS Express(7&period;0) HTTP 错误 500&period;22 - Internal Server Error(vs2013)

    1.错误如下: HTTP 错误 500.22 - Internal Server Error 检测到在集成的托管管道模式下不适用的 ASP.NET 设置. 解决的方法: 首先,找到本地appcmd.x ...

  6. android初学

    1布局 LinearLayout 线性布局 FrameLayout 框架布局 AbsoluteLayout 绝对布局 RelativeLayout 相对布局 TableLayout 表格布局 2标记语 ...

  7. 目标检测之faster-RCNN和FPN

    今年(2017年第一季度),何凯明大神出了一篇文章,叫做fpn,全称是:feature pyramid network for object Detection,为什么发这篇文章,根据 我现在了解到的 ...

  8. 基于rsync的lsyncd自动同步配置

    环境部署 源机:192.168.31.140 目标机:192.168.31.130 源机配置 基于rsync的lsyncd 自动同步,rsync的配置省略 安装lsyncd rpm -ivh lsyn ...

  9. 关于导入高德地图 java&period;lang&period;UnsatisfiedLinkError&colon; Couldn&&num;39&semi;t load XXXfrom loader dalvik&period;system&period;PathClassLoader&lbrack;DexPathLis

    然后后面就是找不到高德地图提供的地图so 就是上面几个 然后不要忘了在buildGradle文件里添加这么一句话 sourceSets { main { jniLibs.srcDirs = ['lib ...

  10. 【Python】新建自定义个数的自定义长度名字

    # -*- coding:utf-8 -*- import random def CreateRandomName(number,length): """ :param ...