《Effective C++》学习笔记 续

条款31:将文件间编译依存关系降至最低

请记住:

  • 支持”编译依存性最小化“的一般构想是:相依于声明式,不要相依于定义式。基于此构想的两个手段是Handle class和Interface class
  • 程序库头文件应该以”完全且仅有声明式“的形式存在。这种做法不论是否涉及template都适用

pimpl手法:在一个类中存放具体实现类的指针。这样,指针大小在32位系统上固定是4字节,无需知道具体实现类的大小。这解决了”接口与实现的分离“。是一种Handle class。

降低编译依存关系:

  • 如果使用object reference或object pointers可以完成任务,就不要使用objects
  • 如果能够,尽量以class声明替换class定义
  • 为声明式和定义式提供不同的头文件

抽象基类是interface class。

Handle class和Interface class可降低编译依存性。
 

条款32:确定你的 public 继承塑模出 is-a 关系

请记住:

  • public继承意味着is-a。适用于base class身上的每一件事一定也适用于derived class身上,因为每一个derived class对象也都是一个base class对象。

条款33:避免遮掩继承而来的名称

内层作用域的名称会遮掩外围作用域的名称

请记住:

  • derived class内的名称会遮掩base class内的名称(一般是一个类中同名函数多个重载的情况)。在public继承下从来没有人希望如此。
  • 为了让被遮掩的名称重见天日,可使用using声明式或转交函数(有一点理解,using)。

如果只想继承父类的部分函数,就可以使用private继承,然后设置一个转交函数,转交函数中使用父类作用域调用父类函数。

条款中,父类有多个重载函数的的情况,不论是虚函数还是非虚函数,都会发生遮掩。
 

 using 可以帮助我们获得base 中被遮掩的函数,但是如果我们只想要多个重载函数中的一个而不是全部的时候该怎么办呢?可以写一个简单的转交函数:

条款34:区分接口继承和实现继承

请记住:

  • 接口继承和实现继承不同。在 public 继承之下,derived classes 总是继承 base class 的接口
  • pure virtual 函数只具体指定接口继承
  • 简朴的(非纯)impure virtual 函数具体指定接口继承及缺省实现继承
  • non-virtual 函数具体指定接口继承以及强制性实现继承

条款35:考虑 virtual 函数以外的其他选择

  1. NVI手法:non-virtual interface

条款36:绝不重新定义继承而来的 non-virtual 函数

请记住:

  • 绝不重新定义继承而来的non-virtual函数
  1. 为什么?因为,重新定义non-virtual函数会打破is-a的关系。

条款37:绝不重新定义继承而来的缺省参数值

 对象的所谓静态类型(static type)就是它在程序中被声明时所采用的类型

virtual 函数是动态绑定的,但是缺省参数却是静态绑定的。意思是你可能会在“调用一个定义于 derived class”内的 virtual 函数的同时,却使用 base class 为它所指定的缺省参数值

请记住:

  • 绝对不要重新定义一个继承而来的缺省参数值,因为缺省参数值都是静态绑定,而 virtual 函数--你唯一应该覆写的东西--却是动态绑定

条款38:通过复合塑模出 has-a 或“根据某物实现出”

请记住:

  • 复合的意义和public继承完全不同
  • 在应用领域,复合意味着has-a。在实现领域,复合意味着is-implemented-terms-of(根据某物实现出)
  1. 复合有一些同义词:分层、内含、聚合、内嵌

条款39:明智而审慎地使用 private 继承

请记住:

  • private继承意味着implemented-in-terms-of(根据某类实现出)。他通常比复合的级别低。但是当derived class需要访问protected base class的成员,或需要重新定义继承而来的virtual函数时,这么设计是合理的。
  • 和复合不同,private继承可以造成empty base最优化(EBO,空白基类最优化)。这对致力于”对象尺寸最小化“的程序库开发人员而言,可能很重要。
     

如果 classes 之间地继承关系是 private,编译器不会自动将一个 derived class 对象转换成一个 base class 对象。由 private base class 继承而来的所有成员,在 derived class 中都会变成 private 属性

