C++虚函数与动态联编

虚调用的几种具体情形

虚调用是相对于实调用而言的,它的本质是动态联编(后面我们会讲到)。

实调用:在发生函数调用的时候,如果函数的地址是在编译阶段确定的,就是实调用。反之,函数的入口地址要在运行时通过查

询虚函数表的方式获得,就是虚调用。

虚调用不能简单理解为"对虚函数的调用", 因为对虚函数的调用很可能是实调用。

下面这个程序,对虚函数的调用就是实调用

#include <iostream>
using namespace std;

class A
{
public:
	virtual void show()
	{
		cout<<"in A::show()\n";
	}
};

class B:public A
{
public:
	void show()
	{
		cout<<"in B::show()\n";
	}
};

void func(A a)
{
	a.show();	//a是A的一个实例,并不是指向类A对象的指针或引用,所以为实调用。
}

int main()
{
	B b;
	func(b);	//调用类A的拷贝构造函数,产生一个A类对象作为a进入函数func()的函数体
			//在函数体内,a是一个纯粹的类A 对象,与类型B 毫无关系
	return 0;
}


在构造函数中调用虚函数,对虚函数的调用实际上是实调用(一般情况下,因避免在构造函数中调用虚函数)。这是虚函数被实调用的另一个例子。怎么理解呢?从概念上说,在一个对象的构造函数运行完毕之前,这个对象还没有完全诞生,所以在构造函数中调用虚函数,实际上都是实调用。请看下面的一个例子。

在构造函数中调用虚函数:

#include <iostream>
using namespace std;

class A
{
public:
	virtual void show()
	{
		cout<<"in A::show()\n";
	}
	A()
	{
		show();	//调用虚函数
	}
};

class B:public A
{
public:
	void show()
	{
		cout<<"in B::show()\n";
	}
	//B()
	//{
	//	show();	//
	//}
};

int main()
{
	A a;
	B b;
	return 0;
}


现在,我们来看一下虚函数到底是干什么用的?

设立虚函数的初衷,就是想在设计基类的时候,对该基类的派生类实施一定程度的控制。可以理解为“通过基类访问派生类成

员”。因此,虚调用最常用的形式是:通过指向基类对象的指针访问派生类对象的虚函数,或通过基类对象的引用调用派生类

对象的虚函数。虚调用是通过查询虚函数表来实现的,而拥有虚函数的对象都可以访问到所属类的虚函数表

派生类对象怎么访问到基类对象的虚函数?

通过指向派生类对象的指针或引用调用基类对象的虚函数,下面就是一个具体例子:

#include <iostream>
using namespace std;

class A
{
public:
	virtual void show()
	{
		cout<<"in A::show()\n";
	}
};

class B:public A
{
public:
	void show()
	{
		cout<<"in B::show()\n";
	}
};

int main()
{
	A a;
	//通过派生类对象的引用pb 实现了调用基类中虚函数show(),,
	//如果把 A中show() 前面的virtual去掉, 则调用的就是B 中的show()
	B &pb = static_cast<B&>(a);
	pb.show();	//调用的是基类 A的 show();
	return 0;
}


是不是实现虚调用一定要显式借助于指针或引用才能实现呢?

当然不是,请看下面的例子:

#include <iostream>
using namespace std;

class A
{
public:
	virtual void show()
	{
		cout<<"in A::show()\n";
	}
	void callfunc()
	{
		show();
	}
};

class B:public A
{
public:
	void show()
	{
		cout<<"in B::show()\n";
	}
};

int main()
{
	B b;
	b.callfunc();	//调用的是A::callfunc(),,但在A::callfunc()调用的是B::show()
			//这就是一个虚调用
	A a;
	a.callfunc();	//这里调用的是A::show()
	return 0;
}

 

虚函数可以是私有的吗?

虚函数一般被声明为公有的,这样实现虚函数的调用会比较方便。但C++并没有要求虚函数必须是公有的,将虚函数设置成私

有的和受保护并不妨碍虚函数之间的覆盖和虚函数的调用。

动态联编怎么实现?

动态联编:是指被调函数的入口地址是在运行时、而不是在编译时决定的。C++利用动态联编来完成虚函数的调用,C++标准

并没有规定如何实现动态联编,但大多数的C++编译器都是通过虚指针(vptr)和虚函数表(vtable)来实现动态联编的。

基本思路:

1.为每一个包含虚函数的类建立一个虚函数表,虚函数表的各个表项存放的是各虚函数在内存中的入口地址;

