C++继承详解之三——菱形继承+虚继承内存对象模型详解vbptr(1)

在我个人学习继承的过程中,在网上查阅了许多资料,这些资料中有关菱形继承的知识都是加了虚函数的,也就是涉及了多态的问题,而我在那个时候并没有学习到多态这一块,所以看很多资料都是云里雾里的,那么这篇文章我想以我自己学习过程中的经验,由简到较难的先分析以下菱形继承,让初学者先对这个问题有一点概念,在后面会由浅入深的继续剖析。

本篇文章不会涉及到多态也就是虚函数的菱形继承,在后面的文章更新中,我会慢慢把这些内容都加进去。

菱形继承(也叫钻石继承)是下面的这种情况:

对应代码如下:

#include <iostream>
using namespace std;

class B
{
public:
    B()
    {
        cout << "B" << endl;
    }
    ~B()
    {
        cout << "~B()" << endl;
    }
private:
    int b;
};
class C1 :public B
{
public:
    C1()
    {
        cout << "C1()" << endl;
    }
    ~C1()
    {
        cout << "~C1()" << endl;
    }
private:
    int c1;
};
class C2 :public B
{
public:
    C2()
    {
        cout << "C2()" << endl;
    }
    ~C2()
    {
        cout << "~C2()" << endl;
    }
private:
    int c2;
};
class D :public C1, public C2
{
public:
    D()
    {
        cout << "D()" << endl;
    }
    ~D()
    {
        cout << "~D()" << endl;
    }
private:
    int d;
};

int main()
{
    cout << sizeof(B) << endl;
    cout << sizeof(C1) << endl;
    cout << sizeof(C2) << endl;
    cout << sizeof(D) << endl;
    return 0;
}

运行结果为:

我们希望上面的代码中D类所对应的对象模型如下:

而实际上上面代码中D类所对应的模型为

菱形继承会造成派生类的数据冗余,比如上例就有D类中包含两个int b这种情况发生。

为了解决菱形继承数据冗余的问题,下面我要引入虚继承的概念。

1.虚继承

虚继承 是面向对象编程中的一种技术,是指一个指定的基类,在继承体系结构中,将其成员数据实例共享给也从这个基类型直接或间接派生的其它类。

虚拟继承是多重继承中特有的概念。虚拟基类是为解决多重继承而出现的。

下图可以看出虚基类和非虚基类在多重继承中的区别

那么为什么要引入虚拟继承呢?

我们已经剖析了一般非虚基类的多重继承得到的派生类的对象模型,那么看看下面的代码会输出什么

#include <iostream>
using namespace std;

class B
{
public:
    B()
    {
        cout << "B" << endl;
    }
    ~B()
    {
        cout << "~B()" << endl;
    }
    int b;
};
class C1 :public B
{
public:
    C1()
    {
        cout << "C1()" << endl;
    }
    ~C1()
    {
        cout << "~C1()" << endl;
    }
private:
    int c1;
};
class C2 :public B
{
public:
    C2()
    {
        cout << "C2()" << endl;
    }
    ~C2()
    {
        cout << "~C2()" << endl;
    }
private:
    int c2;
};
class D :public C1, public C2
{
public:
    D()
    {
        cout << "D()" << endl;
    }
    ~D()
    {
        cout << "~D()" << endl;
    }
    void FunTest()
    {
        b = 10;
    }
private:
    int d;
};

int main()
{
    D d;
    d.FunTest();
    return 0;
}

编译出错,输出

Error   1   error C2385: ambiguous access of ‘b‘    e:\demo\继承\blog\project1\project1\source.cpp    58  1   Project1
    2   IntelliSense: "D::b" is ambiguous   e:\DEMO\继承\blog\Project1\Project1\Source.cpp    58  3   Project1

编译器报错为:不明确的b,即编译器无法分清到底b是继承自C1的还是继承自C2的。

解决上面由于菱形继承而产生二义性与数据冗余的问题,需要用到虚继承。

虚继承的提出就是为了解决多重继承时,可能会保存两份副本的问题,也就是说用了虚继承就只保留了一份副本,但是这个副本是被多重继承的基类所共享的,该怎么实现这个机制呢?

下面我来一步一步的分析这个问题:

1.类中不加数据成员

看下面的代码:

#include <iostream>
using namespace std;

