1. 程式人生 > >[讀書筆記] 深入探索C++物件模型-第四章-Function語義學(中)

[讀書筆記] 深入探索C++物件模型-第四章-Function語義學(中)

繼續整理第四章的內容,注:以下部分圖片來自於原文

1. 單繼承情況下的虛擬函式呼叫: 

對於多型虛擬函式的呼叫(通過基類指標或者引用),例如ptr->z();,需要知道兩個資訊:

    a. ptr所指物件的真實型別,這可以使我們選擇正確的z()實體;

    b. z()實體位置,以便可以呼叫它。

結合以上的所需資訊,需要為每一個多型的類物件身上增加兩個成員:

    a. 一個字串或數字,表示class的型別;

    b. 一個指標,指向某個表格,表格中帶有程式的虛擬函式的執行期地址。

為了找到這些函式地址,每一個虛擬函式會被指派一個表格索引值。也就是說這個虛擬函式表中含有該類物件所有啟用的虛擬函式,包括:

    a. 該類定義的函式,可能會改寫基類的虛擬函式,也可能時該類特有的虛擬函式;

    b. 繼承自基類的函式實體,他們沒有被子類改寫;

    c. 一個pure_virtual_called()函式實體,它既可以扮演純虛擬函式的空間保衛者角色,也可以當作執行期異常處理函式(偶爾)。

例如,以下三個依次繼承的類:


對應的虛擬函式表如下:


再回過頭來看一看之前的例子:ptr->z();對於這個呼叫,我們知道:

    a. 經由ptr可以訪問到該物件對應的虛表;

    b. 雖然不知道哪一個z()函式會被呼叫,但是我知道每一個z()函式都在虛表的第五個slot中也就是slot4。

所以改呼叫會轉化為:(*ptr->vptr[4])(ptr);第二個ptr代表傳入的this指標,這個在上一篇中有過記錄。到目前為止,唯一需要在執行期才能決定的東西就是ptr所指內容的型別,也就是slot4是上述三個虛表的哪一個。

2. 多繼承情況下的虛擬函式呼叫: 

多繼承的複雜主要圍繞在第二個以及後續的基類身上,因為涉及需要在執行期調整this指標。例如,有如下三個類:


此時的虛表情況:


對於Base2來說,Derived支援虛擬函式要複雜很多,依次看三種情況:

a. 通過後繼基類呼叫子類的虛解構函式,對於如下程式碼:

Base2* pbase2 = new Derived;
delete pbase2;

Derived物件的地址必須調整以指向Base2子物件,編譯時期可能產生如下程式碼:

Derived* temp = new Derived;
Base2* pbase2 = temp ? temp + sizeof(Base1) : 0;//調整使其指向Derived中Base2處
如果沒有這樣的調整,指標的任何非多型行為都會失敗,例如pbase2->data_Base2,因為未作調整的pbase2指向Derived起始處,無法訪問Base2的資料成員data_Base2。此時如果需要刪除pbase2所指物件,指標必須在進行一次調整,以指向Derived物件起始處,一種調整方法是,擴充套件虛擬函式表,表中的每一條記錄,不再僅僅時虛擬函式地址,而是地址加上可能的偏移量,該偏移量用以代表this指標的調整情況,於是以下虛擬函式的呼叫操作:
(*pbase2->vptr[1])(pbase2);

會變為:

(*pbase2->vptr[1].faddr)(pbase2 + pbase2->vptr[1].offset);//faddr代表虛擬函式地址,offset代表指標需要的偏移量,例如上例中offset可能為4

但是改做法的缺點是所有的虛擬函式呼叫操作都會受影響,即便不需要調整,空間和時間效率都會有所降低,一種比較由效率的方法時使用thunk技術,所謂thunk,是一小段彙編程式碼用來做兩件事:以適當的offset調整this指標,然後跳轉到對應的虛擬函式,可能像這樣:this += sizeof(Base1); Derived::!Derived(this);,這樣,虛擬函式表中的內容可以直接指向虛擬函式,也可以指向一個相關的thunk(如果需要調整this指標的話),不需要所有虛擬函式都承擔不必要的額外負擔。注:關於Thunk技術,會單獨整理一篇文章

b. 第二個情況是通過子類指標呼叫第二個基類中一個繼承而來的虛擬函式,此種情況下,子類指標必須調整,以指向第二個子物件,例如:

Derived* pder = new Derived;
//呼叫Base2::mumble()
//pder必須向前調整sizeof(Base1)個bytes
pder->mumble();

c. 第三種情況是對於clone()函式,這是一個C++本身的擴充性質:允許一個虛擬函式的返回值型別有所變化,可能是base,也可能是publicly derived type,此時,當我們通過指向Base2的指標呼叫clone()時,又會牽扯this指標的offset問題:

Base2* pbase2_1 = new Derived;
//呼叫Derived* Derived::clone()
//返回值必須被調整,以指向Base2子物件
Base2* pbase2_2 = pbase2_1->clone();
執行語句pbase2_1->clone();時,pbase2_1會被調整指向Derived物件的起始地址,於是clone()的Derived版本會被呼叫,它會傳回一個指標,指向一個新的Derived物件,該物件在被指定給pbase2_2之前,必須經過調整,以指向Base2子物件(subobject)。

虛擬繼承下虛擬函式的呼叫情況會在下一篇中繼續整理。