多重继承与虚继承

参考书籍:《C++ Primer》、《Effective C++

 

多重继承与虚继承

一、     多重继承

1.定义多个类:

多重继承的Panda层次

 

定义一个抽象 ZooAnimal 类保存所有动物园动物公共信息并提供公用接口,Bear 类将包含 Bear 科的独特信息,以此类推。

除了实际的动物园动物类的之外,还有一些辅助类封装不同的抽象,如濒临灭绝的动物。例如,在 Panda 类的实现中,Panda 同时从 Bear Endangered 派生。

 

为了支持多重继承,扩充派生列表

       class Bear : public ZooAnimal {

}

 

       以支持由逗号分隔的基类列表:

class Panda : public Bear, public Endangered {

    };

 

       派生类为每个基类(显式或隐式地)指定了访问级别——publicprotected 

private。像单继承一样,只有在定义之后,类才可以用作多重继承的基类。

 

2.多重继承的派生类从每个基类中继承状态

       在多重继承下,派生类的对象包含每个基类的基类子对象。当我们编写

Panda ying_yang("ying_yang");

的时候,对象 ying_yang 包含一个 Bear 类子对象(Bear 类子对象本身包含一

 ZooAnimal 基类子对象)、一个 Endangered 类子对象以及 Panda 类中声明

的非 static 数据成员(如果有的话)

 

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

       构造派生类型的对象包括构造和初始化它的所有基类子对象。像继承单个基类的情况一样,派生类的构造函数可以在构造函数初始化式中给零个或多个基类传递值:

       // explicitly initialize both baseclasses

    Panda::Panda(std::string name, boolonExhibit)

                    :Bear(name, onExhibit, "Panda"), Endangered(Endangered::critical)

                    {}

    // implicitly use Bear default constructorto initialize the Bear subobject

    Panda::Panda()

          : Endangered(Endangered::critical) {}

 

4.构造的次序

        构造函数初始化式只能控制用于初始化基类的值,不能控制基类的构造次序。基类构造函数按照基类构造函数在类派生列表中的出现次序调用。对 Panda 而言,基类初始化的次序是:

       1ZooAnimal,从 Panda 的直接基类 Bear 沿层次向上的最终基类

2Bear第一个直接基类

3Endangered第二个直接基类,它本身没有基类。

4 Panda,初始化 Panda 本身的成员,然后运行它的构造函数的函数体。

 

注意:构造函数调用次序既不受构造函数初始化列表中出现的基类的影响,也不受基类在构造函数初始化列表中的出现次序的影响。

 

例如,在 Panda 类的默认构造函数中,隐式调用 Bear 类的默认构造函数,它不出现在构造函数初始化列表中,虽然如此,仍在显式列出的Endangered 类构造函数之前调用 Bear 类的默认构造函数。

 

5.析构的次序

       按构造函数运行的逆序调用析构函数。在我们的例子中,调用析构函数的次序是 ~Panda, ~Endangered, ~Bear, ~ZooAnimal

 

二、     转换与多个基类

在单个基类情况下,派生类的指针或引用可以自动转换为基类的指针或引用,对于多重继承也是如此,派生类的指针或引用可以转换为其任意其类的指针或引用。例如,Panda 指针或引用可以转换为 ZooAnimalBear  Endangered 的指针或引用。

在多重继承情况下,遇到二义性转换的可能性更大。编译器不会试图根据派生类转换来区别基类间的转换,转换到每个基类都一样好。例如,如果有print 函数的重载版本:

              void print(const Bear&);

          voidprint(const Endangered&);

 

通过 Panda 对象对 print 函数的未限定调用

    Panda ying_yang("ying_yang");

    print(ying_yang);              // error:ambiguous

导致一个编译时错误,指出该调用是二义性的。

 

1.     多重继承下的虚函数

1 ZooAnimalEndangered 类中的虚函数

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

