C++的繼承法則詳解
一、繼承的概念和定義
1、概念
繼承(inheritance)機制是面向對象程序設計使代碼可以復用的最重要的手段,它允許程序員在保持原有類特性的基礎上進行擴展,增加功能,這樣產生新的類,稱派生類。繼承呈現(xiàn)了面向對象程序設計的層次結構,體現(xiàn)了由簡單到復雜的認知過程。以前我們接觸的復用都是函數(shù)復用,繼承是類設計層次的復用
2、使用
#include <iostream> using namespace std; class Person { }; class Student : public Person { }; class Teacher : public Person { }; int main() { }
作用:是將共有的數(shù)據(jù)和方法提取出來放到父類,自己定義自動獨有的成員,使其減少代碼冗余。
3、定義格式
上述中:
Person:稱為父類或者基類。
Student / Teacher:稱為子類或者派生類。
語法為:子類聲明 :繼承方式 繼承類名
4、繼承關系和訪問限定符
繼承方式有三種:
訪問限定符也有三種:
注意兩者的區(qū)別與聯(lián)系。
5、繼承父類時成員訪問方式的變化(繼承方式與訪問方式的聯(lián)系)
類成員 / 繼承方式 | public 繼承 | protected 繼承 | private 繼承 |
基類的 public 成員 | 派生類的public成員 | 派生類的protected 成員 | 派生類的private 成員 |
基類的 protected 成員 | 派生類的protected 成員 | 派生類的protected 成員 | 派生類的private 成員 |
基類的 private 成
員 | 在派生類中不可見 | 在派生類中不可見 | 在派生類中不可見 |
注意:不可見不是不存在,存在但是用不了,即使是自己的成員函數(shù)也不可以訪問,只有調用父類的成員函數(shù)間接訪問。
6、總結
(1)、基類private成員在派生類中無論以什么方式繼承都是不可見的。這里的不可見是指基類的私
有成員還是被繼承到了派生類對象中,但是語法上限制派生類對象不管在類里面還是類外面
都不能去訪問它。
(2)、基類private成員在派生類中是不能被訪問,如果基類成員不想在類外直接被訪問,但需要在
派生類中能訪問,就定義為protected。可以看出保護成員限定符是因繼承才出現(xiàn)的。
(3)、實際上面的表格我們進行一下總結會發(fā)現(xiàn),基類的私有成員在子類都是不可見?;惖钠渌?br />成員在子類的訪問方式 == Min(成員在基類的訪問限定符,繼承方式),public > protected
> private。
(4)、使用關鍵字class時默認的繼承方式是private,使用struct時默認的繼承方式是public,不過
最好顯示的寫出繼承方式。
5. 在實際運用中一般使用都是public繼承,幾乎很少使用protetced/private繼承,也不提倡
使用protetced/private繼承,因為protetced/private繼承下來的成員都只能在派生類的類里
面使用,實際中擴展維護性不強
二、父類和子類對象賦值兼容轉換
1、賦值兼容也可以叫“切割”:
(1)、派生類對象 可以賦值給 基類的對象 / 基類的指針 / 基類的引用。這里有個形象的說法叫切片或者切割。寓意把派生類中父類那部分切來賦值過去。
2、賦值兼容中對引用和指針的解讀:
其中,r 對象繼承的是父類與子類共有的這部分數(shù)據(jù)的別名,指針也是類似。
3、賦值兼容解釋
平時我們知道,C++類型轉換的時候是會產生臨時對象的并且該臨時對象具有常屬性,賦值兼容這里子類和父類也是兩種類型,會不會產生臨時對象吶?
答案 :這過程不會產生臨時對象,這是語法規(guī)定的特殊處理,證明如下:
三、繼承中的作用域
(一)、介紹:
(1)、在繼承體系中基類和派生類都有獨立的作用域。
(2)、子類和父類中有同名成員,子類成員將屏蔽父類對同名成員的直接訪問,這種情況叫隱藏,
也叫重定義。(在子類成員函數(shù)中,可以使用 基類::基類成員 顯示訪問)
(3)、需要注意的是如果是成員函數(shù)的隱藏,只需要函數(shù)名相同就構成隱藏。
(4)、注意在實際中在繼承體系里面最好不要定義同名的成員。
(二)、同名成員:隱藏
同名成員指子類中定義的變量名可以和父類中的變量名相同,這是可以的。因為子類和父類是屬于兩個作用域。此時子類成員會隱藏父類成員,這個過程叫 隱藏。
class Person { protected: string _name = "父類賦值"; string _sex; int _age; }; class Student : public Person { public : void fun() { cout << _name << endl; } protected: string _name = "子類賦值"; string _tem; }; int main() { Student s; s.fun(); return 0; }
如上隱藏過后,默認訪問的是子類成員,此時向訪問父類成員需要加上域作用限定符。
實際中,不介意設計同名成員,這無疑是自己給自己設坑。
筆試題:
解答:
答案選【d】,首先排除a、c,因為c重寫是在后面多態(tài)部分的知識,所以不作講解。
父類和子類是屬于兩個作用域,而重載是需要再同一作用域中才能構成重載,所以b排除,最后,如果是成員函數(shù):子類和父類中只要函數(shù)名相同就會構成隱藏關系(與返回值和參數(shù)列表無關)。
如圖,會發(fā)現(xiàn)調不到沒有參數(shù)的fun函數(shù),因為該函數(shù)已經被隱藏了。
四、子類的默認成員函數(shù)
(一)、介紹:
6個默認成員函數(shù),“默認”的意思就是指我們不寫,編譯器會變我們自動生成一個,那么在派生類
中,這幾個成員函數(shù)是如何生成的呢?
1. 派生類的構造函數(shù)必須調用基類的構造函數(shù)初始化基類的那一部分成員。如果基類沒有默認
的構造函數(shù),則必須在派生類構造函數(shù)的初始化列表階段顯示調用。
2. 派生類的拷貝構造函數(shù)必須調用基類的拷貝構造完成基類的拷貝初始化。
3. 派生類的operator=必須要調用基類的operator=完成基類的復制。
4. 派生類的析構函數(shù)會在被調用完成后自動調用基類的析構函數(shù)清理基類成員。因為這樣才能
保證派生類對象先清理派生類成員再清理基類成員的順序。
5. 派生類對象初始化先調用基類構造再調派生類構造。
6. 派生類對象析構清理先調用派生類析構再調基類的析構。
7. 因為后續(xù)一些場景析構函數(shù)需要構成重寫,重寫的條件之一是函數(shù)名相同(這個我們后面會講
解)。那么編譯器會對析構函數(shù)名進行特殊處理,處理成destrutor(),所以父類析構函數(shù)不加
virtual的情況下,子類析構函數(shù)和父類析構函數(shù)構成隱藏關系
(二)、默認構造函數(shù)
對于默認構造函數(shù),可以結合自定義類型思考:
(1)、子類獨有成員:按照內置類型和自定義類型分別處理
(2)、父類的成員:會調用父類的構造函數(shù)。
1、如何顯示寫繼承構造函數(shù)
class Person { public: Person(string name) { cout << "調用父類的構造" << endl; } protected: string _name ="父類成員"; }; class Student : public Person { public : void fun() { cout << Person::_name << endl; } protected: string _tem ="子類成員"; }; int main() { return 0; }
錯誤寫法:
正確寫法:
隱式寫法:
有點像自定義類型的處理方式,需要調用父類的默認構造,若父類沒有默認構造,則會報錯。
顯示寫法:
class Person { public: Person(string name) { cout << "調用父類的構造" << endl; } protected: string _name = "父類成員"; }; class Student : public Person { public : Student(string name, string tem) : Person(name), _tem(tem) { cout << "調用子類的構造" << endl; } void fun() { cout << Person::_name << endl; } protected: string _tem = "子類成員"; }; int main() { Student s("1","1"); return 0; }
無論怎么寫,都是先調用父類的構造函數(shù),再調用子類自己的構造函數(shù),因為初始化列表的初始化順序是根據(jù)成員聲明的順序確定的,而父類的成員變量在內存中始終在前面先聲明。
2、顯示寫拷貝構造函數(shù)
顯示寫拷貝構造,因為跟自定義類型處理方式類似,子類成員調用子類的拷貝構造,父類成員調用父類的拷貝構造,所以我們需要知道誰是父類的成員,這里就需要用到賦值兼容的知識,也就是切割問題,直接把子類對象傳遞給父類的拷貝構造,父類會自己割出自己的那部分。
Student(const Student& s): Person(s), _tem(s._tem) { cout << "調用子類的拷貝構造"; }
3、賦值重載的寫法
與拷貝構造類似的,也是使用切割:
錯誤寫法:
因為子類賦值重載與父類的賦值重載函數(shù)名相同,所以構成隱藏關系,所以這樣寫的話是子類自己無限遞歸了,這時我們要調用父類的賦值重載就需要指定作用域:
正確寫法:
Student& operator = (const Student& s) { if (this != &s) { Person::operator=(s); _tem = s._tem; } return *this; }
4、析構函數(shù)的寫法:
錯誤寫法:
原因如下:
C++特殊規(guī)定:子類的析構函數(shù)和父類的析構函數(shù)會構成隱藏關系,由于后面多態(tài)的遠呀,析構函數(shù)被特殊處理了,父類子類的析構函數(shù)的函數(shù)名最后都會被處理成destruuctor(),所以會構成隱藏關系。
正確寫法:
既然是隱藏關系,所以指定作用域:
但我們會發(fā)現(xiàn)居然調用了兩次析構,原因是這里又做了特殊處理:
為了保證先調用子類析構,后調用父類析構,父類的析構會在子類析構后自動調用,所以不需要我們手動調用:
~Student() { cout << "調用子類析構" << endl; }
問題:為什么要先先調用子類析構,后調用父類析構?
解答:
(1)、為了保持構造函數(shù)的調用順序規(guī)則。
(2)、因為子類的析構函數(shù)中可能會使用父類的成員,若先析構父類,父類資源就已經清理釋放,而子類析構中還可能會去訪問父類成員,就可能造成野指針等問題。顯示調用就無法保證一定會先子后父,所以設置成自動調用。
5、繼承類模板
要繼承類模板就必須顯示實例化繼承:
五、繼承與友元
友元關系不能繼承,也就是說基類友元不能訪問子類私有和保護成員(理解爸爸的朋友不是我的朋友)。
六、繼承與靜態(tài)成員
基類定義了static靜態(tài)成員,則整個繼承體系里面只有一個這樣的成員。無論派生出多少個子 類,都只有一個static成員實例 。 運用: 在父類的構造函數(shù)中 ++靜態(tài)成員,可以統(tǒng)計父類和子類創(chuàng)建的對象個數(shù)。
七、單繼承、多繼承、菱形繼承
1、單繼承:
單繼承:一個子類只有一個直接父類時稱這個繼承關系為單繼承。
2、多繼承
多繼承:一個子類有兩個或以上直接父類時稱這個繼承關系為多繼承
注意:父類用“ , ”隔開。
3、菱形繼承
有了多繼承就可能出現(xiàn)菱形繼承,菱形繼承:菱形繼承是多繼承的一種特殊情況。
菱形繼承的問題:從下面的對象成員模型構造,可以看出菱形繼承有數(shù)據(jù)冗余和二義性的問題。 在Assistant的對象中Person成員會有兩份。
class Person { public: Person(string name) { cout << "調用父類的構造" << endl; } Person(const Person& p) { cout << "調用父類的拷貝構造" << endl; } Person& operator = (const Person& s) { return *this; } ~Person() { cout << "調用父類析構" << endl; } protected: string _name = "父類成員"; }; class Student : public Person { public: Student(string name, string tem) : Person(name), _tem(tem) { cout << "調用子類的構造" << endl; } Student(const Student& s) : Person(s), _tem(s._tem) { cout << "調用子類的拷貝構造"; } Student& operator = (const Student& s) { if (this != &s) { Person::operator=(s); _tem = s._tem; } return *this; } ~Student() { cout << "調用子類析構" << endl; } void fun() { cout << Person::_name << endl; } protected: string _tem = "子類成員"; }; class Teacher :public Person { }; class Assistant :Teacher, Student { }; int main() { Assistant s; s._name; return 0; }
對_name訪問不明確,因為既有來自Student的_name,還有來自Teacher的_name。 第一種解決方法是: 訪問_name時可以指明作用域,但是這樣只能暫時解決二義性的問題,沒有解決本質問題,有些信息不可能存在兩份,如年齡,學號等等,這樣設置就會存在空間浪費。 第二種也是最合適的解決方法:使用虛繼承,如下:
解決菱形繼承的問題:虛繼承virtual
在造成二義性的父類(即最頂層的父類)的直接子類繼承處,加個virtual關鍵字:
菱形繼承的使用場景:
菱形問題的底層分析:
2:46:00,偏移量等等知識。
到此這篇關于探索C++的繼承法則的文章就介紹到這了,更多相關C++繼承法則內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
C語言統(tǒng)計輸入字符各個字母出現(xiàn)頻率的解題思路
這篇文章主要介紹了C語言統(tǒng)計輸入字符各個字母出現(xiàn)頻率的解題思路,具有一定的參考價值,感興趣的小伙伴們可以參考一下2015-08-08