【C++ Primer】第18章 用于大型程序的工具 (3)


Part IV: Advanced Topics
Chapter 18. Tools for Large Programs


18.3 多重继承与虚继承

多重继承与虚继承思维导图

多重继承 (multiple inheritance) 是指从多个直接基类派生一个类的能力。多重派生类继承其所有父类的属性。
尽管在概念上很简单,但将多个基类交织在一起的细节可能会带来棘手的设计问题和实现问题。

为了探索多重继承,使用一个动物园动物等级作为教学示例。动物园的动物存在于不同的抽象层次。有个别的动物,以它们的名字区分,如 Ling-ling、Mowgli、Balou。每个动物都属于一个物种;如,Ling-ling 是一只大熊猫。物种又是科的成员;大熊猫是熊科的成员。每个科都是动物界的成员,在本例中,动物界是指动物园中动物总和。

定义一个抽象类 ZooAnimal 保存所有动物园中动物通用的信息,并提供最通用的接口。Bear 类将包含Bear 科特有的信息,以此类推。

除了 ZooAnimal 类之外,应用程序还将包含一些辅助类,用来封装各种抽象,如濒危动物。例如,Panda 类多重派生自 Bear 和 Endangered。

多重继承

派生类中的派生列表可以包含多个基类:

class Bear : public ZooAnimal {
class Panda : public Bear, public Endangered { /* ... */ };

每个基类都有一个可选的访问说明符。通常,如果省略了访问说明符,当使用 class 关键字时,说明符默认为 private,如果使用 struct,说明符默认为 public。

与单一继承一样,派生列表只能包含已定义的类,但此类不能定义为 final。对于派生类可以继承的基类的数量,C++没有限制。基类在给定的派生列表中只能出现一次。

多重继承的派生类从每个基类中继承状态

在多重继承下,派生类的对象包含每个基类的子对象。例如,Panda 对象有一个 Bear 部分(它本身包含一个 ZooAnimal 部分)、Endangered 类部分和 Panda 类中声明的非静态数据成员(如果有的话)。

Panda 对象概念结构

派生构造函数初始化所有基类

构造派生类型的对象时将构造并初始化其所有基类子对象。派生类型的构造函数初始化程序只能初始化其直接基类:

// explicitly initialize both base classes
Panda::Panda(std::string name, bool onExhibit) : Bear(name, onExhibit, "Panda"), Endangered(Endangered::critical) { }
// implicitly uses the Bear default constructor to initialize the Bear subobject
Panda::Panda() : Endangered(Endangered::critical) { }

构造函数初始值列表可以将实参传递给每个直接基类。基类的构造顺序取决于它们在类派生列表中出现的顺序,与它们在构造函数初始值列表中出现的顺序无关。Panda 对象初始化如下:

