C++多继承,虚继承部分总结与示例

 
Category: C_C++

写在前面

写一下多继承, 虚继承的一些部分, 包括一些例子.

参考cppprimer

多继承

简介

多继承是指从多个直接基类中产生派生类的能力. 多继承的派生类继承了所有父类的属性, 所以会带来一些复杂的问题.

示例1: 多继承用法与调用顺序

#include <string>
#include <iostream>
using namespace std;

class ZooAnimal {
public:
    ZooAnimal() { cout << "call ZooAnimal::ZooAnimal()\n"; }
    ~ZooAnimal() { cout << "call ZooAnimal::~ZooAnimal()\n"; }
};

class Endangered {
public:
    Endangered() { cout << "call Endangered::Endangered()\n"; }
    Endangered(int a) : m_a(a) { cout << "call Endangered::Endangered(int)\n"; }
    ~Endangered() { cout << "call Endangered::~Endangered()\n"; }

    static int critical;

private:
    int m_a;
};
int Endangered::critical = 10;

class Bear : public ZooAnimal {
public:
    Bear() { cout << "call Bear::Bear()\n"; }
    Bear(string, bool, string);
    ~Bear() { cout << "call Bear::~Bear()\n"; }
};
Bear::Bear(string name, bool onExhibit, string detail) {
    cout << "call Bear::Bear(string, bool, string)\n";
}

// multi inherit
class Panda : public Bear, public Endangered {
public:
    Panda();
    Panda(string, bool);
    ~Panda() { cout << "call Panda::~Panda()\n"; }
};

Panda::Panda(string name, bool onExhibit)
    : Bear(name, onExhibit, "Panda"), Endangered(Endangered::critical) {
    cout << "call Panda::Panda(string, bool)\n";
}
Panda::Panda() : Endangered(Endangered::critical) {
    cout << "call Panda::Panda()\n";
}

void t1() {
    //
    Panda p1;
    /* call ZooAnimal::ZooAnimal() */
    /* call Bear::Bear() */
    /* call Endangered::Endangered(int) */
    /* call Panda::Panda() */
    /* call Panda::~Panda() */
    /* call Endangered::~Endangered() */
    /* call Bear::~Bear() */
    /* call ZooAnimal::~ZooAnimal() */
}

int main(int argc, char *argv[]) {
    t1();
    return 0;
}

通过输出可以看出, 首先调用最终基类, 然后调用直接基类, 然后是第二基类, 最后是子类.

析构顺序正好相反(由子类至基类).

示例2: 多继承构造函数可能出现的问题

#include <iostream>
#include <memory>
#include <string>
using namespace std;

struct Base1 {
    Base1() { cout << "Base1()\n"; }
    Base1(const string &) { cout << "Base1(const string&) \n"; }
    Base1(std::shared_ptr<int>);
};

struct Base2 {
    Base2() { cout << "Base2()\n"; }
    Base2(const string &) { cout << "Base2(const string&) \n"; }
    Base2(int a) { cout << "Base2(int)\n"; }
};

struct D1 : public Base1, public Base2 {
    using Base1::Base1;
    using Base2::Base2;
    // 定义自己版本的构造函数
    D1(const string &) { cout << "D1(const string &)\n"; }
};

void t1() {
    D1 d1(1);
    /* Base1() */
    /* Base2(int) */
}
void t2() {
    D1 d2("abc");
    /* error: call of overloaded 'D1(const char [4])' is ambiguous */
    /* Base1() */
    /* Base2() */
    /* D1(const string &) */
}

int main(int argc, char *argv[]) {
    // test
    /* t1(); */
    t2();

    return 0;
}

多继承引发的类型转换问题

在只有一个基类的情况下, 派生类的指针或引用能自动转换成一个可访问基类的指针或引用, 多基类情况类似, 可以令某个可访问基类的指针或引用直接指向一个派生类对象. 如下所示:(仍然采用上面的Panda例子)

void print(const Bear &) { cout << "call print(const Bear&)\n"; }
void highlight(const Endangered &) {
    cout << "call highlight(const Endangered&)\n";
}
ostream &operator<<(ostream &os, const ZooAnimal &) {
    os << "call operator<< (ZooAnimal)\n";
    return os;
}
// 如果解注释, 会导致错误
/* error: call of overloaded 'print(Panda&)' is ambiguous */
/* void print(const Endangered &) { cout << "call print(const Endangered&)\n"; } */
void t2() {
    Panda aa("aa", true);
    print(aa);
    highlight(aa);
    cout << aa << endl;
    /* call print(const Bear&) */
    /* call highlight(const Endangered&) */
    /* call operator<< (ZooAnimal) */
}

上面如果为同一函数的参数作两个基类的重载, 那么就会导致二义性错误.

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

与只有一个基类的继承一样, 对象/指针/引用的静态类型决定了能使用的成员.

例如: 如果使用一个ZooAnimal指针, 则只有定义在ZooAnimal中的操作可以调用, 而Pandas中的其他特有部分(其他基类, Bear,Panda,Endangered)都不可见.

class ZooAnimal {
public:
    ZooAnimal() { cout << "call ZooAnimal::ZooAnimal()\n"; }
    void print() { cout << "call ZooAnimal::print()\n"; }
    ~ZooAnimal() { cout << "call ZooAnimal::~ZooAnimal()\n"; }
};

void t3() {
    Bear *pb = new Panda("aa", true);
    pb->print();
    delete pb;
    /* call ZooAnimal::ZooAnimal() */
    /* call Bear::Bear(string, bool, string) */
    /* call Endangered::Endangered(int) */
    /* call Panda::Panda(string, bool) */
    /* call ZooAnimal::print() */
    /* call Bear::~Bear() */
    /* call ZooAnimal::~ZooAnimal() */
}

多继承下的类作用域

在只有一个基类的情况下, 派生类的作用域嵌套在直接基类和间接基类的作用域中. 查找过程沿着继承体系自底向上进行, 直到找到所需的名字. 派生类的名字将隐藏基类的同名成员.

多继承中, 相同的查找过程在所有直接基类中同时进行, 若同一名字在多个基类中找到, 则二义性错误.

所以, 需要显式指明作用域, 或者定义新版本的不同名内容.

虚继承

派生列表中同一基类只能出现一次, 但是实际上派生类可以多次继承同一个类, 派生类可以通过它的两个直接基类分别继承同一个间接基类, 也可以直接继承某个基类, 然后通过另一个基类再一次间接继承该类.

默认情况下, 派生类中含有继承链上的每一个类对应的子部分, 如果某个类在派生过程中出现多次, 则派生类中将包含该类的多个子对象.

虚继承的出现就是用于解决多继承中存在的基类多次使用问题的.

其目的是: 令某一个类作出声明, 承诺愿意共享基类, 其中, 共享的基类子对象称为虚基类.

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

  • 必须在虚派生的真实需求出现之前完成派生操作.
  • 虚派生只会影响从指定了虚基类的派生类中进一步派生出的类, 不会影响派生类本身.
  • 使用virtual说明符(在public之前或者之后都可以)表明了: 在后续的派生类中共享虚基类的同一份实例, 但是并没有规定什么样的类能够作为虚基类.

示例: Panda

#include <string>
#include <iostream>
using namespace std;

class ZooAnimal {
public:
    ZooAnimal() {}
    void print() { cout << "call ZooAnimal::print()\n"; }
    ~ZooAnimal() {}
};

class Endangered {
public:
    Endangered() {}
    Endangered(int a) : m_a(a) {}
    ~Endangered() {}

    static int critical;

private:
    int m_a;
};
int Endangered::critical = 10;

class Raccoon : virtual public ZooAnimal {
public:
    Raccoon() {}
    Raccoon(string, bool, string);
    ~Raccoon() {}
};
Raccoon::Raccoon(string name, bool onExhibit, string detail) {}

class Bear : virtual public ZooAnimal {
public:
    Bear() {}
    Bear(string, bool, string);
    ~Bear() {}
};
Bear::Bear(string name, bool onExhibit, string detail) {}

// multi inherit
class Panda : public Bear, public Raccoon, public Endangered {
public:
    Panda();
    Panda(string, bool);
    ~Panda() {}
};

Panda::Panda(string name, bool onExhibit)
    : Bear(name, onExhibit, "Panda"), Endangered(Endangered::critical) {}
Panda::Panda() : Endangered(Endangered::critical) {}

void t1() {
    Panda a;
    /* call ZooAnimal::ZooAnimal() */
    /* call Bear::Bear() */
    /* call Raccoon::Raccoon() */
    /* call Endangered::Endangered(int) */
    /* call Panda::Panda() */
    /* call Panda::~Panda() */
    /* call Endangered::~Endangered() */
    /* call Raccoon::~Raccoon() */
    /* call Bear::~Bear() */
    /* call ZooAnimal::~ZooAnimal() */
}

虚派生中, 虚基类是由最底层的派生类初始化的, 以上面的程序为例, 当创建Panda对象时, 由Panda构造函数独自控制ZooAnimal的初始化过程.

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

与非虚基类一样, 派生类对象也可以被可访问基类的指针或引用操作.

void dance(const Bear &) { cout << "call dance(const Bear&)\n"; }
void rummage(const Raccoon &) { cout << "call rummage(const Raccoon&)\n"; }
ostream &operator<<(ostream &os, const ZooAnimal &) {
    os << "call operator<< (ZooAnimal)\n";
    return os;
}

void t2() {
    Panda a;
    dance(a);
    rummage(a);
    cout << a;
    /* call dance(const Bear&) */
    /* call rummage(const Raccoon&) */
    /* call operator<< (ZooAnimal) */
}

虚基类成员的可见性

因为在每一个共享的虚基类中只有唯一一个共享的子对象, 所以该基类的成员可以被直接访问, 并且不会产生二义性.

如果虚基类的成员只被一条派生路径覆盖, 则我们仍可以直接访问这个被覆盖的成员, 但是如果成员被多于一个的基类覆盖(例子: 菱形继承), 派生类就必须为该成员自定义一个新版本.

虚继承对象的构造方式

  • 首先使用提供给最底层派生类构造函数的初始值初始化该对象的虚基类子部分;
  • 接下来按照直接基类在派生列表中出现的次序依次对其初始化.

例子中的顺序:

  1. 使用Panda的构造函数初始值列表中提供的初始值构造虚基类ZooAnimal部分
  2. 构造Bear
  3. 构造Raccoon
  4. 构造Endangered
  5. 构造Panda

如果Panda没有显式初始化ZooAnimal基类, 则调用ZooAnimal的默认构造函数, 所以此时一定要有默认构造函数.