条款 32:确定你的 public 继承塑模出 is - a 关系。
以C++进行面向对象编程,最重要的一个规则是:public inheritance(公开继承)意味“is-a"(是一种)的关系。
如果你令class D("Derived")以public形式继承class B("Base"),你便告诉C++编译器说每一个类型为D的对象同时也是一个类型为B的对象,反之不成立。B比D表现出更一般性的概念,而D比B表现出更特殊化的概念。
需要注意的是:
public继承主张,能够施行于base class对象身上的每件事情,也可以施行于derived class对象身上。
条款 33:避免遮掩继承而来的名称。
基础作用域名称掩盖例子:
int x; // global 变量
void SomeFunc()
{
double x; // local 变量
std::cin >> x;
}
这里的local变量x会覆盖global变量,而不关注其是否为不同类型。
在继承体系中,也有类似的名称遮掩和查找顺序。
考虑如下继承例子:
class Base {
private:
int x;
public:
virtual void mf1() = 0;
virtual void mf1(int);
virtual void mf2();
void mf3();
void mf3(double);
...
};
class Derived: public Base {
public:
virtual void mf1();
void mf3();
void mf4();
...
};
// 使用示例说明
Derived d;
int x;
...
d.mf1(); // 没问题调用Derived::mf1
d.mf1(x); // 错误! 因为Derived::mf1遮掩了Base::mf1
d.mf2(); // 没问题,调用Base::mf2
d.mf3(); //没问题,调用Derived::mf3
d.mf3(x); // 错误! 因为Derived::mf3遮掩了Base::mf3
如果你正在使用public继承而又不继承哪些重载函数,就是违反base和derived classes之间的is-a关系,is-a是public继承的基石。
比较丑陋的解决方案是在Derived Class中使用public类型的using
class Base {
private:
int x;
public:
virtual void mf1() = 0;
virtual void mf1(int);
virtual void mf2();
void mf3();
void mf3(double);
...
};
class Derived: public Base {
public:
using Base::mf1;
using Base::mf3;
virtual void mf1();
void mf3();
void mf4();
...
};
// 使用示例说明
Derived d;
int x;
...
d.mf1(); // 没问题调用Derived::mf1
d.mf1(x); // 现在没问题了,调用Base::mf1
d.mf2(); // 没问题,调用Base::mf2
d.mf3(); //没问题,调用Derived::mf3
d.mf3(x); // 现在没问题了,调用Base::mf3
而当类之间的继承关系为private继承时,情况略有不同
class Base {
private:
int x;
public:
virtual void mf1() = 0;
virtual void mf1(int);
virtual void mf2();
void mf3();
void mf3(double);
...
};
class Derived: private Base {
public:
virtual void mf1() {
Base::mf1(); // 转交函数 暗自成为inline
}
...
};
// 使用示例说明
Derived d;
int x;
...
d.mf1(); // 没问题调用Derived::mf1
d.mf1(x); // 错误! Base::mf1()被遮掩了
条款 34:区分接口继承和实现继承。
几何形状绘图的例子:
class Shape {
public:
virtual void draw() const = 0;
virtual void error(const std::string& msg);
int objectID() const;
...
};
class Rectangle: public Shape {...};
class Ellipse: public Shape {...};
pure virtual函数draw,将Shape成为一个抽象class,客户无法创建Shape的实体,而只能创建derived class的实体。
因为是纯虚函数,所以必须被具象的class重新声明,而且draw在基类中不会有定义。
- 也就是说声明一个纯虚函数就是为了让derived class只继承函数接口。
以下是使用示例:
Shape* ps = new Shape; // 错误! Shape是抽象的
Shape* ps1 = new Rectangle; // 没问题
ps1->draw(); // 调用Rectangle::draw
Shape* ps2 = new Ellipse; // 没问题
ps2->draw(); // 调用Ellipse::draw
ps1->Shap::draw(); // 调用Shap::draw
ps2->Shap::draw(); // 调用Shape::draw
- 声明impure virtual(非纯虚)函数的目的,是让derived class继承该函数的接口和缺省实现。
书中给出了一个有意思的例子,不同型号的飞机飞行的故事,基础的类继承关系如下:
class Airport {...}; // 用以表现机场
class Aireplane {
public:
virtual void fly(const Airport& destination);
...
};
void Aireplane::fly(const Airport& destination)
{
缺省代码, 将飞机飞至指定的目的地
}
class ModelA: public Airplane { ... };
class ModelB: public Airplane { ... };
初始时候ModelA和ModelB都自行实现了fly覆盖基类的fly函数,这样可以正确的执行飞行任务。当公司盈利,引入了ModelC型飞机时,此时恰巧粗心的工程师没有覆盖基类的fly函数,那么:
Airport PDX(...);
Aireplane* pa = new ModelC;
...
pa->fly(PDX); // 呕吼 这将飞向哪里?
这也就是书中说的,如果子类没有明确说出我要,那么不要提供,否则就是错误的引子。下面是推荐的一种做法:
class Airplane {
public:
virtual void fly(const Airport& destination) = 0;
...
protected:
void defaultFly(const Airport& destination);
}
void Airplane::defaultFly(const Airport& destination)
{
缺省行为,将飞机飞至指定的目的地。
}
这样,子类在需要时候可自行调用defaultFly函数,例子 如下:
class ModelA:: public Airplane {
public:
virtual void fly(const Airport& destination) {
defaultFly(destination);
}
...
}
这样,新增的ModelC型飞机就天然知道需要实现fly接口了。
需要注意的是Aireplane::defaultFly是一个non-virtual函数,这点也很重要。因为没有任何一个derived class应该重新定义此函数。
上述的修改方式由于多了一个函数defaultFly,仍然让一些人会产生困惑,那么可以借用pure virtual也可以提供一个默认实现的方式提供缺省行为。
class Airplane {
public:
virtual void fly(const Airport& destination) = 0;
...
}
void Airplane::fly(const Airport& destination)
{
缺省行为,将飞机飞至指定的目的地。
}
class ModelA:: public Airplane {
public:
virtual void fly(const Airport& destination) {
Airplane::fly(destiontion);
}
...
}
- 声明non-virtual函数的目的是为了令derived classes继承函数的接口及一份强制性实现。注意这里指代的是强制性的。non-virtual函数的不变形(invariant)凌驾特异性(specialization)。
条款 35:考虑 virtual 函数以外的其他选择。
书中以一个战斗类游戏为例,列举出当实现一个人物生命值的变化函数的设计策略,初始的类定义可能像这样:
class GameCharacter {
public:
virtual int healthValue() const;
};
子类型需要重新实现healthValue,初看很合理,每个任务的生命值计算方式肯定有差异,例如战士、法师、刺客...
这种设计本质上没问题,可以实现诉求,书中描述的思考是有没有更优的方式。至于书中提出的NVI(Non-Virtual Interface)手法,在这个例子中并不是最优方案,这里略过,会在条款37看到其用法。
我们来看第一个优化方式:使用函数指针和策略模式
class GameCharacter; // 前置声明
// 以下函数是计算健康指数的缺省算法
int defaultHealthCalc(const GameCharacter& gc);
class GameCharacter {
public:
typedef int (*HealthCalcFunc)(const GameCharacter&);
explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc) : healthFunc(hcf)
{}
int healthValue() const
{
return healthFunc(*this);
}
...
private:
HealthCalcFunc healthFunc;
};
这么做可以实现:
class EvilBadGuy: public GameCharacter {
public:
explicit EvilBadGuy(HealthCalcFunc hcf = defaultHealthCalc) : GameCharacter(hcf)
{
...
}
...
};
int loseHealthQuickly(const GameCharacter&); // 健康指数计算函数1
int loseHealthSlowly(const GameCharacter&); // 健康指数计算函数2
EvilBadGuy ebg1(loseHealthQuickly); // 相同类型的任务搭配
EvilBadGuy ebg2(loseHealthSlowly); // 不同的健康计算方式
第二种优化方式是结合模板方法:
class GameCharacter; // 前置声明
// 以下函数是计算健康指数的缺省算法
int defaultHealthCalc(const GameCharacter& gc);
class GameCharacter {
public:
typedef std::function<int (const GameCharacter&)> HealthCalcFunc;
explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc) : healthFunc(hcf)
{}
int healthValue() const
{
return healthFunc(*this);
}
...
private:
HealthCalcFunc healthFunc;
};
初看的时候会觉得,这两个有差异吗?
但是这样做会大大提升健康指数的调用灵活性,这里给出例子:
// 函数类型 健康计算函数 其返回值是非int
short calcHealth(const GameCharacter&);
// 函数对象
struct HealthCalculator {
int operator()(const GameCharacter&) const {...}
};
// 其他类型的类,但是提供相对应的接收const GameCharacter&并返回数值的类 注意后续的std::bind与之匹配的使用方式
class GameLevel {
public:
float health(const GameCharacter&) const; // 注意这里返回的是non-int类型
...
};
// 这与第一种方式并无差异
class EvilBadGuy: public GameCharacter {
public:
explicit EvilBadGuy(HealthCalcFunc hcf = defaultHealthCalc) : GameCharacter(hcf)
{
...
}
...
};
// 另一个任务类型 假设其构造函数与EvilBadGuy通
class EyeCandyCaracter: public GameCharacter {
...
};
// 下面是使用
// 人物1 使用某个函数计算健康指数
EviBadGuy ebg1(calcHealth);
// 人物2 使用某个函数对象计算健康指数
EyeCandyCaracter ecc1(HealthCalculator());
GameLevel currentLevel;
...
// 人物3 使用某个成员函数计算健康指数
EvilBadGuy ebg2(std::bind(&GameLevel::health, currentLevel, _1));
第三种方式,就是使用古典Strategy模式,这个方式在了解设计模式的情况下很容易想到,这里不再列出。
条款 36:绝不重新定义继承而来的 non - virtual 函数。
本质还是在于在C++中,基类的non-virtual函数采用的是静态绑定而非动态绑定方式,如果重新定义了基类的non-virtual函数,那么实际上就是对基类同名函数的覆盖操作,这样在多态的形式调用时会表现出"精神分裂"或者说不符合直觉的调用结果。例如:
class B {
public:
void mf();
};
class D {
public:
void mf();
};
D x;
D *pD = &x;
B *pB = &x;
pD->mf(); // 调用D::mf
pB->mf(); // 调用B::mf 而非D::mf! 呕吼 你确认是你希望的结果吗?
这与virtual类型定义函数重写有区别。
条款 37:绝不重新定义继承而来的缺省参数值。
此条款限定为重新定义带缺省参数的virtual函数。原因在于virtual函数系动态绑定,而缺省参数值却是静态绑定。
静态类型就是在程序中被声明时所采用的类型,示例:
class Shape {
public:
enum ShapColor {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:
/*
请注意,当客户以对象调用此函数,一定要指定参数值。因为静态绑定下这个函数并不从其base继承缺省参数值。
但若以指针(或reference)调用此函数,可以不指定参数值。
因为动态绑定下这个函数会从其base继承缺省参数值。
*/
virtual void draw(ShapeColor color) const;
};
// 使用示例
Shape* ps; // 静态类型为Shape*
Shape* pc = new Circle; // 静态类型为Shape*
Shape* pr = new Rectangle; // 静态类型为Shape*
*在本例中,ps,pc和pr都被声明为pointer-to-Shape类型,所以它们都以它为静态类型。注意,不论它们真正指向什么,它们静态类型都是Shape。**
而动态类型则是所指对象的类型,是对象将会有什么行为。动态类型可在程序执行过程中改变。
由于virtual函数是动态绑定而来,调用哪一个函数实现代码,取决于发出调用的那个对象的动态类型。
例如:
pc->draw(Shape::Red); // 调用Circle::draw(Shape::Red);
pr->draw(Shape::Red); // 调用Rectangle::draw(Shape::Red);
但是,正如此条款所言,假设现在有如下的调用方式:
pr->draw(); // 调用Rectangle::draw(Shape::Red); 哦吼 怎么不是预期的Rectangle::draw(Shape::Green)呢?
C++这么实现的原因在于运行效率的权衡。
那么假设我不改变,遵循此条款编写,又会出现什么问题?
class Shape {
public:
enum ShapColor {Red, Green, Blue};
virtual void draw(ShapeColor color = Red) const = 0;
};
class Rectangle: public Shape {
public:
// 注意,这里改成了与base相同的Red类型
virtual void draw(ShapeColor color = Red) const;
};
那么又会有另外的问题,为什么要在基类和子类里写相同的代码,如果基类改了默认值的行为呢?这就带来了相依性(with dependencies)。
使用NVI优化方式:
class Shape {
public:
enum ShapColor {Red, Green, Blue};
void draw(ShapeColor color = Red) const { doDraw(color); }
private:
virtual void doDraw(ShapeColor color) const = 0;
};
class Rectangle: public Shape {
public:
// 注意,不须指定缺省参数值
virtual void doDraw(ShapeColor color) const;
};
条款 38:通过复合塑模出 has - a 或 “根据某物实现出”。
当实现类时,应当考虑是使用has-a的方式实现还是用has-a的方式实现。
条款 39:明智而审慎地使用 private 继承。
private继承可以使用has-a的方式实现。对于EBO(empty base optimization)的慎用场景极少,可不做考虑。
条款 40:明智而审慎地使用多重继承。
多重继承会带来歧义问题,示例:
class BorrowableItem {
public:
void checkOut();
...
};
class ElectronicGadget {
private:
bool checkOut() const;
...
};
class MP3Player:
public BorrowableItem,
public ElectronicGadget
{
...
};
// 调用示例
MP3Player mp3;
mp3.checkOut(); // 调用的是哪一个checkOut?
// 可以尝试使用
mp3.ElectronicGadget::checkOut();
钻石型继承问题:
class File {...};
class InputFile: public File {...};
class OutputFile: public File {...};
class IOFile: Public InputFile, public OutputFile {...};
钻石型继承带来的问题是:是否打算让base class内的成员变量经由每一条路径被复制?例如这里的File假设有一个fileName成员,那么IOFile内有几个这个成员呢?(实际会有两份,但是显然不是预期的场景,没有一个IOFile有两个fileName)。
解决的方式是使用virtual public方式继承:
class File {...};
class InputFile: virtual public File {...};
class OutputFile: virtual public File {...};
class IOFile: Public InputFile, public OutputFile {...};
这里补充一个简单的例子作为验证:
#include <iostream>
using namespace std;
class A {
public:
int x;
A(int x=0) : x(x) { cout << "A构造: x=" << x << endl; }
};
// B虚继承A
class B : virtual public A { // 关键:添加virtual
public:
B(int x=0) : A(x) { cout << "B构造" << endl; }
};
// C虚继承A
class C : virtual public A { // 关键:添加virtual
public:
C(int x=0) : A(x) { cout << "C构造" << endl; }
};
// D继承B和C
class D : public B, public C {
public:
// 关键:虚基类A的构造由最终派生类D直接初始化
D(int x=0) : A(x), B(100), C(200) { cout << "D构造" << endl; }
};
int main() {
D d(30);
cout << "d.x=" << d.x << endl; // 直接访问,无歧义
return 0;
}
执行结果:
A构造: x=30
B构造
C构造
D构造
d.x=30
对于虚继承来说,虚基类的构造函数由最终派生类负责调用。
同时需要注意,如果是使用virtual继承,为避免继承带来的成员变量重复,编译器必须提供若干幕后戏法,而其后果是:使用virtual继承的哪些classes所产生的对象往往比使用non-virtual继承的兄弟们体积大,访问virtual base classes的成员变量时,也比访问non-virtual base classes的成员变量速度慢。
书中给出了多重继承和private实现的结合场景,通过private继承将现有类作为辅助类协助实现的场景。