virtual
的函數是基類期待派生類重新定義的,基類希望派生類繼承的函數不能定義為虛函數。通過基類的引用(或指針)調用虛函數時,發生動態綁定
。引用(或指針)既可以指向基類對象也可以指向派生類對象
,這一事實是動態綁定的關鍵。用引用(或指針)調用的虛函數在運行時確定,被調用的函數是引用(或指針)所指對象的實際類型所定義的。virtual
的目的是啟用動態綁定。成員默認為非虛函數
,對非虛函數的調用在編譯時確定。除了構造函數之外,任意非static成員函數都可以是虛函數
。虛函數的聲明必須與基類中的定義方式完全匹配
,
但有一個例外:返回對基類型的引用(或指針)的虛函數。派生類中的虛函數可以返回基類函數所返回類型的派生類的引用(或指針)。
虛函數的成員函數才能進行動態綁定
基類類型的引用或指針進行函數調用
編譯器都將它當作基類類型對象
。靜態類型
(在編譯時可知的引用類型或指針類型)和動態類型
(指針或引用所綁定的對象的類型這是僅在運行時可知的)可能不同。覆蓋虛函數機制
虛函數與默認實參
編譯時確定
。類型定義,與對象的動態類型無關
。通過基類的引用或指針調用虛函數時,默認實參為在基類虛函數聲明中指定的值,如果通過派生類的指針或引用調用虛函數,則默認實參是在派生類的版本中聲明的值
。
為什麼會希望覆蓋虛函數機制?
最常見的理由是為了派生類虛函數調用基類中的版本。在這種情況下,基類版本可以完成繼承層次中所有類型的公共任務,而每個派生類型只添加自己的特殊工作。
例如,可以定義一個具有虛操作的Camera類層次。Camera類中的display函數可以顯示所有的公共信息,派生類(如PerspectiveCamera)可能既需要顯示公共信息又需要顯示自己的獨特信息。可以顯式調用Camera版本以顯示公共信息,而不是在PerspectiveCamera的display實現中復制Camera的操作。 在這種情況下,已經確切知道調用哪個實例,因此,不需要通過虛函數機制。
基類指針
,則需要運行基類析構函數並清除基類的成員,如果對象實際是派生類型的,則沒有定義該行為。
要保證運行適當的析構函數,基類中的析構函數必須為虛函數
。[Code2]無論派生類顯式定義析構函數還是使用合成析構函數,派生類析構函數都是虛函數。
在復制控制成員中,只有析構函數應定義為虛函數,構造函數不能定義為虛函數。
構造函數是在對象完全構造之前運行的,在構造函數運行的時候,對象的動態類型還不完整。派生類中的賦值操作符有一個與類本身類型相同的形參
,該類型必須不同於繼承層次中任意其他類的賦值操作符的形參類型。盡量不要在構造函數和析構函數中調用虛函數
,因為構造或析構期間的對象類型對虛函數的綁定有影響。
基類構造函數或析構函數中
,將派生類對象當作基類類型對象
對待。
構造函數或析構函數自身類型定義的版本
。
假定找到了名字,編譯器就檢查實參是否與形參匹配。
虛函數必須在基類和派生類中擁有同一原型
。如果基類成員與派生類成員接受的實參不同,就沒有辦法通過基類類型的引用或指針調用派生類函數。[Code3]名字查找與繼承
一個或多個純虛函數
的類是抽象基類。除了作為抽象基類的派生類的對象的組成部分,不能創建抽象類型的對象
。在函數形參表後面寫上
= 0
,double net_price(std::size_t)
const = 0;
共享的基類子對象
。共享的基類子對象稱為虛基類。[Code5]引起更少的二義性問題
Item_base *baseP = &derived; //calls version from the base class regardless of the dynamic type of baseP double d = baseP->Item_base::net_price(42);
/*如果析構函數為虛函數,那麼通過指針調用時,運行哪個析構函數將因指針所指對象類型的不同而不同:*/ Item_base *itemP = new Item_base; // same static and dynamic type delete itemP; // ok: destructor for Item_base called itemP = new Bulk_item; // ok: static and dynamic types differ delete itemP; // ok: destructor for Bulk_item called
class Base { public: virtual int fcn(); }; class D1 : public Base { public: // hides fcn in the base; this fcn is not virtual int fcn(int); // parameter list differs from fcn in Base // D1 inherits definition of Base::fcn() }; class D2 : public D1 { public: int fcn(int); // nonvirtual function hides D1::fcn(int) int fcn(); // redefines virtual fcn from Base }; /*D1 中的 fcn 版本沒有重定義 Base 的虛函數 fcn,相反,它屏蔽了基類的 fcn。結果 D1 有兩個名 為 fcn 的函數:類從 Base 繼承了一個名為 fcn 的虛 函數,類又定義了自己的名為 fcn 的非虛成員函 數,該函數接受一個 int 形參。 但是,從 Base 繼承的虛函數不能通過 D1 對象(或 D1 的引用或指針) 調用, 因為該函數被 fcn(int) 的定義屏蔽了。 類 D2 重定義了它繼承的兩個函數,它重定義了 Base 中定義的 fcn 的原始版本並重定義了 D1 中定義 的非虛版本。*/
Base bobj; D1 d1obj; D2 d2obj; Base *bp1 = &bobj, *bp2 = &d1obj, *bp3 = &d2obj; bp1->fcn(); bp2->fcn(); bp3->fcn(); // ok: virtual call, will call Base::fcnat run time // ok: virtual call, will call Base::fcnat run time // ok: virtual call, will call D2::fcnat run time 三個指針都是基類類型的指針,因此通過在 Base 中查找 fcn 來確定這三 個調用,所以這些調用是合法 的。另外,因為 fcn 是虛函數,所以編譯器會生成代碼,在運行時基於引用指針所綁定的對象的實際類型進 行調用。在 bp2 的情況,基本對象是 D1 類的,D1 類沒有重定義不接受實參的虛函數版本,通過 bp2 的 函數調用(在運行時)調用 Base 中定義的版本。
/*每個 IO 庫類都繼承了一個共同的抽象基類,那個抽象基類管理流的條件狀態並保存流所讀寫的緩沖區。 istream 和 ostream 類直接繼承這個公共基類,庫定義了另一個名為 iostream 的類,它同時繼承 istream 和 ostream,iostream 類既可以對流進行讀又可以對流進行寫。 我們知道,多重繼承的類從它的每個父類繼承狀態和動作,如果 IO 類 型使用常規繼承,則每個 iostream 對象可能包含兩個 ios 子對象:一個包含 在它的 istream 子對象中,另一個包含在它的 ostream 子對 象中,從設計角度講,這個實現正是錯誤的:iostream 類想要對單個緩沖區進行讀和寫,它希望跨越輸入和輸 出操作符共享條件狀態。如果有兩個單獨的 ios 對象,這種共享是不可能的。 在 C++ 中,通過使用虛繼承解決這類問題。*/ class istream : public virtual ios { ... }; class ostream : virtual public ios { ... }; // iostream inherits only one copy of its ios base class class iostream: public istream, public ostream { ... };