本文主要简述一下在Visual Studio中C++对象的内存布局,这里没有什么测试代码,只是以图文的形式来描述一下内存分布,关于测试的代码以及C++对象模型的其他内容大家可以参考一下陈皓先生的几篇博文以及网上的其他一些文章:
《C++虚函数表解析》:http://blog.csdn.net/haoel/article/details/1948051
《C++对象的内存布局(上)》:http://blog.csdn.net/haoel/article/details/3081328
《C++对象的内存布局(下)》:http://blog.csdn.net/haoel/article/details/3081385
根据我自己调试出来的结果来看(Release版本),VS处理对象的原则大致可以分为以下几点:
1、 对于普通成员变量,按照声明次序以及内存的对齐原则存放。
例如对于类A,在32位程序中占据8个字节:
class A { public: void test() { cout << "A::test "; } private: int nA; char chA; };
在内存中布局如下:
2、 如果对象含有虚函数,则在对象的开头处添加一个指针,该指针指向一个虚函数表,表中依序存放着该类中虚函数的地址。
例如对于类A,在32位程序中占据12个字节:
class A { public: virtual void f() { cout << "A::f() "; } virtual void fA() { cout << "A::fA() "; } private: int nA; char chA; };
在内存中布局如下,注意的是虚表的最后一项未必是0:
3、 如果对象只有直接继承(非虚拟继承)而来的父类,按照子类以及父类有没有虚函数可以分为以下几种情况:
<1>:子类和父类都没有虚函数,按照继承顺序先放置各个父类部分,再放置子类部分;
例如对于类B,在32位程序中占据24个字节:
class A1 { private: int nA1; char chA1; }; class A2 { private: int nA2; char chA2; }; class B : public A1, public A2 { private: int nB; char chB; };
在内存中布局如下:
<2>:子类有虚函数,父类没有虚函数,则先放置子类的虚函数表指针,再依次放置父类部分,最后放置子类部分;
例如对于类B,在32位程序中占据20个字节:
class A { private: int nA; char chA; }; class B : public A { public: virtual void f() { cout << "B::f() "; } virtual void fB() { cout << "B::fB() "; } private: int nB; char chB; };
在内存中布局如下:
<3>:子类和父类都有虚函数,则先把父类列表中带有虚函数的父类放到前面,再依次放置没有虚函数的父类,最后放置子类部分(没有虚函数指针),同时修改各个虚函数表以及指针,使得其满足如下条件:第一个虚函数表指针所指向的虚函数表中先存放继承自本父类的虚函数地址,包括原样继承下来的以及重写的,再放置子类独有的虚函数地址,其余的虚函数表指针所指向的虚函数表只包含继承自对应父类的虚函数地址,包括原样继承下来的以及重写的。
例如对于类B,在32位程序中占据40个字节:
class A1 { private: int nA1; char chA1; }; class A2 { public: virtual void f() { cout << "A2::f() "; } virtual void fA2() { cout << "A2::fA2() "; } private: int nA2; char chA2; }; class A3 { public: virtual void f() { cout << "A3::f() "; } virtual void fA3() { cout << "A3::fA3() "; } private: int nA3; char chA3; }; class B : public A1, public A2, public A3 { public: virtual void f() { cout << "B::f() "; } virtual void fA3() { cout << "B::fA3() "; } virtual void fB() { cout << "B::fB() "; } private: int nB; char chB; };
在内存中布局如下:
4、(这一点还不太确定)如果对象有虚基类,无论是自己虚拟继承而来的还是父类虚拟继承而来的,则先按照以上规则将非虚基类部分处理完毕之后,再插入一个指针,再放置该类剩余的成员变量(该指针指向一个表格,表格中的每一项均是一个32位带符号整数——无论32位程序还是64位程序,其中第一项的内容是该指针到本类首部的偏移量,之后依次是该指针到本类虚基类的起始位置的偏移量),当这些全部放置完毕之后,然后再依次放置虚基类。这里有一个问题就是有的时候会在虚基类前面放置一个全零的指针,然而有的时候却又没有,按照我目前测试的结果来看,当子类有构造函数并且只要重写了虚基类的一个函数该虚基类前面就会有这个全零的指针。
例如对于下列程序,在32位程序中占用空间情况分别为:
class A { public: virtual void f() { cout << "A::f() "; } virtual void fA() { cout << "A::fA() "; } private: int nA; char chA; }; class B : public virtual A { public: virtual void f() { cout << "B::f() "; } virtual void fB() { cout << "B::fB() "; } private: int nB; char chB; }; class C { public: virtual void f() { cout << "C::f() "; } virtual void fC() { cout << "C::fC() "; } private: int nC; char chC; }; class D { public: virtual void f() { cout << "D::f() "; } virtual void fD() { cout << "D::fD() "; } private: int nD; char chD; }; class E { public: virtual void f() { cout << "E::f() "; } virtual void fE() { cout << "E::fE() "; } private: int nE; char chE; }; class F : public virtual B, public virtual C, public D, public virtual E { public: F() {} virtual void f() { cout << "F::f() "; } virtual void fB() { cout << "F::fB() "; } virtual void fC() { cout << "F::fC() "; } virtual void fE() { cout << "F::fE() "; } virtual void fF() { cout << "F::fF() "; } private: int nF; char chF; };
A、C、D、E均占12个字节,以A为例,内存布局如下:
B占28个字节,在内存中布局如下,值得注意的一点是,此时B因为已经重写了虚基类A的一个虚函数f(),所以如果再显示定义一个构造函数的话,在B中的A部分之前就会添加一个全0的指针,但是如果把构造函数或者f()的重写随便去掉一个,这个全0指针就不会存在了:
F占92个字节,在内存布局中如下,其中虚基类的排列顺序是按照列表里的顺序来的,比如在本例中,F虚拟继承的有B、C、E三个类,所以在虚基类的排放顺序中先放B,又因为B虚拟继承了A,所以最后的顺序是A、B、C、E。还有另外一点就是因为F定义了一个构造函数,所以虚基类前面会有一个全零指针,如果把这个构造函数去掉的话,F的大小就变成了76个字节,正好是92字节减去4个全零指针的大小: