C++ 的多态性据前辈们所说,是非常难以理解的一部分内容,虽然他实现很简单,但是套用到各种设计模式后,你会非常难以理解,但无论怎样,笔者始终认为,如果了解了内部的实现原理,实际就不会那么难了。本文将介绍虚函数表的相关内容,阐述了它与多态之间难以割舍的关系。
默认情况下,一个没有任何成员变量的类,大小是 1 个字节,如下所示:
#include <iostream> using namespace std; class A { public: void func() { ; } }; int main(int argc, char* argv[]) { cout << "class A size = " << sizeof(A) << endl; return 0; }
给 func 函数增加一个 virtual 关键字后,class A 的大小就变成了 4 个字节。
#include <iostream> using namespace std; class A { public: virtual void func() { ; } }; int main(int argc, char* argv[]) { cout << "class A size = " << sizeof(A) << endl; return 0; }
这中间多了什么东西?使用VS调试一下我们可以看到,a对象中,多了一个成员,是_vfptr,如下图:
这是一个函数指针数组,里面包含了所有类中虚函数的指针。我们案例中只有一个虚函数,所以只看到一个,如果我们多写几个虚函数的话,就能在这个数组中看到多个函数指针。如下图:
我们称之为这个内建的隐藏数组为 “虚函数表” (virtual Table、v-Table)。下图为该函数表的形象图:
【代码推演】
#include <iostream> using namespace std; class A { public: virtual void func(){ cout << "class A func" << endl; } virtual void func1(){ cout << "class A func1" << endl; } virtual void func2(){ cout << "class A func2" << endl; } }; int main(int argc, char* argv[]) { A a; typedef void(*Fun)(); Fun pFun = NULL; cout << "object a address = " << &a << endl; // 虚函数表的地址存放在类对象内存的最起始位置的 4 个字节处 // 而 &a 是一个对象,他的大小由类中的成员决定 // 我们只想要前 4 个字节里面的内容 // 所以把强制转换成 int* 类型 (int*)&a // 再打印解引用后的内容,就得出了前 4 个字节里面存放的数据。 *((int*)&a) // 这个内容被解引用后会被解释成 int 类型的数据,而非 int* 类型 // 所以还需要再强制转换一次为 int* (int*) (*((int*)&a)) // 最后得出的就是 4 个字节的虚函数表 _vfptr 的起始地址 cout << "object a _vfptr address = " << (int*)(*((int*)&a)) << endl; // 得到了虚函数表的起始地址后想调用表中的第一个函数 // 就需要对地址解引用,得出第一个函数的地址 *((int*)(*((int*)&a))) // 然后将其强制转换为一个函数指针,进行调用 cout << "_vfptr func address = " << (int*) *((int*)(*((int*)&a)) + 0) << "\t\t"; pFun = (Fun) *((int*)(*((int*)&a))); pFun(); // 如果想调用第二个函数,那么在这个地址的基础上+1就得到了第二个函数的地址 cout << "_vfptr func1 address = " << (int*) *((int*)(*((int*)&a)) + 1) << "\t\t"; pFun = (Fun) *((int*)(*((int*)&a)) + 1); pFun(); // 一次类推,+2就得到了第三个函数的地址 cout << "_vfptr func2 address = " << (int*) *((int*)(*((int*)&a)) + 2) << "\t\t"; pFun = (Fun) *((int*)(*((int*)&a)) + 2); pFun(); return 0; }
以上,我们只是证实了一下虚函数表的存在,并通过间接的手段调用了一次虚函数表里面的函数。当然我们并不是单纯的只是让大家知道他的存在,而是结合虚函数表,引导大家学习多态的实现。
我们写了一个子类,继承了类 A,并且,在子类中编写了一个与类 A 中同名、同返回值、同参数(同类型、同位置)的函数 func,如下:
class B : public A { public: void func(){ cout << "class B func" << endl; } };
此时,我们生成一个类 B 的对象,当这个对象构造完毕时,我们再次调试查看它继承的类 A 中的虚函数表中的内容。
很明显我们发现,继承下来的类 A 中的虚函数表第一个函数变成了 B::func,实际上,这个操作只是将虚函数表中的函数指针进行了覆盖。这种方式我们就称为覆写。当你使用子类对象初始化一个父类的指针时。这个指针在调用 func 函数时,会优先遍历虚函数表,如果发现同名函数,则调用之。如果没有发现再到非虚函数表以外的成员方法中寻找。这便是“多态”
// 会调用已经覆写的 B 类的 func 函数 A *pb = new B; pb->func();