前面简单的论述过C++对象模型,总觉得不够深入,现近闲来进一步挖掘C++对象内存布局情况。主要讨论:单一继承,多重继承,钻石继承的有无虚函数以及虚拟继承的情况。贴出测试程序,并给出测试结论以及对应的类对象的大小计算。(PS:类对象的内存布局取决于编译器,这里的测试都是基于Visual Studio)
单一的类对象
单一的类对象主要考虑有虚函数的情况,前面提及的博文已有介绍,类中定义了虚函数,就会产生一个虚函数表(实质就是一个函数指针数组,虚函数表不在类中,VS编译环境下,虚函数表位于常量段,虚表指针在类对象中),类每定义一个对象,便会在对象的最前面安插一个虚表指针,指向虚函数表,这样该类定义的对象会多出4 Byte(32位)。
可以在Visual Studio C++ 编译输出中直接看C++内存布局:工程项目——右键“属性”——配置属性——C/C++——命令行——其他选项里添加“/d1reportAllClassLayout ”,即可在编译输出中查看定义的类的内存布局,上面是输出所有定义的类对象,你可以搜索你自己定义的对象。(最好不要把名字定为base,不然一大堆)。
using namespace std;
class parent
{
//public:
int b;
static int a;
virtual void fun1()
{
cout << "parent: fun1" << endl;
}
/*virtual void fun2()
{
cout << "parent: fun2" << endl;
}*/
};
int main()
{
parent obj_b;
return 0;
}
下面便是定义的类对象内存布局情况:vfptr 即表示虚表指针,static 成员不在类对象中
1> class parent size(8):
1> +---
1> 0 | {vfptr}
1> 4 | b
1> +---
如果类中不定义为虚函数,类对象的大小是4,如下:
1> class parent size(4):
1> +---
1> 0 | b
1> +---
下面我们来考虑类继承的情况:
单一继承
1)一般继承,无虚函数
1.1 继承一个基类
class parent
{
//public:
int b;
/*virtual void fun1()
{
cout << "parent: fun1" << endl;
}*/
};
class child: public parent
{
int a;
};
int main()
{
child obj_b;
return 0;
}
内存布局:
1> class child size(8):
1> +---
1> | +--- (base class parent)
1> 0 | | b
1> | +---
1> 4 | a
1> +---
1.2 再考虑继承两个基类的情况
class parent
{
//public:
int b;
/*virtual void fun1()
{
cout << "parent: fun1" << endl;
}*/
};
class parent1
{
int c;
};
class child: public parent, public parent1
{
int a;
};
int main()
{
child obj_b;
return 0;
}
内存布局:
1> class child size(12):
1> +---
1> | +--- (base class parent)
1> 0 | | b
1> | +---
1> | +--- (base class parent1)
1> 4 | | c
1> | +---
1> 8 | a
1> +---
可以看出,一般继承无虚函数的情况下,派生类对象的内存布局为,先存放基类成员,再存放自身成员变量,其大小是简单的基类对象大小与自身成员大小之和。继承多个基类时,数据成员按继承声明的前后顺序放置。
2)存在虚函数的情况
2.1 基类定义虚函数,派生类本身无虚函数
class parent
{
//public:
int b;
virtual void fun1()
{
cout << "parent: fun1" << endl;
}
};
class parent1
{
int c;
};
class child: public parent/*, public parent1*/
{
int a;
};
int main()
{
child obj_b;
return 0;
}
内存布局:
1> class child size(12):
1> +---
1> | +--- (base class parent)
1> 0 | | {vfptr}
1> 4 | | b
1> | +---
1> 8 | a
1> +---
这个和上面的一般继承没啥区别,只不过是基类中定义了虚表指针,然后被派生类一股脑继承下来
2.2 继承多个基类
class parent
{
int b;
virtual void fun1()
{
cout << "parent: fun1" << endl;
}
};
class parent1
{
int c;
virtual void fun2()
{
cout << "parent1" << endl;
}
};
class child: public parent, public parent1
{
int a;
};
int main()
{
child obj_b;
return 0;
}
内存布局
1> class child size(20):
1> +---
1> | +--- (base class parent)
1> 0 | | {vfptr}
1> 4 | | b
1> | +---
1> | +--- (base class parent1)
1> 8 | | {vfptr}
1> 12 | | c
1> | +---
1> 16 | a
1> +---
可以看到,继承多个基类也和无虚函数的情况一样,这里和继承单个的区别在于,派生类会产生两个虚表指针,后面我们会用程序验证,这两个虚表指针指向不同的两个虚函数表,可以顺便总结一下,非虚继承下,派生类继承几个含有虚函数的基类,那么派生类的实例对象就会有几个虚表指针。
2.3 含虚函数情况
1.基类无虚函数,派生类自身定义有虚函数
class parent
{
public:
int b;
/*virtual void fun2()
{
cout << "parent" << endl;
}*/
};
class child: public parent
{
public:
int a;
virtual void fun2()
{
cout << "child" << endl;
}
};
int main()
{
return 0;
}
内存布局
1> class child size(12):
1> +---
1> 0 | {vfptr}
1> | +--- (base class parent)
1> 4 | | b
1> | +---
1> 8 | a
1> +---
可以看出,虚表指针总是放在对象的最前面,哪怕它是定义在派生类中。
基类有虚函数,派生类无虚函数,这个简单,不赘述了
2.当基类,派生类自身都定义虚函数的情况
class parent
{
public:
int b;
virtual void fun2()
{
cout << "parent" << endl;
}
};
class child: public parent
{
public:
int a;
virtual void fun1()
{
cout << "child" << endl;
}
};
int main()
{
return 0;
}
内存布局:
1> class child size(12):
1> +---
1> | +--- (base class parent)
1> 0 | | {vfptr}
1> 4 | | b
1> | +---
1> 8 | a
1> +---
这个一开始以为派生类也会占据一个自身虚表指针空间的,一看发现错了。细看布局,你会发现子类对象中虚表指针是继承的父类对象的,那么子类对象本身就不会产生虚函数指针么?答案是肯定的。那么子类对象的虚函数指针在哪呢?位于同一个虚函数表中。下面我们通过程序验证一下。
typedef void(*fun)(void);
int main()
{
child obj;
fun pFun = NULL;
pFun = (fun)*((int*)*(int*)(&obj));
pFun();
pFun = (fun)*((int*)*(int*)(&obj)+1);
pFun();
return 0;
}
output:
parent
child
上面的语句 pFun = (fun)*((int*)*(int*)(&obj)); 前面说过虚函数表实质是一个函数指针数组,数组里面存放的是虚函数指针。对象中只存放指向虚函数表的虚表指针,并且位于对象的最前面,也就是说虚表指针与对象是同一个地址。回过头看这条程序:(int*)(&obj) 对对象地址也就是虚表指针(虚函数表的地址)强制转换,(int*)*(int*)(&obj) 解引用虚表指针,定位到了虚函数表(函数指针数组),这指向了数组(首位置)。(fun)*((int*)*(int*)(&obj)) 有了前面这个就好理解了,取数组首元素得到虚函数指针,然后强制转换。从上面的测试输出可以看出,这种情况下派生类对象中只有一个虚表指针,派生类的虚函数指针位于同一个虚函数表中,且位于基类虚函数指针的后面。
1> child::$vftable@:
1> | &child_meta
1> | 0
1> 0 | &parent::fun2
1> 1 | &child::fun1
如果派生类中的虚函数名为fun2,自然的就同名覆盖了基类的。
2.3 多重继承情况
1)继承多个基类
class parent
{
public:
int a;
virtual void fun_p()
{
cout << "parent" << endl;
}
};
class adopter
{
public:
int b;
virtual void fun_a()
{
cout << "adopter" << endl;
}
};
class child : public parent, public adopter
{
public:
int c;
virtual void fun_c()
{
cout << "child" << endl;
}
};
int main()
{
return 0;
}
内存布局:
1> class child size(20):
1> +---
1> | +--- (base class parent)
1> 0 | | {vfptr}
1> 4 | | a
1> | +---
1> | +--- (base class adopter)
1> 8 | | {vfptr}
1> 12 | | b
1> | +---
1> 16 | c
1> +---
由于两个基类都有虚函数,这样继承下来,子类对象便会有两个虚表指针,也就是说子类会有两个虚函数表。子类对象中的虚函数指针是存放在继承的第一个父类的虚函数表中
1> child::$vftable@parent@:
1> | &child_meta
1> | 0
1> 0 | &parent::fun_p
1> 1 | &child::fun_c
1>
1> child::$vftable@adopter@:
1> | -8
1> 0 | &adopter::fun_a
那如果父类中只有一个有虚函数呢?
class parent
{
public:
int a;
/*virtual*/ void fun_p()
{
cout << "parent" << endl;
}
};
class adopter
{
public:
int b;
virtual void fun_a()
{
cout << "adopter" << endl;
}
};
class child : public parent, public adopter
{
public:
int c;
virtual void fun_c()
{
cout << "child" << endl;
}
};
内存布局
1> class child size(16):
1> +---
1> | +--- (base class adopter)
1> 0 | | {vfptr}
1> 4 | | b
1> | +---
1> | +--- (base class parent)
1> 8 | | a
1> | +---
1> 12 | c
1> +---
这里故意不在第一个父类定义虚函数,看内存布局会发现,父类adopter处于子类对象的最前面,因为它定义有虚函数,Visual Studio 编译器保证了虚表指针位于对象最前面的原则,这样如果继承的多个父类均有虚函数,那么存放位置根据继承顺序来,如果有个没有定义虚函数,则不管继承的顺序,均按照虚函数优先原则来,有虚函数的放置在最前面。
2)累积继承
class parent
{
int a;
virtual void fun_p()
{
cout << "parent" << endl;
}
};
class child : public parent
{
int b;
virtual void fun_c()
{
cout << "child" << endl;
}
};
class grandchild : public child
{
int c;
virtual void fun_g()
{
cout << "grandchild" << endl;
}
};
int main()
{
grandchild obj_b;
return 0;
}
//内存布局
1> class grandchild size(16) :
1> +-- -
1> | +-- - (base class child)
1> | | +-- - (base class parent)
1> 0 | | | {vfptr}
1> 4 | | | a
1> | | +-- -
1> 8 | | b
1> | +-- -
1> 12 | c
1> +-- -
//虚函数表
1> grandchild::$vftable@:
1> | &grandchild_meta
1> | 0
1> 0 | &parent::fun_p
1> 1 | &child::fun_c
1> 2 | &grandchild::fun_g
从上面看,以及综合前面基类中含有虚函数的情况可以看出,子类对象中的虚表指针取决于继承的定义有虚函数的父类的个数(非虚拟继承),当然如果父类没有虚函数那就取决于本身有没有虚函数了。如果上面的这个子类(应该叫孙类),再继承parent,那么它会多一个虚表指针。而子类若自身也定义虚函数,则它不会产生虚表指针(继承的父类有虚表指针的情况下),而是把它的虚函数指针放置在继承的父类的虚函数表中,这是出于利用虚函数实现多态的目的,这里我们主要讨论内存布局情况,虚函数继承与多态,后面再说。
在介绍钻石型继承前,我们再考虑上面论述的其中几种情况的虚拟继承下的类对象的内存布局:
虚拟继承(虚基类)
我们就以上面的这个累积继承为例并扩展,阐述一下虚拟继承是如何影响类对象内存布局的
class parent
{
int a;
virtual void fun_p()
{
cout << "parent" << endl;
}
};
class child :virtual public parent
{
int b;
virtual void fun_c()
{
cout << "child" << endl;
}
};
class grandchild :virtual public child
{
int c;
virtual void fun_g()
{
cout << "grandchild" << endl;
}
};
一看内存布局,可不是发生了一点点的微秒变化
1> class child size(20):
1> +---
1> 0 | {vfptr}
1> 4 | {vbptr}
1> 8 | b
1> +---
1> +--- (virtual base parent)
1> 12 | {vfptr}
1> 16 | a
1> +---
1> class grandchild size(32):
1> +---
1> 0 | {vfptr}
1> 4 | {vbptr}
1> 8 | c
1> +---
1> +--- (virtual base parent)
1> 12 | {vfptr}
1> 16 | a
1> +---
1> +--- (virtual base child)
1> 20 | {vfptr}
1> 24 | {vbptr}
1> 28 | b
1> +---
先看类child,对比2.3.2,一般继承,基类派生类均含虚函数的情况(sizeof(child_obj) = 12),这里虚拟继承,瞬间多了8 Byte,首先虚拟继承,子类对象中自然会多出一个虚基类指针vbptr,但虚表指针还是在对象的最前面,还有一个最大的改变就是子类会产生两个虚函数表,这样实例化的对象便会有两个虚表指针,这颠覆了前面得出的结论,不能简单的按继承的含虚函数的父类的个数来判定了。
这里阐述的情况是虚拟继承的情况,对于前面子类含虚表指针个数与含虚函数父类个数的关系限于非虚拟继承情况下。对于虚拟继承,可以看出,它是全盘继承某个子类。
可以看出,
1> child::$vftable@child@:
1> | &child_meta
1> | 0
1> 0 | &child::fun_c
1>
1> child::$vbtable@:
1> 0 | -4
1> 1 | 8 (childd(child+4)parent)
1>
1> child::$vftable@parent@:
1> | -12
1> 0 | &parent::fun_p
中间是虚基类指针。上面两个虚表指针指向不同的两个虚函数表,我们可以通过程序验证一下:
typedef void(*fun)(void);
int main()
{
child obj;
fun pFun = NULL;
pFun = (fun)*((int*)*(int*)(&obj));
pFun();
pFun = (fun)*((int*)*((int*)(&obj)+3));
pFun();
return 0;
}
//output
child
parent
对于上面的指针,注意与前面提到的相区别:虚函数表的地址位移和对象内的地址位移。
3、钻石型继承
所谓钻石型继承就是继承方式形如下面的“菱形”结构
A
/ \
B C
\ /
D
下面演示这样一种情况:
class class_A
{
public:
int a;
void fun_a()
{
cout << "a" << endl;
}
};
class class_B : public class_A
{
public:
int b;
void fun_b()
{
cout << "b" << endl;
}
};
class class_C : public class_A
{
public:
int c;
void fun_c()
{
cout << "c" << endl;
}
};
class class_D : public class_B, public class_C
{
public:
int d;
void fun_d()
{
cout << "d" << endl;
}
};
int main()
{
class_D obj;
return 0;
}
内存布局如下
1> class class_D size(20):
1> +---
1> | +--- (base class class_B)
1> | | +--- (base class class_A)
1> 0 | | | a
1> | | +---
1> 4 | | b
1> | +---
1> | +--- (base class class_C)
1> | | +--- (base class class_A)
1> 8 | | | a
1> | | +---
1> 12 | | c
1> | +---
1> 16 | d
1> +---
上面的程序有问题,从内存布局就可以看出,如果调用基类成员a,就会产生二义性,究竟是调用哪个,因为BC内部都继承有A的成员变量,最后的子类对象都有a的两份拷贝。这时候虚基类就横空出世了,前面有讲到虚拟继承。如果上面采用虚继承,那么最后的子类D的实例只会拥有一个,看下面
class class_A
{
public:
int a;
virtual void fun_a()
{
cout << "a" << endl;
}
};
class class_B : virtual public class_A
{
public:
int b;
virtual void fun_b()
{
cout << "b" << endl;
}
};
class class_C : virtual public class_A
{
public:
int c;
virtual void fun_c()
{
cout << "c" << endl;
}
};
class class_D : public class_B, public class_C
{
public:
int d;
virtual void fun_d()
{
cout << "d" << endl;
}
};
int main()
{
class_D obj;
return 0;
}
看内存布局前,先分析下这个派生类class_D的实例会占用多少内存。按照前面总结的来,class_D 非虚继承两个含有虚函数的基类,那么其对象中便会有两个虚表指针(自身的虚函数指针就放在第一个虚函数表中),一股脑继承下来,并且自身不会产生虚表指针。可以直接看继承的class_B,虚继承A,那么class_B就会占用12 Bytes(虚表指针,虚基类指针以及int成员变量),同理 class_C也会占用12 Bytes(虚继承自身也会产生多余的虚表指针),再考虑共继承的A,本身会有虚表指针,然后成员变量会放在派生类的最后,A的成员只会在派生类D中存在一份拷贝(* bytes),这是虚基类的目的,避免二义性,然后class_D,由于是一般继承,自身就是int 型4 Byte。总的下来就是 12+12+8+4 = 36 Bytes。
1> class class_D size(36):
1> +---
1> | +--- (base class class_B)
1> 0 | | {vfptr}
1> 4 | | {vbptr}
1> 8 | | b
1> | +---
1> | +--- (base class class_C)
1> 12 | | {vfptr}
1> 16 | | {vbptr}
1> 20 | | c
1> | +---
1> 24 | d
1> +---
1> +--- (virtual base class_A)
1> 28 | {vfptr}
1> 32 | a
1> +---
如果把最后的class_D 也虚继承 B和C(两个都虚继承才行),那么最后的大小会是多少呢?虚继承首先会多出虚基类指针,然后自身也会多余产生一个虚表指针,那么会多出8Bytes,最后的大小就是44 Bytes,挺大的。
如果只是class_D 只是虚继承其中一个基类,好吧,表示已经凌乱了,在Visual Studio 下,虽然大小不会改变,不会多出虚表指针和虚基类指针,但是内存布局却改变了,
代码不贴了,直接看内存布局也会知道继承关系:
1> class class_D size(36):
1> +---
1> | +--- (base class class_C)
1> 0 | | {vfptr}
1> 4 | | {vbptr}
1> 8 | | c
1> | +---
1> 12 | d
1> +---
1> +--- (virtual base class_A)
1> 16 | {vfptr}
1> 20 | a
1> +---
1> +--- (virtual base class_B)
1> 24 | {vfptr}
1> 28 | {vbptr}
1> 32 | b
1> +---
结合前面的虚继承内存布局会发现,虚继承的东西都是放在后面。进一步深究就没意义了。
下面的程序告诉你,虚表指针不一定总是在对象的最前面:
class class_A
{
public:
int a;
virtual void fun_a()
{
cout << "a" << endl;
}
};
class class_B: virtual public class_A
{
public:
int b;
void fun_b()
{
cout << "b" << endl;
}
};
typedef void(*fun)(void);
int main()
{
class_B obj;
fun pFun = NULL;
pFun = (fun)*((int*)*((int*)(&obj)+2));
pFun();
return 0;
}
//内存布局
1> class class_B size(16) :
1> +-- -
1> 0 | {vbptr}
1> 4 | b
1> +-- -
1> +-- - (virtual base class_A)
1> 8 | {vfptr}
1 > 12 | a
1 > +-- -
前面啰嗦了一大片,我们可以得出以下几点:
- 一般继承下(无虚函数),派生类的内存布局是先放基类成员,多个基类的按继承的顺序放置,最后是派生类自身的成员变量,函数和static变量不在类对象中。这样派生类对象的大小便是继承的基类的大小与自身变量的大小之和。
- 一般继承下(含虚函数),与上面的区别就是考虑虚表指针了。1、派生类只继承一个基类的情况下,不管是基类定义有虚函数还是派生类本身有虚函数,或是两个都有虚函数,最后的派生类对象中只会有一个虚表指针,并且是位于对象的最前面。两个都有虚函数的话,派生类的虚函数指针会存放在虚函数表(数组)中基类的虚函数指针的后面。2、当派生类继承多个基类时,如果多个基类中只有一个基类有虚函数,那就和前面的一样。多个基类都有虚函数,那么派生类对象就会产生多个虚表指针,虚表指针的个数与继承的含虚函数的基类的个数一样,派生类的虚函数指针放在第一个虚函数表中。
- 虚继承下(有虚函数),虚继承除了多出一个虚基类指针外,整个对象的内存布局也会随之改变。只要是虚继承,那么对应的派生类就会产生一个虚基类指针。另外只要是虚继承,那么基类会并且只会在派生类对象中存在一份拷贝,并且虚继承的东西是放在后面,哪怕它带有虚表指针(虚表指针不一定都是在对象的最前面)。进一步总结虚继承下的虚表指针情况,由于虚继承,派生类会存在基类的一份完整拷贝,这样派生类会产生多余的自身的虚表指针。
计算类对象大小的时候,考虑以上几点便很容易得出。附带一句,上面的测试和总结都是基于Visual Studio 编译器。