C++的多态

本文详细解析了C++中的多态特性,包括静态多态和动态多态的概念,并通过具体的代码示例展示了如何实现和理解多态行为。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

多态 指同一个实体同时具有多种形式,也就是接口的多种不同的实现方式。

简单的说:同一个操作作用于不同的对象会有执行不同的操作,从而产生不同的执行结果。用基类(父类)的指针指向派生类(子类)对象,在运行时,通过基类的指针调用实现派生类的方法。

多态是面向对象的一个重要特征,如果一个语言只支持类但是不支持多态,那就只能说是基于对象的语言,而不能称为是面向对象的语言。C++中的多态性体现在运行和编译两个方面,编译时多态是静态多态,在编译时就可以确定对象使用的形式。运行时的多态是动态多态,其具体引用的对象在运行时才能确定。

 下面我们来看一段代码:

#include <iostream>
using namespace std;

class Father
{
public:
	int m_fID;
	Father() { m_fID = 1; }
	void testFunc() { cout << "Father testFunc " << m_fID << endl; }
	virtual void testVFunc() { cout << "Father testVFunc " << m_fID << endl; }
};

class Child : public Father 
{
public :
	int m_cID;
	Child() { m_cID = 2; }
	void testFunc() { cout <<"Child testFunc " << m_fID <<" : "<< m_cID<< endl; }
	void testNFunc() { cout <<"Child testNFunc "<< m_fID <<" : "<< m_cID << endl; }
	virtual void testVFunc() { cout<<"Child testVFunc "<< m_fID <<" : "<< m_cID<< endl; }
};

int main()
{
	Father* pRealFather = new Father();
	Child* pFalseChild = (Child*)pRealFather;
	Father* pFalseFather = new Child();

	pFalseFather->testFunc();
	pFalseFather->testVFunc();
	pFalseChild->testFunc();
	pFalseChild->testVFunc();
	pFalseChild->testNFunc();

	system("PAUSE");
}

同样是调用testFunc和testVFunc,那么运行结果会是什么呢~ 

VS2017运行结果是:

Father testFunc 1
Child testVFunc 1 : 2
Child testFunc 1 : -33686019
Father testVFunc 1
Child testNFunc 1 : -33686019
请按任意键继续. . .

这里有静态多态也有动态多态,为了更好的解释静态和动态多态,需要转化为汇编代码来看一下。

	mov	eax, DWORD PTR tv93[ebp]
	mov	DWORD PTR $T3[ebp], eax
	mov	DWORD PTR __$EHRec$[ebp+8], -1
	mov	ecx, DWORD PTR $T3[ebp]
	mov	DWORD PTR _pFalseFather$[ebp], ecx

; 28   : 
; 29   : 	pFalseFather->testFunc();

	mov	ecx, DWORD PTR _pFalseFather$[ebp]
	call	?testFunc@Father@@QAEXXZ		; Father::testFunc

; 30   : 	pFalseFather->testVFunc();

	mov	eax, DWORD PTR _pFalseFather$[ebp]
	mov	edx, DWORD PTR [eax]
	mov	esi, esp
	mov	ecx, DWORD PTR _pFalseFather$[ebp]
	mov	eax, DWORD PTR [edx]
	call	eax
	cmp	esi, esp
	call	__RTC_CheckEsp

; 31   : 	pFalseChild->testFunc();

	mov	ecx, DWORD PTR _pFalseChild$[ebp]
	call	?testFunc@Child@@QAEXXZ			; Child::testFunc

; 32   : 	pFalseChild->testVFunc();

	mov	eax, DWORD PTR _pFalseChild$[ebp]
	mov	edx, DWORD PTR [eax]
	mov	esi, esp
	mov	ecx, DWORD PTR _pFalseChild$[ebp]
	mov	eax, DWORD PTR [edx]
	call	eax
	cmp	esi, esp
	call	__RTC_CheckEsp

; 33   : 	pFalseChild->testNFunc();

	mov	ecx, DWORD PTR _pFalseChild$[ebp]
	call	?testNFunc@Child@@QAEXXZ		; Child::testNFunc

