Re: [問題] virtual function 的疑惑2

看板C_and_CPP作者 (我要加入劍道社!)時間14年前 (2010/02/11 16:35), 編輯推噓6(601)
留言7則, 6人參與, 最新討論串2/4 (看更多)
(呵欠) ㄟ...好像沒人要回這篇,我只好就我所知的隨便寫一下... 上一篇講了 class member function 的原理,現在我們來看看 virtual function。 (以下我講的 C++ 物件實作方式都只是一個常用的模形,各家 Compiler 真的用的 方法可能都有些微不同) 當你這樣寫的時候: class Foo { public: void f(); virtual void g(); private: int my_data; }; Foo* obj = ...; obj->f(); obj->g(); Compiler 是這樣看待它的... struct VTable { void (*g)(Foo* this); // for virtual member function Foo::g() }; struct Foo { VTable* vtable; int my_data; }; void Foo_f(Foo* this); // for non-virtual member function Foo::f() Foo* obj = ...; Foo_f(obj); // obj->f(); ( *(obj->vtable->g) )(obj) // obj->g(); 也就是說呢,class 中的 virtual member function 其實是個 function pointer, 呼叫它的時候,其實是去 vtable 中查出這個 function pinter 再呼叫。 我這邊為了說明簡單起見,先乎略掉上一篇所說的多重繼承 offset 問題, 真正的 vtable 其實還要多記錄一個 offset 來處理多重繼承的情況。 那 vtable 中的 function pointer 是指向哪裡呢?答案是:它是在呼叫 ctor 時, compiler 偷偷幫你做好了初始化。 void Foo_g(Foo* this) // Foo::g() 的實作 { // ... } VTable foo_vtable; // 這個 vtable 實體是給所有 Foo 物件用的 foo_vtable.g = Foo_g; // 它的 g 這個欄位,指向你的實作 void Foo_ctor(Foo* this) // Foo 的建構式 { ... ... // 這邊是你在 Foo 的建構式中寫的東西 ... this->vtable = &foo_vtable; // 這行是 compiler 幫你加的 } 所以當你呼叫 Foo 的 virtual function 時,compiler 會找出 obj 的 vtable, 從裡面那個對應的 function pointer 得到正確的位址來呼叫。 那現在我們要繼承 Foo 並且改寫 g(),會發生什麼事呢? class Bar : public Foo { public: virtual void g(); private: double my_child_data; }; 同樣地,compiler 為了達成 dynamic binding,幫你產生了對應的 code: struct Bar { Foo_vtable* vtable; int my_data; // 來自 Foo::my_data double my_child_data; // Bar 自己的 member variable }; void Bar_g(Bar* this); // Bar::g() 的實作 VTable bar_vtable; // Bar 的 vtable bar_vtable.g = Bar_g; // 其中 g 的欄位指向 Bar::g() 的實作 void Bar_ctor(Bar* this) // Bar 的建構式 { Foo_ctor(this); // 它會先呼叫爸爸的建構式 ... ... // 這邊是你寫在 Bar::Bar() 中的東西 ... this->vtable = bar_vtable; // 這行是 compiler 幫你加的 } 這麼一來,我們就可以達到 virtual invocation 的效果: Foo* a; a = new Foo; a->g(); // 呼叫的是 Foo::g(),因為在建構式中, // compiler 幫你把 vtable 指向 foo_vtable delete a; a = new Bar; a->g(); // 同樣的道理,這時候 vtable 指向 bar_vtable // 所以會呼叫到 Bar::g() 現在我們回到你的問題... ※ 引述《QQ29 (我愛阿蓉)》之銘言: : 1. : class Base1 { : public: : void f() {cout<<"1"; } : }; : int main() : { : int *p=(new int); : Base1 *a1 = (Base1*)p; : a1->f(); : return 0; : } : 這個情況 a1->呼叫的到f() 印出1.... 這是因為什麼原因他可以link到f()? 因為 f 並不是 virtual function,它只是個一般的 member function member function 和普通的 function 只有一個差異:compiler 會幫你傳 this 所以你可以想成 f 只是如下的普通 function: void Base1_f(Base1* this) { cout <<"1"; } : 想一下 印出sizeof(Base1) 會 印1...這個1不知道是什麼的size... 因為 C++ 的 class 至少占用 1byte...即使它沒有任何成員 我忘了這是不是標準,好像是吧 (真隨便 XD) : 當我加上virtual 就會run time 壞掉 當你加上 virtual,意味著在呼叫它的時候,compiler 會從 a1 這個指標中, 去尋找裡面的 vtable 並試圖取出 Base::f() 的位址, 然而 a1 事實上是個指向 int 的 pointer,裡面並沒有 vtable, 硬來的結果當然是 runtime error : 2. : http://nopaste.csie.org/93cdd : 這是簡單的dynamic binding : 如果以上一篇的觀念(我想可能還不夠解釋dynamic binding) : a1->他自己的vptr 只有記載她自己的a1::f() : 他是什麼機制可以知道Base3也有繼承他 也有override f()? : 我認為他是 a1->Base1::f() => Base2::f() => Base3::f() : 但中間的=>關係 是哪裡有記錄呢? 如前所述...並非 base 知道某個 child 繼承自己 而是所有的物件都有一個 vtable 的指標 它們用這個指標來分辦自己是屬於哪一個 class : 3. : 我印出sizeof(Base3)印出4 : 覺得很怪 他繼承 Base2至少要有Base2的vptr並且還得到Base1的vptr 和自己的vptr : 想說印出12比較合理 不對,如果你是一個單一的繼承鏈,沒有多重繼承, 那 vtable 的指標只有一個 : 我觀念錯很大 : 於是寫成 多繼承一個 : class Base3 : public Base2, public Base1{ : 印出8... : 故意繼承個空class 也印出8 : class Base4{}; : class Base3 : public Base4{ : 仔細想一下 如果我繼承觀念沒錯的話 : class A{int x}; class B:public A{double y;}; : B會擁有 父親的A::x(4) 和自己繼承來的B::x(4) : 和原本自己的B::y(8) 呃...你的繼承觀念顯然有錯 繼承後並不會多一份 B::x 出來 而且我跑的結果: class A { int x; }; class B : public A { double y; }; cout << sizeof(B); 結果非並 24 而是 16 (使用 vc9 測試的結果) 是 16 的原因在於 compiler 會試圖讓 B::y 的位址 align 在 8byte 上 所以會調整 class member 的 layout : sizeof(B) = 24的原因應該是 有三個變數 所以是3* max大小 所以=3*8=24.. 你如果得到 sizeof(B) = 24 應該是因為 B 裡面有 virtual function 為了存 vtable 並且讓 B::y align 在 8byte 上面 compiler 因而在 class 中塞了許多沒用到的空間 完全不是因為有三個變數然後 8*3=24... : 但如果是扯到vptr的話 : 就我測試的結論 : 感覺vptr還是只有一個 而 vtable就一直增加記錄有誰override他 就在加上去 : 所以可能可以解釋 2.的問題 由一個vptr可以一直link到 Base3::f() : 但這樣我就不能解釋 : class Base4{}; : class Base3 : public Base4{ 得到8 class Base{}; class Derived : public Base {}; cout << sizeof(Derived); 我的結果是 1 喔 (gcc 和 vc9 的結果相同) 如果 Base 或 Derived 有 virtual function 結果則都是 4 : 和 : class Base3 : public Base2, public Base1{ 得到8 class Base1{}; class Base2{}; class Derived : public Base1, public Base2 {}; cout << sizeof(Derived); 結果為 1 (gcc 和 vc9 皆是) 若 Base1 和 Base2 其中一個有 virtual function 那結果為 4 (vptr 占的空間) 若 Base1 和 Base2 兩邊都有 virtual function 則結果為 8 (兩份 vptr 占的空間) : 有諸多觀念無法融會貫通 : 手邊的書籍也沒有寫的這麼深入= = (該買深入一點的書了) : 雖然我覺得不懂也沒差反正可以run就好 但是好奇心還是讓我想問這幾項問題 : 問題繁多@@請教各位 : 非常感謝 我覺得你很有心要理解 C++,這很好。 不過的確是該買書的時候了,這些東西書上講的都比我寫的詳細許多。 然後順便找一本 Computer Organization & Assembly 的書來看, 這樣你才會知道 CPU 究竟如何執行你的 code -- ※ 發信站: 批踢踢實業坊(ptt.cc) ◆ From: 140.112.29.108

02/11 16:58, , 1F
先推一下, 晚些時候有空再看....Orz
02/11 16:58, 1F

02/11 17:00, , 2F
l大鳩甘心= = 這都是我過年要看的文章 .....
02/11 17:00, 2F

02/11 17:08, , 3F
對於那個繼承多一份 那邊我知道錯了~ 所以才問了下面那篇orz
02/11 17:08, 3F

02/11 17:46, , 4F
註解部分有筆誤。Foo::f is non-virtual function
02/11 17:46, 4F
感謝指正 ※ 編輯: littleshan 來自: 140.112.29.108 (02/11 17:53)

02/11 18:42, , 5F
推~
02/11 18:42, 5F

02/11 23:51, , 6F
其實 vtable 通常還有一個欄位指向 type info 的存放區。
02/11 23:51, 6F

02/21 02:54, , 7F
大推~就感心~~
02/21 02:54, 7F
文章代碼(AID): #1BSy55qt (C_and_CPP)
討論串 (同標題文章)
文章代碼(AID): #1BSy55qt (C_and_CPP)