如果在派生类内重载了基类的虚拟函数:
#includeusing namespace std; class B{ public: virtual void p(int m) { cout<<"B::p "< p(a1); pd1->p(a2); cout< p(a1); pd2->p(a2); return 0; } 运行程序输出: D::p 1 //通过对象调用,派生类版本掩盖了基类版本,因此调用的都是派生类版本 D::p 2 B::p 1 //通过指向派生类的基类指针调用,由于在指针的静态型别(基类)中所声明的虚拟函数virtual void p(int m),在派生类中没有被重写,因此调用的依然是基类版本 B::p 2 D::p 1 //通过指向派生类的派生类指针调用,由于派生类版本掩盖了基类版本,因此调用的都是派生类版本 D::p 2
可以看到,如果在派生类中重载了基类的虚拟函数,那么派生类的版本依然会掩盖基类版本的可见性。
但是为什么通过指向派生类的基类指针调用的都是基类版本呢?
个人认为:
虚函数的多态实际是通过虚函数表实现的,如果派生类重写了基类的虚函数,那么派生类会将对应的虚函数表的虚函数地址更新为派生类中重写的虚函数;如果派生类没有重写基类的虚函数,那么派生类的虚函数表中保存的依然是基类中的虚函数地址。
但是对于派生类重载了虚函数这种情况,由于重载函数在基类的虚函数表中不存在,所以派生类的虚函数表中依然保存了基类虚函数的索引及地址,然后在派生类中会增加索引来保存派生类中重载的虚函数地址。
当通过静态类型为基类的指针调用虚函数时,该指针指向了派生类对象,因此也获得的是派生类的虚函数表。但是当确定索引时,其实还是静态的,也就是依赖于指针的静态型别来确定索引(编译时确定)也就是只在基类的虚函数表范围内查找匹配的函数并确定索引,因此无法索引到派生类重载的虚函数。
可以通过如下程序进行验证:
#include#include using namespace std; class B{ public: virtual void p(int m) { cout<<"B::p "< p(a1); //pd1->p(s); //无法编译,通过基类指针无法可见派生类的重载虚函数 D *pd2 = &d; //pd2->p(a1); //无法编译,派生类重载虚函数掩盖了基类虚函数的可见性 pd2->p(s); return 0; } 可见在调用虚函数前,实际上编译器会根据指针的静态类型,到对应的类所声明的虚函数列表中做匹配 如果有可匹配的函数声明,然后获取索引,再通过虚函数表获得虚函数地址,进行函数调用
如果希望基类中声明的虚函数在派生类中依然可见,可以通过using对基类虚函数进行声明:
#includeusing namespace std; class B{ public: virtual void p(int m) { cout<<"B::p "< p(a1); pd1->p(a2); cout< p(a1); pd2->p(a2); return 0; } B::p 1 //通过对象调用,由于基类虚函数可见,因此可以匹配到正确的函数 D::p 2 B::p 1 //通过基类指针调用,由于需要根据指针的静态型别(基类指针)匹配函数声明,因此无法匹配到继承类虚函数,因此调用的是基类虚函数 B::p 2 B::p 1 //通过派生类指针调用,由于基类虚函数可见,因此可以匹配到正确的函数 D::p 2
从结果上看,其实在派生类中重置非虚函数和虚函数的表现是相同的。
结论:
1.如果不通过using在派生类中声明基类虚函数版本的可见性,那么派生类内重载的虚拟函数会掩盖基类版本。也就是说,基类中只有基类的版本,派生类中只有派生类的版本(这一点与非虚函数一致)。
2.如果通过using在派生类中声明基类版本的可见性,那么派生类内可以看到基类版本,即:基类只有基类的版本,派生类中同时有基类和派生类的版本(这一点与非虚函数一致)。
3.虚函数在完成调用前,需要先通过函数签名匹配其在虚函数表中的索引,这个过程是基于指针或引用的静态型别进行的,匹配的范围是上述1和2中描述的范围,匹配成功可以获得虚函数在虚函数表中的索引,根据索引拿到虚函数的函数地址,完成对虚函数的多态调用。



