你必须知道的C++继承内存分布结构

此文针对C++多种继承方式下的内存分布结构进行一系列验证!若有错误,感激指正!
由于此文的重点在于内存分布,简便起见,所以没有对内存对齐进行详细的阐明。

工具

  • visual studio 2019 (64位)

预备工作

  • 第一步
    你必须知道的C++继承内存分布结构_第1张图片
    右键你的源文件,选择“属性”,弹出第二步所示的对话框。
  • 第二步
    你必须知道的C++继承内存分布结构_第2张图片
    选择“命令行”,并在右边的“其他选项”中输入如下格式:
    /d1 reportAllClassLayout,它可以看到所有相关类的内存布局;
    若写/d1 reportSingleClassLayoutXXX(XXX为类名),则只会打出指定类XXX的内存布局。

正式工作

共分为单继承,多重继承,菱形继承三种情况,每种情况会进行细分。

  • 首先展示的是一个基类
class Base1 {
public:
	int Base1_a;
	virtual void myfunc1() { cout << "I am Base1" << endl; };
};

它的对应内存结构如下图:
你必须知道的C++继承内存分布结构_第3张图片
根据上图可知,在Base1基类中,有一个int型Base1_a变量和一个虚函数表指针,在该编译器中,虚函数表指针被放在起始处。前面的对应数字表示对应变量相对于起始处的偏移量。再观察上图下方的虚函数表,第一个0表示该虚函数表相对于起始处的偏移量,第二个0表示虚函数表中虚函数的索引号。可见,虚函数表中只有一个虚函数myfunc1,符合事实。

  • 接下来,我们定义一个派生类

一、单继承

3种情况

  1. 没有虚函数
class Derived :public Base1 {
public:
	int Derived_a;
};

它的对应内存结构如下图:
你必须知道的C++继承内存分布结构_第4张图片
由上图可以看到,虚表指针被继承了,且仍位于内存分布的起始处,接着是基类的成员变量Base1_a,最后是子类的成员变量Derived_a。再观察上图下方的虚函数表,由于派生类并没有任何虚函数,故直接继承基类的虚函数表。

  1. 重写基类的虚函数
class Derived :public Base1 {
public:
	int Derived_a;
	virtual void myfunc1() { cout << "I am Derived" << endl; };
};

它的对应内存结构如下图:
你必须知道的C++继承内存分布结构_第5张图片
大体上和第一种情况相似,但是由于派生类改写了基类的虚函数myfunc1,所以虚函数表中的虚函数是Derived的myfunc1。

  1. 派生类不重写基类的虚函数,并新添加一个虚函数
class Derived :public Base1 {
public:
	int Derived_a;
	virtual void myfunc3() { cout << "I am Derived" << endl; };
};

它的内存分布结构如下图:
你必须知道的C++继承内存分布结构_第6张图片
与上述两种情况大致相似,同样是虚函数表不同。由于此情况并没有改写基类的虚函数,且新添加了虚函数myfunc3,故在虚函数表中,索引为0的是基类的虚函数myfunc1,索引为1的是派生类的虚函数myfunc3。

二、多重继承

第二个基类定义如下:

class Base2 {
public:
	int Base2_a;
	virtual void myfunc2() { cout << "I am Base2" << endl; };
};

分四种情况:

  1. 派生类中没有虚函数
class Derived :public Base1,public Base2 {
public:
	int Derived_a;
};

它的内存分布结构如下图:
你必须知道的C++继承内存分布结构_第7张图片
由上图可知,在多重继承中,有n个虚函数表以及n个虚函数表指针,这里n表示含有虚函数的基类的个数,按照继承的顺序,首先是基类Base1,其中有对应的虚函数表指针以及Base1的变量Base1_a,接着是基类Base2,其中有对应的虚函数表指针以及Base2的变量Base2_a,最后才是派生类Derived的成员Derived_a。接着观察上图下方的两个虚函数表,第一个表与前述的虚函数表相似,而第二个表中,-16表示该表相对于起始处的偏移量,由内存分布结构中前面的序号列可以看出。

  1. 派生类中有一个虚函数,且不重写基类的虚函数
class Derived :public Base1,public Base2 {
public:
	int Derived_a;
    virtual void myfunc3() { cout << "I am Derived" << endl; };
};

它的内存分布结构如下图:
你必须知道的C++继承内存分布结构_第8张图片
与第一种情况大致相似,唯一不同的是,由于派生类新添加了一个虚函数,所以在第一个基类对应的虚函数表中的末尾添加了此虚函数myfunc3。由此我们可以推断出,在多重继承中,若派生类新添加了虚函数,会添加到第一个含有虚函数的基类所对应的虚函数表,(这里强调第一个是因为若第一个基类不含虚函数,那么新添加的虚函数不会使得第一个基类增加一个虚函数表和指向它的指针)此时内存分布结构也会发生变化,不再按照继承顺序排列,具体见第4中情况。

  1. 派生类会改写基类的x个虚函数
