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

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 語意的程式碼最好提高警覺,其中可能有問題。

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


留言列表 (4)

發表留言
  • Hsu Wei-Yen
  • 在Java/Android下 這個狀況倒是很容易釐清

    設計一個父類別 然後衍生三個子類別
    分別是 A(一個長整數參數 一個函數) B(兩個長整數 一個函數) C(一個長整數 兩個函數)
    然後用Thread跑無限迴圈 將新增的A/B/C塞進 ArrayList<父類別>中
    塞到記憶體爆炸以前 (Android有明確的記憶體限制) 會發現 B能塞的數量只有A/C的一半
    這答案就很明顯了
  • 如果只是要比物件佔用的空間,C++ 用 sizeof 更簡單

    novus 於 2015/02/12 22:01 回覆

  • chchwy Chang
  • 那個網站的留言板是 Disqus, 應該用 Google 帳號就能回了吧?
  • 我個人很討厭帳號關聯

    novus 於 2015/03/09 23:17 回覆

  • OT Chen
  • Hi novus,

    因為我是想要描述, downcast 時會將強迫 compiler 讓 base class使用 derived class 能用的範圍,所以就用了這次比較差的例子XD 我是知道 object 只會帶 data member ,但是 c++ object 怎麼樣去 access 到 function/virtual functions 的詳細過程,我還不夠清楚XD,只知道會帶一個 vpr/vtb,所以才會寫成這樣,需要再多讀一點書,感謝指正。
  • 123
  • 好文