第一章见 Effective C++ 学习笔记 第一章:让自己习惯 C++
第二章见 Effective C++ 学习笔记 第二章:构造、析构、赋值运算
第三章见 Effective C++ 学习笔记 第三章:资源管理
第四章见 Effective C++ 学习笔记 第四章:设计与声明
第五章见 Effective C++ 学习笔记 第五章:实现
第六章见 Effective C++ 学习笔记 第六章:继承与面向对象设计
第七章见 Effective C++ 学习笔记 第七章:模板与泛型编程
第八章见 Effective C++ 学习笔记 第八章:定制 new 和 delete
第九章见 Effective C++ 学习笔记 第九章:杂项讨论

文章目录

  • 条款 05:了解 C++ 默默编写并调用哪些函数
    • 总结
  • 条款 06:若不想使用编译器自动生成的函数,就该明确拒绝
    • 话题 1:主动声明这些函数,并放到 private 中
    • 话题 2:将这些函数放到基类的 private 中
    • 总结
  • 条款 07: 为多态基类声明 virtual 析构函数
    • 话题 1:并不是始终需要将析构函数修饰为 virtual
    • 话题 2: 纯虚析构函数需要提供定义
    • 总结
  • 条款 08: 别让异常逃离析构函数 (重要)
    • 总结
  • 条款 09: 绝不在构造和析构过程中调用 virtual 函数
    • 总结
  • 条款 10: 令 operator= 返回一个 reference to *this
    • 总结
  • 条款 11: 在 operator= 中处理 “自我赋值” (重要)
    • 话题 1:在 operator= 中,做证同测试
    • 话题 2:使用临时对象
    • 话题 3: 更好的办法,copy and swap 技术
    • 总结
  • 条款 12: 复制对象时勿忘其每一个成分
    • 总结

条款 05:了解 C++ 默默编写并调用哪些函数

Know what functions C++ silently writes and calls.

C++ 中,空类并不是空的。

如果你没有指定构造函数,编译器会自动生成 default 构造函数,如果没有指定 copy 构造函数、copy 赋值操作符和析构函数,编译器也会自动生成空的版本。这几个自动生成的函数是 public 和 inline 的,析构函数是非 virtual 的(除非该空类的基类声明了 virtual 的析构函数)。
不过有个前提,只有这些函数被调用时,编译器才会创建。

自动生成的 copy 构造函数和 copy 赋值运算符,是简单的将类的成员全部拷贝赋值。

如果某个未指定 copy 构造函数或 copy 赋值运算符的类内存在引用成员对象或常量成员对象,需要执行类对象的 copy 操作时,编译器会拒绝编译。因为自动生成的 copy 构造函数或 copy 赋值运算符无法处理对引用成员对象和常量成员对象的赋值操作。
示例代码如下:

template<class T>
class DOG {public:DOG(std::string& name, const T& value);// 这里我们只声明构造函数,不声明 copy 赋值运算符函数
private:std::string& name;    // 引用成员对象const T value;         // 常量成员对象
};std::string newDog("Persephone");
std::string oldDog("Satch");
DOG<int> p(newDog, 2);          // 作者当时养的狗狗
DOG<int> s(oldDog, 36);         // 作者之前养的已经去世的狗狗
p = s;                          // 编译器对这条代码无能为力

如果某个类的基类将 copy 构造函数 和 copy 赋值运算符函数声明为 private,那么该类中也不会由编译器自动生成这两个函数,编译器认为它没办法处理 copy 操作时,调用基类 copy 方法的操作。

总结

  • 编译器可以暗自为 class 创建 default 构造函数、copy 构造函数、copy 赋值运算符和析构函数。

条款 06:若不想使用编译器自动生成的函数,就该明确拒绝

Explicitly disallow the use of compiler-generated functions you do not want.

上个条款中我们知道,如果我们不声明 copy 构造函数和 copy 赋值运算符函数,编译器会在需要的时候自动声明。但如果我们不希望这个类的对象有拷贝操作,如何禁止这种事情发生?

话题 1:主动声明这些函数,并放到 private 中

虽然这样,对象没法拷贝了,但类内成员函数和友元函数还是可以访问,而我们通常不会去实现这些 copy 构造函数和 copy 赋值运算符函数,从而导致链接错误。
C++ iostream 库中的函数就是通过这种办法避免拷贝操作。

话题 2:将这些函数放到基类的 private 中

专门做一个基类,来隐藏 copy 构造函数和 copy 赋值运算符函数。代码如下:

class Uncopyable {protected:Uncopyable() {};~Uncopyable() {};
private:Uncopyable(const Uncopyable&);Uncopyable& operator= (const Uncopyable&);
};
class HFS : private Uncopyable {      // 将想要隐藏拷贝函数的类继承自 Uncopyable...
};

