C++ 虚函数概述

本文详细介绍了C++中的虚函数概念,探讨了虚函数如何实现多态,并通过实例展示了虚函数表的工作原理。文章指出,虚函数是C++多态的基础,编译器为含有虚函数的类生成虚函数表,并在对象构造时插入虚函数表指针。通过基类指针调用虚函数时,系统会根据实际对象类型动态绑定到相应的函数实现,实现多态性。

虚函数是C++里面的一个非常重要的概念,虚函数是实现C++多态的基石。

C++类里面的成员函数默认不是虚函数,如果要把它变成虚函数,只需要在函数前面加上virtual。(JAVA成员函数默认就是虚函数)

几乎所有C++程序员都会使用虚函数。那么虚函数到底是怎么实现的呢?别急,我们先来看看几个简单的问题。

(本文里面所有代码的运行环境是:windows 7 32bit操作系统)

 

几个简单的问题

class CBase{};
class CBase2
{
public:
	void Test()
	{
		std::cout << "CBase2::Test()\n";
	}
};
class CBase3
{
public:
	virtual void Test()
	{
		std::cout << "CBase3::Test()\n";
	}
};


int _tmain(int argc, _TCHAR* argv[])
{
	int size = sizeof(CBase);
	std::cout << "Size of CBase: " << size << "\n";

	size = sizeof(CBase2);
	std::cout << "Size of CBase2: " << size << "\n";

	size = sizeof(CBase3);
	std::cout << "Size of CBase3: " << size << "\n";
	
	return 0;
}

上面代码里面的三个类的大小分别是多少呢?答案是1,1,4。

CBase里面明明什么都没有,为什么它的大小是1呢?CBase确实是个空类,但是空类也是可以被实例化的,也就是说编译器需要给它分配空间,那么0size的东西怎么分配空间啊?这个地方不同的编译器可能会有所不同,我们这里使用的是Microsoft Visual Studio, VS编译器会给这个类插入一个字节。所以sizeof(CBase)的结果是1,(这个字节里面的内容没有什么意义)。

CBase2里面没有任何数据成员,只有一个成员函数,那么为什么它的大小也是1呢?假如我们为CBase2类型创建一个对象,那么这个对象的存储空间里面包含什么呢?这个存储空间里面有CBase类型的数据成员,但是没有成员函数。其实,C++编译器在编译的时候会把类的成员函数的实现放到代码段。每个类的成员函数都只有一份拷贝,也就是说这个类的所有对象共享一个成员函数实现。当程序运行起来并且需要创建一个类的对象的时候,系统会给这个对象分配空间,然后把数据成员放到空间里面,但是不会把成员函数的实现放进去。所以说一个类的对象的存储空间里面只有数据成员。针对CBase2,因为没有任何数据成员,所以CBase2和CBase一样,sizeof(CBase2)也是1。

CBase3的大小为什么是4呢?原因就在于CBase3里面有个虚函数。C++编译器一旦看到某个类里面有虚函数,就会给这个类创建一个虚函数表,然后在这个类里面插入一个数据成员虚函数表指针_vfptr。_vfptr是编译器自动生成的,这是指向类的虚函数表的一个指针,32位程序的话就是4字节,64位程序的话就是8字节。从下面的截图我们可以清楚的看到CBase3的对象p2里面有一个数据成员就是_vfptr,而CBase2的对象p1里面什么都没有。

至此,我们可以得出一个结论,任何一个有虚函数的类,编译器都会为它生成一个虚函数表。并且在这个类的对象构造的时候自动插入一个虚函数表指针。

虚函数表

上面一节中,我们已经知道了拥有虚函数的类都有一个对应的虚函数表,而且每个对象都拥有一个指向这个虚函数表的指针,那么怎么访问这个指针呢?C++的规定是这样的,拥有虚函数表指针的对象的前面4个字节就是虚函数表指针。那么我们就可以这么得到这个指针:

CBase* base = new CBase();
	LPVOID* vtPtr = *(LPVOID**)base;

OK, 我们这里就可以得到一个虚函数表指针,完整代码:

class CBase
{
public:
	virtual void test()
	{
		std::cout <<"CBase::test() \n";
	}
};

class CDerived: public CBase
{
};


