20.菱形继承

20.面向对象-继承-菱形继承


20.1 知识点

什么是菱形继承

  • 示意结构:

        A
       / \
      B   C
       \ /
        D
    
  • 在普通多重继承中,如果父类 B、C 各自继承了 A,那么子类 D 将拥有两份 A

  • 虽然可以通过 B::成员C::成员 来访问其中的内容,但这会导致:

    • 成员访问产生二义性
    • 内存冗余
  • 在实际开发中,往往需要避免上述问题的发生。

如何避免菱形继承带来的重复继承问题

  • 示意结构:

        A
       / \
      B   C
       \ /
        D
    
  • 为了避免重复继承的问题,我们采用虚继承

虚继承的含义:

  • 虚继承是一种告诉编译器:“我继承这个父类时,只需要一个父类实例就够了”的方式。
  • 它可以有效避免菱形继承中重复继承基类的问题。

虚继承的效果:

  • D 中只保留一份 A 的实例,避免冲突和冗余。

使用方式(关键字 virtual):

class A {};

class B : virtual public A {};   // B 虚继承自 A
class C : virtual public A {};   // C 虚继承自 A
class D : public B, public C {}; // D 只保留一个 A 的实例

实例代码调用说明:

D d;

// 使用了虚继承,可以不用显式指定访问来源,也可以指定(不会报错)
cout << d.B::a << endl;
cout << d.C::a << endl;

// 直接访问成员也不会报错
cout << d.a << endl;
cout << d.b << endl;
cout << d.c << endl;
cout << d.d << endl;

虚继承的原理

  • 示意结构:

        A
       / \
      B   C
       \ /
        D
    

普通继承原理:

  • 子类对象中的父类成员是复制粘贴进来的,直接嵌入对象中。
  • 对象结构是“一层套一层”,子类拥有完整的家谱结构。
  • 看到整个家谱里每一层祖宗的字段.父亲的就是我的,我照单全收
  • B 和 C 各自拥有一份 A 的成员,D 继承 B、C 时也继承了两份 A。
  • 如果 B 和 C 中存在同名成员,D 使用时需要指定是 B::成员 还是 C::成员

虚继承原理:

  • 子类对象中不直接嵌入虚基类成员。

  • 通过指针偏移表间接引用虚基类成员。

  • 虚基类中的成员由最底层派生类(如D)统一管理。

  • 通常编译器的处理方式包括:

    • 偏移表:B、C类对象中,保存一个偏移量,表示到真正A对象的地址偏移
    • 指针:B、C类对象中,保存一个指向A子对象的指针
  • 访问虚基类成员流程:

    • 从当前对象地址出发
    • 加上偏移量或跳转指针
    • 跳过去最终访问虚基类成员

性能说明:

  • 虚继承相比普通继承开销略大,但能保证对象结构正确性

  • 性能开销来源:

    • 访问虚基类成员需要一次额外的寻址(指针跳转)
  • 编译器会根据继承复杂性、平台结构、内存优化等因素选择偏移或指针。

通俗理解:

  • 最底层的类 D 才是真正 “拥有” A 的人。
  • B、C 都只是 “借用” A,不拥有 A。
  • B、C 访问 A 时,需要通过 D 跳转去找。

虚基类的构造调用

  • 示意结构:

        A
       / \
      B   C
       \ /
        D
    
  • 在虚继承中,虚基类的构造函数由最底层派生类负责调用

  • 即使中间类 B、C 写了构造,也不会调用虚基类构造函数

开发中要注意的问题:

  1. 谁是被虚继承的类?
  2. 谁是最底层子类?
  • 被虚继承的类中的成员,在继承结构中只保留一份
  • 不像普通继承中,会被直接拷贝到子类。

示例代码:

A.h

#pragma once
class A
{
public:
    A(int aa)
    {
        a = aa;
    }
    int a = 10;
};

B.h

#pragma once
#include "A.h"
class B : virtual public A
{
public:
    B() : A(2)
    {

    }
    int b = 11;
};

C.h

#pragma once
#include "A.h"
class C : virtual public A
{
public:
    C() : A(3)
    {

    }
    int c = 12;
};

D.h

#pragma once
#include "B.h"
#include "C.h"
class D : public B, public C
{
public:
    D() : A(4)
    {

    }
    int d = 13;
};

测试输出说明:

// 虚基类的构造由最终派生类 D 调用
// B 和 C 中的 A 构造不会被调用,D 负责调用 A 构造函数
D d;

// D是最底层 B C 分别虚继承A 那么A只会有1份 所以值是一样的 都是基于D构造时传进去的4创建出来值4
// A 在 D 中只保留一份实例,所以访问结果一致:
cout << d.a << endl;       // 4
cout << d.B::a << endl;    // 4
cout << d.C::a << endl;    // 4

20.2 知识点代码

A.h

#pragma once
class A
{
public:
    A(int aa)
    {
        a = aa;
    }
    int a = 10;
};

B.h

#pragma once
#include "A.h"
class B : virtual public A
{
public:
    B() :A(2)
    {

    }
    int b = 11;

};

C.h

#pragma once
#include "A.h"
class C : virtual public A
{
public:
    C() :A(3)
    {

    }
    int c = 12;
};

