一、多重继承概念
  • 多重继承是指:从多个直接基类而产生派生类的能力
  • 例如:
class ZooAnimal {}; //动物

class Endangered {}; //濒临灭绝的动物

class Bear :public ZooAnimal {};  //熊

//多重继承
class Panda :public Bear, public Endangered {}; //熊猫

C++:93---类继承(多重继承)_构造函数

二、多重继承的语法
  • 继承的每个基类都要有一个可选的访问说明符(public、protected、private)。如果没有的话:对于class来说默认为private,对于struct来说默认为public
  • 继承的基类必须在该类定义之前被声明定义过
  • 继承的基类不能使final的(final参阅:javascript:void(0)
  • 能够继承的基类个数没有限制,但是同一基类只能继承一次
三、构造函数的初始化顺序
  • 构造基类的顺序与派生列表中基类的出现顺序有关,而与构造函数初始化列表中基类的初始化顺序无关
  • 派生类构造自己之前同样需要构造基类对象。例如:
class Panda :public Bear, public Endangered {
public:
    //构造所有基类
    Panda(std::string name, bool onExhibit)
        :Bear(name, onExhibit, "Panda"),
         Endangered(Endangered::cirtical)
    {
    }

    //使用Bear的默认构造函数初始化Bear对象
    Panda::Panda()
        : Endangered(Endangered::cirtical)
    {
    }
};
  • 对于上面的Panda构造函数,其执行顺序为:ZooAnimal->Bear->Endangered->Panda
四、析构函数
  • 派生类的析构同样需要执行基类的虚构函数
  • 析构函数的执行顺序与继承的顺序相反
  • 对于上面的Panda析构函数,其执行顺序为:Panda->Endangered->Bear->ZooAnimal
五、继承的构造函数与多重继承
  • “继承的构造函数”我们在前一篇文章介绍过:javascript:void(0)
  • 继承的构造函数是使用using从继承基类的构造函数的概念
  • 在C++11标准中,允许派生类从它的一个或几个基类中继承构造函数。但是如果从多个基类中继承了相同的构造函数(相同是指参数列表完全相同),则程序会产生错误

演示案例

struct Base1 {
    Base1() = default;
    Base1(const std::string&);
    Base1(std::shared_ptr<int>);
};

struct Base2 {
    Base2() = default;
    Base2(const std::string&);
    Base2(int);
};

//多重继承
struct D1 :public Base1, public Base2 {
    //使用using继承基类中的构造函数
    using Base1::Base1;
    using Base2::Base2; //会产生错误
};
  • 上面的D1从Base1和Base2中继承了所有的构造函数,但是Base1与Base2中都有一个参数为“const std::string&”的构造函数,因此编译器产生错误

C++:93---类继承(多重继承)_二义性_02

  • 为了解决上面的错误,必须自己显式地在本类中定义可能会产生二义性的构造函数,这种方法就是我们所说的覆盖。见下面的演示案例

演示案例

struct Base1 {
    Base1() = default;
    Base1(const std::string&);
    Base1(std::shared_ptr<int>);
};

struct Base2 {
    Base2() = default;
    Base2(const std::string&);
    Base2(int);
};

struct D1 :public Base1, public Base2 {
    using Base1::Base1;
    using Base2::Base2;
    
    //覆盖两个基类的const std::string&参数构造函数版本
    D1(const std::string &s):Base1(s), Base2(s){}
    D1() = default; //一旦定义了自己的构造函数,则必须出现
};
六、多重继承的派生类的拷贝与移动操作

使用非合成版本

  • 与单一继承的原理一致,多重继承的派生类如果定义了自己的拷贝/赋值构造函数和赋值运算符,则必须在完整的对象上执行拷贝、移动、赋值操作(也就是说建议要拷贝、移动、赋值属于基类的部分数据)

使用合成版本

  • 如果派生类没有定义自己的拷贝/赋值构造函数和赋值运算符,那么在执行这些操作时将会自动调用基类的拷贝/赋值构造函数和赋值运算符
七、基类与派生类的类型转换
  • 与单一继承原理一致,可以将一个派生类赋值给一个基类,也可以将一个基类指针/引用指向于一个派生类
  • 例如:
class ZooAnimal {};

class Endangered {};

class Bear :public ZooAnimal {};

class Panda :public Bear, public Endangered {};

void print(const Bear&);
void highlight(const Endangered&);
ostream& operator<<(ostream&, const ZooAnimal&);

int main()
{
    Panda ying_yang("ying_yang");

    print(ying_yang);         //将一个Panda对象传递给一个Bear引用
    highlight(ying_yang);     //将一个Panda对象传递给一个Endangered引用
    cout << ying_yang << endl;//将一个Panda对象传递给一个ZooAnimal引用
	
    return 0;
}

注意函数重载与二义性错误

  • 编译器不会在派生类向基类的转换中进行比较和选择,因为在它看来转换到任意一种基类都一样。但是需要注意二义性的问题:
class ZooAnimal {};

class Endangered {};

class Bear :public ZooAnimal {};

class Panda :public Bear, public Endangered {};

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

int main()
{
    Panda ying_yang("ying_yang");
    print(ying_yang);//产生二义性
	
    return 0;
}

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

  • 与单一继承原理一致,对象、引用、指针的静态类型决定了我们能够使用哪些成员
  • 例如:
    • 我们使用一个ZooAnimal指针指向于一个派生类,那么只能通过这个指针访问属于ZooAnimal的数据成员/方法,而不能调用属于基类的数据成员/方法
    • 我们使用一个Bear指针指向于Panda对象,则只能通过这个指针访问属于Bear以及ZooAnimal的成员,不能访问Panda的数据成员/方法和Endangered的数据成员/方法

演示案例:

class ZooAnimal {
public:
    virtual void print();  //1
    virtual ~ZooAnimal();
};

class Endangered {
public:
    virtual void print();     //1
    virtual void highlight(); //2
    virtual ~Endangered();
};

class Bear :public ZooAnimal {
public:
    virtual void print();  //1
    virtual void toes();   //3
};

class Panda :public Bear, public Endangered {
public:
    virtual void print();     //1
    virtual void highlight(); //2
    virtual void toes();      //3
    virtual void cuddle();    //4
};
  • 现在我们有下面的调用:

int main()
{
    Bear *pb = new Panda("ying_yang");
    
    pb->print();     //正确,调用Panda::print()
    pb->cuddle();    //错误,不属于Bear接口
    pb->highlight(); //错误,不属于Bear接口
    
    delete pb;       //正确,调用Panda::~Panda()
    
    return 0;
}
  • 现在我们有下面的调用:
int main()
{
    Endangered *pb = new Panda("ying_yang");

    pb->print();    //正确,调用Panda::print()
    pb->toes();     //错误,不属于Endangered的接口
    pb->cuddle();   //错误,不属于Endangered的接口
    pb->highlight();//正确,调用Panda::highlight

    delete pb;      //正确,调用Panda::~Panda()

    return 0;
}
八、多重继承下的类作用域
  • 在单一继承下我们说过,派生类的作用域嵌套在直接基类或间接基类的作用域中,也就是说当我们查找一个数据成员/方法时,在派生类中不存在,那么就继续向基类中进行查找,如果查找到了就进行使用
  • 多重继承下派生类的作用域嵌套在所有的基类或间接基类的作用域中

二义性与二义性的解决

  • 当同一个数据成员/函数的名称在不同的基类中出现时,程序不会出现错误(编译器允许定义)。但是如果我们通过派生类对同名的数据成员/函数进行调用,那么就会触发二义性
class A {
public:
    int num;
};

class B {
public:
    int num;
};

//允许多重继承
class C :public A, public B {};

int main()
{
    C c;
    c.num;   //错误,对num地调用产生二义性
    return 0;
}
  • 如果派生类对可能产生二义性的数据成员/函数进行覆盖的话,那么调用就不会产生二义性了。例如:
class A {
public:
    int num;
};

class B {
public:
    int num;
};

class C :public A, public B {
public:
    int num; //覆盖
};

int main()
{
    C c;
    c.num; //正确
    return 0;
}
  • 当然我们也可以在不覆盖的情况下,通过作用域限定符来访问调用哪一版本的数据成员/方法
class A {
public:
    int num;
};

class B {
public:
    int num;
};

class C :public A, public B {};

int main()
{
    C c;
    c.A::num; //调用A中的num
    c.B::num; //调用B中的num
    return 0;
}
  • 当然,我们也可以设计一个函数,用访问访问特定的版本
class A {
protected:
    int num; //不能是private的,否则派生类不可访问
};

class B {
protected:
    int num; //不能是private的,否则派生类不可访问
};

class C :public A, public B {
    int max_numconst()const{
        return std::max(A::num, B::num);
    }
};
  • 注意事项:
    • 有时即使派生类继承的两个函数形参列表不同也可能会发生错误
    • 另外,同名的数据成员/函数,在不同的基类中访问权限不同也可能会发生错误(例如一个数据成员在基类1中是private的,在基类2中是protected的,也会发生错误)