# [20] 繼承 — 虛函數
## FAQs in section [20]:
* [20.1] 什么是“虛成員函數”?
* [20.2] C++ 怎樣同時實現動態綁定和靜態類型?
* [20.3] 虛成員函數和非虛成員函數調用方式有什么不同?
* [20.4] 析構函數何時該時虛擬的?
* [20.5] 什么是“虛構造函數(`virtual` constructor)”?
## 20.1 什么是“虛成員函數”?
從面向對象觀點來看,它是 C++ 最重要的特征:[6.8], [6.9].
虛函數允許派生類取代基類所提供的實現。編譯器確保當對象為派生類時,取代者(譯注:即派生類的實現)總是被調用,即使對象是使用基類指針訪問而不是派生類的指針。這樣就允許基類的算法被派生類取代,即使用戶不知道派生類的細節。
派生類可以完全地取代基類成員函數(覆蓋(override)),也可以部分地取代基類成員函數(增大(augment))。如果愿意的話,后者由派生類成員函數調用基類成員函數來完成。
## 20.2 C++ 怎樣同時實現動態綁定和靜態類型?
當你有一個對象的指針,而對象實際是該指針類型的派生類(例如:一個 `Vehicle*`指針實際指向一個Car 對象)。由此有兩種類型:指針的(靜態)類型(在此是`Verhicle`),和指向的對象的(動態)類型(在此是Car)。
_靜態類型_意味著成員函數調用的合法性被盡可能早地檢查:編譯器在編譯時。編譯器用指針的靜態類型決定成員函數調用是否合法。如果指針類型能夠處理成員函數,那么指針所指對象當然能很好的處理它。例如,如果?`Vehicle`?有某個成員函數,則由于`Car`是一種`Vehicle`,那么`Car`?當然也有該成員函數。
_動態綁定_意味著成員函數調用的代碼地址在最終時刻才被決定:基于運行時的對象動態類型。因為綁定到實際被調用的代碼這個過程是動態完成的(在運行時),所以被稱為“動態綁定”。動態綁定是虛函數導致的結果之一。
## 20.3 虛成員函數和非虛成員函數調用方式有什么不同?
非虛成員函數是靜態確定的。也就是說,該成員函數(在編譯時)被靜態地選擇,該選擇基于指象對象的指針(或引用)的類型。
相比而言,虛成員函數是動態確定的(在運行時)。也就是說,成員函數(在運行時)被動態地選擇,該選擇基于對象的類型,而不是指向該對象的指針/引用的類型。這被稱作“動態綁定”。大多數的編譯器使用以下的一些的技術:如果對象有一個或多個虛函數,編譯器將一個
隱藏的指針放入對象,該指針稱為“virtual-pointor”或“v-pointer”。這個v-pointer指向一個全局表,該表稱為“虛函數表(virtural-table)”或“v-table”。
編譯器為每個含有至少一個虛函數的類創建一個v-table。例如,如果`Cirle`類有虛函數d`draw()`、`move()` 和 `resize()`,那么將有且只有一個和Cricle類相關的v-table,即使有一大堆Circle對象。并且每個?`Circle`對象的?v-poiner將指向?`Circle`的這個?v-table。該?v-table自己有指向類的各個虛函數的指針。例如,`Circle`?的v-table?會有三個指針:一個指向`Circle::draw()`,一個指向?`Circle::move()`,還有一個指向`Circle::resize()`。
在分發一個虛函數時,運行時系統跟隨對象的?v-pointer找到類的?v-table,然后跟隨v-table中適當的項找到方法的代碼。
以上技術的空間開銷是存在的:每個對象一個額外的指針(僅僅對于需要動態綁定的對象),加上每個方法一個額外的指針(僅僅對于虛方法)。時間開銷也是有的:和普通函數調用比較,虛函數調用需要兩個額外的步驟(得到v-pointer的值,得到方法的地址)。由于編譯器在編譯時就通過指針類型解決了非虛函數的調用,所以這些開銷不會發生在非虛函數上。
注意:由于沒有涉及諸如多繼承,虛繼承,RTTI等內容,也沒有涉及諸如page fault,通過指向函數的指針調用函數等空間/時間論的內容,所以以上討論是相當簡單的。如果你想知道其他的內容,請詢問?_[`comp.lang.c++`](news:comp.lang.c++)_;而不要給我發E-MAIL!
## 20.4 析構函數何時該時虛擬的?
當你可能通過基類指針刪除派生類對象時。
虛函數綁定到對象的類的代碼,而不是指針/引用的類。如果基類有虛析構函數,`delete?basePtr`時(譯注:即基類指針),`*basePtr`?的對象類型的析構函數被調用,而不是該指針的類型的析構函數。這通常是一件好事情。
_TECHNO-GEEK WARNING; PUT YOUR PROPELLER HAT ON._
從技術上來說,如果你打算允許其他人通過基類指針調用對象的析構函數(通過`delete`這樣做是正常的),并且被析構的對象是有重要的析構函數的派生類的對象,就需要讓基類的析構函數成為虛擬的。如果一個類有顯式的析構函數,或者有成員對象,該成員對象或基類有重要的析構函數,那么這個類就有重要的析構函數。(注意這是一個遞歸的定義(例如,某個具有重要析構函數的類,它有一個成員對象(它有基類(該基類有成員對象(它有基類(該基類有顯式的析構函數))))))
_END TECHNO-GEEK WARNING; REMOVE YOUR PROPELLER HAT_
如果你對以上的規則理解有困難,試試這個簡單的:類應該有虛析構函數,除非這個類沒有虛函數。原理:如果有虛函數,說明你想通過基類指針來使用派生對象,并且你所可能做的事情之中,可能包含了調用析構函數(通常通過`delete`隱含完成)。一旦你在類中加上了一個虛函數,你就已經需要為每一個對象支付空間代價(每個對象一個指針;注意這是理論上的編譯器特性;實際上每個編譯器都是這樣做的),所以這時使析構函數成為虛擬的通常不會額外付出什么。
## 20.5 什么是“虛構造函數(`virtual` constructor)”?
一種允許你做一些?C++?不直接支持的事情的用法。
你可能通過虛函數?`virtual` `clone()`(對于拷貝構造函數)或虛函數 `virtual` `create()`(對于默認構造函數),得到虛構造函數產生的效果。
```
?class?Shape?{
?public:
???virtual?~Shape()?{?}?????????????????//?虛析構函數
???virtual?void?draw()?=?0;?????????????//?純虛函數
???virtual?void?move()?=?0;
???//?...
???virtual?Shape*?clone()??const?=?0;???//?使用拷貝構造函數_
???virtual?Shape*?create()?const?=?0;???//?使用默認構造函數
?};
?class?Circle?:?public?Shape?{
?public:
???Circle*?clone()??const?{?return?new?Circle(*this);?}
???Circle*?create()?const?{?return?new?Circle();??????}
???//?...
?};
```
在?`clone()`?成員函數中,代碼?`new?Circle(*this)` 調用?`Circle` 的拷貝構造函數來復制`this`的狀態到新創建的`Circle`對象。在?`create()`成員函數中,代碼?`new?Circle()`?調用`Circle`的默認構造函數。
用戶將它們看作“虛構造函數”來使用它們:
```
?void?userCode(Shape&?s)
?{
???Shape*?s2?=?s.clone();
???Shape*?s3?=?s.create();
???//?...
???delete?s2;????//?在此處,你可能需要虛析構函數
???delete?s3;
?}
```
這個函數將正確工作,而不管?`Shape`?是一個`Circle`,`Square`,或是其他種類的?`Shape`,甚至它們還并不存在。
注意:成員函數`Circle`'s `clone()`的返回值類型故意與成員函數`Shape`'s `clone()`的不同。這種特征被稱為“協變的返回類型”,該特征最初并不是語言的一部分。如果你的編譯器不允許在`Circle`類中這樣聲明`Circle*?clone()?const`(如,提示“The return type is different”或“The member function's type differs from the base class virtual function by return type alone”),說明你的編譯器陳舊了,那么你必須改變返回類型為`Shape*。`
- 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] 類庫