虚继承下对象的内存分布

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;
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69

编译出错,输出

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

 
 
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

编译器报错为:不明确的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;
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60

输出为: 
这里写图片描述 
我们分析一下结果:

class B //基类
{
public:
    B()
    {
        cout << "B" << endl;
    }
    ~B()
    {
        cout << "~B()" << endl;
    }
};
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

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

sizeof(B) = 1;
 
 
  • 1
  • 1

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

class B
{
    public:
    int b;
    void fun();
};
int Test()
{
    B b1,b2,b3;
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

那么在内存中模型如下图 
这里写图片描述 
  所以成员函数是单独存储,并且所有类对象公用的。 
  那么有人可能要说那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;
    }
};
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

  由于C1与C2都是虚拟继承,故会在C1,C2内存起始处存放一个vbptr,为指向虚基类表的指针。 
  那么这个指针vbptr指向什么呢? 
  我们在main函数中生成一个C1类对象c1

int main()
{
    C1 c1;
    return 0;
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

在内存中查看c1究竟存了什么 
这里写图片描述 
  由上图我们可以看出,c1占了四个字节,存了一个指针变量,指针变量的内容就是c1的vbptr指向的虚基类表的地址。 
  那我们去c1.vbptr指向的虚基类表中查看下究竟存了什么。

这里写图片描述 
可以看到,这个虚基类表有八个字节,分别存的为0和4。 
  那么0和4代表的都是什么呢? 
   虚基类表存放的为两个偏移地址,分别为0和4。

  其中0表示c1对象地址相对于存放vptr指针的地址的偏移量 
    可以用&c1->vbptr_c1表示。

  其中vptr指的是指向虚表的指针,而虚表是定义了虚函数后才有的,由于我们这里没有定义虚函数,当然也就没有vptr指针,所以偏移量为0. 
  4表示c1对象中基类对象部分相对于存放vbptr指针的地址的偏移量 
  可以用&c1(B)-&vbpt表示,其中&c1(B)表示对象c1中基类B部分的地址。 
  c2的内存布局与c1一样,因为C1,C2都是虚继承自B基类,且C1,C2都没有加数据成员。 
这里写图片描述 
现在大家都对

sizeof(C1) = 4;
sizeof(C2) = 4;
 
 
  • 1
  • 2
  • 1
  • 2

  没有什么疑虑了吧。 
  总结一下,因为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;
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73

这次的输出为: 
这里写图片描述 
这次我们再剖析下各个类的输出大小

class C1 :virtual public B
{
public:
    C1()
    {
        cout << "C1()" << endl;
    }
    ~C1()
    {
        cout << "~C1()" << endl;
    }
    int c1;
};
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

  首先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;
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

在main函数中生成对象c1,那么在内存中的c1是什么样呢? 
这里写图片描述 
我们通过vbptr指针进入c1的虚基类表中 
这里写图片描述 
由上面两图我们可以画出c1的内存布局 
这里写图片描述 
C2跟C1一样。 
所以

sizeof(C1) == 12;
sizeof(C2) == 12;
 
 
  • 1
  • 2
  • 1
  • 2

现在来看看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;
};
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

我们进入内存中看d 
这里写图片描述 
可以看出,前四个字节是vbptr指针,然后是c1类,+另一个vbptr指针+c2类+D类数据成员d+基类B这样的结构 
我们进入第一个vbptr指针中看 
这里写图片描述 
可得出偏移量分别为0(因为没有虚函数),0x14 == 20 
再进入第二个vbptr指针中 
这里写图片描述 
可以看出偏移量分别为0(因为没有虚函数),0x0c == 12 
好了,到这里我们可以画出D类的内存布局了 
这里写图片描述 
所以,

sizeof(D) == 24;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值