多重继承与虚继承
2024年1月6日 2024年1月7日
多重继承 multiple inheritance
: 从多个直接基类中产生派生类的能力
多重继承与单继承相区分
多重继承的派生类继承所有基类的属性
其中一个问题: 直接基类可能拥有共同的基类
多重继承
- 多重继承的派生列表只能包含一定义过的类, 这些类不能被final标记
- 在给定的派生列表中, 同一个基类只能出现一次
1class ZooAnimal { /*...*/ }; 2class Bear : public ZooAnimal { /*...*/ }; 3class Panda : public Bear, public Endangered { /*...*/ };
Panda对象的组成 | |
---|---|
Bear子部分 | ZooAnimal成员 |
Bear成员 | |
Endangered子部分 | Endangered成员 |
Panda成员 |
派生类构造函数初始化所有基类
派生类构造函数负责初始化它的直接基类
基类的构造顺序与派生列表中基类的出现顺序保持一致 , 与派生类构造函数初始值列表中基类的顺序无关
示例
-
显式初始化所有直接基类: 在构造函数初始值列表中将直接基类委托给基类构造函数
- 先初始化Bear子对象
- 先初始化ZooAnimal子对象, 再初始化Bear成员
- 先初始化ZooAnimal子对象, 再初始化Bear成员
- 再初始化Endangered子对象
初始化Endangered成员 - 最后初始化Panda成员
1Panda::Panda(string name, bool onExhibit) 2 : Bear(name, onExhibit, "Panda"), 3 Endangered(Endangered::critical) { }
- 先初始化Bear子对象
-
默认初始化直接基类部分
- 隐式使用Bear的默认构造函数初始化Bear子对象
- 先初始化Bear子对象, 再初始化Endangered子对象
1Panda::Panda() 2 :Endangered(Endangered::critical) { }
- 隐式使用Bear的默认构造函数初始化Bear子对象
继承的构造函数与多重继承
如果派生类从多个基类中继承了相同形参列表的构造函数, 派生类必须为拥有该形参列表的构造函数给出自己的版本
示例
-
D1从Base1和Base2中都继承了D1(const string &), 编译器报错
1struct Base1 2{ 3 Base1() = default; 4 Base1(const string &); 5 Base1(shared_ptr<int>); 6}; 7 8struct Base2 9{ 10 Base2() = default; 11 Base2(const string &); 12 Base1(int); 13}; 14 15struct D1 : public Base1, public Base2 16{ 17 using Base1::Base1; 18 using Base2::Base2; 19};
-
D1需给出D1(const string &)的自定义版本
1struct D1 : public Base1, public Base2 2{ 3 using Base1::Base1; 4 using Base2::Base2; 5 6 D2(const string &s) : Base1(s), Base2(s) { } 7 D2() = default; 8};
析构函数与多重继承
- 派生类的析构函数只负责清楚派生类自己分配的资源
- 析构函数的调用顺序与构造函数相反
在Panda的例子中, 析构函数的调用顺序为:
- 先析构Panda子对象
- 再析构Endangered子对象
- 最后析构Bear子对象
- 先析构Bear子对象
- 再析构ZooAnimal子对象
- 先析构Bear子对象
- 先析构Panda子对象
多重继承的派生类: 拷贝与移动操作
-
自定义拷贝/移动操作
在完整的对象上执行拷贝/移动操作 -
使用编译器提供的
default
合成版本
default
合成版本会对基类部分执行拷贝/移动操作每个基类分别使用自己对应的成员隐式地完成构造, 赋值和销毁工作
类型转换与多个基类
我们可以令指向/绑定基类对象的指针/引用指向/绑定一个派生类对象
要求指针具有访问基类对象的权限:
- 要么派生为public
- 要么在派生类的成员函数或友元中使用指针/引用
1void print(const Bear &); 2void highlight(const Endangered &); 3 4ostream &operator<<(ostream &, const ZooAnimal &); 5Panda yangyang("yangyang"); 6 7print(yangyang); // Bear引用绑定Panda对象 8highlight(yangyang); // Endangered引用绑定Panda对象 9cout << yangyang << endl; // ZooAnimal引用绑定Panda对象
存在二义性
编译器不会比较派生类到基类的类型转换, 在它看来这些转换一样好
1void print(const Bear &); 2void print(const Endangered &); 3 4Panda yangyang("yangyang"); 5print(yangyang); // 二义性错误
动态绑定
虚函数 | 虚函数版本 |
---|---|
ZooAnimal::print | |
Bear::print | |
Endangered::print | |
Panda::print | |
highlight | Endangered::highlight |
Panda::highlight | |
toes | Bear::toes |
Panda::toes | |
cuddle | Panda::cuddle |
析构函数 | ZooAnimal::~ZooAnimal |
Endangered::~Endangered |
1Bear *pb = new Panda("yangyang"); 2pb->print(); // Panda::print 3pb->cuddle(); // 错误: Bear查找cuddle失败 4pb->highlight(); // 错误: Bear查找highlight失败 5delete pb; // Panda::~Panda 6 7Endangered *pe = dynamic_cast<Endangered *>(pb); 8pe->print(); // Panda::print 9pe->toes(); // 错误: Endangered查找toes失败 10pe->cuddle(); // 错误: Endangered查找cuddle失败 11pe->highlight(); // Panda::highlight 12delete pe; // Panda::~Panda
多重继承下的类作用域
在多重继承的情况下, 相同的查找过程在所有直接基类中同时进行. 如果名字在多个基类中都被找到, 则对该名字的使用将具有二义性
每个直接基类作为一棵子树, 程序会并行地在子树中查找名字. 如果名字在超过一棵子树中被找到, 则该名字的使用具有二义性
对于一个派生类来说, 从它的几个基类中分别继承名字相同的成员是完全合法的, 只不过在使用这个名字时必须明确指出它的版本
成员 | 所属类 |
---|---|
max_weight | ZooAnimal::max_weight |
Endangered::max_weight |
程序并行地在Endangered和Bear/ZooAnimal这两棵子树中查找名字
1double d = yangyang.max_weight(); // 存在二义性 2double d2 = yangyang.ZooAnimal::max_weight(); // 正确
示例
1#include <iostream> 2 3using namespace std; 4 5class FooBase1 6{ 7public: 8 FooBase1(int aa) : a(aa) {} 9 FooBase1() : FooBase1(0) {} 10 ~FooBase1() {} 11 12 int tt = 1; 13 14private: 15 int a; 16}; 17 18class FooBase2 19{ 20public: 21 FooBase2(int aa) : a(aa) {} 22 FooBase2() : FooBase2(0) {} 23 ~FooBase2() {} 24 25 int tt = 2; 26 27private: 28 int a; 29}; 30 31class Foo : public FooBase1, public FooBase2 32{ 33public: 34 Foo() {} 35 ~Foo() {} 36}; 37 38int main() 39{ 40 Foo f; 41 cout << f.tt << endl; // 报错: 二义性 42 cout << f.FooBase2::tt << endl; // 正确: 2 43 return 0; 44}
二义性报错
error: member 'tt' found in multiple base classes of different types cout << f.tt << endl; ^ note: member found by ambiguous name lookup int tt = 1; ^ note: member found by ambiguous name lookup int tt = 2; ^ 1 error generated.
其他同名错误
- 函数名相同, 形参列表不同时可能发生错误
参数个数相同时, 对应位置参数可以相互转换 - 成员名字相同, 但访问属性不一样时, 也会发生错误
在一个类中为私有成员, 在另一个类中非私有 - 在同一棵子树中, 同名成员在直接基类还是在间接基类中没有区别
对于具有相同形参列表的同名函数, 可以为派生类定义新版本
1double Panda::max_weight() const 2{ 3 return std::max(ZooAnimal::max_weight(), Endangered::max_weight()); 4}
虚继承
virtual inheritance
虽然类派生列表中, 同一个基类只能出现一次:
- 派生类可以通过直接基类多次继承同一个间接基类
- 派生类可以直接继承某个基类, 然后通过另一个直接基类再次间接继承该类
虚继承的目的是令某个类做出声明, 承诺愿意共享它的基类
共享的基类子对象成为虚基类 virtual base class
在这种机制下, 不论虚基类在继承体系中出现多少次, 派生类中只包含唯一一个共享的虚基类子对象
如果非虚继承, 而派生类多次继承一个基类, 则派生类中将包含该类的多个子对象
虚继承在类派生列表中使用 virtual
关键字, 与派生访问说明符的顺序不作要求
举例
- istream和ostream均继承了名为base_ios的抽象基类
- iostream直接继承istream和ostream, 即继承了base_ios两次
示例
Panda属于Raccoon科还是Bear科是一个有争论的话题. 令Panda同时继承Bear和Raccoon.
为了避免Panda有两个ZooAnimal子对象, 使Raccoon和Bear虚继承ZooAnimal
1class Raccoon : public virtual ZooAnimal { /*...*/ }; 2class Bear virtual public ZooAnimal { /*...*/ }; 3 4class Panda : public Bear, public Raccoon, public Endangered { /*...*/ };
Panda中只有一个ZooAnimal基类部分
虚继承并不直观
必须在虚派生的真实需求出现前完成虚派生的操作:
- 定义Panda时才有虚派生的需求
- 定义Raccoon和Bear时已完成虚派生操作
虚派生只影响从指定了虚基类的派生类中进一步派生出的类, 它不会影响派生类本身
在实际编程过程中, 位于中间层次的基类将其继承声明为虚继承一般不会带来什么问题
支持向基类的常规类型转换
虚基类成员的可见性
不考虑更复杂的情况, 除非出现相应需求
-
通过D对象使用x, x被解析为B的成员
1class B 2{ 3public: 4 int x; 5}; 6 7class D1 : public virtual B { /*...*/ }; 8class D2 : public virtual B { /*...*/ }; 9class D : public D1, public D2 { /*...*/ };
-
通过D对象使用x, 派生类的x比共享虚基类B的x优先级更高
1class B 2{ 3public: 4 int x = 1; 5}; 6 7class D1 : public virtual B { /*...*/ }; 8class D2 : public virtual B { 9public: 10 int x = 2; 11}; 12class D : public D1, public D2 { /*...*/ }; 13 14D d; 15cout << d.x << endl; // 2
-
通过D对象使用x, 存在二义性
1class B 2{ 3public: 4 int x = 1; 5}; 6 7class D1 : public virtual B { 8public: 9 int x = 3; 10}; 11class D2 : public virtual B { 12public: 13 int x = 2; 14}; 15class D : public D1, public D2 { /*...*/ }; 16 17D d; 18cout << d.x << endl; // 错误: 存在二义性 19cout << d.D1::x << endl; // 3