1. 程式人生 > >深入理解C++虛擬函式底層機制和RTTI執行時型別識別

深入理解C++虛擬函式底層機制和RTTI執行時型別識別

    當呼叫一個虛擬函式時,被執行的程式碼必須與呼叫函式物件的動態型別相一致:指向物件的指標或引用的型別是不重要的,編譯器是如何高效地提供這種行為呢?大多數編譯器是使用virtual table和virtual table pointers(vtbl和vptr)。

    一個vtbl通常是一個函式指標陣列,在程式中每個類只要聲明瞭虛擬函式,它就有自己的vtbl,並且類中的vtbl的內容是指向所有該類虛擬函式實現體的指標,例如:

class Test1{
    virtual void A();
    virtual void B();
    virtual void C();
    virtual void D();
};


    Test的虛擬函式表會是這樣的:

如果有一個Test2繼承了Test1:

class Test2:public Test1{
    virtual void C();
    virtual void D();//重定義C和D
}


Test2的虛擬函式表會是這樣的:

這個結果表明:如果你有大量的類或者在每個類中有大量的虛擬函式,你會發現vtbl會yonkers大量的地址空間。

問題:每個類都只需要一個vtbl拷貝,編譯器該把它放在哪裡?

    通常採用啟發式演算法:要在一個目標檔案中生成一個雷的虛擬函式表,要求改目標檔案中包含該類的第一個非內聯,非純虛的函式定義。

    同事虛擬函式表只實現了虛擬函式的一半機制,如果只有這些事沒用的,需要一個指向虛擬函式表的指標來建立類和表的聯絡,每個生命了虛擬函式的物件都帶有它,它是一個看不見的資料成員,指向該類的虛擬函式表,這個看不見的資料成員被稱為vptr,被編譯器載入物件裡,位置只有編譯器知道,其記憶體佈局可能是這樣的:

    但是不同的編譯器放置它的位置不同,存在繼承的情況下,一個物件的vptr經常被資料成員所包圍,如果存在多繼承,將會更加複雜。

    這也就意味著,如果你的資料成員只有4個位元組,那麼額外的虛擬函式指標會使得成員資料大小擴大一倍。

    考慮這段程式碼:

void call(Test1 *ptr)
{
    ptr->A();
}


    通過指標ptr呼叫虛擬函式A,編譯器在這個呼叫過程中生成的程式碼會做如下的事情:

    1.通過物件的vptr找到雷的vtbl,這是一個很簡單的動作,因為編譯器知道在物件內哪能找到vptr(畢竟是由編譯器放置的)。這個代價只是一個偏移調整(找到虛擬函式指標)和一個指標的間接定址(得到vtbl)。

    2.找到對應vtbl內的指向被呼叫函式的指標,這也是很簡單的,因為編譯器為每個虛擬函式在vtbl內分配了一個唯一的索引,這個代價只是在vtbl陣列內的一個偏移。

    3.呼叫第二步找到的指標所指向的函式。

    上述的呼叫會變成這樣:

(*ptr->vptr[i])(ptr);//ptr被當做this指標傳遞給函式


    在實際執行中,虛擬函式所需要的代價和行內函數有關,實際上虛擬函式無法內聯。因為內聯是“編譯期間用被呼叫的函式體本身來代替函式呼叫的指令”,但是虛擬函式是“知道執行時才能知道呼叫了哪一個函式”。

  在多繼承中,問題會更復雜,如果沒有virtual base class,子類會包含多個虛表以及指向虛表的指標,然而,一半會引入虛基類(為了防止鑽石繼承),按照《深入探索C++物件模型》中的說法,這樣會引入兩個問題:

    1.每一個物件必須針對其每一個virtual base class揹負一個額外的指標,但我們卻希望class object有固定的負擔,不因為其virtual base class的數目而有變化。

    2.由於虛繼承串鏈的加長,導致間接存取層次增加。比如:有三層虛繼承,那需要三級間接存取(經由三個virtual base class指標),但我們希望有固定的存取時間。

    第一個問題一般有兩種解決方法:

    Microsoft編譯器引入所謂的virtual base class table,每一個class object,如果有一個或者多個virtual base class,就會由編譯器安插一個指標,指向virtual base class table,至於自己的virtual base class指標則會放在該表格中,比如:

    第二個解決辦法就是在虛擬函式表中放置虛基類的偏移量:

    RTTI:執行時型別識別

    RTTI能讓我們在執行時找到物件和類的有關資訊,所以肯定有某個地方儲存了這些資訊讓我們查詢,這些資訊被存放在type——info的物件中,而RTTI被設為基於虛擬函式表來實現

  因此Test1類的實現可能是這樣: