C++从零开始(十一)中篇
——类的相关知识
本文关于虚函数的论述有一处严重错误,关于此错误已在本文的评论中说明,对此造成的不便,在此深感抱歉。
由于篇幅限制,本篇为《C++从零开始(十一)》的中篇,说明多重继承、虚继承和虚函数的实现方式。
多重继承
这里有个有趣的问题,如下:
struct A { long a, b, c; char d; }; struct B : public A { long e, f; };
上面的B::e和B::f映射的偏移是多少?不同的编译器有不同的映射结果,对于派生的实现,C++并没有强行规定。大多数编译器都是让B::e映射的偏移值为16(即A的长度,关于自定义类型的长度可参考《C++从零开始(九)》),B::f映射20。这相当于先把空间留出来排列父类的成员变量,再排列自己的成员变量。但是存在这样的语义——西红柿即是蔬菜又是水果,鲸鱼即是海洋生物又是脯乳动物。即一个实例既是这种类型又是那种类型,对于此,C++提供了多重派生或称多重继承,用“,”间隔各父类,如下:
struct A { long A_a, A_b, c; void ABC(); }; struct B { long c, B_b, B_a; void ABC(); };
struct AB : public A, public B { long ab, c; void ABCD(); };
void A::ABC() { A_a = A_b = 10; c = 20; }
void B::ABC() { B_a = B_b = 20; c = 10; }
void AB::ABCD() { A_a = B_a = 1; A_b = B_b = 2; c = A::c = B::c = 3; }
void main() { AB ab; ab.A_a = 3; ab.B_b = 4; ab.ABC(); }
上面的结构AB从结构A和结构B派生而来,即我们可以说ab既是A的实例也是B的实例,并且还是AB的实例。那么在派生AB时,将生成几个映射元素?照前篇的说法,除了AB的类型定义符“{}”中定义的AB::ab和AB::c以外(类型均为long AB::),还要生成继承来的映射元素,各映射元素名字的修饰换成AB::,类型不变,映射的值也不变。因此对于两个父类,则生成8个映射元素(每个类都有4个映射元素),比如其中一个的名字为AB::A_b,类型为long A::,映射的值为4;也有一个名字为AB::B_b,类型为long B::,映射的值依旧为4。注意A::ABC和B::ABC的名字一样,因此其中两个映射元素的名字都为AB::ABC,但类型则一个为void( A:: )()一个为void( B:: )(),映射的地址分别为A::ABC和B::ABC。同样,就有三个映射元素的名字都为AB::c,类型则分别为long A::、long B::和long AB::,映射的偏移值依次为8、0和28。照前面说的先排列父类的成员变量再排列子类的成员变量,因此类型为long AB::的AB::c映射的值为两个父类的长度之和再加上AB::ab所带来的偏移。
注意问题,上面继承生成的8个映射元素中有两对同名,但不存在任何问题,因为它们的类型不同,而最后编译器将根据它们各自的类型而修改它们的名字以形成符号,这样连接时将不会发生重定义问题,但带来其他问题。ab.ABC();一定是ab.AB::ABC();的简写,因为ab是AB类型的,但现在由于有两个AB::ABC,因此上面直接书写ab.ABC将报错,因为无法知道是要哪个AB::ABC,这时怎么办?
回想本文上篇提到的公共、保护、私有继承,其中说过,公共就表示外界可以将子类的实例当作父类的实例来看待。即所有需要用到父类实例的地方,如果是子类实例,且它们之间是公共继承的关系,则编译器将会进行隐式类型转换将子类实例转换成父类实例。因此上面的ab.A_a = 3;实际是ab.AB::A_a = 3;,而AB::A_a的类型是long A::,而成员操作符要求两边所属的类型相同,左边类型为AB,且AB为A的子类,因此编译器将自动进行隐式类型转换,将AB的实例变成A的实例,然后再计算成员操作符。
注意前面说AB::A_b和AB::B_b的偏移值都为4,则ab.A_b = 3;岂不是等效于ab.B_b = 3;?即使按照上面的说法,由于AB::A_b和AB::B_b的类型分别是long A::和long B::,也最多只是前者转换成A的实例后者转换成B的实例,AB::A_b和AB::B_b映射的偏移依旧没变啊。因此变的是成员操作符左边的数字。对于结构AB,假设先排列父类A的成员变量再排列父类B的成员变量,则AB::B_b映射的偏移就应该为16(结构A的长度加上B::c引入的偏移),但它实际映射为4,因此就将成员操作符左侧的地址类型的数字加上12(结构A的长度)。而对于AB::A_b,由于结构A的成员变量先被排列,故只偏移0。假设上面ab对应的地址为3000,对于ab.B_b = 4;,AB类型的地址类型的数字3000在“.”的左侧,转成B类型的地址类型的数字3012(因为偏移12),然后再将“.”右侧的偏移类型的数字4加上3012,最后返回类型为long的地址类型的数字3016,再继续计算“=”。同样也可知道ab.A_a = 3;中的成员操作符最后返回long类型的地址类型的数字3000,而ab.A_b将返回3004,ab.ab将返回3024。
同样,这样也将进行隐式类型转换long AB::*p = &AB::B_b;。注意AB::B_b的类型为long B::,则将进行隐式类型转换。如何转换?原来AB::B_b映射的偏移为4,则现在将变成12+4=16,这样才能正确执行ab.*p = 10;。
这时再回过来想刚才提的问题,AB::ABC无法区别,怎么办?注意还有映射元素A::ABC和B::ABC(两个AB::ABC就是由于它们两个而导致的),因此可以书写ab.A::ABC();来表示调用的是映射到A::ABC的函数。这里的A::ABC的类型是void( A:: )(),而ab是AB,因此将隐式类型转换,则上面没有任何语法问题(虽然说A::ABC不是结构AB的成员,但它是AB的父类的成员,C++允许这种情况,也就是说A::ABC的名字也作为类型匹配的一部分而被使用。如假设结构C也从A派生,则有C::a,但就不能书写ab.C::a,因为从C::a的名字可以知道它并不属于结构AB)。同样ab.B::ABC();将调用B::ABC。注意上面结构A、B和AB都有一个成员变量名字为c且类型为long,那么ab.c = 10;是否会如前面ab.ABC();一样报错?不会,因为有三个AB::c,其中有一个类型和ab的类型匹配,其映射的偏移为28,因此ab.c将会返回3028。而如果期望运用其它两个AB::c的映射,则如上通过书写ab.A::c和ab.B::c来偏移ab的地址以实现。
注意由于上面的说法,也就可以这样:void( AB::*pABC )() = B::ABC; ( ab.*pABC )();。这里的B::ABC的类型为void( B:: )(),和pABC不匹配,但正好B是AB的父类,因此将进行隐式类型转换。如何转换?因为B::ABC映射的是地址,而隐式类型转换要保证在调用B::ABC之前,先将this的类型变成B*,因此要将其加12以从AB*转变成B*。由于需要加这个12,但B::ABC又不是映射的偏移值,因此pABC实际将映射两个数字,一个是B::ABC对应的地址,一个是偏移值12,结果pABC这个指针的长度就不再如之前所说的为4个字节,而变成了8个字节(多出来的4个字节用于记录偏移值)。
还应注意前面在AB::ABCD中直接书写的A_b、c、A::c等,它们实际都应该在前面加上this->,即A_b = B_b = 2;实际为this->A_b = this->B_b = 2;,则同样如上,this被偏移了两次以获得正确的地址。注意上面提到的隐式类型转换之所以会进行,是因为继承时的权限满足要求,否则将失败。即如果上面AB保护继承A而私有继承B,则只有在AB的成员函数中可以如上进行转换,在AB的子类的成员函数中将只能使用A的成员而不能使用B的成员,因为权限受到限制。如下将失败。
struct AB : protected A, private B {…};
struct C : public AB { void ABCD(); };
void C::ABCD() { A_b = 10; B_b = 2; c = A::c = B::c = 24; }
这里在C::ABCD中的B_b = 2;和B::c = 24;将报错,因为这里是AB的子类,而AB私有继承自B,其子类无权将它看作B。但只是不会进行隐式类型转换罢了,依旧可以通过显示类型转换来实现。而main函数中的ab.A_a = 3; ab.B_b = 4; ab.A::ABC();都将报错,因为这是在外界发起的调用,没有权限,不会自动进行隐式类型转换。
注意这里C::ABCD和AB::ABCD同名,按照上面所说,子类的成员变量都可以和父类的成员变量同名(上面AB::c和A::c及B::c同名),成员函数就更没有问题。只用和前面一样,按照上面所说进行类型匹配检验即可。应注意由于是函数,则可以参数变化而函数名依旧相同,这就成了重载函数。
虚继承
前面已经说了,当生成了AB的实例,它的长度实际应该为A的长度加B的长度再加上AB自己定义的成员所占有的长度。即AB的实例之所以又是A的实例又是B的实例,是因为一个AB的实例,它既记录了一个A的实例又记录了一个B的实例。则有这么一种情况——蔬菜和水果都是植物,海洋生物和脯乳动物都是动物。即继承的两个父类又都从同一个类派生而来。假设如下:
struct A { long a; };
struct B : public A { long b; }; struct C : public A { long c; };
struct D : public B, public C { long d; };
void main() { D d; d.a = 10; }
上面的B的实例就包含了一个A的实例,而C的实例也包含了一个A的实例。那么D的实例就包含了一个B的实例和一个C的实例,则D就包含了两个A的实例。即D定义时,将两个父类的映射元素继承,生成两个映射元素,名字都为D::a,类型都为long A::,映射的偏移值也正好都为0。结果main函数中的d.a = 10;将报错,无法确认使用哪个a。这不是很奇怪吗?两个映射元素的名字、类型和映射的数字都一样!编译器为什么就不知道将它们定成一个,因为它们实际在D的实例中表示的偏移是不同的,一个是0一个是8。同样,为了消除上面的问题,就书写d.B::a = 1; d.C::a = 2;以表示不同实例中的成员a。可是B::a和C::a的类型不都是为long A::吗?但上面说过,成员变量或成员函数它们自身的名字也将在类型匹配中起作用,因此对于d.B::a,因为左侧的类型是D,则看右侧,其名字表示为B,正好是D的父类,先隐式类型转换,然后再看类型,是A,再次进行隐式类型转换,然后返回数字。假设上面d对应的地址为3000,则d.C::a先将d这个实例转换成C的实例,因此将3000偏移8个字节而返回long类型的地址类型的数字3008。然后再转换成A的实例,偏移0,最后返回3008。
上面说明了一个问题,即希望从A继承来的成员a只有一个实例,而不是像上面那样有两个实例。假设动物都有个饥饿度的成员变量,很明显地鲸鱼应该只需填充一个饥饿度就够了,结果有两个饥饿度就显得很奇怪。对此,C++提出了虚继承的概念。其格式就是在继承父类时在权限语法的前面加上关键字virtual即可,如下:
struct A { long a, aa, aaa; void ABC(); }; struct B : virtual public A { long b; };
这里的B就虚继承自A,B::b映射的偏移为多少?将不再是A的长度12,而是4。而继承生成的3个映射元素还是和原来一样,只是名字修饰变成B::而已,映射依旧不变。那么为什么B::b是4?之前的4个字节用来放什么?上面等同于下面:
struct B { long *p; long b; long a, aa, aaa; void ABC(); };
long BDiff[] = { 0, 8 }; B::B(){ p = BDiff; }
上面的B::p指向一全局数组BDiff。什么意思?B的实例的开头4个字节用来记录一个地址,也就相当于是一个指针变量,它记录的地址所标识的内存中记录着由于虚继承而导致的偏移值。上面的BDiff[1]就表示要将B实例转成A实例,就需要偏移BDiff[1]的值8,而BDiff[0]就表示要将B实例转成B实例需要的偏移值0。为什么还要来个B实例转B实例?后面说明。但为什么是数组?因为一个类可以通过多重派生而虚继承多个类,每个类需要的偏移值都会在BDiff的数组中占一个元素,它被称作虚类表(Virtual Class Table)。
因此当书写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不是类型修饰符,它修饰一个成员函数只是告诉编译器在运用那个成员函数的地方都应该间接获得其地址。
为什么要提供虚这个概念?即虚函数和虚继承的意义是什么?出于篇幅限制,将在本文的下篇给出它们意义的讨论,即时说明多态性和实例复制等问题。