32:确定你的public继承构造出is-a关系
- public继承的意思是,子类是一种特殊的父类(is-a关系)
- 子类必须涵盖父类每一个特点,必须无条件继承父类所有特性和接口
- 否则没有is-a关系,不应该使用public继承
- 因为很多时候凭生活经验判断,可能会错误判断为具有is-a关系,但是子类可能没有父类的某个特性
- 程序设计没有银弹
33:避免覆盖继承而来的名称
- 背景:父类中有多个重载的虚函数(同名),子类只重写了其中一个,会导致子类中父类的其他重载函数不可见
- 根本原因:如果子类重写了父类的重载函数的一部分,在进行名字查找中,可以在相应的静态类型(子类)中查找到名字,但是类型无法匹配
- 避免方法:
- 对于父类的重载方法,子类要么全部重写,要么一个都不重写
- 使用
using
声明
- 使用
- 使用转交函数(forwarding function)?
- 对于父类的重载方法,子类要么全部重写,要么一个都不重写
34:区分接口继承与实现继承
- public继承可以分为
函数接口继承
和函数实现继承
- 基类中声明纯虚函数,派生类只继承其接口,且派生类需要提供实现
- 从代码层面提醒派生类主动实现其接口,即使纯虚函数在基类中也可以有实现(派生类也需要显式指明需要使用基类中的实现)
- 基类中声明虚函数,派生类继承其接口和缺省实现
- 基类中声明普通函数,派生类继承其接口和实现(好的编程习惯是不对子类方法进行重写)
- 基类中声明纯虚函数,派生类只继承其接口,且派生类需要提供实现
35:考虑virtual函数以外的其他选择
- 通常面向多态的做法:
- 将接口设置为virtual的
- 通过
Non-Virtual Interface(NVI)
来实现template method模式- 将接口
Func
的真正实现函数onFunc
设置为private virtual的- 基类中的private virtual方法,通过public继承到派生类,派生类可以进行重写
- 将接口
Func
设置为public non-virtual的,在Func
中调用onFunc
- non-virtual的接口
Func
就称为virtualonFunc
的wrapper
- non-virtual的接口
- 优点:在接口
Func
中调用onFunc
前后,可以前置和后置的工作 - 缺点:在某些场景的继承体系中,virtual函数必须调用基类的版本,因此virtual函数必须是protected甚至public的,此时无法使用NVI
- 将接口
- strategy模式
- 基于
Function Pointers
的strategy模式- 直接在构造函数中传入一个函数指针,用于实现多态
- 进一步的,可以基于C++11的
std::function
来实现strategy模式,在构造函数中传入一个可调用对象 - 古典的strategy模式:将函数指针替换为类指针,使用该类中的成员函数
- 优点:同一种类型可以使用不同的方法进行计算,而且可以在运行期变更使用的函数
- 缺点:函数指针只能访问public成员,否则只能弱化封装性,将外部函数声明为友元
- 基于
36:绝不重写继承而来的non-virtual函数
- 从语法上看
- 虚函数执行的是动态绑定,非虚函数执行的是静态绑定
- 如果有多态调用的需求,设置为虚函数
- 从设计上看
- public继承意味着一种is-a关系,子类是一种特殊的父类,不变性(父类的共性)凌驾于特异性(子类的个性)之上
- 重写public继承而来的non-virtual表示子类修改了父类的特性,违背了is-a关系,造成了设计上的矛盾
37:绝不重写继承而来的(虚函数的)缺省参数值
- 虚函数执行的是动态绑定,但是缺省参数值是静态绑定
- 因此可能执行的是动态类型版本的虚函数,但是缺省参数值是静态类型版本虚函数的,没有使用动态类型版本的缺省参数值,极易引起误会
- 缺省参数值采用静态绑定是为了提高运行时效率,这样可以在编译期将参数确定,而非得到运行时
- 解决方法:
- 如果使用虚函数,则采用相同的缺省参数值
- 使用
Non-Virtual Interface(NVI)
代替虚函数- 将接口
Func
设置为public non-virtual的(因此不期望被重写),并带有缺省参数,因此不管怎么继承,缺省参数值都是相同的 - 将接口
Func
的实现逻辑onFunc
设置为private virtual的,Func
中将缺省参数传递给onFunc
,调用动态版本的虚函数
- 将接口
38:通过复合构造出has-a关系或“根据某物实现出”
- 复合:一个类作为另一个类的数据成员
- 当复合发生在应用域内的对象之间时,表现出has-a的关系
- 比如Person类中有一个Address类
- 当复合发生在实现域内的对象之间时,表现出“根据某物实现出”的关系
- 比如使用List类模拟实现出一个Set类
39: 明智而审慎地使用private继承
- private继承的特点:
- 如果派生类private继承自基类,则从派生类无法转换到基类
- 但是如果派生类public继承自基类,则派生类可以slice(切掉)转换为基类
- private继承的意义:“根据某物实现出”
- 仅仅是为了让派生类使用基类中的某些方法,派生类与基类没有直接意义上的联系
- private继承的使用:当需要进行“根据某物实现出”的时候
- 能用复合,就不要用private:绝大多数private继承的场合都可以使用“public继承+复合”进行代替
- 使用private继承:
- 比如想在Widget的派生类中,不定义OnTick方法,即使使用private继承,在Widget的派生类中仍然可以重新定义OnTick方法(类似NVI中方法)
- 同时Widget编译时必须依赖Timer
1 2 3 4 5 6 7 8 9 10
// 使用private继承 class Timer{ public: virtual void OnTick() const; }; class Widget: private Timer{ private: virtual void OnTick() const; // override } // Widget的派生类中仍有OnTick方法
- 使用复合:
- 在Widget的派生类中,可以没有OnTick方法(同C++11对成员函数使用
final
) - 可以将WidgetTimer定义移出Widget,从而Widget编译时不需要Timer
1 2 3 4 5 6 7 8 9 10 11 12
class Timer{ public: virtual void OnTick() const; }; class Widget{ private: class WidgetTimer: public Timer{ public: virtual void OnTick() const; }; WidgetTimer timer; };
- 在Widget的派生类中,可以没有OnTick方法(同C++11对成员函数使用
- 使用private继承:
- 使用private继承的情况:空白基类最优化(Empty Base Optimization,EBO)
- 能用复合,就不要用private:绝大多数private继承的场合都可以使用“public继承+复合”进行代替
40: 明智而审慎地使用多重继承
- 多重继承中可能遇到歧义调用,需要指明调用哪个基类中的接口
- 即使同名接口一个在基类中是public的,一个是private的(不会被调用),也会发生歧义
- 因为C++首先会找到最佳匹配函数,之后才会验证其可用性,如果两个同名的函数匹配程度相同,则发生二义性
- 遇到菱形继承时,使用虚继承,且尽量少的在虚基类中携带数据
- 多重继承的使用场景:public继承自某个抽象基类,private继承自某个协助实现的基类