程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> 關於C++ >> 解析動態聯編(下篇)

解析動態聯編(下篇)

編輯:關於C++

三 虛函數表VTABLE

動態聯編過程跟我們猜測的大致相同。編譯器在執 行過程中遇到virtual關鍵字的時候,將自動安裝動態聯編需要的機制,首先為這 些包含virtual函數的類(注意不是類的實例)--即使是祖先類包含虛函數而本身 沒有--建立一張虛擬函數表VTABLE。在這些虛擬函數表中,編譯器將依次按照函 數聲明次序放置類的特定虛函數的地址。同時在每個帶有虛函數的類中放置一個 稱之為vpointer的指針,簡稱vptr,這個指針指向這個類的VTABLE。

關於 虛擬函數表,有幾點必須聲明清楚:

1. 每一個類別只能有一個虛擬函數 表,如果該類沒有虛擬函數,則不存在虛擬函數表。

2. C++編譯時候編譯 器會在含有虛函數的類中加上一個指向虛擬函數表的指針vptr。

3. 從一 個類別誕生的每一個對象,將獲取該類別中的vptr指針,這個指針同樣指向類的 VTABLE。

因此類、對象、VTABLE的層次結構可以用下圖表示。其中X類和Y 類的對象的指針 都指向了X,Y的虛擬函數表,同時X,Y類自身也包含了指向虛擬函 數的指針。

為了方 便問題說明,我們將2.cpp例子進行擴展,擴展程序如下。//4.cpp
15. #include <iostream.h >
16. class shape{
17.  public:
18.  virtual void draw(){cout<<"shape::draw ()"<<endl;}
19.  virtual void area() {cout<<"shape::area()"<<endl;}
20.  void fun(){draw();area();}
21. };
22. class circle:public shape {
23. public:
24.  void draw() {cout<<"circle::draw()"<<endl;}
25.  void adjust(){cout<<"circle::adjust()"<<endl;}
26. };
27. main(){
28.  shape oneshape;
29.   oneshape.fun();
30.
31.  circle circleshape;
32.   shape& baseshape=circleshape;
33.  baseshape.fun();
34. }

編譯器在編譯上面這段代碼的時候將為這shape和circle兩個對象 分別建立一個VTABLE表,這些表依次填充派生類對象和基類對象中聲明的所有的 虛函數地址。如果派生類本身沒有重新定義基類的虛函數,那麼填充的就是基類 的虛函數地址。這樣一旦如果函數調用一個派生類不存在的方法時候能夠自動調 用基類方法。然後編譯器在每個類中放置一個vptr,一般置於對象的起始位置, 繼而在對象的構造函數中將vptr初始化為本類的VTABLE的地址。整個結果布局如 下。

圖一

圖一中的rectangle的VTABLE中的area() 和triangle的VTABLE的adjust() 都是填充的基類的虛函數地址。 C++ 編譯程序時候按下面的步驟進行工 作:

①為各類建立虛擬函數表,如果沒有虛函數則不建立。

②暫時 不連接虛函數,而是將各個虛函數的地址放入虛擬函數表中。

③直接連接 各靜態函數。

這些工作做完之後,模塊圖如圖二:

圖二

執行時候,誕生了oneshape和circleshape兩個對象,oneshape對象的 vptr指針指向shape的VTABLE,circleshape對象的vptr指針指向circleshape的 VTABLE,在執行oneshape.fun()的時候,fun函數的this指針指向了oneshape對象 ,進入fun()之後程序繼續執行this->draw(),由於this指向oneshape對象, oneshape的vptr又指向shape類的VTABLE,這樣就從VTABLE中得到需要綁定的函數 的地址,並連接起來。同樣,this-> area()也經由oneshape對象而連接到相 應的函數上,如圖三。

圖三

現在我們執行baseshape.fun()函數。

circle  circleshape;
shape& baseshape=circleshape;
baseshape.fun();

函數進入fun函數之後,函數的this指針將指向 basefun對象,另一方面basefun指向一個circleshape,因此this指針指向的實際 上為circleshape對象,而circleshape的vptr指針指向circle類的虛擬函數表, 這樣編譯器將從虛擬表中取出circle::draw()和circle::area()的地址,進行連 接。因為circle本身沒有重新定義area()方法,因此編譯器使用shape的area()方 法。如圖四。

圖四

遵循上面的思路,基於基類的指針總能找到正確的子類對象的實現。但是 象上面的 this->draw是怎麼編譯的呢。

四 編譯內幕

在上面的 程序中,this指針不同,從而連接到不同的fun函數。那麼C++如何編譯這些指令 呢。道理在於:所有的基類的派生類的虛擬函數表的順序與基類的順序是一樣的 ,對於基類中不存在方法再按照聲明次序進行排放。這樣不管是shape還是circle 或者從shape又繼承出來的其余的類它們的虛擬函數表的第一項總是draw函數的地 址,然後是area的地址。對於circle類,下面的才是adjust的地址。因此不管對 於shape還是circle,this->draw總是編譯成 call this->VTABLE[0]; this->area()總是翻譯成 call this->VTABLE[1]; 程序到真正運行時候將 會發現this的真正指向的對象,如果是shape,則調用shape->VTABLE[0],如果 是circle,則調用circle->VTABLE[1],如圖五。

圖五

請看下面的這個例子。

35. #include
36. class shape{
37. public:
38.  virtual void draw() {cout<<"shape::draw()"<draw();//OK
51.   oneshape->adjust();//錯誤,編譯器無法通過
52.
53.   circle* circleshape;
54.  circleshape->adjust();
55. }

在程序編譯期間,由於oneshape為shape類型的,因此它將檢查shape 的虛擬函數表,發現VTABLE[0]為draw函數的地址,於是翻譯成p->VTABLE[0] 。未來執行期間,p 實際上指向的是circle對象,因此真正調用的為circle- >VTABLE[0]處的函數,即circle::draw。同樣對於adjust函數,C++ 編譯器也 會去檢查shape的VTABLE,結果編譯器無法找到adjust函數,因此編譯無法通過。 對於circleshape,因為它是circleshape類型的,因此它將會檢查circle的 VTABLE,得知VTABLE[2]處為adjust的地址,因此編譯器翻譯成call circleshape ->VTABLE[2],真正執行時候circleshape為circle類型,因此它將綁定circle 的VTABLE[2]處的函數即circle:: adjust()。 就這樣,編譯器借助虛擬函數表實 現了動態聯編的過程,從而使多態的實現有了可能。因此說虛擬函數表是多態性 的幕後功臣一點也不為過。

五 結束語

多態性的實現是一個非常復 雜的過程,上面的討論僅僅是針對簡單繼承而言,即基類只有一個的情況,對於 多重繼承,情況又會有所改變。本文僅是拋磚引玉,希望有興趣的朋友可以一起 探討。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved