1、什麼是多態
多態性可以簡單概括為“一個介面,多種行為”。
也就是說,向不同的對象發送同一個訊息, 不同的對象在接收時會產生不同的行為(即方法)。也就是說,每個對象可以用自己的方式去響應共同的訊息。所謂訊息,就是調用函數,不同的行為就是指不同的實現,即執行不同的函數。這是一種泛型技術,即用相同的代碼實現不同的動作。這體現了物件導向編程的優越性。
多態分為兩種:
(1)編譯時間多態:主要通過函數的重載和模板來實現。
(2)運行時多態:主要通過虛函數來實現。
2、幾個相關概念
(1)覆蓋、重寫(override)
override指基類的某個成員函數為虛函數,衍生類別又定義一成員函數,除函數體的其餘部分都與基類的成員函數相同。注意,如果只是函數名相同,形參或傳回型別不同的話,就不能稱為override,而是hide。
(2)重載(overload)
指同一個範圍出生多個函數名相同,但是形參不同的函數。編譯器在編譯的時候,通過實參的個數和類型,選擇最終調用的函數。
(3)隱藏(hide)
分為兩種:
1)局部變數或者函數隱藏了全域變數或者函數
2)衍生類別擁有和基類同名的成員函數或成員變數。
產生的結果:使全域或基類的變數、函數不可見。
3、幾個簡單的例子
/****************************************************************************************************** * File:PolymorphismTest * Introduction:測試多態的一些特性。 * Author:CoderCong* Date:20141114 * LastModifiedDate:20160113 *******************************************************************************************************/ #include "stdafx.h" #include <iostream> using namespace std; class A { public: void foo() { printf("1\n"); } virtual void fun() { printf("2\n"); } }; class B : public A { public: void foo() //由於基類的foo函數並不是虛函數,所以是隱藏,而不是重寫 { printf("3\n"); } void fun() //重寫 { printf("4\n"); } }; int main(void) { A a; B b; A *p = &a; p->foo(); //輸出1。 p->fun(); //輸出2。 p = &b; p->foo(); //輸出1。因為p是基類指標,p->foo指向一個具有固定位移量的函數。也就是基類函數 p->fun(); //輸出4。多態。雖然p是基類指標,但實際上指向的是一個子類對象。p->fun指向的是一個虛函數。按照動態類型,調用子類函數 return 0; }
4、運行時多態以及虛函數的內部實現
看了上邊幾個簡單的例子,我恍然大悟,原來這就是多態,這麼簡單,明白啦!
好,那我們再看一個例子:
class A { public: virtual void FunA() { cout << "FunA1" << endl; }; virtual void FunAA() { cout << "FunA2" << endl; } }; class B { public: virtual void FunB() { cout << "FunB" << endl; } }; class C :public A, public B { public: virtual void FunA() { cout << "FunA1C" << endl; }; }; int _tmain(int argc, _TCHAR* argv[]) { C objC; A *pA = &objC; B *pB = &objC; C *pC = &objC; printf("%d %d\n", &objC, objC); printf("%d %d\n", pA, *pA); printf("%d %d\n", pB, *pB); printf("%d %d\n", pC, *pC); return 0; }
運行結果:
5241376 1563032
5241376 1563032
5241380 1563256
5241376 1563032
細心的同志一定發現了pB出了問題,為什麼明明都是指向objC的指標,pB跟別人的值都不一樣呢?
是不是編譯器出了問題呢?
當然不是!我們先講結論:
(1)每一個含有虛函數的類,都會產生虛表(virtual table)。這個表,記錄了對象的動態類型,決定了執行此對象的虛成員函數的時候,真正執行的那一個成員函數。
(2)對於有多個基類的類對象,會有多個虛表,每一個基類對應一個虛表,同時,虛表的順序和繼承時的順序相同。
(3)在每一個類對象所佔用的記憶體中,虛指標位於最前邊,每個虛指標指向對應的虛表。
先從簡單的單個基類說起:
class A { public: virtual void FunA() { cout << "FunA1" << endl; } virtual void FunA2() { cout << "FunA2" << endl; } }; class C :public A { virtual void FunA() { cout << "FunA1C" << endl; }}; int _tmain(int argc, _TCHAR* argv[]) { A *pA = new A; C *pC = new C; typedef void (*Fun)(void); Fun fun= (Fun)*((int*)(*(int*)pA)); fun();//pA指向的第一個函數 fun = (Fun)*((int*)(*(int*)pA) +1); fun();//pA指向的第二個函數 fun = (Fun)*((int*)(*(int*)pC)); fun();//pC指向的第一個函數 fun = (Fun)*((int*)(*(int*)pC) + 1); fun();//pC指向的第二個函數 return 0; }
運行結果:
FunA1
FunA2
FunA1C
FunA2
是不是有點暈?沒關係。我一點一點解釋:pA對應一個A的對象,我們可以畫出這樣的一個表:
這就是對象*pA的虛表,兩個虛函數以聲明順序排列。pA指向對象*pA,則*(int*)pA指向此虛擬表,則(Fun)*((int*)(*(int*)pA))指向FunA,同理,(Fun)*((int*)(*(int*)pA) + 1)指向FunA2。所以,出現了前兩個結果。
根據後兩個結果, 我們可以推測*pC的虛表如下圖所示:
也就是說,由於C中的FunA重寫(override)了A中的FunA,虛擬表中虛擬函數的地址也被重寫了。
就是這樣,這就是多態實現的內部機制。
我們再回到最初的問題:為什麼*pB出了問題。
根據上邊的結論,我們大膽地進行猜測:由於C是由A、B派生而來,所以objC有兩個虛擬表,而由於表的順序,pA、pC都指向了對應於A的虛擬表,而pB則指向了對應於B的虛擬表。做個實驗來驗證我們的猜想是否正確:
我們不改變A、B、C類,將問題中的main改一下:
int _tmain(int argc, _TCHAR* argv[]) { C objC; A *pA = &objA; B *pB = &objC; C *pC = &objC; typedef void (*Fun)(void); Fun fun = (Fun)*((int*)(*(int*)pC)); fun();//第一個表第一個函數 fun = (Fun)*((int*)(*(int*)pC)+1); fun();//第一個表第二個函數 fun = (Fun)*((int*)(*((int*)pC+1))); fun();<span style="white-space:pre"> </span>//第二個表第一個函數 fun = (Fun)*((int*)(*(int*)pB)); fun();//pB指向的表的第一個函數 return 0; }
哈哈,和我們的猜測完全一致:
FunA1C
FunA2
FunB
FunB
我們可以畫出這樣的虛函數圖:
暫且這樣理解,編譯器執行B *pB = &objC時不是僅僅是賦值,而是做了相應的最佳化,將pB指向了第二張虛表。
說了這麼多,我是只是簡單地解釋了虛函數的實現原理,可究竟對象的內部的記憶體布局是怎樣的?類資料成員與多個虛表的具體記憶體布局又是怎樣的?編譯器是如何在賦值的時候作了最佳化的呢?我在以後的時間裡會講一下。
以上就是小編為大家帶來的C++中的多態與虛函數的內部實現方法全部內容了,希望大家多多支援雲棲社區~