写英文博客是一件不太容易的事情啊••今天总结一下虚函数的实现原理,用英文对我来说难度挺大的,所以就中文啦••O(∩_∩)O~,下面有些语言可能说的有点奇怪,那是因为我自己翻译了一下自己看到C++英文教程,所以可能在语言组织及其理解上有一定的偏差,欢迎大家指正。O(∩_∩)O谢谢
说到虚函数的实现方法,我们就不得不说到动态联编(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)
通过这些virtual table,编译器和程序就能够确定调用合适的虚函数,即使你仅仅使用了一个指针或者引用指向了基类。很方便吧~~还有一点,若子类定义了新的虚函数,则改函数的地址也将被添加到virtual table中。
但是,调用一个虚函数比调用一个非虚函数的速度要慢一些,原因:首先,我们必须使用*__vptr去获得合适的virtual table,然后通过这张virtual table的索引才可以找到正确的调用函数,只有这样我们才可以调用这个函数。使用虚函数在内存方面也有一定的成本,即每个对象都讲增大,增大量为存储地址的空间。
虚函数的实现机制,现在就说完啦••希望大家有所学习。
Any question do contact me, please~ I'm a caicai bird~~~(O(∩_∩)O~)
just.wuyun@gmail.com
参考资料:
(一个很不错的C++教程网站,很适合初学者,~\(≧▽≦)/~啦啦啦)
《C++ Primmer Plus》 第五版
《C++面向对象程序设计》 张德慧
阅读(11618) | 评论(3) | 转发(1) |