# [23] 繼承 — 你所不知道的
## FAQs in section [23]:
* [23.1] 基類的非虛函數調用虛函數可以嗎?
* [23.2] 上面那個FAQ讓我糊涂了。那是使用虛函數的另一種策略嗎?
* [23.3] 當基類構造函數調用虛函數時,為什么不調用派生類重寫的該虛函數?
* [23.4] 派生類可以重置(“覆蓋”)基類的非虛函數嗎?
* [23.5] “`Warning:?Derived::f(float)?hides?Base::f(int)”`是什么意思?
* [23.6] "virtual table" is an unresolved external 是什么意思?
## 23.1 基類的非虛函數調用虛函數可以嗎?
可以。有時(_并非總是!_)這是一個好主意。例如,假設所有`Shape`(圖形)對象有一個公共的打印算法。但這個算法依賴于它們的面積并且它們都有不同的方法來計算面積。在這種情況下,`Shape`的`area()`方法(譯注:得到`Shape`面積的成員函數)必須是virtual的(可能是純虛(pure-virtual)的),但`Shape::print()`可以在`Shape`中被定義為非虛(non-virtual)的,前提是所有派生類不會需要不同的打印算法。
```
?#include?"Shape.hpp"
?void?Shape::print()?const
?{
?????float?a?=?this->area();??//?area()?為純虛
//?...
?}
```
## 23.2 上面那個FAQ讓我糊涂了。那是使用虛函數的另一種策略嗎?
是的,那是不同的策略。是的,那的確是使用虛函數的兩種不同的基本方法:
1. 假設你遇到了上一個FAQ所描述的情況:每一個派生類都有一個結構完全一樣,只有一小塊不同的方法。因此算法是相同的,但實質不相同。在這種情況下,你最好在基類寫一個全面的算法作為`public:`方法(有時是非虛的),然后在派生類中寫那不同的一小塊。這一小塊在基類中聲明(通常是`protected:`的,純虛的,當然至少是`virtual`的),并且最終在每個派生類中被定義。這種情況下最緊要的問題是包含全面的算法的`public:`方法是否應該是`virtual`的。答案是,如果你認為某些派生類可能需要覆蓋它,就讓它成為`virtual`的。
2. 假設你遇到了上一個FAQ完全相反的情況,每一個派生類都有一個結構完全不同,但有一小塊的大多數(如果不是全部的話)相同的方法。在這種情況下,你最好將全面的算法放在最終在派生類中定義的`public:` `virtual`之中,并且將一小塊可以被只寫一次的公共代碼(避免代碼重復)隱藏在某處(任何地方!)。一般放在基類的`protected:`部分,但不是必須的,也可能不是最好的。找個地方隱藏它們就行了。注意,由于`public:`用戶不需要/不想做它們做的事情,如果在基類中隱藏它們,通常應該使它們是`protected:`的。假定它們是`protected:`的,那么可能不應該是`virtual`的:如果派生類不喜歡它們之一的行為,可以不必調用這個方法。
強調一下,以上列表中的是“既/又”情況,而不是“二者選一”的。換句話說,在任何給定的類上,不必在兩種策略中選擇。既有一個符合策略 #1 的方法`f()`,又有一個符合策略 #2 的方法`g()`是非常正常的。換句話說,在同一個類中,有兩種策略同時工作是非常正常的。
## 23.3 當基類構造函數調用虛函數時,為什么不調用派生類重寫的該虛函數?
當基類被構造時,對象還不是一個派生類的對象,所以如果?`Base::Base()`調用了虛函數 `virt()`,則?`Base::virt()`?將被調用,即使?`Derived::virt()`(譯注:即派生類重寫的虛函數)存在。
同樣,當基類被析構時,對象已經不再是一個派生類對象了,所以如果?`Base::~Base()`調用了`virt()`,則?`Base::virt()`得到控制權,而不是重寫的?`Derived::virt()` 。
當你可以想象到如果?`Derived::virt()`?涉及到派生類的某個成員對象將造成的災難的時候,你很快就能看到這種方法的明智。詳細來說,如果?`Base::Base()`調用了虛函數?`virt()`,這個規則使得?`Base::virt()`被調用。如果不按照這個規則,`Derived::virt()`將在派生對象的派生部分被構造之前被調用,此時屬于派生對象的派生部分的某個成員對象還沒有被構造,而?`Derived::virt()`卻能夠訪問它。這將是災難。
## 23.4 派生類可以重置(“覆蓋”)基類的非虛函數嗎?
合法但不合理。
有經驗的?C++?程序員有時會重新定義非虛函數(例如,派生類的實現可能可以更有效地利用派生類的資源),或者為了回避隱藏規則。即使非虛函數的指派基于指針/引用的靜態類型而不是指針/引用所指對象的動態類型,但其客戶可見性必須是一致的。
## 23.5 “`Warning:?Derived::f(float)?hides?Base::f(int)`”?是什么意思?
意思是:你要完蛋了。
你所處的困境是:如果基類聲明了一個成員函數`f(int)`,并且派生類聲明了一個成員函數?`f(float)`(名稱相同,但參數類型和/或數量不同),那么?`Base`?的?`f(int)`被隱藏(hidden)而不是被重載(overloaded)或被重寫(overridden)(即使?基類的`f(int)`是虛擬的)
以下是你如何擺脫困境:派生類必須有一個被隱藏成員函數的`using`?聲明,例如:
```
?class?Base?{
?public:
???void?f(int);
?};
?class?Derived?:?public?Base?{
?public:
???using?Base::f;????//?This?un-hides?Base::f(int)
???void?f(double);
?};
```
如果你的編譯器不支持`using`語法,那么就重新定義基類的被隱藏的成員函數,即使它們是非虛的。一般來說這種重定義只不過使用`::`語法調用了基類被隱藏的成員函數,如,
```
?class?Derived?:?public?Base?{
?public:
???void?f(double);
???void?f(int?i)?{?Base::f(i);?}??//?The?redefinition?merely?calls?Base::f(int)
?};
```
## 23.6? "virtual table" is an unresolved external 是什么意思?
如果你得到一個連接錯誤"`Error:?Unresolved?or?undefined?symbols?detected:?virtual?table?for?class?Fred`",那么可能是你在?`Fred`?類中有一個未定義的虛成員函數。
編譯器通常會為含有虛函數的類創建一個稱為“虛函數表”的不可思議的數據結構(這就是它如何處理動態綁定的)。通常你根本不必知道它。但如果你忘了為`Fred`?類定義一個虛函數,則有時會得到這個連接錯誤。
許多編譯器將這個不可思議的“虛函數表”放進定義類的第一個非內聯虛函數的編輯單元中。因此如果?`Fred`?類的第一個非內聯虛函數是?`wilma()`,那么編譯器會將?`Fred`?的虛函數表放在?`Fred::wilma()`?所在的編輯單元里。不幸的是如果你意外的忘了定義?`Fred::wilma()`,那么你會得到一個"`Fred`'s virtual table is undefined"(`Fred`的虛函數表未定義)的錯誤而不是“`Fred::wilma()` is undefined”(`Fred::wilma()`未定義)。
- C++ FAQ Lite
- [1] 復制許可
- [2] 在線站點分發本文檔
- [3] C++-FAQ-Book 與 C++-FAQ-Lite
- [6] 綜述
- [7] 類和對象
- [8] 引用
- [9] 內聯函數
- [10] 構造函數
- [11] 析構函數
- [12] 賦值算符
- [13] 運算符重載
- [14] 友元
- [15] 通過 <iostream> 和 <cstdio>輸入/輸出
- [16] 自由存儲(Freestore)管理
- [17] 異常和錯誤處理
- [18] const正確性
- [19] 繼承 — 基礎
- [20] 繼承 — 虛函數
- [21] 繼承 — 適當的繼承和可置換性
- [22] 繼承 — 抽象基類(ABCs)
- [23] 繼承 — 你所不知道的
- [24] 繼承 — 私有繼承和保護繼承
- [27] 編碼規范
- [28] 學習OO/C++
- [31] 引用與值的語義
- [32] 如何混合C和C++編程
- [33] 成員函數指針
- [35] 模板 ?
- [36] 序列化與反序列化
- [37] 類庫