C++内存布局生成步骤

对C++内存布局有过一定了解,但是一直都很暧昧。为了搞清楚,决定探究一下C++内存布局。

本文在VC2008下实验,有兴趣的同学可以在G++实验下

经过仔细研究。现在总结出来3条规则:

规则1.一个没有继承的C++对象的布局总是(虚表指针  + 成员变量),虚表指针至少为4字节,如果你的成员有double的话,由于内存对齐的原因,虚表指针为8字节。如果没有虚函数,那么虚表指针可以省去。


规则2.如果一个类Derived从ClassB1, ClassB2, ....ClassBN类多继承(非虚继承),那么该类对象的内存结构生成过程(这里是指猜测的编译时的生成过程而非运行时的过程)是:

a. 把ClassB1到ClassBN的内存布局全部按代码顺序依次搬过来拼接起。

b. 把Derived的虚表揉合到第一张表虚表(ClassB1的虚表,若ClassB1无虚表则依次找后面有虚表的作为第一虚表)后。揉合的方法是,把Derived新增的虚方法(不是重写覆盖基类的那些方法)加到第一张表虚表末尾。

c. 遍历所有的虚表,替换基类中被Derived重写覆盖的的方法为基类函数指针。

d. 把基类的成员变量加了整个内存布局的最后。


看一个实例

class B1
{
public:
	virtual void fun1(){};
	virtual void fun2(){};
	int m_b1;
};


class B2
{
public:
	virtual void method1(){};
	virtual void method2(){};
	int m_b2;
};




class Derived : public B1, public B2
{
public:
	virtual void fun1(){};
	virtual void method2(){};
	virtual void df(){};
	int m_b3;
};


Derived的内存布局构造顺序出下(编译期计算推测)

 第a步


第b步



第c步


第d步



经过四步操作,Derived内存布局已经完成。这里有个疑问没有解决:为什么Derived的虚表要揉合到第一个基类的虚表中去?

解决这个疑惑,我们先来看看如果调用 

Derived* pdd = new Derived;
pdd->fun1();
pdd->df();
pdd->method2();

生成的代码执行流程是如何的。

调用 pdd->fun1();

 步骤:取this指针,取第一张虚表头,取第一个函数地址,执行。

调用 pdd->df(); 

步骤:取this指针,取第一张虚表头,取第三个函数地址,执行。

调用 pdd->method2(); 

步骤:取this指针,偏移到第二个基类对象,取第二张虚表头,取第二个函数地址,执行。

可以看到在调用派生自第二个基类的方法时,this指针多做了一次偏移计算。

因此可以解释上面的疑惑:因为C++用得最多的是单继承,为了在单继承的时候调用基类和子类虚方法不至于偏移指针,就把子类的虚表和第一个基类虚表进行融合。


根据上述的生成规则。我们可以推导当产生菱形继承时的布局

class A;

class B : publicA;  // B的内存布局包括了一份A

class C : publicA;  // C的内存布局包括了一份A

class D : publicB, public C // D的内存布局包括了一份B和一份C,因此D间接包含了两份A

书上说virtual public能解决这个问题。那么现在总结一下 virtual public的生成规则


规则3.如果一个类Derived从ClassB1, ClassB2, ....ClassBN类多继承(既有虚的,又有非虚继承),那么该类对象的内存结构生成过程(这里是指猜测的编译时的生成过程而非运行时的过程)是:

等下,在介绍这个之前,我先介绍下一个类的完整的内存布局应该由四部分组成。

            
我们看到的规则2生成的布局只是“非虚继承部分”和“Derive成员部分”,当引入虚继承后,每个类的内存布局应该有这四部分。下面再说虚继承的生成过程

a.将所有基类整理顺序,非虚继承基类在前,虚继承基类在后。


b.先不考虑虚继承类,将非虚继承基类和Derived类按照规则2进行内存布局。但处理过程不包含Derived类覆盖的虚继承类的方法(这一点VC和GCC的处理可能不一样,没有亲自实验GCC),也不包括非虚继承基类的“虚继承部分”,也就是说如果非虚继承基类还虚继承于另一个类,那么非虚继承基类本内的布局就已经包括了上述三部分,但在参与规则2的计算时,它的“虚继承部分”暂时不被考虑。


c.在目前已经生成的内存布局“非虚继承部分”和“Derive成员部分”之间插入虚基类指针列表vbptr(virtual base table pointer)


d.将所有的虚继承的基类内存布局依次放到步骤c生成的内存布局之后(包括步骤b暂时忽略的“虚继承部分”和本来的虚继承类,在放入过程中,如果发现有相同的类名,那么就只放一份)


e.遍历“虚继承部分”中的虚表,把虚表替中被覆盖的方法换成Derived方法。


f.根据“虚继承部分”中的虚继承基类的偏移,填充虚基类指针列表vbptr


说了这么多,看个例子吧

class B1
{
public:
	virtual void fun1(){cout << "fun1";}
	virtual void fun2(){cout << "fun2";}
	int m_b1;
};

class B2
{
public:
	virtual void method1(){cout << "method1";}
	virtual void method2(){cout << "method2";}
	int m_b2;
};


class Derived : virtual public B1, public B2
{
public:
	virtual void fun1(){cout << "df::fun";}
	virtual void method2(){cout << "df::method2";}

	int m_b3;
};

按照步骤画出构造图

第a步,调整顺序为 

class Derived : public B2,virtual public B1

 

       第b步

        第c步

        第d步

        第e步

        第f步

 

根据这个步骤,我们可以解释在菱形继承时,如果使用了虚继承,那么在步骤d里面会根据同名类型只有一份的原则,把多余的干掉。

但是这样一来,我们调用来自于虚继承的接口就就颇费周折,如:

        Derived*pdd = new Derived;
pdd->fun1();

调用过程是:取 this指针,计算vbptr的偏移,取vbptr第二项为+8,把+8加在vbptr地址上得到B1vptr,取fun1地址,调用。

很复杂。或许你会问,在编译ppd->fun1()的时候,其实编译器知道B1 vptr的偏移,为什么不直接生成找B1 vptr的代码?

原因如下,在目前的Derived中,B1 vptr的位置是固定的。但是如果把Derived作为某类的父类,再虚继承下,再乱继承几下。根据规则3的第d步,在合并之后,你就不知道B1vptr在哪个地方了。而编译器面对Derived指针,必须生成统一的代码,这就是必须查vbptr表的原因。


以上是我自己实验的结论,如果有不妥的地方,请各位高手不吝指正。我的联系方式 chengyongxin1983@qq.com,转载请注明出处,码字费神。谢谢!
 
 

                
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值