侧边栏壁纸
博主头像
LittleAO的学习小站 博主等级

在知识的沙漠寻找绿洲

  • 累计撰写 125 篇文章
  • 累计创建 27 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

C++之 多重继承和虚继承

LittleAO
2024-09-23 / 0 评论 / 0 点赞 / 15 阅读 / 0 字
温馨提示:
本文最后更新于2024-09-23,若内容或图片失效,请留言反馈。 部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

多重继承

多重继承简单来说就是一个派生类继承了多个基类。其语法规则如下:(以下所有例子都使用C++ Primer中的)

class ZooAnimal {};
class Endangered {};
class Bear : public ZooAnimal {};
class Panda : public Bear, public Endangered {};  // 这里用了多重继承

上面的Panda类继承了Bear类和Endangered类,实现了多重继承。主要的知识到这里就结束了。但实际应用过程中,这样的继承会有许多的问题,下面列举几个:

  • 多重继承的基类有相同的成员会怎么样?

  • 构造多重继承的派生类对象的构造顺序是怎样?

  • 析构顺序是怎样?

  • 多重继承的基类又继承自相同的类,这会发生什么问题?

解决这些问题,需要我们对多重继承的机制进行详细了解。

派生类构造函数构造所有基类

构造派生类对象会同时构造并初始化它的所有基类子对象。多重继承的派生类的构造函数初始值也只能初始化它的直接基类。构造顺序如下:

  • 派生类对象先构造基类;

  • 多重继承则从左到右进行构造;

以上面的例子为例:

  • 欲先构造Panda,现需要构造Bear,再构造Endangerd

  • 欲先构造Bear,则需构造ZooAnimal

则构造一个Panda类对象的构造顺序为:ZooAnimal-> Bear-> Endangerd-> Panda

多重继承的析构

了解了多重继承的构造顺序,析构顺序就变得简单许多:就是构造顺序的反方向。

使用继承的构造函数

C++11 允许派生类使用基类的构造函数作为自己的构造函数,称为继承构造函数。多重继承中使用:

class A {
public:
    A(int x) {};
};

class B {
public:
    B(int x) {};
};

class C : public A, public B {
public:
    using A::A;
    using B::B; // 编译时报错,构造函数重复声明
};

上面的代码中C的构造函数继承了A和B的,但问题在于A和B的构造函数的形参列表相同,导致了二义性,不能通过编译。

将上面某个基类的构造函数形参列表稍微修改一下,就可以通过编译。要不然就自己重新定义一个自己的版本构造函数。

多重继承与拷贝和移动

若派生类定义了自己的拷贝构造函数,则发生拷贝时直接调用,否则就会由编译器生成合成拷贝构造函数。该合成拷贝构造函数对每个基类依次进行拷贝构造。

拷贝构造的顺序和之前提到的构造函数的顺序一致,移动赋值运算符也与其一致。

多个基类的类型转换

在只有一个基类的情况下,派生类的指针可以作为实参放到基类形参之上。就像下面这样:

class ZooAnimal {};
class Bear : public ZooAnimal {};

void get_name(const ZooAnimal& animal) {}
int main()
{
    Bear black_bear;
    get_name(black_bear);
}

多个基类也类似:

#include <iostream>
#include <ostream>
#include <string>
class ZooAnimal {
public:
    ZooAnimal(std::string name) : name(name) {}
protected:
    std::string name;
};

class Bear : public ZooAnimal {
public:
    using ZooAnimal::ZooAnimal;
};

class Endangered {};

class Panda : public Bear, public Endangered {
public:
    using Bear::Bear;
};

void print(const Bear&){}
void highlight(const Endangered&){}
std::ostream& operator<<(std::ostream& os, const ZooAnimal& val) {
    os << "GOOD";
    return os;
}

int main()
{
    Panda ying_yang("ying_yang");
    print(ying_yang);
    highlight(ying_yang);
    std::cout << ying_yang << std::endl;
}

如果有函数的形参为派生类的两个基类类型,若调用了这个函数,则会产生二义性导致编译错误。

// 上面省略
void print(const Bear&){}
void print(const Endangered&){}

int main()
{
    Panda ying_yang("ying_yang");
    print(ying_yang);  // 错误!
}

基于指针或引用类型的查找,基于指针或引用的静态类型,类型中定义了什么就只能用什么,即使基类指针赋值为派生类的对象(虚函数):

#include <iostream>
#include <ostream>
#include <string>
class ZooAnimal {
public:
    ZooAnimal(std::string name) : name(name) {}
    ~ZooAnimal() {}
    virtual void print() {}
protected:
    std::string name;
};

class Bear : public ZooAnimal {
public:
    using ZooAnimal::ZooAnimal;
    virtual void print() {}
    virtual void toes() {}
};

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

class Panda : public Bear, public Endangered {
public:
    using Bear::Bear;
    void print() { std::cout << "Call Panda Print!" << std::endl; }
    void highlight() { std::cout << "Call Panda highlight!" << std::endl; }
    void toes() {}
    void cuddle() {}
};