class B //基类
{
public:
    B()
    {
        cout << "B" << endl;
    }
    ~B()
    {
        cout << "~B()" << endl;
    }
};
class C1 :virtual public B
{
public:
    C1()
    {
        cout << "C1()" << endl;
    }
    ~C1()
    {
        cout << "~C1()" << endl;
    }
};
class C2 :virtual public B
{
public:
    C2()
    {
        cout << "C2()" << endl;
    }
    ~C2()
    {
        cout << "~C2()" << endl;
    }
};
class D :public C1, public C2
{
public:
    D()
    {
        cout << "D()" << endl;
    }
    ~D()
    {
        cout << "~D()" << endl;
    }
};

int main()
{
    cout << sizeof(B) << endl;
    cout << sizeof(C1) << endl;
    cout << sizeof(C2) << endl;
    cout << sizeof(D) << endl;
    return 0;
}

输出为:

我们分析一下结果:

class B //基类
{
public:
    B()
    {
        cout << "B" << endl;
    }
    ~B()
    {
        cout << "~B()" << endl;
    }
};

首先,基类中除了构造函数和析构函数没有其他成员了,所以

sizeof(B) = 1;

有的初学者可能会问为什么为1,首先类在内存中的存储是这样的:

如果有一个类B

class B
{
    public:
    int b;
    void fun();
};
int Test()
{
    B b1,b2,b3;
}

那么在内存中模型如下图

所以成员函数是单独存储,并且所有类对象公用的。

那么有人可能要说那sizeof(B)为什么不为0,那是因为编译器要给对象一个地址,就需要区分开所有的类对象,1只是一个占位符,表示这个对象存在,并且让编译器给这个对象分配地址。

现在sizeof(B)的问题解决,下面看C1与C2

class C1 :virtual public B
{
public:
    C1()
    {
        cout << "C1()" << endl;
    }
    ~C1()
    {
        cout << "~C1()" << endl;
    }
};
class C2 :virtual public B
{
public:
    C2()
    {
        cout << "C2()" << endl;
    }
    ~C2()
    {
        cout << "~C2()" << endl;
    }
};

由于C1与C2都是虚拟继承,故会在C1,C2内存起始处存放一个vbptr,为指向虚基类表的指针。

那么这个指针vbptr指向什么呢?

我们在main函数中生成一个C1类对象c1

int main()
{
    C1 c1;
    return 0;
}

在内存中查看c1究竟存了什么

由上图我们可以看出,c1占了四个字节,存了一个指针变量,指针变量的内容就是c1的vbptr指向的虚基类表的地址。

那我们去c1.vbptr指向的虚基类表中查看下究竟存了什么。

可以看到,这个虚基类表有八个字节,分别存的为0和4。

那么0和4代表的都是什么呢?

虚基类表存放的为两个偏移地址,分别为0和4。

其中0表示c1对象地址相对与存放vptr指针的地址的偏移量

可以用&c1->vbptr_c1表示。

其中vptr指的是指向虚表的指针,而虚表是定义了虚函数后才有的,由于我们这里没有定义虚函数,当然也就没有vptr指针,所以偏移量为0.

8表示c1对象中基类对象部分相对于存放vbptr指针的地址的偏移量

可以用&c1(B)-&vbpt表示,其中&c1(B)表示对象c1中基类B部分的地址。

c2的内存布局与c1一样,因为C1,C2都是虚继承自B基类,且C1,C2都没有加数据成员。

现在大家都对

sizeof(C1) = 4;
sizeof(C2) = 4;

没有什么疑虑了吧。

总结一下,因为C1,C2是虚继承自基类B,所以编译器会给C1,C2中生成一个指针vbptr指向一个虚基类表,即指针vbptr的值是虚基类表的地址。

而这个虚基类表中存储的是偏移量。

这个表中分两部分,第一部分存储的是对象相对于存放vptr指针的偏移量,可以用&(对象名)->vbptr_(对象名)来表示。对c1对象来说,可以用&c1->vbprt_c1来表示。

vptr指针是指向虚表的指针,而只有在类中定义了虚函数才会有虚表,因为我们这个例子中没有定义虚函数,所以没有vptr指针,所以第一部分偏移量均为0。

表的第二部分存储的是对象中基类对象部分相对于存放vbptr指针的地址的偏移量,我们知道在本例中基类对象与指针偏移量就是指针的大小。

在内存中看d究竟存了什么

