《Effective C++》阅读总结(五): 继承与面向对象设计&模板&内存&杂项讨论
第六章 继承与面向对象设计
32. 确定你的public继承塑模出is-a关系
public继承意味着要塑模出is-a的关系,所以每一个子类对象也是相应的父类对象,故适用于父类对象的每一个操作也都适用于子类对象。
33. 避免遮掩继承而来的名称
派生类中的名称(包括数据名称和函数名称(不考虑不同传参))会遮掩基类中的名称,这种机制类似于作用域,先从当前类中搜索名称,如果找不到则到基类中搜索,以此类推。这个名称遮掩规则主要是防止新建的子类继承了较为疏远的基类同名函数,这在大部分情况下是不期望的。如果期望不要遮掩,使用using显式暴露即可。
34. 区分接口继承和实现继承
class的成员函数可分为三类:一般函数、虚函数、纯虚函数。一般函数的接口和实现都会被强制继承;虚函数主要是继承接口且继承一个缺省实现;纯虚函数只有接口继承。带有virtual关键字的虚函数和纯虚函数可通过虚函数表访问到实际类对象的成员函数,即多态调用。
35. 考虑virtual函数以外的其他选择
① virtual函数的替代方案包括NVI(non-virtual interface)和strategy设计模式的多种形式,NVI本身也是一种特殊形式的Template method设计模式;
② 将机能从成员函数移动到class外部的缺点是:非成员函数无法访问class的non-public成员;
③ std::function对象即泛化的函数指针,可指代一切可调用对象,只要可调用对象的函数签名一致,常与std::bind配合使用,用以实现回调机制。
36. 绝不重新定义继承而来的non-virtual函数
这条准则在多态调用的类继承体系中是很重要的,因为一般情况下,多态调用我们会将一个子类对象的地址赋给一个父类指针,然后通过调用父类中的接口,去执行子类中的对应实现。实现这个目的的前提是调用的函数是虚函数(包括纯虚函数),这样才能从虚函数表中找到真正的函数地址。然而如果调用的是非虚函数,那么调用的函数将是指针静态类型所对应的静态绑定的成员函数,这不是我们期望的。当然,如果仅仅是使用子类而不多态调用,重新定义父类非虚函数也未尝不可,那么父类中的同名函数将被遮掩。
37. 绝不重新定义继承而来的缺省参数值
既然是重新定义,那么该成员函数必将是virtual的,virtual函数是动态绑定的,但缺省参数值是静态绑定的,所以如果你将继承而来的virtual函数修改了缺省参数值,那是不会生效的,多态调用时将传入基类指定的缺省参数。
38. 通过复合塑模出has-a关系或“根据某物实现出”
复合即一个对象拥有另一个对象,即塑模了has-a的关系;
在应用域,复合意味着A has-a B,即具体某个东西A拥有着另一个东西B;而在实现域,复合意味着根据某物B实现出某物A,即某物A的实现是要靠B的某些特性来完成的,如根据queue实现出stack。
39. 明智而审慎地使用private继承
private继承意味着根据某物实现出,它要比复合的级别低。当你想要创建一个class A,需要用到另一个class B的某些特性时,直接复合是很直观的手段。但是,如果你想对B的某些接口进行适配改造的话,使用private是一个明智的选择。
40. 明智而审谨地使用多重继承
多重继承要比单一继承复杂,有可能导致歧义。一个常用的场景是:子类public继承某个接口类,并private继承某个辅助类,两相结合来使用。
第七章 模板与泛型编程
模板元编程是C++一个非常大且难掌握的模块了,这块内容还掌握的很浅,现在只会将参数类型抽到模板函数实现上。相关实践较少,等有点积累了再深入学习。
41. 了解隐式接口与编译期多态
① class合template都支持接口和多态
② class的接口是显式的,以函数签名为中心。多态是通过virtual函数发生于运行期;
③ template的接口是隐式的,奠基于有效表达式。其多态是通过template具现化和函数重载解析发生于编译期。
42. 了解typename的双重意义
① 声明template参数时,前缀关键字class和typename等价
② 请使用关键字typename标识嵌套从属类型名称;但不得在base class lists(基类列)或member initialization list(成员初始列)内作为base class修饰符。
43. 学习处理模板化基类内的名称
可在派生类内通过"this->"指涉base class template内的成员名称,或籍由一个明白写出的"base class 资格修饰符"完成。
44. 将与参数无关的代码抽离template
① Template生成多个classes和多个函数,所以任何template代码都不该与某个造成膨胀的template产生依赖关系。
② 因非类型模板参数而造成的代码膨胀,往往可以消除,做法是以函数参数或class成员变量替代template参数。
③ 因类型参数而造成的代码膨胀,往往可以降低。做法是让带有完全相同二进制表述的具现类型共享实现码。
45. 运用成员函数模板接受所有兼容类型
① 请使用成员函数模板生成“可接受所有可兼容类型”的函数。
② 如果你声明成员函数模板用以“泛化copy构造”或“泛化copy赋值符”,你还需要声明正常的copy构造函和copy赋值操作符。
46. 需要类型转换时请为模板定义非成员函数
略
47. 请使用traits classes表现类型信息
略
48. 认识template元编程
① 模板元编程可将工作由运行期移往编译期,从而可以实现早期的错误诊断和更高的执行效率;
② 模板元编程可被用来生成“基于政策选择组合”的客户定制代码,也可用来避免生成对某些特殊类型并不合适的代码。
第八章 定制new和delete
这章的内容深入理解的话,讲的就是C++内存模型的相关东西了,这部分内容都可以单独开一门课了,这里只做简单记录。
49. 了解new-handler的行为
① set_new_handlers允许客户指定一个函数,在内存分配无法获得满足的时候被调用。
② Nothrow new是一个颇为局限的工具,因为它只适用于内存分配;后继的构造函数调用还是有可能抛出异常。
50. 了解new和delete的合理替换时机
有许多时候需要写个自定义的new和delete,包括改善性能、对heap运用错误进行调试、收集heap使用信息。
51. 编写new和delete时需要固守常规
① operator new应该内含一个无穷循环,并在其中尝试分配内存,如果它无法满足需求,就该调用new-handler。它应该有能力处理0 bytes申请。class专属版本则还应该处理“比正确大小更大的(错误)申请”;
② operator delete应该在收到null指针时不做任何事情。class专属版本则还应该处理“比正确大小更大的(错误)申请”。
52. 写了placement new时也要写placement delete
① 当你写了一个placement operator new,请确定也写出了对应的placement operator delete。如果没有这样做,你的程序可能会发生隐蔽而时断时续的内存泄漏。
② 当你声明placement new和placement delete时,请确认不要无意识地遮掩了它们地正常版本。
第九章 杂项讨论
53. 不要轻易忽视编译器的警告
① 严肃对待编译器发出地警告信息,努力在你的编译器地最高(最苛求)警告级别下争取“无任何警告”的荣誉。
② 不要过度依赖编译器的报警能力,因为不同编译器对待事情的态度并不相同。一旦移植到另一个编译器上,你原本依赖的警告信息可能消失。
总结:编译时的警告信息通常被我们忽略,而有些莫名其妙的错误通常在编译时就已经以警告的方式呈现出来了,所以尽可能正确无警告,据说华为的代码要求编译无警告才能上线,不知真假,但这也不失为一种保证运行时稳定的策略。
54. 让自己熟悉包括TR1在内的标准库程序
① C++标准程序库主要机能由STL, iostream、locales组成,并包含C99标准库程序;
② TR1添加了智能指针、泛化函数指针(function<>)、hash-based容器、正则表达式以及另外10个组件的支持;
③ TR1自身只是一份规范。为获得TR1提供的好处,你需要一份实物,一个好的实物来源是Boost。
55. 让自己熟悉Boost
Boost提供了许多TR1组件实现品,以及其他许多程序库。Boost可看作是一个对STL的扩展,也是很多新特性的试验场,诸如function、bind、智能指针这些特性首先出现在了这里,经过很多用户测试证明其稳定性后,也在后续的C++版本中并入了STL.
小结
《Effective C++》真的是一本C++程序员必读的书籍,虽然我现在通读了一遍,但对其中的有些准则还是理解不够透彻,主要原因还是实践太少,后续编码的时候要夺取刻意的思考一下,如何才能写出易维护且高效正确的C++代码。这本书的前6章节堆起来比较透彻容易理解,但从第7章开始谈起模板元编程和内存时,确实是有点晦涩难懂了,原因还是平时写代码模板接触较少、内存也是调接口直接申请,这两部分后续有时间还需单独深入学习。在我写这篇总结的时候,其中有些条款的说法可能已经过时了,比如对某些TR1中的函数的调用,其实早已融入到STL中了,但其中的思考方式、设计原因等才是能让我们触类旁通、拓展到解决其他类似问题的核心知识。侯捷老师的书和课总是给人一种“授人以鱼不如授人以渔”的感觉,受益匪浅。