  • ZooAnimal 是层次结构的最终基类,是 Panda 的第一个直接基类 Bear 的基类,它首先被初始化。
  • Bear 是第一个直接基类,第二个被初始化。
  • Endangered 是第二个直接基类,第三个被初始化。
  • Panda 是最后的派生类,最后被初始化。

继承的构造函数与多重继承

在C++11标准中,派生类可以从一个或多个基类继承中其构造函数(§15.7)。但是,从多个基类继承相同的构造函数(即具有相同形参列表的构造函数)是错误的:

struct Base1 {
	Base1() = default;
	Base1(const std::string&);
	Base1(std::shared_ptr<int>);
};
struct Base2 {
	Base2() = default;
	Base2(const std::string&);
	Base2(int);
};
// error: D1 attempts to inherit D1::D1 (const string&) from both base classes
struct D1: public Base1, public Base2 {
	using Base1::Base1;  // inherit constructors from Base1
	using Base2::Base2;  // inherit constructors from Base2 
};

从多个基类继承相同的构造函数的类,必须定义该构造函数的自己版本:

struct D2: public Base1, public Base2 {
	using Base1::Base1;  //  inherit constructors from Base1
	using Base2::Base2;  //  inherit constructors from Base2
	// D2 must define its own constructor that takes a string
	D2(const string &s): Base1(s), Base2(s) { }
	D2() = default; // needed once D2 defines its own constructor
};

析构函数与多重继承

派生类中的析构函数只负责清理由该类分配的资源,派生类的成员和所有基类被自动销毁。合成析构函数有一个空函数体。

析构函数调用的顺序总是与构造函数运行顺序相反。在示例中,析构函数的调用顺序是 ~Panda,~Endangered,~Bear,~ZooAnimal。

多重继承的派生类的复制和移动操作

若多重继承的类定义了自己的复制/移动构造函数和赋值运算符,则它们必须复制、移动或赋值整个对象 (§15.7)。仅当派生类使用这些成员的合成版本时,才会自动复制、移动或赋值派生类的基类部分。在合成的复制控制成员中,每个基类都使用来自该基类的相应成员隐式地构造、赋值或销毁。

例如,假设 Panda 使用合成版本的成员,

Panda ying_yang("ying_yang");
Panda ling_ling = ying_yang;    // uses the copy constructor

那么 ling_ling 的初始化将调用 Bear 复制构造函数,而在执行 Bear 复制构造函数之前运行 ZooAnimal 复制构造函数。一旦 ling_ling 的 Bear 部分构造完成,就会运行 Endangered 复制构造函数来创建对象的此部分。最后,运行 Panda 复制构造函数。合成的移动构造函数与之类似。

合成的复制赋值运算符的行为类似于复制构造函数。它首先赋值对象的 Bear 部分 (通过 Bear 赋值 ZooAnimal 部分)。接下来,它赋值 Endangered 部分,最后是 Panda 部分。移动赋值的行为类似。

类型转换与多个基类

在单一继承下,指向派生类的指针或引用可以自动转换为指向可访问基类的指针或引用。多重继承也是如此。指向任何对象(可访问)基类的指针或引用可用于指向或引用派生对象。

// operations that take references to base classes of type Panda
void print(const Bear&);
void highlight(const Endangered&);
ostream& operator<<(ostream&, const ZooAnimal&);
Panda ying_yang("ying_yang");
print(ying_yang);     // passes Panda to a reference to Bear
highlight(ying_yang); // passes Panda to a reference to Endangered
cout << ying_yang << endl; // passes Panda to a reference to ZooAnimal

编译器不会根据派生类转换来区分基类,因为转换到每个基类同样好。

void print(const Bear&);
void print(const Endangered&);

Panda ying_yang("ying_yang");
print(ying_yang);             // error: ambiguous

基于指针类型或引用类型的查找

对象、指针或引用的静态类型决定了可以使用哪些成员。如果使用 ZooAnimal 指针,则只有该类中定义的操作可用。Panda 接口中 Bear、Panda 特有的和 Endangered 部分是看不见的。类似地,Bear 指针或引用只知道 Bear 和 ZooAnimal 成员;Endangered 指针或引用仅限于 Endangered 成员。

表18.1 ZooAnimal/Endangered 类中的虚函数

函数类自己定义的版本
printZooAnimal::ZooAnimal
Bear::Bear
Endangered::Endangered
Panda::Panda
highlightEndangered::Endangered
Panda::Panda
toesBear::Bear
Panda::Panda
cuddlePanda::Panda
析构函数ZooAnimal::ZooAnimal
Endangered::Endangered
Bear *pb = new Panda("ying_yang");
pb->print();      // ok: Panda::print()
pb->cuddle();     // error: not part of the Bear interface
pb->highlight();  // error: not part of the Bear interface
delete pb;        // ok: Panda::~Panda() 

当 Panda 通过 Endangered 指针或引用使用时,Panda 接口中 Panda 特有的和 Bear 的部分是不可见的:

Endangered *pe = new Panda("ying_yang");
pe->print();     // ok: Panda::print()
pe->toes();      // error: not part of the Endangered interface
pe->cuddle();    // error: not part of the Endangered interface
pe->highlight(); // ok: Panda::highlight()
delete pe;       // ok: Panda::~Panda()

多重继承下的类作用域

在单一继承下,派生类的作用域嵌套在其直接和间接基类的作用域中。查找是通过向上搜索继承层次结构,直到找到给定的名字。派生类中定义的名字隐藏了该名字在基类中的用法。

在多重继承下,相同的查找在所有直接基类中同时发生。如果一个名字通过多个基类找到,则该名字的使用是具有二义性的。
例,如果通过 Panda 对象、指针或引用使用一个名字,Endangered 和 Bear/ZooAnimal 子树都将被并行检查。如果在多个子树中找到该名字,则该名字的使用具有二义性。

一个类继承多个同名成员是完全合法的。但是,如果要使用该名字,必须指定要使用的版本。

⚠当一个类有多个基类时,该派生类可以从其多个基类继承同名的成员。该名字的非限定用法具有二义性。

例如,如果 ZooAnimal 和 Endangered 都定义了一个名为 max_weight 的成员,而 Panda 没有定义该成员,则下面的调用是一个错误:

double d = ying_yang.max_weight();

Panda 的派生,导致 Panda 有两个名为 max_weight 的成员,这是合法的。派生产生了潜在的二义性。
如果没有 Panda 对象调用 max_weight,这种二义性就可以避免。
如果每次对 max_weight 的调用都明确指出要运行哪个版本:ZooAnimal::max_weight 或Endangered::max_weight,也可以避免此错误。

两个继承的 max_weight 成员的二义性是相当明显的。
即使两个继承函数的形参列表不同,也会产生错误。
即使 max_weight 函数在一个类中是 private,而在另一个类中是 public 或 protected,这也是错误的。
如果 max_weight 是在 Bear 中定义的,而不是在 ZooAnimal 中定义的,调用仍然会出错。

名字查找发生在类型检查之前。当编译器在两个不同的作用域中找到 max_weight 时,它会生成一个错误,指出调用具有二义性。

避免潜在二义性的最佳方法是,在派生类中定义该函数的一个版本来解决二义性。例如,应该给 Panda 类一个 max_weight 函数来解决二义性:

double Panda::max_weight() const {
	return std::max(ZooAnimal::max_weight(), Endangered::max_weight());
}

虚继承

尽管一个类的派生列表中相同的基类只能出现一次,但一个类可以多次从同一基类继承。它可以从它自己的两个直接基类间接继承同一个基类,也可以通过它的另一个基类直接和间接地继承一个特定的类。

例如,IO库中,

basic_ios 类
istream 类
ostream 类
iostream 类

basic_ios 类负责保存流的缓冲内容,并管理流的条件状态。

默认情况下,派生对象包含其派生链中的每个类对应的单独子部分。如果同一基类在派生中出现多次,则派生对象将具有多个该类型的子对象。

这种默认情况不适用于 iostream 这样的类。iostream 对象希望使用相同的缓冲区进行读写操作,并希望其条件状态同时反映输入和输出操作。如果一个 iostream 对象包含 basic_ios 类的两个副本,那么这种共享是不可能的。

在C++中,解决这类问题可通过使用虚继承 (virtual inheritance)。虚继承允许类指定它愿意共享其基类。共享的基类子对象称为虚基类 (virtual base class)。不管相同的虚基类在继承层次结构中出现多少次,派生对象只包含该虚基类的一个共享子对象。

一个不同的 Panda 类

在过去,关于熊猫是属于浣熊科还是熊科有一些争论。为了反映这场争论,将 Panda 改为同时继承 Bear 和 Raccoon。为了避免赋予 Panda 两个 ZooAnimal 基类部分,将定义 Bear 和 Raccoon 虚继承自 ZooAnimal。

虚继承 Panda 结构层次

观察上面的新层次结构,注意到虚继承的一个非直观方面:虚派生必须在此需求出现之前完成。
例如,只有在定义 Panda 时才需要虚继承。然而,如果 Bear 和 Raccoon 没有在从 ZooAnimal 的派生中指定 virtual,Panda 类的设计者就不走运了。

实际上,中间层次的基类将其继承指定为虚继承很少引起什么问题。通常,使用虚继承的类层次结构是由一个人或一个项目设计组一次设计的。对于一个独立开发的类来说,在它的一个基类中需要一个虚基类,并且新基类的开发人员不能更改现有的层次结构,这是非常罕见的。

注:虚派生影响的类是带有虚基类的类随后派生的类;它不影响派生类本身。

使用虚基类

通过在派生列表中包含关键字 virtual 来指定基类是虚拟的:

// the order of the keywords public and virtual is not significant
class Raccoon : public virtual ZooAnimal { /* ... */ };
class Bear : virtual public ZooAnimal { /* ... */ };

上面代码使 ZooAnimal 同时是 Bear 和 Raccoon 的虚基类。

virtual 说明符表示愿意在随后的派生类中共享指定基类的单个实例。对于用作虚基类的类,没有特殊的约束。

对于从具有虚基类的类中继承,不需做任何特殊的操作:

class Panda : public Bear, public Raccoon, public Endangered { };

在此,Panda 通过 Raccoon 和 Bear 基类继承了 ZooAnimal。但是,由于这些类是从 ZooAnimal 虚继承的,Panda 只有一个 ZooAnimal 基类子部分。

支持向基类的常规类型转换

派生类的对象可以通过指向可访问的基类类型的指针或引用进行操作,不论基类是否是虚基类。例如,以下所有 Panda 基类的转换都是合法的:

void dance(const Bear&);
void rummage(const Raccoon&);
ostream& operator<<(ostream&, const ZooAnimal&);
Panda ying_yang;
dance(ying_yang);   // ok: passes Panda object as a Bear
rummage(ying_yang); // ok: passes Panda object as a Raccoon
cout << ying_yang;  // ok: passes Panda object as a ZooAnimal

虚基类成员的可见性

因为只有一个共享子对象对应于每个共享的虚基类,所以可以直接访问该基类中的成员,不会产生二义性。此外,如果虚基类中的成员仅沿一个派生路径被覆盖,则仍然可以直接访问这个被覆盖的成员。如果成员被多个基类覆盖,则派生类一般必须定义自己的版本。

例如,假设类 B 定义了一个名为 x 的成员;类 D1 和类 D2 都从 B 虚继承;类 D 继承自 D1 和 D2。在 D 的作用域中,x 通过它的两个基类都是可见的。如果通过一个 D 对象使用 x,有三种可能:

