看了 林銳 的 《高品質編程指南》8.2.2 令人迷惑的隱藏規則. (這裡的隱藏是指衍生類別的函數屏蔽了與其同名的基類函數)
這一節寫得很好: 1. 把出現隱藏的情況列舉出來了.
2. 舉的例子很貼切, 讓人能更好的理解.
3. 對出現隱藏函數情況的理解.
4. 提出對應的解決方案.
- 如果衍生類別的函數與基類的函數同名, 但是參數不同. 此時, 不論有無 virtual 關鍵字, 基類的函數將被隱藏(注意別與重載混淆).
- 如果衍生類別的函數與基類的函數同名, 並且參數也相同, 但是基類函數沒有 virtual 關鍵字. 此時, 基類的函數被隱藏(注意別與覆蓋混淆).
就是以上兩種情況導致了函數隱藏的情況出現. 看看書裡的例子:
#include <iostream>
using namespace std;
class Base
{
public:
virtual void f(float x){cout << "Base::f(float) " << x << endl;}
void g(float x){cout << "Base::g(float) " << x << endl;}
void h(float x){cout << "Base::h(float) " << x << endl;}
};
class Derived : public Base
{
public:
virtual void f(float x){cout << "Derived::f(float) " << x << endl;}
void g(int x) {cout << "Derived::g(int) " << x << endl;}
void h(float x){cout << "Derived::h(float) " << x << endl;}
};
int main()
{
Derived d;
Base *pb = &d;
Derived *pd = &d;
//沒出現隱藏的情況
pb->f(3.14f); //Derived::f(float) 3.14
pd->f(3.14f); //Derived::f(float) 3.14
//出現隱藏的情況 1
pb->g(3.14f); //Base::g(float) 3.14
pd->g(3.14f); //Derived::g(int) 3 (surprise!)
//出現隱藏的情況 2
pb->h(3.14f); //Base::h(float) 3.14 (surprise!)
pd->h(3.14f); //Derived::h(float) 3.14
system("pause");
return 0;
}
- 如果衍生類別的函數與基類的函數同名, 並且參數也相同, 但是基類函數沒有 virtual 關鍵字. 此時, 基類的函數被隱藏(注意別與覆蓋混淆).
//出現隱藏的情況
pb->h(3.14f); //Base::h(float) 3.14 (surprise!)
pd->h(3.14f); //Derived::h(float) 3.14
個人看法:
如果你學過 java 的多態, 對這個結果應該很難接受.
Derived 對象d 被隱式轉換為 Base 對象, 那麼該 Base 對象跟Derived 對象d 同名的函數被 Derived 對象d 覆蓋. 所以兩者的執行結果應該是一樣的.
但是這裡是 C++, 不是 java. 對於C++ 來說, 如果 Base 類的某個函數沒有 virtual 關鍵字, 那該函數跟 Derived 類的同名函數(參數也相同)是沒有什麼關係的.
這個請看下 《C++ Primer》501頁下面的"關鍵概念: 名字尋找和繼承".
pb 是 Base 類指標, pb指標 綁定到 Derived 對象 d, 但是由於 Base 類的 h(float) 函數不是虛函數, 無論實際對象是什麼類型, 都執行 Base::h(float).
程式會直接在 Base 類中尋找 h 函數; 如果沒有 h 函數, 那就會去其父類中尋找 h 函數 ; 如果還是找不到 h 函數 , 那就會去其父類的上一層類中繼續尋找 h 函數 ; 一次類推, 一直到找到方法A 為止; 如果最終都找不到, 你的程式應該是不能通過編譯的!(這種尋找方式倒是跟 java 一樣)
java 的函數是沒有 virtual 關鍵字的, 但是衍生類別和基類只要函數名和參數相同, 那麼該函數就被覆蓋了. 如果反過來想, 相對於 C++, 那不是 java 的每個函數都是虛函數嗎? 可能C++ 在於效率上考慮, 不想所有的函數都使用動態聯編.
- 如果衍生類別的函數與基類的函數同名, 但是參數不同. 此時, 不論有無 virtual 關鍵字, 基類的函數將被隱藏(注意別與重載混淆).
//出現隱藏的情況 1
pb->g(3.14f); //Base::g(float) 3.14
pd->g(3.14f); //Derived::g(int) 3 (surprise!)
個人看法:
這個其實也不能說是隱藏, 因為 g(float) 和 g(int) 是不同的函數, C++編譯後在符號庫中的名字分別是 _g_float 和 _g_int.即使他們都有 virtual 關鍵字, 但是因為是分別存在與衍生類別和基類中的不同函數, 所以在不存在覆蓋的關係(重載更不可能).
pb 是 Base 類指標, pb指標 綁定到 Derived 對象 d, Base 類根本就沒有 g(int) 函數, 所以 pd 指標是總不可能去調用 Derived::g(int) 函數的.
pb->g(3.14f); 程式在 Base 類中找到匹配的函數 Base::g(float) , 然後調用這個函數.
pd->g(3.14f); //Derived::g(int) 3 (surprise!)
編譯先在 Derived 類中尋找匹配 g(3.14f) 的函數, 他找到了 g(int) , 並且在 Derived 類中只有一個函數匹配. 即使 g(int) 是 virtual 的, 但pd 指標指向的 Derived 對象 d 的 g(int) 函數跟 Derived 類的 g(int) 函數是一樣的, 調用的都是 Derived::g(int) 函數, 所以不存在多態, 也就無需動態聯編了. (需要動態聯編的條件請看《C++ Primer》15.2.4 "virtual 與其他成員函數" 開頭部分, 這裡之所以無需動態聯編, 是因為不滿足動態聯編的第二個條件).
即使 Base 類有匹配的函數virtual g(float x), 但是virtual g(float x) 是存放在 Derived 對象 d 的虛函數表(virtual function table, vtbl, plus 13.4.4) 中的, 如果不進行動態聯編, 程式不會去 vtbl 中尋找對應的函數地址, vtbl 中的函數地址是不會被引用到的, 也就不會被調用了.
所以把 Base 類的 g(float x) 加上 virtual 關鍵字, 結果不會改變; 再把 Derived 類的 g(int) 加上 virtual 關鍵字, 結果也是不變的.
如果 Derived 類添加一個函數 virtual void g(float x){cout << "Derived::g(float) " << x << endl;}, 把 Base 類的 g(float x) 加上 virtual 關鍵字.
那結果就是
pb->g(3.14f); //Derived::g(float) 3.14
pd->g(3.14f); //Derived::g(float) 3.14
pb->g(3.14f)
pb 是 Base 類指標, pb指標 綁定到 Derived 對象 d. 由於 Base 類的 g(float) 函數的 virtual 的, 並且是 Base 類指標調用 g(float) 函數, pb指標綁定的對象 d 的靜態類型是 Derived 類, Derived 類的 g(float) 函數也是 virtual 的, 通常只有在運行程式時才能確定對象的動態類型. 所以編譯器對 虛函數 g(float) 使用動態聯編.
因為 Derived 類提供了虛函數 g(float) 的新定義, 所以在 Derived 對象 d 的虛函數表(vtbl) 中g(float) 函數的地址儲存為 Derived::g(float) 函數的地址. pb 指標調用虛函數 g(float) 時候, 程式到 Derived 對象 d 的虛函數表(vtbl) 中尋找 g(float) 函數的地址, 然後就執行該地址的函數. 所以 pb->g(3.14f) 執行了 Derived::g(float) 函數.
說起來, 子類要重載父類的方法, 還真是麻煩呢, 難道要全部方法copy 過來? 其實也不必要呢, 如果是子類對象能隱式轉換父類對象, 但是子類自有的方法, 對於基類對象來說是不存在的, 基類對象當然也不能調用這些方法了. 所以呢, 子類不必要重載父類的方法, 建一個屬於自己的方法還更好!
virtual 關鍵字, 好像就是在告訴你, 我這個函數可以給衍生類別同名字同參數的函數覆蓋; 純虛函數更是直接告訴衍生類別, 你一定要寫一個同名字同參數的函數覆蓋我, 哈哈!
重要查考: 《C++ Primer》第480頁 "關鍵概念:C++ 中的多態性".
《C++ Primer plus》13.4.4 虛擬成員函數和動態聯編.
《C++ Primer plus》第 449 頁 "虛函數的工作原理".
《C++ Primer》15.2.4 virtual 與其他成員函數.