《Effective C++》条款解读

条款26:尽可能延后变量定义式的出现时间

  1. 一个变量被提前定义出来之后,后续可能因为某些原因没有被使用过,但是你仍得付出变量的构造成本和析构成本
  2. 延迟变量定义到非给他赋初值时,因为构造之后再赋值的成本比初始化的成本高
  3. 如果变量在for循环中被使用,变量在循环外定义,在循环内赋值,调用1次构造函数和析构函数,n次赋值。变量在循环内初始化会调用n次构造函数和n次析构函数。除非赋值成本比构造+析构成本低,或者后续还需要用到这个变量,否则应该选择第二种方案。

条款27:尽量少做转型动作

  1. 转型并非只是告诉编译器把某种类型视为另一种类型,什么都没做,而是真的编译出运行时代码。且对象的布局方式和它们的地址计算方式随编译器的不同而不同。
  2. 转型会让我们写出一些看似正确实际错误的代码。比如说在自类中需要调用父类的方法,一种实现方式是,将this指向的对象强转成父类对象,然后用.操作符调用父类方法,但是实际上改变的只是副本,而不是*this对象,正确的方法应该使用class::方法,或者virtual方法。
  3. dynamic_cast的许多实现版本执行速度相当慢。

条款28:避免返回handles指向对象内部成分

  1. handles包括对象的引用、指针和迭代器,表示用来取得某个对象的东西。
  2. 返回对象内部成员的handlers意味着外部函数可以改变内部数据,即使这个数据被声明为私有,这是一种破坏封装性的行为。如果非要返回内部成员,可以返回const &,使得该成员不被更改。
  3. 返回成员函数的handles同理,即使该函数是私有的,外部也可以调用。
  4. 可能导致dangling handles问题,即原先handles所指东西的所属对象不复存在,因为handles可能比其所指对象的寿命更长。

条款29:为“异常安全”而努力是值得的

  1. 带有异常安全性的函数不会泄露任何资源,不会造成数据破坏。反之,不具有异常安全性的函数可能会造成泄露资源,或者数据被破坏。
  2. 异常安全函数具有三个保证之一:基本保证,如果异常被抛出,程序依然保持在有效状态,没有对象或数据结构被破坏,但是程序的现实状态不可预料,可能和异常前状态一样,也可能是其他有效状态。强烈保证,要么程序执行成功,要么回滚至之前的状态。不抛出异常保证,承诺程序绝不抛出异常。
  3. 往往可以通过“copy-and-swap”实现强烈保证,“copy-and-swap”指为打算修改的对象创建一份副本,在副本上做出修改,如果异常抛出则返回源对象,如果执行成功则交换副本和源对象。但是因为代码中的短板效应,意味着系统的所有部分都要实现强烈保证,在现实上可能达不到。
  4. 如果有一个函数不具备异常安全性,整个系统就不具备异常安全性,所以函数提供的异常安全保证通常最高只是其他函数的异常安全保证的最弱等级。

