先来看一段简单的代码。
这道题也是经常在笔试中会遇到的,很多人可能都不能完全答对,一般只要c++有一定的经验,大部分都能答对前面两个函数调用,对于剩下两个可能就不太确定了,虽然感觉不太实用,但这的的确确能体现一个程序员对类继承的理解深度。
有些人的可能答案是这样的:
CDerived::func1
CBase::func2
CBase::func1
CBase::func2
但结果真是这样的吗?
首先我们分析一下上面的结果。
① 函数调用pBase->func1()的结果显然是CDerived::func1。会c++的都知道,这是多态的表现,基类的指针指向派生类对象,如果该函数是虚函数,那会调用会调用当前实际对象的vtable表中对应的函数,也就是CDerived::func1。
② 函数调用pBase->func2()的结果是CBase::func2。原因在于:如果被调用的函数不是虚函数,那么函数调用会根据指针的类型来调用,而这里是基类指针在调用,所以最终调用的是基类的函数func2() ,输出为CBase::func2。
对于这句话,CDerived* pDerived = (CDerived*)&base,我原先的想法是,这肯定有问题,怎么能直接将基类对象的指针强转成派生类的指针呢,这样肯定会引起对象的切割。但实际上,这和对象切割一点关系都没有,如果是(CDerived)base,将对象base强转成CDerived的对象,这才叫对象切割。所以,这里直接进行指针转化是没有问题的。不就将指针的类型转换一下嘛,又没有改变指针本身的值,所以这里没有危险。
③ 对于函数调用pDerived->func1()来说,因为之前进行了指针的转换,将基类的对象base的地址赋值给派生类的指针,所以很多人就不确定了,到底是调用哪个类的函数呢,是CBase::func1还是CDerived::func1,结果往往是连懵带猜,随便写个算了。其实如果再对vtable稍微深想一下的话,可能就知道应该是什么样的答案了。
我的想法是,无论是pBase->func1()还是pDerived->func1(),它们的做法都是需要先判断函数fun1是不是虚函数,如果是的话,那会就会去vtable里面去找,当前对象应该调用的那个函数。每个类在声明一个或多个对象之后,如果类中有虚函数,那么就会为每个对象镶嵌一个__vfptr的指针,用它来指向vtable表,并且同一个类的所有对象的__vfptr值和vtable表中的内容都是完全一样的【但是,我有个疑问,因为同一个类的所有对象共享一个__vfptr指针,到底__vfptr能不能看成类的静态成员?这个估计与编译器实现有关】。所以,对于pDerived->func1()来说,它会去pDerived所指向对象的的vtable表中查找一下fun1这个函数,而pDerived所指向对象是base,也就是基类的对象,因此,找到的函数就是CBase::func1,所以,答案是:CBase::func1。
④ 如果能理解②和③的话,对于函数调用pDerived->func2()来说,答案就已经很明确了,就是CDerived::func2了。因为fun2不是虚函数,所以函数调用的时候不会去vtable中去找,也就是说,fun2的调用不能根据对象的实际类型来找对应的函数,而只能根据调用的指针的类型来判断调用的函数。
所以,最终的结果是:
CDerived::func1
CBase::func2
CBase::func1
CDerived::func2