class Derived :public Base1,public Base2 {
public:
	int Derived_a;
    virtual void myfunc1() { cout << "I am Derived" << endl; };
	virtual void myfunc2() { cout << "I am Derived" << endl; };
};

它的内存分布结构如下图:
你必须知道的C++继承内存分布结构_第9张图片
内存分布结构图与第二种情况相似,但是两个虚函数表却发生了变化,由于派生类改写了两个基类的虚函数myfunc1和myfunc2,所以对应的基类虚函数表中的虚函数被派生类覆盖。若此处只改写myfunc1,那么第一个虚函数表中对应虚函数被覆盖,第二个表中不变,但若此处只改写myfunc2,则第二个表中对应虚函数被覆盖,而第一个表不变。具体的图此处就不再展示,感兴趣的朋友可以自己尝试一下。

  1. 若基类Base1中没有虚函数,基类Base2中有,观察内存分布是否仍然按照继承顺序排列

此时,基类Base1改写如下:

class Base1 {
public:
	int Base1_a;
};

基类Base2不变,那么此时内存分布结构如下图:
你必须知道的C++继承内存分布结构_第10张图片
由上图可知,在多重继承中,按照继承顺序排列的前提是所有基类都含有虚函数,否则含有虚函数的基类按顺序排在前面,不含虚函数的基类按顺序排在后面,且派生类新添加的虚函数是添加到第一个含有虚函数的基类所对应的虚函数表中。

三、菱形继承
菱形继承中用到了虚继承,是为了解决最下面的派生类中有最上面的基类的多份实例。

class Base {
public:
	int Base_a;
	virtual void myfunc1() = 0;
};
class Base1: public virtual Base {
public:
	int Base1_a;
	virtual void myfunc1() { cout << "I am Base1" << endl; };
};

class Base2: public virtual Base {
public:
	int Base2_a;
	virtual void myfunc2() { cout << "I am Base2" << endl; };
};

class Derived :public Base1,public Base2 {
public:
	int Derived_a;
    virtual void myfunc3() { cout << "I am Derived" << endl; };
};

对应的内存分布结构如下图:
你必须知道的C++继承内存分布结构_第11张图片 你必须知道的C++继承内存分布结构_第12张图片
由上图可知,在原有的虚函数表指针vfptr存在的情况下,多加了虚基类表指针vbptr,用来指向虚基类。而且和多重继承的情况部分相似,只不过分为了共有和私有的部分,共有的部分也就是虚基类的部分,被放到了最下面,而私有的部分仍然按照含虚函数的基类按顺序放在前面,不含虚函数的基类按顺序放在后面。且若派生类对基类的虚函数有修改,则被修改的基类的虚函数表中对应的函数被派生类覆盖,如若没有修改,而是新添加了虚函数,那么此虚函数将添加到第一个含有虚函数的基类所对应的虚函数表的末尾。
接下来我们观察右图中的虚基类表,每一个虚基类表的0号索引所对应的是对应基类的虚函数表指针vfptr相对于该虚基类表指针vbptr的偏移量,而1号索引对应的是虚基类相对于该基类的虚基类表指针vbptr的偏移量。虚函数表仍然可按照前述的进行理解。具体的读者可以按照内存分布结构的序号列来进行验证。

总结

  • 针对单继承而言
    若基类含有虚函数,则单继承中继承基类的虚函数表,且只有1个虚函数表,且单继承的内存分布按照基类->派生类这样的顺序进行排布。若派生类对基类的虚函数进行改写,则虚函数表中对应位置的虚函数将被派生类改写的虚函数覆盖。
  • 针对多重继承而言
    内存分布结构为含有虚函数的基类按顺序排在前列,不含虚函数的基类按继承顺序排在后列。若派生类对某基类的虚函数有改写,那么该基类对应的虚函数表中的对应位置被派生类的虚函数覆盖。若派生类新添加了虚函数,那么该虚函数添加到第一个含有虚函数的基类所对应的虚函数表中的末尾。
  • 针对菱形继承而言
    菱形继承涉及到虚继承,所有内存分布中除了虚函数表指针vfptr外,还存在虚基类表指针vbptr,指向虚基类表。虚基类称为共有部分,一般而言放在内存分布结构的最后面。

你可能感兴趣的:(C++继承内存分布结构,C++,内存分布,继承)