C++对象模型笔记:dynamic binding
编译器对于多态的实现是怎样的呢?下面请看一个例子:
Class Point { Public: Virtual void print(); …… }; Class Point2D : public Point { Public: Virtual void print(); … }; (实现部分略) Point2D pt2d; Point *pt = &pt2d; Pt->print(); //这里的多态要求是要调用Point2D:: print( );
编译器会怎么做呢?用上一篇笔记里面的name mangling是不行的。
当然,在这个例子里面,如果你编译的时候用优化选项,编译器也许会把上面三条语句优化如下:Point2D pt2d; pt2d.print( ); !!!你也许会惊讶:编译器这么牛?!是的,编译器会分基本快,然后对每一个基本块进行优化合并;(相关知识,请参考编译原理,我也已经忘的差不多了);
但是但对于下面的例子,估计再牛的编译器也没有办法:
void printPoint(Point * pt) { pt->print(); …… } //在某处调用: Point pt; … printPoint(&pt); … Point2D pt2d; … //再另外的某处调用 printPoint(&pt);
可以看到,如果不用点措施,牺牲一点东西,printPoint里面是不知道那个Point指针的所指向的真正对象是哪个的。那么,怎么办呢?(换了是你,你说怎么办?)
如果某种技术解决不了某些问题,原因就是在这些问题里面还有一些信息是某些技术所没有用到的。这就是技术的一般方法论(出处在我这里,呵呵)
那么,根据上面的方法论的指导,只需要再增加某些信息,然后再增加某些中间层,把信息放到中间层里面去,也许就可以解决(废话…)
这虽然是废话,但是也显示了多态的实质。所以就叫做显示多态实质的废话吧。
具体如下:
1、编译器遇到了class Point的定义的时候,发现有里面有virtual的成员函数,于是将这个类的定义转换如下:
//c++ 伪代码,实际的编译器是不会这么做的,他会把这些直接转成机器码。 struct Point { void *vptr_point; //vptr,指向下面定义的全局变量vtable_Point; …. //其他数据成员 }; //虚函数Point::print经过name mangling转化后的全局函数 void print_Point(const Point *p){….} //编译器自动生成的构造函数经name mangling转换过来的全局函数,以确 //保vptr正确初始化 void Point_constructor(Point *p) { p->vptr_point = vtable_Point; } void * vtable_Point[] = {&print_Point, } //虚函数表
2、对Point2D的转换如下的伪代码所示:(注意,虽然Point2D的定义里面没有定义vritual,但是其基类Point有成员函数定义了virtual,所以还是有虚函数表,即使Point2D什么都没有写,如下所示:class Point2D : public Point{}也有虚函数表,Point2D的每一个对象也还会有vptr成员。 )
//Point2D的伪代码 struct Point2D { void *vptr_point; /*vptr,其实,这个指针是从基类Point里面继承下来的,所以这的名 字不变,还是vptr_point(再次强调,这是伪代码,不要以为实际编译器 里面真的给vptr取了这个名字啊!)指向下面定义的全局变量.*/ …. //其他数据成员 }; //虚函数Point2D::print经过name mangling转化后的全局函数 void print_Point2D(const Point2D *p){….} //编译器自动生成的构造函数经name mangling转换过来的全局函数,以确保 //vptr正确初始化 void Point2D_constructor(Point2D *p) { p->vptr_point2D = vtable_Point2D; } //Point2D的虚函数表 void * vtable_Point2D[] = {&print_Point2D }; /* 注意,如果Point2D里面没有定义Print(也就是说派生类没有override 虚函数),那么这个地方的初始化就变成 Void * vtable_Point2D[] = {&print_Point}; */
那么下面的语句:
Point2D pt2d; Point *p = &pt2d; p->print();
就会变成类似于下面的伪代码:(C式的,不是C++式的)
struct Point2D pt2d; //再次说明是C式的伪代码,C语言的定义是没有什么构造函数的 Point2D_Constructor(&pt2d); //调用上面提过的由编译器自动生成的构造函数转换过来的全局函数,作用是正确初始化pt2d中的指针,让它指向Point2D的需函数表; //指针类型转换,看我笔记:指针类型转换; Point *p = (Point *)&pt2d; (p->vptr_point)[0] )(pt) //调用p里面的vptr_point[0], 注意上面的初始化中的vptr_point指向Point2D的虚函数表,这个表格的第一项就是放则print相关的入口地址:指向Point2D::print,后面那个pt是把参数(也就是this指针)传进去。
看了之后是不是觉得有点无语啊,怎么虚函数的调用原来这么麻烦!!看来还是C语言好啊,起码不会做这么多事……
注意,不是这样的,以上的过程都是在编译阶段就做好了的,那些虚函数表在编译阶段就已经做好了。所以对于多态的执行的代价如下:
1、 对于空间来说,每一个定义了virtual的类,都在全局数据区里面有一张虚函数表,虚函数表的大小决定于这个类的体系(就是这个类及其基类)中虚函数的个数。(这张表是在编译阶段就已经定义好了)
2、 以上类的每一个对象实例,在空间上多了一个指针的空间。
3、在类的构造函数里面,多了一条语句的开销(这条语句就是初始化上面多出来的指针,指向相应类型的虚函数表),这个要留意,如果类中没有声明构造函数,这个时候,编译器会自动生成一个(说到这里,不要以为编译器无论在什么时候都会为你的类生成一个默认构造函数啊~,以后的笔记会对这个问题重点讨论),因此,还多了一个你可能并不想要的调用函数的开销(当然,也可能是以内联的方式嵌到代码当中,这个就要看编译器的能力了)
4、 在执行语句p->print();的时候,由于编译器已经转换为(p->vptr_point)[0] )(pt);实际上多个间接层,看出来没有,一般的p->f()只需要f_Point()…做全局调用就可以了(name mangling转换),现在却要对p寻址,寻址了还要找vptr_point在取它指向的0-4的字节,然后再调用那个地址….说起来好像间接层不止一个……
以上的4点就是c++中虚函数调用的运行时候所付出的几乎所有代价。
所以以后参加面试的时候,有人问起:class A , A *p; … p->func() 的内部代价是怎样的?你一定一定要答详细一点,有多详细就答多详细,最好能说出前因后果,不要像我一样,当时就答:“当func是A的虚函数的时候,代价会大一些…”……是不是无语了,呵呵
(注:他当时问的是:a.func() 和pa->func()在实现上有什么不同。其实和上面的问题是一样的,我心里也知道有什么不同,只不过答的时候就说了一句话……)
这种运行时才查表来调用函数的机制,被称为动态绑定…,好像很有术语的味道,但其实也就这么回事而已,天下事有难易乎…
关于这种虚函数表的机制的内存布局图,这里就不画了,在我上面的笔记:三种内存布局里面有图;
还有两个地方需要说一下的:
1、 虚函数的实现机制不止一种,其实还有几种机制;不过这一种最高效(c++的目标之一啊),所以几乎所有的编译器都用了这种方法,当然,这种方法也是有缺点的,请参考我的“MFC消息映射原理”,或者是别的文章。
2、关于虚函数表的表项和函数的入口地址关系,在这里似乎用了一种硬编码的方法,比如索引0的表项放的是print函数的,1放的是**函数的…而且所有派生类的虚函数表的表项也得这么做,这个顺序应该是按照类的定义里面那些虚函数的声明次序来的。而且,如果派生类有新的虚函数,这些新的虚函数要在虚函数表中往后插(不能前插,因为前面已经是硬编码了,注意);这个我没有看过别人的见解,是我自己推测出来的,不过想来也应该如此,如有不对之处,请各位多多指教,小弟不胜感激。
3、关于各家编译器实现的差异,关于vptr在对象中的安插位置,不同的编译器中可能不同,比如g++ 3.4.3将其插入到每个对象的最前面一项,在vs2005中是插在最后面一项的。至于有没有插到中间的,我就不知道了。其实有一种情况是插到中间的,这个到讨论多重继承的多态性时候再说。
下面的代码有个疑问,大家不妨看看:
class B { public: virtual void print(int); //注意,这个虚函数带有参数int }; class D : public B { public: void print(float) //注意,这个print带参数float }; /*如果有如下语句:*/ D d; B *pb = &d; D *pd = &d; Pb->print(1.4f); //这个调用了哪个函数? Pd->print(1.4f); //这个函数呢?调用了那个函数?
编译器遇到这种情况,又是怎么做的呢?以上的情况,到底属于怎样的一种情况呢,请看下篇笔记,隐藏和二义性~,谢谢各位观光,呵呵。
PS. 写了这么多篇笔记,关于《深度探索C++对象模型》的内容还没有正式开始讨论呢,还是在热身阶段,本来想看看大家对我写的这些东西有什么反馈的,哪知道一点反馈都没有,要么就是灌水的贴,比如“写的好啊”之类的回帖。
那就有两种可能,一是高手看了觉得我的文章错漏百出,不屑一看;二就是大家看不懂我的文章到底在写些什么东西,觉得莫名其妙;
但是,这些文章代表的是我现在对c++的理解,如果各位发现了问题能告诉我一下,我实在是不胜感激,哪怕是评论说:“你这些文章文笔太烂,语句不通,一点都看不懂”都会对我有所帮助。
因为我觉得一个互动的平台,比一个单独的阅读和笔记更有利于提高双方的能力和理解。
另外一个,是我觉得,学习别人文章的态度,并不是一味的全盘吸收,而应该是有所怀疑,在怀疑的基础上在加以论证,从怀疑出发,经过验证,到最后得到结论,这样的学习的印象会比单独吸收结论要深的多,而且不容易偏信;(再次说明,我在这里的很多推论性的东西都是我自己推猜出来的,没有经过任何权威的肯定,也没有看过编译器源码去验证)
对人则不然,对人际交流来说应该持相反的态度,是先相信你说的话,经过考察,再慢慢的下结论;
但是我们中国人,却恰好相反,对人是一开始持怀疑的态度,慢慢慢慢的熟悉了,才相信你;对别人网上贴的文章,则是一开始就全盘接收,在经过错误的教训之后,才开始提出反对意见(这个时候往往有对其作者的辱骂一起的倾向)。
以上仅代表我个人的观点,不敢强迫于大家,再次谢谢各位在百忙之中能阅读这些笔记。