当 class 不带任何数据,没有 non-static 成员变量,没有 virtual 函数,也没有 virtual base classes 于是这种所谓的 empty classes 对象不适用任何空间,因为没有任何隶属对象的数据需要存储。然后由于技术上的理由,C++裁定凡是独立(非附属)对象都必须有非零大小。假设Empty就是这样的一个类,在大多数编译器中 sizeof(Empty) 获得 1 ,通常C++官方勒令默默安插一个 char 到空对象中。如果有对齐要求,可能不止1个char 大小

条款40:明智而审慎地使用多重继承

请记住:

  • 多重继承比单一继承复杂。他可能导致新的歧义性,以及对virtual继承的需要
  • virtual继承会增加大小、速度、初始化以及赋值复杂度等等成本。如果virtual base class不带任何数据,将是最具实用价值的情况
  • 多重继承的确有正当用途。其中一个情节涉及public继承某个Interface class和private继承某个协助实现的class的两相结合。
  1. 之所以用virtual继承,是因为在菱形继承体系中,基类的一个变量可能会导致孙类里面有两个副本,这就乱套了。
  2. 虚继承的原理:实际上,vbptr指的是虚基类表指针(virtual base table pointer),该指针指向了一个虚表(virtual table),虚表中记录了vbptr与本类的偏移地址;第二项是vbptr到共有基类元素之间的偏移量。
     

条款41:了解隐式接口和编译期多态

对函数重载解析的一点理解:具现化一个模板函数,那么就是给T指定了类型,根据C++的编译法则,这个函数后面会跟上这个类型来描述这个函数名,这会引发多态。

以不同的 template 参数具现化 “function templates” 会导致调用不同的函数,这便是所谓的编译期多态(compile-time polymorphism)

运行期多态和编译期多态之间的差异:类似于 哪一个重载函数该被调用(发生在编译期)和 哪一个 virtual 函数该被绑定(发生在运行期)之间的差异

请记住:

  • class和template都支持接口和多态
  • 对class而言,接口是显示的,以函数签名为中心。多态则是通过virtual函数发生于运行期
  • 对template参数而言,接口是隐式的,奠基于有效表达式。多态则是通过template具现化和函数重载解析发生于编译期。

条款42:了解 typename 的双重意义

请记住:

  • 声明template参数时,前缀关键字class和typename可互换
  • 请使用关键字typename标识嵌套从属类型名称;但不得在base class list或member initialization list内以他作为base class的修饰符

条款43:学习处理模板化基类内的名称

请记住;

  • 可在 derived class templates 内通过 “this->” 指涉 base class templates 内的成员名称,或籍由一个明白写出的 base class 资格修饰符完成

所谓基类资格修饰符:using、作用域说明符::

这种情况:继承模板类调用模板父类的函数,如果像普通继承体系下那么调用,编译器是找不到父类的这个被调用函数的。这种情况出现的原因是:当编译器看到继承模板类时,模板父类并没有被具现化,那么编译器就不知道父类看起来像什么,也就没办法找到父类的成员函数。

模板全特化:类型参数被全部定义,没有其他模板参数可定义
 

条款44:将与参数无关的代码抽离 template

class templates 的成员函数只有被使用时才暗中具现化

请记住:

  • Template生成多个class和多个函数,所以任何template代码都不应该与某个造成膨胀的template参数产生相依关系(比如说非类型参数)
  • 因非类型模板参数而造成的代码膨胀,往往可消除,做法是以函数参数或class成员变量替换template参数
  • 因参数类型造成的代码膨胀,往往可降低,做法是带有完全相同的二进制表述的具现类型共享实现码(比如说类型int和类型long作为模板类的类型时,应该共享代码)
     

条款45:运用成员函数模板接受所有兼容类型

member function template(常简称为 member templates),其作用是为 class 生成函数:

请记住:

  • 请使用 member function templates( 成员函数模板)生成”可接受所有兼容类型“的函数
  • 如果你生命成员函数模板用于”泛化copy构造“或”泛化assignment“操作,你还是要声明正常的copy构造函数和copy assignment操作符。

条款46:需要类型转换时请为模板定义非成员函数

  1. 为什么?因为在template实参推导过程中,从不将隐式类型转换函数纳入考虑。意思是,你想要在一个地方隐式类型转换,但是template没意识到这个事。

请记住:

  • 当我们编写一个 class template,而它所提供之“与此 template 相关的”函数支持“所有参数之隐式类型转换”时,请将那些函数定义为“class template 内部的 friend 函数”。

