C 的多态性据前辈们所说,是非常难以理解的一部分内容,虽然他实现很简单,但是套用到各种设计模式后,你会非常难以理解,但无论怎样,笔者始终认为,如果了解了内部的实现原理,实际就不会那么难了。本文将介绍虚函数表的相关内容,阐述了它与多态之间难以割舍的关系。
默认情况下,一个没有任何成员变量的类,大小是 1 个字节,如下所示:
代码语言:javascript复制#include
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 个字节。
代码语言:javascript复制#include
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)。下图为该函数表的形象图:
【代码推演】
代码语言:javascript复制#include
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) << “tt”;
pFun = (Fun) *((int*)(*((int*)&a)));
pFun();
// 如果想调用第二个函数,那么在这个地址的基础上 1就得到了第二个函数的地址
cout << “_vfptr func1 address = “ << (int*) *((int*)(*((int*)&a)) 1) << “tt”;
pFun = (Fun) *((int*)(*((int*)&a)) 1);
pFun();
// 一次类推, 2就得到了第三个函数的地址
cout << “_vfptr func2 address = “ << (int*) *((int*)(*((int*)&a)) 2) << “tt”;
pFun = (Fun) *((int*)(*((int*)&a)) 2);
pFun();
return 0;
}
以上,我们只是证实了一下虚函数表的存在,并通过间接的手段调用了一次虚函数表里面的函数。当然我们并不是单纯的只是让大家知道他的存在,而是结合虚函数表,引导大家学习多态的实现。 我们写了一个子类,继承了类 A,并且,在子类中编写了一个与类 A 中同名、同返回值、同参数(同类型、同位置)的函数 func,如下:
代码语言:javascript复制class B : public A
{
public:
void func(){ cout << “class B func” << endl; }
};
此时,我们生成一个类 B 的对象,当这个对象构造完毕时,我们再次调试查看它继承的类 A 中的虚函数表中的内容。
很明显我们发现,继承下来的类 A 中的虚函数表第一个函数变成了 B::func,实际上,这个操作只是将虚函数表中的函数指针进行了覆盖。这种方式我们就称为覆写。当你使用子类对象初始化一个父类的指针时。这个指针在调用 func 函数时,会优先遍历虚函数表,如果发现同名函数,则调用之。如果没有发现再到非虚函数表以外的成员方法中寻找。这便是“多态”
代码语言:javascript复制// 会调用已经覆写的 B 类的 func 函数
A *pb = new B;
pb->func();