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

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

1.默认构造函数介绍

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

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

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

2.1 问题出现场景一

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

 1 void logCall(const std::string& funcName); // make a log entry
 2 
 3 class Customer {
 4 
 5 public:
 6 
 7 ...
 8 
 9 Customer(const Customer& rhs);
10 
11 Customer& operator=(const Customer& rhs);
12 
13 ...
14 
15 private:
16 
17 std::string name;
18 
19 };
20 
21 Customer::Customer(const Customer& rhs)
22 
23 : name(rhs.name) // copy rhs’s data
24 
25 {
26 
27 logCall("Customer copy constructor");
28 
29 }
30 
31 Customer& Customer::operator=(const Customer& rhs)
32 
33 {
34 
35 logCall("Customer copy assignment operator");
36 
37 name = rhs.name; // copy rhs’s data
38 
39 return *this; // see Item 10
40 
41 }

 

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

 1 class Date { ... }; // for dates in time
 2 
 3 class Customer {
 4 
 5 public:
 6 
 7 ... // as before
 8 
 9 private:
10 
11 std::string name;
12 
13 Date lastTransaction;
14 
15 };

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

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

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

 1 class PriorityCustomer: public Customer { // a derived class
 2 
 3 public:
 4 
 5 ...
 6 
 7 PriorityCustomer(const PriorityCustomer& rhs);
 8 
 9 PriorityCustomer& operator=(const PriorityCustomer& rhs);
10 
11 ...
12 
13 private:
14 
15 int priority;
16 
17 };
18 
19 PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs)
20 
21 : priority(rhs.priority)
22 
23 {
24 
25 logCall("PriorityCustomer copy constructor");
26 
27 }
28 
29 PriorityCustomer&
30 
31 PriorityCustomer::operator=(const PriorityCustomer& rhs)
32 
33 {
34 
35 logCall("PriorityCustomer copy assignment operator");
36 
37 priority = rhs.priority;
38 
39 return *this;
40 
41 }

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

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

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

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

 1 PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs)
 2 
 3 : Customer(rhs), // invoke base class copy ctor
 4 
 5 priority(rhs.priority)
 6 
 7 {
 8 
 9 logCall("PriorityCustomer copy constructor");
10 
11 }
12 
13 PriorityCustomer&
14 
15 PriorityCustomer::operator=(const PriorityCustomer& rhs)
16 
17 {
18 
19 logCall("PriorityCustomer copy assignment operator");
20 
21 Customer::operator=(rhs); // assign base class parts
22 
23 priority = rhs.priority;
24 
25 return *this;
26 
27 }

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

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

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

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

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

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

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

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

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


作者: HarlanC

博客地址: http://www.cnblogs.com/harlanc/
个人博客: http://www.harlancn.me/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出, 原文链接

如果觉的博主写的可以,收到您的赞会是很大的动力,如果您觉的不好,您可以投反对票,但麻烦您留言写下问题在哪里,这样才能共同进步。谢谢!

