日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > c/c++ >内容正文

c/c++

C++继承和组合——带你读懂接口和mixin,实现多功能自由组合

發布時間:2024/8/23 c/c++ 48 豆豆
生活随笔 收集整理的這篇文章主要介紹了 C++继承和组合——带你读懂接口和mixin,实现多功能自由组合 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

摘要:?本文詳細介紹了C++繼承的三種方式和相關重要概念,整理了眾多繼承與組合中的注意問題。在C++繼承存在不安全的默認實現,非虛函數的覆蓋,多重繼承的函數名沖突、菱形繼承等眾多問題下,如何實現多個功能的自由組合?阿里云高級開發工程師采用mixin,為大家提供了更好擴展性和更高代碼復用度的解決方案

摘要:本文詳細介紹了C++繼承的三種方式和相關重要概念,整理了眾多繼承與組合中的注意問題。在C++繼承存在不安全的默認實現,非虛函數的覆蓋,多重繼承的函數名沖突、菱形繼承等眾多問題下,如何實現多個功能的自由組合?阿里云高級開發工程師采用mixin,為大家提供了更好擴展性和更高代碼復用度的解決方案。
數十款阿里云產品限時折扣中,趕緊點擊這里,領劵開始云上實踐吧!

本次直播視頻精彩回顧,戳這里!?
演講嘉賓簡介:付哲(花名:行簡),阿里云高級開發工程師,哈爾濱工業大學微電子學碩士,主攻方向為分布式存儲與高性能服務器編程,目前就職于阿里云表格存儲團隊,負責后端開發。
以下內容根據演講嘉賓視頻分享以及PPT整理而成。
本文將圍繞一下幾個方面進行介紹:1. C++繼承方式2. 繼承相關重要概念及注意問題3. 問題及解決:如何組合正交的多個功能
一. C++繼承方式C++有三種繼承方式:public/protected/private,這三種繼承方式中,派生類都會繼承基類的public和protected成員,但無法直接訪問基類的private成員,只能通過繼承后的方法來訪問。如圖所示一個簡單的示例:
在本例中,Base為基類,包含public/protected/private三種成員,其中public和protected成員可以被派生類繼承,而mName不可以被派生類直接訪問,只可以通過繼承后的函數F(),G(),H(),I()來訪問。
1. Public繼承采用public繼承方式時,基類的成員在派生類中的訪問級別與基類中一致,即public成員仍是public級別,protected成員仍是protected級別。如對上例中Base進行public繼承,得到下派生類:
其中F()為純虛函數,派生類只繼承到函數的接口,需要再進行具體實現;G()為虛函數,派生類同時繼承了接口和實現;H()為public方法,有實現但不為虛函數,無法在調用指針時觸發多態,該派生類繼承了接口和強制的實現,這是不能改寫的;I()是protected方法,它不是基類的接口,因此派生類只繼承了它的實現。此時,該派生類可以作為一個基類對象使用,例如上圖中創建派生類對象用于兩個函數中,此時派生類引用/指針可轉換為基類引用/指針。
2. Protected繼承protected繼承與public繼承的不同在于,基類的成員在派生類中的訪問級別的改變。public和protected成員都成為protected級別。此時派生類接口不包含基類的接口,因此protected繼承不是is-a的關系。繼承后成員如下圖所示:

3. Private繼承private繼承中,基類的public和protected成員都成為private成員。和protected繼承類似,派生類接口不包含基類的接口,因此private繼承也不是is-a的關系。同時派生類引用/指針不可轉換為基類引用/指針。此外,由于此時派生類成員都為private,那么后續派生類型再也無法繼承該類型。對上例中Base進行private繼承,如下圖所示:
那么綜上所述,C++的繼承方式中:public繼承包括基類的接口與實現;protected繼承只包括基類的實現,且可繼續傳遞;private繼承只包括基類的實現,且不可繼承傳遞。這里值得注意的是,派生類無法繼承基類private成員,這是指派生類無法直接訪問,即基類private成員對派生類對象不可見,但在內存布局中是包含這些private成員的,且派生類的構造、析構、復制也會受到這些private成員影響。例如假設基類中有引用private成員,這不僅導致基類方法無法進行復制和移動,也同樣會導致派生類的無法復制和移動。
二. 相關重要概念1. 純虛函數與抽象類純虛函數是聲明為等于0的虛函數,具體如下圖所示:
此處0填充在虛表中,這會導致純虛函數的虛表為0項,即無法創建虛表,無法實例化。包含純虛函數的類稱為抽象類,此處Base即為一個抽象類。但這里需要注意的是,純虛函數不等于無定義的虛函數。如果這里將圖中的等于0去掉,即F() = 0改為F(),那么Base類是無法派生的,派生類會報錯F()無法使用。
2. 接口繼承與實現繼承接口是類與外界的通信協議,是抽象的;實現是類對協議的反應,是具體的。當稱派生類繼承了基類的某接口時,表示派生類對外的協議中也包含了基類對外的協議,調用該接口時,派生類對象就會被當做基類對象使用。當稱派生類繼承了基類的某實現時,指派生類可以調用基類的某種行為。與Java和C#不同的是,無論是繼承接口還是實現,C++中只有一種繼承語法,并且如上所述,public繼承包括基類的接口與實現,protected和private繼承只包括基類的實現,不包含接口。例如下圖中:
當派生類繼承基類public方法時,draw()方法為純虛函數,只能繼承到接口,即派生類必須改寫該函數,否則不能實例化對象;error()方法為虛函數,繼承到接口與默認實現,即若派生類改寫了該函數,那么該函數就失效了,若派生類未改寫,則可以直接使用該函數;id()方法為非虛函數,繼承到接口與強制實現,即派生類無法改寫該方法。
3. 安全的默認實現大家可能覺得派生類需要給每個純虛函數進行實現,太過繁瑣,虛函數效果更佳。若該純虛函數較為通用,可能在派生類中需要重寫多遍,而虛函數提供了一個默認實現,只需在需要時改寫即可。但請注意,這其實是非常危險的。因為在編寫基類時是不知道未來將會產生哪些派生類,默認實現不一定適用所有派生類。例如下例中:
ModelA和ModelB都可以使用基類的默認實現,但后續加入了不能使用基類的默認實現的ModelC,此時理應為ModelC改寫該實現但被編程人員遺忘了。編譯時由于存在基類的默認實現,因此不會報錯。運行初期可能不會出現問題,但會為后續埋下非常危險的隱患。因此嚴格的代碼規范中禁止虛函數提供默認實現,即必須使用純虛函數。但虛函數可以避免代碼的重復,因此仍存在一定的價值。實際運用中,存在很多派生類需要使用默認實現,那么如何強制要求派生類顯式地使用每個接口,又可以為派生類準備一個可調用的默認實現呢?一種方法就是為純虛函數提供定義。這種定義不會存入虛表,因此基類本身仍然是抽象類,并且派生類仍然需要提供一個顯式實現。但更簡便的方法是在派生類中調用基類的實現,注意此處不能使用虛函數,而是直接使用函數名調用。如下圖示例所示:
如此,需要使用默認實現時只需簡單調用該函數;而不需要使用默認實現時,若程序員忘記編寫具體實現,編譯時便會報錯,強制要求提供函數實現。因此,便可以避免上述默認實現的隱患。
4. 純接口繼承與接口類純接口繼承是指基類只提供接口,不提供定義,即嚴格代碼規范下,基類的所有函數都是純虛函數,不提供具體實現,派生類需要對所有方法進行自定義,這樣的類型稱為純接口類。純接口繼承完全分離了接口與實現,依賴更少,如下例所示:
這樣的接口類有以下三個特點:一,沒有非靜態成員變量;二,所有成員都是public成員;三,所有成員都是純虛函數,析構函數除外,因此在上例Interface類中存在一個有定義的虛的析構函數。純接口繼承的優點是最小化調用處的依賴,且接口與實現完全分離,這樣在只有實現發生變化時,調用處不會受到任何影響。而它的缺點是不利于代碼復用,如果多個派生類都要實現相差不多的方法F(),就需要重復編寫多遍F()的代碼。
5. 確保接口繼承是“is-a”關系在實行接口繼承時,需要確保接口繼承是“is-a”關系。當派生類以public方式繼承一個基類時,它也繼承了這個基類的所有接口,那么所有使用基類接口處都可以使用派生類。從這個角度說,派生類對象是一個(“is-a”)基類對象。這也是基類接口對派生類的約束,派生類需要嚴格保證其所有行為都符合基類接口的要求。但有時派生類并沒有達到這一要求,如下經典示例所示:
本例中,基類Bird包含接口fly,正如大家理解,鳥都會飛。Penguin繼承自Bird,如果采用默認實現那么Penguin也要繼承接口fly。但大家知道企鵝不會飛,也就意味著它不能有fly接口。如果這里給Penguin一個空的fly方法,雖然可實行,但這是不合常理的,必須要向使用者顯示Penguin是不含有fly行為的。如果此處不給出具體實現的話,只有在運行時才會拋出異常,這種意外的異常也是不友好的。這個問題其實源于對基類接口的設計。當已知不是所有鳥都會飛之后,那么便不應該給Bird類一個fly接口,基類的這種接口是一種不合理的強加的約束。一種解決方法是中間再加一個層次:Bird類本身是沒有接口的,而Bird的派生類FlyingBird才會提供fly接口,那么Penguin便繼承Bird類,而不是繼承FlyingBird類。如此若調用Penguin類的fly接口,編譯時就會報錯,便可以防止上述問題的發生。另一個示例為矩形的實現,如下所示:
長方形類中有一提前假設,即它的長和寬是獨立的,改變其中一個值,另一個值不會隨之改變。makeBigger就體現了這種假設,這也是對所有長方形的派生類的要求。但正方形卻不滿足這個要求,它的長和寬必須是相等的。因此正方形根本不應該是長方形的派生類,這種繼承是錯誤的。由此可見,C++中的繼承比現實中的繼承更加嚴格,需要編程人員謹慎的選擇基類與派生類,任何適用于基類的性質都需要適用于派生類。存在任何不滿足“is a”關系的繼承都是不合理的。
6. 不要覆蓋基類的非虛函數當派生類public繼承一個有非虛函數的基類時,派生類也會繼承這個非虛函數,并且是繼承了強制實現。然而與虛函數不同的是,派生類沒辦法改寫這個函數,相反,如果自定義編寫一個同名函數,基類的版本就被“覆蓋了”,如下例所示:
派生類和基類中都有函數F(),但此處不是改寫而是覆蓋,基類接口被覆蓋會導致調用產生的行為不一致。直接通過派生類對象調用F()與通過基類指針調用F(),會產生不一樣的行為!這種不一致就表明派生類與基類不再是is a的關系。因此,不要覆蓋基類的非虛函數。
7. 實現繼承與組合當派生類以protected或private繼承一個基類時,派生類沒有繼承到基類的接口,而是繼承到了基類的實現。這種方式被稱為實現繼承。實現繼承意味著派生類與基類不是is-a的關系,而只是需要復用其實現或功能。例如,若有一個類Password,它需要使用std::string功能,那么一種解決方法就是讓它繼承自std::string,如圖所示:
此時Password類便可以直接調用string方法。但string類本身并沒有設計為可以成為一個基類,可能存在其析構函數不是虛函數,那么使用基類指針指向析構函數時,會忽略派生類對象中的內容。但這里還有另一種選擇,那就是將std::string變成Password的一個成員,而不是Password的基類,這樣仍能使用std::string的各種功能,且不需要增加一種繼承關系。這種方法被稱為“組合”,它是比繼承更靈活的復用方法。一般在可以用組合達到目的時,要盡量避免使用實現繼承。然而,在某些場景下,實現繼承有它獨特的用途。一是在改寫基類的某些功能時,如下例所示:
當Widget只需要復用Timer的其他功能,但不需要Timer的onTick()時,就可以使用private繼承Timer,然后改寫onTick()函數,這是對象組合無法輕易完成的。當然該場景下,結合內部類,組合也可以達到類似效果。用內部類private繼承Timer,與Widget構成組合關系,如此來避免Widget直接繼承Timer。第二種場景是當基類是空類型時,繼承可以應用到空基類優化,在派生類中不占空間,而對象組合則沒有這種優化,空類型成員至少要占用一字節的空間,一般會在八字節及以上。該場景最經典的例子是boost::noncopyable,無論是private繼承,還是將其作為成員變量,都可以令自定義類型無法復制,但private繼承時,因為該基類是空類型,因此在派生類中不占空間。實現繼承的該特性在標準類型庫中被廣泛使用。
8. 多重繼承的問題C++允許一個派生類繼承自多個基類,但逐漸大家意識到這種自由會導致一些棘手的問題,因此Java等語言取消了這種特性,而是派生類只允許有一個基類。多重繼承會導致以下兩個問題。一是不同基類間的名字沖突或者歧義,如下例所示:
此處有A和B兩基類,C同時繼承A和B。雖然A類中的Func()是public函數,B中的func()是private函數,但調用C類的func()函數時,它理應調用A的func(),但C++的名字查找規則是優先于訪問級別檢查的,因此先查找名字,進行重載決議,再檢查訪問級別,而在重載決議時編譯器檢查到了兩個相同優先級的func(),這就產生了沖突。解決這個問題就需要顯式調用某個基類的版本,指定調用函數的namespace:
第二個問題是菱形繼承問題,這比上述問題更加棘手。當語言允許多繼承時,一個基類可能會多次出現在同一個派生類的基類樹中,如下例所示:
InputFile和OutputFile同時繼承于File類,而下一層IOFile類同時繼承InputFile和OutputFile,即繼承了兩次File,這就是菱形繼承。當出現菱形繼承時,意味著派生類對象中有多個相同類型的基類子對象,此時調用該基類方法時會產生如何選擇子對象的混亂。并且有些屬性對派生類是唯一的,比如File屬性,在IOFile中只應有一份。為了解決這個問題,C++增加了虛繼承,虛繼承的基類在派生類中只會有一份。但虛繼承被認為是比多重繼承更糟糕的特性,它比虛函數的開銷更大,且反直覺地要求最終派生類型的構造函數來構造整個繼承鏈條中所有虛繼承的基類。因此,為了避免菱形繼承,一些編程規范規定:不能使用虛繼承;盡量不要使用多重繼承;如果要用多重繼承,盡量模仿Java語言,至多只能有一個基類有實現,其它基類都是接口類。
三. 問題及解決:如何組合正交的多個功能假設有若干個彼此獨立,或說正交的功能,如何將它們組合起來?例如有一個TaskManager類,負責管理所有擁有ITask接口的對象,如下所示:
現在需要為ITask類型增加兩個功能:一是timing功能,即在ITask對象執行Execute方法前后計時;二是logging功能,即在ITask對象對待Execute方法前后打印日志。那這該如何解決呢?
1. 繼承第一種方式是通過繼承復用功能。具體如下所示:
從ITask類派生出一個ILoggingTask類,它增加OnExecute()接口,其派生類只要實現這個接口,在調用ITask::Execute時就能打印日志了。同樣的方法,這里也通過增加ITimingTask類實現timing的功能:
然而當需要同時復用timing和logging該如何解決呢?假設使用上述方法,那么ITimingTask的Execute()與ILoggingTask的Execute()是沖突的,無法同時復用兩個功能。這就體現了通過繼承來復用代碼的缺陷,對于單個功能,可以將需要復用的實現代碼放在基類中,但如果需要同時復用多個功能,通過繼承復用功能就無法解決了。另外,本例中因為增加了一層虛函數,而且還是在虛函數中調用另一個虛函數,這就導致編譯器無法inline代碼,從而增加運行期的開銷。
2. 組合第二種方式是通過組合復用功能。具體如下所示:
這里LoggingTask不再作為基類存在,而是作為代理,把對LoggingTask的請求轉發給它持有的task成員,由task來解決請求。同樣地,這里也增加一個TimingTask類:
接下來就可以通過鏈式傳遞來組合這兩個功能。
通過組合來復用功能仍然也存在一些問題。一是這種方法依然有一些運行期的開銷,比如需要在堆上分配每個對象,多次調用虛函數。但它解決了組合多個功能的問題,不同功能間也耦合較低。二是LonggingTask需要實現一些不用的接口。像LoggingTask這樣純粹的功能,本是不需要實現GetName這樣的接口,但它繼承自ITask,就需要實現ITask所有的接口。假如ITask還有其它接口,LoggingTask也都需要實現,這就增加了代碼的復雜度,使得該模塊特別臃腫。那么這該如何解決呢?
3. 重返繼承這次仍然嘗試用繼承來解決該問題,但與上述第一種繼承方法相比,做出一些變化。前面的方法中增加了兩個基類,且把需要復用的部分放在基類中。而這里把需要復用的部分放在派生類中:
這里將MyTask作為基類,TimingTask中繼承MyTask來執行計時的操作。而LoggingTask繼承TimingTask,如此LoggingTask便同時具有計時和打印兩個功能。但這種方法仍然有很多缺陷:一是不同功能之間因為繼承完全耦合在一起,功能之間無法分割;二是這兩個功能綁定在MyTask類中,導致這兩個功能完全無法被其它類型復用。雖然有這么多致命缺陷,但這種方法仍然有獨特的優勢:首先,不需要堆分配;其次,沒有多余的虛函數定義及其調用;最后,編譯器有機會做更多內聯優化。那么,該如何解決這個方法的缺點呢?由代碼分析可知,上例中的兩個缺點都是因為基類是固定的,無法變化,如果能用模板將基類作為參數傳遞,上述缺點便解決了。
4. Mixin最后一種方法是通過mixin復用功能。mixin本身是面向對象領域的一個非常寬泛的概念,它是有一系列被稱為mixin的類型,這些類型分別實現一個單獨的功能,且這些功能本身是正交的。當需要使用這些功能時,就可以將不同的mixin組合在一起,像搭積木一樣,完成功能復用。一個更清晰的解釋是這樣的:一個mixin就是類里的一小塊,可以用來與其它類或mixin做組合;一個獨立的類與一個mixin的區別在于,一個mixin只建模小的功能點(如timing或printing),并不是用來獨立使用,而是給其它需要這個功能的類做組合。在C++中最常用的實現mixin的方式叫“參數化模板”。這里可以將TimingTask和LoggingTask的基類都換成模板參數:
此時便可以解決前述所有問題,TimingTask和LoggingTask實現完全不耦合,所有代碼獨立,且所有含有Execute()方法的類型都可以組合這兩個功能。組合過程如下所示:
首先新建MyTask對象,將該對象傳遞進入TimingTask類中,生成對象t1,然后可以將t1傳遞進LoggingTask類中生成對象t2。t2便同時具備Timing和Logging功能。C++可以通過繼承方法來支持mixin,而不像其他語言,如Ruby,顯式的支持mixin。如此使用mixin便能夠實現自由組合多個功能,并且囊括了之前方法的所有優點,有更好的擴展性和更高的代碼復用度。當然這個類并不是最終希望得到的,因為它沒有實現ITask接口。因此仍然可以增加一個新的mixin,來將任意含有Execute和GetName方法的類型適配為ITask的派生類:

