C++中的繼承問題(繼承基本概念、菱形虛擬繼承的對(duì)象模型)
一、繼承的概念與定義格式
概念及定義格式
繼承機(jī)制是面向?qū)ο蟪绦蛟O(shè)計(jì)使代碼可以復(fù)用的最重要手段,它允許程序員在保留原有類特性的基礎(chǔ)上進(jìn)行擴(kuò)展,增加功能,這樣產(chǎn)生的類,稱為派生類。繼承呈現(xiàn)了面向?qū)ο蟪绦蛟O(shè)計(jì)的層次結(jié)構(gòu),體現(xiàn)了由簡(jiǎn)單到復(fù)雜的認(rèn)知過程。繼承是類設(shè)計(jì)層次的復(fù)用。
個(gè)人理解:父類實(shí)際上是抽取類的共性,將其它類都有的屬性和方法進(jìn)行提取,再定義其它類時(shí)只需要繼承父類,并寫出該類獨(dú)有的屬性即可。
以Person類為父類,Student類為學(xué)生類舉例:
父類
子類
這里Student類繼承了Person類,學(xué)生中就包含Person類中的name和age兩個(gè)屬性,只需要再寫出Student類獨(dú)有的num屬性即可。
訪問限定符與繼承權(quán)限
一句話總結(jié)上面的表格:繼承權(quán)限決定了子類能繼承的父類的最高權(quán)限。即public繼承不會(huì)改變類成員的訪問權(quán)限;protected繼承方式會(huì)改變?cè)瓉碓L問權(quán)限為public的成員;private繼承方式會(huì)影響原來訪問權(quán)限為public和protected的成員。
另外還有幾點(diǎn)要注意:
父類的private成員被子類繼承了,但是子類不能訪問父類的private成員,通過查看子類的大小可以得知,子類中包含繼承自父類的私有成員變量。
在子類中訪問父類私有成員會(huì)報(bào)錯(cuò):
查看子類大?。?/p>
- protected成員訪問限定符只因?yàn)槔^承體系才出現(xiàn)的,因?yàn)閜rotected在繼承中才有意義
- 實(shí)際中一般使用public繼承
- 使用關(guān)鍵字class默認(rèn)的繼承方式是private,使用struct默認(rèn)的繼承方式是public,一般最好顯式給出繼承權(quán)限。
ps: class和struct的區(qū)別
- 定義類的默認(rèn)訪問權(quán)限不同,class為私有,struct為公有,兼容C語言
- 模板參數(shù)列表中可以使用class,不能使用struct
- 繼承中的默認(rèn)繼承權(quán)限不同,class默認(rèn)private,struct默認(rèn)public
二、賦值兼容規(guī)則
這里的復(fù)制兼容規(guī)則是在public繼承的前提下:
- 可以使用子類對(duì)象給父類對(duì)象賦值賦值,但是不能使用父類對(duì)象給子類對(duì)象賦值
- 可以使用父類指針指向子類對(duì)象,但不能使用子類指針指向父類對(duì)象,如果一定要指向,進(jìn)行強(qiáng)制類型轉(zhuǎn)換后可以,但是會(huì)有指針越界訪問的問題。
- 可以使用父類的引用去引用子類,不能使用子類的引用引用父類,與指針原理相同。
仍以 Person類和Student類舉例:
Person類:
lass Person { protected: string _name; int _age ; };
Student類繼承Person類:
class Student :public Person { protected: int _num = 1; };
分別驗(yàn)證賦值、指針和引用:
原理如圖:
指針和引用原理與上圖相同,父類的指針可以指向子類中繼承自父類的部分;但是子類的指針如果指向父類,訪問_name和_age時(shí)不會(huì)有問題,訪問到_num時(shí)就會(huì)超出父類對(duì)象的范圍,越界訪問,所以編譯器禁止了子類指針指向父類對(duì)象。
三、繼承中的作用域
- 在繼承體系中,父類和子類都有獨(dú)立的作用域
- 如果父類和子類中有同名成員,子類成員會(huì)屏蔽對(duì)父類同名成員的直接訪問,優(yōu)先訪問自己類中的成員,即同名隱藏,也叫重定義。
- 對(duì)于成員函數(shù),只要函數(shù)名相同就構(gòu)成重定義,與類型無關(guān)。
Person類:
class Person { public: void Print() { cout << "Person name:" << _name << endl; cout << "Person age" << _age << endl; } protected: string _name = "ZS"; int _age = 17; };
Student類繼承Person類:
class Student :public Person { public: void Print() { cout << "Student name:" << _name << endl; cout << "Student age:" << _age << endl; cout << "Student num:" << _num << endl; } protected: string _name = "LS"; int _age = 18; int _num = 2; };
驗(yàn)證結(jié)果:
當(dāng)不加作用域限定符時(shí),子類對(duì)象會(huì)優(yōu)先訪問自己的成員變量和成員函數(shù)。
對(duì)程序稍作修改:
這里兩個(gè)Print函數(shù)的參數(shù)不同,看起來像“重載”,但是實(shí)際上是同名隱藏,子類中對(duì)父類的Print函數(shù)進(jìn)行了重定義。
四、子類的默認(rèn)成員函數(shù)
構(gòu)造函數(shù)
父類 沒有顯式定義構(gòu)造函數(shù) 或者父類有 全缺省的構(gòu)造函數(shù) 或者 無參的構(gòu)造函數(shù) ,子類可以不定義構(gòu)造函數(shù)。
即下面三種情況,子類都可以不顯式地給出構(gòu)造函數(shù):
但是如果父類顯式定義了構(gòu)造函數(shù),且不是無參或者全缺省的,子類必須顯式定義構(gòu)造函數(shù),并在初始化列表顯式調(diào)用父類的構(gòu)造函數(shù),因?yàn)槿绻伙@式定義,編譯器會(huì)自動(dòng)調(diào)用父類默認(rèn)拷貝構(gòu)造函數(shù),而父類沒有默認(rèn)的構(gòu)造函數(shù),便會(huì)報(bào)錯(cuò):
正確的寫法:
這里的name是傳遞給Person類構(gòu)造函數(shù)的實(shí)參,即:用name給Student對(duì)象中繼承的_name賦值。
構(gòu)造一個(gè)Student類的對(duì)象分兩步:
- 將從父類繼承的成員初始化
- 將子類新增加的成員初始化
拷貝構(gòu)造函數(shù)
子類的拷貝構(gòu)造函數(shù)必須在初始化列表中顯式調(diào)用父類的拷貝構(gòu)造函數(shù)。
父類沒有定義拷貝構(gòu)造函數(shù),子類可以定義也可以不定義;父類如果定義了拷貝構(gòu)造函數(shù),子類一般要定義,并且要在初始化列表中調(diào)用父類的拷貝構(gòu)造函數(shù)完成從父類繼承的成員的拷貝初始化,否則會(huì)報(bào)錯(cuò):
正確寫法:
此處s是傳遞給拷貝構(gòu)造函數(shù)的參數(shù)。
賦值運(yùn)算符重載
子類的賦值運(yùn)算符重載函數(shù)必須調(diào)用父類的賦值運(yùn)算符重載完成對(duì)父類的賦值。
父類的賦值運(yùn)算符重載:
子類:
析構(gòu)函數(shù)
子類析構(gòu)函數(shù)會(huì)在被調(diào)用完后自動(dòng)調(diào)用父類的析構(gòu)函數(shù)完成清理父類成員,所以清理順序是:先清理子類,再清理父類。
構(gòu)造和析構(gòu)函數(shù)調(diào)用順序
構(gòu)造子類對(duì)象時(shí),先調(diào)用父類的構(gòu)造函數(shù),再調(diào)用子類的構(gòu)造函數(shù),清理對(duì)象時(shí),先調(diào)用子類的析構(gòu)函數(shù),再調(diào)用父類的析構(gòu)函數(shù)。
如圖:
因?yàn)闃?gòu)造子類對(duì)象時(shí)會(huì)在初始化列表中調(diào)用父類的構(gòu)造函數(shù),執(zhí)行完之后才會(huì)執(zhí)行子類的構(gòu)造函數(shù)的函數(shù)體,所以父類的構(gòu)造會(huì)先于子類的構(gòu)造執(zhí)行。
五、繼承與友元、靜態(tài)成員
友元關(guān)系
友元關(guān)系不能繼承
tips:王叔是你父親的好朋友,但是不一定是你的好朋友,王叔的財(cái)產(chǎn)不會(huì) 給你繼承??
定義一個(gè)Display函數(shù),并在Person類中聲明為友元類:
在Display函數(shù)中可以訪問Person類的protected成員,但是不能訪問其子類Student類成員,友元關(guān)系不能繼承。
靜態(tài)成員
父類中聲明了static靜態(tài)成員,則整個(gè)繼承體系只有一個(gè)這樣的成員。無論派生出多少子類,都只有一個(gè)static成員實(shí)例。
定義A、B、C三個(gè)類:
class A { protected: int _a; public: static int _count;//類中聲明為靜態(tài)成員 }; int A::_count = 0;//類外定義
class B:public A { protected: int _b; };
class C :public B { protected: int _c; };
通過不同對(duì)象訪問_count:
六、菱形繼承及菱形虛擬繼承
菱形繼承概念
單繼承
多繼承
菱形繼承
可以看出,菱形繼承實(shí)際就是單繼承和多繼承組合的結(jié)果,是多繼承的一種特殊情況。
菱形繼承實(shí)例:
注:驗(yàn)證環(huán)境為VS2022,win32平臺(tái)
這里C類的大小是20字節(jié),除了其本身成員的4字節(jié),另外16個(gè)字節(jié)都是從兩個(gè)父類繼承來的。模型如圖:
存在問題
對(duì)于上面圖中的菱形繼承,存在的問題十分明顯,那就是數(shù)據(jù)冗余和二義性問題。即Teacher類和Student類都繼承自Person類,那么兩個(gè)類中都會(huì)包含Person類中的成員,Assistant繼承這兩個(gè)類之后,同樣的成員便會(huì)包含兩份,導(dǎo)致數(shù)據(jù)重復(fù),并且在通過Assistant對(duì)象訪問Person類中的成員時(shí),會(huì)有二義性。
通過添加作用域限定符可以解決訪問二義性的問題,如:as.Teacher::_name類似的語句可以指定通過哪個(gè)父類訪問Person類的對(duì)象,但是無法從根本解決數(shù)據(jù)冗余的問題,所以便引入了虛擬繼承的概念。
虛擬繼承的概念
虛擬繼承是指在繼承權(quán)限前面加上一個(gè)virtura關(guān)鍵字
class B1:virtual public A { public: int _b1; };
用虛擬繼承可以解決菱形繼承的二義性和數(shù)據(jù)冗余的問題。對(duì)于上面的菱形繼承,在B1和B2繼承A時(shí)使用虛擬繼承即可解決問題。
虛擬繼承的模型
對(duì)于上面的菱形虛擬繼承,研究其模型。
通過sizeof打印輸出獲取c對(duì)象的大小為24字節(jié):
通過下面的語句為c對(duì)象中的成員賦值:
void Test() { C c; c._a = 1; c._b1 = 2; c._b2 = 3; c._c = 4; cout << sizeof(c) << endl; }
查看其內(nèi)存分布
所以,菱形虛擬繼承將最上面的父類中的成員只保存了一份,并用一個(gè)偏移量指針指向偏移量表格,偏移量表格中保存的就是最上面的父類中的成員變量相對(duì)于當(dāng)前對(duì)象的偏移量。
最終得到的菱形虛擬繼承對(duì)象內(nèi)存模型如圖:
對(duì)象模型與偏移量表格:
總結(jié):假設(shè)B1和B2繼承自A類,最下面的C類繼承自B1和B2類;菱形虛擬繼承是指兩個(gè)子類繼承自同一個(gè)父類時(shí),將繼承方式設(shè)置為虛擬繼承;構(gòu)建對(duì)象時(shí),最頂層的父類A中的成員變量只保存一份,在對(duì)象模型的最下面,這樣就避免了數(shù)據(jù)冗余;B1和B2都有屬于自己的虛基表指針,通過虛基表中的偏移量找到最頂層父類中的_a成員變量;
B1和B1類中都有自己的虛表,這樣便可以通過C類對(duì)象給B1類和者B2類的指針、引用或者對(duì)象賦值,滿足賦值兼容規(guī)則
附錄:
總結(jié)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
C++數(shù)據(jù)封裝以及定義結(jié)構(gòu)的詳細(xì)講解
這篇文章主要詳細(xì)講解了C++數(shù)據(jù)封裝以及定義結(jié)構(gòu),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-05-05C++數(shù)據(jù)結(jié)構(gòu)之雙向鏈表
這篇文章主要為大家詳細(xì)介紹了C++數(shù)據(jù)結(jié)構(gòu)之雙向鏈表,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-05-05select函數(shù)實(shí)現(xiàn)高性能IO多路訪問的關(guān)鍵示例深入解析
這篇文章主要為大家介紹了select函數(shù)實(shí)現(xiàn)高性能IO多路訪問的關(guān)鍵示例深入解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-09-09C++實(shí)現(xiàn)打地鼠游戲設(shè)計(jì)
這篇文章主要為大家詳細(xì)介紹了C++實(shí)現(xiàn)打地鼠游戲設(shè)計(jì),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-12-12C語言中操作sqlserver數(shù)據(jù)庫案例教程
這篇文章主要介紹了C語言中操作sqlserver數(shù)據(jù)庫案例教程,本篇文章通過簡(jiǎn)要的案例,講解了該項(xiàng)技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下2021-07-07C語言中等待socket連接和對(duì)socket定位的方法
這篇文章主要介紹了C語言中等待socket連接和對(duì)socket定位的方法,分別為listen()函數(shù)和bind()函數(shù)的用法,需要的朋友可以參考下2015-09-09C++實(shí)現(xiàn)約瑟夫環(huán)的循環(huán)單鏈表
這篇文章主要為大家詳細(xì)介紹了C++實(shí)現(xiàn)約瑟夫環(huán)的循環(huán)單鏈表,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-10-10zlib庫壓縮和解壓字符串STL string的實(shí)例詳解
這篇文章主要介紹了zlib庫壓縮和解壓字符串STL string的實(shí)例詳解的相關(guān)資料,希望通過本文能幫助到大家,需要的朋友可以參考下2017-10-10