5.2 多重继承下的虚函数 如果从多个有虚函数的基类继承,一个实例就有可能包含多个vfptr.考虑如下的R和S类: struct R { int r1; virtual void pvf(); // new virtual void rvf(); // new }; struct S : P, R { int s1; void pvf(); // overrides P::pvf and R::pvf void rvf(); // overrides R::rvf void svf(); // new }; 这里R是另一个包含虚函数的类。因为S从P和R多重继承,S的实例内嵌P和R的实例,以及S自身的数据成员S::s1.注意,在多重继承下,靠右的基类R,其实例的地址和P与S不同。S::pvf覆盖了P::pvf()和R::pvf(),S::rvf()覆盖了R::rvf()。 S s; S* ps = &s; ((P*)ps)->pvf(); // (*(P*)ps)->P::vfptr[0])((S*)(P*)ps) ((R*)ps)->pvf(); // (*(R*)ps)->R::vfptr[0])((S*)(R*)ps) ps->pvf(); // one of the above; calls S::pvf() 译者注: 调用((P*)ps)->pvf()时,先到P的虚函数表中取出第一项,然后把ps转化为S*作为this指针传递进去; 调用((R*)ps)->pvf()时,先到R的虚函数表中取出第一项,然后把ps转化为S*作为this指针传递进去; 因为S::pvf()覆盖了P::pvf()和R::pvf(),在S的虚函数表中,相应的项也应该被覆盖。然而,我们很快注意到,不光可以用P*,还可以用R*来调用pvf()。问题出现了:R的地址与P和S的地址不同。表达式(R*)ps与表达式(P*)ps指向类布局中不同的位置。因为函数S::pvf希望获得一个S*作为隐藏的this指针参数,虚函数必须把R*转化为S*.因此,在S对R虚函数表的拷贝中,pvf函数对应的项,指向的是一个"调整块"的地址,该调整块使用必要的计算,把R*转换为需要的S*. 译者注:这就是"thunk1: this-= sdPR; goto S::pvf"干的事。先根据P和R在S中的偏移,调整this为P*,也就是S*,然后跳转到相应的虚函数处执行。 在微软VC++(www.cppentry.com)实现中,对于有虚函数的多重继承,只有当派生类虚函数覆盖了多个基类的虚函数时,才使用调整块。 5.3 地址点与"逻辑this调整" 考虑下一个虚函数S::rvf(),该函数覆盖了R::rvf()。我们都知道S::rvf()必须有一个隐藏的S*类型的this参数。但是,因为也可以用R*来调用rvf(),也就是说,R的rvf虚函数槽可能以如下方式被用到: ((R*)ps)->rvf(); // (*((R*)ps)->R::vfptr )((R*)ps) 所以,大多数实现用另一个调整块将传递给rvf的R*转换为S*.还有一些实现在S的虚函数表末尾添加一个特别的虚函数项,该虚函数项提供方法,从而可以直接调用ps->rvf(),而不用先转换R*.MSC++(www.cppentry.com)的实现不是这样,MSC++(www.cppentry.com)有意将S::rvf编译为接受一个指向S中嵌套的R实例,而非指向S实例的指针(我们称这种行为是"给派生类的指针类型与该虚函数第一次被引入时接受的指针类型相同")。所有这些在后台透明发生,对成员变量的存取,成员函数的this指针,都进行"逻辑this调整". 当然,在debugger中,必须对这种this调整进行补偿。 ps->rvf(); // ((R*)ps)->rvf(); // S::rvf((R*)ps) 译者注:调用rvf虚函数时,直接给入R*作为this指针。 所以,当覆盖非最左边的基类的虚函数时,MSC++(www.cppentry.com)一般不创建调整块,也不增加额外的虚函数项。 5.4 调整块 正如已经描述的,有时需要调整块来调整this指针的值(this指针通常位于栈上返回地址之下,或者在寄存器中),在this指针上加或减去一个常量偏移,再调用虚函数。某些实现(尤其是基于cfront的)并不使用调整块机制。它们在每个虚函数表项中增加额外的偏移数据。每当虚函数被调用时,该偏移数据(通常为0),被加到对象的地址上,然后对象的地址再作为this指针传入。 ps->rvf(); // struct { void (*pfn)(void*); size_t disp; }; // (*ps->vfptr[i].pfn)(ps + ps->vfptr[i].disp); 译者注:当调用rvf虚函数时,前一句表示虚函数表每一项是一个结构,结构中包含偏移量;后一句表示调用第i个虚函数时,this指针使用保存在虚函数表中第i项的偏移量来进行调整。 这种方法的缺点是虚函数表增大了,虚函数的调用也更加复杂。 现代基于PC的实现一般采用"调整-跳转"技术: S::pvf-adjust: // MSC++(www.cppentry.com) this -= SdPR; goto S::pvf() 当然,下面的代码序列更好(然而,当前没有任何实现采用该方法): S::pvf-adjust: this -= SdPR; // fall into S::pvf() S::pvf() { … } 译者注:IBM的C++(www.cppentry.com)编译器使用该方法。 5.5 虚继承下的虚函数 T虚继承P,覆盖P的虚成员函数,声明了新的虚函数。如果采用在基类虚函数表末尾添加新项的方式,则访问虚函数总要求访问虚基类。在VC++(www.cppentry.com)中,为了避免获取虚函数表时,转换到虚基类P的高昂代价,T中的新虚函数通过一个新的虚函数表获取,从而带来了一个新的虚函数表指针。该指针放在T实例的顶端。 struct T : virtual P { int t1; void pvf(); // overrides P::pvf virtual void tvf(); // new }; void T::pvf() { ++p1; // ((P*)this)->p1++; // vbtable lookup! ++t1; // this->t1++; } |