Effective C++ 06 继承与面向对象设计

本文探讨了面向对象设计中的继承原则,强调public继承应表达is-a关系,避免遮掩继承名称,区分接口继承和实现继承,并考虑virtual函数的替代方案。此外,还提醒避免重新定义non-virtual函数和继承的缺省参数值,明智使用private继承和多重继承,以实现has-a或is-implemented-in-terms-of关系。

6. 继承与面向对象设计

条款 32:确定你的 public 继承塑模出 is-a 关系

以 C++ 进行面向对象编程,最重要的一个规则是:pubblic inheritance(公开继承)意味”is-a“(是一种)的关系。也就是说,public 继承和 is-a 之间类似于等价关系。

如果令 class D 以 public 形式继承 class B,你便告诉 C++ 编译器说,没有给类型为 D 的对象同时也是一个类型为 B 的对象,反之不成立。也就是 B 比 D 更一般化,D 比 B 更特殊化。

于是,C++ 领域内,任何函数如果期望获得一个类型为 base 的实参,也都愿意接受一个 derived 对象。这个论点只对 public 继承才成立!

is-a 并非唯一存在于 class 之间的关系。另两个常见的关系是 has-a(有一个)和 is-implemented-terms-of(根据某物实现出)。这些关系将在条款 38 和 39 讨论。
注: 世界上并不存在一个适用于所有软件的完美设计,所谓最佳设计,取决于系统希望做什么事,包括现在与未来。

请记住:

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

条款 33:避免遮掩继承而来的名称

其实这个题材和继承无关,而是和作用域有关。参考下面例子:

int x;  // global 变量
void someFunc() {
	double x;  // local 变量
	std::cin >> x;  // 读取一个心事赋予 local 变量 x
}

C++ 的名称遮掩规则所做的唯一事情就是:遮掩名称,内层名称遮掩外层名称。

using 声明

derived class 继承了声明于 base class 内地所有东西,但实际运作方式是,derived class 作用域被嵌套在 base class 作用域内。在继承体系中,编译器首先在 derived class 内查找;如果未找到,则在 base class 内查找;如果还没有找到,就在包含 base class 的命名空间找查找;还是没有找到的话就去 global 作用域找。

可以使用 using 声明式使用被掩盖的 base class 成员:

class Derived : public Base {
public:
	using:: Base::mf1;  // 让 Base class 内名为 mf1 的所有东西哦都在 Derived 内可见
};
转交函数

有时候你并不想继承 base class 的所有函数,这是可以理解的。但是在 public 继承下,这违反了 public 继承所暗示的 is-a 关系。(这也是为什么上述 using 声明式被放在 duerived class 的 public 区域的原因:base class 内的 public 名称在 publicly derived class 内也应该是 public。)然而在 private 继承(见条款 39)下它却可能是有意义的。例如 Derived 以 private 继承 Base,而 Derived 唯一想继承的 mf1 是哪个无参版本。using 声明在这里并没有什么用,因为 using 声明会令继承而来的某给定名称的所有同名函数在 derived class 内都可见。这是我们就需要不同的方法,即一个简单转交函数

class Derived : private Base {
public:
	virtual void mf1() {  // 转交函数 
		Base::mf1();  // 暗自成为 inline
	}
};

请记住:

  • derived class 内的名称会掩盖 base class 内的名称。在 public 继承下从来没有人希望如此。
  • 为了让被掩盖的名称再见天日,可使用 using 声明或转交函数。

条款 34:区分接口继承和实现继承

继承实际上有两部分组成:函数接口继承函数实现继承。这两种继承的差异,类似于函数声明与函数定义之间的差异。

身为 class 设计者,有时候你会希望 derived class 只继承成员函数的接口(也就是声明);有时候你又会希望 derived class 同时继承函数的接口和实现,但又希望能够覆写它们所继承的实现;又有时候你希望 derived class 同时继承函数的接口和实现,并且不允许覆写任何东西。

参考下面的例子:

class Shape {
public:
	virtual void draw() const = 0;
	virtual void error(const std::string& msg);
	int objectId() const;
	...
};

Shape 是个抽象 class,因为它有 pure virtual 函数,所以客户不能够创建 Shape class 实体,只能创建其 derived class 的实体。

Shape class 声明了三个函数。第一个是 draw 是个 pure virtual 函数;error 是个 impure virtual 函数;object ID 是个 non-virtual 函数。

