閱讀548 返回首頁    go 阿裏雲 go 技術社區[雲棲]


C++ 多態分析

貌似公司麵試都喜歡問多態,今天做個總結記錄。

1.什麼是多態

多態就是Polymorphism,一個接口的多種實現。在不同的上下問下,接口的實現表現出不同的特征。

2.多態的好處

多態帶來兩個明顯的好處:一是不用記大量的函數名了,二是它會依據調用時的上下文來確定實現。確定實現的過程由C++本身完成另外還有一個不明顯但卻很重要的好處是:帶來了麵向對象的編程。
3.多態的實現

函數重載,宏多態,模板函數,虛函數。

3.1函數重載(function overloading)

不同的參數列表,不同的返回類型,實現同名調用不同實現的靜態多態。

3.2宏多態

帶變量的宏可以實現簡單的多態。比如下麵的ADD宏,傳入數字表示相加,傳入字符串表示連接。
#define ADD(A, B) (A) + (B);

3.3模板函數

模板函數可以處理不同的數據類型,通過傳入不同類型來實現不同效果。

3.4虛函數實現

這就是通常所說的動態多態。實現機製在於繼承和虛函數。常用方法是創建一個父類的指針來指向不同的子類對象,調用虛函數時,會先找到子類虛表,通過子類虛表調用對應函數,實現運行時多態。

4.靜態多態和動態多態

靜態多態又叫編譯時多態,動態多態又叫運行時多態。編譯時的多態性為我們提供了運行速度快的特點,而運行時的多態性則帶來了高度靈活和抽象的特點。

David Vandevoorde和Nicolai M. Josuttis在他們的著作C++ Templates: The Complete Guide一書中係統地闡述了靜態多態和動態多態技術。因為認為“和其他語言機製關係不大”,這本書沒有提及“宏多態”(以及“函數多態”)。(需要說明的是,筆者本人是這本書的繁體中文版譯者之一,本文正是基於這本書的第14章The Polymorphic Power of Templates編寫而成)

動態多態隻需要一個多態函數,生成的可執行代碼尺寸較小,靜態多態必須針對不同的類型產生不同的模板實體,尺寸會大一些,但生成的代碼會更快,因為無需通過指針進行間接操作。靜態多態比動態多態更加類型安全,因為全部綁定都被檢查於編譯期。正如前麵例子所示,你不可將一個錯誤的類型的對象插入到從一個模板實例化而來的容器之中。此外,正如你已經看到的那樣,動態多態可以優雅地處理異質對象集合,而靜態多態可以用來實現安全、高效的同質對象集合操作。

靜態多態為C++帶來了泛型編程(generic programming)的概念。泛型編程可以認為是“組件功能基於框架整體而設計”的模板編程。STL就是泛型編程的一個典範。STL是一個框架,它提供了大量的算法、容器和迭代器,全部以模板技術實現。從理論上講,STL的功能當然可以使用動態多態來實現,不過這樣一來其性能必將大打折扣。 

靜態多態還為C++社群帶來了泛型模式(generic patterns)的概念。理論上,每一個需要通過虛函數和類繼承而支持的設計模式都可以利用基於模板的靜態多態技術(甚至可以結合使用動態多態和靜態多態兩種技術)而實現。正如你看到的那樣,Andrei Alexandrescu的天才作品Modern C++ Design: Generic Programming and Design Patterns Applied(Addison-Wesley)和Loki程序庫已經走在了我們的前麵。

5.虛函數和虛表

5.1虛函數

對於虛函數調用來說,每一個對象內部都有一個虛表指針,該虛表指針被初始化為本類的虛表。所以在程序中,不管你的對象類型如何轉換,但該對象內部的虛表指針是固定的,所以呢,才能實現動態的對象函數調用,這就是C++多態性實現的原理。
如果基類有虛函數,那麼:
     1、每一個派生類都有虛表。
     2、虛表可以繼承,如果子類沒有重寫虛函數,那麼子類虛表中仍然會有該函數的地址,隻不過這個地址指向的是基類的虛函數實現。如果基類3個虛函數,那麼基類的虛表中就有三項(虛函數地址),派生類也會有虛表,至少有三項,如果重寫了相應的虛函數,那麼虛表中的地址就會改變,指向自身的虛函數實現。如果派生類有自己的虛函數,那麼虛表中就會添加該項。
     3、派生類的虛表中虛函數地址的排列順序和基類的虛表中虛函數地址排列順序相同。

5.2純虛函數

純虛函數不實現函數,讓子類繼承實現(必須實現)。定義方式如下:
virtual void fun()=0;

包含純虛函數的類是抽象類,不能生成對象。

5.3虛表

每個含有虛函數的類有一張虛函數表(vtbl),表中每一項指向一個虛函數的地址,實現上是一個函數指針的數組。
虛函數表既有繼承性又有多態性。每個派生類的vtbl繼承了它各個基類的vtbl,如果基類vtbl中包含某一項,則其派生類的vtbl中也將包含同樣的一項,但是兩項的值可能不同。如果派生類重載(override)了該項對應的虛函數,則派生類vtbl的該項指向重載後的虛函數,沒有重載的話,則沿用基類的值。
在類對象的內存布局中,首先是該類的vtbl指針,然後才是對象數據。在通過對象指針調用一個虛函數時,編譯器生成的代碼將先獲取對象類的vtbl指針,然後調用vtbl中對應的項。對於通過對象指針調用的情況,在編譯期間無法確定指針指向的是基類對象還是派生類對象,或者是哪個派生類的對象。但是在運行期間執行到調用語句時,這一點已經確定,編譯後的調用代碼能夠根據具體對象獲取正確的vtbl,調用正確的虛函數,從而實現多態性。分析一下這裏的思想所在,問題的實質是這樣,對於發出虛函數調用的這個對象指針,在編譯期間缺乏更多的信息,而在運行期間具備足夠的信息,但那時已不再進行綁定了,怎麼在二者之間作一個過渡呢?把綁定所需的信息用一種通用的數據結構記錄下來,該數據結構可以同對象指針相聯係,在編譯時隻需要使用這個數據結構進行抽象的綁定,而在運行期間將會得到真正的綁定。這個數據結構就是vtbl。可以看到,實現用戶所需的抽象和多態需要進行後綁定,而編譯器又是通過抽象和多態而實現後綁定的。

下麵說一下多重繼承。多重繼承的兩個基類如果繼承了同一個類,則其派生類相當於繼承了該類兩次,vtbl也繼承了兩次。對象布局中,該類的數據有兩份,vtbl指針有兩個,分別指向兩次被繼承的vtbl。但派生類重載該類的虛函數時隻能重載一次,那麼重載後的函數地址將占據vtbl的哪個位置?通過寫程序測試,我覺得應該是同時出現在所繼承的兩個vtbl的相應位置,有待進一步驗證。

說到虛函數機製,對象指針的類型轉換也是要弄清的,這裏就不說了。還有一個this指針的問題,提一下。虛函數調用的時候也是需要傳遞this指針的,這沒什麼奇怪,但是這時的this指針就隱含著一個問題,它要和實際調用的虛函數相一致,即this指針也要實現多態性。



最後更新:2017-04-03 12:56:43

  上一篇:go 機房收費係統之DataGridView
  下一篇:go 九度題目1209:最小郵票數