今天有位幫一位朋友釐清觀念的時候,發現他對物件的記憶體分配有些誤解。這位朋友告訴我他的資訊來源是這個網頁:
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 語意的程式碼最好提高警覺,其中可能有問題。

在Java/Android下 這個狀況倒是很容易釐清 設計一個父類別 然後衍生三個子類別 分別是 A(一個長整數參數 一個函數) B(兩個長整數 一個函數) C(一個長整數 兩個函數) 然後用Thread跑無限迴圈 將新增的A/B/C塞進 ArrayList<父類別>中 塞到記憶體爆炸以前 (Android有明確的記憶體限制) 會發現 B能塞的數量只有A/C的一半 這答案就很明顯了
如果只是要比物件佔用的空間,C++ 用 sizeof 更簡單
那個網站的留言板是 Disqus, 應該用 Google 帳號就能回了吧?
我個人很討厭帳號關聯
Hi novus, 因為我是想要描述, downcast 時會將強迫 compiler 讓 base class使用 derived class 能用的範圍,所以就用了這次比較差的例子XD 我是知道 object 只會帶 data member ,但是 c++ object 怎麼樣去 access 到 function/virtual functions 的詳細過程,我還不夠清楚XD,只知道會帶一個 vpr/vtb,所以才會寫成這樣,需要再多讀一點書,感謝指正。
好文
優質文章