pure virtual 函数

pure virtual 函数有两个最突出的特性:它们必须被任何继承了它们的具象 class 重新声明,而且它们在抽象 class 中通常没有定义。所以说:

  • 声明一个 pure virtual 函数的目的,是为了让 derived class 只继承函数接口。
impure virtual 函数

一如往常,derived class 继承其函数接口,但 impure virtual 函数会提供一份实现代码,derived class 可能覆写它。即:

  • 声明 impure virtual 函数的目的,是让 derived class 继承该函数的接口和缺省实现。如果不实现 derived class 自己的版本,就可以使用 base class 的版本。

但是,允许 impure virtual 函数同时指定函数声明和函数缺省行为,却有可能造成危险。

non-virtual 函数

如果成员是个 non-virtual 函数,意味着它并不打算在 derived class 中有不同的行为。实际上一个 non-virtual 成员函数所表现的不变性凌驾其特异性,也就是说它绝不该在 derived class 中被重新定义(也是条款 36 讨论的重点)。就其自身而言:

  • 声明 non-virtual 函数的目的,是为了令 derived class 继承函数的接口以及一份强制性实现。

pure virtual 函数、impure virtual 函数、non-virtual 函数之间的差异,使得你以精确指定你想要 derived class 继承的东西:只继承接口,或是继承接口和一份缺省实现,或是继承接口和一份强制实现。

请记住:

  • 接口继承和实现继承不同。在 public 继承下,derived class 总是继承 base class 的接口。
  • pure virtual 函数只具体指定接口继承。
  • impure virtual 函数具体指定接口继承以及缺省实现继承。
  • non-virtual 函数具体指定接口继承以及强制性实现继承。

条款 35:考虑 virtual 函数以外的其他选择

参考下面的例子:

class GameCharacter {
public:
	virtual int healthValue() const;  // 返回人物的健康指数;derived class 可以重新定义它
};

healthValue 并未声明为 pure virtual,这按时我们将会有一个计算健康指数的缺省算法(见条款 34)。

藉由 non-virtual interface 手法实现 template method 模式(用NVI 手法替换 public virtual)

这个流派主张 virtual 函数应该几乎总是 private。这个流派建议,较好的设计是保留 healthValue 为 public 成员函数,但让它成为 non-virtual,并调用一个 private virtual 进行实际工作:

class GameCharacter {
public:
	int healthValue() const {  // derived class 不重新定义它(见条款 36)
		...  // 事前工作,例如,锁定互斥器、制造运转日志记录项、验证 class 约束条件、验证函数先决条件
		int retVal = doHealthValue();
		...  // 事后工作,例如,互斥器解除锁定、验证函数时候条件、再次验证 class 约束条件
		return retVal;
	}
private:
	virtual int doHealthValue() const {  // derived class 可重新定义它
		...
	}
};

这一设计,让客户通过 public non-virtual 成员函数间接调用 private virtual 函数,称为 non-virtual interface(NVI)手法。它是所谓 template method 设计模式(与 C++ template 并无关联)的一个独特表现形式。non-virtual 函数可以看作 virtual 函数的外覆器。

NVI 手法的一个优点就是,外覆器确保在一个 virtual 函数被调用之前设定好适当场景,并在调用结束之后清理场景。

藉由 function pointer 实现 strategy 模式

使用 NVI 手法来替代 public virtual还是使用 virtual 函数来计算每个人物的健康指数。但是如果说人物健康指数与人物类型无关,这一的计算完全不需要人物这个成分。例如我们可能会要求每个人物的构造函数接受一个指针,指向一个健康计算函数,而我们可以调用该函数进行实际计算:

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;
};

这个做法是常见的 strategy 设计模式的简单引用,它提供了一些有趣的弹性:

  • 同一人物类型不同实体可以有不同的健康计算函数。

例如:

class EvilBadGuy : public GameCharacter {
public:
	explicit EvilBadGuy(HealthCalcFunc hcf = defaultHealthCalc) : defaultHealthCalc(hcf) { ... }
};

int loseHealthQuickly(const GameCharacter&);  // 计算函数1
int loseHealthSlowly(const GameCharacter&);  // 计算函数2
// 相同类型人物,不同的健康计算方式
EvilBadGuy egb1(loseHealthQuickly);
EvilBadGuy egb2(loseHealthSlowly);
  • 某已知人物健康指数计算函数可在运行期变更。例如 GameCharacter 可提供一个成员函数 setHealthCalculator,用来替换当前的健康指数计算函数。

