您好,登錄后才能下訂單哦!
前言
最近在看Scott Meyers大神的《Effective C++》和《More Effective C++》,雖然這兩本書都是古董級的教參了(當然針對C++11/C++14作者所更新的《Modern Effective C++》英文已經發售了,不過還沒中文翻譯版本),但是現在看來仍然收益匪淺,而且隨著對這個復雜語言了解的深入和實踐項目經驗的增加,很多東西和作者產生了一種共鳴,以前種種疑惑突然有種撥云霧而見天日、豁然開朗的感覺,也難怪被列為合格C++程序員之必讀書目。其實C++確實是個可怕的語言,于是市面上針對這個語言的教參也是聆郎滿目層出不窮,當然水平也是參差不齊,像上面所說的Meyers三部曲能夠歷久彌新,也凸顯了這些經典教參的真正價值。
至于最近回歸C++本質,主要是覺得現在后臺開發的RPC、MQ、分布式系統雖然被稱的神乎其神的,但是作為成熟的組件絕大多數公司都可以是直接拿來主義,當然也不可否認其使用經驗的可貴,因為最近線上使用這些組件還是遇到或多或少不少問題的,以后可以少走些坑,然而這種東西也是可遇難求的;反而C++語言本身的使用占用了程序員絕大多數的工作內容,從而直接影響到項目的質量和后續的可維護性。在此,侯捷老師的 勿在浮沙筑高臺 仍如警世名言響徹在耳,一個合格的程序員其扎實的基本功是多么重要。
C++面向對象的東西太多了:public、protected、private訪問和繼承,virtual和多態、多繼承,外加const、缺省參數、名字查找等,光這些元素的排列組合就可以導出很多種情況,看似靈活多變,但不是每種情況都值得去嘗試的。
一、public繼承
public繼承意味著是”is-a”的關系,每個派生類型對象也是一個基類類型對象,基類支持的操作派生類都支持,只不過派生類比基類更具體化一些而已,否則的話應該將派生類不支持的特性給踢出去,比如:
class Bird { ... }; class FlyingBird: public Bird { public: virtual void fly(); ... }; class Penguin: public Bird { ... };
所以,總體來說public繼承是相對比較嚴格的契約關系。當然public繼承是一個比較籠統的概念,細分下來還包括接口繼承、實現繼承、接口和實現繼承。
如果基類聲明了一個pure virtual函數,則其目的是讓派生類只繼承該函數接口;如果基類聲明了一個impure virtual函數,就是讓派生類繼承該函數的接口和其缺省實現;如果某個成員函數是non-virtual函數,則意味著它不打算在派生類中有不同的行為,即派生類繼承該函數接口及一份強制性實現。
對于pure virtual函數的接口聲明,基本沒有什么意義,而non-virtual成員也顯而易見。不過對于impure virtual虛函數,看似提供了缺省實現使用起來會比較方便,而且派生類可以覆蓋其實現也比較靈活,但是如果直接使用這種方式,那么如果基類產生了新的派生類,但是恰好派生類忘記對這個impure virtual函數進行override,而其缺省實現又不滿足新派生類的行為,那么新派生類對象的調用將會引發問題。所以如果想繼承接口,同時又提供缺省實現,那么比較好的方式是將這兩個功能進行分離,用一個pure virtual函數提供接口,再用一個non-virtual protected函數提供缺省實現,而讓派生類手動確認是否使用該默認行為。
class Airplane { public: virtual void fly(const Airport& dest) = 0; protected: void defaultFly(const Airport& dest){ ... } };
除了上面的方式處理impure virtual的缺省實現,其實也可以將其轉換為:仍然使用pure virtual函數聲明接口,不同同時也提供其缺省定義,這樣派生類在override這個pure virtual接口的時候既可以完全重新定義fly的行為,也可以直接一條語句用基類名字直接調用基類的缺省實現(Airplane::fly),其好處是不用引入一個新的函數名字,缺點是缺省實現成了public的了。
說到此處,應該對C++中接口繼承的行為得以了解了。
二、虛函數外的其他選擇
前面我們說到了《C++之virtual函數訪問性》中談及了NVI手法,算是對public virtual的一個強有力的替換工具,不過我們知道其本身也用到了虛函數。虛函數具有運行時開銷,而且其實現也是編譯時間確定運行時候選擇,在有些情況下其靈活性還是受限。
相比于虛函數依據派生類型進行行為的定制化之外,Strategy策略模式顯得更為的靈活。通過在對象內部保存函數指針(或者更泛化的boost::function
函數對象),其行為可以依據具體對象差異化而非派生類型差異化,甚至通過Set接口其行為還可以在運行時候進行變更。雖然Meyers說明如果使用非成員函數,默認將不能訪問類的私有成員,否則就需要對封裝性進行一定程度的妥協松懈,但是通過boost::function+boost::bind
這個強有力的工具,使用繼承體系中的成員函數也是十分方便的。
此處本人感覺,雖然設計模式被奉為C++開發的經典,但是隨著Modern C++在標準上引入更多的特性和功能,C++的開發將必定變的更加友好直觀,也不被過于墨守那古典23式了,畢竟絕大多數的設計模式都通過繼承來實現的,不可避免的增加了程序開發和維護的復雜度。
三、繼承體系來的其他問題
好了,輕松愉快的東西結束了,下面是C++史上的黑暗時刻了。
3.1 繼承而來名字的可見性
C++具有一套名字查找的規則,總體來說就是從局部到外圍,從派生類到基類,從內層名字空間到全局名字空間的查找順序。
由于到此為止我們沒有說明函數重載的情況,所以你此時仍然安之若素:對于public non-virtual函數我們不去重寫,對于virtual函數我們可以override,這一切安好,但是一旦考慮到相同函數名的重載問題,C++有一套理論就會讓你暈乎了:C++防止在應用程序庫或者應用框架中建立的新的派生類被附帶從疏遠的基類中繼承而來的重載函數,所以在繼承的時候C++不會將基類的名字自動導入到派生類中。
好了,這就說明,之前繼承而來的接口,其實也是在使用的時候在派生類作用域中沒有找到該符號,而在基類中找到該符號后滿足調用的,而如果你在基類中定義了其某個重載版本(無論是virtual還是non-virtual)的時候,C++在名字查找的時候就在你的派生類作用域中找到該名字了,然后進行類型檢查和重載,但是重載的版本只限于在派生類中出現的版本,基類的版本不參與重載!!!
所以,在派生類中想增加還是改寫無論virtual還是non-virtual函數的重載版本,第一件事是使用using聲明將基類符號的所有版本聲明到派生的名字作用域中,然后再干其他的。
3.2 絕不重新定義繼承而來的non-virtual函數
C++的non-virtual函數都是編譯期靜態綁定的,其名字查找從其指針的靜態類型開始。
任何情況下,都不要重新定義一個繼承而來的non-virtual函數,否則其調用的版本決定于其指針靜態類型,這與public繼承is-a的一致性關系相互違背。
3.3 絕不重新定義繼承來的缺省參數值
因為上面說到我們不應該重新定義一個繼承而來的non-virtual函數,所以到這里我們可以說:絕對不要重新定義一個繼承而來帶缺省參數值的virtual函數的參數默認值。其原因是:virtual函數是動態綁定的,而缺省參數是靜態綁定的。
所以如果基類和派生類的參數默認值不一致,則使用引用、指針調用發生參數默認值靜態綁定和調用函數體動態綁定將會非常的詭異,所以需要避免這種情況。還有就是如果虛函數參數再基類指定的參數缺省值,而派生類override的時候沒有指明參數缺省值,此時如果客戶端以派生類對象方式調用該函數,則發生的是靜態綁定,需要顯示指定參數值;而如果客戶端以指針、引用的新式調用該函數,則發生的是動態綁定,可以不指定其帶有缺省值的參數。
class Shape { public: virtual void draw(ShapeColor color = Red) const = 0; ... }; class Circle: public Shape { // 如果以對象模式調用draw,必須指定color參數而不能使用缺省參數 public: virtual void draw(ShapeColor color) const; ... };
解決這個問題的一個方式是使用NVI手法,其public non-virtual接口提供默認默認值(且不會被派生類重寫),而private virtual不使用默認默認的特性以規避這種可能的不一致性。
3.4 private繼承
private繼承沒有”is-a”的契約關系了,在使用上一個巨大的差異是:編譯器不再會自動將一個派生類對象轉換為一個基類對象了,這意味著原本接收基類對象的函數參數將不再能夠為其傳遞派生類對象作為實參了(對象、引用、指針類型都不允許,編譯器會報基類S是派生類T不可訪問的基類);同時由基類繼承而來的所有成員,在派生類中都會變成private的訪問權限。
private繼承意味著只有實現部分被繼承,接口部分被全部略去了,所以private繼承應當是采用基類的某些功能幫助派生類完善其功能,從某種情況下說具有”has-a”的符合類型,所以除了考慮到派生類需要訪問基類protected成員和virtual的因素被牽扯進來,否則應該盡量使用組合類型來代替private繼承,而且即使如此,也可以使用下面的手法瞞天過海:
class Timer { public: virtual void onTick() const; ... }; class Widget { private: class WidgetTimer: public Timer { public: virtual void OnTick() const; ... }; WidgetTimer timer; };
關于protected繼承,連Meyers大神都沒用過,那么我又何必廢腦經去考慮他……
參考
Effective C++
總結
以上就是這篇文章的全部內容了,希望本文的內容對大家的學習或者工作能帶來一定的幫助,如果有疑問大家可以留言交流,謝謝大家對億速云的支持。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。