Inheritance and Object-Oriented Design

[toc]

我:请问什么是面向对象原则

ChatGPT:SOLID 是面向对象设计中的五个基本设计原则的首字母缩写,它们是:

  1. 单一职责原则(Single Responsibility Principle,SRP): 每个类或模块应该只有一个改变的理由,即一个类应该只负责一个职责。这个原则指导我们将系统分解为小而独立的部分,每个部分都有自己的职责,使得代码更加清晰、可维护和可重用。
  2. 开放-封闭原则(Open/Closed Principle,OCP): 软件实体(类、模块、函数等)应该对扩展开放,对修改封闭。这意味着当需要改变系统的行为时,不应该修改已有的代码,而是应该通过扩展现有的代码来实现新的行为。这个原则鼓励使用抽象和多态性来实现系统的灵活性和可扩展性。
  3. 里氏替换原则(Liskov Substitution Principle,LSP): 已有的对象实例可以被它的子类实例替换,而程序的行为仍然保持一致。这个原则强调子类应该符合父类的契约,保持对父类的透明性,同时可以通过子类扩展父类的行为。
  4. 接口隔离原则(Interface Segregation Principle,ISP): 客户端不应该依赖于它不需要使用的接口。这个原则指导我们设计精简的接口,将大接口拆分为多个小接口,使得客户端只需要依赖于它们真正需要的接口,避免了不必要的依赖关系,提高了系统的灵活性和可维护性。
  5. 依赖反转原则(Dependency Inversion Principle,DIP): 高层模块不应该依赖于低层模块,它们都应该依赖于抽象。抽象不应该依赖于具体实现细节,而具体实现细节应该依赖于抽象。这个原则通过将高层模块和低层模块都依赖于抽象,实现了模块间的解耦合,提高了代码的灵活性和可维护性。

32. Make sure public inheritance models “is-a”

  • 里氏替换原则:即父类子类表现出更一般的概念,子类父类表现出更特殊化的一般概念。如果子类对象可以替换掉父类对象,并且程序行为不变,则这种继承关系是合理的。
  • 我们因此可以推出任何函数如果期望获得一个类型为Person(或pointer-to-Person 或reference-to- Person) 的实参,都也愿意接受一个Student对象(或pointer-to-student或reference-to-Student) :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Person {... };
class Student: public Person {... };

void eat(const Person& p); // 任何人都会吃
void study(const Student& s); // 只有学生才会去学校学习

Person p; // p 是人
Student s; // s 是学生

eat(p); // 没问题,p 是人
eat(s); // 没问题,s 是学生,而学生也是(is-a)人
study(s); // 没问题,s 是个学生
study(p); // 错误!p 不是个学生,不能调用 study 函数

企鹅和它的鸟事

我们知道大多数鸟是会飞的,而企鹅也是鸟(我也是第一次知道;)),那我们因该如何处理这个鸟类呢?

来看看有几种方法:

  1. 我们把企鹅与会飞的鸟分开
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Bird {
... // 没有声明飞行函数
};

class FlyingBird: public Bird {
public:
virtual void fly();
...
};

class Penguin: public Bird {

... // 没有声明飞行函数

};

  1. 产生一个运行时错误
1
2
3
4
5
6
7
8
9
void error(const std::string& msg);       // 定义在别处

class Penguin: public Bird {
public:
virtual void fly() { error("Attempt to make a penguin fly!");}

...

};
  1. 在编译期拒绝企鹅飞行
1
2
3
4
5
6
7
class Bird {
... // 没有飞行成员函数被声明
};

class Penguin: public Bird {
... //没有飞行成员函数被声明
};
  • 条款18 说过:好的接口可以防止无效的代码通过编译,因此你应该宁可采取“在编译期拒绝企鹅飞行”的设计,而不是“只在运行期才能侦测它们”的设计。

正方形与矩形

正方形应该以public形式继承矩形吗?

我:正方形应该以public形式继承矩形吗?

ChatGPT:

从面向对象设计的角度来看,正方形是否应该以公共形式继承矩形取决于所选择的模型和设计目标。

在数学上,正方形是矩形的一种特殊情况,因此从继承的角度来看,正方形确实可以被视为是矩形的一种。然而,在编程中,继承关系通常应该反映出真正的”is-a”关系。换句话说,如果子类对象在所有情况下都可以替代父类对象而不产生任何副作用,那么继承关系才是适当的。