D.h

#pragma once
#include "B.h"
#include "C.h"
class D : public B, public C
{
public:
    D() : A(4)
    {

    }
    int d = 13;
};

Lesson20_面向对象_继承_菱形继承.cpp

#include <iostream>
using namespace std;
#include "D.h"
int main()
{
    #pragma region 知识点一 什么是菱形继承
    //    A
    //   / \
    //  B   C
    //   \ /
    //	  D
    //在普通多重继承中,如果父类B、C各自继承了A,那么子类D将拥有两份A
    //虽然我们可以通过 B::成员 和 C::成员 来使用其中内容
    //但是这样会让成员访问具备了二义性,还产生了内存冗余
    //因此我们在实际开发中往往需要避免这些问题的发生



    #pragma endregion

    #pragma region 知识点二 如何避免菱形继承带来的重复继承问题
    //    A
    //   / \
    //  B   C
    //   \ /
    //	  D
    //想要避免菱形继承中重复继承问题
    //我们将采用虚继承的方式
    // 
    //虚继承是一种告诉编译器,我继承这个父类时,只要有一个父类实例就够了 的继承方式
    //它可以帮助我们有效的避免菱形继承中重复继承基类的问题
    //让D中只保留一份A的实例,避免冲突和冗余

    //关键字:
    //virtual

    //使用方式
    //class A {};

    //class B : virtual public A {};   // B虚继承自A
    //class C : virtual public A {};   // C虚继承自A
    //class D : public B, public C {}; // D只保留一个A的实例


    D d;

    //用了虚继承可以不用显示指定了 当然显示指定也不会报错
    //cout << d.B::a << endl;
    //cout << d.C::a << endl;

    //直接写不会报错
    cout << d.a << endl;
    cout << d.b << endl;
    cout << d.c << endl;
    cout << d.d << endl;

    #pragma endregion

    #pragma region 知识点三 虚继承的原理
    //    A
    //   / \
    //  B   C
    //   \ /
    //	  D
    //1.普通继承时,子类对象里的的父类成员是复制粘贴过来的,是实实在在嵌入进去的
    //	对象一层套一层,你能看到整个家谱里每一层祖宗的字段!
    //	父亲的就是我的,我照单全收

    //	说人话:
    //  B和C都会有一份父类成员拷贝
    //  D继承B、C时,他们的成员照单全收
    //	D在访问B和C中的同名成员时,需要区分是B中的还是C中的成员

    //2.虚继承时,子类对象中不直接嵌入虚基类成员
    //  而是通过指针或偏移表间接引用虚基类
    //  虚基类中的成员由最底层派生类管理
    //  通常编译器做法是: 
    //  2-1.偏移表	B、C类对象中,保存一个偏移量,表示到真正A对象的地址偏移
    //  2-2.指针	    B、C类对象中,保存一个指向A子对象的指针
    //  访问虚基类成员时:
    //  先从当前对象地址
    //  再加上偏移量
    //  跳过去访问真正的虚基类成员
    // 
    //  虚继承相比普通继承,开销略大,但是可以保证对象结构的正确性
    //	开销变大的原因是:
    //  访问虚基类成员时可能需要一次额外的寻址(指针跳转)
    //  编译器会根据继承结构的复杂性、目标平台的架构、内存优化的需求等因素,选择指针还是偏移表来管理虚基类

    //	说人话:
    //	最底层类D是真正“拥有”A的那个人
    //  中间的 B、C 都只是“借用”A,不是自己的财产
    //  他们要用A,还得找D要(通过偏移)
    #pragma endregion

    #pragma region 知识点四 虚基类的构造调用
    //    A
    //   / \
    //  B   C
    //   \ /
    //	  D
    //在进行虚继承时,虚基类的构造由最终派生类(最底层对象)负责调用
    //中间层(如B、C)即使写了虚基类的构造,也不会生效

    //要注意以下问题
    //1.虚继承的是谁?
    //2.谁是最底层的子类
    //被虚继承的类中的成员 在整个继承关系中 只会有一份 不会像普通继承一样,直接拷贝给子类

    //D是最底层 B C 分别虚继承A 那么A只会有1份 所以值是一样的 都是基于D构造时传进去的4创建出来值4
    cout << d.a << endl;//4
    cout << d.B::a << endl;//4
    cout << d.C::a << endl;//4

    #pragma endregion
}

20.3 练习题

虚继承的作用是什么?

在多重继承中避免“同一个基类被继承多份”的问题。

举例说明:

class A { int a; };
class B : public A { };
class C : public A { };
class D : public B, public C { };

在上述设计中,D 类同时继承了 B 和 C,而 B 和 C 又分别继承自 A,这会导致 D 对象内部出现两份 A 子对象,进而带来二义性问题。

如果改为虚继承,则这样定义:

class A { int a; };
class B : virtual public A { };
class C : virtual public A { };
class D : public B, public C { };

通过 virtual 关键字,B 和 C 都“共享”同一份 A 子对象,从而避免了重复继承的问题。

用一句话描述虚继承的原理

子类不直接包含基类成员,而是通过指针或偏移表间接引用由最底层子类统一管理的唯一一份基类子对象。



转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com

×

喜欢就点赞,疼爱就打赏