在C++的學習過程之中,那麼繼承與多態這兩個區別於C語言的特性你一定要了解,如果想要學好C++,那麼繼承與多態必須要了解清楚,尤其是多態,但是要了解多態首先你又必須了解繼承先,不過即使這兩者都十分了解了,也不敢說已經掌握了C++,因為這只不過是C++之中的冰山一角。(有興趣的可以了解一下網上說的C++的四種境界)
閒話就說到這,開始正式內容了,關於C++之中的繼承,我把在繼承內容所學到的內容與大家分享分享。如果有什麼不對的地方,歡迎大家提出來!
我學習一個內容的時候,總是喜歡從定義入手,然後根據這個定義再去猜想它有什麼功能,然後再去驗證我的猜想(當然並非每一個問題都這樣,一般都是遇到一些比較重要的概念的時候), 來看一下定義:繼承(inheritance)是面對象程序使可以用的最重要的,它允許程序員在保持原有類特性的基礎上進行擴展,增加加能。這樣產生新的類,稱為派生類。繼承呈現了面對象程序設計的層次結構,體現了由簡單到復雜的認知過程。
換種說法就是所謂“繼承”就是在一個已存在的類的基礎上建立一個新的類。已存在的類稱為“基類(base class)”或“父類(father class)”,新建的類稱為“派生類(derived class)”或“子類(son class )”。一個新類從已有的類那裡獲得其已有特性,這種現象稱為類的繼承。
來看一張圖,你就會了解地更清楚了:
說到了繼承,那麼就不得不提到派生了,因為這兩個概念總是出現在一起,或者說誰也離不開誰,通過繼承,一個新建子類從已有的父類那裡獲得父類的特性。從另一角度說,從已有的類(父類)產生一個新的子類,稱為類的派生。類的繼承是用已有的類來建立專用類的編程技術。派生類繼承了基類的所有數據成員和成員函數,並可以對成員作必要的增加或調整。一個基類可以派生出多個派生類,每一個派生類又可以作為基類再派生出新的派生類,因此基類和派生類是相對而言的。一代一代地派生下去,就形成類的繼承層次結構。相當於一個大的家族,有許多分支,所有的子孫後代都繼承了祖輩的基本特征,同時又有區別和發展。與之相仿,類的每一次派生,都繼承了其基類的基本特征,同時又根據需要調整和擴充原 有的特征。
關於什麼是繼承這個概念說完之後,那麼就開始下一個內容:繼承定義格式,同樣來看一張圖
public、protected、private這三者又稱為訪問限定符,用來定義繼承關系
再來看一下不同的繼承方式下的成員變量訪問控制關系的不同:
下面用代碼來驗證一下:
class Base { public: Base() { cout << "B()" << endl; } ~Base() { cout << "B()" << endl; } void ShowBase() { cout << "_pri = " << _pri << endl; cout << "_pro = " << _pro << endl; cout << "_pub = " << _pub << endl; } private: int _pri; protected: int _pro; public: int _pub; }; class Derived :public Base { public: Derived() { cout << "D()" << endl; } ~Derived() { cout << "D()" << endl; } void ShowDerived() { cout << "_d_pri = " << _d_pri << endl; cout << "_d_pro = " << _d_pro << endl; cout << "_d_pub = " << _d_pub << endl; } private: int _d_pri; protected: int _d_pro; public: int _d_pub; };
首先我們來看一下子類與父類在創建對象的時候有什麼關系,首先創建一個基類和一個派生類的對象。
int main() { Base b; Derived d; return 0; }我們會發現運行上面的程序之後,父類調用自己的構造函數沒有什麼問題,子類也調用了父類的構造函數,這是為什麼呢?其實這點要理解也很簡單,我們可以將子類的對象中的內容看做成兩部分構成,一部分就是它繼承父類的內容,還有一部分就是它自己獨有的內容,我們都知道在創建派生類對象的時候,它會去調用構造函數,既然它是由兩部分構成的,那麼自然它需要兩個構造函數來共同構造它,用圖來解釋一下更直觀:
在這裡有必要給說明一下以下三條小提示:
1、基類沒有缺省構造函數,派生類必須要在初始化列表中顯式給出基類名和參數列表。
2、基類沒有定義構造函數,則派生類也可以不用定義,全部使用缺省構造函數。
3、基類定義了帶有形參表構造函數,派生類就一定定義構造函數
同名隱藏
同樣地,還是上面的兩個類,我們創建兩個對象,一個是父類對象,一個是子類對象,我們可以調用分別裡面的
ShowBase()方法以及ShowDerived()方法,這顯然沒有任何問題,但是如果我們將這兩個方法都改成Show()方法,那麼這樣又會怎麼樣呢?這就涉及到了同名隱藏的概念!
什麼是同名隱藏呢?同名隱藏就是:兩個成員函數(包括成員變量)處在不同的作用域之中,但是名字相同(返回值、參數列表可以相同也可以不相同),此時如果你想用派生類的對象去調用基類中的同名方法就無法成功了,不過也有解決的方法,就是在方法前面加上作用域。來看一下在代碼:
class Base { public: Base() { _pri = 0x04; _pro = 0x05; _pub = 0x06; cout << "B()" << endl; } ~Base() { cout << "~B()" << endl; } void /*ShowBase()*/Show() { cout << "_pri = " << _pri << endl; cout << "_pro = " << _pro << endl; cout << "_pub = " << _pub << endl; } void Show(int a) { cout << a << endl; } private: int _pri; protected: int _pro; public: int _pub; }; class Derived :public Base { public: Derived() { _d_pri = 0x01; _d_pro = 0x02; _d_pub = 0x03; cout << "D()" << endl; } ~Derived() { cout << "~D()" << endl; } void /*ShowDerived()*/ Show() { cout << "_d_pri = " << _d_pri << endl; cout << "_d_pro = " << _d_pro << endl; cout << "_d_pub = " << _d_pub << endl; } private: int _d_pri; protected: int _d_pro; public: int _d_pub; }; int main() { Base b; Derived d; b.Show(); d.Show(); d.Show(1); //此處編譯期間就會報錯 d.Base::Show(); //可以通過這樣的方式來訪問同名的函數 return 0; }
這裡無非就是兩個是否可以相互轉化(這裡主要討論指針),相互賦值的關系,派生類可以給基類賦值(可以這樣理解基類需要的內容派生類都有,因為派生類繼承了基類中的內容),但是反過來基類不能直接給派生類賦值,需要進行強轉(前提是在公有繼承下)。還是用上面的類來做一下測試:
int main() { Base * b; Derived * d; d = new Derived; b = new Base; b = d; //派生類可以給基類賦值 b->ShowBase(); //調用函數也不會出問題 //d = &b; //報錯 “=”: 無法從“Base **”轉換為“Derived *” d = (Derived*)&b; //這樣編譯沒有問題,但是盡量不要這樣做,可能會出現無法預計的錯誤 d->ShowDerived(); //如果你用上面這樣的方式會帶來錯誤,裡面打印了隨機值 d->ShowBase(); return 0; }
關於這部分內容總結一下就是:
1. 子類對象可以賦值給父類對象(切割/切片)
2. 父類對象不能賦值給子類對象
3. 父類的指針/引用可以指向子類對象
4. 子類的指針/引用不能指向父類對象(可以通過強制類型轉換完成)
寫到這裡先來一個小小的總結之後再開始下一部分內容:
6. 在實際運用中一般使用都是public繼承,極少場景下才會使用protetced/private繼承.
7.友元關系不能繼承,也就是說基類友元不能訪問子類私有和保護成員,基類定義了static成員,則整個繼承體系裡面只有一個這樣的成員。無論派生出多少個子類,都只有 一個static成員實例。(靜態成員可以被繼承)
還有一個問題就是關於繼承關系之中的構造函數、析構函數調用過程的問題,我用衣一幅圖來表示一下:
關於菱形繼承的問題
在繼承這塊內容的學習過程之中,一定會遇到一個“詭異”的問題就是關於菱形繼承的問題,既然說它“詭異”,那麼他到底是如何“詭異”的呢?主要是因為它不常見(我只見到輸入、輸出流在庫函數的實現之中用到了菱形繼承,有興趣的可以去了解一下),正是因為這樣,導致它顯得有些“詭異”。
還是先來看一張圖:
說到菱形繼承,就不得不說到虛繼承的概念,對於虛繼承,就是為了解決從不同途徑繼承來的同名的數據成員在內存中有不同的拷貝造成數據不一致問題,將共同基類設置為虛基類。這時從不同的路徑繼承過來的同名數據成員在內存中就只有一個拷貝,同一個函數名也只有一個映射。
class A{}; //基類 class B:public A{};//子類 class C:public A{}; class D:public B,public C();
如上代碼中A,B,C,D就構成了一個菱形繼承,如果不用虛基類來實現菱形繼承就會導致模糊調用的現象,所謂模糊調用就是說在D的內存中會保留兩個基類A的對象,如何解決這個問題,利用虛基類就能很好的解決這個問題,即可改為:
class B:virtual public A{};//子類 class C:virtual public A{};
我們可以進一步了解一下,來看一個代碼:
class Base { public: int _pub; }; class Derived_one :/*virtual*/ public Base { public: int _por; }; class Derived_two :/*virtual*/ public Base { public: int _pri; }; class Derived : public Derived_one, public Derived_two { public: int _num; }; int main() { Derived d; cout << sizeof(d) << endl; return 0; }上述代碼在不是虛繼承的情況下結果為20,這很容易理解,但是如果改為虛繼承,那麼結果又發生了變化,結果變成了24,這又是為什麼呢?我們都知道,如果不引人虛擬繼承的概念,那麼上面的代碼就會有數據二義性的問題產生(裡面有兩個來自Base類的數據成員),但是引入虛擬繼承之後,這個二義性問題就可以解決了,還是用圖來說明吧:
最後總結一下虛繼承:
1. 虛繼承解決了在菱形繼承體系裡面子類對象包含多份父類對象的數據冗余&浪費空間的問題。
2. 虛繼承體系看起來好復雜,在實際應用我們通常不會定義如此復雜的繼承體系。一般不到萬不得 已都不要定義菱形結構的虛繼承體系結構,因為使用虛繼承解決數據冗余問題也帶來了性能上的 損耗。