1. 程式人生 > >動態多型的原理

動態多型的原理

多型的三個條件:1.繼承,2.虛擬函式重寫,3.父類指標或引用指向子類物件 什麼是多型?相同物件收到不同訊息或不同物件收到相同訊息時產生的不同動作。 首先是多型的分類,分為靜態多型和動態多型,也可以稱為早繫結和晚繫結,區分的時刻就是程式在編譯階段根據引數個數確定呼叫哪個函式,如果能夠確定就是早繫結如果不能確定就是晚繫結,如果要實現多型就必須要使用虛擬函式。
從編譯角度看: c++編譯器在編譯的時候,要確定每個物件呼叫的函式(非虛擬函式)的地址稱為早期繫結,當我們將子類的物件son的地址賦給父類變數時,c++編譯器進行了型別轉換,此時c++編譯器認為父類變數儲存的就是父類物件的地址,當在main函式中執行父類變數的方法時,呼叫的當然就是父類物件的函式。
從記憶體角度看: 我們構造子類的物件時,首先要呼叫父類的建構函式去構造父類的物件,然後才呼叫子類的建構函式完成自身部分的構造,從而拼接出一個完整的子類物件。當我們將子類物件轉換為父型別時,該物件就被認為是原物件整個記憶體模型的上半部分,那麼當利用型別轉換後的物件指標去呼叫它的方法時,當然也就是呼叫它所在的記憶體中的方法。   正如很多人那麼認為,父類變數指向的是子類的物件,如果希望輸出的是子類的方法,就要用到虛函數了。   前面輸出的結果是因為編譯器在編譯的時候,就已經確定了物件呼叫的函式的地址,要解決這個問題就要使用晚繫結,當編譯器使用晚繫結時候,就會在執行時再去確定物件的型別以及正確的呼叫函式,而要讓編譯器採用晚繫結,就要在基類中宣告函式時使用virtual關鍵字,這樣的函式我們就稱之為虛擬函式,一旦某個函式在基類中宣告為virtual,那麼在所有的派生類中該函式都是virtual,而不需要再顯式地宣告為virtual。 編譯器在編譯的時候,發現父類中有虛擬函式,此時編譯器會為每個包含虛擬函式的類建立一個虛表(即 vtable),該表是一個一維陣列,在這個陣列中存放每個虛擬函式的地址,編譯器另外還為每個物件提供了一個虛表指標(即vptr),這個指標指向了物件所屬類的虛表,在程式執行時,根據物件的型別去初始化vptr,從而讓vptr正確的指向了所屬類的虛表,從而在呼叫虛擬函式的時候,能夠找到正確的函式,由於父類引用實際指向的物件型別是子類,因此vptr指向的子類的虛表,當呼叫父類引用的方法時,根據虛表中的函式地址找到的就是子類的函式.   正是由於每個物件呼叫的虛擬函式都是通過虛表指標來索引的,也就決定了虛表指標的正確初始化是非常重要的,在虛表指標沒有正確初始化之前,我們不能夠去呼叫虛擬函式,那麼虛表指標是在什麼時候,或者什麼地方初始化呢?答案是在建構函式中進行虛表的建立和虛表指標的初始化,在構造子類物件時,要先呼叫父類的建構函式,此時編譯器只“看到了”父類,並不知道後面是否還有繼承者,它初始化父類物件的虛表指標,該虛表指標指向父類的虛表,當執行子類的建構函式時,子類物件的虛表指標被初始化,指向自身的虛表。 虛表可以繼承,如果子類沒有重寫虛擬函式,那麼子類虛表中仍然會有該函式的地址,只不過這個地址指向的是基類的虛擬函式實現,如果基類有3個虛擬函式,那麼基類的虛表中就有三項(虛擬函式地址),派生類也會虛表,至少有三項,如果重寫了相應的虛擬函式,那麼虛表中的地址就會改變,指向自身的虛擬函式實現,如果派生類有自己的虛擬函式,那麼虛表中就會新增該項。
所以關鍵在於知道這個類是基類的還是派生類 注意:通過虛擬函式表指標VPTR呼叫重寫函式是在程式執行時進行的,因此需要通過定址操作才能確定真正應該呼叫的函式,而普通成員函式是在編譯器時就確定了呼叫誰,因此虛擬函式的效率要低很多,所以不是虛擬函式越多越好。

C++也有編譯時多型的特性也就是靜態多型,一般是通過模板和函式過載實現的。不需要虛表,在編譯期通過型別就能通過方法簽名來確定呼叫哪個成員的方法叫CRTP靜態分發,往往只是為了少敲一堆程式碼和所謂的效能優化,解決不了執行期多型要解決的問題。 子類有對父類的虛擬函式的重寫。virtual關鍵字,告訴編譯器這個函式要支援多型,我們不要根據指標型別判斷如何呼叫方法,而是要根據指標所指向的實際物件型別來判斷如何呼叫。物件在建立的時,由編譯器對VPTR指標進行初始化,只有當物件的構造完全結束後VPTR的指向才最終確定,到底是父類物件的VPTR指向父類虛擬函式表還是子類物件的VPTR指向子類虛擬函式表。