int _tmain(int argc, _TCHAR* argv[])
{
	CBase* base = new CBase();
	LPVOID* vtPtr = *(LPVOID**)base;


	CDerived* derived = new CDerived();
	LPVOID* vtPtr2 = *(LPVOID**)derived;
	
	return 0;
}

代码里面我们得到了2个虚函数表指针,一个是基类的,一个是派生类的。看运行结果:



我们可以看到vtPtr指向了CBase的虚函数表,vtPtr2指向了CDerived的虚函数表。那么再看看具体虚函数表里面是什么?先看看vtPtr

看前面的4个字节:85121800,我们知道windows下面地址在内存中是逆序存储的,那么这个地址就是0x00181285,在看看0x00181285对应的是什么?

就是CBase的成员函数CBase::test()。也就是说虚函数表里面存放了虚函数的地址列表。ok,再看看CBase的派生类的对象的虚函数表指针。vfPtr2的值和vfPtr不一样,这说明派生类自己也有一个虚函数表,看看里面的内容:

前面4个字节也保存了地址:0x00181285, 也指向CBase::test()函数。

至此,我们可以得出一个结论:基类和派生类都有自己的一个虚函数表。如果派生类不重写基类的虚函数,那么派生类的虚函数表里面存放的就是基类的虚函数地址。

 

虚函数如何实现多态

通过上面2节,我们已经对虚函数表有个基本了解了。下面我们就来讲一个,虚函数是如何实现C++多态的?我们先把代码改一下,CBase里面有2个虚函数,CDerived继承CBase,然后CDerived重写了其中一个虚函数。看下面的代码:

class CBase
{
public:
	virtual void test()
	{
		std::cout <<"CBase::test() \n";
	}
	virtual void test2()
	{
		std::cout <<"CBase::test2() \n";
	}
};

class CDerived: public CBase
{
public:
	virtual void test()
	{
		std::cout <<"CDerived::test() \n";
	}
};


int _tmain(int argc, _TCHAR* argv[])
{
	CBase* base = new CBase();
	LPVOID* vtPtr = *(LPVOID**)base;


	CDerived* derived = new CDerived();
	LPVOID* vtPtr2 = *(LPVOID**)derived;
	
	return 0;
}

CDerived重写了虚函数test(),而没有重写test2()。运行起来看看:


 CBase和CDerived对象各自指向各自的虚函数表。先看看CBase的虚函数表,

虚函数表里面第一个地址是0x013e1285,从上面的汇编可以看到指向CBase::test(),第二个地址是013e129e,指向另外一个虚函数CBase::test2()。再看看派生类对象的虚函数表:

跟上一个图片相比较,很清楚,第一个函数地址变了,现在是0x013e1299,基类虚函数表的第一个地址是0x013e1285。而第二个地址都是0x013e129e,都指向CBase::test2()。而CDerived的第一个地址现在指向了CDerived::test()。所以结论是:

我们可以在派生类中重写基类的虚函数,这样派生类的虚函数表里的原来指向基类虚函数的地址会被替换成指向派生类虚函数的地址。而对于没有重写的那些基类虚函数,派生类虚函数表也会存放它们的地址,这些地址跟基类的虚函数表里面存放的一样。都指向基类的虚函数。

 

现在,我们就可以来解释多态了。

	CBase* obj = new CDerived();
	obj->test();

在C++里面,使用多态需要有2个条件:

1. 虚函数

2. 通过基类的引用或者指针来调用虚函数。

像上面的例子,CBase的指针是在运行时动态绑定到一个CDerived对象的。当创建CDerived对象的时候,CDerived构造函数会初始化虚函数表指针_vfptr,CDerived的构造函数会将_vfptr指向CDerived的虚函数表。那么当我们调用obj->test()的时候,系统会在CDerived的虚函数表里面查找test函数,通过上面一节,我们知道CDerived的虚函数表的第一个地址是指向CDerived::test()的,而不是CBase的,所以,obj->test()调用的是实际绑定的对象的类型的函数,尽管obj本身是个基类指针。通过这个特性,我们可以拿基类指针当作参数在程序中传递,然后再通过虚函数,系统就会实际调用这个指针指向的实际对象的类型的虚函数。


 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值