像单继承一样,用基类的指针或引用只能访问基类中定义(或继承)的成员,不能访问派生类中引入的成员。

       当一个类继承于多个基类的时候,那些基类之间没有隐含的关系,不允许使用一个基类的指针访问其他基类的成员。

       作为例子,我们可以使用 BearZooAnimalEndangered  Panda 的指针或引用来访问 Panda 对象。所用指针的类型决定了可以访问哪些操作。如果使用 ZooAnimal 指针,只能使用 ZooAnimal 类中定义的操作,不能访问 Panda 接口的 Bear 特定、Panda 特定和 Endangered 部分。类似地,Bear指针或引用只知道 Bear  ZooAnimal 成员, Endangered 指针或引用局限于 Endangered 成员:

Bear  *pb = new Panda("ying_yang");

    pb->print(cout); // ok:Panda::print(ostream&)

    pb->cuddle();    // error: not part of Bear interface

    pb->highlight(); // error: not part ofBear interface

    delete pb;       // ok: Panda::~Panda()

如果将 Panda 对象赋给 ZooAnimal 指针,这个调用集合将完全相同的方式确定。

 

在通过 Endangered 指针或引用使用 Panda 对象的时候,不能访问 Panda 接口的 Panda 特定的部分和 Bear 部分:

Endangered *pe = new Panda("ying_yang");

pe->print(cout);   // ok: Panda::print(ostream&)

pe->toes();       // error: not part of Endangered interface

pe->cuddle();     // error: not part of Endangeredinterface

pe->highlight();   // ok: Endangered::highlight()

delete pe;       // ok: Panda::~Panda()

 

3.     确定使用哪个虚析构函数

假定所有根基类都将它们的析构函数适当定义为虚函数,那么,无论通过哪种指针类型删除对象,虚析构函数的处理都是一致的:

// each pointer points to a Panda

    delete pz;   //pz is a ZooAnimal*

    delete pb;   //pb is a Bear*

    delete pp;   //pp is a Panda*

    delete pe;   //pe is a Endangered*

       假定这些指针每个都向 Panda 对象,则每种情况下发生完全相同的析构函数调用次序。析构函数调用的次序是构造函数次序的逆序:通过虚机制调用 Panda 析构函数。随着 Panda 析构函数的执行,依次调用 EndangeredBear  ZooAnimal 的析构函数。

 

三、     多重继承派生类的复制控制

多重继承的派生类的逐个成员初始化、赋值和析构,表现得与单继承下的一样,使用基类自己的复制构造函数、赋值操作符或析构函数隐式构造、赋值或撤销每个基类。

假定 Panda 类使用默认复制控制成员。ling_ling 的初始化

    Pandaying_yang("ying_yang");   // create a Panda object

    Pandaling_ling = ying_yang;     // uses copy constructor

       使用默认复制构造函数调用 Bear 复制构造函数,Bear 复制构造函数依次在执行 Bear 复制构造函数之前运行 ZooAnimal 复制构造函数。一旦构造了 ling_ling  Bear 部分,就运行 Endangered 复制构造函数来创建对象的那个部分。最后,运行 Panda 复制构造函数。

 

合成的赋值操作符的行为类似于复制构造函数,它首先对对象的 Bear 部分进行赋值,并通过 Bear 对对象的 ZooAnimal 部分进行赋值,然后,对Endangered 部分进行赋值,最后对 Panda 部分进行赋值。

 

       合成的析构函数撤销 Panda 对象的每个成员,并且按构造次序的逆序为基类部分调用析构函数。

 

四、     多重继承下的类作用域

在多重继承下,类作用域更加复杂,因为多个基类作用域可以包围派生类作用域。通常,成员函数中使用的名字和查找首先在函数本身进行,如果不能在本地找到名字,就继续在成员的类中查找,然后依次查找每个基类。在多重继承下,查找同时检察所有的基类继承子树——在我们的例子中,并行查找Endangered 子树和 Bear/ZooAnimal 子树。如果在多个子树中找到该名字,则那个名字的使用必须显式指定使用哪个基类;否则,该名字的使用是二义性的。

 

1.     多个基类可能导致二义性

假定 Bear 类和 Endangered 类都定义了名为 print 的成员,如果 Panda 类没有定义该成员,则 ying_yang.print(cout);

这样的语句将导致编译时错误。

