C++ 中虚继承与非虚继承的对比分析

C++ 中虚继承与非虚继承的对比分析


相同点

  1. 继承机制基础
    虚继承和非虚继承均属于 C++ 的继承机制,均允许派生类访问基类的成员(包括数据和函数),并支持多态性。
  2. 代码复用
    两者均通过继承实现代码复用,派生类可以直接使用基类的功能,减少重复代码。
  3. 多态性支持
    若基类包含虚函数,无论是否虚继承,均可通过基类指针或引用调用派生类的重写函数。

不同点

维度虚继承非虚继承
内存布局引入虚基类指针(vbptr),指向虚基类表(vbtable),增加内存开销直接包含基类子对象,无额外指针
构造函数调用虚基类的构造函数由最底层的派生类直接调用每个直接基类的构造函数由其直接派生类调用
访问方式通过虚基类表间接访问基类成员直接访问基类成员,无间接寻址
多重继承冲突解决菱形继承问题,确保基类只存在一个实例菱形继承时基类重复实例化,导致数据冗余
性能开销间接访问导致额外开销,适用于复杂继承场景直接访问效率更高,适合简单继承结构

虚继承的核心作用

  1. 解决菱形继承问题
    在多重继承中,若多个中间类继承自同一个基类(如 BC 继承自 AD 继承 BC),虚继承确保 A 只有一个实例,避免二义性和内存冗余。
    示例​:

    class A { int data; };
    class B : virtual public A {};
    class C : virtual public A {};
    class D : public B, public C {};  // D 中仅一个 A 实例
    
  2. 消除成员访问二义性
    非虚继承下,若基类成员被多次继承,需通过作用域运算符(如 obj.B::data)显式指定路径;虚继承后可直接访问(obj.data)。


虚继承的典型使用场景

  1. 菱形继承结构
    当类层次需通过多重继承形成“菱形”拓扑时,必须使用虚继承避免基类重复。
  2. 共享基类状态
    若多个派生类需共享基类的同一份数据(如全局配置),虚继承可保证数据一致性。
  3. 复杂框架设计
    在 GUI 框架、接口组合等场景中,虚继承可简化类关系的复杂性。

总结

  • 非虚继承:适用于单继承或不存在公共基类的多重继承,追求高效、简单的内存布局。
  • 虚继承:专为菱形继承设计,通过共享基类实例解决数据冗余和二义性,代价是内存和性能开销。
    实际开发中,​优先避免过度多重继承,仅在必要时使用虚继承。

C++菱形继承问题和虚继承分析

C++菱形继承问题和虚继承分析

二义性

在面向对象中,常常存在这样的事情,一个派生类它有两个或两个以上的基类,这种行为称作多重继承(Multiple Inheritance):

多重继承示意图:

多重继承示意图

如果在多重继承中 Class AClass B 存在同名数据成员,则对 Class C 而言这个同名的数据成员容易产生二义性问题。这里的二义性是指无法直接通过变量名进行读取,需要通过域 (::) 成员运算符进行区分。例如:

//基类A
class A
{
public:
    A() :m_data(1), m_a(1)
    {
    }
    ~A(){}

public:
    int m_data;      //同名变量,类型无要求
    int m_a;
};

//基类B
class B
{
public:
    B() :m_data(1), m_b(1)
    {
    }
    ~B(){}

public:
    int m_data;      //同名变量,类型无要求
    int m_b;
};

class C  : public A, public B
{

};

int _tmain(int argc,  _TCHAR* argv[])
{
    C Data;
   //Data.m_data  = 10;   //错误, 提示指向不明确

   //通过域(::)运算符才可以访问,使用不方便
    Data.A::m_data = 10.1;
    Data.B::m_data = 20;

    std::cout << Data.A::m_data << "   " << Data.B::m_data << std::endl;

    return 0;
}

分析

我们可以通过域 (::) 运算符对同名变量进行读写操作,但是不是很方便,不能直接通过.变量的形式进行操作,形如 Data.m_data

菱形继承

在多重继承中,存在一个很特殊的继承方式,即菱形继承。比如一个类 C 通过继承类 A 和类 B,但是类 A 和类 B 又同时继承于公共基类 N

菱形继承示意图:

菱形继承示意图

这种继承方式也存在数据的二义性,这里的二义性是由于他们间接都有相同的基类导致的。这种菱形继承除了带来二义性之外,还会浪费内存空间

代码如下:

//公共基类
class N
{
public:
    N(int data1, int data2, int data3) :
        m_data1(data1),
        m_data2(data2),
        m_data3(data3)
    {
        std::cout << "call common constructor" << std::endl;
    }
    virtual ~N(){}

    void    display()
    {
        std::cout << m_data1 << std::endl;
    }

public :
    int     m_data1;
    int     m_data2;
    int     m_data3;
};

class A : /*virtual*/ public N
{
public:
    A() :N(11, 12, 13), m_a(1)
    {
        std::cout << "call class A constructor" << std::endl;
    }
    ~A(){}

public :
    int m_a;
};

class B :  /*virtual*/ public N
{
public:
    B() :N(21, 22, 23),m_b(2)
    {
        std::cout << "call class B constructor" << std::endl;
    }
    ~B(){}

public :
    int m_b;
};

