關(guān)于C++繼承你可能會忽視的點(diǎn)
前言
繼承是使代碼復(fù)用的一種重要的手段,我們在C語言時期寫的swap函數(shù)邏輯,通常會單獨(dú)寫出來再給其他函數(shù)復(fù)用,這個繼承可以理解成是類級別的一個復(fù)用,它允許我們在原有類的基礎(chǔ)上進(jìn)行擴(kuò)展,增加新的功能。
一、什么是繼承
舉個例子,當(dāng)我們使用一個結(jié)構(gòu)體去描述一個學(xué)生的信息時,我們可以用到以下的這樣一個組織方式:
struct Student { char sex[20];//性別 int age;//年齡 char stu_id;//學(xué)號 //..... };
當(dāng)我們要描述一名老師的時候我們這個時候可能就是更改學(xué)生信息當(dāng)中的部分的信息,例如上面的性別和年齡是可以通用的,而學(xué)號只需更改成工號即可。那么我們應(yīng)該怎么去達(dá)到復(fù)用的邏輯呢?
:我們可以寫一個struct People,讓struct Student和struct Teacher去復(fù)用它。
struct People { char sex[20];//性別 int age;//年齡 }; struct Student { struct People p; char stu_id;//學(xué)號 //..... }; struct Teacher { struct People p; char work_id;//工號 //..... };
這樣子我們用之前的C語言的知識就可以完成一個簡單的復(fù)用,這樣子做會有幾個不好的地方:
- 對于People內(nèi)部的訪問會比起訪問他自己內(nèi)部定義的變量麻煩一點(diǎn)(就是如定義了Teacher t;要訪問age需要 t.p.age)。
- 若基類(父類)想要對于派生類(子類)有所隱藏,即并不想讓所有的成員函數(shù)/成員變量都給子類所繼承的時候,我們用這種方式很難做到。
基于以上的問題,C++給出了一套繼承邏輯。
上述第一個問題就解決了,我們可以直接在People t,直接訪問基類的成員,第二個問題,對于一些我們想要隱藏的成員函數(shù)/成員變量(即不讓子類可見),我們可以通過繼承方式來控制,在此之前先鋪墊一個知識點(diǎn)。
上圖若是開過c++這門課的同學(xué)肯定都有見過,其中的最左列是表明基類的被繼承的成員是什么類型的,第一行則是以哪種形式繼承。
基類的其他成員在子類的訪問方式 == Min(成員在基類的訪問限定符,繼承方式),public > protected > private 。
其中protected成員變量我們在繼承這塊見得會比較多,下面我們比較一下public/protected/private的在繼承當(dāng)中的作用。
- 若基類的成員當(dāng)中為public,則說明他支持類內(nèi),類外部都可以去訪問他,而這時候我們常用public繼承,因為基類都已經(jīng)開放了這個成員變量,派生類用其他兩種方式繼承都會讓他在派生類的類外部無法訪問,所以實(shí)際上public繼承是最常用的。
- 若基類的成員當(dāng)中是protected類型,表明基類允許子類當(dāng)中可以使用這個成員,但是不希望類外部使用,protected的訪問限定符只是對類外部上鎖,而類內(nèi)是可以隨意使用的,這樣實(shí)際上也是封裝性的一種體現(xiàn)。
- 若基類的成員是private類型,表明不希望子類和外部訪問,在子類當(dāng)中不可見,不可見即子類當(dāng)中無法訪問該成員。但是他是否存在于派生類當(dāng)中?是存在的。
實(shí)踐是檢驗真知的唯一標(biāo)準(zhǔn),下面我們試試是否private的成員在子類真的存在:
class People { public: char address;//住址 protected: double sex;//性別 private: int age;//年齡 }; class Student : protected People { char stu_id[20];//學(xué)號 //..... }; struct Teacher :public People { double work_id;//工號 //..... }; int main() { Teacher t; cout << sizeof(t) << endl;//對t變量的大小測試 printf("%p,%p", &t.address,&t.work_id); return 0; }
驗證結(jié)果為32,即下圖所示,我們可以得知People的元素是在Teacher之上的。
示意圖:
倘若強(qiáng)行訪問,則會報錯。到此問題2的答案也清晰了。
不推薦protected繼承的原因:
class People { public: char address;//住址 protected: double sex;//性別 private: int age;//年齡 }; struct Student : protected People { char stu_id[20];//學(xué)號 //..... }; int main() { Student s1; People p = s1;//error return 0; }
上面這個protected繼承后子類對象賦值給基類報錯!
其實(shí)是因為父類的public對象address在以protected方式繼承時相當(dāng)于在類外部不可訪問,當(dāng)賦值給People對象時,權(quán)限被放大,即若支持賦值,子類address是不對外開放的,而父類卻把成員變量公開了!
解決方案:使用public繼承!
二、基類與派生類的賦值轉(zhuǎn)換
2.1天然支持的理解
這個點(diǎn)是一個十分重要的點(diǎn),在學(xué)習(xí)java語言的時候,經(jīng)常聽到上轉(zhuǎn)型,其實(shí)也就是c++當(dāng)中將子類的對象賦值給父類,即People t = student s;
類似這種,這個過程是天然支持的,接下來敘述一下天然支持的含義。
預(yù)備知識:
int i = 0; double b = i;//1 double& b =i ;//2 error const double& b = i ;//3
從初識c語言的時候,我們就發(fā)現(xiàn)上面代碼的第一條是沒有問題的,這是因為相近類型在精度低給精度高的時候是不會出現(xiàn)問題的。這是因為編譯器會在此期間生成一塊臨時空間(臨時空間具有常性),用i生成一個double類型的i再賦值給b。
在上面的2代碼的時候為什么會出錯呢?原因很簡單,臨時空間具有常性,臨時空間是放到靜態(tài)區(qū)當(dāng)中的,不可修改,當(dāng)用double&時相當(dāng)于會對權(quán)限進(jìn)行放大(即b可能會更改i的內(nèi)容),所以我們加入const屬性的時候代碼3也就能夠跑過了。
上面的子類給父類為什么就是天然支持的呢?
int main() { Teacher t; People& p = t;//true People* p2 = &t;//true People p3 = t;//true return 0; }
上面的程序正常運(yùn)行,父類引用子類對象完全沒有問題!所以我們才說這是天然支持的,不像上面例子是通過轉(zhuǎn)換而來的。
這種派生類對象賦值給基類的對象/基類的指針/基類的引用,稱之為切片,這種說法是十分貼切的。通過切去子類的自己定義的部分在給到基類。
這里會有一些值得注意的點(diǎn):
基類的對象不能賦值給派生類對象?。?/p>
基類的指針是可以通過強(qiáng)制類型轉(zhuǎn)換來賦值給派生類的指針,但是基類的指針必須原先是指向派生類對象才是安全的。倘若基類是多態(tài)類型,可以用RTTI當(dāng)中的dynamic cast來進(jìn)行識別后進(jìn)行安全轉(zhuǎn)換。(后序博客會將,這里簡單說明就是使用dynamic cast,他會判斷指針指向的是不是派生類對象,如果是就轉(zhuǎn)換成功,不是就會返回null)
對于上面第二點(diǎn)做一個解釋,就是如果People * pp指向的是一個People的對象,那么當(dāng)他給到派生類的指針的時候,派生類指針是有可能訪問到未初始化的那部分。因為站在派生類指針的角度
,他并不知道自己的成員是沒有被定義的,倘若它使用了未初始化數(shù)據(jù),就會產(chǎn)生越界報錯??!
相反,如果原先的pp指針指向的是派生類對象(天然支持的),那么當(dāng)我們pp給到派生類的時候,對于派生類的而言,它的數(shù)據(jù)都是初始化好的,所以這個時候是沒有問題的。
三、繼承當(dāng)中的作用域
在繼承體系中基類和派生類都有獨(dú)立的作用域!!
代碼如下:
class People { public: char address[20] = "chang an";//住址 void func(int i) { cout << "People func\n"; } protected: char sex[20] = "nan";//性別 private: int age = 19;//年齡 }; struct Student : public People { char stu_id[20] = "1010";//學(xué)號 //..... void func() { cout << "Student func\n"; } }; int main() { People p; Student s; s.func(); //true s.func(1);//err return 0; }
從上面的例子可以看出,倘若基類和子類都在同一個作用域,那么func的有參和無參是構(gòu)成重載的,但是編譯器這里報錯,說明重載的一個重要條件不滿足,即函數(shù)不在同一作用域當(dāng)中。
這種子類成員將父類的成員屏蔽的情況,叫做隱藏,也叫重定義,倘若需要調(diào)用父類的函數(shù)需要在函數(shù)前顯示調(diào)用(指名類域)即可。
注意:
- 繼承體系當(dāng)中不建議定義同名的成員,因為會引發(fā)誤解。
- 但是在派生類的默認(rèn)成員函數(shù)當(dāng)中會用到這種語法,所以這種語法也是必不可少的??!
- 成員函數(shù)和成員變量都如此,基類定義相同名字的都會對父類進(jìn)行隱藏,調(diào)用都要顯示調(diào)用。
四、派生類的默認(rèn)構(gòu)造成員函數(shù)
1. 派生類的構(gòu)造函數(shù)必須調(diào)用基類的構(gòu)造函數(shù)初始化基類的那一部分成員。如果基類沒有默認(rèn)的構(gòu)造函數(shù),則必須在派生類構(gòu)造函數(shù)的初始化列表階段顯示調(diào)用。
2. 派生類的拷貝構(gòu)造函數(shù)必須調(diào)用基類的拷貝構(gòu)造完成基類的拷貝初始化。
3. 派生類的operator=必須要調(diào)用基類的operator=完成基類的復(fù)制。
4. 派生類的析構(gòu)函數(shù)會在被調(diào)用完成后自動調(diào)用基類的析構(gòu)函數(shù)清理基類成員。因為這樣才能保證派生類對象先清理派生類成員再清理基類成員的順序。
5. 派生類對象初始化先調(diào)用基類構(gòu)造再調(diào)派生類構(gòu)造。
6. 派生類對象析構(gòu)清理先調(diào)用派生類析構(gòu)再調(diào)基類的析構(gòu)。
總結(jié)前面的6個規(guī)則,個人的結(jié)論:
- 構(gòu)造函數(shù),拷貝構(gòu)造,operator=三種情況,都要調(diào)用父類對應(yīng)的構(gòu)造函數(shù)/拷貝構(gòu)造/operator=進(jìn)行對父類的成員變量的初始化,并且倘若父類沒有默認(rèn)的構(gòu)造函數(shù)的時候(比如父類寫了帶參的構(gòu)造函數(shù)),我們就要顯式調(diào)用(Person(參數(shù)…),Person::operator=(參數(shù)…))
- 析構(gòu)函數(shù)只需要清理子類定義的資源,由于在構(gòu)造函數(shù)當(dāng)中我們是先對父類的成員先進(jìn)行構(gòu)造,后對子類的成員進(jìn)行構(gòu)造。由先構(gòu)造后析構(gòu)的順序,所以我們是在析構(gòu)函數(shù)當(dāng)中析構(gòu)子類的資源,析構(gòu)函數(shù)調(diào)用完后編譯器自動幫我們調(diào)用父類的析構(gòu)函數(shù)。
在派生類當(dāng)中基類為一個自定義類型,會自動調(diào)用父類的構(gòu)造函數(shù)進(jìn)行初始化。在拷貝構(gòu)造當(dāng)中,由于子類把父類的部分當(dāng)做自定義類型,倘若沒有顯示調(diào)用拷貝構(gòu)造,就會調(diào)用到構(gòu)造函數(shù)上面對父類的部分進(jìn)行構(gòu)造。
在子類當(dāng)中直接對父類單獨(dú)的成員初始化是錯誤的,一定要把父類當(dāng)做一個整體進(jìn)行初始化!
4.0什么時候需要寫6個默認(rèn)成員函數(shù)
拋出結(jié)論:
1. 若父類沒有默認(rèn)構(gòu)造函數(shù)/默認(rèn)拷貝構(gòu)造函數(shù)或者有需要對成員的初始化(可以在聲明處給缺省值),或者編譯器提供的淺拷貝行為不能滿足我們的需求。
2. 當(dāng)我們成員變量中采用T*,自己維護(hù)在堆上開的空間時,我們往往需要對除取地址重載外的其余默認(rèn)成員函數(shù)進(jìn)行編寫。因為我們沒有選擇容器,自己動手維護(hù)堆上的資源時,若采用編譯器默認(rèn)生成的值拷貝的方式,分分鐘出錯!
4.1構(gòu)造函數(shù)
#include<iostream> using namespace std; class People { public: People() { cout << "People()\n"; } //p1(p) People(const People& p) { cout << "People(const People& p)" << endl; } // p1 = p People& operator=(const People& p) { cout << "People& operator=(const People& p)" << endl; return *this; } private: char name[20]; char address[20]; char tele[20]; }; class Student :public People { public: //無寫構(gòu)造 private: int id; }; int main() { Student t; return 0; }
在沒有寫派生類的構(gòu)造函數(shù)時,派生類會在編譯器生成的默認(rèn)構(gòu)造函數(shù)當(dāng)中在初始化列表處調(diào)用父類的構(gòu)造函數(shù)對父類的資源進(jìn)行初始化。
當(dāng)我們寫了子類的構(gòu)造函數(shù),但是沒有顯示調(diào)用父類的構(gòu)造函數(shù),編譯器依舊會在初始化列表處幫我們調(diào)用父類的構(gòu)造函數(shù)對父類的資源進(jìn)行初始化。
C++規(guī)定了派生類要先對父類資源進(jìn)行初始化,所以不管我們有沒有顯示調(diào)用父類的構(gòu)造函數(shù),編譯器都會幫我們調(diào)用。下面展示一下如何顯示調(diào)用
Student() :People() { cout << "Student()" << endl; }
倘若父類沒有寫默認(rèn)的構(gòu)造函數(shù),這個時候只能用顯示調(diào)用的方法對父類的資源初始化了。
調(diào)用方法看起來有點(diǎn)奇怪,用起來有點(diǎn)像創(chuàng)建匿名對象,但是便于理解,我們可以把他理解成子類當(dāng)中將父類看做自定義類型,所以會去默認(rèn)調(diào)用它的構(gòu)造函數(shù),而我們沒有顯示寫出父類的對象,所以初始化父類的形式用的是類名+(參數(shù)...)
4.2拷貝構(gòu)造
1.當(dāng)我們沒有編寫拷貝構(gòu)造函數(shù)的時候,我們發(fā)現(xiàn)編譯器幫我們默認(rèn)生成的拷貝構(gòu)造會自動調(diào)用父類的拷貝構(gòu)造。
那么我們是否跟構(gòu)造函數(shù)一樣只拷貝子類的資源即可,編譯器是否會幫我們也在初始化列表處對父類資源進(jìn)行拷貝?
不會
看下面這張圖,我們發(fā)現(xiàn)拷貝構(gòu)造當(dāng)中調(diào)用了父類的構(gòu)造函數(shù)
有的同學(xué)就會有疑惑了,實(shí)際上拷貝構(gòu)造也是構(gòu)造,在初始化列表處,對于子類而言,父類相當(dāng)于一個自定義類型對象,子類會調(diào)用父類的構(gòu)造函數(shù)對父類的資源進(jìn)行初始化。
解決方法:顯示調(diào)用父類的拷貝構(gòu)造即可,所以拷貝構(gòu)造這里我們一定要要寫就一定要顯示調(diào)用父類的拷貝構(gòu)造。
Student(const Student& s) :People(s) { cout << "Student(const Student& s)" << endl; }
4.3賦值重載
老樣子,先看看編譯器生成的默認(rèn)的operator=是怎樣的。
很顯然,編譯器會自動調(diào)用父類的operator=對父類的部分進(jìn)行賦值,賦值重載與拷貝構(gòu)造一樣,需要我們顯示調(diào)用父類的賦值重載,否則雖然不會報錯,但是不滿足我們所需要的行為。
所以我們在函數(shù)體內(nèi)調(diào)用父類oeperator=即可:
五、菱形繼承和菱形虛擬繼承
單/多繼承的定義:
單繼承:一種繼承機(jī)制。其中每個子類只能繼承單一的超類。
多繼承:多繼承可以看作是單繼承的擴(kuò)展。所謂多繼承是指派生類具有多個基類,派生類與每個基類之間的關(guān)系仍可看作是一個單繼承。
多繼承本身并沒有問題,但是它的擴(kuò)展形成菱形繼承出現(xiàn)了問題,讓我們學(xué)習(xí)的過程中需要學(xué)習(xí)更加復(fù)雜的解決方案。
5.1菱形繼承
以下面這張圖為例。
struct Base { int base; }; struct A :public Base { int a; }; struct C :public Base { int c; }; struct D :public A ,public C { int d; };
在沒有虛繼承前,對象模型如下圖,可以看出base在D有出現(xiàn)了兩份,也就是在D所創(chuàng)建的對象當(dāng)中都會出現(xiàn)二義性和數(shù)據(jù)冗余的問題!
解決方案
虛繼承,在腰部的類繼承時添加virtual關(guān)鍵字。
struct Base { int base; }; struct A :virtual public Base { int a; }; struct C :virtual public Base { int c; }; struct D :public A ,public C { int d; }; int main() { D d; d.c = 1; d.d = 2; d.a = 3; return 0; }
測試平臺:vs2013/32位
虛繼承后的內(nèi)存對象成員模型,其中每個腰部虛繼承的A,C的對象都多了一個指針,由于我們是小端機(jī),所以對應(yīng)過去我們能看到指向的空間當(dāng)中對應(yīng)8字節(jié),表中頭4字節(jié)00 00 00 00與多態(tài)有關(guān),下面的則是偏移量,由于虛繼承后只有一份A對象的成員變量,并且表結(jié)構(gòu)需要8字節(jié)的空間,所以A,C對象當(dāng)中存放的是虛基表指針,指向的是虛基表,虛基表一般是放在代碼段 當(dāng)中的。
為什么C,A需要去找屬于自己的Base?
基類與派生類的賦值轉(zhuǎn)換時,需要進(jìn)行切片,需要將A,C當(dāng)中的base變量才能賦值給b。
int main() { Base b = A(); Base b2 = C(); return 0; }
上述圖中B是否虛繼承都可以,只要A,C虛繼承,D中都不會出現(xiàn)二義性了。
六、繼承的總結(jié)
在繼承這塊實(shí)際上是c++語法復(fù)雜的一處體現(xiàn)了,有了多繼承,就有了菱形繼承,相對應(yīng)他的解決方案來了,但是我們可以發(fā)現(xiàn)這套解決方案讓他的底層實(shí)現(xiàn)必定變得復(fù)雜了起來,所以正常使用的時候我們并不推薦去折騰菱形繼承,在java等語言都把多繼承這一塊砍掉了,使用多繼承的同時就要考慮復(fù)雜度和性能上的問題。
繼承和組合
繼承是一種復(fù)用的方式,但不是唯一方式!
- public繼承是一種is-a的關(guān)系,每一個派生類都是一個基類對象。
- 組合是一種has-a的關(guān)系,假設(shè)B組合了A,則每個B對象都有一個A對象。
- 繼承方式的復(fù)用常稱之為白箱復(fù)用,在繼承方式中,基類的內(nèi)部細(xì)節(jié)對子類可見,這一定程度上破壞了基類的封裝,伴隨著基類的改變,對派生類的改變很大。并且兩者依賴關(guān)系強(qiáng),耦合度大。
- 對象組合式繼承之外的復(fù)用選擇,對象組合要求被組合對象提供良好的接口定義。這種復(fù)用稱之為黑箱復(fù)用,對象的內(nèi)部實(shí)現(xiàn)細(xì)節(jié)是不可見的。耦合度低。
實(shí)際工程中能用繼承和組合就用組合,組合的耦合度低,代碼的維護(hù)性好,但是繼承在有些關(guān)系就適合用繼承就用繼承,并且要實(shí)現(xiàn)多態(tài)就一定要用繼承。
總結(jié)
繼承作為c++的一塊難點(diǎn),本篇博客不免有些錯誤,歡迎各位大佬指出批評!
到此這篇關(guān)于關(guān)于C++繼承你可能會忽視的點(diǎn)的文章就介紹到這了,更多相關(guān)C++繼承忽視的點(diǎn)內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
C++詳細(xì)講解互斥量與lock_guard類模板及死鎖
線程的主要優(yōu)勢在于,能夠通過全局變量來共享信息。不過,這種便捷的共享是有代價的:必須確保多個線程不會同時修改同一變量,或者某一線程不會讀取正由其他線程修改的變量。為了防止出現(xiàn)線程某甲試圖訪 問一共享變量時,線程某乙正在對其進(jìn)行修改。引入了互斥量2022-07-07C語言編程內(nèi)存分配通訊錄靜態(tài)實(shí)現(xiàn)示例代碼教程
這篇文章主要為大家介紹了C語言編程實(shí)現(xiàn)靜態(tài)的通訊錄示例代碼教程,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2021-10-10關(guān)于C++11的統(tǒng)一初始化語法示例詳解
C++之前的初始化語法很亂,有四種初始化方式,而且每種之前甚至不能相互轉(zhuǎn)換,但從C++11出現(xiàn)后就好了,所以這篇文章主要給大家介紹了關(guān)于C++11的統(tǒng)一初始化語法的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),需要的朋友可以參考下。2017-10-10C++實(shí)現(xiàn)地鐵自動售票系統(tǒng)程序設(shè)計
這篇文章主要為大家詳細(xì)介紹了C++實(shí)現(xiàn)地鐵自動售票系統(tǒng)程序設(shè)計,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-03-03C++ OpenCV實(shí)戰(zhàn)之圖像全景拼接
本文主要介紹了如何使用OpenCV C++ 進(jìn)行圖像全景拼接,文中的示例代碼講解詳細(xì),對我們學(xué)習(xí)OpenCV有一定的幫助,感興趣的可以了解一下2022-01-01Matlab實(shí)現(xiàn)二維散點(diǎn)主方向直方圖的繪制詳解
這篇文章主要為大家詳細(xì)介紹了如何利用Matlab實(shí)現(xiàn)二維散點(diǎn)主方向直方圖的繪制,文中的示例代碼講解詳細(xì),對我們學(xué)習(xí)Matlab有一定幫助,需要的可以參考一下2022-09-09C++?STL實(shí)現(xiàn)非變易查找算法的示例代碼
C++?STL?中的非變易算法(Non-modifying?Algorithms)是指那些不會修改容器內(nèi)容的算法,是C++提供的一組模板函數(shù),下面我們就來看看這一算法的應(yīng)用吧2023-08-08