返回课程

  虚函数

  虚继承了一个函数类型的映射元素,按照虚继承的说法,应该是间接获得此函数的地址,但结果却是间接获得this参数的值。为了间接获得函数的地址,C++又提出了一种语法--虚函数。在类型定义符“{}”中书写函数声明或定义时,在声明或定义语句前加上关键字virtual即可,如下:

  struct A { long a; virtual void ABC(), BCD(); };

  void A::ABC() { a = 10; } void A::BCD() { a = 5; }

  上面等同于下面:

  struct A { void ( A::*pF )(); long a; void ABC(), BCD(); A(); };

  void A::ABC() { a = 10; } void A::BCD() { a = 5; }

  void ( A::*AVF[] )() = { A::ABC, A::BCD }; void A::A() { pF = AVF; }

  这里A的成员A::pF和之前的虚类表一样,是一个指针,指向一个数组,这个数组被称作虚函数表(Virtual Function Table),是一个函数指针的数组。这样使用A::ABC时,将通过给出A::ABC在A::pF中的序号,由A::pF间接获得,因此A a; a.ABC();将等同于( a.*( a.pF[0] ) )();。因此结构A的长度是8字节,再看下面的代码:

  struct B : public A { long b; void ABC(); }; struct C : public A { long c; virtual void

  ABC(); };

  struct BB : public B { long bb; void ABC(); }; struct CC : public C { long cc; void

  ABC(); };

  void main() { BB bb; bb.ABC(); CC cc; cc.cc = 10; }

  首先,上面执行bb.ABC()但没有给出BB::ABC或B::ABC的定义,因此上面虽然编译通过,但连接时将失败。其次,上面没有执行cc.ABC();但连接时却会说CC::ABC未定义以表示这里需要CC::ABC的地址,为什么?因为生成了CC的实例,而CC::pF就需要在编译器自动为CC生成的缺省构造函数中被正确初始化,其需要CC::ABC的地址来填充。接着,给出如下的各函数定义。

  void B::ABC() { b = 13; } void C::ABC() { c = 13; }

  void BB::ABC() { bb = 13; b = 10; } void CC::ABC() { cc = 13; c = 10; }

  如上后,对于bb.ABC();,等同于bb.BB::ABC();,虽然有三个BB::ABC的映射元素,但只有一个映射元素的类型为void( BB:: )(),其映射BB::ABC的地址。由于BB::ABC并没有用virtual修饰,因此上面将等同于bb.BB::ABC();而不是( bb.*( pF[0] ) )();,bb将为13。对于cc.ABC();也是同样的,cc将为13。

  对于( ( B* )&bb )->ABC();,因为左侧类型为B*,因此将为( ( B* )&bb )->B::ABC();,由于B::ABC并没被定义成虚函数,因此这里等同于( ( B* )&bb )->B::ABC();,b将为13。对于( ( C* )&cc )->ABC();,同样将为( ( C* )&cc )->C::ABC();,但C::ABC被修饰成虚函数,则前面等同于C *pC = &cc; ( pC->*( pC->pF[0] ) )();。这里先将cc转换成C的实例,偏移0。然后根据pC->pF[0]来间接获得函数的地址,为CC::ABC,c将为10。因为cc是CC的实例,在其被构造时将填充cc.pF。

  那么如下:

  void ( CC::*CCVF[] )() = { CC::ABC, CC::BCD }; CC::CC() { cc.pF = &CCVF; }

  因此导致pC->ABC();结果调用的竟是CC::ABC而不是C::ABC,这正是由于虚的缘故而间接获得函数地址导致的。同样道理,对于( ( A* )&cc )->ABC();和( ( A* )&bb )->ABC();都将分别调用CC::ABC和BB::ABC。但请注意,( pC->*( pC->pF[0] ) )();中,pC是C*类型的,而pC->pF[0]返回的CC::ABC是void( CC:: )()类型的,而上面那样做将如何进行实例的隐式类型转换?如果不进行将导致操作错误的成员。可以像前面所说,让CCVF的每个成员的长度为8个字节,另外4个字节记录需要进行的偏移。但大多数类其实并不需要偏移(如上面的CC实例转成A实例就偏移0),此法有些浪费资源。VC对此给出的方法如下,假设CC::ABC对应的地址为6000,并假设下面标号P处的地址就为6000,而CC::A_thunk对应的地址为5990。

  void CC::A_thunk( void *this )

  {

  this = ( ( char* )this ) + diff;

  P:

  // CC::ABC的正常代码

  }

  因此pC->pF[0]的值为5990,而并不是CC::ABC对应的6000。上面的diff就是相应的偏

  移,对于上面的例子,diff应该为0,所以实际中pC->pF[0]的值还是6000(因为偏移为0,没

  必要是5990)。此法被称作thunk,表示完成简单功能的短小代码。对于多重继承,如下:

  struct D : public A { long d; };

  struct E : public B, public C, public D { long e; void ABC() { e = 10; } };

  上面将有三个虚函数表,因为B、C和D都各自带了一个虚函数表(因为从A派生)。

  结果上面等同于:

  struct E

  {

  void ( E::*B_pF )(); long B_a, b;

  void ( E::*C_pF )(); long C_a, c;

  void ( E::*D_pF )(); long D_a, d; long e; void ABC() { e = 10; } E();

  void E_C_thunk_ABC() { this = ( E* )( ( ( char* )this ) - 12 ); ABC(); }

  void E_D_thunk_ABC() { this = ( E* )( ( ( char* )this ) - 24 ); ABC(); }

  };

  void ( E::*E_BVF[] )() = { E::ABC, E::BCD };

  void ( E::*E_CVF[] )() = { E::E_C_thunk_ABC, E::BCD };

  void ( E::*E_DVF[] )() = { E::E_D_thunk_ABC, E::BCD };

  E::E() { B_pF = E_BVF; C_pF = E_CVF; D_pF = E_DVF; }

  结果E e; C *pC = &e; pC->ABC(); D *pD = &e; pD->ABC();,假设e的地址为3000,则pC的值为3012,pD的值为3024。结果pC->pF的值就是E_CVF,pD->pF的值就是E_DVF,如此就解决了偏移问题。同样,对于前面的虚继承,当类里有多个虚类表时,如:

  struct A {};

  struct B : virtual public A{}; struct C : virtual public A{}; struct D : virtual public A{};

  struct E : public B, public C, public D {};

  这是E将有三个虚类表,并且每个虚类表都将在E的缺省构造函数中被正确初始化以保证虚继承的含义--间接获得。而上面的虚函数表的初始化之所以那么复杂也都只是为了保证间接获得的正确性。

  应注意上面将E_BVF的类型定义为void( E::*[] )()只是由于演示,希望在代码上尽量符合语法而那样写,并不表示虚函数的类型只能是void( E:: )()。实际中的虚函数表只不过是一个数组,每个元素的大小都为4字节以记录一个地址而已。因此也可如下:

  struct A { virtual void ABC(); virtual float ABC( double ); };

  struct B : public A { void ABC(); float ABC( double ); };

  则B b; A *pA = &b; pA->ABC();将调用类型为void( B:: )()的B::ABC,而pA->ABC( 34 );将调用类型为float( B:: )( double )的B::ABC。它们属于重载函数,即使名字相同也都是两个不同的虚函数。还应注意virtual和之前的public等,都只是从语法上提供给编译器一些信息,它们给出的信息都是针对某些特殊情况的,而不是所有在使用数字的地方都适用,因此不能作为数字的类型。所以virtual不是类型修饰符,它修饰一个成员函数只是告诉编译器在运用那个成员函数的地方都应该间接获得其地址。

  为什么要提供虚这个概念?即虚函数和虚继承的意义是什么?出于篇幅限制,将在本文的下篇给出它们意义的讨论,即时说明多态性和实例复制等问题。


  • 扫一扫 扫二维码继续学习