本文由云棲志愿小組郭雪整理,編輯百見

原文鏈接

干貨好文,請關注掃描以下二維碼:



總結

以上是生活随笔為你收集整理的C++继承和组合——带你读懂接口和mixin,实现多功能自由组合的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。

主站蜘蛛池模板: www成人| 亚洲色图国产 | 狼人精品一区二区三区在线 | 亚洲乱码一区二区三区在线观看 | 爱爱动态图 | 欧美裸体精品 | 亚洲裸体网站 | 最新毛片网 | julia一区二区 | 日本性爱视频在线观看 | 免费午夜影院 | 日韩高清黄色 | 女人囗交吞精囗述 | 亚洲黄av| 久久久久国产精品一区 | 国产亚洲精品久久久久婷婷瑜伽 | 中国av免费 | 精品久久久久一区二区国产 | 在线精品自拍 | 男女视频在线免费观看 | 国产欧美一区二区精品久久久 | 国产一区二区不卡视频 | 亚洲欧洲在线播放 | 99riav国产| 日日夜夜天天 | 日韩在线视频在线观看 | 好吊色青青草 | 国产精品男女 | 国产精品一区在线免费观看 | 无码国模国产在线观看 | 全黄毛片 | www.色人阁.com | 中文国语毛片高清视频 | 黄色欧美在线 | 欧美日韩中文字幕一区二区 | 中文字幕av久久 | 操屁股视频| www伊人网| 噜噜噜精品欧美成人 | 偷拍综合网| 台湾一级视频 | 在线99视频 | 亚洲最新在线观看 | 综合在线播放 | 一级做a爱片久久毛片 | 深爱激情综合网 | 播播激情网 | 91麻豆精品在线观看 | 国产精品性爱在线 | 风流还珠之乱淫h文 | 777色婷婷| 国产农村乱对白刺激视频 | 国产免费黄色片 | 色偷偷av一区二区三区 | av解说在线观看 | 亚洲视频一区在线播放 | 亚洲最大av | 久久亚洲视频 | 免费在线视频一区二区 | 国产一区二区免费电影 | 久久国产日韩欧美 | 日本在线精品视频 | 视频在线国产 | caoporm超碰 | av国产网站 | 亚洲aaa| 亚洲精品影院在线 | 骚虎视频在线观看 | 中文字幕xxxx | 四虎av| 久久99成人| 欧美在线激情视频 | 风间由美在线视频 | 天天操免费视频 | 女十八毛片 | 天天精品视频 | 8x国产一区二区三区精品推荐 | 国产夜夜嗨 | 狠狠干影视 | 91成人国产综合久久精品 | 香蕉视频色 | 国产ts三人妖大战直男 | 久久综合久久88 | 中文字幕一区二区三区四区不卡 | 波多野结衣调教 | 秋霞一级全黄大片 | 后进极品白嫩翘臀在线视频 | 免费视频亚洲 | 中字幕视频在线永久在线观看免费 | 久久视频免费观看 | 日本一级三级三级三级 | 朝桐光av在线 | 日本涩涩视频 | 中文字幕第六页 | 天天干夜夜 | 黄色一级免费视频 | 成人xxx | 能直接看的av网站 | 色呦呦免费 |