同样
,这样也将进行隐式类型转换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。