前言
静态类型:对象声明的类型为静态类型
动态类型:对象所指的类型为动态类型
静态绑定和动态绑定:绑定的是对象的静态类型还是动态类型,virtual函数都是动态绑定,其他的都是静态绑定,包括virtual函数中的默认形参。
虚函数
C++中的虚函数主要是实现了多态机制。关于多态,就是用父类型的指针指向子类型的实例,然后通过父类型的指针调用子类型的函数。这种技术可以让父类的指针有“多种形态”,这是一种泛型技术。所谓泛型技术,就是试图使用不变的代码来实现可变的算法。
虚函数表
虚函数(Virtual Function)是通过一张虚函数(Virtual Table)表来实现的。在这个表中,主要是一个类的虚函数的地址,这张表解决了继承、覆盖的问题,保证其内容反应的是实际的函数。虚函数表被所有类的实例共享。所以当我们用父类的指针操作一个子类的时候,这张虚函数表就像地图一样,指明了实际所应该调用的函数。
C++编译器应该是保证虚函数表的指针存在于对象实例中最前面的位置,这意味着我们可以通过对象实例的地址得到这张虚函数表,然后就可以遍历其中的函数指针了。假设我们有这样一个类:
class Base{
public:
virtual void f(){cout<<"Base::f"<<endl;}
virtual void g(){cout<<"Base::f"<<endl;}
virtual void h(){cout<<"Base::f"<<endl;}
};
按照上面的说法,我们就可以通过Base的实例来得到虚函数表,如下:
typedef void (*Fun)();
Base b;
Fun pFun = NULL;
cout<<"虚函数表的地址:"<<(int *)(&b)<<endl;
cout<<"虚函数表中第一个函数地址:"<<(int *)*(int *)(&b)<<endl;
//Invoke the first virtual function
pFun = (Fun)*((int *)*(int *)(&b));
pFun;
实际运行结果为:
虚函数表的地址:0012FED4
虚函数表中第一个函数地址:0044F148
Base::f
通过这个实例可以看出,我们可以强行把&b转成int *,取得虚函数表的地址,然后再次取地址可以得到第一个虚函数的地址了,也就是Base::f()。通过这个实例,我们也知道调用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()
如果代码看起来太乱,没问题,让我画个图解释一下:
图片中的.是结束符的标志,类似于字符串的’\0’的结束符。这在不同的系统中是不一样的。
下面,我将分别说明“无覆盖”和“有覆盖”时的虚函数表的样子。没有覆盖父类的虚函数是毫无意义的,这样将主要是为了对比。
一般继承(无覆盖)
假设有如下所示的一个继承关系:
请注意,在这个继承关系中,子类没有重载任何父类的函数。那么,在子类的实例中,其虚函数表如下所示:
我们可以看到以下几点:
1) 虚函数按照其声明顺序放在虚函数表中
2) 父类的虚函数在子类的虚函数前
一般继承(有覆盖)
覆盖父类的虚函数是很显然的事情,否则,虚函数将毫无意义。假设有下面的继承关系:
在这个类的设计中,子类只覆盖父类的一个函数:f()。那么,对于子类的虚函数表会是一个什么样子的呢?
从表中可以看出一下几点:
1) 覆盖的f()函数被放到了原来父类函数的位置
2) 没有被覆盖的函数依旧
这样,我们就可以看到对于下面这样的程序:
Base *b = new Derived();
b->fun();
由b所指的内存中,虚函数表的f()的位置已经被Derived::f()函数所取代,于是在实际调用关系发生时,是Derived::f()函数被调用了。
多重继承(无覆盖)
下面,再来看看多重继承的情况,假设有如下的继承关系:
对于其子类的虚函数表如下所示:
我们可以看出:
1) 每个父类都有自己的虚函数表
2) 子类的成员被放到第一个父类的虚函数表中
多重继承(有覆盖)
下面在来看看,多重继承有函数覆盖的情况:
对于子类的虚函数表情况如下:
我们可以看到,三个父类函数表中的f()的位置都被替换成了子类的f()函数指针,这样,任一静态类型的父类来指向子类,并调用f(),都是调用的子类的f()。