六一的部落格


关关难过关关过,前路漫漫亦灿灿。



多重继承 multiple inheritance : 从多个直接基类中产生派生类的能力

多重继承与单继承相区分

多重继承的派生类继承所有基类的属性

其中一个问题: 直接基类可能拥有共同的基类


多重继承

  1. 多重继承的派生列表只能包含一定义过的类, 这些类不能被final标记
  2. 在给定的派生列表中, 同一个基类只能出现一次
1class ZooAnimal { /*...*/ };
2class Bear : public ZooAnimal { /*...*/ };
3class Panda : public Bear, public Endangered { /*...*/ };
Panda对象的组成
Bear子部分 ZooAnimal成员
Bear成员
Endangered子部分 Endangered成员
Panda成员

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

派生类构造函数负责初始化它的直接基类

基类的构造顺序与派生列表中基类的出现顺序保持一致 , 与派生类构造函数初始值列表中基类的顺序无关


示例

  1. 显式初始化所有直接基类: 在构造函数初始值列表中将直接基类委托给基类构造函数

    • 先初始化Bear子对象
      • 先初始化ZooAnimal子对象, 再初始化Bear成员
    • 再初始化Endangered子对象

      初始化Endangered成员
    • 最后初始化Panda成员
    1Panda::Panda(string name, bool onExhibit)
    2    : Bear(name, onExhibit, "Panda"),
    3      Endangered(Endangered::critical) { }
  2. 默认初始化直接基类部分

    • 隐式使用Bear的默认构造函数初始化Bear子对象
    • 先初始化Bear子对象, 再初始化Endangered子对象
    1Panda::Panda()
    2    :Endangered(Endangered::critical) { }

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

如果派生类从多个基类中继承了相同形参列表的构造函数, 派生类必须为拥有该形参列表的构造函数给出自己的版本


示例

  1. 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};
  2. 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};

析构函数与多重继承

  1. 派生类的析构函数只负责清楚派生类自己分配的资源
  2. 析构函数的调用顺序与构造函数相反

    在Panda的例子中, 析构函数的调用顺序为:
    • 先析构Panda子对象
    • 再析构Endangered子对象
    • 最后析构Bear子对象
      • 先析构Bear子对象
      • 再析构ZooAnimal子对象

多重继承的派生类: 拷贝与移动操作

  1. 自定义拷贝/移动操作

    在完整的对象上执行拷贝/移动操作

  2. 使用编译器提供的 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);              // 二义性错误

动态绑定

虚函数 虚函数版本
print 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.

其他同名错误

  1. 函数名相同, 形参列表不同时可能发生错误

    参数个数相同时, 对应位置参数可以相互转换
  2. 成员名字相同, 但访问属性不一样时, 也会发生错误

    在一个类中为私有成员, 在另一个类中非私有
  3. 在同一棵子树中, 同名成员在直接基类还是在间接基类中没有区别

对于具有相同形参列表的同名函数, 可以为派生类定义新版本

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

虚继承

virtual inheritance

虽然类派生列表中, 同一个基类只能出现一次:

  • 派生类可以通过直接基类多次继承同一个间接基类
  • 派生类可以直接继承某个基类, 然后通过另一个直接基类再次间接继承该类

虚继承的目的是令某个类做出声明, 承诺愿意共享它的基类

共享的基类子对象成为虚基类 virtual base class

在这种机制下, 不论虚基类在继承体系中出现多少次, 派生类中只包含唯一一个共享的虚基类子对象

如果非虚继承, 而派生类多次继承一个基类, 则派生类中将包含该类的多个子对象

虚继承在类派生列表中使用 virtual 关键字, 与派生访问说明符的顺序不作要求


举例

  1. istream和ostream均继承了名为base_ios的抽象基类
  2. 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时已完成虚派生操作

虚派生只影响从指定了虚基类的派生类中进一步派生出的类, 它不会影响派生类本身

在实际编程过程中, 位于中间层次的基类将其继承声明为虚继承一般不会带来什么问题


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


虚基类成员的可见性

不考虑更复杂的情况, 除非出现相应需求

  1. 通过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 { /*...*/ };
  2. 通过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
    
  3. 通过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
    

多重继承与虚继承


多重继承 multiple inheritance : 从多个直接基类中产生派生类的能力

多重继承与单继承相区分

多重继承的派生类继承所有基类的属性

其中一个问题: 直接基类可能拥有共同的基类


多重继承

  1. 多重继承的派生列表只能包含一定义过的类, 这些类不能被final标记
  2. 在给定的派生列表中, 同一个基类只能出现一次
1class ZooAnimal { /*...*/ };
2class Bear : public ZooAnimal { /*...*/ };
3class Panda : public Bear, public Endangered { /*...*/ };
Panda对象的组成
Bear子部分 ZooAnimal成员
Bear成员
Endangered子部分 Endangered成员
Panda成员

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

派生类构造函数负责初始化它的直接基类

基类的构造顺序与派生列表中基类的出现顺序保持一致 , 与派生类构造函数初始值列表中基类的顺序无关


示例

  1. 显式初始化所有直接基类: 在构造函数初始值列表中将直接基类委托给基类构造函数

    • 先初始化Bear子对象
      • 先初始化ZooAnimal子对象, 再初始化Bear成员
    • 再初始化Endangered子对象

      初始化Endangered成员
    • 最后初始化Panda成员
    1Panda::Panda(string name, bool onExhibit)
    2    : Bear(name, onExhibit, "Panda"),
    3      Endangered(Endangered::critical) { }
  2. 默认初始化直接基类部分

    • 隐式使用Bear的默认构造函数初始化Bear子对象
    • 先初始化Bear子对象, 再初始化Endangered子对象
    1Panda::Panda()
    2    :Endangered(Endangered::critical) { }

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

如果派生类从多个基类中继承了相同形参列表的构造函数, 派生类必须为拥有该形参列表的构造函数给出自己的版本


示例

  1. 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};
  2. 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};

析构函数与多重继承

  1. 派生类的析构函数只负责清楚派生类自己分配的资源
  2. 析构函数的调用顺序与构造函数相反

    在Panda的例子中, 析构函数的调用顺序为:
    • 先析构Panda子对象
    • 再析构Endangered子对象
    • 最后析构Bear子对象
      • 先析构Bear子对象
      • 再析构ZooAnimal子对象

多重继承的派生类: 拷贝与移动操作

  1. 自定义拷贝/移动操作

    在完整的对象上执行拷贝/移动操作

  2. 使用编译器提供的 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);              // 二义性错误

动态绑定

虚函数 虚函数版本
print 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.

其他同名错误

  1. 函数名相同, 形参列表不同时可能发生错误

    参数个数相同时, 对应位置参数可以相互转换
  2. 成员名字相同, 但访问属性不一样时, 也会发生错误

    在一个类中为私有成员, 在另一个类中非私有
  3. 在同一棵子树中, 同名成员在直接基类还是在间接基类中没有区别

对于具有相同形参列表的同名函数, 可以为派生类定义新版本

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

虚继承

virtual inheritance

虽然类派生列表中, 同一个基类只能出现一次:

  • 派生类可以通过直接基类多次继承同一个间接基类
  • 派生类可以直接继承某个基类, 然后通过另一个直接基类再次间接继承该类

虚继承的目的是令某个类做出声明, 承诺愿意共享它的基类

共享的基类子对象成为虚基类 virtual base class

在这种机制下, 不论虚基类在继承体系中出现多少次, 派生类中只包含唯一一个共享的虚基类子对象

如果非虚继承, 而派生类多次继承一个基类, 则派生类中将包含该类的多个子对象

虚继承在类派生列表中使用 virtual 关键字, 与派生访问说明符的顺序不作要求


举例

  1. istream和ostream均继承了名为base_ios的抽象基类
  2. 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时已完成虚派生操作

虚派生只影响从指定了虚基类的派生类中进一步派生出的类, 它不会影响派生类本身

在实际编程过程中, 位于中间层次的基类将其继承声明为虚继承一般不会带来什么问题


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


虚基类成员的可见性

不考虑更复杂的情况, 除非出现相应需求

  1. 通过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 { /*...*/ };
  2. 通过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
    
  3. 通过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