天天看点

C++ 对象的内存布局(上)

简而言之,我们一个类可能会有如下的影响因素:

1)成员变量

2)虚函数(产生虚函数表)

3)单一继承(只继承于一个类)

4)多重继承(继承多个类)

5)重复继承(继承的多个父类中其父类有相同的超类)

6)虚拟继承(使用virtual方式继承,为了保证继承后父类的内存布局只会存在一份)

上述的东西通常是c++这门语言在语义方面对对象内部的影响因素,当然,还会有编译器的影响(比如优化),还有字节对齐的影响。在这里我们都不讨论,我们只讨论c++语言上的影响。

本篇文章着重讨论下述几个情况下的c++对象的内存布局情况。

1)<b>单一的一般继承</b>(带成员变量、虚函数、虚函数覆盖)

2)<b>单一的虚拟继承</b>(带成员变量、虚函数、虚函数覆盖)

3)<b>多重继承</b>(带成员变量、虚函数、虚函数覆盖)

4)<b>重复多重继承</b>(带成员变量、虚函数、虚函数覆盖)

5)<b>钻石型的虚拟多重继承</b>(带成员变量、虚函数、虚函数覆盖)

我们的目标就是,让事情越来越复杂。

我们简单地复习一下,我们可以通过对象的地址来取得虚函数表的地址,如:

我们同样可以用这种方式来取得整个对象实例的内存布局。因为这些东西在内存中都是连续分布的,我们只需要使用适当的地址偏移量,我们就可以获得整个内存对象的布局。

本篇文章中的例程或内存布局主要使用如下编译器和系统:

1)windows 7 和 vc++ 2010

2)cygwin 和 g++ 3.4.4

下面,我们假设有如下所示的一个继承关系:

C++ 对象的内存布局(上)

请注意,在这个继承关系中,父类,子类,孙子类都有自己的一个成员变量。而了类覆盖了父类的f()方法,孙子类覆盖了子类的g_child()及其超类的f()。

我们的源程序如下所示:

C++ 对象的内存布局(上)

使用图片表示如下:(红色部分为本人修改)

C++ 对象的内存布局(上)

可见以下几个方面:

1)虚函数表在最前面的位置。

2)成员变量根据其继承和声明顺序依次放在后面。

3)在单一的继承中,被overwrite的虚函数在虚函数表中得到了更新。

下面,再让我们来看看多重继承中的情况,假设有下面这样一个类的继承关系。注意:子类只overwrite了父类的f()函数,而还有一个是自己的函数(我们这样做的目的是为了用g1()作为一个标记来标明子类的虚函数表)。而且每个类中都有一个自己的成员变量:

C++ 对象的内存布局(上)

我们的类继承的源代码如下所示:父类的成员初始为10,20,30,子类的为100

我们通过上面的程序来查看子类实例的内存布局:上面程序中,注意我使用了一个s变量,其中用到了sizof(base)来找下一个类的偏移量。(因为我声明的是int成员,所以是4个字节,所以没有对齐问题。关于内存的对齐问题,大家可以自行试验,我在这里就不多说了)

C++ 对象的内存布局(上)

使用图片表示是下面这个样子:

C++ 对象的内存布局(上)

我们可以看到:

1) 每个父类都有自己的虚表。

2) 子类的成员函数被放到了第一个父类的表中。

3) 内存布局中,其父类布局依次按声明顺序排列。

4) 每个父类的虚表中的f()函数都被overwrite成了子类的f()。这样做就是为了解决不同的父类类型的指针指向同一个子类实例,而能够调用到实际的函数。

继续阅读