在一般情况下,正方形与矩形之间的继承关系并不符合”is-a”关系。尽管正方形确实是一种特殊的矩形,但它们在行为和属性上可能会有所不同。例如,矩形通常具有长度和宽度两个属性,而正方形则具有相等的边长。如果以公共形式继承矩形,那么正方形将继承矩形的长度和宽度属性,这可能会导致一些不符合预期的行为。

因此,在大多数情况下,正方形不应该以公共形式继承矩形。相反,可以考虑使用组合或私有继承来实现正方形与矩形之间的关系。例如,可以将正方形类内部包含一个矩形对象,并在必要时委托给它,以实现代码的复用和灵活性。

请记住

  • “public 继承“意味is-a 。适用于base classes 身上的每一件事情一定也适用于derived classes 身上,因为每一个derived class 对象也都是一个base class 对象。

33. A void hiding inherited names.

  • 内层作用域的名称会遮掩(遮蔽)外围作用域的名称
1
2
3
4
5
6
7
8
9
10
#include <iostream>
int x = 0721; // 全局变量

int main()
{
double x = 114.514; // 局部变量

std::cout << x; // 读取局部变量 x 的新值
}
//输出 114.514
  • 那么延伸到继承上,当子类继承了父类并且父类和子类中存在同名的成员函数或成员变量时,子类会覆盖(隐藏)父类中相同名称的成员函数或成员变量。

那么,我们有什么方法来使用父类的成员变量呢?

  1. 通过using声明式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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; // make all things in Base named mf1 and mf3
using Base::mf3; // visible (and public) in Derived's scope

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

如果你继承base class 并加上重载函数,而你又希望重新定义或覆写(推翻)其中一部分,那么你必须为那些原本会被遮掩的每个名称引入一个using 声明式,否则某些你希望继承的名称会被遮掩。

  1. 转交函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Base {
public:
virtual void mf1() = 0;
virtual void mf1(int);

... // 如前
};

class Derived: private Base {
public:
virtual void mf1() // 转发函数;隐式内联(参见 Item 30)
{ Base::mf1(); } // 内联实现

...
};

Derived d;
int x;

d.mf1(); // 可以,调用 Derived::mf1
d.mf1(x); // 错误!Base::mf1() 被隐藏

请记住

  • derived classes 内的名称会遮掩base classes 内的名称。在public 继承下从来没有人希望如此。
  • 为了让被遮掩的名称重见天日,可使用using 声明式或转交函数(forwarding functions) 。

34. Differentiate between inheritance of interface and inheritance of implementation.

表面上直截了当的public 继承概念,经过更严密的检查之后,发现它由两部分组成:函数接口(function interfaces) 继承函数实现(function implementations)继承。这两种继承的差异,很像本书导读所讨论的函数声明与函数定义之间的差异。

考虑如下函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 { ... }; // 椭圆类继承自形状类

  • virtual 关键字用于声明可以在派生类中被重写的虚函数,它可以有默认实现。pure virtual 关键字用于声明没有实现的虚函数,它要求派生类必须提供实现。纯虚函数的存在使得基类成为一个抽象类,无法实例化。
1
2
3
4
5
6
7
8
9
10
11
12
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(); // 调用 Shape::draw

//TODO

35. Consider alternatives to virtual functions

考虑一个健康值计算,由于每个角色计算方式不同,我们采用virtual方式

1
2
3
4
5
class GameCharacter {
public:
virtual int healthValue() const; // 返回角色的健康值评级;派生类可以重新定义这个函数
... // 派生类可以重新定义这个函数
};

借由Non-virtual Interface 手法实现 Template Method 模式

保留healthValue 为public 成员函数,但让它成为non-virtual, 并调用一个private(或protect) virtual 函数(例如doHealthValue)进行实际工作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class GameCharacter {
public:
int healthValue() const // 派生类不需要重新定义此函数 - 参见条款36
{ // 下方查看"before"和"after"的处理
... // 执行 "before" 操作 - 见下文
int retVal = doHealthValue(); // 进行实际工作
... // 执行 "after" 操作 - 见下文
return retVal;
}
...
private:
virtual int doHealthValue() const // 派生类可以重新定义此函数
{
... // 计算角色健康值的默认算法
}
};

这种手法被称为NVI手法。这个non-virtual函数可被称为virtual函数的外覆器。即我们可以在计算健康值前和后做一些工作,如日志记录,互斥锁等。

借由Functions Pointers实现Strategy模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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 防止隐式类型转换
explicit EvilBadGuy(HealthCalcFunc hcf = defaultHealthCalc)
: GameCharacter(hcf)
{ ... }

