如果你看到这篇文章时,急着去吃饭或泡MM,请跳转到蓝色字段开始阅读。
C++中的虚函数的作用主要是实现了多态的机制。关于多态,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。这种技术可以让父类的指针有“多种形态”,这是一种泛型技术。所谓泛型技术,说白了就是试图使用不变的代码来实现可变的算法。比如:模板技术,RTTI技术,虚函数技术,要么是试图做到在编译时决议,要么试图做到运行时决议。
关于虚函数的使用方法,我在这里不做过多的阐述。大家可以看看相关的C++的书籍。在这篇文章中,我只想从虚函数的实现机制上面为大家 一个清晰的剖析。
可以参考《C++ Primer Plus》第五版的图。
对C++ 了解的人都应该知道虚函数(Virtual Function)是通过一张虚函数表(Virtual Table)来实现的。简称为V-Table。在这个表中,主是要一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证其容真实反应实际的函数。这样,在有虚函数的类的实例中这个表被分配在了这个实例的内存中,所以,当我们用父类的指针来操作一个子类的时候,这张虚函数表就显得由为重要了,它就像一个地图一样,指明了实际所应该调用的函数。
这里我们着重看一下这张虚函数表。C++的编译器应该是保证虚函数表的指针存在于对象实例中最前面的位置(这是为了保证取到虚函数表的有最高的性能——如果有多层继承或是多重继承的情况下)。 这意味着我们通过对象实例的地址得到这张虚函数表,然后就可以遍历其中函数指针,并调用相应的函数。
听我扯了那么多,我可以感觉出来你现在可能比以前更加晕头转向了。 没关系,下面就是实际的例子,相信聪明的你一看就明白了。
假设我们有这样的一个类:
1 class Base { 2 public: 3 virtual void f() { cout << "Base::f" << endl; } 4 virtual void g() { cout << "Base::g" << endl; } 5 virtual void h() { cout << "Base::h" << endl; } 6 7 };
按照上面的说法,我们可以通过Base的实例来得到虚函数表。 下面是实际例程:
1 typedef void(*Fun)(void); 2 3 Base b; 4 5 Fun pFun = NULL; 6 7 cout << "虚函数表地址:" << (int*)(&b) << endl; 8 cout << "虚函数表 — 第一个函数地址:" << (int*)*(int*)(&b) << endl; 9 10 // Invoke the first virtual function 11 pFun = (Fun)*((int*)*(int*)(&b)); 12 pFun();
实际运行经果如下:
虚函数表地址:0012FED4
虚函数表 — 第一个函数地址:0044F148
Base::f
通过这个示例,我们可以看到,我们可以通过强行把&b转成int *,取得虚函数表的地址,然后,再次取址就可以得到第一个虚函数的地址了,也就是Base::f(),这在上面的程序中得到了验证(把int*强制转成了函数指针)。通过这个示例,我们就可以知道如果要调用Base::g()和Base::h(),其代码如下:
1 (Fun)*((int*)*(int*)(&b)+0); // Base::f() 2 (Fun)*((int*)*(int*)(&b)+1); // Base::g() 3 (Fun)*((int*)*(int*)(&b)+2); // Base::h()
这个时候你应该懂了吧。什么?还是有点晕。也是,这样的代码看着太乱了。没问题,让我画个图解释一下。如下所示:
注意:在上面这个图中,我在虚函数表的最后多加了一个结点,这是虚函数表的结束结点,就像字符串的结束符“/0”一样,其标志了虚函数表的结束。这个结束标志的值在不同的编译器下是不同的。在WinXP+VS2003下,这个值是NULL。而在Ubuntu 7.10 + Linux 2.6.22 + GCC 4.1.3下,这个值是如果1,表示还有下一个虚函数表,如果值是0,表示是最后一个虚函数表。
下面我们打开编译器,输入一段代码
1 #include <iostream> 2 using namespace std; 3 4 typedef void(*Fun)(void); 5 6 class Scientist 7 { 8 private: 9 char name[40]; 10 char sex; 11 public: 12 Scientist(const char *n="none", const char se='n'); 13 void showname(); 14 virtual void show_all(); 15 virtual ~Scientist() {} 16 }; 17 class Physicist : public Scientist 18 { 19 private: 20 char field[40]; 21 public: 22 Physicist(const char *n="none", const char se='n', const char *f="none"); 23 void show_all(); 24 void show_field(); 25 }; 26 27 Scientist::Scientist(const char *n, const char se) 28 { 29 cout<<"Call Scientist constructor"<<endl; 30 strncpy(name, n, 40-1); 31 name[40-1]=' '; 32 sex = se; 33 } 34 void Scientist::showname() 35 { 36 cout<<"Call Scientist showname."<<endl; 37 cout<<"Scientist name: "<<name<<endl; 38 } 39 void Scientist::show_all() 40 { 41 cout<<"Call Scientist show_all."<<endl; 42 cout<<"Scientist name: "<<name<<endl; 43 cout<<"Scientist Sex: "<<sex<<endl; 44 } 45 Physicist::Physicist(const char *n, const char se, const char *f):Scientist(n,se) 46 { 47 cout<<"Call Physicist constructor"<<endl; 48 strncpy(field, f, 40-1); 49 field[40-1]=' '; 50 } 51 void Physicist::show_all() 52 { 53 cout<<"Call Physicist show_all."<<endl; 54 Scientist::show_all(); 55 cout<<"field: "<<field<<endl; 56 } 57 void Physicist::show_field() 58 { 59 cout<<"Call Physicist show_field."<<endl; 60 cout<<"Physicist field: "<<field<<endl; 61 } 62 int main() 63 { 64 Physicist adam("Adam Crusher", 'M', "nuclear structure"); 65 Scientist *psc = &adam; 66 psc->show_all(); 67 68 cout<<endl; 69 Fun pFun = NULL; 70 71 cout<<endl; 72 cout << "虚函数表地址:" << (int*)(&adam) << endl; 73 cout << "虚函数表 — 第一个函数地址:" << (int*)*(int*)*(int*)(&adam)<< endl; 74 cout << "虚函数表 — 第二个函数地址:" << (int*)*((int*)*(int*)(&adam)+1)<< endl; 75 pFun = (Fun)*((int*)*(int*)(&adam)); 76 pFun(); 77 return 0; 78 }
运行结果:
分析:
程序首先定义一个子类物理学家对象adma,然后再定义一个科学家类型指针psc指向adma,然后通过psc调用show_all()函数。大家都知道,如果show_all()为非虚函数,那么编译器将采用静态联编编译,即根据指针类型选择方法,本程序中将调用Scientist::show_all()。如果show_all()函数使用了virtual,程序采用动态联编,程序根据指针指向的对象的类型来选择方法,此例中调用Physicist::show_all()。根据运行结果,程序成功调用了hysicist::show_all()。
下面开始讨论adma对象中的虚函数表。给出我画的图:
adam是对象,和int double声明的变量一样,必然有地址,其地址为0x0012FF1C。那么由于上文说了虚函数表(指向函数指针的数组),虚函数表的指针存在于对象实例中最前面的位置。那么*该地址0x0012FF1C的值就应该是虚函数表的地址0x0046F0D0,把该地址当做一个数组名,那么*该地址,为第一个元素的值,即指向Physicist::show_all()函数的指针0x004012BC。
上面的对吗?其实我也不知道,看看调试结果,至少让我们眼睛直观的相信。
adma对象地址0x0012FF1C,_vfptr地址0x0046F0D0,Physicist::show_all()地址,Physicist::析构函数地址0x004011A4。
从内存中看0x0012FF1C的第一个元素确实为虚函数表的地址0x0046F0D0,然后下面是name数组的内容......
虚函数表中的内容,也如愿所长的为0x004012BC,0x004011A4。
继续分析程序,&adma为取adma对象的地址,把它转化为int*类型输出(不是必须的)。
按理说&adma为取adma对象的地址,那么再对&adma取*,应该是其第一个元素的值,但能这样写吗?*(&adma)很明显不行,编译器解释为*&adma,* &抵消。
蓝色字段上文用*(int*)(&adam)。但这里有个问题,上文代码中标明是“虚函数表 — 第一个函数地址”,其实这个应该是虚函数表的地址,详见上图。
(int*)*(int*)(&adam)为第一个函数Physicist::show_all的地址,那么*((int*)*(int*)(&adam))为Physicist::show_all函数本身,用函数指针pFun指向它。运行pFun,即相当于运行Physicist::show_all。
Call Physicist show_all. Call Scientist show_all. Scientist name: Scientist Sex:
咦?这里的值怎么都变成空的了,难道没有this指针了,编译器又不知道哪个对象了吗?
下面看修改的代码:
1 cout<<endl; 2 cout << "虚函数表地址:" << (int*)(&adam) << endl; 3 cout << "虚函数表 — 第一个函数地址:" << (int*)*(int*)*(int*)(&adam)<< endl; 4 cout << "虚函数表 — 第二个函数地址:" << (int*)*((int*)*(int*)(&adam)+1)<< endl; 5 pFun = (Fun)*((int*)*(int*)(&adam)); 6 pFun();
"虚函数表地址:" << (int*)(&adam) 不变。
*(int*)(&adam)为对象第一个元素的值,即虚函数表的地址。用(int*)*(int*)(&adam)输出0x0046F0D0。
*(int*)*(int*)(&adam)为虚函数表数组中第一个元素的值,用(int*)*(int*)*(int*)(&adam)输出。依次类推cout << "虚函数表 — 第二个函数地址:" << (int*)*((int*)*(int*)(&adam)+1)<< endl;
文章蓝色字段以上参考:http://blog.csdn.net/haoel/article/details/1948051/