C++动态多态之动态绑定机制的细节分析

C++动态多态之动态绑定机制的细节分析

前言

最近在研究关于多态特性的知识,众所周知,动态绑定(或称动态联编)(•́⌄•́๑)૭✧是动态多态的实现基础,而实现动态绑定的重要基础是虚函数,正是有了动态绑定机制,使得基类指针作为一个接口同时能调用派生类和基类中的函数,即所谓的一个接口,多种实现,本文不会全面讲述动态多态的实现细节,而是着重于分析动态绑定的底层机制,如有不妥之处,欢迎指正。

正文

测试环境: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释放空间,虽然使用的是基类的虚表指针(被派生类继承),但是实际上,该指针已经指向被修改过的虚函数表,由上所述,各函数地址顺序不变,那么,调用的就是派生类对象的析构函数,即实现动态绑定。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值