AutoCAD 3DMAX C语言 Pro/E UG JAVA编程 PHP编程 Maya动画 Matlab应用 Android
Photoshop Word Excel flash VB编程 VC编程 Coreldraw SolidWorks A Designer Unity3D
 首页 > C++

C++程序设计从零开始(十一)中篇

51自学网 http://www.wanshiok.com

 

  因此当书写B b; b.aaa = 20; long a = sizeof( b );时,a的值为20,因为多了一个4字节来记录上面说的指针。假设b对应的地址为3000。先将B的实例转换成A的实例,本来应该偏移12而返回3012,但编译器发现B是虚继承自A,则通过B::p[1]得到应该的偏移值8,然后返回3008,接着再加上B::aaa映射的8而返回3016。同样,当b.b = 10;时,由于B::b并不是被虚继承而来,直接将3000加上B::b映射的偏移值4得3004。而对于b.ABC();将先通过B::p[1]将b转成A的实例然后调用A::ABC。

  为什么要像上面那样弄得那么麻烦?首先让我们来了解什么叫做虚(Virtual)。虚就是假象,并不是真的。比如一台老式电视机有10个频道,即它最多能记住10个电视台的频率。因此可以说1频道是中央1台、5频道是中央5台、7频道是四川台。这里就称频道对我们来说代表着电台频率是虚假的,因为频道并不是电台频率,只是记录了电台频率。当我们按5频道以换到中央5台时,有可能有人已经调过电视使得5频道不再是中央5台,而是另一个电视台或者根本就是一片雪花没有信号。因此虚就表示不保证,其可能正确可能错误,因为它一定是间接得到的,其实就相当于之前说的引用。有什么好处?只用记着按5频道就是中央5台,当以后不想再看中央5台而换成中央2台,则同样的“按5频道”却能得到不同的结果,但是程序却不用再编写了,只用记着“按5频道”就又能实现换到中央2台看。所以虚就是间接得到结果,由于间接,结果将不确定而显得更加灵活,这在后面说明虚函数时就能看出来。但虚的坏处就是多了一道程序(要间接获得),效率更低。

  由于上面的虚继承,导致继承的元素都是虚的,即所有对继承而来的映射元素的操作都应该间接获得相应映射元素对应的偏移值或地址,但继承的映射元素对应的偏移值或地址是不变的, 为此红字的要求就只有通过隐式类型转换改变this的值来实现。所以上面说的B转A需要的偏移值通过一个指针B::p来间接获得以表现其是虚的。

  因此,开始所说的鲸鱼将会有两个饥饿度就可以让海洋生物和脯乳动物都从动物虚继承,因此将间接使用脯乳动物和海洋生物的饥饿度这个成员,然后在派生鲸鱼这个类时,让脯乳动物和海洋生物都指向同一个动物实例(因为都是间接获得动物的实例的,通过虚继承来间接使用动物的成员),这样当鲸鱼填充饥饿度时,不管填充哪个饥饿度,实际都填充同一个。而C++也正好这样做了。如下:

struct A { long a; };
struct B : virtual public A { long b; }; struct C : virtual public A { long c; };
struct D : public B, virtual public C { long d; };
void main() { D d; d.a = 10; }

  当从一个类虚继承时,在排列派生类时(就是决定在派生类的类型定义符“{}”中定义的各成员变量的偏移值),先排列前面提到的虚类表的指针以实现间接获取偏移值,再排列各父类,但如果父类中又有被虚继承的父类,则先将这些部分剔除。然后排列派生类自己的映射元素。最后排列刚刚被剔除的被虚继承的类,此时如果发现某个被虚继承的类已经被排列过,则不用再重复排列一遍那个类,并且也不再为它生成相应的映射元素。

  对于上面的B,发现虚继承A,则先排列前面说过的B::p,然后排列A,但发现A需要被虚继承,因此剔除,排列自己定义的映射元素B::b,映射的偏移值为4(由于B::p的占用)。最后排列A而生成继承来的映射元素B::a,所以B的长度为12。

  对于上面的D,发现要从C虚继承,因此:

  排列D::p,占4个字节。

  排列父类B,发现其中的A是被虚继承的,剔除,所以将继承映射元素B::b(还有前面编译器自动生成的B::p),生成D::b,占4个字节(编译器将B::p和D::p合并为一个,后面说明虚函数时就了解了)。

  排列父类C,发现C需要被虚继承,剔除。

  排列D自己定义的成员D::d,其映射的偏移值就为4+4=8,占4个字节。

  排列A和C,先排列A,占4个字节,生成D::a。

  排列C,先排列C中的A,结果发现它是虚继承的,并发现已经排列过A,进而不再为C::a生成映射元素。接着排列C::p和C::c,占8个字节,生成D::c。

  所以最后结构D的长度为4+4+4+4+8=24个字节,并且只有一个D::a,类型为long A::,偏移值为0。

  如果上面很昏,不要紧,上面只是给出一种算法以实现虚继承,不同的编译器厂商会给出不同的实现方法,因此上面推得的结果对某些编译器可能并不正确。不过应记住虚继承的含义—— 被虚继承的类的所有成员都必须被间接获得,至于如何间接获得,则不同的编译器有不同的处理方式。

  由于需要保证间接获得,所以对于long D::*pa = &D::a;,由于是long D::*,编译器发现D的继承体系中存在虚继承,必须要保证其某些成员的间接获得,因此pa中放的将不再是偏移值,否则d.*pa = 10;将导致直接获得偏移值(将pa的内容取出来即可),违反了虚继承的含义。为了要间接访问pa所记录的偏移值,则必须保证代码执行时,当pa里面放的是D::a时会间接,而D::d时则不间接。很明显,这要更多和更复杂的代码,大多数编译器对此的处理就是全部都使用间接获得。因此pa的长度将为8字节,其中一个4字节记录偏移,还有一个4字节记录一个序号。这个序号则用于前面说的虚类表以获得正确的因虚继承而导致的偏移量。因此前面的B::p所指的第一个元素的值表示B实例转换成B实例,是为了在这里实现全部间接获得而提供的。

  注意上面的D::p对于不同的D的实例将不同,只不过它们的内容都相同(都是结构D的虚类表的地址)。当D的实例刚刚生成时,那个实例的D::p的值将是一随机数。为了保证D::p被正确初始化,上面的结构D虽然没有生成构造函数,但编译器将自动为D生成一缺省构造函数(没有参数的构造函数)以保证D::p和上面从C继承来的C::p的正确初始化,结果将导致D d = { 23, 4 };错误,因为D已经定义了一个构造函数,即使没有在代码上表现出来。

  那么虚继承有什么意义呢?它从功能上说是间接获得虚继承来的实例,从类型上说与普通的继承没有任何区别,即虚继承和前面的public等一样,只是一个语法上的提供,对于数字的类型没有任何影响。在了解它的意义之前先看下虚函数的含义。


  三、虚函数

  虚继承了一个函数类型的映射元素,按照虚继承的说法,应该是间接获得此函数的地址,但结果却是间接获得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不是类型修饰符,它修饰一个成员函数只是告诉编译器在运用那个成员函数的地方都应该间接获得其地址。

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

 
 

上一篇:C++程序设计从零开始(十一)下篇  下一篇:C++程序设计从零开始(十一)上篇