C++虚函数的一点分析与思考 (四)

2014-11-24 02:27:51 · 作者: · 浏览: 10

main: 27] fakerect:003EF77C
CShape::SetColor: 12] pShape:003EF780
main: 30] pfakeshape:003EF780
可以看出fakerect的地址为003EF77C,在SetColor的时候进行强转的时候偏移了四个字节,当然我们通过CShape* pfakeshape = pfakerect直接进行强转的时候,还是会偏移4个字节。
结论:
对于SetColor这种很纯粹的方法来说,它位于CShape类,它仅仅看到CShape类的成员,进行成员赋值的时候,它也是根据自身的类的描述结果进行赋值,但是在将CRect*赋值给CShape*的时候,编译器会查看这个类的描述,发现CShape类没有虚函数,CRect类有虚函数,因此在CRect*转换为CShape*的时,编译器会进行自动4个字节的偏移。我认为这个应该是在编译器编译的时候自动做好的。由于我现在还没有达到能看懂编译器对代码所生成的目标代码的地步,也就没有继续向下追踪,如果你可以读懂它生成的目标代码,可以跟踪一下,是不是对于pfakerect转换为pfakeshape这一句的目标代码中,pfakeshape已经自动进行了偏移4个字节,我认为应该是编译器编译期的行为。


想做一个简单总结了:
OK,通过以上描述(说的比较乱,本身语言表达能力不行,还是边写边思考边测试的,望你理解,有问题可留言),上面的基本的讨论大部分都是针对普通成员变量m_color的赋值,非虚函数SetColor的运行机制。
1,首先讲述了继承的父类和子类的内存排布,主要包含了普通成员变量,vptr,以及字节对齐的内存(欲验证字节对齐的朋友,可以在CRect去掉虚函数再加一个double的成员变量就可以看到CRect的大小变化了。),也许你会问静态数据成员是不是位于对象里面,可以明确的答复你,不会,静态对象成员是位于类,不是位于对象的,因此不可能每一个对象存储一份copy。上面的三种其实就是整个对象的内存占用。
2,然后讲述了普通成员函数SetColor的运行机制,它被编译成的目标代码实际上是以C的机制进行的,在进行rect1.SetColor(10);这样的调用的时候,实际上也是以C的方式进行的调用SetColor(10, &rect1),这个通过加入一个普通的SetColor函数,与它对比做了验证,还测试了时间消耗,也相差不大。还讨论了通过fakerect也确定SetColor的运行机制,它就可以当成很普通的C赋值,不管你传入的实际类型是什么,它所要做的就是对传递过来的地址进行一定m_color的偏移,然后赋值。
3,然后又讨论了拓展延伸一部分,简单描述了虚函数,虚函数对于对象内存大小的影响。一不小心又扯远了,讲到了typeinfo结构,讲到了typeinfo同样是占用了vptr所指向的vbtl指针表的一个表项,可以通过类型的强转从vbtl中读取typeinfo的结构与通过typeid取到的结构的地址进行对比,讨论了Vs2008的不一致和MINGW的一致,之所以可以比较,是基于一个类的typeinfo只有一份内存来进行的。每一个对象(含有虚函数的)会存有一个指针指向它(以实现运行期的动态类型识别),typeid可以获取到它(这个应该是编译器设置的,编译器就分配好typeinfo的内存,然后给typeid指令返回它的地址)。
4,然后又讨论了父类对象无虚函数,子类对象有虚函数,在进行SetColor的时候,内部机制到底是如何运行的。再次验证了对于SetColor来说,它只根据自己自身对象的CShape结果来决定对m_color所进行的偏移,对于父类没有虚函数,子类有虚函数,于是将CRect*赋值给CShape*时外部负责默认偏移了4个字节的地址,这样可以保证SetColor赋值时仅仅考虑自身的CShape的结构,那么这个SetColor就可以在编译期根据自身的结构来对m_color进行自身的偏移,编译生成一份唯一的目标代码,外部传递的参数来保证传递过来的有效性。于是我们看到了将CRect*结构传递给CShape*的时候,外部负责进行4个字节的偏移传递给SetColor的pShape,对于SetColor方法来说,它只考虑自身的结构。还阐述了这个是编译器的工作,不过仅仅是个人猜测,理论上会是这样,为了高效而言。
5,简单测试了一下,对于偏移和不偏移的情况,cpu耗费时间基本相同,可以大概确定是编译期进行的偏移,运行期进行偏移的话,时间消耗应该会大的多。不过这个需要能观测到目标代码的大牛来查看确定。


---------------------------2013/5/12 12:22:33---------------------------------------------------------
1,对虚函数的着迷,让我禁不住再看看虚函数表:
1)既然虚函数表中有一个虚函数位于上述的pp[0]的位置,那么我们可否取出来尝试调用一下以验证一些正确性呢,说实话,有点不合常理,不过上
述的取typeinfo的时候,不就是那么的不合常理吗?好东西就是用来折腾的。
main.cpp加入以下代码:

const type_info* ptypeidinfo = &(typeid(rect1));
TRACE_FUCTION_AND_LINE("ptypeidinfo: %p", ptypeidinfo);
int *p = (int*)&rect1;
int *pp = (int*)(p[0]);//vptr
type_info *prectinfo = (type_info*) (*(pp-1));//virtual function address
TRACE_FUCTION_AND_LINE("prectinfo: %p", prectinfo);

typedef void* pDisplayFunc(CRect* rect);
TRACE_FUCTION_AND_LINE("test begin:");
pDisplayFunc* pfunc = (pDisplayFunc*)(*pp);
pfunc(&rect1);
TRACE_FUCTION_AND_LINE("test end..");
pp是vptr的值,它指向的是一个vtbl,因此可以取出它的表的第0项,*pp就是它的第0项的值,它是一个函数指针,是指向display函数,因此根据上面的讨论可以判断它的C函数原型应该是void display(CRect*);的,故而将它强转为pDisplayFunc,然后通过pfunc(&rect1)进行调用,然后你会发现它真的就是调用的CRect的display方法。
输出如下,可以看到确实调用的CRect的display方法:
[ main: 47] test begin:
[ CRect::display: 44]
[ main: 50] test end..
2)再玩点传参的吧
Shape.h中CRect类再加入一个虚函数:
virtual void display1(int i){
TR