c++对象模型(继承,虚拟继承,菱形继承)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_36391130/article/details/81671417

谈及c++对象模型,内容还是比较繁复,为了便于理解,我将循序渐进的按照以下步骤说明:
1.对象模型是什么?
2.单继承下的对象模型。
3.对象模型与内存对齐
4.多重继承
5.菱形继承
6.虚拟继承

1.对象模型是什么
C++中的对象模型指对象的存储布局,那么类里的成员有如下几种:
非静态的成员变量被配置于对象中,排列顺序和声明顺序一致
静态的成员变量则被存储在对象之外;存储在全局的数据段中,永远存在且只存在一份实体;若对其取地址,得到指向其数据类型的指针,而不是指向成员的指针;如果有两个类中声明了名字相同的static变量,那么他们又都存储在程序的数据段,这时候就会导致命名冲突,编译器则会通过name-mangling对其暗中进行编码,以使得不同类的同名的static成员变量获得不同的编码。
普通成员方法被存储在对象之外,
静态的成员方法也被存储在对象之外,
虚函数则通过虚函数表的机制存储(一个类产生若干指向虚函数的指针,这些指针被存放在一个表中,称之为虚函数表;每个类的对象都被添加了一个指针(vptr(vptr的位置相对于成员变量的位置不确定(编译器决定))),这个指针指向这张虚函数表),
如下图所示:
这里写图片描述

有了对象模型的概念,我们就可以开心的去探索C++对象模型了。接下来,我们来写一些代码,通过查看其内存的方法来探究对象模型。

2.具体继承下的对象模型。
如果有一个空类,类里面什么也没有,那么它的大小是多少呢?
下图中:定义空类B,空类D,且D继承自B,然后打印B,D各自的对象大小,再调出各自对象的内存空间
这里写图片描述
观察上图可以看到,尽管B,D都是空类,可是他们的对象都被分配了一个字节的空间。而且这一个空间内是随机值。

这是因为编译器在两个空类的对象中安插进去了一个字节,使得这两个对象在内存中占有唯一的地址

接下来,我们将D的继承方式改为virtual:(虚拟继承详见第6点)
这里写图片描述
这个时候,B的对象依旧是占1字节,而D对象占4字节。

这个现象的原因需要结合两点:1.当语言支持virtual base class 时,会有一些额外负担,这个额外负担体现在一个指针上,它指向虚拟基类子对象或者一个相关的表格,(表格里存储了虚拟基类子对象的偏移量)(在上图中,存储的指针指向一个表格,表格中存储偏移量) 2.某些编译器对虚拟继承的基类做了特殊处理,这种处理结果之下,一个空的虚拟继承的基类子对象被视为子类对象最开头的一部分,也就是说它并不花费额外的空间;
结合以上两点:D虽然是空类,但是因为虚拟继承,它的对象并不空(存了一个指针);于是这个对象就拥有的自己独一无二的地址,也就不需要那一个字节来保证对象地址的唯一了。(如果仍旧添加一个字节,那么D的对象是4+1个字节,又因为内存对齐,D的对象应是占8个字节。实际上不添加一个字节)

了解了上述的内容再来看非空的类,就比较简单了,比如:
这里写图片描述
A类为基类,有两个成员变量;B类为派生类,也有两个成员变量。
B类对象的对象模型就是由基类的成员和自己的成员堆叠而成,非常简单。

如果是虚拟的继承方式呢?
这里写图片描述
上图中,派生类的对象模型中,前四个字节放置一个指针(指向偏移量表),随后放置派生类自己的成员,之后再放置从基类继承而来的成员。(这些排列顺序不确定,随编译器而定)

3.对象模型与内存对齐
看一看内存对齐和继承对对象模型的影响:
这里写图片描述
上图中,在Test类中声明四个变量共占7个字节,求出对象大小为8个字节,其中一个字节的填充。
接下来,我们将_t2 和 _t3变量放到其他类中,并且用两级继承关系再将他们包含在一个类中,看一看那种情况下的对象模型:
这里写图片描述
我们发现,通过继承的方式,派生类的成员变量并没有如同之前一样排列,而是中间放置了三个填充字节,处理器为什么要这样做呢?这样做是为了使得派生类对象中的基类子对象保持其原样性。也就是基类与派生类相互独立,两者的对象模型不受继承关系的影响。

4.多重继承
如下继承关系的类,每个类中都有一个成员变量和一个虚函数。
这里写图片描述

