虚函数是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;}
int a;
int b;
};
对于Base类,他的对象结构用个图画一下:
因为Base类中存在虚函数,首先知道函数并不会占用空间,这里是用一个指针指向Base类的虚函数表,这个指针的位置是在空间的起始位置(假设内存空间是从上开始分配的),其实这跟指针数组是一个道理:
这是一个存储char类型指针的数组,是用双指针char**来指向的,指向的就是这个指针数组的首地址。
类中指向虚函数表的指针同样也是一个双指针:
typedef void(*pfunc)(void);
int main() {
Base b;
cout << "b的首地址:" << &b << endl;
cout << "虚函数指针的地址:" << (int **)*(int*)&b << endl;
cout << "第一个虚函数的地址:" << *((int **)*(int*)&b + 0) << endl;
cout << "第二个虚函数的地址:" << *((int **)*(int*)&b + 1) << endl;
cout << "第三个虚函数的地址:" << *((int **)*(int*)&b + 2) << endl;
cout << "第四个虚函数的地址:" << *((int **)*(int*)&b + 3) << endl;
pfunc pf = (pfunc)*((int **)*(int*)&b + 0);
pf();
pf = (pfunc)*((int **)*(int*)&b + 1);
pf();
pf = (pfunc)*((int **)*(int*)&b + 2);
pf();
system("pause");
return 0;
}
由于双指针只占4个字节,只要对对象空间取前四字节的地址就可以了,因为(int*)&b将&b转成了int型,那么只会取四个字节,对其解引用后,取这四个字节的首地址,这是就是一个int**型的,就能得到虚函数指针的首地址,对这个首地址加个偏移量继续解引用便得到对应的虚函数首地址,结果是这样的:
可以看到对象b的首地址与虚函数指针的首地址以及第一个虚函数的地址都是不同的,当然虚函数表的最后是空的,其地址自然就是0,用一个函数指针指向各个虚函数的地址,很清楚地运行出了各个函数的输出。
一、对于没有override的父类虚函数的子类虚函数表
class Derive : public Base {
virtual void f1() { cout << "Base::f1()" << endl; }
virtual void g1() { cout << "Base::g1()" << endl; }
virtual void h1() { cout << "Base::h1()" << endl; }
};
typedef void(*pfunc)(void);
int main() {
Derive b;
cout << "b的首地址:" << &b << endl;
cout << "虚函数指针的地址:" << (int **)*(int*)&b << endl;
cout << "第一个虚函数的地址:" << *((int **)*(int*)&b + 0) << endl;
cout << "第二个虚函数的地址:" << *((int **)*(int*)&b + 1) << endl;
cout << "第三个虚函数的地址:" << *((int **)*(int*)&b + 2) << endl;
cout << "第四个虚函数的地址:" << *((int **)*(int*)&b + 3) << endl;
cout << "第五个虚函数的地址:" << *((int **)*(int*)&b + 4) << endl;
cout << "第六个虚函数的地址:" << *((int **)*(int*)&b + 5) << endl;
cout << "第七个虚函数的地址:" << *((int **)*(int*)&b + 6) << endl;
pfunc pf = (pfunc)*((int **)*(int*)&b + 0);
pf();
pf = (pfunc)*((int **)*(int*)&b + 1);
pf();
pf = (pfunc)*((int **)*(int*)&b + 2);
pf();
pf = (pfunc)*((int **)*(int*)&b + 3);
pf();
pf = (pfunc)*((int **)*(int*)&b + 4);
pf();
pf = (pfunc)*((int **)*(int*)&b + 5);
pf();
system("pause");
return 0;
}
这里让Derive类继承与Base类,但并不重写Base类中的虚函数,那么输出结果是这样的:
画个图表示一下:
虚函数表此时就变成这个样子,包括父类的虚函数和子类自己的虚函数,最后以一个NULL指针为结尾。
一、对于override的父类虚函数的子类虚函数表
class Derive : private Base {
virtual void f() { cout << "Derive::f()" << endl; }
virtual void g1() { cout << "Derive::g1()" << endl; }
virtual void h1() { cout << "Derive::h1()" << endl; }
};
typedef void(*pfunc)(void);
int main() {
Derive b;
cout << sizeof(b) << endl;
cout << "b的首地址:" << &b << endl;
cout << "虚函数指针的地址:" << (int **)*(int*)&b << endl;
cout << "第一个虚函数的地址:" << *((int **)*(int*)&b + 0) << endl;
cout << "第二个虚函数的地址:" << *((int **)*(int*)&b + 1) << endl;
cout << "第三个虚函数的地址:" << *((int **)*(int*)&b + 2) << endl;
cout << "第四个虚函数的地址:" << *((int **)*(int*)&b + 3) << endl;
cout << "第五个虚函数的地址:" << *((int **)*(int*)&b + 4) << endl;
cout << "第六个虚函数的地址:" << *((int **)*(int*)&b + 5) << endl;
pfunc pf = (pfunc)*((int **)*(int*)&b + 0);
pf();
pf = (pfunc)*((int **)*(int*)&b + 1);
pf();
pf = (pfunc)*((int **)*(int*)&b + 2);
pf();
pf = (pfunc)*((int **)*(int*)&b + 3);
pf();
pf = (pfunc)*((int **)*(int*)&b + 4);
pf();
system("pause");
return 0;
}
这里让子类只override了父类f()这个虚函数,输出结果是这样的:
可以看到这是父类的f()就被完全覆盖了,此时虚函数表是这样的:
此时子类重写的f()函数完全覆盖了原来父类f()函数的位置,而没有被重写的函数依旧。
三、虚函数的工作原理
虚函数之所以实现了动多态,就是因为他的工作原理:
Base *p = &b;
p->f();
对于这么一段代码,他的输出是:
那就来针对p->f()这句来说说是如何工作的:
1、首先根据:Base *p = &b。确定对象的类型是Derive类型的;
2、确定了类型就能知道虚函数表的位置,利用vptr+偏移量(虚函数在虚函数表的位置)取匹配虚函数的入口;
3、根据虚函数的入口调用虚函数。
从虚函数的工作原理就可以了看出为什么成为动多态了,因为只有在运行时才知道对象的类型,根据对象类型才能找到虚函数表的位置,其中虚函数在虚函数表中的位置是原本就知道的,关键就是这个虚函数表的位置是在运行时才知道的。
也正因为虚函数的工作原理导致了指向虚函数表的指针会占用空间,并且效率低,但这都远远比不上虚函数能实现多态这一优点,所以虚函数在实际应用中是十分广泛的。
注意:
在上面代码中Derive是public继承Base的,我试验了private继承,发现父类Base的虚函数仍然会出现在Derive类中的虚函数表中。细心的话会发现我写的Derive的成员都是private的,但我用函数指针,让main函数中也可以调用其中的private权限的成员仍然是可以的,这就牵扯到安全性问题,还没有答案,还在求问中,有回答再补上。