换句话说,健康指数计算函数不再是 GameCharacter 继承体系内的成员函数。这一事实意味着,这些计算函数并没有特别访问“即将被计算健康指数”的哪个对象的内部成分。

藉由 tr1::function 完成 strategy 模式

这种方法可以不再使用函数指针(如前例的 healthFunc),而是改用一个类型为 tr1::function 的对象,这一的对象可持有任何可调用物(函数指针、函数对象或成员函数指针):

class GameCharacter;  // 前置声明
// 以下函数是计算健康指数的缺省算法
int defaultHealthCalc(const GameCharacter& gc);
class GameCharacter {
public:
		// HealthCalcFunc 可以是任何可调用物,只要可调用五接受任何兼容 GameCharacter,并返回任何兼容 int 的东西
		typedef std::tr1::function<int (const HealthCalcFunc&)> HealthCalcFunc;
	explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc) : healthFunc(hcf) { }
	int healthValue() const {
		return healthFunc(*this);
	}
private:
	HealthCalcFunc healthFunc;
};

和前一个设计(其 GameCharacter 持有的是函数指针)比较,这个设计几乎相同。唯一不同的是 GameCharacter 持有一个 tr1::function 的对象,相当于一个指向函数的泛化指针。

古典的 strategy 模式

古典的 strategy 做法会将健康计算函数做成一个分离的继承体系中的 virtual 成员函数。

class GameCharacter;  // 前置声明
class HealthCalcFunc {
public:
	virtual int calc(const GameCharacter& gc) const { ... }
};
HealthCalcFunc defaulthHealthCalc;
class GameCharacter {
public:
	explicit GameCharacter(HealthCalcFunc* phcf = &defaultHealthCalc) : healthFunc(hcf) { }
	int healthValue() const {
		return pHealthCalc->calc(*this);
	}
private:
	HealthCalcFunc* pHealthCalc;
};

这个解法的吸引力在于,熟悉标准 stragegy 模式的人很容易识别它,而且只要为 HealthCalcFunc 继承体系添加要给 derived class 就可以提供一个新的健康算法。

摘要

本条款的根本忠告是,当你为解决问题而寻找某个设计方法时,不妨考虑 virtual 函数的替代方法:

  • 使用 non-virtual interface 手法,那是 template method 设计模式的一种特殊形式。它以 public non-virtual 成员函数包裹较低访问性的 virtual 函数。
  • 将 virtual 函数替换为函数指针成员变量,这是 strategy 设计模式的一种分解表现形式。
  • 以 tr1::function 成员变量替换 virtual 函数没因而允许使用任何可调用五搭配一个兼容于需求的签名式。这也是 strategy 设计模式的某种形式。
  • 将继承体系内的 virtual 函数替换为另一继承体系内的 virtual 函数。这是 strategy 设计模式的传统实现手法。

请记住:

  • virtual 函数的替代方案包括 NVI 手法及 strategy 设计模式的多种形式。NVI 手法自身是一个特殊形式的 template method 设计模式。
  • 将机能从成员函数一道 class 外部,带来的一个缺点是,非成员函数无法访问 class 的 non-public 成员。
  • tr1::function 对象的行为就像一般函数指针。这样的对象可接纳“与给定目标签名式兼容”的所有可调用物。

条款 36:绝不重新定义继承而来的 non-virtual 函数

参考下面例子:

class B {
public:
	void mf();
};
class D : public { ... };

思考下面的行为:

D x;  // x 是一个类型为 D 的对象
B* pB = &x;  // 获得一个指针指向 x
pB->mf();  // 经由该指针调用 mf
D* pD = &x;  // 获得一个指针指向 x
pD->mf();  // 经由该指针调用 mf

二者都是通过对象 x 调用成员函数 mf。由于二者所调用函数都相同,凭借对象也相同,但是行为也应该相同吗?

是的,理应如此,但是实际上可能不是这一,如果 mf 是个 non-virtual 函数而 D 定义有自己的 mf 版本,就不是这样了:

class D : public B {
public:
	void mf();  // 掩盖了 B::mf 见条款 33
};
pB->mf();  // 调用 B::mf
pD->mf();  // 调用 D::mf