Panda 类的派生(它导致有两个名为 print 的成员)是完全合法的。派生只是导致潜在的二义性,如果没有 Panda 对象调用 print,就可以避免这个二义性。如果每个 print 调用明确指出想要哪个版本——Bear::print 还是Endangered::print,也可以避免错误。只有在存在使用该成员的二义性尝试的时候,才会出错。

 

2.     首先发生名字查找

虽然两个继承的 print 成员的二义性相当明显,但是也许更令人惊讶的是,即使两个继承的函数有不同的形参表,也会产生错误。类似地,即使函数在一个类中是私有的而在另一个类中是公用或受保护的,也是错误的。最后,如果在 ZooAnimal 类中定义了 print  Bear 类中没有定义,调用仍是错误的。

名字查找总是以两个步骤发生:首先编译器找到一个匹配的声明(或者,在这个例子中,找到两个匹配的声明,这导致二义性),然后,编译器才确定所找到的声明是否合法

 

3.     避免用户级二义性

可以通过指定使用哪个类解决二义性:

   ying_yang.Endangered::print(cout);

 

避免潜在二义性最好的方法是,在解决二义性的派生类中定义函数的一个版本。例如,应该给选择使用哪个 print 版本的 Panda 类一个 print 函数:

    std::ostream& Panda::print(std::ostream&os)  const

    {

        Bear::print(os);        // print the Bear part

        Endangered::print(os);  // print the Endangered part

        return os;

    }

 

五、     虚继承

每个 IO 库类都继承了一个共同的抽象基类,那个抽象基类管理流的条件状态并保存流所读写的缓冲区。istream  ostream 类直接继承这个公共基类,库定义了另一个名为 iostream 的类,它同时继承 istream  ostreamiostream类既可以对流进行读又可以对流进行写。IO 继承层次的简化版本如图所示:

如果 IO 类型使用常规继承,则每个 iostream 对象可能包含两个 ios 子对象:一个包含在它的 istream 子对象中,另一个包含在它的 ostream 子对象中,从设计角度讲,这个实现正是错误的:iostream 类想要对单个缓冲区进行读和写,它希望跨越输入和输出操作符共享条件状态。如果有两个单独的 ios 对象,这种共享是不可能的。

虚继承是一种机制,类通过虚继承指出它希望共享其虚基类的状态。在虚继承下,对给定虚基类,无论该类在派生层次中作为虚基类出现多少次,只继承一个共享的基类子对象。共享的基类子对象称为虚基类

istream  ostream 类对它们的基类进行虚继承。如果其他类(如 iostream 同时继承它们两个,则派生类中只出现它们的公共基类的一个副本。通过在派生列表中包含关键字 virtual 设置虚基类:

        classistream : public virtual ios { ... };

class ostream : virtual public ios { ... };

 

class iostream: public istream, public ostream { ...};

图虚继承Panda层次

通常,使用虚继承的类层次是一次性由一个人或一个项目设计组设计的,独立开发的类很少需要其基类中的一个是虚基类,而且新基类的开发者不能改变已经存在的层次。

 

六、     虚基类的声明

通过用关键字 virtual 修改声明,将基类指定为通过虚继承派生。例如:

        classRaccoon : public virtual ZooAnimal { /* ... */ };

    class Bear : virtual public ZooAnimal { /*... */ };

 

任何可被指定为基类的类也可以被指定为虚基类,虚基类可以包含通常由非虚基类支持的任意类元素。

 

1.     支持到基类的常规转换

即使基类是虚基类,也照常可以通过基类类型的指针或引用操纵派生类的对象。例如,即使 Panda 类将它的 ZooAnimal 部分作为虚基类继承,下面所有 Panda的基类转换也能正确执行:

    voiddance(const Bear*);

    void rummage(const Raccoon*);

    ostream& operator<<(ostream&,const ZooAnimal&);

    Panda ying_yang;

    dance(&ying_yang);             //ok: converts address to pointer to Bear

    rummage(&ying_yang);               // ok: converts address to pointerto Raccoon

    cout << ying_yang;               //ok: passes ying_yang as a ZooAnimal

 

2.     虚基类成员的可见性

使用虚基类的多重继承层次比没有虚继承的引起更少的二义性问题。

 

       假定通过多个派生路径继承名为 X 的成员,有下面三种可能性:

1)如果在每个路径中 X 表示同一虚基类成员,则没有二义性,因为共享该成员的单个实例。