...

};
int loseHealthQuickly(const GameCharacter&); // 健康计算函数
int loseHealthSlowly(const GameCharacter&); // 不同行为的函数

EvilBadGuy ebg1(loseHealthQuickly); // 具有不同健康行为的相同类型角色
EvilBadGuy ebg2(loseHealthSlowly); // 具有不同健康行为的相同类型角色

通过Strategy设计模式,我们可以为不同角色设置不同策略,还可以在运行时修改策略(比如boss的不同阶段)

请记住

36. Never redefine an inherited non-virtual function.

  • non-virtual函数是静态绑定的,用什么指针(引用)调用mf(),调用的版本是该指针的类型。如base指针只能调用base的mf(),derived指针只能调用derived的mf()。
  • virtual函数是动态绑定的。调用的mf()函数是根据你所指对象的类型来调用对应的成员函数mf()。

正确做法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class B {
public:
virtual void mf() { /* ... */ } // 将此函数声明为虚函数
// ...
};

class D: public B {
public:
void mf() override { /* ... */ } // 使用 override 关键词明确指出是要重写基类的虚函数
// ...
};

B *pB = new D();
pB->mf(); // 调用 D::mf,因为 mf 是虚函数,且 pB 指向 D 类型的对象

D *pD = new D();
pD->mf(); // 调用 D::mf

37. Never redefine a function’s inherited default parameter value.

我们能继承两种函数virtual函数和non-virtual函数,我们在上一节已经讨论了non-virtual函数继承,那么接下来聊一下vtubervirtual继承吧。

敲重点:virtual functions(虚拟函数)是 dynamically bound(动态绑定),而 default parameter values(缺省参数值)是 statically bound(静态绑定)。

静态绑定(early binding)与动态绑定(late binding)

一个 object(对象)的 static type(静态类型)就是你在程序文本中声明给它的 type(类型)。考虑这个 class hierarchy(类继承体系):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 几何形状的类
class Shape {
public:
// 枚举类型表示形状的颜色
enum ShapeColor { Red, Green, Blue };

// 所有形状都必须提供一个绘制自己的函数
virtual void draw(ShapeColor color = Red) const = 0;
...
};

// 矩形类,继承自Shape类
class Rectangle: public Shape {
public:
// 注意不同的默认参数值 — 不好!
virtual void draw(ShapeColor color = Green) const;
...
};

// 圆形类,继承自Shape类
class Circle: public Shape {
public:
virtual void draw(ShapeColor color) const;
...
};

现在考虑这些 pointer:

1
2
3
Shape *ps;                      
Shape *pc = new Circle;
Shape *pr = new Rectangle;

ps,pc 和 pr 全被声明为 pointer-to-Shape 类型,它们的静态类型都是 Shape*。

一个对象的动态类型取决于它当前引用的对象的类型。也就是说,动态类型表明它有怎样的行为。在上面的例子中,pc 的动态类型是 Circle,而 pr 的 动态类型是 Rectangle。至于 ps,它没有一个实际的 动态类型

动态类型,就像它的名字所暗示的,能在程序运行中变化,特别是通过 assignments(赋值):

1
2
ps = pc; // ps的动态类型现在是 Circle*
ps = pr; // ps的动态类型现在是 Rectangle*

virtual functions动态绑定,意味着被调用的特定函数取决于被用来调用它的那个对象动态类型

1
2
pc->draw(Shape::Red);             // calls Circle::draw(Shape::Red)
pr->draw(Shape::Red); // calls Rectangle::draw(Shape::Red)

但是当你考虑带有缺省参数值的virtual 函数,花样来了,因为就如我稍早所说,virtual 函数是动态绑定,而缺省参数值却是静态绑定。意思是你可能会在“调用一个定义于derived class 内的virtual 函数”的同时,却使用base class 为它所指定的缺省参数值

1
pr->draw(); // calls Rectangle::draw(Shape::Red)!	

可以看到使用了Shape的缺省参数。

我:为什么c++virtual functions(虚拟函数)是 dynamically bound(动态绑定),而 default parameter values(缺省参数值)是 statically bound(静态绑定)。

GLM-4:

C++中虚拟函数(virtual functions)是动态绑定的,而缺省参数值(default parameter values)是静态绑定的,这是由于C++语言的设计和实现机制决定的。