class C : public A ,  public B
{
public:
    //负责对基类的初始化
    C() : A(), B(),
        m_c(3)
    {
        std::cout << "call class C constructor" << std::endl;
    }
    void show()
    {
        std::cout << "m_c=" << m_c << std::endl;
    }

 public :
    int m_c;
};

我们通过 VS 自带的内存分析模型工具,得到如下的内存分布模型:

内存分布模型:

内存分布模型

我们发现在类 C 中存在两份的基类 N,分别存在类 A 和类 B 中,如果数据多则严重浪费空间,也不利于维护,我们引用基类 N 中的数据还需要通过域运算符进行区分。例如:

C data;
data.A::m_data1 = 10;
data.B::m_data1 = 10;

虚基类 & 虚继承

为了解决上述菱形继承带来的问题,C++ 中引入了虚基类,其作用是在间接继承共同基类时只保留一份基类成员,虚基类的声明如下:

class A//A 基类
{ ... };

//类B是类A的公用派生类, 类A是类B的虚基类
class B : virtual public A
{  ... };

//类C是类A的公用派生类, 类A是类C的虚基类
class C : virtual public A
{  ... };

虚基类并不是在声明基类时声明的,而是在声明派生类时,指定继承方式声明的。

虚继承是声明类时的一种继承方式,在继承属性前面添加 virtual 关键字。

//此时基类N并不是虚基类,因为声明继承类D时没有指定虚继承方式
class D : public N
{
  ...
};

在内存分析模型中没有 virtual 关键字:

内存模型:

内存模型

虚基类的初始化

这里直接说明结论,对于虚基类的初始化是由最后的派生类中负责初始化。

在最后的派生类中不仅要对直接基类进行初始化,还要负责对虚基类初始化。例如:

// 类A和类B是虚继承方式
class C : public A, public B
{
public:
    //负责对直接基类的初始化 以及虚基类的初始化
    C() : A(), B(), N(31,32,33) ,
        m_c(3)
    {
        std::cout << "call class C constructor" << std::endl;
    }
    void show()
    {
        std::cout << "m_c=" << m_c << std::endl;
    }

public:
    int m_c;
};

虚基类构造次数

C++ 编译系统只执行最后的派生类对基类的构造函数调用,而忽略其他派生类对虚基类的构造函数调用。从而避免对基类数据成员重复初始化。因此,虚基类只会构造一次

若不是普通的继承,则存在重复构造,左边普通继承,右边虚继承,看下图:

构造次数对比:

构造次数对比

有虚基类的派生类中,虚基类只有一个构造函数输出 “call common constructor”。

虚基类的内存分析

我们通过内存模型分析工具,得到最后派生类的内存模型对比图:

左图是有虚基类的派生类 C 的内存分析,右边是菱形继承的内存分析:

内存模型对比图:

内存模型对比图

我们发现虚继承的派生类 C 的内存比非虚继承的派生类 C 内存要小 8 个字节,并且虚基类在派生类中只存在一份,虚基类有 virtual 关键字标识。

因此我们在派生类中引用基类 N 的数据,不需要使用域成员运算符,因为只有一个基类对象。例如:

C data;
data.m_data1 = 10;

虚基类的完整分析代码

//公共基类
class N
{
public:
    N(int data1, int data2, int data3) :
        m_data1(data1),
        m_data2(data2),
        m_data3(data3)
    {
        std::cout << "call common constructor" << std::endl;
    }
    virtual ~N(){}

    void    display()
    {
        std::cout << m_data1 << std::endl;
    }

public:
    int     m_data1;
    int     m_data2;
    int     m_data3;
};

//虚继承方式
class A : virtual public N
{
public:
    A() :N(11, 12, 13), m_a(1)
    {
        std::cout << "call class A constructor" << std::endl;
    }
    ~A(){}

public:
    int m_a;
};

//虚继承方式
class B :  virtual public N
{
public:
    B() :N(21, 22, 23), m_b(2)
    {
        std::cout << "call class B constructor" << std::endl;
    }
    ~B(){}

public:
    int m_b;
};

  // 类A和类B是虚继承方式
class C : public A, public B
{
public:
    //负责对直接基类的初始化 以及虚基类的初始化
    C() : A(), B(), N(31,32,33),
        m_c(3)
    {
        std::cout << "call class C constructor" << std::endl;
    }
    void show()
    {
        std::cout << "m_c=" << m_c << std::endl;
    }

public:
    int m_c;
};

//此时基类N不是虚基类
class D : public N
{
public:
    //负责对基类的初始化
    D() :N(41, 42, 43),
        m_d(4)
    {
        std::cout << "call class D constructor" << std::endl;
    }
    void show()
    {
        std::cout << "m_d=" << m_d << std::endl;
    }

public:
    int m_d;
};

int _tmain(int argc,  _TCHAR* argv[])
{
    C data;
    //直接使用基类数据
    data.m_data1 = 10;

    return 0;
}
t m_c;
};

//此时基类N不是虚基类
class D : public N
{
public:
    //负责对基类的初始化
    D() :N(41, 42, 43),
        m_d(4)
    {
        std::cout << "call class D constructor" << std::endl;
    }
    void show()
    {
        std::cout << "m_d=" << m_d << std::endl;
    }

public:
    int m_d;
};

int _tmain(int argc,  _TCHAR* argv[])
{
    C data;
    //直接使用基类数据
    data.m_data1 = 10;

    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值