前言
最近在研究关于多态特性的知识,众所周知,动态绑定(或称动态联编)(•́⌄•́๑)૭✧是动态多态的实现基础,而实现动态绑定的重要基础是虚函数,正是有了动态绑定机制,使得基类指针作为一个接口同时能调用派生类和基类中的函数,即所谓的一个接口,多种实现,本文不会全面讲述动态多态的实现细节,而是着重于分析动态绑定的底层机制,如有不妥之处,欢迎指正。
正文
测试环境:win7/64位,VS2015
首先,创建两个基类base,base1;而类dr继承这两个基类,演示一下动态绑定和静态绑定的情况:
// An highlighted block
#include<iostream>
using namespace std;
class base
{
public:
virtual void vpo() { cout << "vpo\n"; };
virtual void vpo1(int a) { cout << a<<endl; };
virtual ~base() { cout << "base dead\n"; };
int k;
char a;
};
class base1
{
public:
virtual void vpoo() { cout << "vpoo\n"; };
virtual void vpoo1(int a) { cout << a << endl; };
virtual ~base1() { cout << "base1 dead\n"; };
int ko;
char ao;
};
class dr :public base,public base1
{
public:
//void vpo() { cout << "dr\n"; };
void vpo1(int a) { cout << a <<" dr"<< endl; };
virtual void vpo2(int a) { cout << a << " dr" << endl; };
int m;
int n;
};
int main()
{
base ba;
dr d1;
ba.vpo();
ba.vpo1(0);
cout<<endl;
d1.vpo();
d1.vpo1(1);
d1.vpo2(2);
cout << endl;
//以上调用都是妥妥的静态绑定
base* p = new dr();
p->vpo();//vpo虽然是虚函数但是未被重写(注释掉了),这里实际上是调用base版本的vpo函数
p->vpo1(2);//vpo1被重写,这里调用派生类dr版本的vpo1函数
//像上面这样,调用的函数是虚函数,触发动态绑定
delete p;
return 0;
}
注意:在派生类中重写基类虚函数时,不需要加virtual也是虚函数。
下面先看一下基类的内存布局:
// An highlighted block
//base
1> class base size(16):
1> +---
1> 0 | {vfptr}
1> 8 | k
1> 12 | a
1> | <alignment member> (size=3)
1> +---
1>//以下是base的虚函数表
1> base::$vftable@:
1> | &base_meta
1> | 0
1> 0 | &base::vpo
1> 1 | &base::vpo1
1> 2 | &base::{dtor}
//base1
1> class base1 size(16):
1> +---
1> 0 | {vfptr}
1> 8 | ko
1> 12 | ao
1> | <alignment member> (size=3)
1> +---
1>//以下是base1的虚函数表
1> base1::$vftable@:
1> | &base1_meta
1> | 0
1> 0 | &base1::vpoo
1> 1 | &base1::vpoo1
1> 2 | &base1::{dtor}
显然,这两个类实例化必须要一个虚表指针(如果该类或基类中有虚函数的话),占8字节,int变量占4字节,char变量1字节,这里就是13字节,但是一个类有16字节,这里应该是字节对齐的问题,本机是8字节对齐,所以还要一个3字节的alignment member。
扯远了,简单叙述一下虚函数的调用机制:虚表指针指向类的所有对象共享的虚函数表,当需要调用虚函数时,编译器通过虚表指针vfptr指向虚函数表vftable,获取表中被调函数的地址值,实现间接调用虚函数。
注意,这里的两个类都定义了虚析构函数(virtual destructor),因此,在类的布局里可以看到,类的虚函数表中都保存了该析构函数的地址,即base::{dtor}、base1::{dtor}。
以下是派生类dr的内存布局:
// An highlighted block
//dr
1> class dr size(40):
1> +---
1> | +--- (base class base)
1> 0 | | {vfptr}
1> 8 | | k
1> 12 | | a
1> | | <alignment member> (size=3)
1> | +---
1> | +--- (base class base1)
1> 16 | | {vfptr}
1> 24 | | ko
1> 28 | | ao
1> | | <alignment member> (size=3)
1> | +---
1> 32 | m
1> 36 | n
1> +---
1>
1> dr::$vftable@base@:
1> | &dr_meta
1> | 0
1> 0 | &base::vpo
1> 1 | &dr::vpo1
1> 2 | &dr::{dtor}
1> 3 | &dr::vpo2
1>
1> dr::$vftable@base1@:
1> | -16
1> 0 | &base1::vpoo
1> 1 | &base1::vpoo1
1> 2 | &thunk: this-=16; goto dr::{dtor}
显然,派生类继承了各基类的虚函数表,内存布局按声明的顺序,先是base,再是base
1,继承基类的成员顺序也和基类的布局一致,继承基类的虚函数表中虚函数顺序也和基类虚函数表顺序一致,只是有的虚函数被重写后更新了地址。
总结
动态绑定就是如此,基类指针指向派生类对象时,若要调用一个在派生类中被重写的虚函数,首先是通过虚表指针找到派生类的虚函数表(继承于基类),而虚表的相关地址值已经被修改过了,比如在派生类中重写的虚函数地址替换掉基类的原虚函数地址,而且表中各函数地址顺序不变,因此,基类指针实际上调用的是派生类中重写的虚函数。所以,基类指针调用在派生对象的虚函数时,实际上是以指向对象的真实类型进行操作,而这是编译器运行时通过程序上下文才知道的,这就是动态绑定。
同理,base基类声明虚析构函数后,派生类dr的虚函数表中出现了dr的析构函数dr::{dtor}替换原来在这里的base析构函数base::{dtor},虽然这两个析构函数不一样,但是编译器处理后会覆盖基类的虚析构函数,这也就是为什么基类必须是虚析构函数:基类指针指向派生类对象时,考虑到最后delete释放空间,虽然使用的是基类的虚表指针(被派生类继承),但是实际上,该指针已经指向被修改过的虚函数表,由上所述,各函数地址顺序不变,那么,调用的就是派生类对象的析构函数,即实现动态绑定。