C++ 继承 | 对象切割、菱形继承、虚继承、对象组合
文章目錄
- 繼承
- 繼承的概念
- 繼承方式及權(quán)限
- using改變成員的訪問(wèn)權(quán)限
- 基類與派生類的賦值轉(zhuǎn)換
- 回避虛函數(shù)機(jī)制
- 派生類的默認(rèn)成員函數(shù)
- 友元與靜態(tài)成員
- 多繼承
- 菱形繼承
- 虛繼承
- 組合
繼承
繼承的概念
繼承可以使得子類具有父類的屬性和方法或者重新定義、追加屬性和方法等。
當(dāng)創(chuàng)建一個(gè)類時(shí),我們可以繼承一個(gè)已有類的成員和方法,并且在原有的基礎(chǔ)上進(jìn)行提升,這個(gè)被繼承的類叫做基類,而這個(gè)繼承后新建的類叫做派生類。基類必須是已經(jīng)定義而非僅僅聲明,因此,一個(gè)類不能派生它本身。
繼承這種通過(guò)生成子類的復(fù)用通常被稱為 白箱復(fù)用(white-box reuse) 。術(shù)語(yǔ) 白箱 是相對(duì)可視性而言:在繼承方式中,父類的內(nèi)部細(xì)節(jié)對(duì)子類可見(jiàn)。
派生類的作用域嵌套在基類的作用域之內(nèi)。
class [派生類名] : [繼承類型] [基類名][繼承類型] [基類名] 的組合被稱為派生列表,值得注意的是,派生列表僅出現(xiàn)在定義中,而不能出現(xiàn)在聲明中:
class A : public B; // ERROR:派生列表不能出現(xiàn)在聲明中正確實(shí)現(xiàn)如下:
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() // 將父類的Print函數(shù)重定向成自己的Print函數(shù){Human::Print();cout << "stuNum:" << _stuNum << endl;}protected:string _stuNum; // 增加的成員變量 };int main() {Human h1;Student s1;h1.Print();cout << endl;s1.Print();return 0; }基類和派生類都具有他們各自的作用域,那如果出現(xiàn)同名的成員(如上面的 Print函數(shù) ),此時(shí)會(huì)怎么樣呢?這里就要牽扯到一個(gè)概念——隱藏。(隱藏而非 重載 ,方法名雖然相同,但處于不同的作用域中。)
隱藏:也叫做重定義,當(dāng)基類和派生類中出現(xiàn)重名的成員時(shí),派生類就會(huì)將基類的同名成員給隱藏起來(lái),然后使用自己的。(但是隱藏并不意味著就無(wú)法訪問(wèn),可以通過(guò)聲明基類作用域來(lái)訪問(wèn)到隱藏成員。)
因此 s1 調(diào)用 Print函數(shù) 時(shí)不會(huì)調(diào)用父類的,而是調(diào)用自己的。
繼承方式及權(quán)限
繼承的方式和類的訪問(wèn)限定符一樣,分為 public(公有繼承)、private(私有繼承)、protected(保護(hù)繼承) 三種。
關(guān)于 protected :
基類成員的訪問(wèn)說(shuō)明符/派生類的繼承方式
總結(jié)來(lái)講父類成員的訪問(wèn)權(quán)限決定了子類是否能訪問(wèn)該成員,而繼承方式?jīng)Q定了父類成員在子類中的新權(quán)限是怎樣的:
- public:繼承自父類的成員在父類中是什么權(quán)限,子類中就是什么權(quán)限。
- protected:繼承自父類的成員其訪問(wèn)權(quán)限都變成 protected。
- private:繼承自父類的成員其訪問(wèn)權(quán)限都變成 private。
派生類向基類轉(zhuǎn)換的可訪問(wèn)性
假設(shè) D 繼承自 B:
- 只有當(dāng) D 公有地繼承 B 時(shí),派生類對(duì)象才能使用派生類向基類的轉(zhuǎn)換;如果繼承方式是受保護(hù)的或者私有的,則不能使用該轉(zhuǎn)換。
- 不論 D 以什么方式繼承 B,D 的成員函數(shù)和友元都能使用派生類向基類的轉(zhuǎn)換。換言之,派生類向其直接基類的類型轉(zhuǎn)換對(duì)于派生類的成員和友元來(lái)說(shuō)永遠(yuǎn)是可訪問(wèn)的。
- 如果繼承方式是公有的或者受保護(hù)的,則派生類的成員和友元可以使用 D 向 B 的類型轉(zhuǎn)換;反之,如果繼承方式是私有的,則不能使用。
默認(rèn)的繼承方式
默認(rèn)情況下:
- 使用 class 關(guān)鍵字定義的派生類是私有繼承的
- 使用 struct 關(guān)鍵字定義的派生類是公有繼承的
using改變成員的訪問(wèn)權(quán)限
我們說(shuō)繼承方式?jīng)Q定了派生類的對(duì)象及派生類的子類對(duì)繼承來(lái)的成員的訪問(wèn)權(quán)限,但這不是絕對(duì)的,我們可以通過(guò) using 改變成員的訪問(wèn)權(quán)限,但只能改變派生類能訪問(wèn)的成員,即基類中的 protected 和 public 成員。
class Human { private:int pri; protected:int pro; public:int pub; };class Teacher : private Human { // 私有繼承 private:// 只能被類的成員or友元訪問(wèn) public:using Human::pri; // 錯(cuò)誤:using只能為派生類可訪問(wèn)的成員提供聲明using Human::pro; // Teacher的對(duì)象、成員、友元、子類都可以訪問(wèn) protected:using Human::pro; // Teacher的對(duì)象、成員、友元可以訪問(wèn)using Human::pub; };基類與派生類的賦值轉(zhuǎn)換
我們?cè)?四種強(qiáng)制轉(zhuǎn)換類型中的 dynamic_cast 部分 提到過(guò)父類與子類的賦值轉(zhuǎn)換
派生類可以賦值給基類的對(duì)象、指針或者引用,這樣的賦值也叫做 對(duì)象切割 。
當(dāng)把派生類賦值給基類時(shí),可以通過(guò)切割掉多出來(lái)的成員如 _stuNum 來(lái)完成賦值。
但是 基類對(duì)象 不可以賦值給 派生類 ,因?yàn)樗荒軕{空多一個(gè) _stuNum 成員出來(lái)。
但是 基類的指針卻可以通過(guò)強(qiáng)制類型轉(zhuǎn)換賦值給派生類對(duì)象 , 如:
int main() {Human h1;Student s1;Human* hPtrs = &s1; // 指向派生類對(duì)象Human* hPtrh = &h1; // 指向基類對(duì)象// 傳統(tǒng)方法Student* pPtr = (Student*)hPtrs; // 沒(méi)問(wèn)題Student* pPtr = (Student*)hPtrh; // 有時(shí)候沒(méi)有問(wèn)題,但是會(huì)存在越界風(fēng)險(xiǎn)// 如果父類之中包含虛函數(shù),可以使用dynamic_castStudent* pPtr = dynamic_cast<Student*>(hPtrh);// 如果確認(rèn)基類向派生類的轉(zhuǎn)換是安全的,可以使用static_castStudent* pPtr = static_cast<Student*>(hPtrs);return 0; }總結(jié)來(lái)講:
- 派生類可以賦值給基類的對(duì)象、指針或者引用
- 基類對(duì)象不能賦值給派生類對(duì)象
- 基類的指針可以通過(guò)強(qiáng)制類型轉(zhuǎn)換賦值給派生類的指針。但是必須是基類的指針是指向派生類對(duì)象時(shí)才是安全的,否則會(huì)存在越界的風(fēng)險(xiǎn)。但基類如果是多態(tài)類型(父類之中包含虛函數(shù)),則可以使用 RTTI 的 dynamic_cast 來(lái)實(shí)現(xiàn) 指向基類的基類指針 到 派生類對(duì)象 的安全轉(zhuǎn)換。
回避虛函數(shù)機(jī)制
我們說(shuō)多態(tài)是為了實(shí)現(xiàn)子類對(duì)于同一操作的不同結(jié)果,但有時(shí)候,派生類需要調(diào)用其父類的虛函數(shù)版本,而非自己的虛函數(shù)版本:
int main() {Human h1;Student s1;Human* hPtrs = &s1; // 指向派生類對(duì)象hPtrs->print(); // 由于hPtrs指向子類對(duì)象,因此調(diào)用子類的虛函數(shù)hPtrs->Human::print(); // 強(qiáng)行調(diào)用Human中的虛函數(shù),而不在意hPtrs的動(dòng)態(tài)類型return 0; }派生類的默認(rèn)成員函數(shù)
之前有寫(xiě)過(guò) 類的默認(rèn)六個(gè)成員函數(shù)
class Human { public:Human(){cout << "Human 構(gòu)造函數(shù)" << endl;}~Human(){cout << "Human 析構(gòu)函數(shù)" << endl;}protected:string _name;int _age; };class Student : public Human { public:Student(){cout << "Student 構(gòu)造函數(shù)" << _name << endl;}~Student(){//~Human(); 不需要手動(dòng)調(diào)用父類的析構(gòu)函數(shù),編譯器會(huì)在子類析構(gòu)函數(shù)結(jié)束后自動(dòng)調(diào)用。cout << "Student 析構(gòu)函數(shù)" << endl;} protected:string _stuNum; };int main() {Student s1;return 0; }
可以看到,調(diào)用派生類的默認(rèn)成員函數(shù)時(shí)都會(huì)調(diào)用基類的默認(rèn)構(gòu)造函數(shù)。
- 派生類的構(gòu)造函數(shù)必須調(diào)用基類的構(gòu)造函數(shù)初始化基類的那一部分成員。如果基類沒(méi)有默認(rèn)的構(gòu)造函數(shù),則必須在派生類構(gòu)造函數(shù)的初始化列表階段顯示調(diào)用。
- 派生類的拷貝構(gòu)造函數(shù)必須調(diào)用基類的拷貝構(gòu)造完成基類的拷貝初始化。
- 派生類的 operator= 必須要調(diào)用基類的 operator= 完成基類的復(fù)制。
- 派生類的析構(gòu)函數(shù)會(huì)在被調(diào)用完成后自動(dòng)調(diào)用基類的析構(gòu)函數(shù)清理基類成員。
在派生類的析構(gòu)函數(shù)中,基類的析構(gòu)函數(shù)會(huì)被隱藏,為了實(shí)現(xiàn)多態(tài),它們都會(huì)被編譯器重命名為 destructor 。
友元與靜態(tài)成員
友元
友元關(guān)系是不會(huì)繼承的(友元關(guān)系不具有傳遞性),可以這樣理解,你長(zhǎng)輩的朋友并不是你的朋友。
- 基類的友元能訪問(wèn)基類的私有/保護(hù)成員,但不能訪問(wèn)子類的私有/保護(hù)成員。(當(dāng)然基類本身也無(wú)法訪問(wèn)子類的私有成員。)
- 子類訪問(wèn)父類友元的私有成員就更不用想了:
- 一是友元關(guān)系并不對(duì)稱,A 是 B 的友元,B 不一定是 A 的友元,也就是說(shuō)父類本身都不一定能訪問(wèn)父類友元的私有成員(父類不一定是其友元的友元),何況子類;
- 二是就算父類是其友元的友元,但友元關(guān)系不具有傳遞性,子類不一定是父類友元的友元。
- 子類的友元無(wú)法訪問(wèn)父類的私有/保護(hù)成員。
靜態(tài)成員
無(wú)論繼承了多少次,派生了多少子類,靜態(tài)成員在這整個(gè)繼承體系中有且只有一個(gè)。靜態(tài)成員不再單獨(dú)屬于某一個(gè)類亦或者是某一個(gè)對(duì)象,而是屬于這一整個(gè)繼承體系。
多繼承
如果一個(gè)子類同時(shí)繼承兩個(gè)或以上的父類時(shí),此時(shí)就是多繼承。
多繼承雖然能很好的繼承多個(gè)父類的特性,達(dá)到復(fù)用代碼的效果,但是他也有著很多的隱患,例如菱形繼承的問(wèn)題,這也就是為什么后期的一些語(yǔ)言如 java 把多繼承去掉的原因。
菱形繼承
class Human { public:int _age; };class Student : public Human { public:int _stuNum; };class Teacher : public Human { public:int _teaNum; };這里有著人類、學(xué)生類、老師類。在學(xué)校中,還存在著同時(shí)具有老師和學(xué)生這兩個(gè)屬性的人,也就是助教。所以我們可以讓他同時(shí)繼承 teacher類 和 student類 。
class Assistant : public Teacher, public Student { };按照道理來(lái)說(shuō),各個(gè)類的大小應(yīng)該是這樣的。Human 類4個(gè)字節(jié),Teacher 和 Student 都是8個(gè)字節(jié),而 Assistant 是12個(gè)字節(jié)。但是實(shí)際上 Assistant 卻是16字節(jié)。
這就是菱形繼承的 數(shù)據(jù)冗余 和 二義性 問(wèn)題的體現(xiàn)。
這里的 Teacher 和 Student 都從 Human 中繼承了相同的成員 _age 。但是 Assistant 再?gòu)?Teacher 和 Student 繼承時(shí),就分別把這兩個(gè) _age 都給繼承了過(guò)來(lái)。
這就是數(shù)據(jù)冗余問(wèn)題。
倘若我們想要給那個(gè) _age 賦值:
因?yàn)槔锩娲嬖趦蓚€(gè)一樣的 _age ,因此需要指定作用域:
這也就是二義性問(wèn)題。
虛繼承
想解決二義性很簡(jiǎn)單,當(dāng)多個(gè)類繼承同一個(gè)類時(shí),就在繼承這個(gè)類時(shí),為其添加一個(gè)虛擬繼承的屬性。
class Student : virtual public Human { public:int _stuNum; };class Teacher : virtual public Human { public:int _teaNum; };這時(shí)就可以看到,它只繼承了一次。
接下來(lái)看看大小:
按照道理來(lái)說(shuō),a 應(yīng)該是 12字節(jié),t 、 s 應(yīng)該是 8字節(jié) 啊?這里就牽扯到了C++的對(duì)象模型,先推薦一篇博客: C++中的虛函數(shù)(表)實(shí)現(xiàn)機(jī)制以及用C語(yǔ)言對(duì)其進(jìn)行的模擬實(shí)現(xiàn)
這里多出來(lái)的 8個(gè)字節(jié),其實(shí)是兩個(gè)虛基表指針(vbptr)。同理,s 、t 多出來(lái)的 4字節(jié),是 一個(gè)vbptr 。
因?yàn)檫@里 Human 中的 _age 是 teacher 和 student 共有的,所以為了能夠方便處理,在內(nèi)存中分布的時(shí)候,就會(huì)把這個(gè)共有成員 _age 放到對(duì)象組成的最末尾的位置。然后在建立一個(gè)虛基表,這個(gè)表記錄了各個(gè)虛繼承的類在找到這個(gè)共有的元素時(shí),在內(nèi)存中偏移量的大小,而虛基表指針則指向了各自的偏移量。
這里打個(gè)比方:
通過(guò)這個(gè)偏移量,他們能夠找到自己的 _age 的位置。
為什么需要這個(gè)偏移量呢?
int main() {Assistant a;Teacher t = a; Student s = a;return 0; }如上,當(dāng)把對(duì)象 a 賦值給 t 和 s 的時(shí)候,因?yàn)樗麄兓ハ鄾](méi)有對(duì)方的 _stuNum 和 _teaNum,所以他們需要進(jìn)行對(duì)象的切割,但是又因?yàn)?_age 存放在對(duì)象的最尾部,所以只有知道了自己的偏移量,才能夠成功的在切割了沒(méi)有的元素時(shí),還能找到自己的 _age 。
組合
那除了繼承還有什么好的代碼復(fù)用方式嗎?那答案肯定是有的,就是組合。組合就是將多個(gè)類組合在一起,實(shí)現(xiàn)代碼復(fù)用。
繼承和組合又有什么區(qū)別呢?
- 繼承是一種 is a 的關(guān)系,基類是一個(gè)大類,而派生類則是這個(gè)大類中細(xì)分出來(lái)的一個(gè)子類,但是他們本質(zhì)上其實(shí)是一種東西。正如:學(xué)生也是人,所以他可以很好的繼承人的所有屬性,并增加學(xué)生獨(dú)有的屬性。
- 組合是一種 has a 的關(guān)系,就是一種包含關(guān)系。對(duì)象a 是 對(duì)象b 中的一部分,對(duì)象b 包含 對(duì)象a 。
組合這種通過(guò)對(duì)方開(kāi)放接口來(lái)實(shí)現(xiàn)的復(fù)用被稱為 黑箱復(fù)用(black-box reuse),因?yàn)閷?duì)象的內(nèi)部細(xì)節(jié)是不可見(jiàn)的。對(duì)象只以 黑箱 的形式出現(xiàn)。
class Study { public:void ToStudy(){cout << "Study" << endl;} };class Student : public Human { public:Study _s;int _stuNum; };這里的 Student類 中包含了一個(gè) Study類 ,學(xué)習(xí)是學(xué)生日常生活中不可缺少的一部分。
比較組合和繼承:
- 組合的依賴關(guān)系弱,耦合度低。保證了代碼具有良好的封裝性和可維護(hù)性。 在組合中,幾個(gè)類的關(guān)聯(lián)不大,我只需要用到你那部分的某個(gè)功能,我并不需要了解你的實(shí)現(xiàn)細(xì)節(jié),只需要你開(kāi)放對(duì)應(yīng)的接口即可,并且如果我要修改,只修改那一部分功能即可。
- 繼承的依賴關(guān)系就非常的強(qiáng),耦合度非常高。 因?yàn)槟阋朐谧宇愔行薷暮驮黾幽承┕δ?#xff0c;就必須要了解父類的某些細(xì)節(jié),并且有時(shí)候甚至?xí)薷牡礁割?#xff0c;父類的內(nèi)部細(xì)節(jié)在子類中也一覽無(wú)余,嚴(yán)重的破壞了封裝性。并且一旦基類發(fā)生變化時(shí),牽一發(fā)而動(dòng)全身,所有的派生類都會(huì)有影響,這樣的代碼維護(hù)性會(huì)非常的差。一個(gè)可用的解決方法就是只繼承抽象類,因?yàn)槌橄箢愅ǔL峁┹^少的實(shí)現(xiàn)。
但是大部分場(chǎng)景下,如果繼承和組合都可以選擇,那么 優(yōu)先使用對(duì)象組合,而不是類繼承 。
總結(jié)
以上是生活随笔為你收集整理的C++ 继承 | 对象切割、菱形继承、虚继承、对象组合的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 建行分期通没有pos机如何刷出来?这是没
- 下一篇: C++ new和delete