  • 如果 x 在 D1 或 D2 中都没有定义,它将被解析为 B 中的成员;没有二义性。一个 D 对象只包含一个 x 的实例。
  • 如果 x 是 D1 和 D2 其中一个类中的成员,没有二义性,派生类中的 x 版本优先于共享虚基类 B 中的 x。
  • 如果在 D1 和 D2 中都定义了 x,那么直接访问该成员具有二义性。

与非虚多重继承层次结构类似,解决这种二义性问题最好是,派生类为该成员提供自己的实例。

构造函数与虚继承

在虚派生中,虚基类由最终派生的构造函数初始化。
例如,当创建 Panda 对象时,Panda 构造函数独自控制 ZooAnimal 基类的初始化过程。

如果使用常规的初始化规则,虚基类可能会初始化多次。

当然,层次结构中的每个类在某个时候都可能是“最终派生的”对象。只要能创建从虚基类的派生的类型的独立对象,该类中的构造函数就必须初始化其虚基类。
例如,当创建一个 Bear 对象时,不涉及进一步的派生类型。在这种情况下,Bear 构造函数会直接初始化 ZooAnimal 基类:

Bear::Bear(std::string name, bool onExhibit): ZooAnimal(name, onExhibit, "Bear") { }
Raccoon::Raccoon(std::string name, bool onExhibit): ZooAnimal(name, onExhibit, "Raccoon") { }

创建 Panda 时,它是最终派生的类型,并控制共享 ZooAnimal 基类的初始化。尽管 ZooAnimal 不是 Panda 的直接基类,但 Panda 构造函数初始化 ZooAnimal:

Panda::Panda(std::string name, bool onExhibit)
      : ZooAnimal(name, onExhibit, "Panda"),
        Bear(name, onExhibit),
        Raccoon(name, onExhibit),
        Endangered(Endangered::critical),
        sleeping_flag(false)   { }

如何构造虚继承的对象

具有虚基类的对象的构造顺序与一般顺序略有不同:首先使用最终派生类的构造函数中提供的初始化程序,初始化对象的虚基类子部分。一旦对象的虚基类子部分被构造,直接基类子部分就按照它们在派生列表中出现的顺序构造。

例如,当创建一个 Panda 对象时,