当派生类想要做拷贝操作时,因为会调用到基类的 copy 构造函数或 copy 赋值运算符函数,从而被编译器拒绝。
和话题 1 中相比,将问题提前在编译器中暴露。
缺点是可能会导致多重继承。
Boost 库中提供了这样一个函数:noncopyable。

总结

  • 为禁止编译器自动生成函数的机制,可将对应成员函数声明为 private 并不实现。
  • Uncopyable 是另一种可行的做法。

条款 07: 为多态基类声明 virtual 析构函数

Declare destructors virtual in polymorphic base classes.

当子类对象经基类指针删除,同时基类中的析构函数是 non-virtual 的,那么很可能最后子类部分的内容不会被析构掉,导致资源泄漏。
virtual 函数也可以修饰其他成员函数,如果一个基类里有 virtual 修饰的成员函数,那么它也必当有一个 virtual 的析构函数。

话题 1:并不是始终需要将析构函数修饰为 virtual

如果一个类没有任何 virtual 的成员函数,那么它理应被认为它不被设计为一个会被继承的类,这时,不应该将析构函数修饰为 virtual。原因是,带有 virtual 之后,编译器会自动生成虚函数表(virtual table),对应声明的对象将带有虚函数表指针(virtual table pointer),这将会占据额外的内存空间。
需要注意,如果你继承的基类没有 virtual 的析构函数,使用它作为基类是很危险的。所有 STL 容器都是不应该被继承的(C++ 没有禁止继承的操作,比如 Java 的 final class,所以这里是个小坑)。
总结一下,并不是所有类都是为多态设计的,多态的设计里,析构函数修饰为 virtual,其他情况下,如 STL 等,并不是为多态而设计的,所以它们的析构函数是 non-virtual 的。

话题 2: 纯虚析构函数需要提供定义

我们知道,如果一个成员函数被修饰为 pure virtual 的,表示所在的这个类是一个抽象类,抽象类不能定义对象。
但是,必须为纯虚析构函数提供一份定义:

class AW {public:virtual ~AW () = 0;
}
AW::~AW() {}       // 空的定义

原因是,派生类对象被析构时,会首先调用基类的析构函数,即使它是 pure virtual 的也同理,所以为了避免链接错误,还是要给一个定义。

总结

  • 带多态性质的基类应该声明 virtual 的析构函数。如果类内包含任何 virtual 的成员函数,那它就应该使用 virtual 的析构函数。
  • 类的设计不一定是要继承的,或并不是要用于多态的,这时不应该声明 virtual 析构函数。

条款 08: 别让异常逃离析构函数 (重要)

Prevent exceptions from leaving destructors.

析构函数中抛出异常,会让析构动作停止,这可能会导致部分资源不会被成功析构。尽量在析构函数中解决所有异常。
可以在析构函数中用 try 块捕获异常,并 abort 程序或者忽略异常,都不是特别好的办法,前者会导致程序非正常结束,后者会导致不可预见的问题。
如果某个操作可能在失败时抛出异常,而又必须处理该异常,那这个操作就必须放在析构函数以外的其他函数中。

总结

  • 析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下他们(不传播)或结束程序。
  • 如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么 class 应该提供一个普通的函数(非析构函数)来执行这个操作。

条款 09: 绝不在构造和析构过程中调用 virtual 函数

Never call virtual functions during construction or destruction.

派生类在构造时,会先调用基类的构造函数,而此时,派生类还不存在,如果基类构造函数中调用了 virtual 函数,那实际上调用的是基类的 virtual 函数,而不是派生类的版本,而一旦基类中的 virtual 函数是 pure virtual 的,就出错了。但如果其他情况下没出错,程序会出现更诡异的问题。
析构函数也是同理。
有些编译器会针对这种情况做警告,但有时它和链接器都无能为力。比如:

class Base {public:Base() { init(); }               // init 是一个普通成员函数virtual void log() const; // log 是一个 virtual 成员函数
private:void init() {log();                    // 里边调用了 virtual 函数}void log() {...;                      // 也有一份实现}
};
class Derive {public:virtual void log() const;
};

这个程序,我们编译时是正常的,编译器检查不到 Derive() -> Base() -> Base::init() -> Base::log() 这条调用链,但实际运行时你会发现,基类的虚函数被错误的调用了。

唯一的解决办法就是,不要在构造函数和析构函数中调用 virtual 成员函数,如果非得调用,那就把这个 virtual 成员函数改成 non-virtual 的。

总结

