您好,登錄后才能下訂單哦!
今天小編給大家分享一下C++中單繼承與多繼承如何使用的相關知識點,內容詳細,邏輯清晰,相信大部分人都還太了解這方面的知識,所以分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后有所收獲,下面我們一起來了解一下吧。
C++的繼承機制相對其他語言是比較復雜的一種,不同于java只支持單繼承,C++不僅支持單繼承,也支持多繼承,對于多繼承中的菱形問題會引發一系列的麻煩,C++的兩個重要缺陷,一個是多繼承,一個是垃圾回收器。本文將詳細講解C++的單繼承和多繼承,以及菱形繼承的解決方法及原理。
繼承是面向對象設計使代碼可以復用的重要手段,它允許程序員在保持原有類的基礎上進行擴展。被擴展的類稱為基類或者父類,擴展生成的類叫做子類或者派生類,繼承是類設計層次的復用。
繼承的作用是使得子類中既包含父類的成員,也可以包含自己的成員。
class Person { private: string _name; int _age; }; class Student :public Person { private: int _id; };
看這一段代碼,其中子類Student繼承了父類Person,Student后的public表示的是繼承方式。
繼承方式和父類的成員屬性共同決定了子類中的成員屬性。我們用一張表來表示三者之間的關系。
類成員/繼承方式 | public繼承 | protected繼承 | private繼承 |
---|---|---|---|
基類的public成員 | 派生類的public成員 | 派生類的protected成員 | 派生類的private成員 |
基類的protected成員 | 派生類的protected成員 | 派生類的protected成員 | 派生類的private成員 |
基類的private成員 | 派生類中不可見 | 派生類中不可見 | 派生類中不可見 |
我們只需要兩點來記憶這個表格:
1.基類的private成員在派生類中無論以什么方式繼承都是不可見的。
2.子類中的成員屬性取繼承方式和父類成員屬性中權限小的那個: public>protected>private
表格的說明:
1.不可見的意思不是沒有被繼承,而是不能使用,在底層繼承下來比沒有繼承下來更方便。
2.在父類中private和protected沒有區別,但是在子類中,protected成員可以在類內訪問,而private不能,因此可以說protected是為了繼承而存在的。
3.如果不寫繼承方式,如果子類是class定義的,那么默認為private繼承,是struct定義的,默認是public繼承。
4.不可見與private成員區別:不可見指的是在類內與類外都不能使用,private成員在類內可以使用,在類外不可以使用。
5.不想給子類訪問的成員我們設成private。
我們定義了一個父類person和它的派生類student,以上是它們各自的成員。
當我們將一個派生類的對象賦值給基類的對象時,發生的過程我們稱之為切片。即只將子類中父類成員賦值過去。當父類中有private成員時,同樣會進行切片,只是不顯示而已,因此繼承中盡量不要定義私有成員。
注意,這種賦值兼容方式僅限于公有繼承。
私有繼承不支持切片,這是因為對于父類中的public成員,私有或保護繼承之后會轉變成private/protected類型,而賦值時會發生將派生類對象中的private/protected成員賦值給父類對象中的public成員的現象,但是private/protected成員在類外是不能被訪問的,因此不支持私有繼承。
Person b; Student a; b = a; Person* ptr = &a; Person& ref = a;
注意一個細節,我們可以使用引用賦值,說明這里并不存在類型轉換的行為,因為類型轉換中間會產生臨時變量,需要使用const引用。
double d; const int& r=d;//發生了類型轉換。
先說結論:
父類對象不可以直接賦值給子類對象。
這是因為子類對象中有父類不存在的類型,無法進行賦值。也不能通過所謂的強制類型轉換進行賦值。
但是C++支持指針和引用的賦值:
Person b; Student a; a = (Student)b;//不正確 Student* ptr = (Student*)&b;//支持 Student& ref = (Student&)b;//支持
雖然指針和引用可以,但是當指針向下訪問的時候超過父類對象的時候會出現問題。
會出現指向空的情況。
基類和派生類都有各自獨立的作用域。
如果不同的域內有同名的成員,我們根據就近原則或者指定作用域的方式來指定成員的位置。
隱藏:子類與父類中出現同名成員,子類成員將屏蔽父類成員對同名成員進行直接訪問,這種情況叫隱藏,也叫重定向。
注意如果是成員函數的隱藏,只要函數名相同就會構成隱藏,與參數無關。
舉一個例子:
class Person { protected: string _name = "小六子"; int _num = 111; }; class Student :public Person { public: void Print() { cout << "姓名:" << _name << endl; cout << "身份證號:" << Person::_num << endl; cout << "學號:" << _num << endl; } protected: int _num = 999; }; int main() { Student s1; s1.Print(); }
在這段代碼中,Person和Student分別定義了_num,當子類對象中的成員函數直接訪問_num時,根據的是就近原則,訪問的是子類中的_num,當要訪問父類中的_num時,需要使用::來指定類域,就可以進行訪問。父類中的_num與子類中的_num構成隱藏。
這段代碼打印的結果是:
這里有一道小小的題目,是關于函數隱藏的:
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中的兩個函數能否調用成功?
b.func(10)可以調用成功,因為構成了隱藏。
b.func()不能調用成功,會發生變異報錯,因為隱藏了調不動。
對于六大默認成員函數我們這里暫時先討論4種重要的,即:構造函數,析構函數,拷貝構造,賦值運算符重載。
當我們不在子類中書寫時,編譯器會默認生成。這里只需要記住一句話:
繼承下來的成員調用父類的來處理,自己的按基本規則來處理。
以構造函數舉例:派生類中的父類成員調用父類中的構造函數,自己的成員按照構造函數自動生成的規則來。
自己寫的情況
1.父類沒有默認構造函數,需要我們自己寫構造函數。
2.子類有資源需要釋放,需要我們自己寫析構函數。
3.如果子類涉及淺拷貝問題,需要自己寫拷貝構造和賦值重載。
構造函數
父類成員調用對應的父類構造函數處理。子類成員按普通類處理。
舉一個例子:
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); }
看這一段代碼,父類中沒有默認構造函數(注意與默認成員函數區分),因此要初始化父類中的對象需要我們自己書寫子類中的構造函數。在書寫構造函數時,父類對象成員初始化使用父類中的構造函數,子類成員的初始化按正常方式書寫即可。
拷貝構造和運算符重載函數
Student(const Student& s) :Person(s) ,_num(s._num) {} Student& operator=(const Student& s) { if (this != &s) { Person::operator=(s);//不指明類域的話會發生自己調自己的情況 _num = s._num; return *this; } } int main() { Student s1(2,"zhangsan",2); Student s2(s1); Student s3 = s2; }
我們可以通過調試來查看結果:
析構函數
析構函數比較特殊,對于父類中的析構函數,我們不需要指定去書寫,就像下面這種情況:
//父類中的析構 ~Person() { cout << "~Person" << endl; } //子類中的析構 ~Student() { Person::~Person(); }
注意,析構函數的名字在最后會被統一處理成destructor(),如果不指定類域的話,父類析構函數和子類析構函數會構成隱藏,因此需要指定類域。
對于上述int中的代碼,需要析構三個子類對象,打印出的結果是:
我們發現調用了六次父類中的析構函數。這說明每個對象的父類成員都被析構了兩次。如果需要釋放空間,則一定會報錯。
先說結論:我們自己實現子類構造函數時,不需要顯示調用父類析構函數,我們顯示調用一次,它還會自動調用一次。
下面簡單說明一下,為什么程序需要自動調用:
我們知道變量的定義是發生在棧中的,因此就存在構造和析構的順序問題,棧滿足先入后出原則,因此先構造的需要后析構。
在構造的過程中,我們會先初始化父類成員,再初始化子類成員。因此我們需要先析構子類成員,再析構父類成員。
如果先析構父類會打亂棧的順序,因此編譯器會自動調用父類的析構函數。
這個只需要記住兩點:
1.友元關系不能繼承。
2.靜態成員會被繼承下來,無論繼承多少,靜態成員只有一個。
一個類有兩個及以上父類時稱這個繼承關系為多繼承。
class Student { public: protected: int _id; }; class Teacher { public: protected: int _course; }; class Assistant:public Student,public Teacher { public: protected: protected: };
我們使用逗號表示分隔,即繼承多個父類。可以通過調試來觀察子類Assitant的內容:
菱形繼承是多繼承的一種情況:
具有這樣的繼承關系的稱為菱形繼承。
菱形繼承出現的問題:從對象成員模型構造,可以看出菱形繼承有數據冗余和二義性的問題。
數據冗余指的是類Assistant中會有兩份Person的成員,二義性指的是這兩份成員每一次調用不知道調用的的是哪一個,需要指定類域。
這段代碼表示的就是菱形繼承的關系:
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; }
我們通過調試可以觀測a中的內容,發現會存在兩份Person中的成員:
如果要對這兩個Person成員賦值時,需要指定類域。
a.Student::_name = "xxx"; a.Teacher::_name = "yyy"; }
這就是所謂的二義性,在實際中一個人不能有兩個名字,對于冗余性來說,如果Person中有一個很大的數組浪費的空間會很多。
虛繼承可以解決菱形繼承的二義性和數據冗余問題。如上面的繼承關系,在Student和Teacher的繼承Person時使用的虛擬繼承,即可解決問題。需要注意的是,虛擬繼承不要在其他地方去使用。
class Student:virtual public Person { public: protected: int _num; }; class Teacher:virtual public Person { public: protected: int _id; };
只需要在菱形的腰部兩個父類加入virtual關鍵詞即可。
注意要在菱形的腰部。
當加完之后,在Assistant的對象中,Person類的_name成員就只有一個了。無論是否指定類域,更改的變量都只有一個:
內存演示
要研究虛繼承的原理,我們給出一個簡化的菱形繼承結構,再借助內存窗口窗口觀察對象成員的模型。
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; }
當沒使用虛繼承(即沒有使用virtual時)
我們使用內存窗口來觀察內容:
通過觀察內存中的布局,我們發現d中的B父類對象和C父類對象中的內容分別是連續存放的,B中有父類A中成員_a的值是1,其自己成員_b的值是3,兩者的內存是挨著的,C同理,對于D類中自己的成員_d,放在了內存的最后。
確定d中B類對象和C類對象的存儲順序是根據繼承順序決定的。由于上述代碼是class D :public B, public C,因此B類的對象會存在C類的前面。
而當我們給腰部加上virtual構成虛繼承之后:
class B:virtual public A { public: int _b; }; class C:virtual public A { public: int _c; };
使用virtual之后,我們發現已經將A中對象_a放入在了最后,因此無論指定不指定類域,改變的都是同一個_a的值。
但同時我們發現內存中多了兩行,那么這兩行是干什么的呢?
虛基表
從格式來看,這兩行顯然是都是地址。
我們再開辟一個內存2,向其中輸入上面地址,我們發現地址中存儲的內容是00 00 00 00,C類對象中同理,這里就不演示了。
這里00 00 00 00的意義在后面多態中會學習到,注意看它的下一個位置存放的是00 00 00 14
這里是十六進制,因此表示的是20這個數字。
再來看內存1:
兩者的地址之差剛剛好是20個字節。
因此我們可以知道:在虛繼承中,B類對象和C類對象的內存中新加入的是一個地址,分別用于尋找兩者與A類型變量的偏移量。B類對象與A類對象的偏移量是20,同理可驗證C類對象的偏移量是12。而內存2也有一個專有名詞:虛基表
總結:A一般叫做虛基類,在D里面,A類成員放在一個公共的位置,有時B要找A,C要找A,就要通過虛基表中的偏移量進行計算。
比如,當我們再用B類和C類建立兩個變量:
B b = d; C c = d;
此時會發生切片處理,需要將d中的A類對象賦值到b和c中,此時就需要使用到虛基表來尋找。
再比如:
B* pb = &d; pb->_a = 10;
pb指向了d的首地址,要更改d中的_a的值,指針pb也需要使用虛基表來進行尋找。
首先我們要對繼承和組合進行區分:
繼承表示的是子類繼承父類,組合表示的是在一個類中定義了另一個類的成員變量。
//繼承 class A { public: int _a; }; class B:public A { public: int _b; }; //組合 class C { public: int _c; }; class D { public: int _d; C _obj; };
我們需要明確一點:類之間,模塊之間最好是低耦合,高內聚的,因為方便維護。
低耦合:類之間依賴關系越弱越好。
高內聚:內部成員關系緊密。
1.繼承對應于白盒:B可以直接使用A中的公有和保護成員,破壞了封裝性。
2.組合對應于黑盒:D只能使用C的公有,不能直接使用保護成員。
舉一個例子:
如果A中有5個public,5個protected
對于組合來說,非基類只能使用這5個public,基類中的其他成員隨便修改都不會影響該非基類。
對于繼承來說,基類中一切的改變都會影響子類。
那可以拋棄繼承的語法嗎?當然是不行的。
多態是建立在繼承的基礎上的。
1.如果B就是一個A,比如Student是一個Person,我們稱這種關系為is-a關系,此時適合使用繼承。
2.如果D被包含于C,比如head包含eyes,我們稱這種關系為has-a關系,此時適合使用組合。
3.當遇到特殊情況,is-a和has-a都可以講通時,優先使用組合。
以上就是“C++中單繼承與多繼承如何使用”這篇文章的所有內容,感謝各位的閱讀!相信大家閱讀完這篇文章都有很大的收獲,小編每天都會為大家更新不同的知識,如果還想學習更多的知識,請關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。