類繼承-虛擬函式(2)
關於指標和引用的相容性:
通常,C++不允許將一種型別的地址賦給另一終型別的指標,也不允許將一種型別的引用指向另一種型別。但是,在類繼承中,我們看到了可以將派生了Derived物件的地址賦給基類指標Based*,或者將基類的引用Based&指向派生類Derived物件,只是允許的,為什麼呢?
首先,公有繼承確定了派生類物件和基類物件是一種is-a的關係(即Derived物件都是一個Based物件)。將派生類引用或者指標轉換成基類引用或者指標被稱為向上強制轉換,公有繼承不需要進行顯式型別轉換,派生類擁有基類的所有資料成員和方法,對基類的任何操都是適用於派生類。向上強制轉換是可以傳遞的,即Based的引用或者指標可以指向Based物件、Derived物件、DerivedPlus物件。
相反的過程:將基類的引用或者指標轉換成派生類的引用或者指標,稱為向下強制轉換,是不允許的,因為Is-a關係是不可逆的。派生類的新增資料成員和方法都不適用於基類。
動態聯編和靜態聯編:
在編寫的程式碼中,通常會存在過載函式和普通函式,編譯器通過函式名和函式引數確定使用哪個函式,在編譯過程中就確定原始碼中的函式呼叫解釋為特定的程式碼塊,稱為靜態聯編。
然而使用了虛擬函式,使用哪個函式是不能在編譯時確定的,因為編譯器不確定使用者將使用哪種型別的物件,所以編譯器必須生成能夠在程式執行時選擇正確的虛擬函式方法的程式碼,即動態聯編。
虛擬函式工作原理:
編譯器處理虛擬函式的方法是:給每個物件新增一個隱藏成員,隱藏成員中儲存一個指向函式地址陣列的指標(vptr),存放虛擬函式的地址陣列即虛擬函式表(virtual function table,vtbl)。當然,vtbl是該類所有物件共有的(畢竟該類所有的函式地址都是一樣的,沒必要每個物件都儲存一張表,完全可以公用),但是vptr必須是每個物件都有。
基類對物件包含一個指標,該指標指向基類中所有虛擬函式的地址表。派生類物件將包含一個指向獨立地址表的指標:如果派生類提供了虛擬函式的新定義,該虛擬函式表將儲存新函式的地址;如果沒有重新定義虛擬函式,將儲存函式的原始版本的地址;如果派生類定義了新的虛擬函式,則將該函式的地址加到派生類的虛擬函式表中。
呼叫虛擬函式時,程式將檢視儲存在物件中的vtbl地址(難道是公有成員?)然後轉向相應的函式地址表。通過vtbl就達到了跟蹤基類指標或引用指向的物件型別。
小結:使用虛擬函式時,在記憶體和執行效率方面有一定成本。非虛擬函式效率比虛擬函式高,但是不具備動態聯編功能。
- 每個物件都將增大,增大的量為儲存地址的空間
- 對於每一個類,編譯器都將建立一個虛擬函式地址表(陣列)
- 對於每個虛擬函式的呼叫,都將執行一項額外的操作,即到vtbl中查詢函式地址。
使用虛擬函式的注意事項
-
1建構函式:
建構函式不能是虛擬函式,和一般的成員函式不一樣。派生類物件建立時呼叫的是派生類的建構函式,而不是基類的建構函式。派生類不繼承基類的建構函式,因此將基類的建構函式宣告為虛擬函式是沒有意義的。
(還有一個說法有待考證:虛擬函式表是呼叫建構函式是建立的,虛擬函式表存放的是該類的虛擬函式的地址,如果建構函式是虛擬函式,那麼虛擬函式表中就會有建構函式的地址,當我們呼叫建構函式時,事先根據vptr找到vtbl中虛建構函式的地址,執行建構函式,這和虛擬函式表是在執行建構函式是建立的相矛盾了)(啊啊啊啊!!!虛擬函式表是在編譯期就建立了,各個虛擬函式這時被組織成了一個虛擬函式的入口地址的陣列.而物件的隱藏成員–虛擬函式表指標是在執行期–也就是建構函式被呼叫時進行初始化的,這是實現多型的關鍵。) -
解構函式
為保證派生類能夠正確順序的析構,解構函式應當是虛擬函式。 -
友元
友元不能是虛擬函式,因為友元不是類成員,而只有類成員可以是虛擬函式。但是友元函式可以使用虛成員函式。 -
重新定義將隱藏方法
重新定義繼承的方法並不是過載,這將隱藏基類中的同名方法,而不論引數列表是否相同。
因此派生類中重新定義了基類的同名方法,應保持和原來基類的函式原型完全一致,但如果函式的返回值是基類的指標或者引用,則可以修改成指向派生類的指標或者引用(例外),允許返回型別隨類型別的變化而變化。
如果基類的方法被過載了,最好在派生類中重新定義這些基類方法,否者未重新定義的方法將被隱藏。
純虛擬函式
純虛擬函式的格式:(宣告的結尾處為=0)
virtual void test()=0;
純虛擬函式提供未實現的方法,包含純虛擬函式的類稱為抽象基類(Abstruct Base Class,簡稱ABC)。不能建立抽象基類的物件(已經很抽象了,還想有物件)。
可以使用抽象基類的指標或者引用管理其派生類物件。
純虛方法用於定義派生類的通用介面。