目录
相关文章
|
8月前
|
编译器 C++ 开发者
【C++篇】深度解析类与对象(下)
在上一篇博客中,我们学习了C++的基础类与对象概念,包括类的定义、对象的使用和构造函数的作用。在这一篇,我们将深入探讨C++类的一些重要特性,如构造函数的高级用法、类型转换、static成员、友元、内部类、匿名对象,以及对象拷贝优化等。这些内容可以帮助你更好地理解和应用面向对象编程的核心理念,提升代码的健壮性、灵活性和可维护性。
|
8月前
|
编译器 C语言 C++
类和对象的简述(c++篇)
类和对象的简述(c++篇)
|
7月前
|
编译器 C++
类和对象(中 )C++
本文详细讲解了C++中的默认成员函数,包括构造函数、析构函数、拷贝构造函数、赋值运算符重载和取地址运算符重载等内容。重点分析了各函数的特点、使用场景及相互关系,如构造函数的主要任务是初始化对象,而非创建空间;析构函数用于清理资源;拷贝构造与赋值运算符的区别在于前者用于创建新对象,后者用于已存在的对象赋值。同时,文章还探讨了运算符重载的规则及其应用场景,并通过实例加深理解。最后强调,若类中存在资源管理,需显式定义拷贝构造和赋值运算符以避免浅拷贝问题。
|
7月前
|
存储 编译器 C++
类和对象(上)(C++)
本篇内容主要讲解了C++中类的相关知识,包括类的定义、实例化及this指针的作用。详细说明了类的定义格式、成员函数默认为inline、访问限定符(public、protected、private)的使用规则,以及class与struct的区别。同时分析了类实例化的概念,对象大小的计算规则和内存对齐原则。最后介绍了this指针的工作机制,解释了成员函数如何通过隐含的this指针区分不同对象的数据。这些知识点帮助我们更好地理解C++中类的封装性和对象的实现原理。
|
7月前
|
编译器 C++
类和对象(下)C++
本内容主要讲解C++中的初始化列表、类型转换、静态成员、友元、内部类、匿名对象及对象拷贝时的编译器优化。初始化列表用于成员变量定义初始化,尤其对引用、const及无默认构造函数的类类型变量至关重要。类型转换中,`explicit`可禁用隐式转换。静态成员属类而非对象,受访问限定符约束。内部类是独立类,可增强封装性。匿名对象生命周期短,常用于临时场景。编译器会优化对象拷贝以提高效率。最后,鼓励大家通过重复练习提升技能!
|
8月前
|
安全 编译器 C语言
【C++篇】深度解析类与对象(中)
在上一篇博客中,我们学习了C++类与对象的基础内容。这一次,我们将深入探讨C++类的关键特性,包括构造函数、析构函数、拷贝构造函数、赋值运算符重载、以及取地址运算符的重载。这些内容是理解面向对象编程的关键,也帮助我们更好地掌握C++内存管理的细节和编码的高级技巧。
|
8月前
|
存储 程序员 C语言
【C++篇】深度解析类与对象(上)
在C++中,类和对象是面向对象编程的基础组成部分。通过类,程序员可以对现实世界的实体进行模拟和抽象。类的基本概念包括成员变量、成员函数、访问控制等。本篇博客将介绍C++类与对象的基础知识,为后续学习打下良好的基础。
|
4月前
|
人工智能 机器人 编译器
c++模板初阶----函数模板与类模板
class 类模板名private://类内成员声明class Apublic:A(T val):a(val){}private:T a;return 0;运行结果:注意:类模板中的成员函数若是放在类外定义时,需要加模板参数列表。return 0;
110 0
|
4月前
|
存储 编译器 程序员
c++的类(附含explicit关键字,友元,内部类)
本文介绍了C++中类的核心概念与用法,涵盖封装、继承、多态三大特性。重点讲解了类的定义(`class`与`struct`)、访问限定符(`private`、`public`、`protected`)、类的作用域及成员函数的声明与定义分离。同时深入探讨了类的大小计算、`this`指针、默认成员函数(构造函数、析构函数、拷贝构造、赋值重载)以及运算符重载等内容。 文章还详细分析了`explicit`关键字的作用、静态成员(变量与函数)、友元(友元函数与友元类)的概念及其使用场景,并简要介绍了内部类的特性。
185 0
|
6月前
|
编译器 C++ 容器
【c++11】c++11新特性(上)(列表初始化、右值引用和移动语义、类的新默认成员函数、lambda表达式)
C++11为C++带来了革命性变化,引入了列表初始化、右值引用、移动语义、类的新默认成员函数和lambda表达式等特性。列表初始化统一了对象初始化方式,initializer_list简化了容器多元素初始化;右值引用和移动语义优化了资源管理,减少拷贝开销;类新增移动构造和移动赋值函数提升性能;lambda表达式提供匿名函数对象,增强代码简洁性和灵活性。这些特性共同推动了现代C++编程的发展,提升了开发效率与程序性能。
217 12