詳解C++中單繼承與多繼承的使用
前言
C++的繼承機(jī)制相對其他語言是比較復(fù)雜的一種,不同于java只支持單繼承,C++不僅支持單繼承,也支持多繼承,對于多繼承中的菱形問題會引發(fā)一系列的麻煩,C++的兩個重要缺陷,一個是多繼承,一個是垃圾回收器。本文將詳細(xì)講解C++的單繼承和多繼承,以及菱形繼承的解決方法及原理。
1.繼承的概念和定義
(1)繼承的概念
繼承是面向?qū)ο笤O(shè)計(jì)使代碼可以復(fù)用的重要手段,它允許程序員在保持原有類的基礎(chǔ)上進(jìn)行擴(kuò)展。被擴(kuò)展的類稱為基類或者父類,擴(kuò)展生成的類叫做子類或者派生類,繼承是類設(shè)計(jì)層次的復(fù)用。
繼承的作用是使得子類中既包含父類的成員,也可以包含自己的成員。
(2)繼承的定義方法
class Person { private: string _name; int _age; }; class Student :public Person { private: int _id; };
看這一段代碼,其中子類Student繼承了父類Person,Student后的public表示的是繼承方式。
(2)繼承后子類的成員類型
繼承方式和父類的成員屬性共同決定了子類中的成員屬性。我們用一張表來表示三者之間的關(guān)系。
類成員/繼承方式 | public繼承 | protected繼承 | private繼承 |
---|---|---|---|
基類的public成員 | 派生類的public成員 | 派生類的protected成員 | 派生類的private成員 |
基類的protected成員 | 派生類的protected成員 | 派生類的protected成員 | 派生類的private成員 |
基類的private成員 | 派生類中不可見 | 派生類中不可見 | 派生類中不可見 |
我們只需要兩點(diǎn)來記憶這個表格:
1.基類的private成員在派生類中無論以什么方式繼承都是不可見的。
2.子類中的成員屬性取繼承方式和父類成員屬性中權(quán)限小的那個: public>protected>private
表格的說明:
1.不可見的意思不是沒有被繼承,而是不能使用,在底層繼承下來比沒有繼承下來更方便。
2.在父類中private和protected沒有區(qū)別,但是在子類中,protected成員可以在類內(nèi)訪問,而private不能,因此可以說protected是為了繼承而存在的。
3.如果不寫繼承方式,如果子類是class定義的,那么默認(rèn)為private繼承,是struct定義的,默認(rèn)是public繼承。
4.不可見與private成員區(qū)別:不可見指的是在類內(nèi)與類外都不能使用,private成員在類內(nèi)可以使用,在類外不可以使用。
5.不想給子類訪問的成員我們設(shè)成private。
2.基類與派生類的賦值轉(zhuǎn)換
(1)派生類賦值給基類
我們定義了一個父類person和它的派生類student,以上是它們各自的成員。
當(dāng)我們將一個派生類的對象賦值給基類的對象時,發(fā)生的過程我們稱之為切片。即只將子類中父類成員賦值過去。當(dāng)父類中有private成員時,同樣會進(jìn)行切片,只是不顯示而已,因此繼承中盡量不要定義私有成員。
注意,這種賦值兼容方式僅限于公有繼承。
私有繼承不支持切片,這是因?yàn)閷τ诟割愔械膒ublic成員,私有或保護(hù)繼承之后會轉(zhuǎn)變成private/protected類型,而賦值時會發(fā)生將派生類對象中的private/protected成員賦值給父類對象中的public成員的現(xiàn)象,但是private/protected成員在類外是不能被訪問的,因此不支持私有繼承。
Person b; Student a; b = a; Person* ptr = &a; Person& ref = a;
注意一個細(xì)節(jié),我們可以使用引用賦值,說明這里并不存在類型轉(zhuǎn)換的行為,因?yàn)轭愋娃D(zhuǎn)換中間會產(chǎn)生臨時變量,需要使用const引用。
double d; const int& r=d;//發(fā)生了類型轉(zhuǎn)換。
(2)基類給派生類
先說結(jié)論:
父類對象不可以直接賦值給子類對象。
這是因?yàn)樽宇悓ο笾杏懈割惒淮嬖诘念愋?,無法進(jìn)行賦值。也不能通過所謂的強(qiáng)制類型轉(zhuǎn)換進(jìn)行賦值。
但是C++支持指針和引用的賦值:
Person b; Student a; a = (Student)b;//不正確 Student* ptr = (Student*)&b;//支持 Student& ref = (Student&)b;//支持
雖然指針和引用可以,但是當(dāng)指針向下訪問的時候超過父類對象的時候會出現(xiàn)問題。
會出現(xiàn)指向空的情況。
3.繼承中的作用域
(1)隱藏的概念
基類和派生類都有各自獨(dú)立的作用域。
如果不同的域內(nèi)有同名的成員,我們根據(jù)就近原則或者指定作用域的方式來指定成員的位置。
隱藏:子類與父類中出現(xiàn)同名成員,子類成員將屏蔽父類成員對同名成員進(jìn)行直接訪問,這種情況叫隱藏,也叫重定向。
注意如果是成員函數(shù)的隱藏,只要函數(shù)名相同就會構(gòu)成隱藏,與參數(shù)無關(guān)。
舉一個例子:
class Person { protected: string _name = "小六子"; int _num = 111; }; class Student :public Person { public: void Print() { cout << "姓名:" << _name << endl; cout << "身份證號:" << Person::_num << endl; cout << "學(xué)號:" << _num << endl; } protected: int _num = 999; }; int main() { Student s1; s1.Print(); }
在這段代碼中,Person和Student分別定義了_num,當(dāng)子類對象中的成員函數(shù)直接訪問_num時,根據(jù)的是就近原則,訪問的是子類中的_num,當(dāng)要訪問父類中的_num時,需要使用::來指定類域,就可以進(jìn)行訪問。父類中的_num與子類中的_num構(gòu)成隱藏。
這段代碼打印的結(jié)果是:
(2)例題
這里有一道小小的題目,是關(guān)于函數(shù)隱藏的:
class A { public: void func() { cout << "func" << endl; } }; class B :public A { public: void func(int i) { A::func(); cout << "func(int i)->" << i << endl; } }; void Test() { B b; b.func(10); b.func(); }
提問在Test中的兩個函數(shù)能否調(diào)用成功?
b.func(10)可以調(diào)用成功,因?yàn)闃?gòu)成了隱藏。
b.func()不能調(diào)用成功,會發(fā)生變異報(bào)錯,因?yàn)殡[藏了調(diào)不動。
4.派生類的默認(rèn)成員函數(shù)
對于六大默認(rèn)成員函數(shù)我們這里暫時先討論4種重要的,即:構(gòu)造函數(shù),析構(gòu)函數(shù),拷貝構(gòu)造,賦值運(yùn)算符重載。
(1)默認(rèn)生成的成員函數(shù)
當(dāng)我們不在子類中書寫時,編譯器會默認(rèn)生成。這里只需要記住一句話:
繼承下來的成員調(diào)用父類的來處理,自己的按基本規(guī)則來處理。
以構(gòu)造函數(shù)舉例:派生類中的父類成員調(diào)用父類中的構(gòu)造函數(shù),自己的成員按照構(gòu)造函數(shù)自動生成的規(guī)則來。
(2)自己寫
自己寫的情況
1.父類沒有默認(rèn)構(gòu)造函數(shù),需要我們自己寫構(gòu)造函數(shù)。
2.子類有資源需要釋放,需要我們自己寫析構(gòu)函數(shù)。
3.如果子類涉及淺拷貝問題,需要自己寫拷貝構(gòu)造和賦值重載。
構(gòu)造函數(shù)
父類成員調(diào)用對應(yīng)的父類構(gòu)造函數(shù)處理。子類成員按普通類處理。
舉一個例子:
class Person { public: Person(string name , int num=2) :_name(name) ,_num(num) {} protected: string _name ; int _num ; }; class Student :public Person { public: Student(int num,string _name,int _num) :_num(num) ,Person(_name,_num) {} protected: int _num; }; int main() { Student s1(2,"zhangsan",2); }
看這一段代碼,父類中沒有默認(rèn)構(gòu)造函數(shù)(注意與默認(rèn)成員函數(shù)區(qū)分),因此要初始化父類中的對象需要我們自己書寫子類中的構(gòu)造函數(shù)。在書寫構(gòu)造函數(shù)時,父類對象成員初始化使用父類中的構(gòu)造函數(shù),子類成員的初始化按正常方式書寫即可。
拷貝構(gòu)造和運(yùn)算符重載函數(shù)
Student(const Student& s) :Person(s) ,_num(s._num) {} Student& operator=(const Student& s) { if (this != &s) { Person::operator=(s);//不指明類域的話會發(fā)生自己調(diào)自己的情況 _num = s._num; return *this; } } int main() { Student s1(2,"zhangsan",2); Student s2(s1); Student s3 = s2; }
我們可以通過調(diào)試來查看結(jié)果:
析構(gòu)函數(shù)
析構(gòu)函數(shù)比較特殊,對于父類中的析構(gòu)函數(shù),我們不需要指定去書寫,就像下面這種情況:
//父類中的析構(gòu) ~Person() { cout << "~Person" << endl; } //子類中的析構(gòu) ~Student() { Person::~Person(); }
注意,析構(gòu)函數(shù)的名字在最后會被統(tǒng)一處理成destructor(),如果不指定類域的話,父類析構(gòu)函數(shù)和子類析構(gòu)函數(shù)會構(gòu)成隱藏,因此需要指定類域。
對于上述int中的代碼,需要析構(gòu)三個子類對象,打印出的結(jié)果是:
我們發(fā)現(xiàn)調(diào)用了六次父類中的析構(gòu)函數(shù)。這說明每個對象的父類成員都被析構(gòu)了兩次。如果需要釋放空間,則一定會報(bào)錯。
先說結(jié)論:我們自己實(shí)現(xiàn)子類構(gòu)造函數(shù)時,不需要顯示調(diào)用父類析構(gòu)函數(shù),我們顯示調(diào)用一次,它還會自動調(diào)用一次。
下面簡單說明一下,為什么程序需要自動調(diào)用:
我們知道變量的定義是發(fā)生在棧中的,因此就存在構(gòu)造和析構(gòu)的順序問題,棧滿足先入后出原則,因此先構(gòu)造的需要后析構(gòu)。
在構(gòu)造的過程中,我們會先初始化父類成員,再初始化子類成員。因此我們需要先析構(gòu)子類成員,再析構(gòu)父類成員。
如果先析構(gòu)父類會打亂棧的順序,因此編譯器會自動調(diào)用父類的析構(gòu)函數(shù)。
5.友元與靜態(tài)成員
這個只需要記住兩點(diǎn):
1.友元關(guān)系不能繼承。
2.靜態(tài)成員會被繼承下來,無論繼承多少,靜態(tài)成員只有一個。
6.多繼承
(1)概念
一個類有兩個及以上父類時稱這個繼承關(guān)系為多繼承。
class Student { public: protected: int _id; }; class Teacher { public: protected: int _course; }; class Assistant:public Student,public Teacher { public: protected: protected: };
我們使用逗號表示分隔,即繼承多個父類。可以通過調(diào)試來觀察子類Assitant的內(nèi)容:
(2)復(fù)雜的菱形繼承
菱形繼承是多繼承的一種情況:
具有這樣的繼承關(guān)系的稱為菱形繼承。
菱形繼承出現(xiàn)的問題:從對象成員模型構(gòu)造,可以看出菱形繼承有數(shù)據(jù)冗余和二義性的問題。
數(shù)據(jù)冗余指的是類Assistant中會有兩份Person的成員,二義性指的是這兩份成員每一次調(diào)用不知道調(diào)用的的是哪一個,需要指定類域。
這段代碼表示的就是菱形繼承的關(guān)系:
class Person { public: string _name; }; class Student:public Person { public: protected: int _num; }; class Teacher:public Person { public: protected: int _id; }; class Assistant:public Student,public Teacher { public: protected: protected: int _course; }; int main() { Assistant a; }
我們通過調(diào)試可以觀測a中的內(nèi)容,發(fā)現(xiàn)會存在兩份Person中的成員:
如果要對這兩個Person成員賦值時,需要指定類域。
a.Student::_name = "xxx"; a.Teacher::_name = "yyy"; }
這就是所謂的二義性,在實(shí)際中一個人不能有兩個名字,對于冗余性來說,如果Person中有一個很大的數(shù)組浪費(fèi)的空間會很多。
(3)虛繼承解決菱形繼承問題
虛繼承可以解決菱形繼承的二義性和數(shù)據(jù)冗余問題。如上面的繼承關(guān)系,在Student和Teacher的繼承Person時使用的虛擬繼承,即可解決問題。需要注意的是,虛擬繼承不要在其他地方去使用。
class Student:virtual public Person { public: protected: int _num; }; class Teacher:virtual public Person { public: protected: int _id; };
只需要在菱形的腰部兩個父類加入virtual關(guān)鍵詞即可。
注意要在菱形的腰部。
當(dāng)加完之后,在Assistant的對象中,Person類的_name成員就只有一個了。無論是否指定類域,更改的變量都只有一個:
(4)虛繼承的原理
內(nèi)存演示
要研究虛繼承的原理,我們給出一個簡化的菱形繼承結(jié)構(gòu),再借助內(nèi)存窗口窗口觀察對象成員的模型。
class A { public: int _a; }; class B:public A { public: int _b; }; class C:public A { public: int _c; }; class D :public B, public C { public: int _d; }; int main() { D d; d.B::_a = 1; d.C::_a = 2; d._b = 3; d._c = 4; d._d = 5; return 0; }
當(dāng)沒使用虛繼承(即沒有使用virtual時)
我們使用內(nèi)存窗口來觀察內(nèi)容:
通過觀察內(nèi)存中的布局,我們發(fā)現(xiàn)d中的B父類對象和C父類對象中的內(nèi)容分別是連續(xù)存放的,B中有父類A中成員_a的值是1,其自己成員_b的值是3,兩者的內(nèi)存是挨著的,C同理,對于D類中自己的成員_d,放在了內(nèi)存的最后。
確定d中B類對象和C類對象的存儲順序是根據(jù)繼承順序決定的。由于上述代碼是class D :public B, public C,因此B類的對象會存在C類的前面。
而當(dāng)我們給腰部加上virtual構(gòu)成虛繼承之后:
class B:virtual public A { public: int _b; }; class C:virtual public A { public: int _c; };
使用virtual之后,我們發(fā)現(xiàn)已經(jīng)將A中對象_a放入在了最后,因此無論指定不指定類域,改變的都是同一個_a的值。
但同時我們發(fā)現(xiàn)內(nèi)存中多了兩行,那么這兩行是干什么的呢?
虛基表
從格式來看,這兩行顯然是都是地址。
我們再開辟一個內(nèi)存2,向其中輸入上面地址,我們發(fā)現(xiàn)地址中存儲的內(nèi)容是00 00 00 00,C類對象中同理,這里就不演示了。
這里00 00 00 00的意義在后面多態(tài)中會學(xué)習(xí)到,注意看它的下一個位置存放的是00 00 00 14
這里是十六進(jìn)制,因此表示的是20這個數(shù)字。
再來看內(nèi)存1:
兩者的地址之差剛剛好是20個字節(jié)。
因此我們可以知道:在虛繼承中,B類對象和C類對象的內(nèi)存中新加入的是一個地址,分別用于尋找兩者與A類型變量的偏移量。B類對象與A類對象的偏移量是20,同理可驗(yàn)證C類對象的偏移量是12。而內(nèi)存2也有一個專有名詞:虛基表
總結(jié):A一般叫做虛基類,在D里面,A類成員放在一個公共的位置,有時B要找A,C要找A,就要通過虛基表中的偏移量進(jìn)行計(jì)算。
比如,當(dāng)我們再用B類和C類建立兩個變量:
B b = d; C c = d;
此時會發(fā)生切片處理,需要將d中的A類對象賦值到b和c中,此時就需要使用到虛基表來尋找。
再比如:
B* pb = &d; pb->_a = 10;
pb指向了d的首地址,要更改d中的_a的值,指針pb也需要使用虛基表來進(jìn)行尋找。
7.繼承與組合
(1)兩者區(qū)別
首先我們要對繼承和組合進(jìn)行區(qū)分:
繼承表示的是子類繼承父類,組合表示的是在一個類中定義了另一個類的成員變量。
//繼承 class A { public: int _a; }; class B:public A { public: int _b; }; //組合 class C { public: int _c; }; class D { public: int _d; C _obj; };
(2)繼承與組合的區(qū)別
我們需要明確一點(diǎn):類之間,模塊之間最好是低耦合,高內(nèi)聚的,因?yàn)榉奖憔S護(hù)。
低耦合:類之間依賴關(guān)系越弱越好。
高內(nèi)聚:內(nèi)部成員關(guān)系緊密。
1.繼承對應(yīng)于白盒:B可以直接使用A中的公有和保護(hù)成員,破壞了封裝性。
2.組合對應(yīng)于黑盒:D只能使用C的公有,不能直接使用保護(hù)成員。
舉一個例子:
如果A中有5個public,5個protected
對于組合來說,非基類只能使用這5個public,基類中的其他成員隨便修改都不會影響該非基類。
對于繼承來說,基類中一切的改變都會影響子類。
那可以拋棄繼承的語法嗎?當(dāng)然是不行的。
多態(tài)是建立在繼承的基礎(chǔ)上的。
(3)使用情況
1.如果B就是一個A,比如Student是一個Person,我們稱這種關(guān)系為is-a關(guān)系,此時適合使用繼承。
2.如果D被包含于C,比如head包含eyes,我們稱這種關(guān)系為has-a關(guān)系,此時適合使用組合。
3.當(dāng)遇到特殊情況,is-a和has-a都可以講通時,優(yōu)先使用組合。
8.總結(jié)
C++的語法復(fù)雜在于C++是第一個吃螃蟹的人,很多地方會考慮太多,拿多繼承舉例,有了多繼承就有了菱形繼承,有了菱形繼承,就有了菱形虛擬繼承,底層實(shí)現(xiàn)就更為復(fù)雜了,所以一般不建議設(shè)計(jì)多繼承,設(shè)計(jì)了多繼承也不建議設(shè)計(jì)菱形繼承。
到此這篇關(guān)于詳解C++中單繼承與多繼承的使用的文章就介紹到這了,更多相關(guān)C++單繼承 多繼承內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
C++?基本數(shù)據(jù)類型中int、long等整數(shù)類型取值范圍及原理分析
這篇文章主要介紹了C++?基本數(shù)據(jù)類型中int、long等整數(shù)類型取值范圍及原理分析,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-11-11