如上图所示,d的内存中存了两个指针,我们进入指针存放的地址看里面究竟是什么:

如上图所示,d中存放了两个虚基类指针,每个虚基类表中存储了偏移量。

说了这么多,还是太抽象了。

现在看一下内存布局:

2.类中加数据成员

上面我们剖析了不加数据成员的菱形继承,下面剖析一下加数据成员的,这样可以更清晰的看出内存布局

#include <iostream>
using namespace std;

class B
{
public:
    B()
    {
        cout << "B" << endl;
    }
    ~B()
    {
        cout << "~B()" << endl;
    }
    int b;
};
class C1 :virtual public B
{
public:
    C1()
    {
        cout << "C1()" << endl;
    }
    ~C1()
    {
        cout << "~C1()" << endl;
    }
    int c1;
};
class C2 :virtual public B
{
public:
    C2()
    {
        cout << "C2()" << endl;
    }
    ~C2()
    {
        cout << "~C2()" << endl;
    }
    int c2;
};
class D :public C1, public C2
{
public:
    D()
    {
        cout << "D()" << endl;
    }
    ~D()
    {
        cout << "~D()" << endl;
    }
    void fun()
    {
        b = 0;
        c1 = 1;
        c2 = 2;
        d = 3;
    }
    int d;
};

int main()
{
    cout << sizeof(B) << endl;
    cout << sizeof(C1) << endl;
    cout << sizeof(C2) << endl;
    cout << sizeof(D) << endl;
    D d;
    d.fun();
    return 0;
}

这次的输出为:

这次我们再剖析下各个类的输出大小

class C1 :virtual public B
{
public:
    C1()
    {
        cout << "C1()" << endl;
    }
    ~C1()
    {
        cout << "~C1()" << endl;
    }
    int c1;
};

首先C1,C2都是虚继承自基类B的,所以我就一起剖析了。

首先B占四个字节没有问题,因为B类中有int b数据成员,所以B类占四个字节。

那么C1,C2是虚继承自B类的,所以C1,C2的内存布局是相似的,在这里我只剖析一下C1。

我们在C1类中加一个Fun成员函数,为了更清楚的看到内存布局

class C1 :virtual public B
{
public:
    C1()
    {
        cout << "C1()" << endl;
    }
    ~C1()
    {
        cout << "~C1()" << endl;
    }
    void Fun()
    {
        b = 5;
        c1 = 6;
    }
    int c1;
};
int main()
{
    C1 c1;
    c1.Fun();
    return 0;
}

在main函数中生成对象c1,那么在内存中的c1是什么样呢?

我们通过vbptr指针进入c1的虚基类表中

由上面两图我们可以画出c1的内存布局

C2跟C1一样。

所以

sizeof(C1) == 12;
sizeof(C2) == 12;

现在来看看D类的内存布局

class D :public C1, public C2
{
public:
    D()
    {
        cout << "D()" << endl;
    }
    ~D()
    {
        cout << "~D()" << endl;
    }
    void fun()//fun()函数主要帮助我们看D类的内存布局
    {
        b = 0;//基类数据成员
        c1 = 1;//C1类数据成员
        c2 = 2;//C2类数据成员
        d = 3;//D类自己的数据成员
    }
    int d;
};

我们进入内存中看d

可以看出,前四个字节是vbptr指针,然后是c1类,+另一个vbptr指针+c2类+D类数据成员d+基类B这样的结构

我们进入第一个vbptr指针中看

可得出偏移量分别为0(因为没有虚函数),14

再进入第二个vbptr指针中

可以看出偏移量分别为0(因为没有虚函数),12

好了,到这里我们可以画出D类的内存布局了

所以,

sizeof(D) == 24;

这就是不带虚函数的菱形继承,关于带虚函数的菱形继承因为涉及到多态的知识,我放在后面会专门再写一篇文章的。

如果有问题,欢迎评论私信指正。

有更加深入的想法也欢迎讨论。

时间: 2024-08-08 23:11:44

C++继承详解之三——菱形继承+虚继承内存对象模型详解vbptr(1)的相关文章

虚继承

