实现上是个函数指针的数组。
1、验证虚函数表的存在
class A { int f(int i); }; Sizeof(A) = 1; | class A { int f(int i); virtual int add(int j); //几个virtual都是4 }; Sizeof(A)=4; |
1字节就是这个类在内存中的标记. 我们知道函数不会影响类的大小,函数的地址是另外被存在一片内存区域。 当一个结构中不包含成员变量时,其实就是一个只有1字节的无类型变量。 |
2、多态的原理
多态是如何利用这个虚函数表实现的呢?
struct Super
{
int data;
virtual int add(int i){ return i; };
virtual string toString()=0;
};
class Sub: public Super
{
public:
string toString(){ return string("Sub class"); }
};
Super* s = new Sub();
cout<<s->toString()<<endl;
delete s;
输出”Sub class”。
编译器会把虚表编译成上面的样式,注意两件事情:
第一, 虚函数表在类所有成员的最前面。
第二, Sub因为重写了toString,Sub的虚函数表就记录者Sub的toString的地址。
上面的程序Super* s = new Sub();
s实际指向的是Sub类对象的内存区域,所以调用方法的时候会根据这片内存记录的函数地址进行调用,多态就是这么实现的。
3、访问虚函数表,手动调用虚函数
既然虚函数表存在于内存,那么就会有方法找到这片内存区域然后调用,下面就用程序来说明这项工作。
首先来看一项技术
struct S
{
int k;
int m;
};
S a;
int i = 5;
a.k = 5;
a.m = 4;
cout<<*(int*)&a<<endl;输出5。
如此咱们就有了方法访问一个类的任意一片内存了,那么就先找到虚函数表的地址吧!
了解一个数据类型DWORD_PTR,它被定义为32位的指针(64位系统下64位),我们就用这个指针来找到虚函数表的地址。
DWORD_PTR pvTable;
pvTable = (DWORD_PTR)&Sub();
Sub()创建了一个Sub的临时对象,&Sub()表示取这个对象的地址,然后把它强制类型转换为DWORD_PTR,上文说过虚表是在类的最上面,所以pvTable就指向了这个类的虚函数表。
如下代码访问此类的虚函数:
((Sub*)pvTable)->toString()
利用这种技术可以实现动态调用,具体实现不说明了,可以参考<<精通MFC>>P19,基本思想是实现一个与被调用类匹配的结构,然后进行类型转换就可以映射到相应位置的内存。
4、虚函数表
class Base { |
按照上面的说法,我们可以通过Base的实例来得到虚函数表。下面是实际例程:
//定义一个无型参的返回类型为void的函数指针变量类型:Fun | ||
输出: | ||
通过这个示例,我们可以看到,我们可以通过强行把&b转成int *,取得虚函数表的地址,然后,再次取址就可以得到第一个虚函数的地址了,也就是Base::f(),这在上面的程序中得到了验证(把int* 强制转成了函数指针)。通过这个示例,我们就可以知道如果要调用Base::g()和Base::h(),其代码如下:
(Fun)*((int*)*(int*)(&b)+0); // Base::f() |
画个图解释一下。如下所示:
注意:在上面这个图中,我在虚函数表的最后多加了一个结点,这是虚函数表的结束结点,就像字符串的结束符“/0”一样,其标志了虚函数表的结束。这个结束标志的值在不同的编译器下是不同的。在WinXP+VS2003下,这个值是NULL。而在Ubuntu 7.10 + Linux 2.6.22 + GCC 4.1.3下,这个值如果是1,表示还有下一个虚函数表,如果值是0,表示是最后一个虚函数表。
5、一般继承(无虚函数覆盖)
下面,分别说明“无重载”和“有重载”时的虚函数表的样子。没有重载父类的虚函数是毫无意义的。我之所以要讲述没有重载的情况,主要目的是为了给一个对比。在比较之下,我们可以更加清楚地知道其内部的具体实现。 虚函数按照其声明顺序放于表中。2)父类的虚函数在子类的虚函数前面。
6、一般继承(有虚函数覆盖)
我们从表中可以看到下面几点,1)重载的f()函数被放到了虚表中原来父类虚函数的位置。2)没有被重载的函数依旧。这样,我们就可以看到对于下面这样的程序,
Base *b = new Derive();b->f(); |
由b所指的内存中的虚函数表的f()的位置已经被Derive::f()函数地址所取代,于是在实际调用发生时,是Derive::f()被调用了。这就实现了多态。
7、多重继承(无虚函数重载)
8、多重继承(有虚函数覆盖)
9、安全性
9.1、通过父类型的指针访问子类自己的虚函数
我们知道,子类没有重载父类的虚函数是一件毫无意义的事情。因为多态也是要基于函数重载的。虽然在上面的图中我们可以看到Base1的虚表中有Derive的虚函数,但我们根本不可能使用下面的语句来调用子类的自有虚函数:
Base1 *b1 = new Derive();b1->f1(); //编译出错 |
任何妄图使用父类指针想调用子类中的未重载父类的成员函数的行为都会被编译器视为非法,所以,这样的程序根本无法编译通过。但在运行时,我们可以通过指针的方式访问虚函数表来达到违反C++语义的行为。
9.2、访问non-public的虚函数
如果父类的虚函数是private或是protected的,但这些非public的虚函数同样会存在于虚函数表中,所以,我们同样可以使用访问虚函数表的方式来访问这些non-public的虚函数,这是很容易做到的。
class Base { virtual void f() { cout << "Base::f" << endl; }
};
class Derive : public Base{ };
typedef void(*Fun)(void);
void main() {Derive d;Fun pFun = (Fun)*((int*)*(int*)(&d)+0);pFun();
}