此文针对C++多种继承方式下的内存分布结构进行一系列验证!若有错误,感激指正!
由于此文的重点在于内存分布,简便起见,所以没有对内存对齐进行详细的阐明。
共分为单继承,多重继承,菱形继承三种情况,每种情况会进行细分。
class Base1 {
public:
int Base1_a;
virtual void myfunc1() { cout << "I am Base1" << endl; };
};
它的对应内存结构如下图:
根据上图可知,在Base1基类中,有一个int型Base1_a变量和一个虚函数表指针,在该编译器中,虚函数表指针被放在起始处。前面的对应数字表示对应变量相对于起始处的偏移量。再观察上图下方的虚函数表,第一个0表示该虚函数表相对于起始处的偏移量,第二个0表示虚函数表中虚函数的索引号。可见,虚函数表中只有一个虚函数myfunc1,符合事实。
一、单继承
分3种情况
class Derived :public Base1 {
public:
int Derived_a;
};
它的对应内存结构如下图:
由上图可以看到,虚表指针被继承了,且仍位于内存分布的起始处,接着是基类的成员变量Base1_a,最后是子类的成员变量Derived_a。再观察上图下方的虚函数表,由于派生类并没有任何虚函数,故直接继承基类的虚函数表。
class Derived :public Base1 {
public:
int Derived_a;
virtual void myfunc1() { cout << "I am Derived" << endl; };
};
它的对应内存结构如下图:
大体上和第一种情况相似,但是由于派生类改写了基类的虚函数myfunc1,所以虚函数表中的虚函数是Derived的myfunc1。
class Derived :public Base1 {
public:
int Derived_a;
virtual void myfunc3() { cout << "I am Derived" << endl; };
};
它的内存分布结构如下图:
与上述两种情况大致相似,同样是虚函数表不同。由于此情况并没有改写基类的虚函数,且新添加了虚函数myfunc3,故在虚函数表中,索引为0的是基类的虚函数myfunc1,索引为1的是派生类的虚函数myfunc3。
二、多重继承
第二个基类定义如下:
class Base2 {
public:
int Base2_a;
virtual void myfunc2() { cout << "I am Base2" << endl; };
};
分四种情况:
class Derived :public Base1,public Base2 {
public:
int Derived_a;
};
它的内存分布结构如下图:
由上图可知,在多重继承中,有n个虚函数表以及n个虚函数表指针,这里n表示含有虚函数的基类的个数,按照继承的顺序,首先是基类Base1,其中有对应的虚函数表指针以及Base1的变量Base1_a,接着是基类Base2,其中有对应的虚函数表指针以及Base2的变量Base2_a,最后才是派生类Derived的成员Derived_a。接着观察上图下方的两个虚函数表,第一个表与前述的虚函数表相似,而第二个表中,-16表示该表相对于起始处的偏移量,由内存分布结构中前面的序号列可以看出。
class Derived :public Base1,public Base2 {
public:
int Derived_a;
virtual void myfunc3() { cout << "I am Derived" << endl; };
};
它的内存分布结构如下图:
与第一种情况大致相似,唯一不同的是,由于派生类新添加了一个虚函数,所以在第一个基类对应的虚函数表中的末尾添加了此虚函数myfunc3。由此我们可以推断出,在多重继承中,若派生类新添加了虚函数,会添加到第一个含有虚函数的基类所对应的虚函数表,(这里强调第一个是因为若第一个基类不含虚函数,那么新添加的虚函数不会使得第一个基类增加一个虚函数表和指向它的指针)此时内存分布结构也会发生变化,不再按照继承顺序排列,具体见第4中情况。
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; };
};
它的内存分布结构如下图:
内存分布结构图与第二种情况相似,但是两个虚函数表却发生了变化,由于派生类改写了两个基类的虚函数myfunc1和myfunc2,所以对应的基类虚函数表中的虚函数被派生类覆盖。若此处只改写myfunc1,那么第一个虚函数表中对应虚函数被覆盖,第二个表中不变,但若此处只改写myfunc2,则第二个表中对应虚函数被覆盖,而第一个表不变。具体的图此处就不再展示,感兴趣的朋友可以自己尝试一下。
此时,基类Base1改写如下:
class Base1 {
public:
int Base1_a;
};
基类Base2不变,那么此时内存分布结构如下图:
由上图可知,在多重继承中,按照继承顺序排列的前提是所有基类都含有虚函数,否则含有虚函数的基类按顺序排在前面,不含虚函数的基类按顺序排在后面,且派生类新添加的虚函数是添加到第一个含有虚函数的基类所对应的虚函数表中。
三、菱形继承
菱形继承中用到了虚继承,是为了解决最下面的派生类中有最上面的基类的多份实例。
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; };
};
对应的内存分布结构如下图:
由上图可知,在原有的虚函数表指针vfptr存在的情况下,多加了虚基类表指针vbptr,用来指向虚基类。而且和多重继承的情况部分相似,只不过分为了共有和私有的部分,共有的部分也就是虚基类的部分,被放到了最下面,而私有的部分仍然按照含虚函数的基类按顺序放在前面,不含虚函数的基类按顺序放在后面。且若派生类对基类的虚函数有修改,则被修改的基类的虚函数表中对应的函数被派生类覆盖,如若没有修改,而是新添加了虚函数,那么此虚函数将添加到第一个含有虚函数的基类所对应的虚函数表的末尾。
接下来我们观察右图中的虚基类表,每一个虚基类表的0号索引所对应的是对应基类的虚函数表指针vfptr相对于该虚基类表指针vbptr的偏移量,而1号索引对应的是虚基类相对于该基类的虚基类表指针vbptr的偏移量。虚函数表仍然可按照前述的进行理解。具体的读者可以按照内存分布结构的序号列来进行验证。