虚拟函数的动态绑定:

  1. 多态性:C++的面向对象编程支持多态性,允许通过基类指针或引用调用派生类的函数。动态绑定是实现多态性的关键,它确保了在运行时能够根据对象的实际类型来调用正确的函数版本。
  2. 虚函数表(vtable):C++通过虚函数表来实现动态绑定。每个包含虚函数的类都有一个与之关联的虚函数表,表中包含了该类的虚函数的地址。当一个对象被创建时,它的构造函数会初始化一个指向其虚函数表的指针。当通过指针或引用调用一个虚函数时,实际调用的函数地址是从虚函数表中获取的,而不是在编译时确定的。
  3. 运行时类型信息(RTTI):C++的运行时类型信息允许程序在运行时查询对象的实际类型。这是通过虚函数表和类型信息实现的,它使得动态绑定成为可能。

缺省参数值的静态绑定:

  1. 编译时确定:缺省参数值是在编译时确定的,它们是函数签名的一部分。当编译器解析一个函数调用时,它会根据函数的声明来确定是否需要使用缺省参数值。这意味着缺省参数值与函数的静态类型相关联,而不是与对象的动态类型相关联。
  2. 效率考虑:静态绑定缺省参数值可以提高效率,因为编译器可以在编译时确定所有需要的参数,而不需要在运行时进行额外的查找。
  3. 一致性和可预测性:静态绑定缺省参数值确保了函数的行为是一致的,因为缺省参数值不会因为通过不同的指针或引用调用而改变。

由于这些原因,C++中的虚拟函数是动态绑定的,而缺省参数值是静态绑定的。这种设计允许C++在提供多态性的同时保持编译时的类型检查和效率。然而,这也意味着在使用虚函数和缺省参数时需要特别注意,以避免由于静态绑定和动态绑定的不同而产生的混淆和错误。

让我们给Rectangle加上默认参数。

1
2
3
4
5
class Rectangle: public Shape {
public:
virtual void draw(ShapeColor color = Red) const;
...
};

喔欧,代码重复。更糟的是,代码重复又带着相依性:如果Shape 内的缺省参数值改变了,所有“重复给定缺省参数值”的那些derived classes 也必须改变,简直就是项目维护的灾难!

NVI!

通过nvi手法,实现以下代码。由于non-virtual 函数应该绝对不被derived classes 覆写(见条款36) ,这个设计很清楚地使得draw 函数的color 缺省参数值总是为Red。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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;
...
};

请记住

  • 绝对不要重新定义一个继承而来的缺省参数值,因为缺省参数值都是静态绑定,而virtual 函数——你唯一应该覆写的东西却是动态绑定。

38. Model “has-a” or “is-implemented-in-terms-of” through composition.

  • 复合(composition) 是类型之间的一种关系,当某种类型的对象内含它种类型的对象,便是这种关系。

  • 复合意味has-a (有一个)is-implemented-in-terms-of(根据某物实现出)

  • 应用域(application domain),复合意味has-a (有一个)。在实现域(implementation domain) ,复合意味is-implemented-in-terms-of (根据某物实现出)

那么什么对象是应用域、实现域?

  • 应用域:人、汽车、一张张视频画面等等。
  • 实现域:缓冲区(buffers) 、互斥器Cmutexes) 、查找树(search trees) 等等。

下面的Person 对象由string, Address, PhoneNurnber 构成。属于应用域,那该复合则为has-a (有一个)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Address { ... };             // 住址类,表示一个人的居住地

class PhoneNumber { ... }; // 电话号码类,表示一个电话号码

class Person {
public:
...

private:
std::string name; // 字符串对象,存储人的姓名
Address address; // Address对象,存储人的居住地址
PhoneNumber voiceNumber; // PhoneNumber对象,存储人的语音电话号码
PhoneNumber faxNumber; // PhoneNumber对象,存储人的传真号码
};

那么什么情况下是is-implemented-in-terms-of (根据某物实现出)

假设我们想重新定义一个set,默认的set存储一个节点需要三个指针(基于红黑树等数据结构实现)。在日常使用情况下没有问题,但是我们写出的程序要求空间比速度重要呢?例如set 不得不存储大量数据。

在这里,我们使用linked list来实现它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
template<class T>                   // 使用列表表示集合的正确方式
class Set {
public:
bool member(const T& item) const; // 成员函数:检查集合中是否包含特定项

void insert(const T& item); // 成员函数:向集合中插入项
void remove(const T& item); // 成员函数:从集合中移除项

std::size_t size() const; // 成员函数:返回集合大小

private:
std::list<T> rep; // 集合数据的表示
};

为什么我们不直接public继承list?

