C++学习从零开始(四) |
虚函数 虚继承了一个函数类型的映射元素,依照虚继承的说法,应该是 直接 获得此函数的地址,但 后果却是 直接 获得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不是类型 润饰符,它 润饰一个成员函数只是告诉编译器在 使用那个成员函数的地方都应该 直接 获得其地址 。 为何要提供虚这个概念?即虚函数和虚继承的 意思是什么?出于篇幅 制约,将在本文的下篇给出它们 意思的 探讨,即时 注明多态性和实例复制等问题 。 |