C++的内存布局

    本文将介绍的是C++的内存布局。C++的很多优良特性(比如运行时多态),都是因为C++的虚函数机制,它让C++变的很强大;但同时,它也让C++变的“很难学习”。本文重点讨论的是C++中存在继承和虚函数的情况(因为不存在虚函数的情况比较简单,将简单介绍),有以下几种:

  •     单重继承
  •     多重继承(无共同祖先)
  •     多重继承(有共同祖先)
  •     虚拟继承

    内存布局,个人理解就是变量,函数等,在内存中存储顺序的展现以及这个展现顺序的规则。一句话就是,变量、函数在内存中如何存储。在进行本文讨论之前,先通过在无继承类的内存布局,对C++虚函数的存储结构虚表进行讨论,并且本文中的例子都是通过gun gcc compiler编译。

无继承的内存布局

    假设我们存在一个类base,它有两个整型变量age和sex,然后还有两个虚函数function_1和function_2,一个接口函数function_3,那么base的定义就是下边这样:

C++的内存布局_第1张图片

为了测试base类的内存布局,我们定义一个函数指针function:
typedef void(*function)(void);
然后用以下代码来打印出base中各个变量,函数的地址,以及该地址的函数名称:

C++的内存布局_第2张图片

    为了容易阅读,对结构(int*)*(int*)(&base_exp)进行简单说明,从右向左,首先&base_exp取base_exp的地址,假设为A,然后(int*)(&base_exp)将地址A转换为int*指针类型,*(int*)(&base_exp)取A指向的内容B,然后(int*)*(int*)(&base_exp)将B转换为指针int*类型。就是说,A指向B,而地址B储存的也是一个地址,其实B就是该实例的虚表地址。

对上边的代码进行编译运行,得到如下的输出: 
C++的内存布局_第3张图片
从输出以及我们的测试代码可以得到以下的结论:

  • base实例的大小占用12字节。
  • 类中非虚成员函数(function_3)地址不存在于该类的内存布局中。
  • 类中虚成员函数地址按照定义顺序存储于该实例的虚表中,而该虚表的地址在实例内存布局的最开始。
  • 类的成员变量按定义顺序存储于虚表结构的后边。
  • “A指向B,而地址B储存的也是一个地址,其实B就是该实例的虚表地址”,将这句话放到这个实例中,就是说base_exp的地址A(0x22ff14)指向地址B(0x472c38),B就是该实例的虚表地址,而从地址B开始就是存储该实例虚函数的地址。

接下来看看有继承情况的内存布局情况

单重继承的内存布局

在原base类的基础上,我们为其增加子类derive_1,在derive_1类中,对base类中的虚函数function_1进行实现,然后新增虚函数function_4,增加成员变量position,那么derive_1类的实现应该像下边这样:

C++的内存布局_第4张图片

有了上边的介绍,我们直接写出下边的测试代码:

C++的内存布局_第5张图片

编译运行得到以下输出:

C++的内存布局_第6张图片

由以上输出可以看出:

  • derive_exp的大小为16字节
  • 类的虚表地址存在于该类内存布局的最开始,虚函数的顺序按照父类、子类的定义顺序
  • 类的成员变量根据父类、子类以及在类中的定义顺序存储

    虚表中的两个地址0x472cb8、0x472cbc存放的是类的虚虚构函数,没有打印出来是因为这会导致程序运行错误,可以根据类似的测试方式打印出来试试。

多重继承无共同祖先的内存布局

在原来的base类、derive_1类的基础上增加base_1类,形成derive_1的多重继承。Base_1类的实现如下:

C++的内存布局_第7张图片

测试代码如下:

C++的内存布局_第8张图片

编译运行,输出如下:

C++的内存布局_第9张图片

从以上输出可以得出:

  • derive_exp的大小为24
  • 先是子类与第一个父类base的虚函数地址,虚析构函数地址(0x472d50、0x472d54),以及继承自父类的成员变量,然后是第二个父类base_1的虚函数地址和成员变量,最后是子类自己的变量
  • 类的虚函数以及成员变量在内存中的布局跟父类继承的先后顺序相同

多重继承有共同祖先

为了出现共同的祖先,这个我们需要增加base的一个子类derive类,还有derive和derive_1的共同子类son。定义如下:

C++的内存布局_第10张图片

测试代码如下:

C++的内存布局_第11张图片

编译运行,测试结果如下:

C++的内存布局_第12张图片

从以上输出可以看出:

  • son_exp的大小为36
  • 开始是son继承自derive_1和base的虚函数和成员变量,然后是son继承自derive和base的虚函数和成员变量,最后是son自己的成员变量,在整个所有的继承过程中,都是最先继承、最先定义的出现在最前边
  • Base类的虚函数以及成员变量,由于是derive和derive_1的共同祖先,每个都出现了两份的实例。因此,我们在测试代码中调用的时候使用son_exp.derive::age这个形式来标识age继承于哪个直接父类,直接son_exp.age调用将会出错。由于这个原因,从son的角度来看,age有可能有两个值,分别继承于两个父类,这个是我们不愿意看到的,因此有了下边将要测试的虚拟继承
  • 同样,在虚函数表中空的地址上存放的依然是虚析构函数的地址

虚拟继承的内存布局

继承的关系如上例,不过这次全部改为虚拟继承,继承关系如下:

C++的内存布局_第13张图片

测试代码修改如下:

C++的内存布局_第14张图片

编译运行后输出如下:

C++的内存布局_第15张图片

  • 从以上输出可以看出,虚拟继承与上例的区别:继承自共同祖先的虚函数和变量放在了布局的最后边,并且只保留了一个实例,而其他的继承布局跟上例相同。

你可能感兴趣的:(C++的内存布局)