因为如果以public继承list,那么将会允许set持有相同元素,这显然是不正确的,这里我们使用private继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
template<class T>                   // 使用 list 作为 Set 的正确方式
class Set {
public:
bool member(const T& item) const; // 成员函数:检查元素是否存在
void insert(const T& item); // 成员函数:插入元素
void remove(const T& item); // 成员函数:移除元素
std::size_t size() const; // 成员函数:返回集合大小

private:
std::list<T> rep; // 用于存储集合数据的表示
};

template<typename T>
bool Set<T>::member(const T& item) const
{
return std::find(rep.begin(), rep.end(), item) != rep.end(); // 使用 std::find 查找元素
}

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 = std::find(rep.begin(), rep.end(), item); // 查找元素位置
if (it != rep.end()) rep.erase(it); // 如果找到元素,则移除
}

template<typename T>
std::size_t Set<T>::size() const
{
return rep.size(); // 返回列表大小作为集合大小
}

请记住

  • 复合(composition) 的意义和public 继承完全不同。
  • 应用域(application domain),复合意味has-a (有一个)。在实现域(implementation domain) ,复合意味is-implemented-in-terms-of (根据某物实现出)

39. Use private inheritance judiciously.

当以public继承时,编译器在必要时刻(为了让函数调用成功)将students 暗自转换为Persons 。

考虑如下例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Person { ... };
class Student: private Person { ... }; // 继承关系现在是私有的

void eat(const Person& p); // 任何人都可以吃饭

void study(const Student& s); // 只有学生才能学习

Person p; // p 是一个 Person

Student s; // s 是一个 Student

eat(p); // 没问题,p 是一个 Person

eat(s); // 错误!一个 Student 不是一个 Person,难道学生不是人吗

private继承:

  • 如果classes 之间的继承关系是private, 编译器不会自动将一个derived class 对象(例如Student) 转换为一个base class 对象(例如Person) 。
  • 由private base class 继承而来的所有成员,在derived class 中都会变成private 属性,纵使它们在base class 中原本是protected 或public 属性。
  • 借用条款34 提出的术语, private 继承意味只有实现部分被继承,接口部分应略去。

Private 继承意味is-implemented-in-terms-of (根据某物实现出),这个事实有点令人不安,因为条款38 才刚指出复合(composition) 的意义也是这样。你如何在两者之间取舍?

答案很简单:尽可能使用复合,必要时才使用private 继承。何时才是必要?主要是当protected 成员或virtual 函数牵扯进来的时候。其实还有一种激进情况,那是当空间方面的利害关系足以踢翻private 继承的支柱时。

假设我们需要定时对Widget对象状态进行检测,那么继承一个Timer类来定时实现他,当使用public继承:

1
2
3
4
5
6
7
class Timer {
public:
explicit Timer(int tickFrequency);
// 每个滴答周期自动调用的函数
virtual void onTick() const;
...
};

我们不想让客户可以调用这个函数,毕竟这不是Widget的一部分,违反条款18 的忠告:“让接口容易被正确使用,不易被误用"。改用private继承

1
2
3
4
5
6
7
class Widget: private Timer {		//Timer定义必须可见
private:
// 在时钟周期内观察 Widget 使用数据等
virtual void onTick() const;
...
};

但是private继承相较于复合,有点不好:

  • 我们不能阻止derived class重新定义OnTick

即使基类的公有和保护成员在派生类中变为私有成员,派生类仍然可以通过重写基类中的虚函数来提供自己的实现。这是因为虚函数的覆盖是基于对象的实际类型,而不是基于继承的访问权限。因此,即使使用了私有继承,派生类仍然可以重写基类中的虚函数,以提供多态行为。

  • 编译依存性较高
  • //TODO

那么,让我们来看一下复合

1
2
3
4
5
6
7
8
9
10
class Widget {
private:
class WidgetTimer: public Timer {
public:
virtual void onTick() const;
...
};
WidgetTimer timer;
...
};

在这种情况下,Widget子类无法重新定义onTrick函数。

虽然在大多数情况下复合更适合,但是private继承在一定情况下更适合。

考虑如下例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>

using namespace std;

class Base {

};

class Derived : private Base {
public:
int derived_data;
};


int main()
{
cout << sizeof(Base) << endl;
cout << sizeof(Derived) << endl;

}
//1
//4

可以看到,空白基类也要占用一个字节的大小。而在派生子类,继承的空白基类不占大小。C++ 裁定凡是独立(非附属)对象都必须有非零大小

//TODO

EBO?

Use multiple inheritance judiciously

//TODO

请记住