C++函數繼承
引言
C++三大特征分別為封裝,繼承和多態,它們構成了面向對象編程的基石,它們協同工作以提升代碼的模塊化,可復用性和靈活性
封裝:提高代碼的維護性(當程序出現問題時可以準確定位)
繼承:提高代碼的復用性(在不做任何修改或者操作源碼就能實現代碼的復用)
多態:提高代碼的擴展性(后期文章會介紹)
組合
C++中實現代碼復用操作主要有兩種,分別是組合和繼承,二者各有優缺點
#include <iostream>using namespace std;?class A{public:void func(){cout << "Hello world" << endl;}int m_num;};?class B{public:?//創建A類成員A a;?void func(){//此時B包含Aa.func();}};?int main(){B b;b.func();?return 0;}
執行結果:
上述代碼就是一個典型的組合操作的實現,B包含A對象作為成員變量,這樣B就可以調用A中的方法,此時修改A的內部邏輯并不會影響B(只要接口不變),操作時也更加直接,可以組合多個類,操作方法都是通過成員對象來訪問,這樣就在不復制代碼內容的情況下使用了其他類中的功能。但這種方法每次組合一個類就要多創建一個成員,此時會增大內存占用。
繼承
繼承同樣在代碼復用方面起著重要作用
#include <iostream>using namespace std;?class A{public:void print(){cout << "Hello world" << endl;}int m_num;};?class B : public A{public:?//繼承之后無需聲明A類成員便可直接調用A類內的共有函數void func(){print();}};?int main(){A a;B b;b.func();?//繼承了A類之后便可以直接使用與A相同的成員b.m_num = 10;b.print();?cout << b.m_num << endl;cout << a.m_num << endl;?cout << sizeof(b) << endl;?return 0;}?
輸出結果
在這里與組合相比,繼承就可以減少內存開銷,繼承之后B稱為A的子類或派生類,A稱為B的父類。派生類的實例化對象大小:父類的對象大小 + 派生類的新成員;與此同時我們還應該注意繼承的空間分配問題,繼承后派生類和父類并不公用內存,派生類會在繼承后在自己的內存空間內開辟新的內存來存放與父類不同的成員,大致原理如下
根據程序輸出結果也不難看出修改了B類成員后A類成員并沒有一同發生改變
覆蓋
#include <iostream>using namespace std;?class A{public:void print(){cout << "Hello world" << endl;}int m_num = 12;};?class B : public A{public://與父類重名的成員會在派生類中重新創建int m_num;int count;// 繼承之后無需聲明A類成員便可直接調用A類內的共有函數void func(){print();}?//與父類重名的函數會發生覆蓋void print(){cout << "Hi world" << endl;}};?int main(){A a;B b;b.func();?b.m_num = 10;b.print();?cout << sizeof(b) << endl;?cout << b.m_num << endl;cout << a.m_num << endl;?return 0;}
輸出結果
根據輸出結果不難推理出派生類中的重名函數以及重名成員會覆蓋子類中的對應函數和成員
三種繼承方式
公有繼承
基類的公有成員和屬性成為派生類的共有;基類的被保護的屬性和方法成為派生類的被保護;基類私有成員不能被繼承
保護繼承
基類的公有成員和屬性成為派生類的私有;基類的被保護的屬性和方法成為派生類的私有;基類私有成員不能被繼承
私有繼承
基類的公有成員和屬性成為派生類的被保護的;基類的被保護的屬性和方法成為派生類的被保護的;基類私有成員不能被繼承
三種繼承方式操作方法基本相同,只是權限控制不同,由于我們使用繼承的出發點是實現代碼復用,所以三種繼承方式中使用最多的就是公有繼承
繼承后函數的執行順序
#include <iostream>using namespace std;?class A{public:A(){cout << "A" << endl;}explicit A(int num) : m_num(num){cout << "A int" << endl;}?~A(){cout << "~A" << endl;}int m_num;};?class B : public A{public://使用初始化列表,調用A類的帶參構造函數,如果不使用初始化列表則調用無參構造函數B() : A(5){cout << "B" << endl;}?~B(){cout << "~B" << endl;}?int m_count;};?int main(){B b;cout << b.m_num << endl;return 0;}
輸出結果
由輸出結果不難看出,繼承之后構造函數的執行順序為先調用基類的構造函數然后再派生類的構造函數,析構函數則相反。
同時派生類繼承的屬性需要基類的構造函數進行初始化;如果沒有無參構造函數,那么派生類里所有的構造函數都要顯示調用基類的構造函數
子對象的構造順序
當一個類中聲明而多個子對象時它們的構造函數調用順序又是怎樣的呢
#include <iostream>using namespace std;?class Test{public:?Test(int index) : m_index(index){cout << "Test int" << endl;}?~Test(){cout << "~Test" << endl;}?int m_index;};?class Test1{public:?Test1(int index) : m_index(index){cout << "Test1 int" << endl;}?~Test1(){cout << "~Test1" << endl;}?int m_index;};?class B{public://通過初始化列表來初始化對象//子對象在初始化列表中的順序B() : t1(0),t(0){cout << "B" << endl;}?~B(){cout << "~B" << endl;}?int m_count;?//子對象的聲明順序Test t;Test1 t1;};?int main(){B b;?return 0;}
輸出結果
根據上述代碼可以推出子對象構造函數的調用順序與子對象的聲明順序相關。先聲明的對象先構造
多基類的構造順序
#include <iostream>using namespace std;?class A{public:?A(int index) : m_index(index){cout << "A int" << endl;}?~A(){cout << "~A" << endl;}?int m_index;};?class B{public:?B(int index) : m_index(index){cout << "B int" << endl;}?~B(){cout << "~B" << endl;}?int m_index;};?//繼承順序class C : public A, public B{public://通過初始化列表來初始化對象//列表順序C() : B(0),A(0){cout << "C" << endl;}?~C(){cout << "~B" << endl;}?int m_count;?};?int main(){C c;?return 0;}
輸出結果
根據輸出結果不難看出當繼承了多個基類時基類的構造順序由繼承順序決定
二義性問題
多繼承會產生二義性問題,具體形式如下、
直接指定路徑
當我們想要在D類內訪問a時編譯器就會面臨兩個選擇,要么通過類B訪問,要么通過類C訪問,此時就會產生多繼承問題
代碼實現如下
#include <iostream>?using namespace std;//基類Aclass A{public:A(int a) : m_a(a){cout << "A" << endl;}int m_a;};?//基類B繼承Aclass B : public A{public:B(int b) : m_b(b),A(1){cout << "B" << endl;}int m_b;};?//基類C繼承Aclass C : public A{public:C(int c) : m_c(c),A(2){cout << "C" << endl;}int m_c;};?//派生類D同時繼承B和Cclass D : public B,public C{public:D(int d) : m_d(d), B(2), C(3){cout << "D" << endl;}int m_d;};?int main(){D d(4);?//引發報錯,二義性問題導致編譯器我無法確定訪問路徑cout << d.m_a << endl;return 0;}
輸出結果:
此時解決問題的方法由兩種,一種是直接指明路徑將輸出語句內容更換為cout << d.C::m_a << endl;
,此時程序就能正常輸出
這種直接指定路徑的方法就可以解決二義性的問題,但是通過輸出結果我們發現A被構造了兩次,因此相比于這種方法我們會傾向于選擇使用虛繼承來解決問題
虛繼承
#include <iostream>?using namespace std;class A{public:A(int a) : m_a(a){cout << "A" << endl;}int m_a;};?//此時B和C采用虛繼承class B : virtual public A{public:B(int b) : m_b(b),A(1){cout << "B" << endl;}int m_b;};?class C : virtual public A{public:C(int c) : m_c(c),A(2){cout << "C" << endl;}int m_c;};?class D : public B,public C{public:D(int d) : m_d(d), B(2), C(3), A(1){cout << "D" << endl;}int m_d;};?int main(){D d(4);?cout << d.C::m_a << endl;return 0;}
輸出結果
此時我們通過觀察發現程序中A類只被構造了一次,在D類中實現的構造,同時需要注意的是當使用虛繼承時B和C都要采用虛繼承,如果只有一個采用了虛繼承那么還是會引發報錯,因為此時A被構造了兩次,又產生了二義性問題
組合和繼承的選擇
當兩者之間是has-a(例如:學生有書包,汽車有發動機)關系時通常使用組合方式,當兩者關系為is-a關系(例如:老師是人,學生也是人)時使用繼承方式