条款47:请使用 traits classes 表现类型信息

请记住:

  • traits calss使得类型相关信息在编译期可用。他们以template和template特化完成实现
  • 整合重载技术后,tarits class有可能在编译期对类型执行if…else测试

上述的重载函数是根据 tarits 的类型直接在编译期间就可以确定调用哪一个函数,而放在 if else 里面判断的时候需要等到运行时才能确定

  1. 五种类型的迭代器:

    • Input迭代器:只能一步一步向前移动,只读
    • Output迭代器:只能一步一步向前移动,只写
    • Forward迭代器:只能一步一步向前移动,可读可写
    • Bidirectional迭代器:双向移动
    • Random Access迭代器:双向随机跳跃

条款48:认识 template 元编程

Template metaprogramming (TMP 模板元编程)是编写 template-based C++ 程序并执行于编译期的过程。

  • TMP(template metaprogramming)模板元编程可将工作由运行期移至编译期,因而得以实现早期错误侦察和更高的执行效率
  • TMP可被用来生成”基于政策选择组合“的客户定制代码,可用来避免生成对某些特殊类型并不适合的代码

条款49:了解 new-handler 的行为

当operator new 抛出异常以反映一个未获满足的内存需求之前,它会先调用一个客户指定的错误处理函数,一个所谓的 new-handler(并非全部事实,见条款51)

请记住:

  • set_new_handler允许客户指定一个函数,在内存分配无法获得满足时被调用
  • Nothrow new是一个颇为局限的工具,因为它只适用于内存分配;猴急的构造函数调用还是可能抛出异常的

条款50:了解 new 和 delete 的合理替换时机

请记住:

  • 有许多理由需要些个自定的new和delete,包括改善效能、对heap运用错误进行调试,收集heap使用信息

总的来说,这个条款讲的是,何时重载operator new()和operator delete()

  • 为了检测运用错误:比如delete时失败,抛异常
  • 为了收集动态分配内存之使用统计信息:比如想知道已经分配了多少内存
  • 为了增加分配和回收速度:默认的分配方案可能是中庸的
  • 为了降低缺省内存管理器带来的空间额外开销
  • 为了弥补缺省分配器中的非最佳齐位:相当于是自己定义内存对齐位
  • 为了将相关对象成簇集中:减少缺页中断次数
  • 为了获得非传统的行为:即自定义行为
     

条款51:编写 new 和 delete 时需固守常规

请记住:

  • operator new应该内含一个无穷循环,并在其中尝试分配内存,如果他无法满足内存需求,就该调用new-handler。它也应该有能力处理0-byte申请。class专属版本则还应该处理”比正确大小更大的错误申请“
  • operator delete应该收到null指针时不做任何事情。class专属版本则还应该处理”比正确大小更大的错误申请“
     

条款52:写了 placement new 也要写 placement delete

请记住:

  • 写了placement new也要写placement delete。如果没有写,你的程序可能会发生隐微的时断时续的内存泄漏
  • 当你声明placement new和placement delete,请确保不要无意识地掩盖了他们的正常版本

所谓placement new,就是携带额外参数的operator new

条款53:不要轻易忽略编译器的警告

请记住:

  • 严肃对待编译器发出的警告信息。努力在你的编译器的最高警告级别下争取”无任何警告“的荣誉
  • 不要过度依赖编译器的报警能力,因为不同的编译器对待事情的态度并不相同。一旦移植到另一个编译器上,你原本依赖的警告信息有可能消失。

条款54:让自己熟悉包括 TR1 在内的标准程序库

请记住:

  • C++标准程序库的主要机能由STL、iostreams、locales组成
  • TR1添加了智能指针、一般化函数指针(function)、bind、hash-table容器、正则表达式等
  • TR1自身只是一份规范,为了获得TR1提供的好处,你需要一份实物。一个好的实物来源是Boost
     

条款55:让自己熟悉Boost

请记住:

  • Boost是一个社群,也是一个网站。致力于免费、源码开放、同僚复审的C++程序开发库。Boost在C++标准化过程中扮演深具影响力的角色。
  • Boost提供许多TR1组件实现品,以及其他许多库。

参考文章:《Effective C++》笔记_effectivec++ 笔记-CSDN博客