C++类的继承
繼承是很大程度上解決了代碼重復的問題。比如狗和貓都屬于動物,我們需要在貓和狗的類里面寫上動物年齡,動物名字等。但是動物都可以有年齡和起名稱,這個時候我們寫個動物類,把動物都有的屬性寫進去,狗和貓繼承動物類就好了,就不需要對狗和貓的類里面分別寫年齡和名字了。
本篇博客主要記錄以下問題:
一、基本語法和繼承方式
二、繼承中的對象模型
三、繼承中對象的構造析構順序
四、繼承同名成員的處理方式(包括普通成員、靜態成員)
五、多繼承語法
六、棱形繼承
一、基本語法和繼承方式
1.基本語法
繼承的基本格式為:
class 子類名 :繼承方式 父類名
還允許一個類繼承多個類,多繼承語法格式為:
class 子類名 : 繼承方式 父類名1,繼承方式 父類名2…
2.繼承方式
繼承方式分為公有(public)繼承,保護(protect)繼承,私有(private)繼承。
不同的繼承方式,繼承的成員的屬性會發生變化,如下圖:
總結:
(1)首先,不管哪種繼承方法,父類的私有成員子類都沒辦法訪問。
(2)子類以公有方式繼承父類的話,父類的公有成員在子類中依舊是公有屬性,保護成員依舊是保護屬性。
(3)子類以私有方式繼承父類的話,父類的公有成員和保護屬性成員都統一為保護屬性。
二、繼承中的對象模型
代碼:
class A { public:int a_a; protected:int a_b; private:int a_c; };class B :public A {int Geta_a(){return a_a;}int Geta_b(){return a_b;}/*//不可訪問a_c,因為a_c是父類的私有屬性,子類不可訪問int Geta_c(){return a_c;}*/ private:int b_a; };父類的私有成員,子類無法訪問。無法訪問不代表沒有繼承。父類的所有成員,子類都會完全繼承下來,只不過編譯器將繼承下來的私有成員給隱藏了,不允許子類訪問。
比如我們用類大小看一下:
輸出結果如下:
類B的大小為16字節。類B自己有一個int類型成員。那么還有12個字節都是從父類繼承下來的。所以父類三個成員都被子類繼承了,包括不允許子類訪問的私有屬性。
我們可以用vs2019的開發者工具進行查看:
發現從A類繼承下來的是有私有成員a_c的,只不過編譯器對其進行了隱藏不讓訪問而已。
總結:
父類的所有非靜態成員,子類都會完全繼承下來,只不過編譯器將繼承下來的私有成員給隱藏了,不允許子類訪問。
三、繼承中的構造和析構順序
代碼:
class A { public:A(){cout << "這里是A構造函數" << endl;}~A(){cout << "這里是A析構函數" << endl;} public:int a_a; protected:int a_b; private:int a_c; };class B :public A { public:B(){cout << "這里是B構造函數" << endl;}~B(){cout << "這里是B析構函數" << endl;} private:int b_a; };int main() {B b;return 0; }
發現是類A先調用構造函數的,這是因為只有A類先調用構造函數,類B構造的對象才能拿到類A的成員。
四、繼承同名成員的處理方式
繼承會引發一些問題,比如說如果子類有和父類相同名稱的成員,或者說子類繼承了兩個父類,兩個父類有同名的成員,這樣的話,使用成員的時候,編譯器會犯難,不知道要使用哪一個成員。這里我們拿子類和父類同名成員時的處理方式。
1.繼承普通同名成員的處理方式
1.繼承普通同名成員變量的處理方式
class A { public:int a;int b;; private:int c; };class B :public A { public:int a; };int main() {B b;b.a = 10; //B類里面本身的變量ab.A::a = 20;//B類繼承的成員變量acout << b.a << " " << b.A::a << endl;b.b = 20; //B類繼承的成員變量bb.A::b = 30; //B類繼承的成員變量bcout << b.b << " " << b.A::b << endl;return 0; }運行結果:
總結:
(1)當訪問的成員變量不是同名成員變量時,直接以對象點的形式訪問就可以。
(2)當訪問的成員變量是同名成員變量時,以對象點的方式訪問本類本身的同名成員變量,以對象點+作用域的方式訪問父類的成員變量。
2.繼承普通同名成員函數的處理方式
代碼:
運行結果:
當子類與父類有同名成員函數的時候,發現哪怕父類的成員函數和子類的成員函數形參列表不同(比如父類的fun(int)和子類的fun()明顯不同,構成了函數重載),但是子類對象卻不能以對象點的方式調用父類函數fun(int)。這是由于只要子類有同名的成員函數,那么父類的同名成員函數都會被隱藏,統一不能以對象點的方式調用,必須以對象點加作用域的方式調用。
總結:
(1)如果子類和父類不存在同名的函數,那么直接以對象點的方式調用即可。
(2)如果子類和父類的成員函數的函數名相同,那么對于父類的所有同名函數,子類必須以對象點+作用域的方式調用父類的同名函數。此時子類以對象點的方式調用子類本身的同名函數。
2.繼承同名靜態成員的處理方式
首先,要知道靜態成員只有一個,繼承的話也是只有一個,即子類和父類的對象都共用同一個靜態成員。
如下驗證代碼:
通過類B對象修改靜態成員變量的值,類A的靜態成員變量值也會修改,打印出來地址,兩個類的對象都是用同一個靜態成員變量。
1.繼承同名靜態成員變量的處理方式
代碼:
總結:
發現有同名靜態成員變量時,和同名普通成員變量處理方式一樣。只不過對于靜態成員變量,多了一個直接用域名訪問的方式B::A::a。
2.繼承同名靜態成員函數的處理方式
class A { public:static void fun(){cout << "this is A fun()" << endl;}static void fun(int tmp){cout << "this is A fun(int)" << endl;} public:int a;int b;; private:int c; };class B :public A { public:static void fun(){cout << "this B fun()" << endl;} };int main() {B b;//B類本身的靜態fun()函數b.fun();B::fun();//調用繼承的靜態fun函數b.A::fun();b.A::fun(10);B::A::fun();B::A::fun(10);return 0; }運行結果:
總結:
發現有同名靜態成員函數時,和同名普通成員函數處理方式一樣。只不過對于靜態成員函數,多了一個直接用域名訪問的方式B::A::fun()。
但是注意,兩個"::"的意義不同,第一個代表用類名訪問,第二個是代表A類作用域下。
五、棱形繼承(鉆石繼承)
1.棱形繼承的概念
兩個派生類繼承一個基類,又有一個派生類繼承這兩個派生類。
如下圖:
可能舉例不太恰當,但是就是這么個意思。
2.棱形繼承帶來的問題
1.羊繼承了動物的數據,駝同樣繼承了動物的數據,羊駝繼承了羊和駝的數據,繼承本身是有繼承性的,那么羊駝就有了兩份動物的數據。
2.羊駝繼承自動物的數據有兩份,造成了資源浪費。
總的來說就是相同作用的數據有兩份,就這么個問題。
平常中,是不允許這樣繼承的,盡量避免多繼承和棱形繼承的發生。
類SheepTuo的m_age有兩份,但是只需要一份就夠了,所以資源浪費。
可以清楚的看見Sheep 和Tuo 分別從Animal繼承了m_age,而SheepTuo 又繼承了Sheep 和Tuo,所以SheepTuo有兩份不同作用域下的m_age。我們要做的就是讓SheepTuo的m_age只有一份。
下面解決數據有兩份的問題。
3.解決棱形繼承帶來的問題
利用虛繼承解決棱形繼承問題。
虛繼承,即在繼承方式前面再加一個virtual關鍵字。
vbptr是虛指針,由于Sheep和Tuo都是虛繼承的方式繼承Animal的,所以這倆類只是各多了一個虛指針,指向對應的vbtable,vbtable是虛表,Sheep和Tuo的虛指針會查虛表,記錄偏移量,比如Sheep的虛指針地址偏移量為0,Sheep的虛表里面的記錄的偏移量是8,0+8就是Animal的m_age的地址偏移量;同理對于Tuo來說4+4也是Animal的m_age的地址偏移量。這樣的話,SheepTuo就只包含一個m_age。那么如果把SheepTuo繼承Sheep和Tuo的方式都改為虛繼承呢?那么SheepTuo自己也會有一個虛指針,這個虛指針指向一個虛表,虛表里放著從Sheep和Tuo還有Animal繼承過來的內容。此時仍舊只有一份Animal的m_age,如下圖:
總結
- 上一篇: 运算符重载(加减运算符、前置加加(减减)
- 下一篇: 列表初始化和赋值初始化的使用注意事项