造成这种行为的原因是,non-virtual 函数如 B::mf 和 D::mf 都是静态绑定(见条款 37).这意思是,由于 pB 被声明为一个 pointer-to-B,通过 pb 调用的 non-virtual 函数永远是 B 所定义的版本,即使 pB 指向一个类型是 B 的派生类的对象。

另一方面,virtual 函数确实动态绑定(见条款 37),所以它们不受这个问题的影响。如果 mf 是个 virtual 函数,不论是通过 pB 或 pD 调用 mf,都会调用 pD::mf,因为 pB 和 pD 真正指向的都是一个类型为 D 的对象。

如果编写 class D 并重新定义继承自 class B 的 non-virtual 函数 mf,D 对象很可能展现出精神分裂的不一致行径。条款 32 说过,所谓 public 继承意味 is-a 的关系;条款 34 则描述为什么再 class 内声明一个 non-virtual 函数会为该 class 建立一个不变性凌驾其特异性。那么:

  • 适用于 B 对象的每一件事,也适用于 D 对象,因为每一个 D 对象都是一个 B 对象。
  • B 的 derived class 一定会继承 mf 的接口和实现,因为 mf 是 B 的一个 non-virtual 函数。

现在,如果 D 重新定义 mf,这个设计便会出现矛盾。如果一定要设计这样的 mf 应该声明为 virtual 函数。

请记住:

  • 绝不要重新定义继承而来的 non-virtual 函数。

条款 37:绝不重新定义继承而来的缺省参数值

本条款讨论局限于“继承一个带有缺省才数值的 virtual 函数”。virutal 函数是动态绑定的,而缺省参宿值却是静态绑定。

对象的静态类型,就是它再程序中被声明时所采用的类型。参考下面的例子:

// 一个用来描述几何形状的 class
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:
	// 注意,这样写,则当客户以对象调用此函数,一定要指定参数值,因此静态绑定这个函数时,并不从其 base 继承缺省参数值。但若以指针(或引用)调用此函数,可以不指定参数值,因为动态绑定下这个函数会从其 base 继承缺省参数值
	virtual void draw(ShapeColor color) const;
};

考虑下面这些指针:

Shape* ps;  // 静态类型为 Shape*
Shape* pc = new Circle;  // 静态类型为 Shape*
Shape* pr = new Rectangle;  // 静态类型为 Shape*

对象的动态类型,是指目前所指对象的类型。也就是说,动态类型可以表现出一个对象将会有什么行为。

动态类型,可以在程序执行过程中改变(通常是经由赋值动作):

ps = pc;  // ps 动态类型是 Circle*
ps = pr;  // ps 的动态类型是 Rectangle*

virtual 函数是动态绑定,而缺省参数是却是静态绑定。也就是说,你可能会在调用一个定义于 derived class 的 virtual 函数的同时,却使用 base class 为它指定缺省参数值:

pr->draw();  // 调用 Rectangle::draw(Shape::Red)

此例种,pr 动态类型是 Rectangle*,所以调用的是 Rectangle 的 virtual 函数,但是 Rectangle::draw 的函数缺省参数值应该是 Green,由于 pr 的静态类型是 Shape*,所以此调用的缺省参数值来自 Shape class。

但是当你遵守这个规则:

class Rectangle : public Shape {
public:
	virtual void draw(ShapeColor color = Red) const;
};

此时,代码重复,而且还带着相依性,如果 Shapr 内的缺省参数值改变了,所有重复给定缺省参数值的那些 derived class 也必须做出改变。

聪明的做法是考虑替代设计,例如条款 35 所给出的 virtual 函数的替代设计,其中之一是 NVI 手法:

class Shape {
public:
	enum ShapeColor {Red, Green, Blue};
	virtual void draw(ShapeColor color = Red) const {  // non-virtual
		doDrow(color);  // 调用一个 virtual
	}
private:
	virtual void doDraw(ShapeColor color) const = 0;  // 真正工作在此处完成
};
class Rectangle : public Shape {
public:
	...
private:
	virtual void doDraw(ShapeColor color) const;  // 注意,不需指定缺省参数值
};

由于 non-virtual 函数不应该被 derived class 覆写,所以这个 draw 函数的 color 缺省参数值总是 Red。

请记住:

  • 绝不要重新定义一个继承而来的缺省参数值,因为缺省参数值都是静态绑定,而 virtual 函数却是动态绑定的。