2.在该类的每一个对象中设置一个指向虚函数表的指针(这就是为什么含有虚函数的对象会多出4个字节的大小);

3.在调用虚函数的时候,先利用虚指针找到虚函数表,确定虚函数的入口地址在表中的位置,获取入口地址完成调用。

下面来详细了解一下虚指针和虚函数表:

(1) 虚指针(vptr)放在对象的哪个位置?

虚指针是作为对象的一部分存放在对象的空间中的,一个类只有一个虚函数表,因此该类的所有对象的虚指针都指向同一个地

方。在不同的编译器中,虚指针在对象中的位置是不同的,在Vistual C++中,虚指针位于对象的其实位置,在GUN C++中,

虚指针位于对象的尾部而不是头部。那么怎么确定虚指针到底存放在哪呢,看下面的程序:

#include <iostream>
using namespace std;

class HaveVirtual
{
	int i;
public:
	HaveVirtual()
	{
		i = 1;
	}
	virtual void show()
	{
		cout<<"you are hear\n";
	}
};

int main()
{
	HaveVirtual hv;
	unsigned long *p;
	p = reinterpret_cast<unsigned long*>(&hv);
	cout<<p[0]<<endl;
	cout<<p[1]<<endl;
	return 0;
}

通过观察p[0] 和 p[1]的值,就可以判断虚指针放在哪了。

(2)虚函数表的内部结构

 一个类只有一个虚函数表,所有的类都不会和其它的类共享同一张虚函数表。

怎么创建虚函数表呢?

1.确定当前类包含的虚函数的个数。一个类的虚函数有两个来源:一是继承自父类(可能在当前类中改写),其它的是在当前类

中新声明的虚函数;

2.为所有虚函数排序。继承自父类的所有虚函数,排在当前类新声明的虚函数之前,新声明的虚函数按照在当前类中声明的顺

序排列;

3.确定虚函数的入口地址。继承自父类的虚函数,如果在当前类中被改写,则虚函数的入口地址是改写之后的函数的地址,否

则保留父类中的虚函数的入口地址。新声明的虚函数的入口地址就是在当前类中的函数的入口地址。

(3)虚函数表放在哪里

虚函数表放在应用程序的常量区。虚函数的每一项代表了一个函数的入口地址,类型是Double Word

(4)通过访问虚函数表手动调用虚函数

既然知道了虚函数表的位置和结构,那么就可以通过访问虚函数表,手动调用虚函数。

下面是一个手动调用虚函数的例子:

#include <iostream>
using namespace std;

typedef void (*funptr)();	//定义一个函数指针funptr

void ExecuteVirtualFunc(void * pObj, int index)
{
	funptr p;
	unsigned long * pAddr;
	pAddr = reinterpret_cast<unsigned long*>(pObj);	//取得对象的虚指针
				//visual C++中虚指针放在对象的头部
	pAddr = (unsigned long *)*pAddr;	//通过虚指针得到虚函数表的首地址
	p = (funptr)pAddr[index];		//通过索引获得虚函数入口地址
	_asm
	{
		mov ecx, pObj	//将对象的首地址放入寄存器 ecx
	}
	p();	//调用函数
}

class Base
{
	int i;
public:
	Base()
	{
		i = 0;
	}
	virtual void f1()
	{
		cout<<"Base's f1()\n";
	}
	virtual void f2()
	{
		cout<<"Base's f2()\n";
	}
	virtual void f3()
	{
		cout<<"Base's f3()\n";
	}
};

class Derived:public Base
{
	int j;
public:
	Derived()
	{
		j = 2;
	}
	virtual void f4()
	{
		cout<<"Derived's f4()\n";
	}
	void f3()
	{
		cout<<"Derived's f3()\n";
	}
	void f1()
	{
		cout<<"Derived's f1()\n";
	}
};

int main()
{
	Base b;
	Derived d;
	ExecuteVirtualFunc(&b, 1);	//调用对象b 的第2个虚函数 f2()
	ExecuteVirtualFunc(&d, 3);	//调用对象d 的第4个虚函数 f4()
	return 0;
}


调用类的非静态成员函数是,必须同时给出对象的首地址,所以在程序中使用内联汇编代码_asm {  mov ecx, pObj ecx }来达

到这个目的。在Visual C++中,在调用类的非静态成员函数之前,对象的首地址都是送往寄存器 ecx 的。

 

  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值