天天看點

C++虛函數原理(轉)

編譯器到底做了什麼實作的虛函數的晚綁定呢?我們來探個究竟。

      編譯器對每個包含虛函數的類建立一個表(稱為V TA B L E)。在V TA B L E中,編譯器放置特定類的虛函數位址。在每個帶有虛函數的類 中,編譯器秘密地置一指針,稱為v p o i n t e r(縮寫為V P T R),指向這個對象的V TA B L E。通過基類指針做虛函數調 用時(也就是做多态調用時),編譯器靜态地插入取得這個V P T R,并在V TA B L E表中查找函數位址的代碼,這樣就能調用正确的函數使晚捆綁發生。為每個類設定V TA B L E、初始化V P T R、為虛函數調用插入代碼,所有這些都是自動發生的,是以我們不必擔心這些。利用虛函數, 這個對象的合适的函數就能被調用,哪怕在編譯器還不知道這個對象的特定類型的情況下。(《C++程式設計思想》)

      在任何類中不存在顯示的類型資訊,可對象中必須存放類資訊,否則類型不可能在運作時建立。那這個類資訊是什麼呢?我們來看下面幾個類:

class no_virtual

{

public:

     void fun1() const{}

     int fun2() const { return a; }

private:

     int a;

}

class one_virtual

     virtual void fun1() const{}

class two_virtual

     virtual int fun2() const { return a; }

       以上三個類中:

      no_virtual沒有虛函數,sizeof(no_virtual)=4,類no_virtual的長度就是其成員變量整型a的長度;

      one_virtual有一個虛函數,sizeof(one_virtual)=8;

       two_virtual 有兩個虛函數,sizeof(two_virtual)=8; 有一個虛函數和兩個虛函數的類的長度沒有差別,其實它們的長度就是no_virtual的 長度加一個void指針的長度,它反映出,如果有一個或多個虛函數,編譯器在這個結構中插入一個指針( V P T R)。在one_virtual 和 two_virtual之間沒有差別。這是因為V P T R指向一個存放位址的表,隻需要一個指針,因為所有虛函數位址都包含在這個表中。

這個VPTR就可以看作類的類型資訊。

        那我們來看看編譯器是怎麼建立VPTR指向的這個虛函數表的。先看下面兩個類:

class base

     void bfun(){}

     virtual void vfun1(){}

     virtual int vfun2(){}

class derived : public base

     void dfun(){}

     virtual int vfun3(){}

     int b;

兩個類VPTR指向的虛函數表(VTABLE)分别如下:

base類

                     ——————

VPTR——> |&base::vfun1 |

                      ——————

                    |&base::vfun2 |

                    ——————

derived類

                      ———————

VPTR——> |&derived::vfun1 |

                     ———————

                  |&base::vfun2    |

                    ———————

                   |&derived::vfun3 |

        每當建立一個包含有虛函數的類或從包含有虛函數的類派生一個類時,編譯器就為這個類建立一個VTABLE,如上圖所示。在這個表中,編譯器放置了在這個類 中或在它的基類中所有已聲明為virtual的函數的位址。如果在這個派生類中沒有對在基類中聲明為virtual的函數進行重新定義,編譯器就使用基類 的這個虛函數位址。(在derived的VTABLE中,vfun2的入口就是這種情況。)然後編譯器在這個類中放置VPTR。當使用簡單繼承時,對于每 個對象隻有一個VPTR。VPTR必須被初始化為指向相應的VTABLE,這在構造函數中發生。

一旦VPTR被初始化為指向相應的VTABLE,對象就"知道"它自己是什麼類型。但隻有當虛函數被調用時這種自我認知才有用。

        個人總結如下:

      1、從包含虛函數的類派生一個類時,編譯器就為該類建立一個VTABLE。其每一個表項是該類的虛函數位址。

      2、在定義該派生類對象時,先調用其基類的構造函數,然後再初始化VPTR,最後再調用派生類的構造函數( 從二進制的視野來看,所謂基類子類是一個大結構體,其中this指針開頭的四個位元組存放虛函數表頭指針。執行子類的構造函數的時候,首先調用基類構造函數,this指針作為參數,在基類構造函數中填入基類的vptr,然後回到子類的構造函數,填入子類的vptr,覆寫基類填入的vptr。如此以來完成vptr的初始化。 )

      3、在實作動态綁定時,不能直接采用類對象,而一定要采用指針或者引用。因為采用類對象傳值方式,有臨時基類對象的産生,而采用指針,則是通過指針來通路外部的派生類對象的VPTR來達到通路派生類虛函數的結果。

       VPTR 常常位于對象的開頭,編譯器能很容易地取到VPTR的值,進而确定VTABLE的位置。VPTR總指向VTABLE的開始位址,所有基類和它的子類的虛函 數位址(子類自己定義的虛函數除外)在VTABLE中存儲的位置總是相同的,如上面base類和derived類的VTABLE中vfun1和vfun2 的位址總是按相同的順序存儲。編譯器知道vfun1位于VPTR處,vfun2位于VPTR+1處,是以在用基類指針調用虛函數時,編譯器首先擷取指針指 向對象的類型資訊(VPTR),然後就去調用虛函數。如一個base類指針pBase指向了一個derived對象,那pBase->vfun2 ()被編譯器翻譯為 VPTR+1 的調用,因為虛函數vfun2的位址在VTABLE中位于索引為1的位置上。同理,pBase->vfun3 ()被編譯器翻譯為 VPTR+2的調用。這就是所謂的晚綁定。

繼續閱讀