  • 在构造和析构期间不要调用 virtual 函数,因为这类调用从不下降至 derived class (比起当前执行构造函数和析构函数的那一层)。

条款 10: 令 operator= 返回一个 reference to *this

Have assignment operators return a reference to *this.

因为赋值支持连锁操作,比如: a = b = c;,所以重载赋值运算符必须返回一个指向操作数左侧对象的指针,也就是 *this。
这不只适用于标准赋值运算符,也适用于任何赋值相关的运算符重载,比如 +=。
内置类型和标准程序库都满足这种规范。

总结

  • 令赋值(assignment)操作符返回一个 reference to *this。

条款 11: 在 operator= 中处理 “自我赋值” (重要)

Handle assigment to self in operaotr=.
同一个对象自己等于自己一般不会有人这么写,但有些时候不一定能看出来,比如:

a[i] = a[j]; // i == j
*px = *py;   // px == py

有些时候,你要自己实现 operator=,如下:

class B {...};
class Widget {...
private:B* pb;
};Widget&
Widget::operator=(const Widget& rhs)
{delete pb;  // pb 是 Wdiget 类中的一个指针对象pb = new B(*rhs.pb);return *this;
}

这个代码中,当operator= 的左值和右值是同一个对象时,就出错了,返回的对象中 pb 指向了一段已经被销毁的内存。

话题 1:在 operator= 中,做证同测试

也就是开头加一段:if (this == &rhs) return *this;。判断如果是同一个对象,就直接返回。
但仍然可能有问题,比如 new B 时出异常了,导致 pb 赋值失败,那返回的对象中 pb 指向的位置也是被销毁的。

话题 2:使用临时对象

先将 pb 赋给临时对象,再 new B,然后把临时对象 delete 掉。
这是一个行得通的办法。

Widget&
Widget::operator=(const Widget& rhs)
{B* temp = pb;pb = new B(*rhs.pb);delete temp;return *this;
}

话题 3: 更好的办法,copy and swap 技术

使用交换技术代替赋值。

class Widget {...
void swap(Widget& rhs);        // 用于交换 rhs 和 *this 的数据
...
};Widget&
Widget:: operator=(const Widget& rhs)
{Widget temp(rhs);         // 仍然需要临时变量swap(temp);return *this;
}
Widget::operator=(Widget rhs) // 另一种变体,传值方式会复制一份副本
{swap(rhs);               // ths 本身是副本,直接交换return *this;
}

总结

  • 确保当对象自我赋值时 operator= 有良好行为。其中技术包括比较“来源对象”和“目标对象”的地址、精心周到的语句顺序、以及 copy-and-swap。
  • 确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确。

条款 12: 复制对象时勿忘其每一个成分

Copy all parts of an object.

Copy 函数包括 copy 构造函数和 copy 赋值运算符,这两个函数不定义的话,编译器会生成默认版本,我们需要确保我们自己定义的 copy 函数一切正常。

如果你先写完了构造函数和 copy 函数,之后又新增了成员变量,那么你需要自己留意在所有这些构造函数和 copy 函数中添加这个新的成员变量的操作,编译器不会提醒你。一旦你忘记了,你的初始化或赋值操作就是不完整的。

如果是一个派生类中的 copy 函数,除了处理好派生类内的成员对象的 copy 操作,还要负责基类中对象的 copy 操作,也就是在初始化列表中完成对基类的 copy 操作。如下代码:

class PC : public Base {public:...PC(const PC& rhs);PC& operator=(const PC& rhs);
private:int p;
};PC::PC(const PC& rhs): Base(rhs),  // 注意这里,调用基类的 copy 构造函数p(rhs.p)
{...
}PC&
PC::operator=(const PC& rhs)
{...Base::operator=(rhs);  // 注意这里,手动调用基类的 copy 赋值运算符函数p = rhs.p;return *this;
}

另外,如果两个 copying 函数中的内容基本一致,比如上面代码 … 部分的内容很多且一致,不要想着用一个 copying 函数调用另一个。应该另外写一个成员函数,在包装这些共同的代码。

总结

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

Effective C++ 学习笔记 第二章:构造、析构、赋值运算相关推荐

  1. 《Go语言圣经》学习笔记 第二章 程序结构

    Go语言圣经学习笔记 第二章 程序结构 目录 命名 声明 变量 赋值 类型 包和文件 作用域 注:学习<Go语言圣经>笔记,PDF点击下载,建议看书. Go语言小白学习笔记,几乎是书上的内 ...

  2. 小吴的《机器学习 周志华》学习笔记 第二章 模型评估与选择

