C++ 面向对象(一)继承:继承、对象切割、菱形继承、虚继承、继承与组合
目錄
- 繼承
- 繼承的概念
- 繼承方式
- 基類與派生類的賦值轉(zhuǎn)換
- 作用域與隱藏
- 派生類的默認(rèn)成員函數(shù)
- 友元與靜態(tài)成員
- 友元
- 靜態(tài)成員
- 多繼承
- 菱形繼承
- 虛繼承
- 繼承和組合
- 什么是組合
- 如何選擇組合和繼承
繼承
繼承的概念
繼承,是面向?qū)ο笕筇匦灾?#xff0c;是可以使代碼復(fù)用的最重要的手段之一。我們可以在保持原有結(jié)構(gòu)的基礎(chǔ)上,在對(duì)類的功能進(jìn)行進(jìn)一步的拓展,使得創(chuàng)建和維護(hù)一個(gè)類變得更加的高效和簡單
當(dāng)創(chuàng)建一個(gè)類時(shí),我們可以繼承一個(gè)已有類的成員和方法,并且在原有的基礎(chǔ)上進(jìn)行提升,這個(gè)被繼承的類叫做基類,而這個(gè)繼承后新建的類叫做派生類。
繼承的方法很簡單
class [派生類名] : [繼承類型] [基類名]例如:
class Human { public:Human(string name = "張三", int age = 18) : _name(name), _age(age){}void Print(){cout << "name:" << _name << endl;cout << "age:" << _age << endl;}protected:string _name;int _age; };class Student : public Human { public:Student(string stuNum = "123456") : Human(), _stuNum(stuNum){}void Print(){Human::Print();cout << "stuNum:" << _stuNum << endl;}protected:string _stuNum; };int main() {Human h1;Student s1;h1.Print();cout << endl;s1.Print();return 0; }
這里的派生類Student就復(fù)用了Human的方法和成員,并在里面增加了新內(nèi)容。
繼承方式
繼承的方式和類的訪問限定符一樣,分為public(公有繼承),private(私有繼承), protected(保護(hù)繼承)三種。
不同的繼承方式,在派生類中繼承下來的基類成員的訪問權(quán)限也不一樣。
主要特點(diǎn)就是,繼承之后的成員訪問權(quán)限是選取繼承方式和原有權(quán)限中最私密的那個(gè)。(除原私有成員會(huì)不可見)
原基類的private成員無論以什么方式繼承下來后都是不可見的。不可見并不是沒有繼承,而是在派生類中被隱藏了,無法訪問。
基類與派生類的賦值轉(zhuǎn)換
派生類可以賦值給基類的對(duì)象、指針或者引用,這樣的賦值也叫做對(duì)象切割。
例如上面的Human類和Student類
從這幅圖可以看出來,當(dāng)把派生類賦值給基類時(shí),可以通過切割掉多出來的成員如_stuNum的方式來完成賦值。
但是基類對(duì)象如果想賦值給派生類,則不可以,因?yàn)樗荒軕{空多一個(gè)_stuNum成員出來。
但是基類的指針卻可以強(qiáng)制類型轉(zhuǎn)換賦值給派生類對(duì)象, 如:
- 派生類可以賦值給基類的對(duì)象、指針或者引用
- 基類對(duì)象不能賦值給派生類對(duì)象
- 基類的指針可以通過強(qiáng)制類型轉(zhuǎn)換賦值給派生類的指針。但是必須是基類的指針是指向派生類對(duì)象時(shí)才
是安全的,否則會(huì)存在越界的風(fēng)險(xiǎn)。這里基類如果是多態(tài)類型,可以使用RTT的dynamic_cast來
進(jìn)行識(shí)別后進(jìn)行安全轉(zhuǎn)換。
作用域與隱藏
基類和派生類都具有他們各自的作用域,那如果出現(xiàn)同名的成員,此時(shí)會(huì)怎么樣呢?這里就要牽扯到一個(gè)概念——隱藏。
隱藏:隱藏,也叫做重定義,當(dāng)基類和派生類中出現(xiàn)重名的成員時(shí),派生類就會(huì)將基類的同名成員給隱藏起來,然后使用自己的。(但是隱藏并不意味著就無法訪問,可以通過聲明基類作用域來訪問到隱藏成員。)
class Human { public:Human(string name = "張三", int age = 18) : _name(name), _age(age){}void Print(){cout << "name:" << _name << endl;cout << "age:" << _age << endl;}protected:string _name;int _age; };class Student : public Human { public:Student(string stuNum = "123456") : Human(), _stuNum(stuNum){}void Print(){Human::Print();cout << "stuNum:" << _stuNum << endl;}protected:string _stuNum; };例如這里的Print就構(gòu)成了隱藏
同時(shí),這里還有個(gè)需要注意的問題,在基類與派生類中,同名的方法并不能構(gòu)成重載,因?yàn)樘幱诓煌淖饔糜蛑小6灰獫M足方法名相同,就會(huì)構(gòu)成隱藏。
派生類的默認(rèn)成員函數(shù)
在每一個(gè)類中,都會(huì)有6個(gè)默認(rèn)的成員函數(shù),這些函數(shù)即使我們自己不去實(shí)現(xiàn),編譯器也會(huì)幫我們實(shí)現(xiàn)。
之前有寫過
類的默認(rèn)六個(gè)成員函數(shù)
可以看到,調(diào)用派生類的默認(rèn)成員函數(shù)時(shí)都會(huì)調(diào)用基類的默認(rèn)構(gòu)造函數(shù)。
這里還有個(gè)需要注意的地方,在派生類的析構(gòu)函數(shù)中不需要自己去調(diào)用基類的析構(gòu)函數(shù),編譯器會(huì)在派生類析構(gòu)函數(shù)結(jié)束后自動(dòng)調(diào)用。
~Student(){//~Human();//報(bào)錯(cuò), 這里基類的析構(gòu)函數(shù)會(huì)被隱藏cout << "Student 析構(gòu)函數(shù)" << endl;}同時(shí),在派生類中,基類的析構(gòu)函數(shù)會(huì)被隱藏,雖然它們這里的名字不同,但是為了實(shí)現(xiàn)多態(tài), 它們都會(huì)被編譯器重命名為destructor。
友元與靜態(tài)成員
友元
友元關(guān)系是不會(huì)繼承的,可以這樣理解,你長輩的朋友并不是你的朋友,之前的關(guān)系不會(huì)繼承。所以基類的友元不能訪問子類的私有和保護(hù)成員
靜態(tài)成員
無論繼承了多少次,派生了多少子類,靜態(tài)成員在這整個(gè)繼承體系中有且只有一個(gè)。靜態(tài)成員不再單獨(dú)屬于某一個(gè)類亦或者是某一個(gè)對(duì)象,而是屬于這一整個(gè)繼承體系
多繼承
如果一個(gè)子類同時(shí)繼承兩個(gè)或以上的父類時(shí),此時(shí)就是多繼承。
多繼承雖然能很好的繼承多個(gè)父類的特性,達(dá)到復(fù)用代碼的效果,但是他也有著很多的隱患,例如菱形繼承的問題,這也就是為什么后期的一些語言如java把多繼承去掉的原因。
菱形繼承
那么, 什么是菱形繼承呢?這里就舉一個(gè)例子
為了方便觀看我就只保留成員變量
這里有著人類,學(xué)生類,老師類。在學(xué)校中,還同時(shí)具有老師和學(xué)生這兩個(gè)屬性的人,也就是助教,助教可能是本科的老師助理,同時(shí)也是研究生或者博士生在讀。所以我們可以讓他同時(shí)繼承teacher類和student類
按照道理來說,各個(gè)類的大小應(yīng)該是這樣的。human類4個(gè)字節(jié),teacher和student都是8個(gè)字節(jié),而assistant是12個(gè)字節(jié)。
但是實(shí)際上assistant卻是16字節(jié)
這就是菱形繼承的數(shù)據(jù)冗余和二義性問題的體現(xiàn)。
這里的teacher和student都從human中繼承了相同的成員_age。但是assistant再從teacher和student繼承時(shí),就分別把這兩個(gè)_age都給繼承了過來,導(dǎo)致這里有了兩個(gè)一樣的成員
這就是數(shù)據(jù)冗余問題。
倘若我們想要給那個(gè)_age賦值
因?yàn)槔锩娲嬖趦蓚€(gè)一樣的_age,這是編譯器就會(huì)報(bào)錯(cuò)通知我們指定的不夠明確。我們還需要指定作用域
這也就是二義性問題。
那么怎樣才能解決這個(gè)問題呢,那就得用到虛繼承來解決。
虛繼承
解決這個(gè)問題很簡單,當(dāng)多個(gè)類繼承同一個(gè)類時(shí),就在繼承這個(gè)類時(shí),為其添加一個(gè)虛擬繼承的屬性。
如:
這時(shí)就可以看到,它只繼承了一次。
接下來看看大小
按照道理來說,他應(yīng)該是12字節(jié),為什么呢?
這里就牽扯到了C++的對(duì)象模型
因?yàn)槲疫€沒有詳細(xì)的了解C++的對(duì)象模型,只是草草的讀了幾篇博客和看了一點(diǎn)點(diǎn)的C++對(duì)象模型中的其中一小節(jié)。所以這里引用一下別的大佬的博客,我再稍微總結(jié)一下。
從內(nèi)存布局看C++虛繼承的實(shí)現(xiàn)原理
這里多出來的8個(gè)字節(jié),其實(shí)是兩個(gè)虛基表指針。
因?yàn)檫@里Human中的_age是teacher和student共有的,所以為了能夠方便處理,在內(nèi)存中分布的時(shí)候,就會(huì)把這個(gè)共有成員_age放到對(duì)象組成的最末尾的位置。然后在建立一個(gè)虛基表,這個(gè)表記錄了各個(gè)虛繼承的類在找到這個(gè)共有的元素時(shí),在內(nèi)存中偏移量的大小,而虛基表指針則指向了各自的偏移量。
這里打個(gè)比方:
通過這個(gè)偏移量,他們能夠找到自己的_age的位置。
為什么需要這個(gè)偏移量呢?
int main() {Assistant a;Teacher t = a; Student s = a;return 0; }拿這個(gè)舉例子,當(dāng)把對(duì)象a賦值給t和s的時(shí)候,因?yàn)樗麄兓ハ鄾]有對(duì)方的_stuNum和_teaNum,所以他們需要進(jìn)行對(duì)象的切割,但是又因?yàn)開age存放在對(duì)象的最尾部,所以只有知道了自己的偏移量,才能夠成功的在切割了沒有的元素時(shí),還能找到自己的_age。
從這里就可以看出來,多繼承存在著很多缺點(diǎn),尤其是菱形繼承,他在底層引出了很多復(fù)雜的模型和概念,這也是為什么很多面向?qū)ο笳Z言都刪除了多繼承的原因,因?yàn)樗茈y掌控。所以在使用是需要非常謹(jǐn)慎,盡量不要使用多繼承,更不要構(gòu)造出菱形繼承。
繼承和組合
什么是組合
那除了繼承還有什么好的代碼復(fù)用方式嗎?那答案肯定是有的,就是組合。
組合是什么呢?組合就是將多個(gè)類組合在一起,實(shí)現(xiàn)代碼復(fù)用。
繼承和組合又有什么區(qū)別呢?這里就舉幾個(gè)例子
1.繼承
繼承是一種is a的關(guān)系,就是基類是一個(gè)大類,而派生類則是這個(gè)大類中細(xì)分出來的一個(gè)子類,但是他們本質(zhì)上其實(shí)是一種東西。
如:
學(xué)生也是人,所以他可以很好的繼承人的所有屬性,并在其實(shí)增加學(xué)生獨(dú)有的屬性。
2.組合
組合是一種has a的關(guān)系,就是一種包含關(guān)系,比如對(duì)象a是對(duì)象b的成員,那么他們的關(guān)系就是對(duì)象b的組成中包含了對(duì)象a,對(duì)象a是對(duì)象b中的一部分,對(duì)象b包含對(duì)象a。
例如:
這里的Student類中包含了一個(gè)Study類,學(xué)習(xí)是學(xué)生中非常重要的一部分,并且是不可缺少的一部分,每個(gè)學(xué)生都需要學(xué)習(xí),學(xué)習(xí)是學(xué)生的本職。
如何選擇組合和繼承
如果綜合考慮的話,其實(shí)應(yīng)該多使用組合,因?yàn)樵诮M合中,幾個(gè)類的關(guān)聯(lián)不大,你是我的一部分,所以我也只需要用到你那部分的某個(gè)功能,我并不需要了解你的實(shí)現(xiàn)和細(xì)節(jié),只需要你開放對(duì)應(yīng)的接口即可,并且如果我要修改,只修改那一部分功能即可。所以這就導(dǎo)致了組合的依賴關(guān)系弱,耦合度低,十分符合軟件工程的低耦合,高類聚。這樣保證了代碼具有良好的封裝性和可維護(hù)性。
那么繼承呢?繼承的依賴關(guān)系就非常的強(qiáng),耦合度非常高。因?yàn)槟阋朐谧宇愔行薷暮驮黾幽承┕δ?#xff0c;就必須要了解父類的某些細(xì)節(jié),并且有時(shí)候甚至?xí)薷牡礁割?#xff0c;父類的內(nèi)部細(xì)節(jié)在子類中也一覽無余,嚴(yán)重的破壞了封裝性。并且一旦基類發(fā)生變化時(shí),牽一發(fā)而動(dòng)全身,所有的派生類都會(huì)有影響,這樣的代碼維護(hù)性會(huì)非常的差,因?yàn)楹茈y在不了解具體細(xì)節(jié)的情況下能夠不影響到子類的實(shí)現(xiàn)。但是繼承也不是無用的,很多關(guān)系都很適合用繼承,并且多態(tài)的實(shí)現(xiàn)也需要用到繼承,這個(gè)還是得參考具體場景。
但是大部分場景下,如果繼承和組合都可以選擇,那就優(yōu)先選擇組合。
這個(gè)大佬講的也非常好,可以借鑒學(xué)習(xí)一下
優(yōu)先使用對(duì)象組合,而不是類繼承
總結(jié)
以上是生活随笔為你收集整理的C++ 面向对象(一)继承:继承、对象切割、菱形继承、虚继承、继承与组合的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: C++ 泛型编程(二):非类型模板参数,
- 下一篇: 计算机网络 | 网络基础 :网络协议,协