多继承与虚函数重复
既然说到了多继承,那么还有一个问题可能会需要解决,那就是如果两个父类里都有相同的虚函数定义,在子对象的布局里会是怎么样个情况?是否依然可以将这个虚函数指向到正确的实现代码上呢?
修改前面一个源代码,在parent2的接口里增加下面的虚函数定义:
virtual int fun1(){cout<<"parent2::fun1()"<<endl;return 0;};
上面的fun1的定义与parent1类里的完全重复相同(类型,参数列表),增加上面的代码后立即开始编译,程序正常编译通过。运行之,得到下面的结果:
child1::fun1()
child1::fun2()
这个程序居然正确的完成了执行,编译器在其中做了些怎样的工作,是怎么样避免掉队fun1函数的冲突问题呢?
让我们来看看这个时候的child1的对象布局:
class child1 size(8):
+---
| +--- (base class parent1)
0 | | {vfptr}
| +---
| +--- (base class parent2)
4 | | {vfptr}
| +---
+---
child1::$vftable@parent1@:
| &child1_meta
| 0
0 | &child1::fun1
child1::$vftable@parent2@:
| -4
0 | &child1::fun2
1 | &thunk: this-=4; goto child1::fun1
child1::fun1 this adjustor: 0
child1::fun2 this adjustor: 4
恩~~~还是两个vfptr在child1的对象布局里(不一样就怪啦,呵呵),但是第二个vfptr所指的虚函数表的内容有所变化哦!
注意看红色字体部分,虚函数表里并没有直接填写child::fun1的代码,而是多了一个 &thunk: this-=4;然后才goto child1::fun1!注意到一个关键名词thunk了吧?没错,vc在这里使用了名为thunk的技术,避免了虚函数fun1在两个基类里重复出现导致的冲突问题!(除了thunk,还有其他方法可以解决此类问题的)。
现在,我们知道为什么相同的虚函数不会在子类里出现冲突的情况了。
但是,倘若我们在基类里就是由两个冲突的普通函数,而不是虚函数,是个怎样的情况呢?
多继承产生的冲突与虚继承,虚基类
我们在parent1和parent2里添加一个相同的函数void fun3(),然后再进行编译,通过了!查看类对象布局,跟上面的完全一致。但是在main函数里调用chobj.fun3()的时候,编译器却不再能正确编译了,并且会提示“error C2385: 对”fun3“的访问不明确”的错误信息,没错,编译器不知道你要访问哪个fun3了。
如何解决这样的多继承带来的问题呢,其实有一个简单的做法。就是在方法前限定引用的具体是哪个类的函数,比如:chobj.parent1::fun3(); ,这样的写法就写明了是要调用chobj的父类parent1里的fun3()函数!
我们再看看另外一种情况,从parent1和parent2里抹去刚才添加的fun3函数,将之放到一个共同的基类里:
class commonbase
{
public:
void fun3(){cout<<"commonbase::fun3()"<<endl;}
};
而parent1和parent2都修改为从此类继承。可以看到,在这个情况下,依然需要使用chobj.parent1::fun3(); 的方式才可以正确调用到fun3,难道,在这种情况下,就不能自然的使用chobj.fun3()这样的方式了吗?
虚继承可以解决这个问题——我们在parent1和parent2继承common类的地方添加上一个关键词virtual,如下:
class parent1:virtual public commonbase
{
public:
virtual int fun1(){cout<<"parent1::fun1()"<<endl;return 0;};
};
给parent2也同样的处理,然后再次编译,这次chobj.fun3()可以编译通过了!!!
编译器这次又在私下里做了哪些工作了呢????
class child1 size(16):
+---
| +--- (base class parent1)
0 | | {vfptr}
4 | | {vbptr}
| +---
| +--- (base class parent2)
8 | | {vfptr}
12 | | {vbptr}
| +---
+---
+--- (virtual base commonbase)
+---
child1::$vftable@parent1@:
| &child1_meta
| 0
0 | &child1::fun1
child1::$vftable@parent2@:
| -8
0 | &child1::fun2
1 | &thunk: this-=8; goto child1::fun1
child1::$vbtable@parent1@:
0 | -4
1 | 12 (child1d(parent1+4)commonbase)
child1::$vbtable@parent2@:
0 | -4
1 | 4 (child1d(parent2+4)commonbase)
child1::fun1 this adjustor: 0
child1::fun2 this adjustor: 8
vbi: class offset o.vbptr o.vbte fVtorDisp
commonbase 16 4 4 0
这次变化可大了去了!!!
首先,可以看到两个类parent1和parent2的对象布局里,都多了一个vbptr的指针。而在child1的对象布局里,还有一个virtual base commonbase的虚拟基类。再看看两个vbptr的内容:
12 (child1d(parent1+4)commonbase) 这个很好理解,从parent1的vbptr开始,偏移12个字节,指向的是virtual base commonbase!
再看看4 (child1d(parent2+4)commonbase) ,从parent2的vbptr开始,便宜4个字节,也指向了virtual base commonbase!
这下明白了。虚基类在child1里只有一个共同的对象布局了,所以就可以直接用chobj.fun3()啦,当然,在commonbase里的其他成员变量此时也可以同样的方式访问了!
虽然解决方案有了,但是在一个系统的设计里,如果有一个基类出现多继承冲突的情况,大部分情况下都说明这样的设计是有问题的,应该尽量避免这样的设计,并且尽量用纯虚函数,来提取一些抽象的接口类,把共同的方法接口都抽取出来,通常就能避免多继承的问题。