C++數(shù)據(jù)結(jié)構(gòu)繼承的概念與菱形繼承及虛擬繼承和組合
??博客代碼已上傳至gitee:https://gitee.com/byte-binxin/cpp-class-code
??繼承的概念
繼承:繼承機(jī)制是面向?qū)ο蟪绦蛟O(shè)計使代碼可以復(fù)用的最重要的手段,它允許程序員在保持原有類特性的基礎(chǔ)上進(jìn)行擴(kuò)展,增加功能,這樣產(chǎn)生新的類,稱派生類。繼承呈現(xiàn)了面向?qū)ο蟪绦蛟O(shè)計的層次結(jié)構(gòu),體現(xiàn)了由簡單到復(fù)雜的認(rèn)知過程。以前我們接觸的復(fù)用都是函數(shù)復(fù)用,繼承是類設(shè)計層次的復(fù)用。
??繼承的定義
語法:
說明: 派生類會將基類的成員變量和成員函數(shù)都繼承下來,但是訪問限定符會根據(jù)繼承方式而發(fā)生變化。
繼承方式有三種:
- public繼承
- protected繼承
- private繼承
訪問限定符:
- public訪問
- protected訪問
- private訪問
繼承基類成員的訪問方式的變化:
類成員/繼承方式 | public繼承 | protected繼承 | private繼承 |
---|---|---|---|
基類的public成員 | 派生類的public成員 | 派生類的protected成員 | 派生類的private成員 |
基類的protected成員 | 派生類的protected成員 | 派生類的protected成員 | 派生類的private成員 |
基類的private成員 | 派生類中不可見 | 派生類中不可見 | 派生類中不可見 |
總結(jié):
- 基類的private成員在派生類中都是不可見的,這里的不可見是指基類的私有成員還是被繼承到了派生類對象中,但是語法上限制派生類對象不管在類里面還是類外面都不能去訪問它。
- 基類成員在父類中的訪問方式=min(成員在基類的訪問限定符,繼承方式),public>protected>private。
- 一般會把基類中不想讓類外訪問的成員設(shè)置為protecd成員,不讓類外訪問,但是讓派生類可以訪問。
??基類和派生類對象之間的賦值轉(zhuǎn)換
派生類對象會通過 “切片” 或 “切割” 的方式賦值給基類的對象、指針或引用。但是基類對象不能賦值給派生類對象。
實例演示:
class Person { public: Person(const char* name = "") :_name(name) {} void Print() { cout << "name:" << _name << " age:" << _age << endl; } protected: string _name = ""; int _age = 1; }; class Student : public Person { public: Student() :Person("xiaoming") {} void Print() { cout << "name:" << _name << " age:" << _age << " _stuid:" << _stuid << " _major:" << _major << endl; } private: int _stuid = 0;// 學(xué)號 int _major = 0;// 專業(yè) }; int main() { Student s; // 子類對象可以賦值給父類的對象、指針和引用 反過來不行 // Student對象通過 “切片” 或 “切割” 的方式進(jìn)行賦值 Person p1 = s; Person* p2 = &s; Person& p3 = s; p1.Print(); p2->Print(); p3.Print(); // 基類的指針可以通過強(qiáng)制類型轉(zhuǎn)換賦值給派生類的指針 Student* ps = (Student*)p2; ps->Print(); return 0; }
總結(jié):
- 派生類對象可以“切片”或“切割”的方式賦值給基類的對象,基類的指針或基類的引用,就是把基類的那部分切割下來。
- 基類對象不能給派生類對象賦值。
- 基類的指針可以通過強(qiáng)制類型轉(zhuǎn)換賦值給派生類的指針。但必須是基類的指針指向派生類的對象才是安全的,因為如果基類是多態(tài)類型,會引發(fā)多態(tài)。
??繼承中的作用域
在繼承體系中,基類和派生類對象都有獨(dú)立的作用域,子類中的成員(成員變量和成員函數(shù))會對父類的同名成員進(jìn)行隱藏,也叫重定義。
實例演示:
class Person { public: Person(const char* name = "") :_name(name) {} void Print() { cout << "name:" << _name << " age:" << _age << endl; } protected: string _name = ""; int _age = 1; }; class Teacher : public Person { public: void Print() { cout << "name:" << _name << " age:" << _age << " jobid:" << _jobid << endl; } private: int _jobid = 0;// 工號 }; int main() { Teacher t; t.Print(); t.Person::Print();// 子類會隱藏(重定義)父類的同名成員(同名函數(shù)或同名成員變量) 可以通過指定域作用限定符訪問 return 0; }
代碼運(yùn)行結(jié)果如下:
得出結(jié)論: 子類中的成員(成員變量和成員函數(shù))會對父類的同名成員進(jìn)行隱藏,如果相要訪問父類的同名成員,必須指定類域訪問。
看下面一個小問題: 請問A中的fun函數(shù)和B中的fun函數(shù)是構(gòu)成重載還是隱藏?
class A { public: void fun() { cout << "func()" << endl; } }; class B : public A { public: void fun(int i) { A::fun(); cout << "func(int i)->" << i << endl; } }; void Test() { B b; b.fun(10); };
答案: 兩個函數(shù)在不同的作用域,不可能構(gòu)成重載。因為構(gòu)成重載的條件是兩個函數(shù)必須在同一作用域,而隱藏是要求在基類和派生類不同作用域的,所以這里同名成員是構(gòu)造隱藏。
??派生類的默認(rèn)成員函數(shù)
C++中的每個對象中會有6個默認(rèn)成員函數(shù)。默認(rèn)的意思就是我們不寫,編譯器會生成一個。那么在繼承中,子類的默認(rèn)成員函數(shù)是怎么生成的呢?
先看下面一個例子:
class Person { public: Person(const char* name = "", int age = 1) :_name(name) ,_age(age) { cout << "Person()" << endl; } Person(const Person& p) :_name(p._name) , _age(p._age) { cout << "Person(const Person& p)" << endl; } Person& operator=(const Person& p) { _name = p._name; _age = p._age; cout << "Person& operator=(const Person& p)" << endl; return *this; } void Print() { cout << "name:" << _name << " age:" << _age << endl; } ~Person() { cout << "~Person()" << endl; } protected: string _name; int _age; }; class Student : public Person { public: Student(const char* name, int age, int stuid = 0) :Person(name, age)// 此處調(diào)用父類的構(gòu)造函數(shù)堆繼承下來的成員進(jìn)行初始化,不謝的話,編譯器調(diào)用父類的默認(rèn)構(gòu)造函數(shù) , _stuid(stuid) { cout << "Student()" << endl; } Student(const Student& s) :Person(s)// 子類對象可以傳給父類的對象、指針或引用 ,_stuid(s._stuid) { cout << "Student(const Student& s)" << endl; } Student& operator=(const Student& s) { cout << "Student& operator=(const Student& s)" << endl; if (this != &s) { Person::operator=(s);// 先完成基類的復(fù)制 _stuid = s._stuid; } return *this; } void Print() { cout << "name:" << _name << " age:" << _age << " _stuid:" << _stuid << endl; } ~Student() { // 基類和派生類的析構(gòu)函數(shù)的函數(shù)名都被編譯器處理成了destruction,構(gòu)成隱藏,是一樣指定域訪問 //Person::~Person();// 不需要顯示調(diào)用 編譯器會自動先調(diào)用派生類的析構(gòu)函數(shù),然后調(diào)用基類的析構(gòu)函數(shù) cout << "~Student()" << endl; } private: int _stuid;// 學(xué)號 };
測試1:構(gòu)造函數(shù)和析構(gòu)函數(shù)
void test1() { Student s("小明",18,10); }
代碼運(yùn)行結(jié)果如下:
總結(jié)1: 子類的構(gòu)造函數(shù)必須調(diào)用基類的構(gòu)造函數(shù)初始化基類的那一部分成員,如果基類沒有默認(rèn)構(gòu)造函數(shù),則必須在派生類構(gòu)造函數(shù)的初始化列表階段顯示調(diào)用。子類的析構(gòu)函數(shù)會在被調(diào)用完成后自動調(diào)用基類的析構(gòu)函數(shù)清理基類的成員。不需要顯示調(diào)用。這里子類和父類的析構(gòu)函數(shù)的函數(shù)名會被編譯器處理成destructor,這樣兩個函數(shù)構(gòu)成隱藏。
測試2:拷貝構(gòu)造函數(shù)
void test2() { Student s1("小明", 18, 10); Student s2(s1); }
代碼運(yùn)行結(jié)果如下:
總結(jié)2: 子類的拷貝構(gòu)造必須代用父類的拷貝構(gòu)造完成父類成員的拷貝。
測試3:operator=
結(jié)論3: 子類的operator=必須調(diào)用基類的operator完成基類的賦值。
思考
如何設(shè)計一個不能被繼承的類? 把該類的構(gòu)造函數(shù)設(shè)為私有。如果基類的構(gòu)造函數(shù)是私有,那么派生類不能調(diào)用基類的構(gòu)造函數(shù)完成基類成員的初始化,則無法進(jìn)行構(gòu)造。所以這樣設(shè)計的類不可以被繼承。(后面還會將加上final關(guān)鍵字的類也不可以被繼承)
總結(jié):
- 子類的構(gòu)造函數(shù)必須調(diào)用基類的構(gòu)造函數(shù)初始化基類的那一部分成員,如果基類沒有默認(rèn)構(gòu)造函數(shù),則必須在派生類構(gòu)造函數(shù)的初始化列表階段顯示調(diào)用。
- 子類的拷貝構(gòu)造必須代用父類的拷貝構(gòu)造完成父類成員的拷貝。
- 子類的operator=必須調(diào)用基類的operator完成基類的賦值。
- 子類的析構(gòu)函數(shù)會在被調(diào)用完成后自動調(diào)用基類的析構(gòu)函數(shù)清理基類的成員。不需要顯示調(diào)用。
- 子類對象會先調(diào)用父類的構(gòu)造在調(diào)用子類的構(gòu)造。
- 子類對象會先析構(gòu)子類的析構(gòu)再調(diào)用父類的析構(gòu)。
??繼承中的兩個小細(xì)節(jié)
??繼承和友元
友元關(guān)系不能被繼承。也就是說基類的友元不能夠訪問子類的私有和保護(hù)成員。
??繼承和靜態(tài)成員
基類定義的static靜態(tài)成員,存在于整個類中,不屬于某個類,無論右多少個派生類,都這有一個static成員。
實例演示:
class Person { public: Person() { ++_count; } // static成員存在于整個類 無論實例化出多少對象,都只有一個static成員實例 static int _count; }; int Person::_count = 0; class Student :public Person { public: int _stuid; }; int main() { Student s1; Student s2; Student s3; // Student()._count = 10; cout << "人數(shù):" << Student()._count - 1 << endl; return 0; }
代碼運(yùn)行結(jié)果如下:
??單繼承和多繼承(菱形繼承)
單繼承: 一個子類只有一個直接父類時稱這個繼承關(guān)系為單繼承。
多繼承: 一個子類有兩個或以上的直接父類時稱這個繼承關(guān)系為多繼承。
菱形繼承: 多繼承的一種特殊情況。
多繼承帶來的問題: 子類會得到兩份BenZ的數(shù)據(jù),會造成數(shù)據(jù)冗余和二義性。
??虛擬繼承
??概念
為了解決菱形繼承帶來的數(shù)據(jù)冗余和二義性的問題,C++提出來虛擬繼承這個概念。虛擬繼承可以解決前面的問題,在繼承方式前加椰果virtual的關(guān)鍵字即可。
class Person { public: string _name; }; // 不要在其他地方去使用。 class Student : virtual public Person { public: int _num; //學(xué)號 }; class Teacher : virtual public Person { public: int _id; // 職工編號 }; class Assistant : public Student, public Teacher { protected: string _majorCourse; // 主修課程 };
??虛擬繼承的原理
先看下面一串代碼:
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 = 4; d._c = 5; d._d = 6; return 0; }
我們通過內(nèi)存窗口查看它的對象模型:
原理: 從上圖可以看出,A對象同時屬于B和C,B和C中分別存放了一個指針,這個指針叫虛基表指針,分別指向的兩張表,叫虛基表,虛基表中存的是偏移量,B和C通過偏移量就可以找到公共空間(存放A對象的位置)。
??組合與繼承
總結(jié)一下幾點:
- 組合和繼承都屬于類層次的復(fù)用。
- public繼承是一種is-a的關(guān)系。也就是說每個派生類對象都是一個基類對象-。
- 組合是一種has-a的關(guān)系。假設(shè)B組合了A,每個B對象中都有一個A對象。
- 優(yōu)先使用對象組合,而不是類繼承 。
- 繼承允許你根據(jù)基類的實現(xiàn)來定義派生類的實現(xiàn)。這種通過生成派生類的復(fù)用通常被稱為白箱復(fù)用。術(shù)語“白箱”是相對可視性而言:在繼承方式中,基類的內(nèi)部細(xì)節(jié)對子類可見 。繼承一定程度破壞了基類的封裝,基類的改變,對派生類有很大的影響。派生類和基類間的依賴關(guān)系很強(qiáng),耦合度高。
- 對象組合是類繼承之外的另一種復(fù)用選擇。新的更復(fù)雜的功能可以通過組裝或組合對象來獲得。對象組合要求被組合的對象具有良好定義的接口。這種復(fù)用風(fēng)格被稱為黑箱復(fù)用,因為對象的內(nèi)部細(xì)是不可見的。對象只以“黑箱”的形式出現(xiàn)。 組合類之間沒有很強(qiáng)的依賴關(guān)系,耦合度低。優(yōu)先使用對象組合有助于你保持每個類被封裝。
- 實際盡量多去用組合。組合的耦合度低,代碼維護(hù)性好。不過繼承也有用武之地的,有些關(guān)系就適合繼承那就用繼承,另外要實現(xiàn)多態(tài),也必須要繼承。類之間的關(guān)系可以用繼承,可以用組合,就用組合。
C++的缺陷之一:
多繼承就是一個。多繼承會帶來菱形繼承,菱形繼承又會帶來數(shù)據(jù)冗余和二義性,為了解決這個問題,又引入了虛擬繼承。進(jìn)而導(dǎo)致C++的底層結(jié)構(gòu)對象模型非常復(fù)雜,這樣會帶來一定的損失。所以說盡量不要設(shè)計出菱形繼承。
??總結(jié)
C++的繼承使我們變得更加的富有,其中多繼承也是C++的缺陷。我們要盡量避開不好的而選擇好的一面。這篇博客就介紹到這里了,喜歡的話,歡迎點贊。支持和關(guān)注~
到此這篇關(guān)于C++數(shù)據(jù)結(jié)構(gòu)繼承的概念與菱形繼承及虛擬繼承和組合的文章就介紹到這了,更多相關(guān)C++ 繼承內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
C語言結(jié)構(gòu)體嵌套與對齊超詳細(xì)講解
這篇文章主要介紹了C語言結(jié)構(gòu)體嵌套與對齊,C語言中結(jié)構(gòu)體是一種構(gòu)造類型,和數(shù)組、基本數(shù)據(jù)類型一樣,可以定義指向該種類型的指針。結(jié)構(gòu)體指針的定義類似其他基本數(shù)據(jù)類型的定義2022-12-12探究在C++程序并發(fā)時保護(hù)共享數(shù)據(jù)的問題
這篇文章主要介紹了探究在C++程序并發(fā)時保護(hù)共享數(shù)據(jù)的問題,也有利于大家更好地理解C++多線程的一些機(jī)制,需要的朋友可以參考下2015-07-07