C++中的虚函数作用主要是实现多态机制。关于多态,简而言之就是用基类指针指向其派生类的实例,然后通过基类指针调用实际派生类的成员函数。这种技术可让基类的指针有“多种形态”,这是一种泛型技术。所谓泛型技术,说白了就是试图使用不变的代码来实现可变的算法。比如:模板技术,RTTI技术,虚函数技术,要么是试图做到在编译时决议,要么试图做到运行时决议。
泛型就是用不变的代码实现可变的算法。泛型技术有两种实现机制:在编译时决议,在运行是决议。
虚函数是一种在运行是决定哪个函数被调用的。其是一种运行是决议的泛型机制。
关于虚函数的使用方法,我在这里不做过多的阐述。大家可以看看相关的C++的书籍。本使用经验将从虚函数的实现机制上为大家做一个清晰的剖析。
对C++有所了解的人都应知道虚函数(Virtual Function)是通过一张虚函数表(Virtual Table)实现调用的。这个表是一个类的虚函数地址表,此表解决了继承、覆盖的问题,保证其内容真实反应实际的函数。这样,在有虚函数的类的实例中这个表被分配在了这个实例的内存中,所以,当我们用父类的指针来操作一个子类的时候,这张虚函数表就显得由为重要了,它就像一个地图一样,指明了实际所应该调用的函数。
通过基类指针做虚函数调用时(也就是做多态调用时),编译器静态地插入取得这个VPTR,并在VTA BLE表中查找函数地址的代码,这样就能调用正确的函数使晚捆绑发生。为每个类设置VTABLE、初始化VPTR、为虚函数调用插入代码,所有这些都是自动发生的。
C++的编译器应该是保证虚函数表的指针存在于对象实例中最前面的位置(为了保证取到虚函数表的有最高的性能—如果有多层继承或是多重继承的情况下)。 这意味着我们通过对象实例的地址得到这张虚函数表,然后就可以遍历其中函数指针,并调用相应的函数。
假设有这样的一个类:
class Base
{
public:
virtual void f() { cout << "Base::f" << endl; }
virtual void g() { cout << "Base::g" << endl; }
virtual void h() { cout << "Base::h" << endl; }
private:
int m_nValule;
};
按照上面的说法,我们可以通过Base的实例来得到虚函数表。
typedef void(*Fun)(void);
Base b;
Fun pFun = NULL;
cout << "虚函数表地址:" << (int*)(&b) << endl;
cout << "虚函数表 — 第一个函数地址:" << (int*)*(int*)(&b) << endl;
pFun = (Fun)*((int*)*(int*)(&b));
pFun();
在VC2010中,实际运行经果如下:
虚函数表地址:0012FED4
虚函数表 — 第一个函数地址:0044F148
Base::f
通过这个示例,我们可以看到,我们可以通过强行把&b转成int *,取得虚函数表的地址,然后,再次取址就可以得到第一个虚函数的地址了,也就是Base::f(),这在上面的程序中得到了验证(把int* 强制转成了函数指针)。通过这个示例,我们就可以知道如果要调用Base::g()和Base::h(),其代码如下:
(Fun)*((int*)*(int*)(&b)+0); // Base::f()
(Fun)*((int*)*(int*)(&b)+1); // Base::g()
(Fun)*((int*)*(int*)(&b)+2); // Base::h()
图13-1可以更直观的展示上述代码的意义。
说明
- 在图13-1中,在虚函数表的最后的一个结点是虚函数表的结束结点,就像字符串的结束符“/0”一样,其标志了虚函数表的结束。
- 这个结束标志的值在不同的编译器下是不同的。在WinXP+VS2003下,这个值是NULL。而在Ubuntu 7.10 + Linux 2.6.22 + GCC 4.1.3下,这个值是如果1,表示还有下一个虚函数表,如果值是0,表示是最后一个虚函数表。
下面继续说明“无覆盖”和“有覆盖”时的虚函数表的样子。没有覆盖父类的虚函数是毫无意义的。我之所以要讲述没有覆盖的情况,主要目的是为了给一个对比。在比较之下,我们可以更加清楚地知道其内部的具体实现。
来看看无虚函数覆盖继承时的虚函数表是什么样的。假设有如图13-2所示的一个继承关系:
在这个继承关系中,子类没有重载任何父类的函数。那么,在派生类的实例中,其虚函数表如下所示: 对于实例:Derive d; 的虚函数表如图13-3所示:
无虚函数覆盖虚函数表特点:
- 虚函数按照其声明顺序放于表中。
- 基类的虚函数在派生类的虚函数前面。
覆盖基类的虚函数是很显然的事情,不然,虚函数就变得毫无意义。来看一下子类中有虚函数重载了基类的虚函数,会是一个什么样子?假设有下面这样的一个继承关系,如图13-4。
为了更好的呈现覆盖的效果的效果,在这个类的设计中,我只覆盖了父类的一个函数:f()。那么,对于派生类的实例,其虚函数表如图13-5所示:
存在虚函数覆盖虚函数表特点:
- 覆盖的f()函数被放到了虚表中原来父类虚函数的位置。
- 没有被覆盖的函数,在虚函数表中没有发生任何变化。
根据以上分析,对于下面的程序,
Base *b = new Derive();
b->f();
由b所指的内存中的虚函数表的f()的位置已经被Derive::f()函数地址所取代,于是在实际调用发生时,是Derive::f()被调用了。这就实现了多态。
下面再来看多重继承的情况,假设有如图13-6所示的多重继承无虚函数覆盖继承关系。
多重继承无虚函数覆盖继承关系的虚函数表,如图13-7所示:
多重继承无虚函数覆盖特点
- 每个父类都有自己的虚表。
- 子类的成员函数被放到了第一个父类的表中(所谓的第一个父类是指声明顺序的首个类)。这样做就是为了解决不同的父类类型的指针指向同一个子类实例,而能够调用到实际的函数。
最后,我们分析发生虚函数覆盖的情况。如图13-8子类覆盖了父类的f()函数。
多重继承虚函数覆盖的虚函数表,如图13-9所示:
Derive d;
Base1 *b1 = &d;
Base2 *b2 = &d;
Base3 *b3 = &d;
b1->f(); // Derive::f()
b2->f(); // Derive::f()
b3->f(); // Derive::f()
b1->g(); // Base1::g()
b2->g(); // Base2::g()
b3->g(); // Base3::g()
虽然虚函数可以给我们带来多态,但是虚函数依然存在安全问题,下述介绍两种比较常见的虚函数安全问题。
(1)通过父类型的指针无法访问子类自己的虚函数。
我们知道,子类没有重载父类的虚函数是一件毫无意义的事情。因为多态也是要基于函数重载的。虽然在上面的图中我们可以看到Base1的虚表中有Derive的虚函数,但我们根本不可能使用下面的语句来调用子类的自有虚函数:
Base1 *b1 = new Derive();
b1->f1(); //编译出错
任何妄图使用父类指针想调用子类中的未覆盖父类的成员函数的行为都会被编译器视为非法,所以,这样的程序根本无法编译通过。但在运行时,我们可以通过指针的方式访问虚函数表来达到违反C++语义的行为。
(2)通过巧妙的机制可访问non-public的虚函数。
如果父类的虚函数是private或是protected的,但这些非public的虚函数同样会存在于虚函数表中,所以,我们同样可以使用访问虚函数表的方式来访问这些non-public的虚函数,这是很容易做到的。
class Base
{
private:
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();
}
请谨记
- 掌握C++的虚函数实现机制,有助于我们编写出安全易用的C++程序。同样我们也需要明白C++虚函数存在的安全性问题,避免使用这些不安全的机制。