您好,登錄后才能下訂單哦!
這篇文章主要介紹了C++繼承與菱形繼承怎么定義的相關知識,內容詳細易懂,操作簡單快捷,具有一定借鑒價值,相信大家閱讀完這篇C++繼承與菱形繼承怎么定義文章都會有所收獲,下面我們一起來看看吧。
繼承機制是面向對象程序設計的一種實現代碼復用的重要手段,它允許程序員在保持原有類特性的基礎上進行拓展,增加其他的功能,在此基礎上也就產生了一個新的類,稱為派生類。繼承呈現了面向對象程序設計的層次結構,是類設計層次的復用。
//以下代碼就是采用了繼承機制的一個場景 class person { protected: char _name[28]; int _age; char _id[30]; }; //繼承是代碼復用的一種重要手段 class student :public person { protected: char _academy[50]; //學院 };
繼承的格式
在前面的例子中,person是基類,student是派生類,繼承方式是public. 這是很容易記憶的,person是基礎的類,student是在person這個類的基礎之上派生出來的。這就非常地像父子關系,所以基類又可以稱為父類,派生類又可為子類。子類的后面緊跟著:
,是:
后面這個類派生出來的。
繼承關系和訪問限定符
繼承的幾種方式和訪問限定符是相似的。
三種繼承方式:public繼承、protected繼承、private繼承。
三種訪問限定符:public訪問、protected訪問、private訪問。
基類類成員的訪問權限和派生類繼承基類的繼承方式, 關系到了基類被繼承下來的類成員在派生類中的情況。ps:這句話起始很好理解地,就是這句話寫起來就變得繞口和復雜了,哈哈哈????.
基類成員/繼承方式 | public繼承 | protected繼承 | private繼承 |
---|---|---|---|
public成員 | 在派生類中為public成員 | 在派生類中為protected成員 | 在派生類中為private成員 |
protected成員 | 在派生類中為protected成員 | 在派生類中為protected成員 | 在派生類中為private成員 |
private成員 | 在派生類中不可見 | 在派生類中不可見 | 在派生類中不可見 |
這里的不可見指的是:基類中的private成員也是被繼承下來了的,只是在語法上,在派生類的類里和類外都不能夠訪問。
記住這個特殊的點,那么其他的就可理解為“權限問題”,這里“權限只能縮小,不能放大”。例如,基類的public成員以private繼承方式繼承下來,為“權限小的那個”,也就是繼承下來后在派生類中是private成員。
class person { protected: char _name[28]; char _id[30]; private: int _age; }; class teacher :public person { public: teacher() :_age(0) //基類的private成員在派生類里不能訪問 { } protected: char _jodid[20]; //工號 }; int main(void) { teacher t1; t1._age; //基類的private成員在類外不能訪問 return 0; }
派生類的對象可以賦值給其基類的對象、基類的指針、基類的引用。
就像上面這樣,取基類需要被賦值的值過去即可。
派生類賦值給基類的對象、基類的指針、基類的引用。在派生類中取基類需要的,就像把派生類給切割了一樣、所以這里有一個形象的稱呼:切割/切片
class Person { protected: string _name; // 姓名 string _sex; // 性別 int _age; // 年齡 }; class Student : public Person { public: int _id; // 學號 }; int main(void) { //可以將派生類賦值給基類的對象、指針、引用 Person p; Student s; p = s; Person* Pptr = &s; Person& Refp = s; //注意不能將將基類對象給派生類對象 //s = p; //允許將基類指針賦值給派生類指針,但是需要強制轉換 Student* sPtr = (Student*)Pptr; return 0; }
【注意】
1、不允許基類對象賦值給派生類對象
2、允許基類指針賦值給派生類指針, 但是需要強制轉化。這種轉化雖然可以,但是會存在越界訪問的問題。
基類和派生類都有獨立的作用域。繼承下來的基類成員在一個作用域,派生類的成員在另一作用域。
//以下代碼的運行結果是什么? class Person { protected: string _name = "楊XX"; // 姓名 int _num = 12138; // 身份證號 }; class Student : public Person { public: void Print() { cout <<_num << endl; } protected: int _num = 52622; // 學號 }; void Test() { Student s1; s1.Print(); };
基類中有一個_num 給了缺省值“12138”, 派生類中也有一個_name,給了缺省值“52622”,那么在派生類里直接使用_name,使用的具體是哪一個類里的?
使用的是派生類Student里的。
總結:基類和派生類中如果有同名成員,派生類將屏蔽基類對同名成員的直接訪問,這種情況稱為隱藏 , 或者稱為重定義。
如果想要訪問,則使用基類::基類成員
顯示的訪問。
class Person { protected: string _name = "楊XX"; // 姓名 int _num = 12138; // 身份證號 }; class Student : public Person { public: void Print() { cout << "身份證號:" << Person::_num << endl; cout << "學號:" << _num << endl; } protected: int _num = 52622; // 學號 }; void Test() { Student s1; s1.Print(); }; int main(void) { Test(); return 0; }
運行結果
我們已經了解了什么是隱藏。那么來看一下下面這些代碼。
//以下的兩個函數構成隱藏還是重載? class A { public: void func() { cout << "func()" << endl; } }; class B : public A { public: void func(int num) { cout << "func(int num)" << endl; } }; void Test() { B b; b.func(10); }
函數重載要求在同一作用域,而被繼承下來的基類成員和派生類成員在不同的作用域,所以構成的是隱藏。
```cpp //以下代碼的運行結果是什么? class A { public: void func() { cout << "func()" << endl; } }; class B : public A { public: void func(int num) { cout << "func(int num)" << endl; } }; void Test() { B b; b.func(); }
因為func()
函數隱藏了,在派生類的作用域內沒有func()
函數,所以會出現編譯報錯。
類有8個默認成員函數,這里只說重點的四個默認成員函數:構造函數、析構函數、拷貝構造函數、賦值重載函數
如果我們不寫派生類的構造函數和析構函數,編譯器會做如下的事情:
1、基類被繼承下來的部分會調用基類的默認構造函數和析構函數
2、派生類自己也會生成默認構造和析構函數,派生類自己的和普通類的處理一樣
如果我們不寫派生類的賦值構造函數和拷貝構造函數,編譯器會做如下的事情
3、基類被繼承下來的部分會調用基類的默認拷貝構造函數和賦值構造函數。
4、派生類自己也會生成默認賦值拷貝構造函數和賦值函數,和普通類的處理一樣。
什么情況下需要自己寫?
1、父類沒有合適的默認構造函數,需要自己顯示地寫
2、如果子類有資源需要釋放,就需要自己顯示地寫析構函數
3、如果子類存在淺拷貝的問題,就需要自己實現拷貝構造和賦值函數解決淺拷貝的問題。
如果需要自己寫派生類的這幾個重點成員函數,那么該如何寫?
//如果需要自己實現派生類的幾個四個重點默認成員函數,需要如何實現?該注意什么? class Person { public: Person(const char* name) :_name(name) { cout << "Person(const char* name)" << endl; //方便查看它什么被調用了 } Person(const Person& p) :_name(p._name) { cout << "Person(const Person& p)" << endl; } Person& operator=(const Person& p) { cout << "Person& operator=(const Person& p)" << endl; //首先排除自己給自己賦值 if (this != &p) { _name = p._name; } return *this; } ~Person() { cout << "~Person()" << endl; } protected: string _name; //姓名 }; class Student : public Person { protected: int _id; //學號 int* _ptr = new int[10]; //給一個需要自己實現默認成員函數場景用以舉例 };
1、實現派生類的構造函數:需要調用基類的構造函數初始化被繼承下來的基類部分的成員。如果基類沒有合適的默認構造函數,就需要在實現派生類構造函數的初始化列表階段顯示調用。
2、實現派生類的析構函數:派生類的析構函數會在被調用完成后自動調用基類的析構函數清理被繼承下來的基類成員。這樣可以保證派生類自己的成員的清理先于被繼承下來的基類成員。ps:析構函數名字會被統一處理成destructor()
,所以被繼承下來的基類的析構函數和派生類的析構函數構成隱藏。
3、實現派生類的拷貝構造函數:需要調用基類的拷貝構造函數完成被繼承下來的基類成員的拷貝初始化。
4、實現派生類的operator=:需要調用基類的operator=完成被繼承下來的基類成員的賦值。
5、派生類對象初始化先調用基類構造再調用派生類構造。
class Student : public Person { public: Student(const char* name, int id) : Person(name) , _id(id) { cout << "Student()" << endl; } Student(const Student& s) : Person(s) , _id(s._id) { cout << "Student(const Student& s)" << endl; } Student& operator = (const Student& s) { cout << "Student& operator= (const Student& s)" << endl; if (this != &s) { Person::operator =(s); _id = s._id; } return *this; } ~Student() { cout << "~Student()" << endl; } protected: int _id; //學號 };
繼承可分為單繼承和多繼承。
單繼承:一個派生類只有一個直接基類
多繼承:一個派生類有兩個或兩個以上的直接基類。
而多繼承中又存在著一種特殊的繼承關系,菱形繼承
它們之間的繼承關系邏輯上就類似一個菱形,所以稱為菱形繼承。菱形繼承相對于其他繼承關系是復雜的。
B中有一份A的成員,C中也有一份A的成員,D將B和C都繼承了,那么D中被繼承下來的A的成員不就有兩份了嗎?不難看出,菱形繼承有數據冗余和二義性的問題。
class Person { public: string _name; // 姓名 }; class Student : public Person { public: int _num; //學號 }; class Teacher : public Person { public: int _id; // 職工編號 }; class Assistant : public Student, public Teacher { public: string _majorCourse; // 主修課程 }; int main() { // 二義性、數據冗余 Assistant a; a._id = 1; a._num = 2; // 這樣會有二義性無法明確知道訪問的是哪一個 a._name = "peter"; return 0; }
上面的繼承關系如下:
此時Assitant中有兩份_name.存在數據冗余和二義性的問題。
二義性的問題是比較好解決的,使用::
指定就可以了,但是并不能解決數據冗余的問題。
int main() { // 二義性、數據冗余 Assistant a; a._id = 1; a._num = 2; a.Student::_name = "小張"; a.Teacher::_name = "張老師"; return 0; }
虛擬繼承可以解決繼承的數據冗余和二義性的問題。如上面所畫的邏輯繼承關系。在開始可能產生數據冗余和二義性的地方使用虛擬繼承,即可解決,但是在其他地方不要去使用虛擬繼承。
虛擬繼承格式
虛擬繼承解決數據冗余和二義性的原理
為了更好地研究,在這里給出一個比較簡單的菱形繼承體系
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; }
B和C中都有一份A的數據可以看出數據的冗余。
現在增加虛擬繼承機制,解決數據冗余和二義性。
class A { public: int _a; }; class B : virtual public A { public: int _b; }; class C : virtual 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; }
再次調式調用內存窗口,會發現
和沒有采用虛擬繼承的內存窗口有較大的變化。
B中的地址0x00677bdc里有什么?C中的地址0x00677be4里有什么?
從內存窗口可看出,菱形虛擬繼承,內存中只在對象組成的最高處地址保存了一份A,A是B、C公共的。而B和C里分別保存了一個指針,該指針指向一張表。這張表稱為虛基表,而指向虛基表的指針稱虛基指針。虛基表中保存的值,是到A地址的偏移量,通過這個偏移量就能夠找到A了。
在沒有學習繼承之前,我們其實頻繁地使用組合。
class head { private: int _eye; int _ear; int _mouth; }; class hand { private: int _arm; int _fingers; }; class Person { //組合 //一個人由手、頭等組合 hand _a; head _b; };
繼承是一種is-a的關系, 每一個派生類是基類,例如,Student是一個Person, Teacher 是一個Person
組合是一種has-a的關系,Person組合了head, hand, 每一個Person對象中都有一個head、hand對象。
如果某種情況既可以使用繼承又可以使用組合,那么優先使用對象組合,而不是類繼承。
友元關系不能被繼承,好比父親的朋友不一定是你的朋友。
如果基類中定義了靜態成員,當這個基類被實例化后出現了一份,那么整個繼承體系中都只有這一份實例。
關于“C++繼承與菱形繼承怎么定義”這篇文章的內容就介紹到這里,感謝各位的閱讀!相信大家對“C++繼承與菱形繼承怎么定義”知識都有一定的了解,大家如果還想學習更多知識,歡迎關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。