一.派生類的默認成員函數
1.14個常見默認成員函數
?默認成員函數,默認的意思就是指我們不寫,編譯器會自動為我們生成一個,那么在派生類中,這幾個成員函數是如何生成的呢?
1.派生類的構造函數必須調用基類的構造函數初始化基類的那一部分成員。如果基類沒有默認的構造函數,則必須在派生類構造函數的初始化列表顯示調用。
2.派生類的拷貝構造函數必須調用基類的拷貝構造完成基類的拷貝初始化。
3.派生類的operator=必須要調用基類的operator=完成基類的復制。需要注意的是派生類的operator=隱藏了基類的operator=,所以顯示調用基類的operator=,需要指定基類作用域
4.派生類的析構函數會在被調用完成后自動調用基類的析構函數清理基類成員。因為這樣才能保證派生類對象先清理派生類成員再清理基類成員的順序。
5.派生類對象初始化先調用基類構造再調派生類構造。
6.派生類對象析構清理先調用派生類析構再調基類的析構。
7.因為多態中一些場景析構函數需要構成重寫,重寫的條件之一是函數名相同。那么編譯器會對析構函數名進行特殊處理,處理成destructor(),所以基類析構函數不加 virtual的情況下,派生類析構函數和基類析構函數構成隱藏關系。
首先先寫一個父類Person,便于下面的講解。
在寫子類的4個默認函數時要把父類成員當成一個整體
首先將其中的構造函數:
正如第一條所言,我們在寫子類的構造函數時,在初始化列表部分要顯示調用父類的構造函數,這里顯式調用的格式就如上圖所示,如果父類還有其他的成員變量,那么一并都寫入父類的構造函數種。
接下來呢時拷貝構造函數,這類有人可能有疑惑:為什么直接傳s就可以呢?
這個就涉及到我們上一篇講的基類和派生類之間的轉化,有印象的朋友就能記起來子類對象可以賦值給父類的引用或指針,本質就是“切片”,而我們上面寫的Person類中的拷貝構造函數正是引用,所以這里直接傳子類對象即可。
接著是=符號重載,這里面要注意的是我們要指定作用域來調用Person類中的=符號重載,至于原因也和上一篇講的知識有關:最后講的隱藏規則。
這里如果不指定作用域,那么子類的=符號重載會將父類的=符號重載給隱藏,這里會一直調用自己,一直遞歸,最終導致棧溢出。
而最后的析構函數呢情況比較復雜,就如上圖所示,按理說應該沒問題的,就和上面的構造函數和拷貝構造函數一樣,那么這里為何為報錯呢?
原因就正如第七條所言,因為都是destructor,所以子類的析構函數把父類的析構函數給隱藏了,所以這里就找不到父類的析構函數。
所以要利用父類的析構函數就要指定作用域,但是析構函數比較特殊,我們在實際應用中不會去顯示調用父類的析構函數,原因就如第六條所言,我們要調用子類的析構函數,再調用父類的析構函數,而這里我們如果在子類析構函數中上去就直接調用父類的析構函數,那么就會使順序顛倒。
并且因為第四條所言,那么就會導致父類的析構函數被調用兩次:
很明顯,是不能這么用的,這肯定會出問題的。所以在實際運用中我們是不需要主動去調用父類的析構函數,這也是析構函數和前面三個默認函數的區別。
4.2實現一個不能被繼承的類
不能被繼承的方式我講解兩種:c++98和c++11兩種不同的方式。
c++98的方法就是將父類的構造函數放在private下,這種方式為什么可以呢?
就如上面講的構造函數,子類構造函數要顯示調用父類的構造函數,而父類的構造函數不能訪問,通過這種機制就導致父類無法被繼承。
當我們創建對象時就會出現這樣的報錯。
而c++11的方法相較于c++98簡單了許多,直接在類名后面加一個關鍵字final,這樣這個類就無法被繼承了,就如上圖所示。
二.繼承和友元
友元關系不能被繼承,也就是說基類友元不能訪問派生類私有和保護成員。
我們以上面的例子為例,在講解之前,注意我上面寫的前置聲明,因為Display函數同時用到了兩個類,而student類還沒有實現,所以就在前面加上一個前置聲明,來告訴編譯器我下面有一個類叫student。
通過上面的例子可以看出,Display并不能訪問子類的保護成員。這就像你父親的朋友不是你的朋友一樣,是一個道理,所以是無法訪問的。
而要想訪問呢就在子類中進行友元聲明即可訪問。
三.繼承與靜態成員
基類定義了startic靜態成員,則整個繼承體系里面只有一個這樣的成員。無論派生出多少個派生類,都只有一個static成員實例。
可以看出如果是非靜態成員,父類和子類_name地址是不一樣的,說明派生類繼承下來了,父類和派生類對象各有一份。
而靜態成員通過檢驗可以看出,父類和子類_count的地址是一樣的,說明父類和子類共用同一份靜態變量。
四.多繼承及菱形繼承問題
4.1繼承模型
單繼承:一個派生類只有一個直接基類時稱這個繼承關系為單繼承
多繼承:一個派生類有兩個或以上直接基類時稱這個繼承關系為多繼承,多繼承對象在內存中的模型是,先繼承的基類在前面,后面繼承的基類在后面,派生類成員在放到最后面。
菱形繼承:菱形繼承是多繼承的一種特殊情況。菱形繼承的問題,從下面的對象成員模型構造,可以看出菱形繼承有數據冗余和二義性的問題。支持多繼承就一定會有菱形繼承,像Java就直接不支持多繼承,規避掉了這里的問題,所以實踐中我們也是不建議設計出菱形繼承這樣的模型的。
上面就是單繼承的示意圖,下面就是多繼承的示意圖,也是菱形繼承的示意圖。
嚴格的多繼承第二張圖去掉上面的Person,這樣就成了一個標準的多繼承。
單繼承和多繼承比較簡單,主要來講菱形繼承的問題:
1.數據冗余
以第二幅圖為例,如果Person中有一個_name的成員變量,那么student中會有一份name,teacher中也會有一份,這就導致Assistant在同時繼承student和teacher時,就會有兩份name,這就是數據冗余,其實也就是造成空間的浪費。
我們都知道沒必要存兩份name,但是菱形繼承就會導致這個問題。
而數據冗余又會引出二義性的問題。
2.二義性
此時創建一個Assistant對象,訪問name就會報錯,因為編譯器不知道到底要訪問student中的name還是teacher中的name,這就是二義性。
而要解決這種問題有兩種辦法:
第一種辦法就是指定作用域,指定你要訪問哪個作用域的name,可以解決問題。
4.2虛繼承
很多人說c++語法復雜,其實多繼承就是一個體現。有了多繼承,就存在菱形繼承,有了菱形繼承就有菱形虛擬繼承,底層實現就很復雜,性能也會有一些損失,所以最好不要設計出菱形繼承。多繼承可以認為是c++的缺陷之一,后來的一些編程語言都沒有多繼承,如java。
而虛繼承就是上面的第二種解決方式:
此時我們不指定作用域也不會報錯,因為虛繼承,顧名思義就是看似繼承了,其實沒有繼承,到最后Assistant中只有一份name。
使用虛繼承后構造函數就要引用Person類的構造函數,因為是虛繼承,student和teacher類中的構造函數都會調用Person類的構造函數,你傳參過去編譯器不知道到底用誰的name。
并且顯式調用Person類的構造函數時,其實編譯器并不會走student和teacher類中Person類構造函數那一行,也就是說寫了之后只會走Assistant類中顯式調用的Person類構造函數,不走其他兩個的,即使顯式調用了。
這也就是虛繼承很燃的地方,比較繞,這也是因為多繼承引出的問題而填的坑。
我們通過調試也可以發現,當修改了name之后,所有的name都會發生改變,也說明了name只有一份,看似student和teacher中有name,其實根本就沒有把name放入進去。
虛繼承呢大家也不必深究,因為實際操作中我們也避免生成菱形繼承,并且虛繼承也會造成性能損失,所以了解一下即可。
最后我們再看一道題:
問:p1,p2和p3的關系?
A.p1==p2==p3? ?B.p1<p2<p3? ?C.p1==p3!=p2? ?D.p1!=p2!=p3
大家可以思考一下這個問題。
答案呢選c,這個題呢涉及到了多繼承中的指針偏移問題:
通過這個圖就可以清晰觀察到創建對象時指針的位置,p3==p1完全是巧合,正好都指向這段空間的起始位置,而p2就發生了指針偏移,經過切片后,base2基類的地址在中間的,因為前面是base1基類的地址,所以沒有指向起始位置。
大家思考一下這個是菱形繼承嗎?
答案是菱形繼承,在多繼承的情況下只要有公共的基類,就是菱形繼承,這個怎么體現呢?
B繼承了A,C繼承了B,那么C中就有一份A,D繼承了A,那么D中也有一份A,所以是菱形繼承。
再思考一下,如果要加虛繼承那么加在哪兩個位置呢?
答案加在BD兩處,因為BD兩處是直接導致有兩份A的地方,所以是BD兩處加。
5.繼承和組合
1.public繼承是一種is-a的關系。也就是說每個派生類對象都是一個基類對象。組合是一種has-a的關系。假設B組合了A,每個B對象中都有一個A對象。
2.繼承允許你根據基類的實現來定義派生類的實現。這種通過生成派生類的復用通常被稱為白箱復用(white-box reuse)。術語“白箱”是相對可視性而言:在繼承方式中,基類的內部細節對派生類可見。繼承一定程度破壞了基類的封裝,基類的改變,對派生類有很大的影響。派生類和基類間的依賴關系很強,耦合度高。
3.對象組合是類繼承之外的另一種復用選擇。新的更復雜的功能可以通過組裝或組合對象來獲得。對象組合要求被組合的對象具有良好定義的接口。這種復用風格被稱為黑箱復用(black-box reuse),因為對象的內部細節是不可見的。對象只以“黑箱”的形式出現。組合類之間沒有很強的依賴關系,耦合度低。優先使用對象組合有助于你保持每個類被封裝。
4.優先使用組合,而不是繼承。實際盡量多去用組合,組合的耦合度低,代碼維護性好。不過也不太那么絕對,類之間的關系就適合繼承(is-a)那就用繼承,另外要實現多態,也必須要繼承。類之間的關系既適合用繼承(is-a)也適合組合(has-a),就用組合。
組合的大概形式就如上圖所示。
這也是個概念性的知識,大家看上面的解釋,知道基本形式怎么用就行。
以上就是c++繼承(下)的內容。