------------------siwuxie095 看如下实例: 有 4 个类,其中:类 A 是父类,类 B 和 类 C 都继承 类 A, 而 类 D 继承了 类 B 和 类 C,称这种继承关系为 菱形继承 在菱形继承中,既有多继承,又有多重继承: 那么问题来了: 当实例化 D 的对象时,发现:D 是从 B 继承来的,B 是从 A 继承来的, D 也是从 C 继承来的,C 是从 A 继承来的 这样,D 中将含有两个完全一样的 A 的数据,这种情况是不能容忍的, 因为在一个对象中有两份完全相

【整理】C++虚函数及其继承、虚继承类大小

参考文章: http://blog.chinaunix.net/uid-25132162-id-1564955.html http://blog.csdn.net/haoel/article/details/1948051/ 一.虚函数与继承 1.空类,空类单继承,空类多继承的sizeof #include <iostream> using namespace std; class Base1 { }; class Base2 { }; class Derived1:public Base1

C++中对C的扩展学习新增内容———面向对象(继承)多继承和虚继承

多继承和虚继承 1.多继承的优缺点 (1) 多继承可以复用多个类的代码(函数,变量). (2) 多继承会带来二义性问题. // 1. 多继承优缺点 class Flyable { public: void fly() { cout << "飞翔..." << endl; } void sound() { cout << "嗖嗖..." << endl; } }; class Runnable { public: voi

多重继承,虚继承,MI继承中虚继承中构造函数的调用情况

先来测试一些普通的多重继承.其实这个是显而易见的. 测试代码: [cpp] view plain copy print? //测试多重继承中派生类的构造函数的调用顺序何时调用 //Fedora20 gcc version=4.8.2 #include <iostream> using namespace std; class base { public: base() { cout<<"base created!"<<endl; } ~base()

看到的关于虚函数继承及虚继承解释比较好的文章的复制

(来源于:http://blog.chinaunix.net/uid-25132162-id-1564955.html) 1.空类,空类单继承,空类多继承的sizeof #include <iostream> using namespace std; class Base1 { }; class Base2 { }; class Derived1:public Base1 { }; class Derived2:public Base1, public Base2 { }; int main(

C++ 继承之虚继承与普通继承的内存分布

仅供互相学习,请勿喷,有观点欢迎指出~ class A { virtual void aa(){}; }; class B : public virtual A { char j[3]; //加入一个变量是为了看清楚class中的vfptr放在什么位置 public: virtual void bb(){}; }; class C : public virtual A { char i[3]; public: virtual void cc(){}; }; class C1 : public A

C++ Primer 学习笔记_96_用于大型程序的工具 --多重继承与虚继承[续1]

用于大型程序的工具 --多重继承与虚继承[续1] 四.多重继承下的类作用域 成员函数中使用的名字和查找首先在函数本身进行,如果不能在本地找到名字,就继续在本类中查找,然后依次查找每个基类.在多重继承下,查找同时检察所有的基类继承子树 -- 在我们的例子中,并行查找 Endangered子树和Bear/ZooAnimal子树.如果在多个子树中找到该名字,则那个名字的使用必须显式指定使用哪个基类;否则,该名字的使用是二义性的. [小心地雷] 当一个类有多个基类的时候,通过对所有直接基类同时进行名字查

C++基础6 【继承】 类型兼容 satatic 多继承 虚继承 【多态】 案例 虚析构函数 重载重写重定义

[继承] 继承的访问控制域 图 类型兼容性原则  指针 与 引用 用子类直接初始化父类 类的继承模型示意 图 [继承结论] [非常重要的概念] 继承与组合混搭情况下,构造和析构调用原则 原则:先构造父类,再构造成员变量.最后构造自己 先析构自己,在析构成员变量.最后析构父类 继承中,同名的成员变量的处理办法 继承中,同名的成员函数的处理办法 派生类中的static关键字 如果静态成员变量,你没有使用,也没有初始化的话 编译不会报错 经典错误 : 类中函数默认是private的,无法在外部访问 具

C++ 虚继承的对象模型

我们知道,虚继承的基类在类的层次结构中只可能出现一个实例.虚基类在类的层次结构中的位置是不能固定的,因为继承了虚基类的类可能会再次被其他类多继承. 比如class A: virtual T{} 这时T的位置如果相对于A是固定值的话,假设偏移是X,当再有个类 class B:virtual T{} :这时假设在B里面T的偏移是固定的Y,而当再有一个类,class C: B, A {} 的时候,像这种情况,T在C里面的实例就只能有一份,由B和A共享继承,显然这时候的T的位置相对于A或者B的偏移和这两