06.inheritance-and-objected-oriented-design
本文对应原书的第六部分,主要介绍使用
item32 让public 继承塑造出is-a 关系
用
class Person{...}
class Student:public Person{...}
上述代码描述的就是
class Bird
{
public:
virtual void fly();
}
class Penguin:public Bird
{...}
在这个继承体系下,我们得出企鹅会飞,但事实上且不会飞。这就是自然语言的二义性给我们造成的误导,我们说鸟会飞,真正的意思是一般的鸟都有飞行能力,但是有些鸟不会飞。我们需要更准确的继承体系来更准确的反映我们的真正意思:
class Bird
{...}
class FlyingBird:public Bird
{
public:
virtual void fly();
}
class Penguin:public Bird
{...}
除了上述修改类的继承体系外,还有一种思想是:为类
class Penguin:public Bird
{
public:
virtual void fly(){error("Attempt to make a penguin fly!");} // 调用fly函数会提示错误
}
Penguin p;
p.fly(); // 通过编译,运行时会提示错误
class Bird
{...} // 无飞行函数
class Penguin:public Bird
{...} // 无飞行函数
生活的经验给了我们关于对象继承的直觉,然而这样的直觉并不一定是正确的。比如,我们实现一个正方形类继承自矩形类:
class Rectangle
{
public:
virtual void setHeight(int newHeight);
virtual void setWidth(int newWidth);
virtual int height() const;
virtual int width() const;
...
}
void makeBigger(Rectangle& rect)
{
int oldHeight = rect.height();
rect.setWidth(rect.width()+10); // 宽度增加10
assert(rect.height()==oldHeight); // 判断高是否改变,
}
class Square:public Rectangle{...}
Square s;
assert(s.height()==s.width()); // 正方形的高度等于宽度
makeBigger(s);
assert(s.height()==s.width()); // 理论上来说应该仍然成立
根据正方形的定义,宽高相等应该是在任何时候都成立的。然而makeBigger
却破坏了正方形的属性,所以在这种情况下正方形并不是一个矩形,即
Note:总结
public 继承意味着类与类之间是is-a 的关系,适用于基类的每一件事情也适用于其派生类,因为每一个派生类对象也是一个基类对象。
item33 避免继承中发生的名称覆盖
名称屏蔽的发生其实和继承无关,而是和作用域有关。举个例子:
int x; // 全局变量
void someFunc()
{
double x; // 局部变量
std::cin>>x;
}

在类的继承关系中,派生类能够访问基类的成员,这是因为派生类继承了基类中声明的成员,具体的关系是派生类的作用域嵌套在基类的作用域内。
class Base
{
private:
int x;
public:
virtual void mf1()=0;
virtual void mf2();
void mf3();
...
}
class Derived:public Base
{
public:
virtual void mf1();
void mf4(){ mf2();}
}

接下来我们在派生类中重载函数
class Base
{
private:
int x;
public:
virtual void mf1() = 0;
virtual void mf1(int); // 基类中重载mf1函数
virtual void mf2();
void mf3();
void mf3(double); // 基类中重载mf3函数
...
};
class Derived: public Base
{
public:
virtual void mf1();
void mf3(); // 增加新的mf3函数
void mf4();
...
}

