C++?繼承的范例講解
1.繼承的概念
繼承,是面向?qū)ο蟮娜筇匦灾弧@^承可以理解成是類級(jí)別的一個(gè)復(fù)用,它允許我們在原有類的基礎(chǔ)上進(jìn)行擴(kuò)展,增加新的功能。
當(dāng)創(chuàng)建一個(gè)類時(shí),我們可以繼承一個(gè)已有類的成員和方法,并且在原有的基礎(chǔ)上進(jìn)行提升,這個(gè)被繼承的類叫做基類,而這個(gè)繼承后新建的類叫做派生類。
用法如下:
class [派生類名] : [繼承類型] [基類名]
例如:
class Person { public: string _name; int _age; }; class Student : public Person { protected: string _stuNum; };
這里的派生類Student就復(fù)用了Person的方法和成員,并在此基礎(chǔ)上擴(kuò)展補(bǔ)充。
2.繼承方式
繼承的方式和類的訪問限定符一樣,分為public(公有繼承),private(私有繼承), protected(保護(hù)繼承)三種。
不同的繼承方式,在派生類中繼承下來的基類成員的訪問權(quán)限也不一樣。
基類的其他成員在子類的訪問方式 = Min(成員在基類的訪問限定符,繼承方式)
備注:
1.在實(shí)際運(yùn)用中一般使用都是public繼承,幾乎很少使用protetced/private繼承,也不提倡使用protetced/private繼承,因?yàn)閜rotetced/private繼承下來的成員都只能 在派生類的類里面使用,實(shí)際中擴(kuò)展維護(hù)性不強(qiáng)。
2.使用關(guān)鍵字class時(shí)默認(rèn)的繼承方式是private,使用struct時(shí)默認(rèn)的繼承方式是public,不過最好顯示的寫出繼承方式。
3.基類與派生類的賦值轉(zhuǎn)換
派生類可以賦值給基類的對象、指針或者引用,這樣的賦值也叫做對象切割。
例如上面的Person類和Student類
這種賦值只能是派生類賦給基類(但需要割掉多出來的成員例如_ stuID),而基類對象不能賦給派生類。
基類的指針可以強(qiáng)制類型轉(zhuǎn)換賦值給派生類的指針, 如:
int main() { Person p1; Student s1; Person* hPtr1 = &s1;//指向派生類對象 Person* hPtr2 = &p1;//指向基類對象 Student* pPtr = (Student*)hPtr1;//沒問題 Student* pPtr = (Student*)hPtr2;//有時(shí)候沒有問題,但是會(huì)存在越界風(fēng)險(xiǎn) return 0; }
小結(jié):
1.派生類可以賦值給基類的對象、指針或者引用
2.基類對象不能賦值給派生類對象
3.基類的指針可以通過強(qiáng)制類型轉(zhuǎn)換賦值給派生類的指針。**??但是必須是基類的指針是指向派生類對象時(shí)才是安全的,否則會(huì)存在越界的風(fēng)險(xiǎn)。**這里基類如果是多態(tài)類型,可以使用RTT的dynamic_cast來進(jìn)行識(shí)別后進(jìn)行安全轉(zhuǎn)換。
4.作用域與隱藏
??隱藏:隱藏,也叫做重定義,當(dāng)基類和派生類中出現(xiàn)重名的成員時(shí),派生類就會(huì)將基類的同名成員給隱藏起來,然后使用自己的。(但是隱藏并不意味著就無法訪問,可以通過指明基類作用域來顯式訪問隱藏成員。)
class Person { public: void f(int age) { cout << "姓名" << _name << endl; cout << "年齡" << _age << endl; } protected: string _name; int _age; }; class Student : public Person { public: void f() { Person::f(32);//需顯式調(diào)用f函數(shù) cout << "學(xué)號(hào)" << _stuNum << endl; } private: string _stuNum; };
例如這里的f( )就構(gòu)成了隱藏
同時(shí),這里還有個(gè)需要注意的問題,在基類與派生類中,同名的方法并不能構(gòu)成重載,因?yàn)樘幱诓煌淖饔糜蛑小6灰獫M足方法名相同,就會(huì)構(gòu)成隱藏。
5.派生類的默認(rèn)成員函數(shù)
在每一個(gè)類中,都會(huì)有6個(gè)默認(rèn)的成員函數(shù),這些函數(shù)即使我們自己不去實(shí)現(xiàn),編譯器也會(huì)幫我們實(shí)現(xiàn)。
??這里有兩點(diǎn)需要注意(筆試題常考):
1.構(gòu)造函數(shù),拷貝構(gòu)造,operator=三種情況,都要調(diào)用父類對應(yīng)的構(gòu)造函數(shù)/拷貝構(gòu)造/operator=進(jìn)行對父類的成員變量的初始化,并且倘若父類沒有默認(rèn)的構(gòu)造函數(shù)的時(shí)候(比如父類寫了帶參的構(gòu)造函數(shù)),我們就要顯式調(diào)用(Person(參數(shù)…),Person::operator=(參數(shù)…))
??構(gòu)造函數(shù)顯示調(diào)用
派生類的構(gòu)造函數(shù)必須調(diào)用基類的構(gòu)造函數(shù)初始化基類的那一部分成員。如果基類沒有默認(rèn)的構(gòu)造函 數(shù),則必須在派生類構(gòu)造函數(shù)的初始化列表階段顯示調(diào)用。
Student() :People() { cout << "Student()" << endl; }
??拷貝構(gòu)造顯示調(diào)用
建議拷貝構(gòu)造都用顯示調(diào)用,不然免不了出現(xiàn)子類拷貝構(gòu)造當(dāng)中調(diào)用了父類的構(gòu)造函數(shù)的情況(因?yàn)榭截悩?gòu)造也是構(gòu)造,在初始化列表處,對于子類而言,父類相當(dāng)于一個(gè)自定義類型對象,子類會(huì)調(diào)用父類的構(gòu)造函數(shù)對父類的資源進(jìn)行初始化。)
Student(const Student& s) :People(s) { cout << "Student(const Student& s)" << endl; }
??派生類的operator=必須要調(diào)用基類的operator=完成基類的復(fù)制。
Student& operator=(const Student& s) { cout << "Student& operator = (const Student& s)" << endl; if (this != &s) { Person:: operator=(s); } }
??析構(gòu)函數(shù)
由于編譯器會(huì)將析構(gòu)函數(shù)的名字處理成destructor,因此派生類和基類的析構(gòu)函數(shù)會(huì)構(gòu)成隱藏關(guān)系,故若要派生類要調(diào)用基類的析構(gòu)函數(shù),那么需要顯式調(diào)用,但是編譯器會(huì)默認(rèn)在派生類的析構(gòu)函數(shù)調(diào)用結(jié)束后調(diào)用基類的析構(gòu)函數(shù),這樣就析構(gòu)兩次了。
~Person() { cout << "~Person()" << endl; } ~Student() { Person:: ~Person(); cout << "~Student()" << endl; }
在派生類中,基類的析構(gòu)函數(shù)會(huì)被隱藏,雖然它們這里的名字不同,但是為了實(shí)現(xiàn)多態(tài), 它們都會(huì)被編譯器重命名為destructor。在調(diào)用子類的構(gòu)造函數(shù)時(shí),我們是先調(diào)用父類的構(gòu)造函數(shù),后對子類的成員進(jìn)行構(gòu)造。由先構(gòu)造后析構(gòu)的順序,所以我們是在析構(gòu)函數(shù)當(dāng)中析構(gòu)子類的資源,析構(gòu)函數(shù)調(diào)用完后編譯器自動(dòng)幫我們調(diào)用父類的析構(gòu)函數(shù)。
6.友元與靜態(tài)成員
??1.友元
友元關(guān)系是不會(huì)繼承的,如果子類要使用父類的友元,則子類自己也要將其定義為友元。
??2.靜態(tài)成員
基類定義了static靜態(tài)成員,無論繼承了多少次,派生了多少子類,靜態(tài)成員在這整個(gè)繼承體系中有且只有一個(gè)。靜態(tài)成員不再單獨(dú)屬于某一個(gè)類亦或者是某一個(gè)對象,而是屬于這一整個(gè)繼承體系。
7.菱形繼承與虛繼承
首先簡單介紹下單繼承、多繼承的概念
單繼承:一個(gè)子類只有一個(gè)直接父類時(shí)稱這個(gè)繼承關(guān)系為單繼承
多繼承:一個(gè)子類有兩個(gè)或以上直接父類時(shí)稱這個(gè)繼承關(guān)系為多繼承
菱形繼承:菱形繼承是多繼承的一種特殊情況,下面簡單舉例介紹下菱形繼承及其帶來的二義性問題:
class Human { public: int _age; }; class Student : public Human { public: int _stuNum; }; class Teacher : public Human { public: int _teaNum; }; class Assistant : public Teacher, public Student { };
哦豁!??!菱形繼承這個(gè)關(guān)系感受到了吧?。。?/p>
??直接講下這個(gè)菱形繼承帶來的二義性問題:
按照道理來說,各個(gè)類的大小應(yīng)該是這樣的。human類4個(gè)字節(jié),teacher和student都是8個(gè)字節(jié),而assistant是12個(gè)字節(jié),但是實(shí)際上assistant卻是16字節(jié)。
??為什么assistant會(huì)有16字節(jié)?
這就是菱形繼承的數(shù)據(jù)冗余和二義性問題的體現(xiàn)。
這里的teacher和student都從human中繼承了相同的成員 _age。但是assistant再從teacher和student繼承時(shí),就分別把這兩個(gè) _age都給繼承了過來,導(dǎo)致這里有了兩個(gè)一樣的成員。
在這樣的情況下,后續(xù)想給 _age賦值,也會(huì)被編譯器提示指示不明確,報(bào)錯(cuò)。
??菱形繼承的二義性是很致命的問題,如何解決呢?
虛繼承,在腰部的類繼承時(shí)添加virtual關(guān)鍵字。
class Student : virtual public Human { public: int _stuNum; }; class Teacher : virtual public Human { public: int _teaNum; }; class Assistant : public Teacher, public Student { };
這次,二義性問題解決了,teacher和student都是12個(gè)字節(jié),而assistant是20個(gè)字節(jié)。
想知道為什么?點(diǎn)這里:從內(nèi)存角度看待虛繼承
??簡單小結(jié)一下
1.可以看到?jīng)]有虛繼承的情況下,Assitant中的成員連續(xù)排列出現(xiàn)了Teacher和Student中的_age是兩個(gè)不同的值,但實(shí)際上一個(gè)人不會(huì)有兩個(gè)年齡,所以這就出現(xiàn)了數(shù)據(jù)冗余。
2.這里多出來的8個(gè)字節(jié),其實(shí)是兩個(gè)虛基表指針。因?yàn)檫@里Human中的 _age是 teacher和 student共有的,所以為了能夠方便處理,在內(nèi)存中分布的時(shí)候,就會(huì)把這個(gè)共有成員 _age放到對象組成的最末尾的位置。然后通過了Teacher和Student的兩個(gè)指針,指向的一張表。這兩個(gè)指針叫虛基表指針,這兩個(gè)表叫虛基表。虛基表中存的偏移量。通過偏移量可以找到下面的Human。由此可見,使用了虛擬繼承后,就可以解決菱形繼承導(dǎo)致的問題。
??為什么Teacher、Student需要去找屬于自己的 _age?
基類與派生類的賦值轉(zhuǎn)換時(shí),需要進(jìn)行切片。
int main() { Assistant a; Teacher t = a; Student s = a; return 0; }
當(dāng)把對象a賦值給t和s的時(shí)候,因?yàn)樗麄兓ハ鄾]有對方的 _stuNum和 _teaNum,所以他們需要進(jìn)行對象的切割,但是又因?yàn)?_age存放在對象的最尾部,所以只有知道了自己的偏移量,才能夠成功的在切割了沒有的元素時(shí),還能找到自己的 _age。
8.繼承和組合
??繼承是一種復(fù)用的方式,但不是唯一方式!
1.public繼承是一種is-a的關(guān)系,就是基類是一個(gè)大類,而派生類則是這個(gè)大類中細(xì)分出來的一個(gè)子類,但是他們本質(zhì)上其實(shí)是一種東西。
2.組合是一種has-a的關(guān)系,就是一種包含關(guān)系,比如對象a是對象b的成員,那么他們的關(guān)系就是對象b的組成中包含了對象a,對象a是對象b中的一部分,對象b包含對象a。
3.繼承方式的復(fù)用常稱之為白箱復(fù)用,在繼承方式中,基類的內(nèi)部細(xì)節(jié)對子類可見,這一定程度上破壞了基類的封裝,伴隨著基類的改變,對派生類的改變很大。并且兩者依賴關(guān)系強(qiáng),耦合度大。
4.對象組合式繼承之外的復(fù)用選擇,對象組合要求被組合對象提供良好的接口定義。這種復(fù)用稱之為黑箱復(fù)用,對象的內(nèi)部實(shí)現(xiàn)細(xì)節(jié)是不可見的。耦合度低。
實(shí)際工程中能用繼承和組合就用組合,組合的耦合度低,代碼的維護(hù)性好,但是繼承在有些關(guān)系就適合用繼承就用繼承,并且要實(shí)現(xiàn)多態(tài)就一定要用繼承。
到此這篇關(guān)于C++ 繼承的范例講解的文章就介紹到這了,更多相關(guān)C++ 繼承內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
C語言16進(jìn)制與ASCII字符相互轉(zhuǎn)換
大家好,本篇文章主要講的是C語言16進(jìn)制與ASCII字符相互轉(zhuǎn)換,感興趣的同學(xué)趕快來看一看吧,對你有幫助的話記得收藏一下2022-01-01C++實(shí)現(xiàn)將輸入的內(nèi)容輸出到文本文件
這篇文章主要介紹了C++實(shí)現(xiàn)將輸入的內(nèi)容輸出到文本文件問題,具有很好的參考價(jià)值,希望對大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-08-08C++可調(diào)用對象callable object深入分析
所謂的callable object,表示可以被某種方式調(diào)用其某些函數(shù)的對象。它可以是:一個(gè)函數(shù)、一個(gè)指向成員函數(shù)的指針、一個(gè)函數(shù)對象,該對象擁有operator()、一個(gè)lambda表達(dá)式,嚴(yán)格的說它是一種函數(shù)對象2022-08-08