虚继承 (virtual public)详解

虚继承 (virtual public) 是 C++ 中解决菱形继承问题​(Diamond Problem)的核心机制。它确保了在多重继承的层级结构中,如果某个类是多个派生类的共同基类(虚基类),则在最底层的派生类对象中,​该基类的子对象只存在一份副本

核心问题:菱形继承与数据冗余/歧义

classDiagram
    class Base {
        +int data
    }
    class Derived1 {
    }
    class Derived2 {
    }
    class Final {
    }
    Base <|-- Derived1 : public
    Base <|-- Derived2 : public
    Derived1 <|-- Final : public
    Derived2 <|-- Final : public
  1. 普通继承的问题​:
    • 如果 Derived1Derived2 都使用普通 public 继承自 Base
    • 那么 Final 继承自 Derived1Derived2 时,Final 对象中将包含两份Base 的子对象(一个来自 Derived1 链,一个来自 Derived2 链)。
    • 数据冗余​:如果 Base 有成员变量(如 int data;),Final 对象里会有两个独立的 data,占用额外空间。
    • 访问歧义​:当在 Final 内部或通过 Final 对象访问 Base 的成员时,编译器无法确定你想访问哪一份副本(是通过 Derived1 继承来的还是 Derived2 继承来的?),导致编译错误。

虚继承 (virtual public) 的解决方案

classDiagram
    class Base {
        +int data
    }
    class Derived1 {
    }
    class Derived2 {
    }
    class Final {
    }
    Base <|-- Derived1 : virtual public
    Base <|-- Derived2 : virtual public
    Derived1 <|-- Final : public
    Derived2 <|-- Final : public
  • Derived1Derived2 使用 ​**virtual public**​ 继承自 Base 时:
    • Base 被声明为 Derived1Derived2虚基类​(Virtual Base Class)。
  • 关键效果​:当 Final 继承自 Derived1Derived2 时(无论是以 public 还是其他方式),在 Final 的对象中,​只会存在一份 Base 的子对象
    • 消除冗余​:Base 的成员变量(如 data)在 Final 对象中只有一份。
    • 消除歧义​:所有通过 FinalDerived1Derived2 访问的 Base 成员,都指向这同一个子对象,编译不再有歧义错误。

实现原理(编译器幕后工作)

编译器通过以下两种主要机制之一(或结合)来实现虚继承:

  1. 虚基类指针(vbcp/vbptr)​​:

    • 每个包含虚基类的派生类对象中,编译器会添加一个或多个指向虚基类子对象的指针​(通常是隐藏的)。
    • Final 对象中:
      • Derived1 部分包含一个指向共享 Base 子对象的指针。
      • Derived2 部分也包含一个指向同一个共享 Base 子对象的指针。
      • 实际的 Base 子对象通常放置在 Final 对象的尾部或其他特定位置。
  2. 虚基类偏移量表​:

    • 类似于虚函数表(vtable),编译器可能为每个类生成一个包含虚基类子对象相对于该类起始地址的偏移量的表。
    • 对象中存储指向该表的指针。
    • 访问虚基类成员时,通过该指针查找偏移量表,然后计算虚基类成员的实际地址。

对对象大小的影响

  • 虚继承会增加对象的大小,主要因为:
    • 存储虚基类指针(或多个指针)。
    • 存储虚基类偏移量表指针(如果使用该机制)。
    • 可能因对齐要求而产生填充字节(padding)。
  • 共享 Base 子对象节省的空间​ VS ​指针/表指针带来的开销​:当 Base 很大且继承路径很多时,共享通常节省空间;当 Base 很小且继承路径少时,指针开销可能更明显。

构造函数调用顺序的更改

虚继承引入了构造顺序的特殊规则:

  1. 虚基类优先​:在任何非虚基类的派生类构造函数执行之前,​所有虚基类的构造函数必须被完全构造好。
  2. 从最派生类负责​:最终派生类(如 Final)的构造函数直接负责调用其所有虚基类​(Base)的构造函数。
    • 即使虚基类是通过中间类(如 Derived1Derived2)引入的。
  3. 中间类构造函数的 Base 调用被忽略​:
    • Derived1Derived2 的构造函数初始化列表中,对 Base 构造函数的调用会被编译器忽略​(因为 Final 已经直接调用了它)。
    • 如果 Final 没有显式调用 Base 的构造函数,编译器会尝试调用 Base默认构造函数。如果 Base 没有默认构造函数,会导致编译错误。
class Base {
public:
    Base(int v) : data(v) {}
    int data;
};

class Derived1 : virtual public Base {
public:
    Derived1() : Base(10) {} // 如果Final调用了Base,这里的Base(10)会被忽略
    // ...
};

class Derived2 : virtual public Base {
public:
    Derived2() : Base(20) {} // 如果Final调用了Base,这里的Base(20)会被忽略
    // ...
};

class Final : public Derived1, public Derived2 {
public:
    Final() : Base(5) {} // Final *必须* 直接负责初始化虚基类Base
    // ...
};

Final 对象创建时:

  1. Base(5) 被调用(构造共享的 Base 子对象)。
  2. Derived1() 被调用(但其中的 Base(10) 被忽略)。
  3. Derived2() 被调用(但其中的 Base(20) 被忽略)。
  4. Final() 自身的函数体执行。

什么时候使用虚继承

  • 明确需要解决菱形继承问题​:当你的类设计存在或潜在存在菱形结构,并且你希望共享共同基类的状态时。
  • 接口类常用​:纯粹定义接口的抽象基类(纯虚函数)经常被用作虚基类,因为它们通常不包含状态(成员变量),此时使用虚继承主要是为了统一接口和避免潜在的歧义。

总结:虚继承关键点

特性普通继承虚继承 (virtual public)​
菱形结构结果公共基类出现多个副本公共基类只有一个共享副本
核心目的建立 "is-a" 关系,派生类包含基类副本解决菱形问题,确保公共基类共享
数据成员在最终类中可能有冗余在最终类中只有一份,无冗余
成员访问可能导致歧义(需要作用域解析)无歧义,访问共享成员
对象大小可能较大(多个基类副本)通常较小(单个共享),但需加指针开销
构造函数调用由直接派生类初始化基类最终派生类负责初始化所有虚基类
内存布局线性或相对简单更复杂,涉及虚基类指针/偏移量表
适用场景一般继承,单继承解决菱形继承问题,接口继承

虚继承是 C++ 为解决多重继承中菱形继承问题​(数据冗余和成员访问歧义)而引入的机制。通过在中间派生类(如图中的 Derived1Derived2)使用 virtual public 继承自公共基类(Base),可以保证在最底层的派生类(Final)对象中,该公共基类的子对象仅有一份共享副本。这会带来内存布局的变化(通常增加指向共享基类的指针),并显著改变构造函数的调用顺序(由最终派生类直接负责初始化虚基类)。虽然它能优雅解决复杂继承问题,但也会增加对象大小和运行时开销,因此应仅在确实需要解决菱形问题时使用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

浩瀚之水_csdn

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值