条款 38:通过符合塑模出 has-a 或 根据某物实现出

复合式类型之间的一种关系,当某种类型的对象内含它种类型的对象,便是这种关系,例如:

class Address { ... };
class Person {
private:
	std::string name;  // 合成成分物
	Address address;  // 同上
};

符合意味着 has-a(有一个)或 is-implemented-in-terms-of(根据某物实现出)。当符合发生于应用域内的对象之间,表现出 has-a 关系;当它发生于实现域内则是表现 is-implemented-in-terms-of 的关系。

上述的 Person class 示范 has-a 关系。Person 有一个名称,有一个地址。区分 is-a 和 has-a 并不麻烦。比较麻烦的是区分 is-a 和 is-implemented-in-terms-of 这两种对象的关系。

假如,我们通过 list 来实现 Set,于是声明 Set template 如下:

template<typename T>
class Set : public std::list<T> { ... };  // 将 list 应用于 set 的错误做法

但是 list 可以重复元素,但是 Set 并不允许,所以二者不是 is-a 关系(见条款 32 ),所以 public 继承不适合用来塑模它们。但是你应该知道 Set 对象可以根据一个 list 对象实现出来,所以正确的做法是:

template<typename 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;  // 用来表述 Set 的数据
};

Ser 成员函数可大量依赖 list 及标准库其他部分提供的机能来完成。这关系并非是 is-a,而是 is-implemented-in-terms-of。

请记住:

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

条款 39:明智而审慎地使用 private 继承

参考下面例子:

class Person { ... };
class Student : private Person { ... };  // private 继承
void eat(const Person& p);  // 任何人都会吃
void study(const Student& s);  // 学生才学习

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

eat(p);  // 正确,p 是人,人会吃
eat(s);  // 错误! 

显然 private 继承并不是 is-a 关系。如果 class 之间的继承关系是 private ,则编译器不会自动将一个 derived calss 对象转换为一个 base class 对象。这也是为什么通过 s 调用 eat 错误的原因;由 private base class 继承而来的所有成员,在 drived class 中都会变成 private 属性,即使它们在 base class 中是 public 或 protected 成员。

private 继承意味 is-implemented-in-terms-of(根据某物实现出)。如果让 class D 以 private 形式继承 class B,用意是为了采用 class B 内已有的某些特性,而不是因为 B 对象和 D 对象存在任何观念上的关系。借助条款 34 提出的术语,private 继承意味只有实现部分被继承,接口部分略去。

private 继承和复合类型

private 继承和条款 38 中所提的实现域中的复合关系的意义是一样的,但是尽可能的使用复合,只有当 protected 成员和/或 virtual 函数牵扯进来时才使用 private 继承。

参考下面例子:

class Timer {
public:
	explicit Timer(int tickFrequency);
	virtual void onTick() const;  // 定时器每滴答一次,此函数就被自动调用一次
};

我们将修改 Widget class,让它记录每个成员函数被调用次数。为了让 Widget 重新定义 Timer 内的 virtual 函数,Widget 必须继承自 Timer。但 public 明显不合适,因为 Widget 并不是个 Timer,所以我们必须以 private 形式继承 Timer:

class Widget : private Timer {
private:
	virtual void onTick() const;  // 查看 Widget 的数据等等
};

通过 private 继承,我们就将 Timer 的 public onTick 函数在 Widget 内变成 private,而我们重新声明(定义)时仍然把他留在那。

当然我们也可以以复合取代:

class Widget {
private:
	class WidgetTimer : public Timer {
	public:
		virtual void onTick() const;
	};
	WidgetTimer timer;
};

这样,Widget 可以拥有 derived class,而且阻止了 derived class 重新定义 onTick。如果 Widget 继承自 Timer,就无法实现,因为 derived class 可以重新定义 virtual 函数,即使不调用它(见条款 35)。但如果 WidgetTimer 是 Widget 内部的一个 private 成员并继承 Timer,Widget 的 derived class 将无法取用 WidgetTimer,因此无法继承它或重新定义它的 virtual 函数。这也是 Java 和 C# 中阻止 deived class 重新定义 virtual 函数的能力。