; 34   : 
; 35   : 	system("PAUSE");

	mov	esi, esp
	push	OFFSET ??_C@_05DIAHPDGL@PAUSE?$AA@
	call	DWORD PTR __imp__system
	add	esp, 4
	cmp	esi, esp
	call	__RTC_CheckEsp

看第一次(pFalseFather->testFunc();)和第三次(pFalseChild->testFunc();)的调用,其调用的代码段已经在编译出来的汇编语言中非常明确了,在C++代码中都是通过某个对象指针调用testFunc()方法,执行的结果却是不同的:第一次:Father testFunc 1,第三次:Child testFunc 1 : -33686019。从汇编代码看原因很简单:第一次调用的代码段是?testFunc@Father@@QAEXXZ也就是Father::testFunc,而第三次调用的代码段是?testFunc@Child@@QAEXXZ也就是Child::testFunc。在编译完成的时候就能确定API用哪种实现,这就是编译期的多态(静态多态)。

再来看一下第二次(pFalseFather->testVFunc();)和第三次(pFalseChild->testVFunc();)的调用,编译完成后没有明确具体的执行的代码段,直到运行时拿到CPU寄存器里的指针了,才知道这个指针指向代码段。这就是运行期的多态(动态多态)。

然后我们来通过代码分析一下运行结果:

	Father* pFalseFather = new Child();

	pFalseFather->testFunc();
	pFalseFather->testVFunc();

pFalseFather是一个指向Child对象的指针,它的内存布局是:

而pFalseFather->testVFunc()调用了vptl指向的函数 ,上面已经提到了,pFalseFather是指向Child对象的指针,而Child对象实现了自己的testVFunc方法,在new一个Child对象时,编译器会将vptl指向它自己的testVFunc,所以会调用Child::testVFunc()。当调用pFalseFather->testFunc()代码时,这不是virtual函数,所以汇编代码中直接调用了Father::testFunc()实现。C++规则,如果不是virtual字段函数,调用它的程序将在编译时就直接调用到函数实现。m_fID在Father的构造函数中被初始化为1,m_cID在Child的构造函数中被初始化为2,所以大印结果为:Child testVFunc 1 : 2。

这里需要强调一点:想要在C++中取得多态的行为,被调用的对象必须是虚函数,而对象则必须是通过指针或者引用法操作。

 

下面再来看最后的三个调用:

	Father* pRealFather = new Father();
	Child* pFalseChild = (Child*)pRealFather;
	Father* pFalseFather = new Child();

	pFalseChild->testFunc();
	pFalseChild->testVFunc();
	pFalseChild->testNFunc();

pRealFather是一个指向Father对象的指针,它的内存空间是这样的:

pFalseChild是一个Child类型指针,但是实际上是指向一个Father对象。首先它调用的是testFunc函数,到底调用的是Father还是Child的实现呢?上面提到过,非virtual函数一律在编译期根据类型决定,所以它的调用的是Child::testFunc()。这里m_fID在Father的构造函数中被初始化为1,而m_cID已经越界了,所以m_cID的值无法预期,打印结果是:Child testFunc 1 : -33686019。

然后,它调用了testVFunc(),因为它实际上指向的是一个Father对象,所以它执行的是Father::testVFunc(),打印结果是:Father testVFunc 1。

最后,它调用了testNFunc,真实的Father对象对应的Father类中可没有这个函数,但是实际编译执行都没有问题,这是问什么呢?因为指针pFalseChild的类型是Childl类型,编译完成的汇编语言在这里直接调用了?testNFunc@Child@@QAEXXZ(Child::testNFunc),虽然m_cID依然也是越界了,但是不影响程序的执行。

参考文档:

https://blog.youkuaiyun.com/russell_tao/article/details/7167929

https://baike.baidu.com/item/%E5%A4%9A%E6%80%81/2282489?fr=aladdin

The C++ programming Language,Special Edition

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值