    小吴的<机器学习 周志华>学习笔记 第二章 模型评估与选择 上一周我们介绍了第一章的基础概念,这一次将带来第二章的前三节.后面的2.4 比较检验与2.5 偏差与方差,涉及概率论与数理统计概 ...

  3. PhalAPI学习笔记 ——— 第二章接口服务请求

    PhalAPI学习笔记 --- 第二章接口服务请求 前言 接口服务请求 接口服务请求案例 自定义接口路由 开启匹配路由 配置路由规则 nginx apache 服务请求 结束语 前言 公司业务需要转学 ...

  4. [go学习笔记.第二章] 2.go语言的开发工具以及安装和配置SDK

    一.工具介绍: 1.Visual Studio Code 一个运行于Mac,Windows,和linux上的,默认提供Go语言的语法高亮的IED,可以安装Go语言插件,还可以支持智能提示,编译运行等功 ...

  5. 小吴的《机器学习 周志华》学习笔记 第二章 2.4 比较检验、2.5 偏差与方差

    小吴的<机器学习 周志华>学习笔记 第二章 2.4 比较检验. 2.5 偏差与方差 2.4 比较检验 上一周提到了实验的评价方法和性能量度,步骤简单可以看成:先使用某种实验评估方法测得学习 ...

  6. 机器人导论(第四版)学习笔记——第二章

    机器人学导论(第四版)学习笔记--第二章 2. 空间描述和变换 2.1 引言 2.2 描述:位置.姿态与位姿 2.3 映射:从一个坐标系到另一个坐标系的变换 2.4 算子:平行,旋转和变换 2.5 总 ...

  7. Kotlin学习笔记 第二章 类与对象 第十一节 枚举类 第八节密封类

    参考链接 Kotlin官方文档 https://kotlinlang.org/docs/home.html 中文网站 https://www.kotlincn.net/docs/reference/p ...

  8. 《Effective Java》学习笔记 第二章 创建和销毁对象

    第二章 创建和销毁对象 何时以及如何创建对象,何时以及如何避免创建对象,如何确保他们能够适时地销毁,以及如何管理对象销毁之前必须进行的各种清理动作. 1 考虑用静态工厂方法代替构造器 一般在某处获取一 ...

  9. Kotlin学习笔记 第二章 类与对象 第十四 十五节 委托 委托属性

    参考链接 Kotlin官方文档 https://kotlinlang.org/docs/home.html 中文网站 https://www.kotlincn.net/docs/reference/p ...

  10. Kotlin学习笔记 第二章 类与对象 第一节类与继承(补)

    参考链接 Kotlin官方文档 Kotlin docs | Kotlin 本系列为参考Kotlin中文文档 kotlin官方文档2020版.pdf-其它文档类资源-CSDN下载 第二章 第一节 类与继 ...

最新文章

  1. 安装完python需要再安装编辑器-最好用的Python编辑器——Pycharm之安装与设置
  2. Perl新接触的小命令
  3. 三天流量有效期具体怎么算_信用证具体的费用怎么算?
  4. wget命令出现Unable to establish SSL connection.错误
  5. 如何成为一名数据中心运营工程师?
  6. C语言实现Graph图的算法(附完整源码)
  7. 英文版Ubuntu 16.04系统如何解决gedit中文显示乱码的问题
  8. 双亲委派机制_面试官:双亲委派机制的原理和作用是什么?
  9. Unity带参数的协程
  10. php phantomjs 安装_安装php-phantomjs
  11. js高级程序设计(第五章)
  12. 富文本编辑器在Java中使用
  13. 多元有序logistic回归分析_多元logistics回归分析
  14. 计算标准累积正态分布_神说要有正态分布,于是就有了正态分布。
  15. rstudio 连接mysql_Rstudio ODBC 连接MySQL
  16. css实现固定宽高比例的div
  17. 英语版今日头条到底有多不靠谱?
  18. 常用符号的Unicode表
  19. 如何查看自己电脑的ip地址
  20. (附源码)计算机毕业设计SSM智慧灭火器管理系统

热门文章

  1. 动画效果--漫天飞雪
  2. 分类问题中正负样本分布不均衡问题的解决方法
  3. python倒数切片_python的切片操作
  4. 地图 Api 使用小记 (use 51ditu)
  5. 【Git】fatal: Unable to create ‘.git/index.lock’: File exists.
  6. 第一天的学习内容----Excel自动化处理
  7. 提高你修养的100句话
  8. \t\t林荫苗圃 苗木和苗圃 好苗木种植技术是关键 它好我也好
  9. colorAccent,colorPrimary,colorPrimaryDark……来这里你就明白了
  10. 鸿运当头凤梨花怎么养 凤梨花养殖方法及注意事项