目录
- 条款32. 确定你的public继承塑模出is-a关系
- 条款33.避免遮掩继承而来的名称
- 条款34.区分接口继承和实现继承
- 条款35.考虑virtual函数以外的其他选择
- 条款36.绝不重新定义继承而来的non-virtual函数
- 条款37.绝不重新定义继承而来的缺省参数值
- 条款38.通过复合塑模出has-a或“根据某物实现出”
- 条款39.明智而审慎地使用private继承
- 条款40.明智而审慎地使用多重继承
条款32. 确定你的public继承塑模出is-a关系
is-a:表示一种关系,即public继承的子类对象应该也可以当作一种父类对象,适用于base classes身上的每一件事情一定也适用于derived classes身上,因为每一个derived class对象也都是一个base class对象。
条款33.避免遮掩继承而来的名称
问题:子类重写父类成员函数时,会覆盖原本应该继承的父类所有同名称的成员函数,破坏了public继承的“is-a”原则。
解决方法:可在子类定义中加入“using Base::functionname;”,使得父类Base中的名为“fuctionname”的所有函数(包括所有重载函数)在子类中曝光。
条款34.区分接口继承和实现继承
- 接口继承和实现继承不同。在public继承之下, derived classes总是继承base class的接口。
- pure virtual函数只具体指定接口继承。
- impure virtual函数具体指定接口继承及缺省实现继承:在继承接口的同时,依然可以使用父类的接口实现,除非子类自己重写虚函数,但是要警惕当前子类是否能够安全使用父类默认接口实现。
- non-virtual函数具体指定接口继承以及强制性实现继承:non-virtual函数的继承意味着可能需要在子类中重写该接口函数,因此需要自己对接口重新实现。
条款35.考虑virtual函数以外的其他选择
- 代替方法1:NVI(non-virtual interface)手法,令客户通过public non-virtual成员函数间接调用private virtual函数。其中,non-virtual成员函数被成为 virtual函数函数的外覆器(wrapper)。外覆器可以确保在一个virtual函数被调用之前设定好适当的场景,并在调用结束之后进行清理工作。
在NVI手法下 virtual函数也可以是protected。某些class继承体系要求derived class在 virtual函数的实现内必须调用其base class 的对应兄弟,而为了让这样的调用合法,virtual函数必须是protected,不能是private。
有时候virtual 函数甚至一定得是public(例如具备多态性质的base classes的析构函数—见条款7),这么一来就不能实施NVI手法了。 - 代替方法2:将virtual函数替换为”函数指针成员变量。在类中声明一个函数指针成员变量,用于接收不同行为的非成员函数地址,并在构造函数中进行初始化。为了解决非成员函数无法访问成员变量的问题,可以建立friend关系或者调用public成员函数获取信息。
- 代替方法3:用std::function代替上述函数指针,将会得到一个更加”泛化的指针“,因为它可以自动进行隐式转换,而函数指针固定类型后就无法改变。
条款36.绝不重新定义继承而来的non-virtual函数
原因:当一个指向子类对象的指针调用被重写的non-virtual函数时,调用父类还是子类的non-virtual函数并不取决于对象是子类对象还是父类对象,而是取决于最初指针声明的类型。此外,我们也应当遵守适用于基类对象的每一件事,也适用于继承类对象;基类的派生类一定会继承non-virtual的接口和实现。
条款37.绝不重新定义继承而来的缺省参数值
原因:缺省参数值都是静态绑定,而virtual函数是动态绑定。
静态类型:被声明时采用的初始类型;
动态类型:目前所指对象的类型。
虚函数调用哪一个取决于动态类型是哪一个对象。
而缺省参数值取决于静态类型。
如果为了实现多态用基类Shape指针指向派生类对象,那么用基类指针调用虚函数draw时,默认参数还是取决于基类的默认参数,而非派生类对象的默认参数。
由于虚函数大概率会在派生类中被重写,但是其默认参数必须与基类虚函数默认参数保持一致。后续如果需要修改默认参数,那么基类以及所有派生类的该默认参数均要被手动一一修改。可采用前面阐述的NVI方法来实现统一虚函数的默认参数:
条款38.通过复合塑模出has-a或“根据某物实现出”
-
在应用领域,复合表示has-a(有一个):用一个复合类对象由多个子对象组合而成。
-
在实现领域,符合表示is-implemented-in-terms-of(根据某物实现出):用list实现set。
条款39.明智而审慎地使用private继承
- 如果classes之间的继承关系是private,编译器不会自动将一个derived class对象转换为个base class对象;由private base class继承而来的所有成员,在 derived class中都会变成private属性,纵使它们在base class 中原本是protected 或public属性。
- Private继承意味is-implemented-in-terms of(根据某物实现出)。它通常比复合( composition)的级别低。
- 当两个类不存在”is-a“关系时,并且derived class需要访问protected base class 的成员,或需要重新定义继承而来的 virtual函数时,那么就可以使用private继承。
- 和复合( composition)不同,private继承可以造成empty base最优化。这对致力于“对象尺寸最小化”的程序库开发者而言,可能很重要。如果用复合则会造成对象嵌套,浪费空间;而私有继承在保证可以使用基类的成员前提下,尽可能减少占有的空间。(实例化一个没有非静态成员变量、没有虚函数的”空类“(empty class)也会被分配1个字节的空间,而继承空类将不会浪费空间)(目前感觉这一点还未体现得很明显)
- 复合和private继承都意味 is-implemented-in-terms-of,但复合比较容易理解,所以无论什么时候,只要可以,你还是应该选择复合。
条款40.明智而审慎地使用多重继承
- 多重继承比单一继承复杂。它可能导致新的歧义性(不同基类有同名成员),以及对virtual继承的需要。
- virtual继承会增加大小(虚指针)、速度(指针索引)、初始化(及赋值)复杂度等等成本。如果virtualbase classes不带任何数据,将是最具实用价值的情况。
- 多重继承的确有正当用途。其中一个情节涉及“public继承某个Interface class”和“private继承某个协助实现的class”的两相组合。