class A
{
public:
    A(int a1 = 1) 
        :_a1(a1){}
    virtual void a_fun1(){}
private:
    int _a1;
};
class B : public A
{
public:
    B(int b1 = 2)
        :_b1(b1){}
    virtual void b_fun1(){}
private:
    int _b1;
};
class C 
{
public:
    C(int c1 = 3)
        :_c1(c1){}
    virtual void c_fun1(){}
private:
    int _c1;
}; 
class D:public B,public C
{
public:
    D(int d1 = 4)
        :_d1(d1){}
    virtual void d_fun1(){}
private:
    int _d1;
};
int main()
{
    A a; B b; C c; D d;
    cout << sizeof(a) << endl;
    cout << sizeof(b) << endl;
    cout << sizeof(c) << endl;
    cout << sizeof(d) << endl;

    return 0;
}

运行结果:
这里写图片描述
画张图看起来就很简洁:
这里写图片描述

5.菱形继承
接下来看看菱形继承,菱形继承就是继承关系像菱形一样。像下图这样的继承关系 :)
这里写图片描述

class A
{
public:
    A(int a1 = 1) 
        :_a1(a1){}
    virtual void a_fun1(){}
private:
    int _a1;
};
class B : public A
{
public:
    B(int b1 = 2)
        :_b1(b1){}
    virtual void b_fun1(){}
private:
    int _b1;
};
class C :public A
{
public:
    C(int c1 = 3)
        :_c1(c1){}
    virtual void c_fun1(){}
private:
    int _c1;
}; 
class D:public B,public C
{
public:
    D(int d1 = 4)
        :_d1(d1){}
    virtual void d_fun1(){}
public:
    int _d1;
};
int main()
{
    A a; B b; C c; D d;
    cout << sizeof(a) << endl;
    cout << sizeof(b) << endl;
    cout << sizeof(c) << endl;
    cout << sizeof(d) << endl;
    return 0;
 }

运行结果:
(从上面的多重继承,我们已经对vptr较为熟悉了,在这里就不再将虚表机制展开说了,我们主要关注各对象的成员变量)
这里写图片描述
从上图中,我们可以理清各对象中的成员都有些什么:
A a:vptr 和 _a1
B b:vptr 和 _a1 和 _b1
C c:vptr 和 _a1 和 _c1
D d:vptr1 和 _a1 和 _b1 加 vptr2 和 _a1 和 _d1
从这里可以得到些什么信息呢?
a,b,c都没什么好说的,我们之前的分析已经对它们做出了解释。在d中,我们发现_a1存了两份,显然,一份是B中来的,一份是C中来的。当然,我们可以加上作用域限定符来分别访问他们,那么,如果想要只存一份 _a1 有没有办法呢?答案是有,看虚拟继承。

6.虚拟继承
将菱形继承中的继承方式改为如下图所示继承方式:
这里写图片描述
代码如下:

class A
{
public:
    A(int a1 = 1) 
        :_a1(a1){}
    virtual void a_fun1(){}
private:
    int _a1;
};
class B : virtual public A
{
public:
    B(int b1 = 2)
        :_b1(b1){}
    virtual void b_fun1(){}
private:
    int _b1;
};
class C : virtual public A
{
public:
    C(int c1 = 3)
        :_c1(c1){}
    virtual void c_fun1(){}
private:
    int _c1;
}; 
class D:public B,public C
{
public:
    D(int d1 = 4)
        :_d1(d1){}
    virtual void d_fun1(){}
public:
    int _d1;
};
int main()
{
    A a; B b; C c; D d;
    cout << sizeof(a) << endl;
    cout << sizeof(b) << endl;
    cout << sizeof(c) << endl;
    cout << sizeof(d) << endl;
    return 0;
 }

运行结果:
这里写图片描述
通过上图分析,除过A的对象,其他虚拟继承之后的类都包含一个指针,这个指针指向一张偏移量表;而且在菱形继承中_a1包含了两份的问题也解决了,上图中内存4里, _a1变量不仅只包含了一份,而且被放置在了d对象最末尾的位置。
上面的图不直观,我们画一下他们的对象模型:
这里写图片描述
那么总结出菱形虚拟继承中一般的规律:
b和c中,先放自己的虚函数表,再放偏移量表的指针,然后是自己的成员变量,最后再放基类对象
d中成员的排布,先放b再放c然后是自己的成员变量,自己的虚函数放在第一个(主)虚表指针指向的虚函数表里,最后放具体菱形继承中重复的成员。

猜你喜欢

转载自blog.csdn.net/qq_36391130/article/details/81671417
今日推荐