  • 首先构造虚基类 ZooAmimal 部分,使用 Panda 构造函数初始值列表中指定的初始化程序。
  • 接下来构造 Bear 部分。
  • 接下来构造 Raccoon 部分。
  • 接下来构造第三个直接基类 Endangered 部分。
  • 最后,构造 Panda 部分。

如果 Panda 构造函数没有显式初始化 ZooAnimal 基类,则使用 ZooAnimal 默认构造函数。如果 ZooAnimal 没有默认构造函数,则代码出错。

🔗注:虚基类总是在非虚基类之前构造,不论它们出现在继承层次结构的何处。

构造函数与析构函数顺序

一个类可以有多个虚基类。在这种情况下,虚拟子对象按它们在派生列表中出现的顺序从左到右依次构造。
例如,在下面的 TeddyBear 派生中,有两个虚基类:直接虚基类 ToyAnimal,Bear 的虚基类 ZooAnimal:

class Character { /* ... */ };
class BookCharacter : public Character { /* ... */ };
class ToyAnimal { /* ... */ };
class TeddyBear : public BookCharacter, public Bear, public virtual ToyAnimal { /* ... */ };

为了确定是否存在虚基类,按照直接基类的声明顺序对其依次进行检查。若存在,则首先构造虚基类,然后按声明顺序依次运行非虚基类的构造函数。因此,要创建 TeddyBear,将按以下顺序调用构造函数:

ZooAnimal();        // Bear's virtual base class
ToyAnimal();        // direct virtual base class
Character();        // indirect base class of first nonvirtual base class
BookCharacter();    // first direct nonvirtual base class
Bear();             // second direct nonvirtual base class
TeddyBear();        // most derived class 

在合成的复制构造函数和合成的移动构造函数中,使用相同的顺序,在合成的赋值运算符中按此顺序对成员赋值。
对象按照与它构造相反的顺序进行销毁。TeddyBear 部分首先被销毁,而 ZooAnimal 部分最后被销毁。


【C++ Primer】目录

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值