C++中的繼承問(wèn)題(繼承基本概念、菱形虛擬繼承的對(duì)象模型)
一、繼承的概念與定義格式
概念及定義格式
繼承機(jī)制是面向?qū)ο蟪绦蛟O(shè)計(jì)使代碼可以復(fù)用的最重要手段,它允許程序員在保留原有類(lèi)特性的基礎(chǔ)上進(jìn)行擴(kuò)展,增加功能,這樣產(chǎn)生的類(lèi),稱(chēng)為派生類(lèi)。繼承呈現(xiàn)了面向?qū)ο蟪绦蛟O(shè)計(jì)的層次結(jié)構(gòu),體現(xiàn)了由簡(jiǎn)單到復(fù)雜的認(rèn)知過(guò)程。繼承是類(lèi)設(shè)計(jì)層次的復(fù)用。
個(gè)人理解:父類(lèi)實(shí)際上是抽取類(lèi)的共性,將其它類(lèi)都有的屬性和方法進(jìn)行提取,再定義其它類(lèi)時(shí)只需要繼承父類(lèi),并寫(xiě)出該類(lèi)獨(dú)有的屬性即可。
以Person類(lèi)為父類(lèi),Student類(lèi)為學(xué)生類(lèi)舉例:
父類(lèi)

子類(lèi)

這里Student類(lèi)繼承了Person類(lèi),學(xué)生中就包含Person類(lèi)中的name和age兩個(gè)屬性,只需要再寫(xiě)出Student類(lèi)獨(dú)有的num屬性即可。
訪(fǎng)問(wèn)限定符與繼承權(quán)限


一句話(huà)總結(jié)上面的表格:繼承權(quán)限決定了子類(lèi)能繼承的父類(lèi)的最高權(quán)限。即public繼承不會(huì)改變類(lèi)成員的訪(fǎng)問(wèn)權(quán)限;protected繼承方式會(huì)改變?cè)瓉?lái)訪(fǎng)問(wèn)權(quán)限為public的成員;private繼承方式會(huì)影響原來(lái)訪(fǎng)問(wèn)權(quán)限為public和protected的成員。
另外還有幾點(diǎn)要注意:
父類(lèi)的private成員被子類(lèi)繼承了,但是子類(lèi)不能訪(fǎng)問(wèn)父類(lèi)的private成員,通過(guò)查看子類(lèi)的大小可以得知,子類(lèi)中包含繼承自父類(lèi)的私有成員變量。
在子類(lèi)中訪(fǎng)問(wèn)父類(lèi)私有成員會(huì)報(bào)錯(cuò):

查看子類(lèi)大小:

- protected成員訪(fǎng)問(wèn)限定符只因?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ū)別
- 定義類(lèi)的默認(rèn)訪(fǎng)問(wèn)權(quán)限不同,class為私有,struct為公有,兼容C語(yǔ)言
- 模板參數(shù)列表中可以使用class,不能使用struct
- 繼承中的默認(rèn)繼承權(quán)限不同,class默認(rèn)private,struct默認(rèn)public
二、賦值兼容規(guī)則
這里的復(fù)制兼容規(guī)則是在public繼承的前提下:
- 可以使用子類(lèi)對(duì)象給父類(lèi)對(duì)象賦值賦值,但是不能使用父類(lèi)對(duì)象給子類(lèi)對(duì)象賦值
- 可以使用父類(lèi)指針指向子類(lèi)對(duì)象,但不能使用子類(lèi)指針指向父類(lèi)對(duì)象,如果一定要指向,進(jìn)行強(qiáng)制類(lèi)型轉(zhuǎn)換后可以,但是會(huì)有指針越界訪(fǎng)問(wèn)的問(wèn)題。
- 可以使用父類(lèi)的引用去引用子類(lèi),不能使用子類(lèi)的引用引用父類(lèi),與指針原理相同。
仍以 Person類(lèi)和Student類(lèi)舉例:
Person類(lèi):
lass Person
{
protected:
string _name;
int _age ;
};
Student類(lèi)繼承Person類(lèi):
class Student :public Person
{
protected:
int _num = 1;
};
分別驗(yàn)證賦值、指針和引用:

原理如圖:

