C++多態的實現

C++的魔力

在C++中,通過繼承,子類可以獲得父類的成員,通過多態,C++可以實現在運行期根據對象的實際類型調用正確的虛函數,C++還有C語言不能做到的重載行爲…C++的這種魔力是怎麼實現的呢?
實際上,C++是使用C語言作爲代碼生成語言的,就好像當寫完一個C++程序時,C++預處理器先將C++代碼轉化爲C語言代碼,然後再由C語言編譯器生成可執行文件一樣。當使用繼承時,子類獲得父類的成員並不是C++具有什麼神奇魔力,而是編譯器把父類中那些可以被繼承的成員複製——把代碼拷貝一份,放到子類聲明中——到了子類中,當通過對象調用成員函數時,這個調用操作會被轉化爲非成員函數,並添加指向調用該函數的對象的指針作爲額外參數,當使用重載函數時,重載函數名會被根據參數數目、類型、常量性、static與否幾個指標修改爲獨一無二的名稱,定義一個對象,預處理器會把調用相應類函數的構造函數寫到相應的位置,對象離開作用域時,析構函數也會被添加到正確的位置,這造成了構造函數與析構函數被自動調用一樣(是自動調用,只是實際上這些動作是由編譯器做的,並不是C++語言就具有這種神奇能力)……最後C++程序被改寫成了一個C語言程序,只是我們看不到這個C語言程序而已。

C++多態的正確使用形式

C++中的多態是指通過指向derived class object之base class指針或引用進行調用,以達到在運行期根據base class指針所指對象的實際子類型做出不同的操作。兩個重點,第一是代碼表面看起來是操作基類,運行時的操作由指針的所指對象的實際類型決定,如果是用derived class指針指向base class object就不是多態的正確用法;第二是指針或引用,不能使base class object “指向”derived class object,這樣做不但達不到多態的效果,還可能會因爲對象切割而得到錯誤。多態爲什麼必須使用base class指針指向derived class object,爲什麼必須使用指針或引用呢?先來解答第二個問題。

爲什麼是指針或引用

這是因爲指針或引用是一種存儲內存地址的數據類型,爲了存儲指針或引用,計算機內存中必須有一塊區域提供給存放指針或引用,通常這塊區域的大小是4byte(32位機上),這也就是“任何指針佔用的空間一樣”的意思,在這個4byte空間裏放置了一個值,這個值指向了內存中的另一個位置,當計算機要訪問一個指針指向的對象的時候,會根據指針變量裏存儲的值跳轉到這個位置,然後根據指針的類型,解釋這個位置後的一定空間內數據的意思。而每定義一個對象的時候,只能得到被定義的對象本身的地址。因此,如果使用base class object是不能“指向”derived class object的,object只能指向自身(變量名會被翻譯爲變量存儲的地址,計算機就是根據這個地址訪問變量的)。而使用指針或引用呢,根據指針或引用變量的值,就可以找到指向的對象,如果只是爲了找到這個對象而不做任何其他操作,那麼指向的對象是任何類型都無所謂,但是如果要讀取對象的實際數據,就需要知道怎麼解釋這個位置上的數據了。因此使用指向derived class object的base class指針可以找到derived class object。但是爲了調用derived class object中表現多態的函數,就需要另外一項設施了。是的,virtual table,就是它。

爲什麼是父類指針指向子類對象

virtual table是存放在object外的一張表,表裏面放置了指向虛函數的指針vptr,嗯…實際實現中還在表的第一行存放了一個指向關於類定義本身一些信息的指針,typeid函數就是根據這個指針運作的。而在對象裏只存儲了指向這張表的指針,如果在每個對象中都放置一張同樣的表豈不是很浪費空間?爲了進一步節約空間,在不需要virtual table的對象裏就不放置vptr,怎麼確定哪些類需要?很簡單,聲明瞭virtual函數的那個類和他的子類都需要(基類有一張自己的表,子類也有一張自己的表,兩個類裏的vptr不會指向同一張表)!有了virtual table後,裏面的函數指針怎麼放置呢,六個法則:1)表頭放置指向type_info的指針;2)第二項放置指向virtual析構函數的指針;3)其他虛函數按聲明的順序存放在接下來的表項裏;4)子類中與父類中相同的虛函數(重載版本也要對應,這裏說的相同不僅僅是函數名相同)所在的表項位置索引是一樣的;5)子類改寫了父類的虛函數,那麼就放置子類改寫後的函數指針,如果沒有改寫,就把父類中這個虛函數的指針拷貝過來,放在相應的位置上;6)子類新添加的虛函數按在子類中的添加順序放在接下來的位置,這使得子類和父類的虛函數表不一樣大,如果子類沒有添加新的虛函數,那麼子類與父類的虛函數表是一樣大的。
virtual table已經準備就緒,多態就能展現了,將一個base class指針指向某個對象,這個對象可能是一個base class object,可能是derived class A object,也可能是一個derived class B object,這些都無所謂,只需要從這個對象佔據的空間裏,找到vptr,再根據函數名找到它的索引號,由索引號就能找到想要調用的函數所在的地址,於是多態得以實現。這個查找過程是由編譯器執行的,編譯器知道每個類中虛函數的索引號,當寫下Base* pBase = new SomeClass,pBase->SomeVirtualFunc(),後一語句會在編譯器內部轉化爲pBase->vptr【index】()。這樣看來,程序並不是真正的運行期多態,需要的行爲還是在編譯期確定下來的,只是原來需要手工做的事情交由C++預處理器來做了。這就解釋了爲什麼要爲展現多態的基類添加一個virtual destructor,如果不是的話,虛析構函數就不會出現在virtual table中,當調用delete刪除一個base class指針時,總是調用base class的析構函數,不會調用子類的析構函數,這就可能造成內存泄露。
以上只是簡單繼承中多態的展現,當使用多重繼承時,子類如果從n個基類中繼承,那麼就會有n張virtual tables,每個基類導致子類添加一張virtual table,當使用base class A指針指向子類時,就會使用與base class A相關的那張virtual table,爲了做到這一點,預處理器有需要做出更多的工作,如初始化或刪除base class指針時,都要做出必要的指針調整步驟,以確保正確。如果使用的是虛擬繼承,那麼情況就更復雜了,這個我還沒弄懂。
如果是用derived class指針指向base class object的話,那麼就總是使用base class中的vptr,那麼行爲就總是屬於base class的。一般而言,總是子類表現出更特殊的行爲,也是子類們擁有更多的表現形式,而這正是多態的意思,如果以derived class指針指向base class object,那麼就是把多態丟棄了。另一個問題也得到了解釋,使用繼承所要承受的“額外”開銷:1)vptr們佔用的空間;2)編譯期更多的時間。但是這種額外並不絕對,想想爲了表現多態自己手工打造而花費的空間和時間吧。

結語

好幾次聽人說起只要編程思想好,用C語言也能寫出面向對象的程序來,是的,這句話不假,看完C++多態實現的方式,這句話的正確性得到進一步肯定。但是如果想用C語言寫面向對象的程序,還不如趁早改爲C++,那些重複的事情爲什麼不交給計算機做呢?省力而且更靠譜——這裏是指使用預處理器。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章