由于名字屏蔽,上述代码中Base::mf1
和Base::mf3
不再被类
Derived d;
int x;
d.mf1(); // 正确,调用了Derived::mf1
d.mf1(x); // 错误,Base::mf1(int)被屏蔽
d.mf2(); // 正确,调用了Derived::mf2
d.mf3(); // 正确,调用了Derived::mf3
d.mf3(x); // 错误,Base::mf3(double)被屏蔽
如果想要避免
class Base
{
public:
virtual void mf1() = 0;
virtual void mf1(int);
virtual void mf2();
void mf3();
void mf3(double);
private:
int x;
}
class Derived:public Base
{
public:
// 让类Base中名为mf1和mf3的所有函数在Derived中可见
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

在
class Base
{
public:
virtual void mf1() = 0;
virtual void mf1(int);
...
}
class Derived:public Base
{
public:
virtual void mf1() // 转发函数
{
Base::mf1();
}
...
}
Derived d;
int x;
d.mf1(); // 正确,调用Derived::mf1
d.mf1(x); // 错误,Base::mf1(int)b被屏蔽
转发函数的另一个作用是帮助那些不支持
Note:总结
- 派生类中的函数名称会屏蔽基类中的所有同名函数,与函数的参数和返回值无关。
- 为了在派生类中引入被屏蔽的基类函数,可以使用
using 声明式或转发函数(forwarding function) 。
item34 区分接口继承和实现继承
class Shape
{
public:
virtual void draw() =0;
virtual void error(const std::string& msg);
int objectedID() const;
...
}
class Rectangle:public Shape{...}
class Ellipse:public Shape{...}
draw
,用来绘制形状;第二个是error
,准备让需要报出错误的函数调用;第三个是objectID
,用来返回当前对象的标识码。每个函数的声明方式不同:draw
是纯虚函数,error
是虚函数,objectID
是普通函数。
- 纯虚函数
纯虚函数有两个突出的特性:
1) 必须在具象类中被实现;2) 在抽象类中通常没有被定义。这两个性质表明了:声明一个纯虚函数的目的就是为了让派生类继承函数接口。Shape 是一个抽象类,无法进行实例化,其派生类要进行实例化就必须提供draw
函数的实现。虽然我们也可以在抽象类中为纯虚函数函数提供定义,但是无法通过对象进行访问,只能通过shape::draw()
这样的形式进行访问。
Shape* ps = new Shape(); // 错误,Shape无法实例化
Shape* ps1 = new Rectangle(); // 正确
ps1->draw(); // 调用Rectangle::draw()
Shape* ps2 = new Ellipse();
ps2->draw(); // 调用Ellipse::draw()
ps1->Shape::draw(); // 调用Shape::draw()
ps2->Shape::draw();
- 虚函数 普通的虚函数和纯虚函数有一些不同,普通虚函数不仅提供了继承的函数接口,也提供了默认的函数实现,派生类也可以重写函数的实现。虚函数的性质表明了:声明一个虚函数的目的是让派生类同时继承函数的接口和默认实现,并能够重写实现。
class Shape
{
public:
virtual void error(const std::string& msg);
}
该接口表示,每个类都必须提供一个当发生错误时可被调用的函数,但每个类可以自定义错误行为。如果不想自定义,可以使用基类提供的默认实现。但是,允许虚函数同时指定函数声明和函数默认实现的行为,可能会造成危险。我们考虑一个航空公司设计的飞机继承体系:
class Airport{...}
class Airplane
{
public:
virtual void fly(const Airport& destination);
}
void Airplane::fly(const Airport& destination)
{
... // 默认实现
}
class ModelA:public Airplane{...}
class ModelB:public Airplane{...}
上述代码中,fly
的默认实现。现新增fly
函数,因此当派生类fly
函数时会调用默认实现。
class ModelC:public Airplane
{
... // 未声明fly函数
}
Airport PDX;
Airplane* pa = new ModelC;
...
pa->fly(PDX); // 调用Airplane::fly
上述代码的问题不在于
- 为默认实现定义一个独立的函数,具体做法是将函数声明为纯虚函数来提供接口,另外定义一个独立的
defaultFly
函数来提供默认的实现。
class Airplane
{
public:
virtual void fly(const Airport& destination) = 0;
protected:
void defaultFly(const Airport& destination)
{
... // 默认实现
}
}
class ModelA:public Airplane
{
public:
virtual void fly(const Airport& destination)
{
defaultFly(destination);
}
}
class ModelB:public Airplane
{
public:
virtual void fly(const Airport& destination)
{
defaultFly(destination);
}
}
class ModelC:public Airplane
{
public:
virtual void fly(const Airport& destination);
}
上述代码中类
- 为纯虚函数提供默认实现,利用纯虚函数必须在派生类中重新实现,但也可以拥有自己的实现 这一特性来进行实现。
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)
{
Airport::fly(destination);
}
}
class ModelC:public Airplane
{
public:
virtual void fly(const Airport& destination);
}
void ModelC::fly(const Airport& destination)
{
... // 定制实现
}
这与上一个方案一模一样,只不过纯虚函数Airplane::fly
替换了独立函数Airplane::defaultFly
。本质上是将
- 普通函数 如果成员函数是一个非虚函数,这意味着并不打算在派生类中对其进行重写,声明非虚函数的目的是为了让派生类继承函数的接口以及一个强制的实现。
纯虚函数、普通虚函数、非虚函数之间的差异可以让我们准确指定派生类继承的东西,即只继承接口、继承接口和默认实现、或是继承接口和强制的实现。由于不同类型的声明有着不同的意义,当我们声明函数时必须要谨慎选择,避免常犯的两个错误:
- 第一个错误是将所有的函数声明为非虚函数。这使得派生类没有空间进行特殊化,实际上任何一个类只要是被用来作为基类,都会拥有若干的虚函数。
- 第二个错误是将所有成员函数声明为虚函数。在接口类中这样做是正确的
(item37) ,但是某些不应该在派生类中被重新定义的函数应该声明为非虚函数。
Note:总结
- 接口继承和实现继承不同。在
public 继承中,派生类总是继承基类的接口。 - 纯虚函数只具体指定接口继承。
- 普通虚函数指定接口继承以及默认的实现继承。
- 非虚函数指定接口继承以及强制性实现继承。
item35 考虑virtual 函数的替代方法
假如我们在设计一个游戏软件,打算为游戏中的角色设计一个继承体系,提供一个healthValue
成员函数来表示游戏角色的健康状态。最直接的做法就是声明一个
class GameCharacter
{
public:
virtual int healthValue() const; // 返回角色的健康状态值
}
上述代码中提供了一个普通的虚函数,这意味着提供了一个默认的实现。这种设计方式简单明白,所以一般都不会去考虑其他的替代设计。但事实上,更好的设计方法是存在的:
NVI 可以实现模板方法模式,用非虚函数来调用更加封装的虚函数- 用函数指针代替虚函数,实现策略模式
- 用
std::function
实现策略模式,可以支持兼容目标函数签名的可调用对象
- 通过
NVI 实现模板方法模式 所谓的NVI(non-virtual interface) ,就是将所有的虚函数声明为private ,然后通过一个public non-virtual 函数来调用该virtual 函数,我们把这个public 函数称为virtual 函数的包装器(wrapper) 。
class GameCharacter
{
public:
int healthValue() const
{
// do sth before
int retValue = doHealthValue();
// do sth after
return retValue;
}
private:
virtual int doHealthValue() const // 派生类可以重新实现该方法
{...}
}
上述代码表示的就是doHealthValue
函数前后设置一些上下文的工作,比如给互斥量加锁doHealthBalue
在派生类中是无法访问的,但是派生类中却对其进行了重新定义。虽然看着有些矛盾,但是
- 通过函数指针实现策略模式
NVI 方法对于public virtual 函数来说只是一个简单的替代方案,事实上我们可以完全将healthValue
完全独立出来,让它不受角色的影响,只在构造函数时将该函数作为参数传入。
class GameCharacter; // 前置声明
int defaultHealthCalc(const GameCharacter& gc); // 默认的计算方法
class GameCharacter //
{
public:
typedef int (*HealthCalcFunc)(const GameCharacter& gc); // 声明一个函数指针
explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc): // 传入函数指针
healthFunc(hcf){}
int healthValue() const
{
return healthFunc(*this);
}
...
private:
HealthCalcFunc healthFunc;
}
上述代码就是一个常见的策略设计模式的简单应用。相较于
- 同一个角色类的不同对象可以有不同的
healthCalcFunc
,只需要在构造时传入不同的策略就可实现。 - 角色的
healthCalcFunc
可以动态待变,只需要提供一个setHealthCalclator
成员方法即可。
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);
我们使用外部函数实现了策略模式,但因为healthCalcFunc
是外部函数,这就意味着其无法访问类的私有成员。一般来说,解决方法是:弱化
- 通过
std::function 实现策略模式 事实上,通过函数指针来实现策略模式过于死板,我们完全可以使用std::function
对象来替代函数指针。std::function
是一种通用的多态函数包装器,它是一个对象,这就意味它可以保存任意一种类型兼容的可调用实体(callable entity) ,如函数对象,成员函数指针,lambda 表达式等。
class GameCharacter; // 前置声明
int defaultHealthCalc(const GameCharacter& gc); // 默认的计算方法
class GameCharacter //
{
public:
typedef istd::function<int (const GameCharacter&)> HealthCalcFunc; // 使用std::function
explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc):
healthFunc(hcf){}
int healthValue() const
{
return healthFunc(*this);
}
...
private:
HealthCalcFunc healthFunc;
}
和使用函数指针的不同在于,上述代码用std::function
声明了一个目标签名式const GameCharacter
的引用作为参数,并返回一个HealthCalcFunc
可以持有任何与签名式兼容的可调用实体。所谓的兼容,意思是这个可调用实体的参数可以被隐式转换为const GameCharacter
,其返回类型可以被隐式转换为
short calcHealth(const GameCharacter&); // 健康计算方法,返回值为short
struct HealthCalculator
{
int operator()(const GameCharacter&) const
{...}
};
class GameLevel
{
public:
float health(const GameCharacter&) const; // 成员函数,返回值为float
}
class EvilBadGuy:public GameCharacter
{...}
class EyeCandyCharacter:public GameCharacter
{...}
EvilBadGuy ebg1(calcHealth); // 人物1,使用某个函数计算健康值
EyeCandyCharacter ecc1(HealthCalculator()); // 人物2,使用某个函数对象计算健康值
GameLevel currentLevel;
EvilBadGuy ebg2(std::bind(&GameLevel::health,currentLevel,_1)); // 人物3,使用某个成员函数计算健康值
上述代码中,无论时类型兼容的函数、函数对象还是成员函数,都可以用来初始化一个GameCharacter
对象。同时,需要注意的是std::bind
的用法。
- 经典策略模式
忽略上面关于策略模式的实现,我们讨论更一般的实现方式。经典的策略模式会将健康计算函数设计成一个分离的继承体系中