第二,可以将 Widget 的编译依存性降至最低。如果 Widget 继承 Timer,当 Widget 被编译时 Timer 的定义必须可见。但如果 WidgetTimer 移除 Widget 之外而 Widget 内涵指针指向一个 WidgetTimer,Widget 可以只带着一个简单的 WidgetTimer 声明式即可。

什么时候使用 private 继承

有一种激进情况涉及空间最优化时,可能会促使你选择 private 继承而不是 继承加复合。

C++ 裁定凡是独立(非附属)对象都必须有非零大小,所以如果你这样做:

class Empty { };  // 没有数据,所以其对象应该不使用内存

class HoldsAnInt {  // 应该只需要一个 int 空间
private:
	int x;
	Empty e;  // 应该不需要内存
};

但事实上,你会发现 sizeof(HoldsAnInt) > sizeof(int),也就是说一个 Empty 成员变量要求了内存。在大多数编译器中 sizeof(Empty) 获得 1,面对大小为零的独立(非附属)对象,通常 C++ 官方勒令安插一个 char 到空对象内。但是这个约束不适用于 derived class 对象内的 base class 成分,因为它们并非独立(非附属)对象:

class HoldsAnInt : private Empty { 
private:
	int x;
};

此时几乎可定 sizeof(HoldsAnInt) == sizeof(int)。这就是所谓的 EBO(空白基类最优化)。如果你是一个程序开发人员,你得客户非常在意空间,那么 EBO 值得被你关注,还有 EBO 一般只在但一次继承(而非多重继承)下才可行,EBO 无法被施行欲拥有多个 base 的 derived class 身上。

尽管如此,大多数 class 并非 empty,所以 EBO 很少成为 private 继承的真正理由。复合和 private 继承都意味 is-implemented-in-terms-of,但复合更容易理解,所以无论什么时候,只要可以们还是应该选择复合。

请记住:

  • private 继承意味 is-implemented-in-terms-of(根据某物实现出)。它通常比复合的级别低。但是当 derived class 需要访问 protected base class 的成员,或需要重新定义继承而来的 virtual 函数时,这么设计是合理的。
  • 和复合不同,private 继承可以造成 empty base 最优化。这对于致力于对象尺寸最小化的程序库开发者而言,可能很重要。

条款 40:明智而审慎地使用多重继承

一旦设计多重继承(MI),C++ 便会划分为两个阵营。其中之一认为如果单一继承(SI)是好的,多重继承一定更好。另一派认为,单一继承足够好,多重继承不值得使用。

需要了解的是,当使用多重继承时,程序有可能从一个以上 base class 继承相同名称(如函数、typedef 等)。那会导致较多的歧义发生。

钻石型多重继承

参考下面例子:

class File { ... };
class InputFile : public File { ... };
class OutputFile : public File { ... };
class IOFile : public InputFile, public OutputFile { ... };

从某个角度说,IOFile 从其每个 base class 继承一份,所以其对象内应该有两份 fileName 成员变量。但从另一个角度说,IOFile 对象应该只有一个文件名称,所以它继承自两个 base class 而来的 fileName 不该重复。

C++ 对二者没有倾斜立场;两个方案都支持 —— 虽然缺省做法是执行赋值。如果这不是你要的,你必须令那个带有此数据的 class(即 File)成为一个 virtual base class。为了这样做,必须令所有直接继承它的 class 采用 virtual 继承:

class File { ... };
class InputFile : virtual public File { ... };
class OutputFile : virtual public File { ... };
class IOFile : public InputFile, public OutputFile { ... };

从正确行为的观点看,public 继承应该总是 virtual,这样可以避免继承的来的成员变得重复。但是这样的后果是:使用 virtual 继承的那些 class 所产生的对象往往比用 non-virtual 继承的体积大,访问 virtual base class 的成员变量也速度也更慢,总之你需要为 virtual 继承付出代价。

对于 virtual 继承来说,第一,非必要不使用 virtual base,平常请使用 non-virtual 继承;第二,如果你必须使用 virtual base class,尽可能避免在其中放置数据。

请记住:

  • 多重继承比单一继承复杂。它可能导致新的歧义性,以及对 virtual 继承的需要。
  • virtual 继承会增加大小、速度、初始化(及及赋值)复杂度等等成本。如果 virtual base class 不带任何数据,将是最具使用价值的情况。
  • 多重继承的确有正当用途。其中一个情节涉及,public 继承某个 interface class,private 继承某个协助实现的 class 的组合。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值