指針和引用原理與上圖相同,父類(lèi)的指針可以指向子類(lèi)中繼承自父類(lèi)的部分;但是子類(lèi)的指針如果指向父類(lèi),訪(fǎng)問(wèn)_name和_age時(shí)不會(huì)有問(wèn)題,訪(fǎng)問(wèn)到_num時(shí)就會(huì)超出父類(lèi)對(duì)象的范圍,越界訪(fǎng)問(wèn),所以編譯器禁止了子類(lèi)指針指向父類(lèi)對(duì)象。
三、繼承中的作用域
- 在繼承體系中,父類(lèi)和子類(lèi)都有獨(dú)立的作用域
- 如果父類(lèi)和子類(lèi)中有同名成員,子類(lèi)成員會(huì)屏蔽對(duì)父類(lèi)同名成員的直接訪(fǎng)問(wèn),優(yōu)先訪(fǎng)問(wèn)自己類(lèi)中的成員,即同名隱藏,也叫重定義。
- 對(duì)于成員函數(shù),只要函數(shù)名相同就構(gòu)成重定義,與類(lèi)型無(wú)關(guān)。
Person類(lèi):
class Person
{
public:
void Print()
{
cout << "Person name:" << _name << endl;
cout << "Person age" << _age << endl;
}
protected:
string _name = "ZS";
int _age = 17;
};
Student類(lèi)繼承Person類(lèi):
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í),子類(lèi)對(duì)象會(huì)優(yōu)先訪(fǎng)問(wèn)自己的成員變量和成員函數(shù)。
對(duì)程序稍作修改:

這里兩個(gè)Print函數(shù)的參數(shù)不同,看起來(lái)像“重載”,但是實(shí)際上是同名隱藏,子類(lèi)中對(duì)父類(lèi)的Print函數(shù)進(jìn)行了重定義。
四、子類(lèi)的默認(rèn)成員函數(shù)
構(gòu)造函數(shù)
父類(lèi) 沒(méi)有顯式定義構(gòu)造函數(shù) 或者父類(lèi)有 全缺省的構(gòu)造函數(shù) 或者 無(wú)參的構(gòu)造函數(shù) ,子類(lèi)可以不定義構(gòu)造函數(shù)。
即下面三種情況,子類(lèi)都可以不顯式地給出構(gòu)造函數(shù):

但是如果父類(lèi)顯式定義了構(gòu)造函數(shù),且不是無(wú)參或者全缺省的,子類(lèi)必須顯式定義構(gòu)造函數(shù),并在初始化列表顯式調(diào)用父類(lèi)的構(gòu)造函數(shù),因?yàn)槿绻伙@式定義,編譯器會(huì)自動(dòng)調(diào)用父類(lèi)默認(rèn)拷貝構(gòu)造函數(shù),而父類(lèi)沒(méi)有默認(rèn)的構(gòu)造函數(shù),便會(huì)報(bào)錯(cuò):

正確的寫(xiě)法:

這里的name是傳遞給Person類(lèi)構(gòu)造函數(shù)的實(shí)參,即:用name給Student對(duì)象中繼承的_name賦值。
構(gòu)造一個(gè)Student類(lèi)的對(duì)象分兩步:
- 將從父類(lèi)繼承的成員初始化
- 將子類(lèi)新增加的成員初始化
拷貝構(gòu)造函數(shù)
子類(lèi)的拷貝構(gòu)造函數(shù)必須在初始化列表中顯式調(diào)用父類(lèi)的拷貝構(gòu)造函數(shù)。
父類(lèi)沒(méi)有定義拷貝構(gòu)造函數(shù),子類(lèi)可以定義也可以不定義;父類(lèi)如果定義了拷貝構(gòu)造函數(shù),子類(lèi)一般要定義,并且要在初始化列表中調(diào)用父類(lèi)的拷貝構(gòu)造函數(shù)完成從父類(lèi)繼承的成員的拷貝初始化,否則會(huì)報(bào)錯(cuò):

正確寫(xiě)法:

此處s是傳遞給拷貝構(gòu)造函數(shù)的參數(shù)。
賦值運(yùn)算符重載
子類(lèi)的賦值運(yùn)算符重載函數(shù)必須調(diào)用父類(lèi)的賦值運(yùn)算符重載完成對(duì)父類(lèi)的賦值。
父類(lèi)的賦值運(yùn)算符重載:

