多态 指同一个实体同时具有多种形式,也就是接口的多种不同的实现方式。
简单的说:同一个操作作用于不同的对象会有执行不同的操作,从而产生不同的执行结果。用基类(父类)的指针指向派生类(子类)对象,在运行时,通过基类的指针调用实现派生类的方法。
多态是面向对象的一个重要特征,如果一个语言只支持类但是不支持多态,那就只能说是基于对象的语言,而不能称为是面向对象的语言。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