条款30:透彻了解inlining的里里外外

  1. inline函数能避免函数调用带来的开销,且编译器最优化机制通常都用来浓缩不含函数调用的代码,所以编译器因此有能力对函数本体执行最优化。
  2. inline可能会增加目标代码的大小,过度热衷inlining会造成程序体积太大,可能会导致额外的换页行为,降低指令高速缓存的命中率。
  3. inline只是对编译器的申请,而不是强制命令。
  4. inline函数和templates函数通常被定义于头文件中,因为inlining在大多数程序中是编译器行为,为了将一个“函数调用”替换为“被调用函数的本体”,编译器必须知道这个函数长什么样子。templates一旦被使用,编译器为了将它具体化,也需要知道它张什么样子。如果所有由该templates具体化得到的函数都应该inline,那可以将templates声明为inline,否则不应该。
  5. 不能将虚函数声明为inline,因为virtual意味着直到运行时才确定调用哪个函数。
  6. 不能将构造函数和析构函数声明为inline,因为即使一个空构造函数或析构函数,编译器也会为其填充构造成员变量或是父类对象,并且保证异常安全的代码。声明为inline会导致这些代码插入到其自类的相同位置。
  7. 不能调试inline函数。因为inline函数被定义的地方并不是代码执行的地方,真正被执行的地方是inline函数被展开的地方。

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

  1. 当头文件中接口和实现并未分离时,如果其中任何一处被改变,或者头文件所依赖的其他头文件有任何改变,那么每个包含该头文件的文件也必须重新编译。
  2. 实现类的接口和实现的分离,应该将类分为两个类,一个定义接口,一个实现接口。在定义接口的类中包含指向实现接口类的指针成员,这种设计被称为pimpl idiom(pointer to implementation),客户只include类的定义,而不知道具体实现,如此当接口实现变化时,并不会影响客户编写的代码。
  3. 将声明和定义放在不同的头文件中。
  4. 实现接口和实现分离的一种方式是将类定义为接口,其中通常不带成员变量,也没有构造函数,只有虚析构函数和纯虚函数。由工厂函数或虚构造函数(静态成员函数)创建该类的对象。
  5. 接口和实现分离会带来额外的成本和开销,可以考虑以渐进方式使用这项技术。

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

  1. public继承意味着is-a,适用于base class的每一件事情一定也适用于derived class,因为每一个derived class对象都是一个base class对象。(Liskov Substitution Principle)反之则不成立,并不是所有适用于子类的函数都适用于父类。
  2. 但是有时不严谨的继承关系会打破这一原则。如在鸟类中定义函数fly,企鹅也是一种鸟,但是企鹅并不会飞。正方形是一种矩形,按说所有适用于矩形的操作也适用于正方形,但是矩形的长宽可以不等,正方形的长宽必须相等。
  3. 类之间除了is-a的关系,还有has-a和is-implemented-in-terms-of(根据某物实现出)的关系。

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

  1. 继承中子类的作用域是被嵌套在父类的作用域内的,即子类会优先查找定义在自己内部的函数,查找不到函数名称才会去父类的作用域中查找。

  2. 当父类和子类中出现同名函数时(不管是non-virtual函数,还是virtual函数),定义在子类中的函数会遮掩在父类中定义的同名函数,即使该同名函数在父类中是重载函数,而且子类并没有定义出全部重载函数,子类对象也不会调用父类中符合的函数,即子类把父类中所有的同名函数都屏蔽了。这违背了条款32,父类对象和子类对象并不是is-a的关系

  3. 在子类中使用using Base::fun;可以暴露出父类中定义的全部同名函数。

  4. 有些情况下你只希望暴露出父类定义的同名函数的一个或几个(而不是全部),而又不想违背条款32,可以使用private继承,还有转交函数,转交函数是在子类中定义一个你想暴露父类函数的同名函数(参数类型和返回类型也相同),在函数中调用Base::fun;

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

  1. 接口继承和实现继承不同,public继承下总是会继承接口。
  2. 声明纯虚函数的目的是让子类只继承函数接口,子类必须实现继承来的纯虚函数。从子类对象中调用父类的纯虚函数是可以的,但是意义不大。
  3. 声明虚函数的目的是让子类继承函数的接口和默认实现。但是当子类忘记实现自己的虚函数,使用父类的默认实现时,又可能造成风险,解决办法是让父类声明纯虚函数和一个默认实现函数,在子类重写纯虚函数时,调用父类的默认实现函数。也可以在父类外实现父类的纯虚函数。
  4. 声明非虚函数的目的是让子类继承接口和一份强制实现,它不该在子类中被重新定义,非虚函数的表示其不变性凌驾特异性。
  5. 两个常见的错误,第一个是将所有的成员函数声明为non-virtual,第二个是将所有的成员函数声明为virtual。

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

  1. 使用non-virtual interface(NVI),它是Template Method设计模式的一种特殊形式,以public non-virtual成员函数包装降低访问级别(private或protected)的virtual函数,前提是包装器函数内的执行流程是稳定的,而具体步骤函数的实现是变化的,从而实现抽象不应该依赖实现细节,实现细节应该依赖抽象。
  2. 将virtual函数替换为“函数指针成员变量”,这是Strategy设计模式的一种分解表现形式,在构造函数中接收函数指针,赋给函数指针成员变量。这样做法的风险在于如果外部传入函数依赖类内non-public方法,就会使得类要弱化封装程度(提高访问级别或声明为friend函数)。
  3. 使用function替换virtual函数,允许任何可调用物搭配一个兼容需求的签名式(函数名称,参数类型,返回类型),这也是Strategy设计模式的形式,在构造函数中接收一个可调用对象,包括普通函数指针,函数对象,成员函数指针。
  4. 将继承体系的virtual函数替换为另一个继承体系的virtual函数,这是Strategy设计模式的传统实现手法,在类中包含另一个类的成员变量,由这个类定义virtual函数,由他的子类继承并定义实现出该函数。

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

  1. 非虚函数是静态绑定,虚函数是动态绑定,所以当一个子类对象的地址被赋值给父类指针和子类指针,通过这两个指针访问非虚函数,一个是定义在父类的方法,一个是定义在子类的方法,这就导致了二义性。
  2. 根据条款32:确定你的public继承塑模出is-a关系,和条款34:区分接口继承和实现继承中非虚函数的不变性凌驾其特异性。所以你应该遵循此条款。

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

  1. 缺省参数值就是默认参数值,virtual函数是动态绑定(晚绑定),但是默认参数却是静态绑定(早绑定),如果子类在继承到的虚函数中重新定义传入的默认参数,并且创建了一个子类对象,这个对象中该函数的默认值是从父类中继承得到的,而不是重新定义的。c++坚持这么做的原因是为了程序的执行速度和编译器实现上的简易度。
  2. 即使子类在继承到的虚函数中填写与父类相同的默认参数值,当父类的默认参数值改变时,子类的默认参数值也需要改变,这就带来了重新定义继承虚函数默认参数值的风险。

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

  1. public继承表示是is-a的关系,复合表示是has-a或is-implemented-in-terms-of的关系。如果是可以抽象的真实世界存在的事物,如人、汽车、视频画面等,这些对象属于应用域,而如果是软件中的人工制品,如缓冲区、锁、查找树等,这些对象属于应用域。当复合/组合发生在应用域内的对象则是has-a的关系,当发生在应用域则是is-implemented-in-terms-of的关系。
  2. 复合/组合关系和继承关系完全不同,当继承实现不了,或者违背原则时,应该考虑用组合来实现,并且组合是比继承更好的关系。

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

  1. 如果类之间的继承关系是private,编译器不会自动将一个子类对象转换为一个父类对象,即不能使用父类指针接收子类对象。private继承来的父类所有成员的属性都会变成private,意味着只有实现部分被继承,接口部分应略去。所以private继承不表示子类和父类之间有什么关系,而是你单纯想复用父类中定义的函数。
  2. private继承意味着implemented-in-terms-of(根据某物实现出),条款38也提到组合也产生这样的关系,但是要尽可能使用组合,必要时才使用private继承。

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

  1. 多重继承比单继承复杂,当子类继承的多余一个类中定义了相同名称的函数,就会导致歧义性,并且多重继承很容易形成“钻石型多重继承”,即一个父类被两个子类继承,这两个子类又同时被另一个类继承。“钻石型多重继承”在默认情况下会使得最底层的子类对象拥有两份最顶层父类对象的副本,要解决这个问题需要使用虚继承。
  2. 虚继承会增加对象的大小,增加对象初始化及赋值的复杂度,降低运行速度,如果最顶层父类不带有任何数据情况会好一些。
  3. 多重继承的确有其正当用途。

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

  1. 对于模板类/函数,接口是隐式的,基于有效表达式。多态则是通过template具体化和函数重载解析发生在编译期。

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

  1. 模板内出现的类型如果是依赖于一个模板参数,称之为从属类型,如果从属类型在类内呈嵌套状,我们称它为嵌套从属类型。如果类型并不依赖模板参数,这样的类型称为非从属类型。默认情况下,嵌套从属类型不是类型,除非你使用typename告诉编译器它是类型,如typename C::iterator,typename Base<T>::Nested。但是在继承时,和初始化列表里不应该加typename

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

  1. 在子类模板内通过this->使用父类模板内的成员类型,或者显示写出base::成员类型

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

  1.  
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章