标签:
说到虚函数的实现方法,我们就不得不说到动态联编(dynamic binding)和静态联编(static binding)。静态联编意味着编译器能够直接将标识符和存储的物理地址联系在一起。每一个函数都有一个唯一的物理地址,当编译器遇到一个函数调用时,它将用一个机械语言说明来替代函数调用,用来告诉CPU跳至这个函数的地址,然后对此函数进行操作。这个过程是在编译过程中完成的(注:调用的函数在编译时必须能够确定),所以静态联编也叫前期联编(early binding)。但是,如果使用哪个函数不能在编译时确定,则需要采用动态联编的方式,在程序运行时在调用函数,所以动态联编也叫后期联编(late binding)。
在C++继承多态中,如若要在派生类中重新定义基类的方法,则要把它声明为虚函数,并且用指针或者引用去调用它的方法,实现动态联编,否则编译器默认的将是静态联编。小看一下这个例子:
Example1:
#include <iostream> using namespace std;
class A { public: void f() { cout << "A" << endl; } //注意此处的函数不是虚函数
};
class B : public A { public: void f() { cout << "B" << endl;} }; int main (void) {
A a, *pa; B b;
a = b; //将子类对象赋给基类对象 a.f();
pa = &b; //用子类的对象的地址给基类指针初始化(符合赋值兼容规则)
pa->f();
return 0; }
|
运行结果:
原因:编译器默认为静态联编方式,所以函数f(),在编译过程中就已经定死了,在子类中尽管你重新定义了f()的方法,但是编译器不知道应该调用哪个函数,所以就只会用的静态联编时的函数方法。
Example 2:
#include <iostream> using namespace std;
class A { public: virtual void f() { cout << "A" << endl; } //注意此处声明了虚函数
};
class B : public A { public: void f() { cout << "B" << endl;} }; int main (void) { A a, *pa; B b;
a = b; //将子类对象赋给基类对象,这样做不能实现动态联编,虚函数特性失效 a.f(); b.f(); pa = &b; pa->f(); A &aa = b; //定义成引用类型也是可以的 aa.f ();
return 0; }
|
运行结果:
先总结一下两个程序,若基类不声明为虚函数,则在A a = b时,或者 A &a = b, A *a = &b,创建并初始化对象时,是根据引用或指针的类型来选择方法的。这也就是我们第一个程序运行的结果的具体解释(觉得自己上面说的静态联编有点抽象,不知道这样说明会不会更容易理解一些。。O(∩_∩)O~)。若使用了virtual声明为虚函数,则程序将根据引用或指针指向的对像的类型来选择方法。这就程序二运行结果的原因。
对比两个结果就能很清楚的看到虚函数的作用。但是它具体的实现原理是什么呢?
C++采用了动态联编的一种特殊形式去实现虚函数,称为虚函数表。虚函数表是一张函数查找表,用以解决以动态联编方式调用函数。它为每个可以被类对象调用的虚函数提供一个入口,这样当我们用基类的指针或者引用来操作子类的对象时,这张虚函数表就提供了编译器实际调用的函数。虚函数表其实是存储了为类对象进行声明的虚函数地址。当我们创建一个类对象时,编译器会自动的生成一个指针*__vptr(一个隐藏指针),该指针指向这个类中所有虚函数的地址表。(实际上,虚函数表就是一个函数地址数组表。),请注意,*__vptr和*this指针不同,*this是一个被编译器用作解决自引用的函数参数,而*__vptr则是一个真正的指针。
每一个类,不管是基类还是子类都有一个自己的virtual table,而*__vptr也是被继承过来的。
我们再看一个例子:
Example:
#include <iostream> using namespace std;
class A { public: virtual void f() { cout << "A’s f()" << endl; } //f()被声明为虚函数 virtual void g() { cout << "A’s g()"<< endl; } //g()被声明为虚函数
};
class B : public A { public: void f() { cout << "B’s f()" << endl; } };
class C : public A { public: void g() { cout << "C’s g()" << endl; } }; int main (void) { A *pa; B b; C c; pa = &b; pa -> f(); pa -> g(); pa = &c; pa -> f(); pa -> g();
return 0; }
|
运行结果:
B’s f() A’s g() A’s f() C’s g()
|
这个程序就能够反映出虚函数是怎样通过virtual table实现的,自己绘了一张图:应该能比较清楚的反映情况(借鉴于learnCpp.com)
1、c++实现多态的方法
其实很多人都知道,虚函数在c++中的实现机制就是用虚表和虚指针,但是具体是怎样的呢?从more effecive c++其中一篇文章里面可以知道:是每个类用了一个虚表,每个类的对象用了一个虚指针。具体的用法如下:
class A
{
public:
virtual void f();
virtual void g();
private:
int a
};
class B : public A
{
public:
void g();
private:
int b;
};
//A,B的实现省略
因为A有virtual void f(),和g(),所以编译器为A类准备了一个虚表vtableA,内容如下:
B因为继承了A,所以编译器也为B准备了一个虚表vtableB,内容如下:
注意:因为B::g是重写了的,所以B的虚表的g放的是B::g的入口地址,但是f是从上面的A继承下来的,所以f的地址是A::f的入口地址。
然后某处有语句 B bB;的时候,编译器分配空间时,除了A的int a,B的成员int b;以外,还分配了一个虚指针vptr,指向B的虚表vtableB,bB的布局如下:
vptr : 指向B的虚表vtableB |
int a: 继承A的成员 |
int b: B成员 |
当如下语句的时候:
A *pa = &bB;
pa的结构就是A的布局(就是说用pa只能访问的到bB对象的前两项,访问不到第三项int b)
那么pa->g()中,编译器知道的是,g是一个声明为virtual的成员函数,而且其入口地址放在表格(无论是vtalbeA表还是vtalbeB表)的第2项,那么编译器编译这条语句的时候就如是转换:call *(pa->vptr)[1](C语言的数组索引从0开始哈~)。
这一项放的是B::g()的入口地址,则就实现了多态。(注意bB的vptr指向的是B的虚表vtableB)
另外要注意的是,如上的实现并不是唯一的,C++标准只要求用这种机制实现多态,至于虚指针vptr到底放在一个对象布局的哪里,标准没有要求,每个编译器自己决定。我以上的结果是根据g++ 4.3.4经过反汇编分析出来的。
参考:http://blog.chinaunix.net/uid-24178783-id-370328.html
http://blog.csdn.net/jiangnanyouzi/article/details/3720807
虚函数实现机制
标签:
原文地址:http://www.cnblogs.com/wangxue0218/p/4673988.html