PHP交流群:45503780
主要参考的网址:http://blog.csdn.net/digu/article/details/1892581(个人删了很多)
第一部分:对虚函数的内部实现简要介绍
举个例子:说明虚指针的存在
class _virtual
{
public:
virtual void fun1() const{}
virtual int fun2() const { return a; }
private:
int a;
}
sizeof(_virtal)=8 ; 为什么会等于8?在32位系统上,int只占用4个字节。另外的4个字节就是这里将要说的---指向虚函数表的指针(PS: 有一个虚函数和两个虚函数的类的长度没有区别,其实它们的长度就是类的 长度加一个void指针的长度,它反映出,如果有一个或多个虚函数,编译器在这个结构中插入一个指针(VPTR)。这是因为 VPTR指向一个存放地址的表,只需要一个指针,因为所有虚函数地址都包含在这个表中)。
第二部分:虚函数表与类的对应关系
那我们来看看编译器是如何建立VPTR指向的虚函数表的。先看下面两个类:
class base
{
public:
void bfun(){}
virtual void vfun1(){}
virtual int vfun2(){}
private:
int a;
}
class derived : public base
{
public:
void dfun(){}
virtual void vfun1(){}
virtual int vfun3(){}
private:
int b;
}
两个类VPTR指向的虚函数表(VTABLE)分别如下:
base类
——————
VPTR——> |&base::vfun1 |
——————
|&base::vfun2 |
——————
derived类
———————
VPTR——> |&derived::vfun1 |
———————
|&base::vfun2 |
———————
|&derived::vfun3 |
———————
解释:每当创建一个包含有虚函数的类或从虚函数的类派生一个类时,编译器就为这个类创建一个VTABLE,如上图所示。在这个表中,编译器放置了在这个类 中或在它的基类中所有已声明为virtual的函数的地址。如果在这个派生类中没有对在基类中声明为virtual的函数进行重新定义,编译器就使用基类 的这个虚函数地址。(在derived的VTABLE中,vfun2的入口就是这种情况。)然后编译器在这个类中放置VPTR。当使用简单继承时,对于每 个对象只有一个VPTR指针。VPTR必须被初始化为指向相应的VTABLE,这在构造函数中发生。一旦VPTR被初始化为指向相应的VTABLE,对象就"知道"它自己是什么类型。但只有当虚函数被调用时这种自我认知才有用。
通过上面的分析,可以得出C++中虚函数的调用方法:首先,取得对象中的虚表指针;然后,通过虚表指针找到相应的虚表;最后,通过在虚表内的偏移量找到相应的函数来调用。
1、从包含虚函数的类派生一个类时,编译器就为该类创建一个VTABLE。其每一个表项是该类的虚函数地址。
2、在定义该派生类对象时,先调用其基类的构造函数,然后再初始化VPTR,最后再调用派生类的构造函数( 从二进制的视野来看,所谓基类子类是一个大结构体,其中this指针开头的四个字节存放虚函数表头指针。执行子类的构造函数的时候,首先调用基类构造函数,this指针作为参数,在基类构造函数中填入基类的vptr,然后回到子类的构造函数,填入子类的vptr,覆盖基类填入的vptr。如此以来完成vptr的初始化。 )
3、在实现动态绑定时,不能直接采用类对象,而一定要采用指针或者引用。因为采用类对象传值方式,有临时基类对象的产生,而采用指针,则是通过指针来访问外部的派生类对象的VPTR来达到访问派生类虚函数的结果。
4. VPTR 常常位于对象的开头,编译器能很容易地取到VPTR的值,从而确定VTABLE的位置。VPTR总指向VTABLE的开始地址,所有基类和它的子类的虚函 数地址(子类自己定义的虚函数除外)在VTABLE中存储的位置总是相同的
5. 虚函数是在程序运行时动态绑定函数地址的,也叫晚绑定(PS:早绑定指编译器在编译期间即知道对象的具体类型并确定此对象调用成员函数的确切地址)。