C++认知继承
對于繼承,這是C++中相當重要的語法,
學習此語法可以更好的認知C++這個恢弘的世界。
- 介紹繼承
- 類繼承舉例
- 類繼承的認知
- 繼承關系和訪問限定符
- private訪問與protected訪問的區別
- 分析訪問限定與繼承方法的排列組合
- 賦值兼容規則
- 提問:基類與派派生類創建的對象是否可以互相賦值
- 指針與引用
- 為什么
- 好玩的地方
- 建議
- 繼承中的作用域
- 基類與派生類中一樣標識符的成員
- 注意
- 派生類
- is-a關系
- 派生的構造函數
- 派生的拷貝(復制)構造函數
- 派生類的賦值運算符重載
- 注:不要將賦值與初始化搞混了
- 析構函數
- 類設計
- 構造函數不能被繼承
- 析構函數
- 友元函數
- 靜態成員
- 有關使用基類方法的說明
- 菱形繼承
- 但是,C++作為一個高效的語言怎么能容忍數據冗余和二義性的問題?
- 虛繼承
- 組合與繼承
介紹繼承
C++作為一個面向對象的語言,而面向對象的編程的主要目的之一就是提供可重用的代碼
當開發大型項目時,重用經過測試的代碼,可是比新編寫的代碼要可靠好多。
比如,使用經過深海實戰的深潛器可是比用新技術制造的嶄新深潛器要可靠的多(嘻嘻,參考龍族3,凱撒小隊使用迪里雅斯特號進入神葬所)
已有代碼經過多次測試調整,bug已經極大程度的減少了。對于項目和小組成員的血壓比較友好。
在C++中,有類模板,函數模板,可以重復使用,降低開發者血壓。
而對于C++的類而言,也提供了更高層次的重用。
有類庫的概念,類庫是由類聲明和實現構成的,組合了數據表示和類方法。
C++提供了類繼承,來為開發者提供對類的修改和拓展的方法。
類繼承,能夠從已有的類派生出新的類,而派生類繼承了原有類(基類)的特征,包括方法。
正如 繼承一筆財產 遠比自己 白手起家 容易成功。
通過繼承派生出的類遠比自己重新設計一個類容易。
(話糙理不糙)
類繼承舉例
- 某個字符串類,可以派生出一個類,添加指定字符串顯示顏色的數據類型。
- 在已有的類中添加功能(成員函數)
- 航空公司給普通乘客提供基礎服務可以看作一個類,而給商務艙乘客提供跟高級的服務可以在普通服務類派生出一個商務艙服務類,并添加更多服務方法。
通過對原始類進行修改來滿足開發者的更多需求,而繼承機制只需要提供新特性,且不需要訪問源代碼就能派生出新類。
類繼承的認知
當兩個類中有大量重復的信息(類成員變量或成員函數),僅存在少量差別時,可以使用類繼承,對類進行復用。
舉例:
#include <iostream> #include <string> #include <vector> using namespace std;class Player { protected:string _name;//名字bool hasTable;//是否有比賽場地};class Member_Player:public Player { protected:vector<int> History_Score;//歷史得分double Rating;//獲勝比 }; int main(void) {Player Tom;//普通玩家TomMember_Player Jack;//VIP(辦卡)玩家Jackreturn 0; }繼承關系和訪問限定符
private/public/protected是C++中的關鍵字,他們描述了對類成員的訪問控制。
描述了對類成員的訪問權限大小,
public > protected >= private,
對于沒有涉及到繼承時,protected與private的權限是一樣的,類外都是不能直接訪問其所修飾的成員。
public是完全開放的的權限,任何成員都能在類外直接被訪問。
而繼承方式,也是靠這三個關鍵字來控制。
列了一張表
這里面,派生類中的public成員、protected成員、private成員的意思,是指,從基類(父類)中繼承的成員,在派生類(子類)中的訪問限定是這些權限。protected和private者兩權限是一樣的,都是不能直接訪問,但是可以通過public的成員函數去訪問。
private訪問與protected訪問的區別
但是,
protected修飾
private修飾
這就說明protected與private在繼承中還是存在一些差別。
我的理解是這樣的,protected訪問限定,在基類中功能和private是一樣的,阻止類外訪問成員。而繼承后,在派生類中,privates是完全阻止在類(基類)外進行訪問,在派生類中通過成員函數去訪問也是不允許的,
也就是說基類中的private成員正在派生類中是完全不可見的,
但是在派生類中是存在的,只是派生類中是完全沒有權限去訪問的。
總結一下:
對基類的外部而言,protected成員與private成員相似;
但對于派生類中,protected成員與基類中的public成員相似。
對于基類中private成員,派生類中在邏輯(語法)上是不可訪問,不可見的,是不存在的;
但從內存(物理)的角度來說,基類中的private成員是存在于派生類中。
分析訪問限定與繼承方法的排列組合
總的來說,對于繼承方式與訪問權限都是
公開(public)> 保護(protected)> 私有(private),最后在派生類中,對于基類中的成員,都是權限遵循較小的那個權限。
如果是private繼承,那么基類中的成員在派生類中都是不可見、無法訪問的,無論在基類中的公開還是保護成員(邏輯上),但是在物理上還是依舊在派生類中(參見上)。
也就是說,對于派生類繼承基類的成員而言,要經歷繼承方式與訪問限制。而這些都存在一定的權限,所以,大概總結一下,最后派生類中,對于基類的成員的繼承權限(訪問權限),找繼承方式于、與訪問限制中較小的權限,作為派生類繼承來的權限。
public(公有) > protected(保護) > private(私有)賦值兼容規則
提問:基類與派派生類創建的對象是否可以互相賦值
答:派生類對象可以給基類對象賦值
但,基類對象不能給派生類對象賦值
這就存在一個切割賦值的概念了。
派生類對象給基類對象賦值
基類對象給派生類對象賦值
指針與引用
基類和派生類的指針與引用。
其中這兩者的關系是,
基類指針和引用可以指向派生類的對像,
但是派生類的指針和引用不能指向基類。
為什么
在我學習C語言的時候,認識到一個概念,指針其實就是計算機內存中的地址,指針是計算機內存中的一個變量空間,變量空間的大小取決于計算機是多少位機。
指針是一個變量空間,該空間儲存的值就是計算機中的某個空間的地址(非法的或合法的)。
但是指針并不止步于此,指針還決定了訪問空間的字節大小,這是取決于指針的類型。也就是說,指針還要考慮能夠在指向空間中訪問的字節大小。這是非常關鍵的概念。
當基類指針指向派生類對象時,基類指針其實也就只能訪問,派生類中,屬于基類中的成員,對于派生類新增的成員是無法訪問的。
也就說,對于這個指針是合法的,不存在任何訪問問題。
但是對于派生類指針而言,就存在越界訪問的問題(可能,只是我這個水平的理解)。
因為,派生類中比基類多了一些成員,對于派生類指針,其訪問字節大小要比基類指針多,那么就存在一些非法空間可能會被訪問。
所以,C++考慮安全起見,就不會允許可能存在越界的情況(拙見,大佬指正一下)。
而對于引用而言,和指針是一樣的,因為,引用就是指針的封裝,底層其實還是指針(匯編角度)。
好玩的地方
這樣,基類的構造函數或者拷貝構造函數中的基類指針或引用也能接收派生類對象。這樣,可以使用派生類對象去初始化一個基類對象,某種意義上,基類對象初始化為一個派生類對象,盡管只是初始化了派生類中基類的部分。
對于賦值,其實是調用了隱式賦值運算符重載。
建議
對于C++繼承而言,有三種繼承方式,有三種訪問限制。
根據上面的分析,盡量使用public繼承, 對于基類中的成員變量使用protected限定。
使用private訪問限定,導致派生類中根本不可見基類對象,無意義。使用protected或private訪問控制,會導致,積累詞語無法再類外訪問或不可見,對于派生類繼承后沒什么用,所以不推薦。
繼承中的作用域
類是存在作用域的,基類于派生類是存在繼承關系,但是,這依就是兩個獨立的作用域。
基類與派生類中一樣標識符的成員
一樣標識符的成員,包括成員變量與成員函數。
這是允許定義一樣的標識符的成員。
如果是直接調用,只能調用派生類中的Print函數,
如果想調用基類中的Print函數,必須要聲明類域。
還有,如果想在派生類中使用基類中的成員變量(前提是不能為private),直接調用即可,如果派生類中沒有和基類中一樣的成員變量。
我把派生類改了一下。
在派生類中增加了一個與基類一樣的成員變量。
調用后
看看代碼執行了上面,輸出結果是什么。
我調用了基類指針去修改派生類中的基類部分的成員變量。
說明,當調用派生類中的Print函數時,使用的成員變量默認是派生類中的成員變量。只有顯示調用基類中的Print函數時,其中成員變量的使用是其基類自己的。
也就是說,在不同的類域中,遵循“就近原則”,智慧線在自動的類域中先找,如果有就直接使用,如果找不到,就回去更大的域中去尋找。
無論是成員函數還是成員變量。
其實,在基類與派生類中,有一樣的成員是,這兩個一樣的成員構成隱藏關系,而不是重載關系。因為,這是在兩個獨立的域中,重載是要求在一個作用域中。
當存在上面的例子的情況時,派生類的成員會隱藏基類的成員,除非顯式指明作用域。
例:
注意
最好不要在派生類中定義與基類同名的成員。這個雖然可以通過生你們作用域來指定調用,但是,人是最大的bug,一旦出錯,還不好查出來問題在哪。
所以,強烈建議不要使用同名的成員
派生類
is-a關系
這個關系代表:派生類對象也是一個基類對象,可以對基類對象執行任何操作,也可以對派生類對象執行。
也代表包含的關系,
例如,一個fruit類,保存水果的重量和熱量,而一個banana類代表一個香蕉的特性,因為香蕉是一個水果中的一種,所以可以從FRUIT類中派生出BANANA類,還可以為BANANA類增加新的特性。這是一種完全包含的關系。
如:
水果可以是早餐,但早餐不一定是水果,不一定是香蕉,早餐和水果就無法構成is-a的關系
派生的構造函數
當創建一個派生類的對象時,并不是直接就根據派生類去創建對象。
首先,會創建基類對象。也就是說,在派生類對象在入棧幀時,基類對象已經被創建好了(理解成變量入棧)。
比如有,創建派生類對象時,首先會調用,基類的構造函數,然后才會調用派生類的構造函數。
單步調試可以完整的看到這個過程。
介紹一下,創建派生類對象的整個流程。
派生的拷貝(復制)構造函數
對于派生類的基類而言,這都不是什么問題,賦值兼容規則,可以讓派生類切割去構造一個基類,然后再回調用函數里的定義,創建相應的派生類對象。
還有一點,派生類切割創建基類對象時,調用的是基類的構造函數,這個函數也要處理深拷貝的問題
其中涉及了深淺拷貝的問題,,這里需要自己去重新編寫函數定義,默認的拷貝構造函數并不適合深拷貝。
在下列情況,會調用拷貝構造函數
派生類的賦值運算符重載
賦值運算符通常用于同類對象之間的賦值,
注:不要將賦值與初始化搞混了
如果語句中創建了新的對象,這是初始化;
如果是語句修改已有對象的值,這是賦值。
基類
Player& operator=(const Player& tmp){_name = tmp._name;hasTable = tmp.hasTable;return *this;}派生類
Member_Player& operator=(const Member_Player& tmp){History_Score = tmp.History_Score;Rating = tmp.Rating;_name = tmp._name;hasTable = tmp.hasTable;return *this;}可能有些書上,回提供轉移構造函數和轉移賦值函數,其實,以我目前的水平看來,都是要先進行類型轉換的(構造了一個臨時對象),都是要轉換成同樣的類型,再處理。
析構函數
用于處理回收類創建對象而申請的資源。
這其實根本就不需要我們去顯示調用,不然,我們使用類干什么?
看看編譯器是怎么處理這些類的
因為,這些都是屬于棧這個內存空間中,而棧的特性是后入先出,在這里也是一樣的。對象創建,就會進入棧,為其開辟棧幀。
還有一點,我們先前提到的,使用派生類創建一個對象,要先創建一個基類對象,然后才會創建派生類添加的成員,一起構成一個對象。
當然,可能有人想在派生類的析構中先先清理掉其基類對象,調用基類的析構,如:
~Member_Player(){Player::~Player();cout << "~Member_Player()" << endl;}這樣的話,
會調用兩次析構,這就非常不符合,我們的設計原理,如果存在new分配空間的對象,在析構中delete兩次,程序就會直接報錯,所以這是不正確的也就是說,我們不應該在派生類的析構中去顯式調用基類的析構。
其正確的結構應該是這樣的,對象都是在棧中進行處理。
當,程序結束,系統要開始回收資源,對于創建的對象也要開始調用析構函數,回收資源,對象就要調用析構函數,出棧。這也是要符合棧頂特性的。
就這樣依次打印析構函數里的信息,就會出現最后的效果。
類設計
構造函數不能被繼承
構造函數不同于其他的類方法,因為這個方法,它是負責創建新的對象的,而其他的方法只是被現有的對象調用,這是構造函數不被繼承的原因之一。
派生類對象繼承基類的方法,就意味著,這個對象已經被創建好了,而對象沒有創建好,派生類對象也就沒法使用
繼承意味著派生類對象可以使用基類對象的方法,然而,在構造函數完成工作前,基類對象是不存在的。
析構函數
一定要定義顯式析構函數來釋放所有的自定義類型(如:new分配內存創建的),并完成類對象所需要的特殊的清理工作。對于基類而言,即使不需要,編譯器會自動處理那些成員,也最好提供一個虛析構函數。
析構函數也是不能被繼承的,在程序結束時,編譯器會先去調用派生類的析構函數,然后再去調用派生類的析構函數。
友元函數
友元函數并不是類成員,因此不能被繼承。
友元只是類外可以突破類的訪問權限的普通函數。
靜態成員
靜態成員得治并不在棧幀里,其位置是在靜態區中,無論以什么樣的繼承方式,靜態區中只有那為一一個獨一無二的靜態變量。
有關使用基類方法的說明
- 派生類自動使用繼承而來的基類方法,如果派生類沒有重新定義該方法或者定義了一個函數名一摸一樣的函數。
- 派生類的構造函數自動調用基類的構造函數,如果沒有在初始化成員列表中顯示調用(賦值)基類構造函數。
- 派生類的友元函數可以通過強制類型轉換,將派生類的引用或者指針轉換為基類引用或者指針,然后使用該指針或引用來調用基類的友元函數。
菱形繼承
顧名思義,就是繼承方式特別像菱形
大概長這樣
Jack這個人,即是貴賓用戶,又是工作人員。
比如:你在銀行有5個億的存款,銀行會把你認證為VIP用戶,然后你感到整天揮霍,很無聊,想找些事做,然后銀行經理就會給你安排一個保安(bushi)、某個部門的小負責人。這樣你也就是銀行的員工了。
你在銀行就有兩個身份了,既是VIP用戶,又是銀行員工。
但是,這也引發了一個問題,Worker繼承了Player這個類,而Member_Player也繼承了Player,而Player這個類中的成員有_name,個人的名字,那Worker和Member_Player又被Person給繼承了,那么其繼承的兩個類中有一樣的成員,那么當訪問該成員時,訪問到到底是哪個成員呢?
訪問不明確,這就造成了二義性的問題,
而且,有大量一樣的成員,這又會造成數據冗余的問題。
當然,對于二義性的問題,可以通過指定作用域來解決。
比如:
你在銀行的部門里,是副部長的職位,別人都叫你王副部長。
然而,當銀行經理在辦理業務時,稱呼你,是叫你王總,也就是說你會有不同的稱呼
這樣就能正常編譯,還解決了二義性的問題。
但是,C++作為一個高效的語言怎么能容忍數據冗余和二義性的問題?
虛繼承
可以在Person所繼承的兩個基類(Worker和Member_Player)對他們的基類進行虛擬繼承,因為,二義性和數據冗余的問題出在這上面。
使用了虛繼承的代碼
這兩個一樣的成員_name也能一起變化了,實際上這就是一個成員。
我給你們看看在內存中怎么處理的
看到了沒?這個_name的地址都是一樣的地址,即使是顯式調用了不同類中的_name,但當復制到時候,其變化的位置是沒有改變的,但是值發生了改變,就說明這兩個變量實際上是一個變量。
這樣就將虛繼承中一樣的成員化為一份
這樣,就解決了數據冗余和二義性的問題。
組合與繼承
感謝大佬觀看,感謝感謝!
總結
- 上一篇: 使用MPICH构建一个四节点的集群系统
- 下一篇: 云创大数据荣膺英特尔“行业贡献奖”