有一个需求:
接口A有两个纯虚函数MethodA1和MethodA2;接口B继承自接口A,并且有它自己的虚函数MethodB1;一个类CAImpl实现了接口A,现在要做一个类CTest,它要继承自接口A和接口B,并且接口A中的实现希望用CAImpl中的。
第一个版本应该是这个样子的
class CInterfaceA
{
public:
virtualvoid MethondA1() = 0;
virtualvoid MethondA2() = 0;
};
class CInterfaceB :public CInterfaceA
{
public:
virtualvoid MethodB1() = 0;
};
class CAImpl :public CInterfaceA
{
public:
virtualvoid MethondA1()
{
printf("MethondA1\n");
}
virtualvoid MethondA2()
{
printf("MethondA2\n");
}
};
class CTest :public CInterfaceB, public CAImpl
{
public:
virtualvoid MethodB1()
{
printf("MethodB1\n");
}
};
编译,VC2005环境报错
error C2259: 'CTest' : cannot instantiate abstract class
due to following members:
'void CInterfaceA::MethondA1(void)' : is abstract
see declaration of 'CInterfaceA::MethondA1'
'void CInterfaceA::MethondA2(void)' : is abstract
see declaration of 'CInterfaceA::MethondA2'
等等
竟然说CTest类没有实现接口函数MethodA1和MethodA2,好吧,让我们修改CTest的实现为
class CTest :public CInterfaceB, public CAImpl
{
public:
virtualvoid MethodB1()
{
printf("MethodB1\n");
}
virtualvoid MethondA1()
{
printf("MethondA1 On CTest\n");
}
virtualvoid MethondA2()
{
printf("MethondA2 On CTest\n");
}
};
来运行起来看看CTest类的虚函数表到底是什么样子的
上图是CTest运行时候的内存结构,可以看到CTest的实例oTest继承的两个父类CInterfaceB和CAImpl各自都有自己的虚函数表,并且两个不同的虚函数表里指向CTest::Method1和CTest::Method2的指针也不一样,用下面的代码调用一下看结果
CTest oTest;
oTest.MethondA1();
oTest.CAImpl::MethondA1();
结果为:
MethondA1 On CTest
MethondA1
请按任意键继续. . .
这证明了确实在类CTest中有两个不同的Method1函数,而CTest中能看到的Method1和Method2函数是继承自CInterfaceB中的,所以如果不得不实现这两个函数。
那么为了实现我们的需求,上面的代码错在哪儿了呢。答案是虚继承。虚继承允许多个接口继承自一个父接口并且在运行时刻会把所有虚继承的父接口替换成统一的一个虚函数表,让我们看看修改后的代码
class CInterfaceA
{
public:
virtualvoid MethondA1() = 0;
virtualvoid MethondA2() = 0;
};
class CInterfaceB :virtual public CInterfaceA
{
public:
virtualvoid MethodB1() = 0;
};
class CAImpl :virtual public CInterfaceA
{
public:
virtualvoid MethondA1()
{
printf("MethondA1\n");
}
virtualvoid MethondA2()
{
printf("MethondA2\n");
}
};
class CTest :public CInterfaceB, public CAImpl
{
public:
virtualvoid MethodB1()
{
printf("MethodB1\n");
}
};
注意泛红的字体,两处virtual是告诉系统,CInterfaceB和CAImpl都是虚继承自CInterfaceA,编译,有警告,不理它,运行,看看这次的内存结构
发现没,接口CInterfaceB中的CInterfaceA和类CAImpl中的CInterfaceA的虚函数表相同了。
现在分析一下那个我们可以忽略的警告
warning C4250: 'CTest' : inherits 'CAImpl::CAImpl::MethondA1' via dominance
see declaration of 'CAImpl::MethondA1'
warning C4250: 'CTest' : inherits 'CAImpl::CAImpl::MethondA2' via dominance
see declaration of 'CAImpl::MethondA2'
意思是我们的类CTest中没有实现函数MethodA1和MethodA2,但是在CAImpl中实现了,所以CTest会用CImpl中的实现代码,为啥要做为一个警告出现呢,如果我们修改代码,让CInterfaceB也实现这MthodA1函数,编译器就会迷惑了,因为它不清楚究竟是要用CAImpl中实现的函数还是用CInterfaceB中实现的MethodA1函数,这时候警告就变成错误了,很明显,二义性。编译器是通过这个警告提醒我们应该在CTest中实现虚继承的接口CInterfaceA中的所有函数,所以我们如果修改CTest定义为
class CTest :public CInterfaceB, public CAImpl
{
public:
virtualvoid MethodB1()
{
printf("MethodB1\n");
}
virtualvoid MethondA1()
{
printf("MethondA1 on CTest\n");
}
virtualvoid MethondA2()
{
printf("MethondA2 on CTest\n");
}
};
内存结构为
调用代码和输出为
CTest oTest;
oTest.MethondA1();
oTest.CAImpl::MethondA1();
结果为:
MethondA1 On CTest
MethondA1
请按任意键继续. . .
内存结构中所有CInterfaceA中虚函数表都是一致的,这不奇怪,因为虚函数就是在最后运行的时候才能指定到底虚函数表用哪一套实现。
但输出却有点奇怪,既然2个虚函数表一样,为什么第二个输出是“MethodA1”而不是“MethodA1 On CTest”呢,那么只有一个解释,
oTest.CAImpl::Method1,调用的是初始化的CAmpl里的函数而不是最后生成的虚函数表里的。
总结一下,我们实现需求要采用的办法为所有继承接口A的子类都要虚继承,在子类中的一个子类实现接口A或者在最后的CTeset中实现接口A中的函数。