归妹趋无妄,无妄趋同人,同人趋大有。甲转丙,丙转庚,庚转癸。子丑之交,辰巳之交,午未之交。风雷是一变,山泽是一变,水火是一变。乾坤相激,震兑相激,离巽相激······好了,要背心法的同学请课后自行背诵。
特别说明:本系列并非严格按照《独孤九剑》的剑式进行命名,来学剑招的请自备传送卷轴,恕不报销往返机票。
之所以称之为“拨云见日”,就是要在这招中和大家一起揭开C++对象内存的迷雾,让她在众位少侠面前一览无余(想想心里还真有点小激动呢o(* ̄︶ ̄*)o)。
我们从最基础的开始,一步步直到出任CEO,迎娶白富美(额,有点扯远了)。
环境说明:本机是windows32位机,实验环境为VC6.0
1. 当我们定义了一个空类的时候
Class base{};
Base b = Base();
Base *pb = &b;
cout<<sizeof(Base)<<endl; //输出为1
cout<<*((char *)pb)<<endl; //输出一个空字符
也就是说,当一个类为空类的时候,它的大小其实是1个字节(编译器悄悄地给了那个可怜的类一个字节)。为什么要插入这一个字节,个人认为是这样的:类是对象的模板,如果类的大小为0,那么它构造出的对象就相当于无物(大小为0的对象!)。这个时候究竟是有对象还是没有对象恐怕就要上升到哲学的高度了。用区区一个字节的内存,谈笑间就解决了这一难题,岂不妙哉。
2. 非空无继承的时候
class Base
{
Public: //方便后面的验证
static int s; //静态变量不会占用类中的存储空间,它将存在全局区
char a;
int i;
char b;
<span style="white-space:pre"> </span>virtual void sum(); //测试时先实现该函数,否则将报错
};
此时sizeof(Base)操作将得到的答案是16,是的并不是6(1+4+1)。这是由内存对齐(为了更快的与cup进行数据交换)和存储虚函数表指针所引起的(虚函数本身虽然不保存在类中,但是指向虚函数表的指针将被保存,在具体实现多态时会用到)。
其中,nonstatic datamembers在class object中的排列顺序将和其被声明的顺序一样,members的排列只需符合“较晚出现的members在class object中有较高的地址”这一条即可。也就是说各个members并不一定连续,例如编译器在内存对齐时填补的bytes就会出现在两个连续的members之间。而虚函数表指针被放在对象首部(不同的编译器会有所不同)。关于虚表指针的位置,可用以下语句进行验证:
Base b;
cout<<((long)&(b.a) - (long)&b)<<endl;//该表达式的结果为4,说明成员a前还有不知名的生物存在,毫无疑问,在这种情况下肯定是虚函数表指针无疑了。
此时的内存布局如下左图示:
注:padding表示编译器的填充部分。
将i和b的声明顺序交换后,该类的对象内存如上右图所示,此时大小为12。
3. 单一继承的时候
class Base
{
Public:
int i;
char c;
<span style="white-space:pre"> </span>virtual void sum(); //测试时先实现该函数,否则将报错
};
class Little : public Base
{
Public:
int i;
virtual void out(); //测试时先实现该函数,否则将报错
};
此时sizeof(Little)得到的值为16。这里说明两点:首先,虽然父类和子类都有成员变量i,但他们在不同的内存中,互不干扰;其次,子类也有虚函数,但是子类的对象中并不会新添加一个虚表指针(该虚函数将被添加到父类虚函数表中,由父类部分的虚函数指针统一指向)。可用下式证明:
Little l;
cout<<((long)&(l.i)- (long)&(l.c))<<endl;//值为4,说明他们之间并没有其它成员。
关于第二点,我还将在独孤九剑第三式中详加说明。
下面左图是当前Little对象的内存情况:
该内存布局可由以下语句佐证:
Little l;
cout<<((long)&(l.Base::i) - (long) &l)<<endl; //值为4
cout<<((long)&(l.i) - (long)&(l.Base::i))<<endl; //值为8
若把Base中的sum虚函数去掉,则Little对象的内存布局如上右图。此时Little对象没有继承虚表指针,而是由自己新建了一个。
4. 多继承的时候
class Base1
{
public:
char *pc;
virtual void sum1();
};
class Base2
{
public:
int ** pi;
virtual void sum2();
};
class Little : public Base1,public Base2
{
public:
double *** pd;
virtual void out();
};
这次把数据成员都换成指针,为各位看官换换口味。
此时,sizeof(Base1) 和 sizeof(Base2) 都为 8,sizeof(Little)为20。Little的对象内存如下图所示:
说明的主要是三点:1.不管是几级指针,在32位环境下都是占4个字节;2.基类成员在子类对象中的顺序取决于继承时的声明顺序;3.子类对象依然公用父类部分的虚函数指针,没有自己新建一个。
以下语句可用于验证内存分布:
Little l;
cout<<((long)&(l.pc) - (long)&(l))<<endl;//4
cout<<((long)&(l.pi) - (long)&(l))<<endl;//12
cout<<((long)&(l.pd) - (long)&(l))<<endl;//16
cout<<((long)&(l.pi) - (long)&(l.pc))<<endl;//8
5. 多继承 + 虚继承
class VirtualBase
{
public:
int vbi;
};
class Base1 : public virtual VirtualBase
{
public:
char *pc;
virtual void sum1();
};
class Base2 : public virtual VirtualBase
{
public:
int **pi;
virtual void sum2();
};
class Little : public Base1, public Base2
{
public:
double ***pd;
virtual void out();
};
此时,sizeof(Base1) 和 sizeof(Base2) 都为 16,sizeof(Little)为32。假设Base1有实例对象b1,Base2有实例对象b2,则它们的内存如下图所示:
其中__vptr_Base1和__vptr_Base2是指向虚函数表的指针;而__vbptr_Base1和__vbptr_Base2是指向虚基类表的指针。
而此时,Little对象的内存如下图所示:
该布局可用如下语句佐证:
Little l;
cout<<((long)&(l.vbi) - (long)&(l))<<endl;//28 该成员被放在了最后
cout<<((long)&(l.pc) - (long)&(l))<<endl;// 8
cout<<((long)&(l.pi) - (long)&(l))<<endl;// 20
cout<<((long)&(l.pd) - (long)&(l))<<endl;// 24
cout<<((long)&(l.pi) - (long)&(l.pc))<<endl;// 12
说明:在VC++中,对每个继承自虚基类的类实例,将增加一个隐藏的“虚基类表指针”(vbptr)成员变量,从而达到间接计算虚基类位置的目的。该变量指向一个全类共享的偏移量表,表中项目记录了对于该类而言,“虚基类表指针”与虚基类之间的偏移量”。
下面进一步说明虚基类指针指向的虚基类表的内容。
上面Little类实例的__vbptr_Base1和__vbptr_Base2指针所指向的虚基类表的内存分布如下左图所示:
说明:虚基类表中的第一个索引是自己的内存地址相对于自己存放指向虚基类表的地址的偏移量(为-4,因为中间隔了一个虚函数表指针);第二个索引开始是基类内存相对于它自己指向虚基类表地址的偏移量;最后以0标志虚基类表的结束。
右图是VC6.0的实验结果,__vbptr_Base1所指向的内容紧挨着__vbptr_Base2所指向的内容。很容易验证,__vbptr_Base1的值与__vbptr_Base2的值差了12字节,且有:
*((int *)*((int*)(&l) + 1) - 2) == *((int *)*((int*)(&l) + 4) + 1) == 12
从而证明了右图的正确性(并不一定适用于其它编译器)。
到此第一式就练完了,各位少侠是否觉得神清气爽、格外精神(*^-^*)