有兩種方法來看待這個問題:理論的方法和實踐的方法。讓我們先從實踐的方法開始。畢竟,理論家一般都很耐心。
假設類d公有繼承於類b,並且類b中定義了乙個公有成員函式mf。mf的引數和返回型別不重要,所以假設都為void。換句話說,我這麼寫:
class b ;
class d: public b ;
甚至對b,d或mf一無所知,也可以定義乙個型別d的物件x,
d x; // x是型別d的乙個物件
那麼,如果發現這麼做:
b *pb = &x; // 得到x的指標
pb->mf(); // 通過指標呼叫mf
和下面這麼做的執行行為不一樣:
d *pd = &x; // 得到x的指標
pd->mf(); // 通過指標呼叫mf
你一定就會感到很驚奇。
因為兩種情況下呼叫的都是物件x的成員函式mf,因為兩種情況下都是相同的函式和相同的物件,所以行為會相同,對嗎?
對,會相同。但,也許不會相同。特別是,如果mf是非虛函式而d又定義了自己的mf版本,行為就不會相同:
class d: public b {
public:
void mf(); // 隱藏了b::mf; 參見條款50
pb->mf(); // 呼叫b::mf
pd->mf(); // 呼叫d::mf
行為的兩面性產生的原因在於,象b::mf和d::mf這樣的非虛函式是靜態繫結的(參見條款38)。這意味著,因為pb被宣告為指向b的指標型別,通過pb呼叫非虛函式時將總是呼叫那些定義在類b中的函式 ---- 即使pb指向的是從b派生的類的物件,如上例所示。
相反,虛函式是動態繫結的(再次參見條款38),因而不會產生這類問題。如果mf是虛函式,通過pb或pd呼叫mf時都將導致呼叫d::mf,因為pb和pd實際上指向的都是型別d的物件。
所以,結論是,如果寫類d時重新定義了從類b繼承而來的非虛函式mf,d的物件就可能表現出精神**症般的異常行為。也就是說,d的物件在mf被呼叫時,行為有可能象b,也有可能象d,決定因素和物件本身沒有一點關係,而是取決於指向它的指標所宣告的型別。引用也會和指標一樣表現出這樣的異常行為。
實踐方面的論據就說這麼多。我知道你現在想知道的是,不能重新定義繼承而來的非虛函式的理論依據是什麼。我很高興解答。
條款35解釋了公有繼承的含義是 "是乙個",條款36說明了為什麼 "在乙個類中宣告乙個非虛函式實際上為這個類建立了一種特殊性上的不變性"。如果將這些分析套用到類b、類d和非虛成員函式b::mf,那麼,
· 適用於b物件的一切也適用於d物件,因為每個d的物件 "是乙個" b的物件。
· b的子類必須同時繼承mf的介面和實現,因為mf在b中是非虛函式。
那麼,如果d重新定義了mf,設計中就會產生矛盾。如果d真的需要實現和b不同的mf,而且每個b的物件 ---- 無論怎麼特殊 ---- 也真的要使用b實現的mf,那麼,每個d將不 "是乙個" b。這種情況下,d不能從b公有繼承。相反,如果d真的必須從b公有繼承,而且d真的需要和b不同的mf的實現,那麼,mf就沒有為b反映出特殊性上的不變性。這種情況下,mf應該是虛函式。最後,如果每個d真的 "是乙個" b,並且如果mf真的為b建立了特殊性上的不變性,那麼,d實際上就不需要重新定義mf,也就決不能這樣做。
不管採用上面的哪一種論據都可以得出這樣的結論:任何條件下都要禁止重新定義繼承而來的非虛函式。
條款37 決不要重新定義繼承而來的非虛函式
class b class d public b 甚至對b,d或mf一無所知,也可以定義乙個型別d的物件x,d x x是型別d的乙個物件 那麼,如果發現這麼做 b pb x 得到x的指標 pb mf 通過指標呼叫mf 和下面這麼做的執行行為不一樣 d pd x 得到x的指標 pd mf 通過指標呼叫...
重新定義繼承而來的非虛函式
在一次應聘過程中,負責技術的招聘人員提出了乙個實際開發中遇到的問題 class base class derive public base 結果編譯卻發現錯誤。他想知道是怎麼回事。當時怎麼看怎麼像函式過載 公有繼承嘛 似乎沒什麼問題呀,只好說不知道。後來終於在 effective c 2nd 中找到...
不要重新定義繼承來的非虛函式
effective c item 36 class b class d public b 對於這個繼承體系,有這樣的 d x b pb x pb foo test b d pd x pd foo test d驚訝的發現,兩個語句的行為不一樣.兩者所呼叫的函式相同,物件也相同,因此行為也應該相同 但是...