今天有位幫一位朋友釐清觀念的時候,發現他對物件的記憶體分配有些誤解。這位朋友告訴我他的資訊來源是這個網頁:

http://ot-note.logdown.com/posts/173174/note-cpp-named-type-convertion

本來我應該直接在原頁面回應,但那個頁面好像必須要弄個啥鬼帳號才能回,所以我還是在這裡說明好了。

主要問題來自 DOWNCAST 標題下的這一段文字

如果你只配置一個 Base 的空間,因為裡面只有一個 function ,所以只會有一個 int 的空間 來存放 int getAge() 的位置,有效的操作為 this 代表本 object , this+4 代表 int getAge()。...

就我朋友的解讀,作者似乎認為物件當中會存放成員函數或函數指標,這當然是錯誤的。我之前也曾遇過有些人以為一個 class 的函數越多,生成的物件也越肥,這當然也是錯的。

如果要解說物件繼承體系之間的 memory layout 關係,其實只能用一般的資料成員,因為成員函式是完全不同的故事。

簡單的說,成員函式獨立存在於程式的 code section,不會佔用個別物件的空間,物件也不需要特別去記錄成員函數的指標,靜態的 function binding 靠編譯器就能搞定。通常編譯器會提供一種叫做 thiscall 的 calling convention 來表示這回事。

所以成員函數的位址和物件毫無關聯,正常來說不可能透過 this 指標去推算成員函數的位址。

在 dynamic binding 的情況下,確實需要知道 virtual 函式的指標才能呼叫,不過個別物件內部仍然不需儲存 virtual 函式指標,這些資訊放在 vtable 中,個別物件只要保存 vtable 的位址即可。

另外,virtual 函式也有可能是 static binding,此時處理方式與一般的非 virtual 函式相同,編譯器即可決定,完全不必透過 vtable。

最後想幫原文補充一點,upcasting 真的很安全嗎?這個機制本身沒什麼問題,但是在某些用途下會變得很危險:

struct Base {
    Base(const Base& other) : x(other.x) {}
    ...
    int x;
};

struct Derived : public Base {
    ...
    int y;
}

void func(Base b) {
    b.someVirtualFunc();
}

如果有人拿一個 Derived 物件餵給 func() 就悲劇了,這種情形稱為 object slicing,很不幸的是 C++ 的語言機制完全無力阻止這種事情發生。這告訴我們一個經驗法則: 如果你在操作一個繼承體系,幾乎所有的物件傳遞、轉型都應該透過指標或參照進行,如果發現 value 語意的程式碼最好提高警覺,其中可能有問題。

arrow
arrow
    全站熱搜

    novus 發表在 痞客邦 留言(5) 人氣()