面向对象程序设计Ⅲ

1、多重继承:

  • 多重继承(multiple inheritance):指从多个直接基类中产生派生类的能力,多重继承的派生类继承了所有父类的属性。

  • 派生类的派生列表中可以包含多个基类。派生列表只能包含已经被定义过的类,且这些类不是final的,在某个给定的派生列表中,同一个基类只能出现一次。

  • 多重继承的派生类从每个基类中继承状态: 派生类的对象包含有每个基类的子对象。

  • 派生类构造函数初始化所有基类: 构造一个派生类对象将同时构造并初始化所有基类子对象,多重继承的派生类构造函数只能初始化它的直接基类。基类的构造顺序同派生列表中基类的出现顺序保持一致,与派生类构造函数初始值列表中基类的顺序无关。

  • class Bear : public ZooAnimal{};class Panda : public Bear, public Endangered{}; // 派生列表。
  • 继承的构造函数与多重继承:

    • 允许派生类从它的一个或几个基类中继承构造函数,如果从多个基类中继承了相同的构造函数(形参列表完全相同)则出现出错。

    • struct Base1{    Base1() = default;    Base1(const std::string&);    Base1(std::shared_ptr<int>);};struct Base2{    Base2() = default;    Base2(const std::string&);    Base2(int);}// D1从两个基类中都继承D1::D1(const string&)出错struct D1 : public Base1, public Base2{    using Base1 :: Base1; // 从Base1继承构造函数    using Base2 :: Base2;// 从Base2继承构造函数}
    • 如果从多个基类中继承相同的构造函数,则这个类必须为该构造函数定义自己的版本。

    • struct D2 : public Base1, public Base2{    using Base1 :: Base1; // 从Base1继承构造函数    using Base2 :: Base2;// 从Base2继承构造函数    // D2自定义一个接受string的构造函数    D2(const string & s) : Base1(s), Base2(s){};    // 默认、拷贝和移动构造函数不会被继承,这些构造函数按照规则被合成,由于这里自定义了接受了string的构造函数,所以需要显示的写出默认构造函数。编译器不会再为D2隐式合成默认构造函数了。    D2() = default; }
  • 多重继承的派生类的拷贝和移动操作:

    • 多重继承的派生类如果定义了自己的拷贝/赋值构造函数和赋值运算符,必须再完整的对象上执行拷贝、移动与赋值操作。只有当派生类使用合成版本的拷贝、移动和赋值成员时,才会自动对其基类部分执行这些操作。
  • 类型转换与多个基类:

    • 可以令某个可访问基类的指针或引用直接指向一个派生类对象。编译器不会在派生类向基类的集中转换中进行比较和选择,如果几个基类都定义一个重载的函数,如果不使用带有前缀限定符的重载函数会出现二义性错误。

    • void print(const Bear&);void print(const Endangered&);// Panda类继承了Bear, Endangered类Panda ying_yang("ying_yang");print(ying_yang); // 二义性错误。
    • 指针、引用,对象的静态类型决定了可以使用那些成员,如果使用基类A指针,则只能调用基类中定义的操作。不能调用派生类从其他基类中继承来的操作。调用的还是派生类自定义的版本。

    • Bear *pb = new Panda("ying_yang");pb->print(); // 正确, Panda::print()pb->cuddle(); // 错误, Bear中未定义cuddle函数pb->higlight(); // 错误, Bear中未定义higlight函数delete pb; // 正确, 调用Panda::~Panda()

  • 多重继承下的类作用域:

    • 仅一个基类的情况下,派生类作用域嵌套在直接基类和间接基类的作用域中,查找过程沿着继承体系自底向上进行,直到找到所需名字,派生类的名字将隐藏基类的同名成员。
    • 多继承中相同的查找过程在所有直接基类中同时进行,若名字在多个基类中被找到,则该名字的使用将具有二义性。
    • 对派生类来说,它从几个基类中分别继承名字相同的成员完全合法,但是使用 时必须明确指出它的版本。不加前缀限定符将引发二义性。
    • 要想避免潜在的二义性,最好在派生类中为该函数定义一个新的版本。

2、虚继承:

  • 默认情况下,派生类中含有继承链上每个类对于的子部分,若某个类在派生中出现了多次,则派生类中将包含该类的多个子对象。

  • 派生类可以多次继承同一个类,还会产生菱形继承的问题。

  • 菱形继承:

    • 两个派生类继承同一个基类

    • 又有某个类同时继承者两个派生类

    • 菱形继承问题:

      • 羊继承了动物的数据,驼同样继承了动物的数据,当草泥马使用数据时,就会产生二义性。
      • 草泥马继承自动物的数据继承了两份,其实我们应该清楚,这份数据我们只需要一份就可以。
  • C++通过 虚继承 机制解决上述问题。目的是令某个类做出声明,承诺愿意共享它的基类,共享的基类子对象称为虚基类,无论虚基类在继承体系中出现了多少次,派生类中都只包含唯一一个共享的虚基类子对象。

  • 问题:必须在虚派生的真实需求出现前就已经完成虚派生的操作。

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

  • 使用虚基类:在派生列表中添加关键字virtual,愿景:后续的派生类中共享虚基类的同一份实例。

  • 无论基类是不是虚基类,派生类对象都能被可访问基类的指针或引用操作。

  • 虚基类成员的可见性:假设基类B中定义了成员x,D1,D2从B虚继承得到,D继承了D1,D2,通过D的对象使用x有三种可能:

    • 若D1,D2中没有x的定义,则x被解析为B的成员,不存在二义性。
    • 若x是B自定义的成员,同时是D1和D2中某一个的成员,同样没有二义性,因派生类的x比共享虚基类B的x优先级高。
    • 若D1和D2中都有x的定义,则直接访问x会产生二义性。
    • 解决二义性的方法是在派生类中为成员自定义新的实例。
  • 虚基类由最低层的派生类初始化。

  • 虚继承的对象的构造方式:首先使用提供给最底层派生类构造函数的初始值初始化该对象的虚基类子部分,然后按照基类在派生列表中出现的次序对其进行初始化。

  • 虚基类总是先于非虚基类构造,与它在继承体系中的次序和位置无关。

  • 构造函数与析构函数的次序:

    • 一个类可以由多个虚基类,虚的子对象按照他们在派生列表中出现的属性从左到右依次构造,编译器按照基类的声明顺序对其依次坚持,以缺点其中是否含有虚基类,如果有,则先构造虚基类,然后按照声明顺序逐一构造其他非虚基类。