子類(lèi):

析構(gòu)函數(shù)
子類(lèi)析構(gòu)函數(shù)會(huì)在被調(diào)用完后自動(dòng)調(diào)用父類(lèi)的析構(gòu)函數(shù)完成清理父類(lèi)成員,所以清理順序是:先清理子類(lèi),再清理父類(lèi)。

構(gòu)造和析構(gòu)函數(shù)調(diào)用順序
構(gòu)造子類(lèi)對(duì)象時(shí),先調(diào)用父類(lèi)的構(gòu)造函數(shù),再調(diào)用子類(lèi)的構(gòu)造函數(shù),清理對(duì)象時(shí),先調(diào)用子類(lèi)的析構(gòu)函數(shù),再調(diào)用父類(lèi)的析構(gòu)函數(shù)。
如圖:

因?yàn)闃?gòu)造子類(lèi)對(duì)象時(shí)會(huì)在初始化列表中調(diào)用父類(lèi)的構(gòu)造函數(shù),執(zhí)行完之后才會(huì)執(zhí)行子類(lèi)的構(gòu)造函數(shù)的函數(shù)體,所以父類(lèi)的構(gòu)造會(huì)先于子類(lèi)的構(gòu)造執(zhí)行。
五、繼承與友元、靜態(tài)成員
友元關(guān)系
友元關(guān)系不能繼承
tips:王叔是你父親的好朋友,但是不一定是你的好朋友,王叔的財(cái)產(chǎn)不會(huì) 給你繼承??
定義一個(gè)Display函數(shù),并在Person類(lèi)中聲明為友元類(lèi):


在Display函數(shù)中可以訪(fǎng)問(wèn)Person類(lèi)的protected成員,但是不能訪(fǎng)問(wèn)其子類(lèi)Student類(lèi)成員,友元關(guān)系不能繼承。
靜態(tài)成員
父類(lèi)中聲明了static靜態(tài)成員,則整個(gè)繼承體系只有一個(gè)這樣的成員。無(wú)論派生出多少子類(lèi),都只有一個(gè)static成員實(shí)例。
定義A、B、C三個(gè)類(lèi):
class A
{
protected:
int _a;
public:
static int _count;//類(lèi)中聲明為靜態(tài)成員
};
int A::_count = 0;//類(lèi)外定義
class B:public A
{
protected:
int _b;
};
class C :public B
{
protected:
int _c;
};
通過(guò)不同對(duì)象訪(fǎng)問(wèn)_count:

六、菱形繼承及菱形虛擬繼承
菱形繼承概念
單繼承

多繼承

菱形繼承

可以看出,菱形繼承實(shí)際就是單繼承和多繼承組合的結(jié)果,是多繼承的一種特殊情況。
菱形繼承實(shí)例:

注:驗(yàn)證環(huán)境為VS2022,win32平臺(tái)
這里C類(lèi)的大小是20字節(jié),除了其本身成員的4字節(jié),另外16個(gè)字節(jié)都是從兩個(gè)父類(lèi)繼承來(lái)的。模型如圖:

存在問(wèn)題
對(duì)于上面圖中的菱形繼承,存在的問(wèn)題十分明顯,那就是數(shù)據(jù)冗余和二義性問(wèn)題。即Teacher類(lèi)和Student類(lèi)都繼承自Person類(lèi),那么兩個(gè)類(lèi)中都會(huì)包含Person類(lèi)中的成員,Assistant繼承這兩個(gè)類(lèi)之后,同樣的成員便會(huì)包含兩份,導(dǎo)致數(shù)據(jù)重復(fù),并且在通過(guò)Assistant對(duì)象訪(fǎng)問(wèn)Person類(lèi)中的成員時(shí),會(huì)有二義性。
通過(guò)添加作用域限定符可以解決訪(fǎng)問(wèn)二義性的問(wèn)題,如:as.Teacher::_name類(lèi)似的語(yǔ)句可以指定通過(guò)哪個(gè)父類(lèi)訪(fǎng)問(wèn)Person類(lèi)的對(duì)象,但是無(wú)法從根本解決數(shù)據(jù)冗余的問(wèn)題,所以便引入了虛擬繼承的概念。
虛擬繼承的概念
虛擬繼承是指在繼承權(quán)限前面加上一個(gè)virtura關(guān)鍵字
class B1:virtual public A
{
public:
int _b1;
};
用虛擬繼承可以解決菱形繼承的二義性和數(shù)據(jù)冗余的問(wèn)題。對(duì)于上面的菱形繼承,在B1和B2繼承A時(shí)使用虛擬繼承即可解決問(wèn)題。

