c++系列-菱形继承与虚继承

首先 来看看什么是菱形继承:

当一个类通过多条继承路径继承自同一个基类时,如果没有虚继承,基类的成员可能会在派生类中出现多份冗余拷贝,导致不必要的内存浪费和访问不明确的问题。

### 菱形继承问题
经典的菱形继承结构如下:

```cpp
class A {
public:
    int value;
};

class B : public A { };

class C : public A { };

class D : public B, public C { };
```

在这个例子中,`D`类通过`B`和`C`两条路径继承了`A`类,这样`D`类会有两份`A`类的成员(如`value`),造成内存冗余和访问冲突。如果需要访问`A`类的成员,`D`类必须指定是通过`B`还是`C`来访问,代码复杂且不安全。

### 虚继承解决问题
为了解决上述问题,可以使用虚继承,确保基类`A`在派生类中只存在一份拷贝:

```cpp
class A {
public:
    int value;
};

class B : virtual public A { };

class C : virtual public A { };

class D : public B, public C { };
```

在这个例子中,`B`和`C`都通过虚继承继承了`A`类,这样`D`类中只会有一份`A`类的拷贝。

### 关键点
1. **单一实例**:虚继承保证了基类在最终的派生类中只会有一份实例。
2. **构造函数调用**:在虚继承中,基类的构造函数由最终派生类负责调用,而不是中间派生类。比如在上面的例子中,`A`的构造函数会由`D`来调用,而不是`B`或`C`。
3. **内存布局复杂性**:虚继承的内存布局比普通继承复杂,编译器会使用虚表(vtable)来记录虚基类的偏移,因此可能会略微影响性能。

### 示例

```cpp
#include <iostream>

class A {
public:
    A() { std::cout << "A constructor\n"; }
    int value;
};

class B : virtual public A {
public:
    B() { std::cout << "B constructor\n"; }
};

class C : virtual public A {
public:
    C() { std::cout << "C constructor\n"; }
};

class D : public B, public C {
public:
    D() { std::cout << "D constructor\n"; }
};

int main() {
    D d;
    d.value = 10;  // 通过虚继承,D类中只有一份A的实例
    std::cout << "d.value: " << d.value << std::endl;
    return 0;
}
```

输出结果为:
```
A constructor
B constructor
C constructor
D constructor
d.value: 10
```

通过虚继承,`D`类只含有一个`A`类的实例,这样就避免了菱形继承的问题。

好  到这里我们知道虚继承是用来解决菱形继承的,那么底层原理呢

在 C++ 中,当使用**虚继承**时,编译器会引入一些额外的机制来处理复杂的内存布局,确保虚基类在最终派生类中只存在一份实例。这就是**虚基类表(Virtual Table, vtable)**的作用,它记录虚基类在内存中的位置和偏移,以便在运行时可以正确访问虚基类的成员。

 

### 虚继承中的内存布局问题

 

在普通的多重继承中,派生类会直接包含其所有基类的成员,这导致每个继承路径上都有基类的完整实例。但在虚继承中,为了确保派生类中只有一个虚基类的实例,编译器无法静态地确定虚基类在内存中的确切位置,因为这个位置可能依赖于多个继承路径(类似菱形继承结构)。

 

### 虚表(vtable)的作用

 

在虚继承中,编译器为每个包含虚基类的派生类维护一个**虚基类表(vtable)**。这个表包含了虚基类在内存中的**偏移量**,即相对于派生类对象的基类部分在内存中的位置。通过查阅这个虚基类表,编译器可以在运行时动态确定虚基类实例的位置,从而正确地访问基类的成员。

 

具体来说:

 

1. **虚基类表的生成**:每个派生类在继承虚基类时,编译器会为该派生类生成一个虚基类表,其中记录了虚基类的内存地址或相对偏移。

   

2. **指针间接访问**:在访问虚基类成员时,编译器不会直接从派生类的对象布局中定位成员,而是通过虚基类表中的信息间接确定虚基类的位置。这种通过指针间接访问基类成员的方式就是虚表机制的一部分。

 

### 例子分析

 

假设有如下代码使用虚继承:

 

```cpp

class A {

public:

    int value;

};

 

class B : virtual public A { };

 

class C : virtual public A { };

 

class D : public B, public C { };

```

 

当创建 `D` 类的对象时,由于 `D` 通过 `B` 和 `C` 虚继承了 `A`,编译器会生成一个虚基类表(vtable),用于记录 `A` 在 `D` 类对象中的偏移。

 

例如,访问 `D` 类中的 `A::value` 时,编译器会:

 

1. 查找 `D` 类的虚基类表,找到 `A` 类的内存偏移量。

2. 根据偏移量,在 `D` 类的对象内存布局中定位到 `A::value` 的地址。

3. 通过这个地址来访问 `value` 成员。

 

### 虚表带来的影响

 

- **性能开销**:由于虚表的引入,访问虚基类成员时需要通过虚表查找偏移,从而带来了额外的性能开销。相较于普通继承,虚继承的内存访问变得稍微复杂且缓慢。

- **内存开销**:虚继承会额外引入虚表,增加了内存的使用,特别是在有多个虚基类时,虚表会变得更加复杂。

 

### 总结

 

- 虚继承使得基类在多重继承中只实例化一次,但引入了复杂的内存布局。

- 编译器通过虚基类表(vtable)记录虚基类在派生类中的内存偏移,确保正确访问虚基类的成员。

- 虚表机制引入了性能和内存的额外开销,但解决了菱形继承等问题带来的复杂性。

 

通过这种机制,C++ 实现了对复杂继承关系的管理,使得开发者可以使用虚继承而不用担心内存冗余问题。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值