C++语言通过引入虚函数表的形式来支持多态特性,并且为了解决多重继承中的冗余和二义性问题又引入了虚继承,这使得C++类的内存模型呈现出一定的复杂性。
C++要求所有实例化的对象都要有相应的内存地址,因此对一个不包含任何成员变量、成员函数的空类的实例会占用一个字节的内存空间。而非空类则按照以下规则安排其成员在内存中的排列顺序:
成员函数不占用内存空间
同一个类(不包括父类)中的成员变量在内存中按照在类中的声明次序依次排列,排列顺序与访问权限、变量名没有关系
继承自父类的成员变量排在当前类所有成员变量之前
继承自多个父类的成员变量按照类继承列表中的声明顺序依次排列每个父类中的成员变量
根据这些规则,定义下面这些类并限制他们间的继承关系,我们可以很容易的得出KTestClass类任一实例的内存模型:
class KTopClass
{
public:
KTopClass() : m_iTopVar(0)
{
cout << "KTopClass constructed." << endl;
}
private:
int m_iTopVar;
};
class KLBaseClass : public KTopClass
{
public:
KLBaseClass() : m_iLVar(0)
{
cout << "KLBaseClass constructed." << endl;
}
private:
int m_iLVar;
};
class KRBaseClass
{
public:
KRBaseClass() : m_iRVar(0)
{
cout << "KRBaseClass constructed." << endl;
}
private:
int m_iRVar;
};
class KTestClass : public KLBaseClass, public KRBaseClass
{
private:
int m_iVar1;
public:
KTestClass() : m_iVar1(0), m_iVar2(0) { cout << "KTestClass constructed." << endl; }
int m_iVar2;
};
为了支持多态特性,C++引用虚函数表,父类中的所有虚函数均列在虚函数表中(按照声明顺序,纯虚函数也不例外),子类首先继承父类的虚函数表(非虚继承),如果重写了某个虚函数,那么就用自己所重写的函数地址去覆盖继承下来的虚函数表中的对应虚函数的旧地址,这个旧地址可能是父类新定义的虚函数的地址,也可能是父类覆盖了父类的父类中的虚函数后的“新地址”,之后再将当前子类所新定义的虚函数依次加在父类虚函数表的后面。如果子类没有重写父类的虚函数,那么直接继承父类的虚函数表,并将当前子类所新定义的虚函数依次加在父类虚函数表的后面。处理完虚函数表后,然后再依次排列父类的成员变量。重复这个过程直到所有父类按照继承顺序依次处理完毕,然后再排列子类的成员变量,成员变量的排列规则同上一节所述。需要注意的是如果是多继承,那么子类的虚函数是接在第一个被继承的有虚函数的父类虚函数表后面的。
举个较为复杂的例子来理解:
class KTopClass
{
public:
KTopClass() : m_iTopVar(0)
{
cout << "KTopClass constructed." << endl;
}
virtual void virtual_top_test(){ cout << "KTopClass::virtual_top_test." << endl; }
virtual void pure_virtual_top_test() = 0;
private:
int m_iTopVar;
};
class KLBaseClass : public KTopClass
{
public:
KLBaseClass() : m_iLVar(0)
{
cout << "KLBaseClass constructed." << endl;
}
virtual void virtual_lbase_test(){ cout << "KLBaseClass::virtual_lbase_test." << endl; }
void pure_virtual_top_test()
{
cout << "KLBaseClass::pure_virtual_top_test." << endl;
}
private:
int m_iLVar;
};
class KRBaseClass
{
public:
KRBaseClass() : m_iRVar(0)
{
cout << "KRBaseClass constructed." << endl;
}
virtual void virtual_rbase_test(){ cout << "KRBaseClass::virtual_rbase_test." << endl; }
private:
int m_iRVar;
};
class KTestClass : public KLBaseClass, public KRBaseClass
{
private:
int m_iVar1;
public:
KTestClass() : m_iVar1(0), m_iVar2(0) { cout << "KTestClass constructed." << endl; }
void virtual_lbase_test(){ cout << "KTestClass::virtual_lbase_test." << endl; }
int m_iVar2;
};
内存模型如下:
这个例子中一共有两张虚表,注意这里所说的虚表实际上是指向一个数组的指针,因此每一张虚表占用四个字节。
存在虚继承的情况下,类实例的内存模型与没有虚继承的情况基本是相同的,但是要注意一个原则,即被虚继承的父类在子类中是共享的,而非虚继承的父类在每个子类中都有一份。如果要用一个例子来说明,我们可以先将上一节中的KRBaseClass类的声明修改一下,使其也继承自KTopClass类,其余部分保持不变,KRBaseClass 部分修改如下:
class KRBaseClass : public KTopClass
{
public:
KRBaseClass() : m_iRVar(0)
{
cout << "KRBaseClass constructed." << endl;
}
virtual void virtual_rbase_test(){ cout << "KRBaseClass::virtual_rbase_test." << endl; }
void pure_virtual_top_test()
{
cout << "KRBaseClass::pure_virtual_top_test." << endl;
}
private:
int m_iRVar;
};
这时的内存模型就变为:
可见在KTestClass的实例中有两份KTopClass,这就是所谓的非虚继承的父类在每个子类中都有一份,这造成了一定的冗余和调用时的二义性,为了消除这种二义性,C++引入了虚继承,被虚继承的父类在子类中是共享的。可以设计如下的继承结构来说明这个问题:
class B { /* ... */ };
class X : virtual public B { /* ... */ };
class Y : virtual public B { /* ... */ };
class Z : public B { /* ... */ };
class AA : public X, public Y, public Z { /* ... */ };
在实际的AA的实例中是有两份B的子对象的,其中一份由X、Y子对象共享,因为它们是虚继承自B类的,而另一份是来自于Z子对象的。
我们还是回到最开始的例子中,我们将KLBaseClass类和KRBaseClass均改为虚继承自KTopClass类:
class KTopClass
{
public:
KTopClass() : m_iTopVar(0)
{
cout << "KTopClass constructed." << endl;
}
virtual void virtual_top_test(){ cout << "KTopClass::virtual_top_test." << endl; }
virtual void pure_virtual_top_test() = 0;
private:
int m_iTopVar;
};
class KLBaseClass : virtual public KTopClass
{
public:
KLBaseClass() : m_iLVar(0)
{
cout << "KLBaseClass constructed." << endl;
}
virtual void virtual_lbase_test(){ cout << "KLBaseClass::virtual_lbase_test." << endl; }
void virtual_top_test(){ cout << "KLBaseClass::virtual_top_test." << endl; }
private:
int m_iLVar;
};
class KRBaseClass : virtual public KTopClass
{
public:
KRBaseClass() : m_iRVar(0)
{
cout << "KRBaseClass constructed." << endl;
}
virtual void virtual_rbase_test(){ cout << "KRBaseClass::virtual_rbase_test." << endl; }
private:
int m_iRVar;
};
class KTestClass : public KLBaseClass, public KRBaseClass
{
private:
int m_iVar1;
public:
KTestClass() : m_iVar1(0), m_iVar2(0) { cout << "KTestClass constructed." << endl; }
void virtual_lbase_test(){ cout << "KTestClass::virtual_lbase_test." << endl; }
void pure_virtual_top_test()
{
cout << "KTestClass::pure_virtual_top_test." << endl;
}
int m_iVar2;
};
此时KTestClass实例的内存模型变成了如下形式,即所有被虚继承的父类对应的子对象被统一放在了最后,实际上,不同编译器对放置位置的处理稍有不同,有的是将虚继承的子对象放在了子对象所有成员变量的最后面,有的则是放在了所有父类子对象之后,子类子对象之前。
到这里,很容易就会产生一个疑问,如果KLBaseClass和KRBaseClass中只有KRBaseClass是虚继承自KTopClass呢,实际上这种情况下,KRBaseClass所对应的那一份KTopClass子对象被放在最后,而KLBaseClass那一份仍按原来的规则存放。
本文分别分析了有无虚函数以及虚继承情况下类对象的内存模型,从底层对C++的多态特性的实现进行了解释,但需要注意的是,实际中不同编译器下对内存模型的实现细节是稍有不同的。