虛擬繼承的模型
對(duì)于上面的菱形虛擬繼承,研究其模型。
通過(guò)sizeof打印輸出獲取c對(duì)象的大小為24字節(jié):

通過(guò)下面的語(yǔ)句為c對(duì)象中的成員賦值:
void Test()
{
C c;
c._a = 1;
c._b1 = 2;
c._b2 = 3;
c._c = 4;
cout << sizeof(c) << endl;
}
查看其內(nèi)存分布


所以,菱形虛擬繼承將最上面的父類(lèi)中的成員只保存了一份,并用一個(gè)偏移量指針指向偏移量表格,偏移量表格中保存的就是最上面的父類(lèi)中的成員變量相對(duì)于當(dāng)前對(duì)象的偏移量。
最終得到的菱形虛擬繼承對(duì)象內(nèi)存模型如圖:

對(duì)象模型與偏移量表格:

總結(jié):假設(shè)B1和B2繼承自A類(lèi),最下面的C類(lèi)繼承自B1和B2類(lèi);菱形虛擬繼承是指兩個(gè)子類(lèi)繼承自同一個(gè)父類(lèi)時(shí),將繼承方式設(shè)置為虛擬繼承;構(gòu)建對(duì)象時(shí),最頂層的父類(lèi)A中的成員變量只保存一份,在對(duì)象模型的最下面,這樣就避免了數(shù)據(jù)冗余;B1和B2都有屬于自己的虛基表指針,通過(guò)虛基表中的偏移量找到最頂層父類(lèi)中的_a成員變量;
B1和B1類(lèi)中都有自己的虛表,這樣便可以通過(guò)C類(lèi)對(duì)象給B1類(lèi)和者B2類(lèi)的指針、引用或者對(duì)象賦值,滿(mǎn)足賦值兼容規(guī)則
附錄:

總結(jié)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
C++實(shí)現(xiàn)簡(jiǎn)單學(xué)生信息管理系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了C++實(shí)現(xiàn)簡(jiǎn)單學(xué)生信息管理系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-03-03
c++標(biāo)準(zhǔn)庫(kù)讀寫(xiě)ini文件的實(shí)現(xiàn)示例
本文介紹了一個(gè)完整的INI文件類(lèi)的實(shí)現(xiàn),包含讀取和寫(xiě)入操作,通過(guò)IniFile.h頭文件和IniFile.cpp實(shí)現(xiàn)文件,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2024-10-10
基于C++實(shí)現(xiàn)kinect+opencv 獲取深度及彩色數(shù)據(jù)
本文的主要思想是Kinect SDK 讀取彩色、深度、骨骼信息并用OpenCV顯示,非常的實(shí)用,有需要的小伙伴可以參考下2015-12-12
Qt串口通信開(kāi)發(fā)之QSerialPort模塊簡(jiǎn)單使用方法與實(shí)例
這篇文章主要介紹了Qt串口通信開(kāi)發(fā)之QSerialPort模塊簡(jiǎn)單使用方法與實(shí)例,需要的朋友可以參考下2020-03-03
C語(yǔ)言函數(shù)指針與回調(diào)函數(shù)的實(shí)現(xiàn)
本文主要介紹了C語(yǔ)言函數(shù)指針與回調(diào)函數(shù)的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-05-05
C語(yǔ)言簡(jiǎn)單實(shí)現(xiàn)快速排序
快速排序是一種不穩(wěn)定排序,這篇文章主要為大家詳細(xì)介紹了C語(yǔ)言簡(jiǎn)單實(shí)現(xiàn)快速排序,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-01-01
Qt使用SQLite數(shù)據(jù)庫(kù)實(shí)現(xiàn)數(shù)據(jù)增刪改查
這篇文章主要為大家詳細(xì)介紹了Qt如何使用SQLite數(shù)據(jù)庫(kù)實(shí)現(xiàn)數(shù)據(jù)增刪改查功能,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起了解一下2023-06-06

