C++语言中类的继承是C++重要特性之一,暂且将类的继承分为成员变量以及成员函数两个方面。我们知道标准C++选择的方式是每个类对象存储一份自己的成员变量,那么这份成员变量的存储区是怎么布局的呢?下面分几种情况记录一些自己看法和体会(部分参考《Inside The C++ Object Model》)。
1,非静态成员变量的单一继承且不含VIRTUAL函数
C++标准并没有对继承来的基类的成员变量和派生类自身的成员变量的布局做一个顺序上的规定,而是由编译器来决定。大部分的编译器都是将基类成员变量放在前面,且变量之间遵循其声明的次序。
例如:class A class B : public class A
{ {
public: public:
int val; char a2;
char a1;
} }
对于A类对象其内存布局很简单,和一个struct结构体的内存布局是相同的。32位计算机情况下,由于内存对齐的原因,其所占的内存大小应该为4 + 1 + 3 = 8字节,当B以public继承方式继承A的成员变量后, B的内存布局即为A的成员变量位于其内存分布的前端,注意:此时B继承并不仅仅继承A中实际变量所占的内存字节,其为了内存对齐而补充的字节也一并继承,即B中继承的来自A的成员变量的内存字节数也为8字节,而不是4+1=5字节。然后B定义的自身的成员变量紧跟于继承而来的成员变量之后,同时也要注意内存对齐!则B的成员变量所占字节总数应该是8 + 1 +3 = 12字节。
至于为什么要将内存对齐而补充的空间一并继承据《inside……》一书介绍,是因为在C++中经常会有利用基类指针指向一个派生类成员的情况出现,例如 B m_b; A* PA = &m_b; 由指针的性质知道,PA所能指向的内存区的大小应该为数据类型A所占的大小,即为8字节,如果继承时未将对齐而填充的字节也一并继承,则m_b整个内存字节为4+1+1+2 = 8字节,此时用PA竟然可以访问不属于A类型的成员,即a2,这是不符合C++语义的。因为利用基类指针只可以访问派生类对象中继承的基类成员。这只是我的一种理解,不知是否正确,而《inside……》书中是以复制操作来解释的,具体见P106。
2,非静态成员的单一继承,且基类中声明了虚函数
若基类中定义了虚函数,首先基类对象的内存布局中会增加对虚函数的支持,即一个指向VIRTUAL TABLE的指针VPTR成员,在运行期将使得每个对象利用此指针找到相应的virtual table。而构造函数应该支持对该指针赋正确的虚表地址。那么VPTR如何布局到对象的内存区域呢?一般来说该指针都被放在对象内存区域最前端!因此派生类继承了基类成员之后,其对象内存区的前端也应该是一个虚表指针变量!
若基类中没有定义虚函数,而派生类中定义了虚函数,则基类对象中不会添加虚表指针这个变量,而派生类中对象会添加虚表指针成员,且将此指针放于内存区的前端,即继承而来的基类成员变量之前,在此种情况下如果利用一个基类指针指向派生类的对象,利用该指针访问继承来的基类成员时需要编译器作出调整,该基类指针应该作出适当偏移,以跳过这个派生类的虚表指针。
若基类定义了虚函数,派生类也定义了虚函数(不管是否重载基类虚函数还是定义新的虚函数),由虚表的性质,派生类的虚表会由继承的基类的虚函数列表和自身虚函数列表共同构成,且基类虚函数在前。因为只有一张虚函数表,因此在派生类对象中只会有一个虚表指针成员,该指针也会被放于内存分布的最前端。由于基类对象有虚表指针,此时用基类指针来指向一个派生类对象并访问继承的基类成员时,不需要编译器作出偏移调整。
3,非静态成员的多重继承且无虚函数
多重继承下,继承的各个基类成员变量的排列为:视各个基类对象内存布局为一个单元,按照多重继承的顺序,从左到右的先后顺序在内存区上先后排列!最后为派生类自己定义的成员变量!
4,非静态成员的多重继承且有虚函数
此时虚表指针的问题又出现了:
1)基类定义了虚拟函数,而派生类未定义虚拟函数。此时定义了虚拟函数的基类对象内存布局中有虚表指针成员,因此有多少个定义了虚函数的基类就会在派生类对象内存布局中有多少个虚表指针,分别指向继承来的各个基类的虚函数表!且各个虚表指针成员位于该继承来的基类对象内存单元的前端。
2)基类定义了虚拟函数,(待续)