如图所示,GameCharacter
是某个继承体系的根类,体系中的EvilBadGuy
和EyeCandyCharacter
都是派生类,HealthCalcFunc
是另一个继承体系的根类,体系中的SlowHealthLoser
和FastHealthLoser
都是派生类,每个GameCharacter
对象都包含一个指针,用来指向一个来自HealthCalcFunc
继承体系的对象。
class GameCharacter;
class HealthCalcFunc
{
public:
virtual int calc(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;
}
熟悉策略模式的人一眼就能看出上述代码是策略模式的经典实现,其可以通过继承HealthCalcFunc
很方便的生成新的策略。
Note:总结
virtual 函数的替代方案包括:NVI 方法以及上面提到的三种策略设计模式。- 将功能提取到外部函数的缺点是非成员函数无法访问类的私有成员。
std::function 对象的行为就像一般的函数指针,但是其可以接受任何类型兼容的可调用实体。
item36 不要重写继承来的非虚函数
例如
class B
{
public:
void mf();
...
}
class D:public B
{
public:
void mf(); // 覆盖率B::mf()
...
}
//
D x;
B* pB = &x;
D* pD = &x;
pB->mf(); // 调用的是B::mf
pD->mf(); // 调用的是D::mf
上述代码中,指针pB
和pD
都指向的是派生类的对象,并通过对象来调用成员函数pB
是一个类型为pB
调用的非虚函数永远都是类pB
和pD
指向的都是类型为
事实上,在派生类中重新定义基类的非虚函数在设计上是矛盾的。基类中声明的非虚函数意味着适用于基类对象的特性同样适用于派生类对象。如果重新定义了非虚函数,那么派生类和基类就不再是
Note:总结
- 在任何情况下都不要去重新定义一个继承来的非虚函数。
item37 不要重定义通过继承得到的默认参数值
我们将需要继承的函数分为两类:
对象的静态类型,就是它在程序中被声明时采用的类型。对于以下的
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*
Shape* pc = new Circle; // 静态类型为Shape*
Shape* pr = new Rectangle; // 静态类型为Shape*
在上述代码中,ps
,pc
和pr
都声明为Shape*
。
对象的动态类型就是当前指向的对象的类型,也就是说动态类型决定了一个对象会有什么行为。在上述代码中,pc
的动态类型是Circle*
,pr
的动态类型是Rectangle*
,pc
没有动态类型,因为其没有指向任何对象。
pc->draw(Shape::Red); // 调用Circle::draw(Shape::Red)
pr->draw(Shape::Red); // 调用Rectangle::draw(Shape::Red)
pr->draw(); // 调用Rectangle::draw(Shape::Red)
在上述代码中,pr
的动态类型是Rectangle*
,所以调用的是Rectangle::draw()
的默认参数是Green
。但是pr
的静态类型是Shape*
,所以实际调用的默认参数值来自于基类
解决矛盾的方法就是考虑
class Shape
{
public:
enum ShapeColor{Red,Green,Blue};
void draw(ShapeColor color=Red) const
{
doDraw(color);
}
private:
virtual void doDraw(ShapeColor color) const =0;
}
class Rectangle:public Shape
{
public:
...
private:
virtual void doDraw(ShapeColor color) const;
}
上述代码中,由于非虚函不应该在派生类中被重新定义,所以这个设计可以很清楚地使得
Note:总结
- 不要在派生类中重新定义继承得到的默认参数值,因为默认参数值是静态绑定的,而
virtual 函数是动态绑定的。
item38 通过组合塑造has-a 或use-a 关系
组合是类型之间的一种关系,当某种类型的对象内包含其他类型的对象,就是组合关系。如下面的代码中,
class Address{...}
class PhoneNumber{...}
class Person
{
public:
...
private:
std::string name;
Address address;
PhoneNumber faxNumber;
}
上面的
template<class T>
class Set
{
public:
bool member(const T& item) const;
bool insert(const T& item);
voie remove(const T& item);
std::size_t size() const;
private:
std::list<T> rep; // 用来描述set的数据
}
类
template<typename T>
bool Set<T>::member(const T& item) const
{
return std::find(rep.begin(),rep.end())!=rep.end();
}
template<typename T>
void Set<T>::insert(const T& item)
{
if (!member(item))
rep.push_back(item);
}
template<typename T>
void Set<T>::remove(const T& item)
{
typename std::list<T>::iterator it = // see Item 42 for info on
std::find(rep.begin(), rep.end(), item); // "typename" here
if (it != rep.end())
rep.erase(it);
}
template<typename T>
std::size_t Set<T>::size() const
{
return rep.size();
}
综上,
Note:总结
- 组合和
public 继承的意义完全不同。 - 在应用域,组合意味着
has-a 的关系,即一个类型中包含了另一个类型的对象作为属性。 - 在实现域,组合意味着
use-a 的关系,即一个类使用了另一个类的数据和方法来实现自生的功能。
item39 慎用private 继承
class Person{...}
class Student:private Person{...}; // 私有继承
void eat(const Person& p); //
Person p; // p is a Person
Student s; // s is a Student
eat(p); // 正确
eat(s); // 错误
上述代码中,Person
对象可以正确调用,但是Student
对象不能正确调用。这就是
- 在
private 继承中,编译器不会自动将一个派生类对象转换为一个基类对象; - 由
private 从基类中继承来的所有成员,包括public 和protected ,都会变成private 属性。
在
我们设计一个
class Timer
{
public:
explicit Timer(int tickFrequency);
virtual void onTick() const;
}
class Widget::private Timer
{
private:
virtual void onTick() const;
}
上述代码中,为了能在onTicker()
在private
,这样既可以在
class Widget
{
private:
class WidgetTimer:public Timer
{
public:
virtual void onTick() const;
...
}
WidgetTimer timer;
}
上述代码的组合方式虽然比
- 我们需要将
Widget 设计为一个基类,但是我们不想在派生类中重新定义onTick
。如果Widget 与Timer 之间的关系是继承,这样的设计是无法实现的(item35 ,派生类中需要重新定义虚函数,即使不会使用) 。但是如果WidgetTimer 是Widget 中的一个private 成员,Widget 的派生类将无法获得WidgetTimer 对象的值,就无法继承或重新定义它的虚函数。 - 最小化
Widget 的编译依赖性。如果Widget 继承了Timer ,那么Widget 被编译时Timer 的定义就必须可见,这就意味着Widget 的定义文件必须要#include Timer.h
。如果将WidgetTimer 移出Widget 之外而通过一个指针指向WidgetTimer 对象,Widget 就可以只需要一个WidgetTimer 的声明式,不再需要#include
任何与Timer 相关的东西。
- 当派生类需要访问基类中的
protected 成员时。因为对象组合后只能访问public 成员,而private 继承可以访问protected 成员。 - 派生类需要重新定义基类中的虚函数时。
我们考虑一个不带任何数据的空白基类,其对象理论上应该是不使用任何空间的,因为对象中没有任何数据需要存储。但是
class Empty{}
class HoldsAnInt
{
private:
int x;
Empty e; // 独立的对象
}
上述代码中,Empty
的对象是一个独立对象,编译器会给其分配一个内存,因此会有sizeof(HoldsAnInt) > sizeof(int)
。
如果我们通过使用
class HoldsAnInt:private Empty
{
private:
int x;
}
在上述代码中,sizeof(HoldsAnInt) == sizeof(int)
是成立的,这就是所谓的
Note:总结
private 继承描述的是use-a 的关系,但是在编程过程中还是优先使用组合的方式。只有在派生类需要访问基类的protected 成员,或者需要重新定义继承而来的virtual 函数时,才使用private 继承。- 和组合不同,
private 继承可以实现空白基类的最优化。
item40 慎用多继承
所谓的多继承,就是一个类有一个以上的基类。这就意味着程序有可能东一个以上的基类中继承相同的名称
class A
{
public:
void chechOut();
}
class B
{
private:
void checkOut();
}
class MP3Player:public A,
private B
{...}
MP3Player mp;
mp.checkOut(); // 会发生歧义,无法确定调用的是哪个函数
checkOut
的调用是有歧义的,即使一个是
mp.A::checkOut(); // 调用的是A::checkOut()
在多继承中,如果基类拥有更高的基类,就有可能导致菱形继承。
class File{}
class InputFile:public File{}
class OutputFile:public File{}
class IOFile:public InputFile, public OutputFile{}

我们假设类
class File{}
class InputFile:virtual public File{}
class OutputFile:virtual public File{}
class IOFile:public InputFile,
public OutputFile
{...}

basic_ios
,basic_istream
,basic_ostream
和basic_iostream
。
为了保证程序的正确性,我们希望
作者对于
上面提到的不包含数据的虚基类,在Person
的接口类定义为:
class IPerson
{
public:
virtual ~IPerson();
virtual std::string name() const = 0;
virtual std::string birthDate() const = 0;
}
用户只能通过指针或引用来使用
// 工厂函数,根据一个数据库ID创建一个Person对象
std::shared_ptr<IPerson> makePerson(DatebaseID personIdentifier);
// 从用户手上获取一个数据库ID
DatabaseID askUserForDatabaseID();
DatabaseID id(askUserForDatabaseID());
std::shared_ptr<IPerson> pp(makePerson(id)); // 创建一个对象支持IPerson接口
多继承的一个重要的应用就是:组合
class IPerson
{
public:
virtual ~IPerson();
virtual std::string name() const = 0;
virtual std::string birthDate() const = 0;
}
class DatabaseID{...}
class PersonInfo // 包含若干函数,实现IPerson接口
{
public:
explicit PersonInfo(DatabaseID pid);
virtual ~PersonInfo();
virtual const char* theName() const;
virtual const char* theBirthDate() const;
virtual const char* valueDelimOpen() const;
virtual const char* valueDelimClose() const;
}
class CPerson:public IPerson,private PersonInfo
{
public:
explicit CPerson(DatabaseID pid):PersonInfo(pid){}
virtual std::string name() const
{
return PersonInfo::theName();
}
virtual std::string birthDate() const
{
return PersonInfo::theBirthDate();
}
private:
const char* valueDelimOpen() const // 重新定义继承来的函数
{
return "";
}
const char* valueDelimClose() const // 重新定义继承来的函数
{
return "";
}
}
Note:总结
- 多继承比单继承复杂,多继承可能导致调用的歧义性以及菱形继承。
- 解决菱形继承问题需要用到虚继承,但是虚继承会带来更多的空间消耗和时间消耗,以及复杂的初始化成本。不带数据成员的虚基类是最有价值的。
- 多重继承有它的合理用途,即把
public 继承接口类和private 继承协助实现类结合起来。