首页 > 代码库 > 面向对象--多继承&派生类对象内存布局分析&各基类指针所指向的位置分析

面向对象--多继承&派生类对象内存布局分析&各基类指针所指向的位置分析

背景

原文链接:ordeder  http://blog.csdn.net/ordeder/article/details/25477363


关于非虚函数的成员函数的调用机制,可以参考:

http://blog.csdn.net/yuanyirui/article/details/4594805

成员函数的调用涉及到面向对象语言的反射机制。


虚函数表机制可以查看下面这个blog:

http://blog.csdn.net/haoel/article/details/1948051

总结为:

 其一:派生类由基类派生后,除了从基类中继承相应的基类数据成员,如果基类有虚函数,那么派生类还构建了一个指向虚函数表的指针__vfptr,该指针指向一个函数指针数组。
 数组中存放了基类相应虚函数的入口地址。
 其二:如果派生类重写了基类的虚函数,那么编译器对应的操作为将指向虚函数表的指针__vfptr指向的函数指针数组中相应的虚函数入口地址改变为当前派生类实现的函数入口地址;
 其三:基类指针指向派生类后,实际上指向的是从基类派生到派生类那段成员的首地址(存放__vfptr,如果定义有虚函数),基类指针在调用虚函数的额时候,是通过查该__vfptr地址指向的函数指针数组来查找函数入口地址。由二可知,如果派生类重写了虚函数,那么以上查找的虚函数的入口地址将是派生类重写的函数的入口地址。

基础知识:
1. 指针的内存截断原则,即当指针指向某个内存后,指针的取值操作会依据指针的类型类读取内存值。换句话说,内存存储的01数据信息知识信息的载体,而指针才是信息的元数据。通过指针类型才能解析出具体内存中01信息的意义。
2. C++的继承原则:编译器解释继承操作:会将基类的成员变量拷贝到派生类中。
3. 基类指针指向派生类后,根据第1点所述,基类指针只能解析派生类中从基类拷贝到派生来的那部分内存。

关于多继承的一个问题

如果C同时派生了基类A和基类B,那么C的对象的内存是如何分布的呢?基类指针pa和基类指针pb对这块派生类C对象的内存所存储的内容又做和解析?或者说pa和pb指向的地址是否是C对象的首地址呢?如果是,以依据1所说,必将解析出错误的结果。
pa = &c
pb = &c

pa ?= pb ???

实例分析

定义两个基类A和B,用C同时继承了A和B。通过分析C对象成员的内存地址分布,来分析C是如何实现A和B的继承,以及虚函数表如何维护。

#include <stdio.h>
 
class A
{
public:
	virtual void f(){
		printf("A:f\n");
	}
	virtual void g(){
		printf("A:g\n");
	}
	virtual void f1(){
		printf("A:f1\n");
	}
	int a;
};

class B
{
public:
	virtual void f(){
		printf("B:f\n");
	}
	virtual void g(){
		printf("B:g\n");
	}
	virtual void g1(){
		printf("B:g1\n");
	}
	int b;
};

class C : public A, public B 
{
public:
	void f(){
		printf("C:f\n");
	}
	void g(){
		printf("C:g\n");
	}
	int c;
};

typedef void (*Func)();

void main() {

	A a;
	B b;
	C c;

	B *pb = &c;
	A *pa = &c;
	
	puts("C对象的成员内存分布初探:");
	printf(" &c:\t%x\n &c.a:\t%x\n &c.b:\t%x\n &c.c:\t%x\n",&c,&c.a,&c.b,&c.c);

	puts("pa 与 pb");
	printf("pa:\t%x\npa->a:\t%x\n",pa,&pa->a);
	printf("pb:\t%x\npb->b:\t%x\n",pb,&pb->b);
	
	puts("C对象完整的布局");
	printf("pavtb:\t%x\npa->a:\t%x\n",pa,&pa->a);
	printf("pbvtb:\t%x\npa->a:\t%x\n",pb,&pb->b);
	printf("&c.c:\t%x\n",&c.c);

	puts("pavtb 和 pbvtb 内容分析:");
	int *pVtb = (int *)pa;
	Func funp;
	printf("pa->vftab[0]:\t");
	funp = (Func)(((int*)(*pVtb))[0]);
	funp();
	printf("pa->vftab[1]:\t");
	funp = (Func)(((int*)(*pVtb))[1]);
	funp();
	printf("pa->vftab[2]:\t");
	funp = (Func)(((int*)(*pVtb))[2]);
	funp();

	pVtb = (int *)pb;
	printf("pb->vftab[0]:\t");
	funp = (Func)(((int*)(*pVtb))[0]);
	funp();
	printf("pb->vftab[1]:\t");
	funp = (Func)(((int*)(*pVtb))[1]);
	funp();
	printf("pb->vftab[2]:\t");
	funp = (Func)(((int*)(*pVtb))[2]);
	funp();

}
调试分析:

C对象在VC中的结构图:


运行结果及分析:

C对象的成员内存分布初探:
 &c:    18ff24
 &c.a:  18ff28
 &c.b:  18ff30
 &c.c:  18ff34
//1.c的首地址和c.a的首地址不是同一个地址!
//2.c.a到c.b是int类型,内存分布为4个字节,但是他们之间的内存地址差为8(18ff28 - 18ff2c - 18ff30),说以c.a和c.b之间有内容!

pa 与 pb
pa:     18ff24
pa->a:  18ff28
pb:     18ff2c
pb->b:  18ff30
//从这个结果看来,c的首地址与pa同地址,pb的首地址和上文2所说的神秘消失的内存同地址。

C对象完整的布局
pavtb:  18ff24
pa->a:  18ff28
pbvtb:  18ff2c
pa->a:  18ff30
&c.c:   18ff34
//其实:pa和pb指向的地址为虚函数表的指针所在的地址。

pavtb 和 pbvtb 内容分析: (前文blog链接已经有详细的原理说明)
pa->vftab[0]:   C:f
pa->vftab[1]:   C:g
pa->vftab[2]:   A:f1
pb->vftab[0]:   C:f
pb->vftab[1]:   C:g
pb->vftab[2]:   B:g1

总结

从语句class C : public A, public B可以解释下图:


        图中描述了上文例程中C的对象在内存中的分布情况。C先继承了A,后继承了B,那么C对象的内存分布先为A函数构造的虚函数表指针以及继承的A成员对象,紧接着是B函数构造的虚函数表指针以及继承的B成员对象,之后才是C的成员对象。而且这也很好的解释了同样执行如下语句后
pa = &c
pb = &c
pa和pb的地址为何是不同的这个问题。其中pa指针指向的是C对象中从A类继承过来的成员的首地址(这里将虚函数表成员也看出是一个成员看待),同理pb也是指向了B类在C中相关继承成员区域的首地址。

总而言之:

1.继承是按照类为整体进行组织的,且如果有继承虚函数,那么将有多余的一个虚函数表指针。

2.基类指针指向派生类后,同样是按照指针强制转化原则来解析派生类对象的部分区块内容(指针截断)

3.多继承中,各个基类的指针指向派生类后,各自基类指针指向的是派生类中与本身基类相关的派生类区块首地址。