int main()
{
    // 例1
    Bear *pb = new Panda("ying_yang");
    pb->print();    // 访问Panda::print()
    //pb->cuddle();   // 错误,Bear没有cuddle成员
    //pb->highlight();// 错误,Bear没有highlight成员
    delete pb;      // 访问Panda::~Panda()

    // 例2
    Endangered *pe = new Panda("yang_yang");
    pe->print();
    //pe->toes();     // 错误
    //pe->cuddle();   // 错误
    pe->highlight();
    delete pe;
}
classDiagram class ZooAnimal { - name: string + ZooAnimal(name) + ~ZooAnimal() + print() } class Bear { + Bear(name) + ~Bear() + print() + toes() } class Endangered { + print() + highlight() + ~Endangered() } class Panda { + Panda(name) + print() + highlight() + toes() + cuddle() } ZooAnimal <|-- Bear Endangered <|-- Panda Bear <|-- Panda 查看全部

若继承的多个类中有重名函数,通过派生类访问这个函数会导致二义性错误:

class Bear : public ZooAnimal {
public:
    using ZooAnimal::ZooAnimal;
    virtual void print() {}
    virtual void toes() {}
    void max_weight() {}
};

class Endangered {
public:
    virtual void print() {}
    virtual void highlight() {}
    void max_weight() {}
    ~Endangered() {}
};

class Panda : public Bear, public Endangered {
public:
    using Bear::Bear;
    void print() { std::cout << "Call Panda Print!" << std::endl; }
    void highlight() { std::cout << "Call Panda highlight!" << std::endl; }
    void toes() {}
    void cuddle() {}
};


int main()
{
    Panda pd("ying_yang");
    pd.max_weight();    // 二义性错误
}

一种可能的解决的方法,就是使用类作用域,例如:

Panda pd("ying_yang");
pd.Bear::max_weight();
pd.Endangered::max_weight();

最好的解决方法就是在Panda类中重新写一个新的max_weight,通过类作用域来确定使用哪个类的函数。

虚继承

多重继承有一种特殊情况,就是基类可能继承自同一个类,专有名词为“菱形继承”。IO标准库就有一个菱形继承的情况:iostream多重继承为ostream和istream,同时这两个类又继承自base_ios。

如果出现了菱形继承,代表着我们的派生类会有两个原始基类的副本,着显然行不通。不同的语言有不同的方法解决这个问题。在C++中,我们通过虚继承来解决这个问题。

稍微修改一下我们之前写过的类,如果嫌麻烦,可以看UML图大致了解。

#include <iostream>
#include <ostream>
#include <string>
class ZooAnimal {
public:
    ZooAnimal(std::string name) : name(name) {}
    ~ZooAnimal() {}
    virtual void print() {}
protected:
    std::string name;
};

class Raccoon : virtual public ZooAnimal {
public:
    using ZooAnimal::ZooAnimal;
    virtual void print() {}
    virtual void toes() {}
    void max_weight() {}
};

class Bear : virtual public ZooAnimal {
public:
    using ZooAnimal::ZooAnimal;
    virtual void print() {}
    virtual void toes() {}
    void max_weight() {}
};

class Endangered {
public:
    virtual void print() {}
    virtual void highlight() {}
    void max_weight() {}
    ~Endangered() {}
};

class Panda : public Bear, public Raccoon, public Endangered {
public:
    using Bear::Bear;
    void print() { std::cout << "Call Panda Print!" << std::endl; }
    void highlight() { std::cout << "Call Panda highlight!" << std::endl; }
    void toes() {}
    void cuddle() {}
};
classDiagram class ZooAnimal { - name: string + ZooAnimal(name) + ~ZooAnimal() + print() } class Raccoon { + Raccoon(name) + ~Raccoon() + print() + toes() + max_weight() } class Bear { + Bear(name) + ~Bear() + print() + toes() + max_weight() } class Endangered { + print() + highlight() + max_weight() + ~Endangered() } class Panda { + Panda(name) + print() + highlight() + toes() + cuddle() } ZooAnimal <|-- Raccoon ZooAnimal <|-- Bear Endangered <|-- Panda Bear <|-- Panda Raccoon <|-- Panda 查看全部

这段代码就使用了虚继承,其基本的语法格式如下:

class Raccoon : virtual public ZooAnimal;
class Raccoon : public virtual ZooAnimal; // 另一种写法

继承虚基类表明我们允许以共享的方式分享这个类,不论虚基类在继承体系中出现了多少次,在派生类中只包含一个共享的虚基类子对象。

不论是基类和虚基类,派生类对象,都能被可访问基类的指针和引用操作。和多重继承的性质一样。

例如,假定类B定义了一个名为x的成员,D1和D2都是从B虚继承得到的,D继承了D1和D2,则在D的作用域中,x通过D的两个基类都是可见的。如果我们通过D的对象使用x,有三种可能性:

  • 如果在D1和D2中都没有x的定义,则x将被解析为B的成员,此时不存在二义性,一个D的对象只含有x的一个实例。

  • 如果x是B的成员,同时是D1和D2中某一个的成员,则同样没有二义性,派生类的x比共享虚基类B的x优先级更高。

  • 如果在D1和D2中都有x的定义,则直接访问x将产生二义性问题。

构造顺序和析构顺序

判断构造顺序,遵循一个准则:虚继承基类先构造,自底向上构造。以上面的虚继承构造为例:

  • 首先使用Panda的构造函数初始值列表构造虚基类ZooAnimal;

  • 再构造Bear;

  • 再构造Raccoon;

  • 再构造Endangered;

  • 最后构造Panda;

析构函数和构造函数以相反的顺序执行。

0

评论区