在c++中编译器是如何实现虚函数的,为什么虚函数能够做到动态绑定的呢?
在c++编译器中为了达到动态绑定的目的,编译器会通过一个表格,在执行期间“间接”调用实际上要绑定的函数,而这个表格就是虚函数表,
在虚函数表中的每个元素指向一个虚函数的地址。而在含有虚函数的类中编译器会自动的给它添加一个成员变量vptr这个变量用来指向这个虚函数
表。举个例子如下:
在此例子中的class1类的所有继承类都会有一个vptr的成员变量并且会继承虚函数表,当我们要调用一个虚函数的时候都是先通过vptr找到虚函数表再在虚函数表中找到虚函数的地址然后再调用这个函数。在虚函数表中的虚函数地址都是按照虚函数的申明次序一一填入虚函数表的,因此当我们在派生类中重写虚函数的时候派生类中继承下来的虚函数表中的对应虚函数指针就不再是父类的虚函数地址了而会被派生类的这个虚函数的地址取代。例如下图所示:
在上图中我们可以看到当派生类中对虚函数vfunc2重写之后虚函数表中的虚函数指向地址变成了派生类这个虚函数的地址值了。这样当我们在类中使用虚函数的时候随着你在派生类中重定义虚函数然后将虚函数表中的函数地址覆盖之后派生类所调用的虚函数就派生类总的虚函数了不再是父函数中的了。
这篇文章讲的更好。
http://www.cnblogs.com/winston/archive/2008/06/30/1232542.html
多态是面向对象的基本特征之一。而虚函数是实现多态的方法。那么virtual function到底如何实现多态的呢?
1 基类的内存分布情况
请看下面的sample
class A
{
void g(){.....}
};
则sizeof(A)=1;
如果改为如下:
class A
{
public:
virtual void f()
{
......
}
void g(){.....}
}
则sizeof(A)=4! 这是因为在类A中存在virtual function,为了实现多态,每个含有virtual function的类中都隐式包含着一个静态虚指针vfptr指向该类的静态虚表vtable, vtable中的表项指向类中的每个virtual function的入口地址
例如 我们declare 一个A类型的object :
A c;
A d;
则编译后其内存分布如下:
从 vfptr所指向的vtable可以看出,每个virtual function都占有一个entry,例如本例中的f函数。而g函数因为不是virtual类型,故不在vtable的表项之内。说明:vtab属于类 成员静态pointer,而vfptr属于对象pointer
2 继承类的内存分布状况
假设代码如下:
public B:public A
{
public :
int f() //override virtual function
{
return 3;
}
};
则
A c;
A d;
B e;
编译后,其内存分布如下:
从中我们可以看出,B类型的对象e有一个vfptr指向vtable address:0x00400030 ,而A类型的对象c和d共同指向类的vtable address:0x00400050a
3 动态绑定过程的实现
我们说多态是在程序进行动态绑定得以实现的,而不是编译时就确定对象的调用方法的静态绑定。
其过程如下:
程序运行到动态绑定时,通过基类的指针所指向的对象类型,通过vfptr找到其所指向的vtable,然后调用其相应的方法,即可实现多态。
例如:
A c;
B e;
A *pc=&e; //设置breakpoint,运行到此处
pc=&c;
此时内存中各指针状况如下:
可以看出,此时pc指向类B的虚表地址,从而调用对象e的方法。
继续运行,当运行至pc=&c时候,此时pc的vptr值为0x00420050,即指向类A的vtable地址,从而调用c的方法。
这就是动态绑定!(dynamic binding)或者叫做迟后联编(lazy compile)。
为了更加透析多态的原理,我们可以debug 程序在runtime时候的对象内存分布情况。
以下面这段简单的程序为例
//
#include " stdafx.h "
class Base
{
public :
int m_data;
static int m_staticvalue;
Base( int data)
{
m_data = data;
}
virtual void DoWork()
{
}
};
class AnotherBase
{
public :
virtual void AnotherWork()
{}
};
class DerivedClass: public Base, public AnotherBase
{
public :
DerivedClass( int t_data):Base(t_data)
{}
virtual void DoWork()
{
}
virtual void AnotherWork()
{
}
};
int Base::m_staticvalue = 1 ;
int main( int argc, char * argv[])
{
DerivedClass b( 1 );
b.DoWork();
return 0 ;
}
当程序运行后我们设置很简单的breakpoint: bp simplestack!derivedclass::dowork. 断点命中后的call stack如下:
0:000> kb
ChildEBP RetAddr Args to Child
0012ff20 0040102a 00daf6f2 00daf770 7ffd7000 SimpleStack!DerivedClass::DoWork
0012ff80 004012f9 00000001 00420e80 00420dc0 SimpleStack!main+0x2a
0012ffc0 7c817077 00daf6f2 00daf770 7ffd7000 SimpleStack!mainCRTStartup+0xe9
0012fff0 00000000 00401210 00000000 78746341 kernel32!BaseProcessStart+0x23
这时,我们可以看看DerivedClass对象的内存内分布情况:
0:000> dt SimpleStack!DerivedClass 0012ff74
+0x000 __VFN_table : 0x0040c020 //指向虚表的指针1
+0x004 m_data : 1
=0040d030 Base::m_staticvalue : 1 //(类成员)
+0x008 __VFN_table : 0x0040c01c //指向虚表的指针2
可以看到,DerivedClass对象中包含两个指向虚表的指针,地址分别为0x0040c020 和0x0040c01c 。一个为指向override了BaseClass的方法的虚表,一个指向orverride了AnotherBase方法的虚表。
可以查看对应虚表中的方法:
0:000> dds 0x0040c01c
0040c01c 00401140 SimpleStack!DerivedClass::AnotherWork
0040c020 00401110 SimpleStack!DerivedClass::DoWork
0040c024 004010e0 SimpleStack!Base::DoWork
0040c028 004011a0 SimpleStack!AnotherBase::AnotherWork
......
通过以上分析,应该可以透析多态的本质了。