条款 32:确定你的 public 继承塑模出 is-a 关系
- public inheritance(公开继承)意味 is-a 的关系。【base 类的方法接口在 derived 类中都会有所体现】
- 如果你令 class
D
(Derived
)以 public 形式继承 classB
(Base
),你便是在告诉 C++ 编译器(亦或是看你代码的人):每一个类型为D
的对象同时也是一个类型为B
的对象,反之则不成立。 - 在 C++ 领域中,任何函数如果期望获得一个类型为
B
(或 pointer-to-B 或 reference-to-B)的实参,都也愿意接受一个D
对象(或 pointer-to-D 或 reference-to-D)。
- 如果你令 class
- 如果 derived 类无法完全继承 base 类的所有方法,可以采取两种方案:
- 第一种方案是在将调用错误限制在编译期,也就是直接不声明这个接口。【但其实就违背了本条款】
- 第二种方案则是在运行期内提示错误,也就是在这个实现中报错即可。
- is-a 并非是唯一存在于 class 之间的关系,另两个常见的关系是 has-a(有一个)和 is-implemented-in-terms-of(根据实物实现出)。
条款 33:避免遮掩继承而来的名称
- 本条款重点描述的是名字与作用域的关系,和继承权限(public、private)、虚机制均无关系。
- 编译器对变量名称的匹配是由内向外的,一旦找出就停止搜索,然后再检查变量类型是否匹配正确。
- derived classes 内的名称会遮掩 base classes 内的名称。
- 基本理由是:防止程序员在程序库或应用框架内建立新的 derived class 时,附带地从疏远的 base class 中继承了重载函数。
- 为了让 base class 中被遮掩的名称起作用,可以采用 using class 或转交函数。
- using class 方案:
- 在 public 继承中采取的方案。
- 意思就是说使用 using 语句显式表明从 base class 中继承过来的函数名称。
- 这样的方案会涵盖 base 类中一个名称可取得的所有重载版本。
- 转交函数方案:
- 有时候,并不想继承 base class 的所有函数。
- 这时候就通过转交函数来获取特定的一个函数:
class Base { public: virtual void mf1() = 0; virtual void mf1(int); ... }; class Derived : private Base { public: virtual void mf1() // 转交函数,暗自成为inline(原因,见条款30) { Base::mf1(); } ... }; ... Derived d; int x; d.mf1(); // 很好,调用的是Derived::mf1 d.mf1(x); // 错误! Base::mf1被遮掩了
条款 34:区分接口继承和实现继承
- 接口继承和实现继承的区别就类似函数声明和函数定义之间的差异。
- pure-virtual 只指定接口继承。
- impure-virtual 指定接口继承和缺省的实现继承。
- non-virtual 指定接口继承和强制性的实现继承。
条款 35:考虑 virtual 函数以外的其他选项
- 如果有设计模式的基础会更容易理解该条款的内容。
- 该条款主要通过一个继承体系来体现:
class GameCharacter { public: virtual int healthValue() const; ... };
- 这个函数不是纯虚函数,所以这表明会有个缺省的实现(这就容易成为该类的弱点),所以最好找一个替换虚函数的方案,本节内容就是讨论能够替换 virtual 函数的方案。
- 方案一,借由 Non-Virtual Interface(NVI)手法实现 Template Method 模式:【调用留给 base 类,而真正的实现变成虚函数,允许 derived 类去重写】
-
这个方法,主张 virtual 函数应该几乎总是 private。
-
这个方法就是保留
healthValue
函数为 public,但让它成为 non-virtual,并间接调用一个 private virtual 函数:class GameCharacter { public: int healthValue() const { // 派生类 不重新定义它 ... // 做一些 其他工作 int retVal = doHealthValue(); ... return retVal; } ... private: virtual int doHealthValue() const { // 派生类可以重新定义它 ... // 缺省算法,可以重新定义 } };
-
优点在于:在它做真正的事情前后都可以加入一些其他东西,就像上面定义体现的那样;NVI 方法允许派生类重新定义 virtual 函数,决定如何去实现,但基类有着何时被调用的权利。
-
缺点在于:这个方法让 virtual 函数是 private 的;有时候 derived 类的重写函数需要调用 base 类的对应虚函数,这时候就没有办法把虚函数设置为 private 的,也就是说这时候就没办法实施NVI方法。【降低了封装性】
-
- 方案二,通过 Function Pointers 实现 Strategy 模式:【弱化 class 的封装】
- 这个方法主张,健康指数的计算与人物类型无关,因此就不需要这个成分,完全可以让每个人物(每个 derived 类)通过一个函数来自己计算:
class GameCharacter; int defaultHealthCalc(const GameCharacter& gc); class GameCharacter { public: typdef int (*HealthCalcFunc) (const GameCharacter&); explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc) : healthFunc(hcf) {} int healthValue() const { return healthFunc(*this); } ... private: HealthCalcFunc healthFunc; };
- 优点在于:同一人物类型不同实体可以有不同的健康计算函数;某个已知的人物健康指数计算函数可以在运行期变更。
- 缺点在于:人物的健康根据该人物的 public 接口得来的信息加以计算就没问题,但是要用到 non-public 信息计算,就会出问题。
- 这个方法主张,健康指数的计算与人物类型无关,因此就不需要这个成分,完全可以让每个人物(每个 derived 类)通过一个函数来自己计算:
- 方案三,通过
tr1::function
完成 Strategy 模式:- 不用函数指针,而使用
tr1::function
,这个对象可以保存任何可调用户:class GameCharacter; int defaultHealthCalc(const GameCharacter& gc); class GameCharacter { public: typedef std::tr1::function<int (const GameCharacter&)> HealthCalcFunc; explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc) : healthFunc(hcf) {} int healthValue() const { return healthFunc( *this); } ... private: HealthCalcFunc healthFunc; }; // HealthCalcFunc是个typedef,用来表现 tr1::function的某个实体,行为像一个函数指针。 // 它的含义就是 接受一个指向GameCharacter的引用,并返回int。 // 实现如下: short calcHealth(const GameCharacter&); struct HealthCalculator { // 为计算健康设计的成员对象 int operator() (const GameCharacter&) const { ... } }; class GameLevel { public: float health(const GameCharacter&) const; // 计算健康的成员函数 ... }; class EvilBadGuy: public GameCharacter { ... // 同前 }; class EveCandyCharacter: public GameCharacter { ... // 另一个人物类型,假设构造函数同EvilBadGuy }; EvilBadGuy edg1(calcHealth); // 人物1,使用某个函数计算健康指数 EyeCandyCharacter ecc1(HealthCalculator()); // 人物2,使用某个函数对象计算健康指数 GameLevel currentLevel; ... EvilBadGuy ebg2( std::tr1::bind(&GameLevel::health, currentLevel, _1) // 人物3,使用某个成员函数计算健康指数 {};
- 不用函数指针,而使用
- 方案四,古典的 Strategy 模式:
- 设计模式中标准 Strategy 模式,代码如下:
class GameCharacter; class HealthCalcFunc { public: ... virtual int clac(const GameCharacter& gc) const { ... } ... }; HealthCalcFunc defaultHealthCalc; class GameCharacter { public: explicit GameCharacter(HealthCalcFunc* phcf = &defaultHealthCalc) : pHealthCalc(phcf) {} int healthValue() const { return pHealthCalc->calc(*this); } ... private: HealthCalcFunc* pHealthCalc; };
- 具体来说,传统实现就是将继承体系内的 virtual 函数替换为另一个继承体系内的 virtual 函数。
- 设计模式中标准 Strategy 模式,代码如下:
条款 36:绝不重新定义继承而来的 non-virtual 函数
- 在派生类中,如果重新定义基类的函数,将会遮掩基类的相应函数。
- 造成这样现象的原因是,non-virtual 函数都是静态绑定,如果重新定义,打断了 is-a 的关系。
条款 37:绝不重新定义继承而来的缺省参数值
- 有了条款 36,可以知道本条款的讨论是限定在继承一个带有缺省参数值的 virtual 函数的场景下。
- virtual 函数是动态绑定,而缺省参数值是静态绑定。
- 可能会在调用一个定义于 derived class 内的 virtual函数的同时,却使用 base class 为它所指定的缺省参数值。【C++ 这样做主要是节省执行效率】
class Shape { public: enum ShapeColor { Red, Green, Blue }; // 绘制自己 virtual void draw(ShapeColor color = Red) const = 0; ... }; class Rectangle : public Shape { public: // 重定义 缺省参数值 virtual void draw(ShapeColor color = Green) const; ... }; class Circle : public Shape { public: virtual void draw(ShapeColor color) const; // 注释① ... }; Shape *ps; Shape *pc = new Circle;// pc的静态类型是Shape*,动态类型是Circle* Shape *pr = new Rectangle;// pr的静态类型是Shape*,动态类型是Rectangle* // 结果如下: pc->draw(ShapeColor::Red);// 相当于调用Circle::draw(ShapeColor::Red) pr->draw(ShapeColor::Red);// 相当于调用Rectangle::draw(ShapeColor::Red) pr->draw();// 也相当于调用Rectangle::draw(ShapeColor::Red)
- 替换方法是 NVI 手法:
- 让基类的 public non-virtual 函数调用 private virtual 函数,让 non-virtual 函数负责指定缺省参数值,virtual 函数负责实现具体的东西。
条款 38:通过复合塑模出 has-a 或 “根据某物实现出”
- 复合,是类型之间的一种关系,当某种类型的对象内含它种类型对象,便是这种关系。【复合 = 内嵌 = 聚合 = 内含】
- public 继承意味着 is-a 关系。
- 复合意味着 has-a 或 is-implemented-in-terms-of:
- 当发生于应用域内的对象之间,表现出 has-a 关系(一般就是
A
类的 private 中含有B
类);【has-a 的意思是有一个,也就是内含】 - 当发生于实现域内则表现 is-implemented-in-terms-of。【就好比如 queue 是由 deque 来实现的】【is-implemented-in-terms-of 的意思是根据某物实现出】
- 当发生于应用域内的对象之间,表现出 has-a 关系(一般就是
- 区分:
- 复合的意义与 public 继承完全不同。
- is-implemented-in-trems-of 的实现方法不止有复合,还有 private 继承。
条款 39:明智而审慎地使用 private 继承
- private 继承的首要规则:
- 如果 class 之间的继承关系是 private,编译器不会自动将一个 derived class 对象转换为一个 base class 对象。【无法自动动态转换】
- 由 private base class 继承而来的所有成员,在 derived class 都会变成 private 属性,纵使它们在 base class 中原本是 protected 或 public 属性。
- private 继承意味 is-implemented-in-terms of(根据某物实现出),它通常比复合的级别低。【private 继承意味只有实现部分被继承,接口部分应略去;如果
D
以 private 形式继承B
,意思是D
对象根据B
对象实现而得,再没有其他含义了】- private 继承的使用场合是:当一个意欲成为 derived class 者想访问一个意欲成为 base class 者的 protected 成分;为了重新定义一个或多个 virtual 函数。
- 和复合不同,private 继承可以造成 empty base 最优化,根据代码进行分析:
// 第一种情况: // 在这种情况下sizeof(HoldsAnInt) = 8 class Empty {};//没有数据,所以其对象应该不使用任何内存 class HoldsAnInt { private: int x; Empty e;// 对齐,没有优化,sizeof(Empty) = 1 }; // private继承的情况: // 在这种情况下sizeof(HoldsAnInt) = 4 // 这就是所谓的空基类最优化(EBO) class HoldsAnInt :private Empty { private: int x; };
条款 40:明智而审慎地使用多重继承
- 多重继承会比单一继承复杂。
- 多重继承可能导致歧义,即程序可能从多个基类继承了相同的名称(函数、typedef 等);为了解决这种歧义,首先要明确调用哪个基类的函数,然后带上那个基类。
- 菱形继承问题,即所继承的基类在它们体系中又有共同的基类;要想只有一份 most base 类的成分,就只能将中间的类虚拟继承自 most base 类,如代码所示:
class File { ... }; class InputFile: virtual public File { ... }; class OutputFile: virtual public File { ... }; class IOFile: public InputFile, public OutputFile { ... };
- virtual 继承会增加大小、速度、初始化(及赋值)复杂度等成本,如果 virtual base classes 不带任何数据,将是最具实用价值的情况。
- 多重继承的确有正当用途,其中一个情节涉及 public 继承某个 Interface class 和 private 继承某个协助实现的 class 的两相组合。