2)如果在某个路径中 X 虚基类的成员,而在另一路径中 X 是后代派生类的成员,也没有二义性——特定派生类实例的优先级高于共享虚基类实例

3)如果沿每个继承路径 X 表示后代派生类的不同成员,则该成员的直接访问是二义性的

 

七、     特殊的初始化语义

通常,每个类只初始化自己的直接基类。在应用于虚基类的进修,这个初始化策略会失败。如果使用常规规则,就可能会多次初始化虚基类:类将沿着包含该虚基类的每个继承路径初始化。

为了解决这个重复初始化问题,从具有虚基类的类继承的类对初始化进行特殊处理。在虚派生中,由最低层派生类的构造函数初始化虚基类。

虽然由最低层派生类初始化虚基类,但是任何直接或间接继承虚基类的类一般也必须为该基类提供自己的初始化式。只要可以创建虚基类派生类类型的独立对象,该类就必须初始化自己的虚基类,这些初始化式只有创建中间类型的对象时使用。

在我们的层次中,可以有 Bear Raccoon  Panda 类型的对象。创建 Panda 对象的时候,它是最低层派生类型并控制共享的 ZooAnimal 基类的初始化:创建Bear 对象(或 Raccoon 对象)的时候,不涉及更低层的派生类型。在这种情况下,Bear(或 Raccoon)构造函数像平常一样直接初始化它们的 ZooAnimal 基类:

Bear::Bear(std::string name, bool onExhibit):

             ZooAnimal(name, onExhibit,"Bear") { }

    Raccoon::Raccoon(std::string name, boolonExhibit)

           : ZooAnimal(name, onExhibit,"Raccoon") { }

 

虽然 ZooAnimal 不是 Panda 的直接基类,但是 Panda 构造函数也初始化 ZooAnimal 基类:

       Panda::Panda(std::string name, boolonExhibit)

          : ZooAnimal(name, onExhibit,"Panda"),

            Bear(name, onExhibit), 

            Raccoon(name, onExhibit),

            Endangered(Endangered::critical),

            sleeping_flag(false) { }

       创建 Panda 对象的时候,这个构造函数初始化 Panda 对象的 ZooAnimal 部分。

 

1.     怎样构造虚继承的对象

Bear winnie("pooh");    // Bear constructor initializes ZooAnimal

Raccoon meeko("meeko"); // Raccoonconstructor initializes ZooAnimal

Panda yolo("yolo");     // Panda constructor initializes ZooAnimal

 

当创建 Panda 对象的时候,

1)首先使用构造函数初始化列表中指定的初始化式构造 ZooAnimal 部分。

2)接下来,构造 Bear 部分。忽略 Bear 的用于 ZooAnimal 构造函数初始化列表的初始化式。

3)然后,构造 Raccoon 部分,再次忽略 ZooAnimal 初始化式。

(4)    最后,构造 Panda 部分。

 

2.     构造函数与析构函数的次序

无论虚基类出现在继承层次中任何地方,总是在构造非虚基类之前构造虚基类。

按声明次序检查直接基类,确定是否存在虚基类。例中,首先检查 BookCharacter 的继承子树,然后检查 Bear 的继承子树,最后检查 ToyAnimal 的继承子树。按从根类开始向下到最低层派生类的次序检查每个子树。

TeddyBear 的虚基类的构造次序是先 ZooAnimal  ToyAnimal。一旦构造了虚基类,就按声明次序调用非虚基类的构造函数:首先是 BookCharacter,它导致调用 Character 构造函数,然后是 Bear。因此,为了创建 TeddyBear 对象,按下面次序调用构造函数:

       ZooAnimal();           // Bear's virtual base class

    ToyAnimal();           // immediate virtual base class

    Character();           // BookCharacter's nonvirtual baseclass

    BookCharacter();       // immediate nonvirtual base class

    Bear();                // immediate nonvirtual baseclass

    TeddyBear();           // most derived class

在合成复制构造函数中使用同样的构造次序,在合成赋值操作符中也是按这个次序给基类赋值。保证调用基类析构函数的次序与构造函数的调用次序相反。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值