繼承
1、私有繼承:基類的公有成員和保護成員都作為派生類的私有成員,並且不能被這個派生類的子類所訪問。
公有繼承:基類的公有成員和保護成員作為派生類的成員時,它們都保持原有的訪問權限,而基類的私有成員在派生類中是不可見的。
在公有繼承時,派生類的成員函數可以訪問基類中的公有成員和保護成員;派生類的對象僅可以訪問基類中的公有成員。
保護繼承:基類的所有公有成員和保護成員都成為派生類的保護成員,並且只能被它的派生類成員函數訪問,不能被它派生類的對象訪問。
2、注意:
1》基類的private成員在派生類中是不能被訪問的,如果基類成員不想在類外被直接訪問,但需要在派生類中訪問,就需要定義成protected。可以看出保護成員限定符是因繼承才出現的。
2》public繼承是一個接口繼承,保持is-a原則,每個父類可用的成員對子類也可以用,因為每個子類對象也都是一個父類對象。
3》procted/private繼承是一個實現繼承,基類的部分成員並非完全成為子類接口的一部分,是has-a的關系原則,所以非特殊情況下不會使用這兩種繼承關系,在絕大部分的場景下使用的都是公有繼承。
4》不管是哪種繼承方式,在派生類內部都可以訪問基類的公有成員和保護成員,基類的私有成員存在但是在子類中不可見(不可以訪問)
5》使用class關鍵字是默認的繼承方式是private,使用struct時默認的繼承方式是public,不過最好顯示的寫出繼承方式。
6》在實際應用中一般使用的都是public繼承,極少場景下才會使用protected/private繼承。
3、派生類的默認成員函數:
在繼承關系裡面,在派生類中如果沒有顯示的定義這六個成員函數,編譯系統則會默認合成這六個默認的成員函數。
類的六個默認成員函數:
1》構造函數
2》拷貝構造函數
3》析構函數
4》賦值操作重載
5》取地址操作符重載
6》const修飾的取地址操作符重載
4、繼承體系中的作用域:
1》在繼承體系中基類和派生類是兩個不同的作用域
2》子類和父類中有同名成員,子類成員將屏蔽父類對成員的直接訪問(在子類成員函數中,可以使用 基類::基類成員 訪問)
——隱藏:子類可以隱藏繼承的成員變量。對於子類可以從父類繼承成員變量,只要子類中定義的成員變量和父類中的成員變量相同時,子類就隱藏了繼承的成員變量,即子類對象以及子類自己聲明定義的方法操作的變量是子類重新定義的成員變量。 子類也可以隱藏已經繼承的方法,子類通過重寫來隱藏繼承的方法。
方法重寫是指:子類中定義一個方法,並且這個方法的名字、返回值類型、參數個數與父類繼承的方法完全相同。子類通過方法的重寫可以把父類的狀態和行為改變為自身的狀態和行為。
如果子類想使用被隱藏的父類方法,必須使用super關鍵字。
——重定義
3》注意在實際繼承體系裡面最好不要定義同名成員。
5、派生類的構造函數:
1》派生類的數據成員包含了基類中說明的數據成員和派生類中說明的數據成員。
2》構造函數不能被繼承,因此,派生類的構造函數必須通過調用基類的構造函數來初始化基類的數據成員。
3》如果派生類中還有子對象時,還應包含對子對象初始化的構造函數。
6、派生類構造函數的調用順序:
1》基類的構造函數(按照繼承列表中的順序調用)
2》派生類中對象的構造函數(按照在派生類中成員對象聲明順序調用)
3》派生類構造函數
注意:
1》基類沒有缺省構造函數,派生類必須要在初始化列表中顯示給出基類名和參數列表。
2》基類沒有定義構造函數,則派生類也可以不用定義,全部使用缺省構造函數。
3》基類定義了帶有形參列表的構造函數,派生類就一定要定義構造函數。
補充:缺省構造函數又叫默認構造函數(default constructor)。當聲明對象時,編譯器會調用一個構造函數。若聲明的類中沒有聲明構造函數,編譯器就會自動調用一個缺省構造函數,該函數相當於一個不接受任何參數,不進行任何操作的構造函數。而當類中已經有聲明的構造函數時,編譯器就不會調用缺省構造函數。
7、派生類的析構函數:由於析構函數也不能被繼承,因此在執行派生類的析構函數時,基類的析構函數也將被調用。
執行順序是:
1》先執行派生類的析構函數,
2》派生類包含成員對象的析構函數(調用順序和成員對象在類中的聲明順序相反)
3》基類析析構函數(調用順序和基類在派生列表中聲明的順序相反)
8、構造函數的功能是在創建對象時,用給定的值對對象進行初始化。
沒有參數的構造函數稱為默認構造函數(缺省構造函數)。默認構造函數有兩種:一種是系統自動提供的;另一種是程序員定義的。
使用系統提供的默認構造函數給創建的對象初始化時,外部類對象和靜態類對象的所有數據成員為默認值,自動對象的所有數據成員為無意義值。
9、構造函數分兩類:一類是帶參數的構造函數,可以是一個參數,也可以是多個參數;另一類是默認構造函數,即不帶參數的構造函數。
11、子類型:用來描述類型之間的一般和特殊的關系。當有一個已知類型S,它至少另一個類型T的行為,它還可以包含自己的行為,這時,則稱類型S是類型T的子類型。子類型的概念涉及行為共享,即代碼重用問題,它與繼承有著密切的關系。在繼承中,公有繼承可以實現子類型。
子類型的重要性就在於減輕編寫代碼的負擔,提高了代碼重用率。
因此一個函數可以用於某類型的對象,則它也可以用於該類型的各個子類型的對象,這樣就不用為處理這些子類型的對象去重載該函數。
12、類型適應:子類型與類型適應是一致的,A類型是B類型的子類型,那麼B類型必將適應於A類型。
13、賦值兼容規則:通常在公有繼承方式下,派生類是基類的子類型,這時派生類對象與基類對象之間的一些關系規則稱為賦值兼容規則.
在公有繼承方式下,兼容規則規定:
1》子類對象可以賦值給父類對象
2》父類對象不能賦值給子類對象
3》父類的指針/引用可以指向子類對象
4》子類的指針/引用不能指向父類對象(可以通過強制類型轉換完成)
使用上述規則,必須注意兩點:
1》具有派生類公有繼承類的條件。
2》上述三條規定是不可逆的。
——賦值兼容規則:
1》為什麼派生類可以給基類賦值?
——派生類訪問空間大於基類
3》父類指針/引用可以指向子類對象(多態實現)
14、單繼承、多繼承、菱形繼承:
單繼承:一個子類只有一個直接父類時,稱這個繼承關系為單繼承
多繼承:一個子類有兩個或者兩個以上直接父類時,稱這個繼承關系為多繼承
菱形繼承:菱形繼承存在二義性和數據冗余的問題
——虛繼承解決了菱形繼承的二義性和數據冗余的問題。
1》虛繼承解決了在菱形繼承體系裡面子類對象包含多分父類對象的數據冗余和浪費空間的問題。
2》虛繼承體系看起來很復雜,在實際應用中我們通常不會定義如此復雜的繼承體系,一般不到萬不得已都不要定義菱形繼承的體系結構,英文使用虛繼承解決數據冗余問題也帶來了性能上的損耗。
15、什麼情況下編譯器會合成一種缺省構造函數?《深入理解C++對象模型》
——1》類中有類類型成員對象,該成員對象它有自己的缺省構造函數,這時編譯器也會在該類中合成一個缺省的構造函數(不一定是繼承和派生的關系)
2》繼承層次,基類中有缺省構造函數,而派生類中沒有構造函數,這時編譯器就會在派生類中合成一個缺省的構造函數
3》虛擬繼承時,在派生類中會合成缺省構造函數
4》基類含有純虛函數時,在派生類中會合成缺省構造函數。
——默認構造函數是編譯器默認合成的
——缺省構造函數是裡面帶缺省值的
16、友元與繼承:
友元關系不能繼承,也就是說基類友元不能訪問子類私有和保護成員
注意:
友元可以是一個函數,該函數被稱為友元函數;友元也可以是一個類,該類被稱為友元類。
在C++中,自定義函數可以充當友元,友元只是能訪問指定類的私有和保護成員的自定義函數,不是指定類的成員,自然不能被繼承。
使用友元時要注意:
1》友元關系不能被繼承
2》友元關系是單向的,不具有交換性
3》友元關系不具有傳遞性
注意事項:
1》友元可以訪問類的私有成員
2》友元只能出現在類定義的內部,友元聲明可以出現在任何地方,一般放在類定義的開始或者結尾。
3》友元可以是普通的非成員函數,或者前面定義的其它類的成員函數,或者整個類。
4》類必須將重載函數集中每一個希望設置為友元的函數都聲明為友元。
5》友元關系不能被繼承,基類的友元對派生類的成員沒有特殊的訪問權限。如果基類被授予友元關系,則只有幾類具有特殊的訪問權限。該基類的派生類不能訪問授予友元關系的類。
17、繼承與靜態成員:
基類定義了一個static成員,則整個繼承體系裡面只有一個這樣的成員,無論派生多少個子類,都只有一個static成員實例
多態
1、對象的類型:
靜態多態:編譯器在編譯期間完成的,編譯器根據函數實參的類型(可能會進行隱式類型轉換),可推斷出要調用哪一個函數,如果有對應的函數就調用該函數,否則編譯錯誤。
動態多態:(動態綁定)在程序執行期間(非編譯期)判斷所引用對象的實際類型,根據其實際類型調用相應的方法。
class Base { }; class Derive :public Base { }; int main() { int a; Base b; Derive d; Base *pb = &b;//pb 基類指針類型(靜態類型--編譯器在編譯過程中確定的類型);;動態類型(它所指向的類型)---Base* pb = &d; system( "pause"); return 0; } Base *pb=&b; pb=&d;
——這裡pb的類型發生了變化,也就是它的動態類型,它的動態類型為Derive *
2、多態:函數重載也是一種多態。意思是具有多種形式或形態的情形
靜態類型的多態:在編譯期間就確定的關系(早綁定)
動態類型的多態:在執行期間判斷所引用對象啊的實際類型,根據其實際類型決定調用的相應方法(晚綁定)(通過虛函數的機制實現)
——1》使用virtual實現,
2》通過基類類型的引用或指針的調用。。在運行過程中找指針的指向,先定義一個基類的指針,在指向要調用的派生類
3》在基類中一定要加vietual,,派生類中可以不加;
4》在派生類中需要重新實現這個基類中方法。
3、
class Base { public: virtual void FunTest()//虛函數 {
cout << "Base::FunTest()" << endl; } }; class Derive :public Base//在派生類中包含FunTest(),除了基類中的構造函數和析構函數其余的均會被繼承 { public: virtual void FunTest()//虛函數 { cout << "Derive::FunTest()" << endl; } }; int main() { Derive d; d.FunTest(); Base b; b.FunTest(); Base* pb = &b; pb->FunTest(); //這裡調用的是b的FunTest pb = &d; pb->FunTest(); //這裡調用的是d的FunTest system( "pause"); return 0; }
virtual:這個關鍵字可以實現多態。
需要注意:1》在基類中的虛函數前一定要加virtual關鍵字。在派生類中重寫該函數時,可加可不加virtual關鍵字。
2》調用時,要用基類的指針/引用指向派生類的對象
動態綁定條件:1》必須是虛函數 2》通過基類類型的引用或者指針調用
4、繼承體系中同名成員函數關系:
1》重載:
1》在同一個作用域;
2》函數名相同、參數不同
3》返回值可以不同
2》重寫(覆蓋):
1》不在同一作用域(分別在基類和派生類)
2》函數名相同、參數相同、返回值相同(協変例外)
3》基類函數必須有virtual關鍵字
4》訪問修飾符可以不同
3》重定義(隱藏):
1》在不同作用域中(分別在基類和派生類)
2》函數名相同
3》在基類和派生類中只要不構成重寫就是重定義
5、構造函數不能定義成virtual
---1》構造函數是用來構造對象,virtual需要通過對象調用,,而在構造函數中的virtual,還沒有出構造函數,此時並沒有成功構造對象,所以不行。
2》當一個構造函數被調用時,它要做的首要事情之一是初始化它的VPTR。因此,它只能知道它是當前類的,而完全忽視這個對象後面是否還有繼承者。當編譯器為這個構造函數產生代碼時,它是為這個類的構造函數產生代碼——既不是為基類,也不是為它的派生類(因為類不知道誰繼承他)。所以它使用的VPTR必須是對於這個類的VTABLE。而且,VPTR的狀態是由最後調用的構造函數確定的。
但是,當這一系列構造函數正在發生時,每個構造函數都已經設置VPTR指向他自己的VTABLE。如果函數調用使用虛機制,它將只產生通過他自己的VTABLE的調用,而不是最後的VTABLE(所有構造函數被調用之後才會有最後的VTABLE)
6、拷貝構造不能定義成virtual,
7、賦值運算符重載可以定義成virtual一般情況下不建議這樣做。
8、靜態成員函數、友元函數不可以定義成虛函數----因為它們兩個沒有this指針,虛函數底層是用this指針實現的
——類的普通成員函數(包括虛函數)在編譯時加入this指針,通過這種方式可以與對象捆綁,而靜態函數編譯時不加this指針,因為靜態函數是給所有的類對象公用的,因此在編譯時沒有加this指針,所以無法與對象捆綁,而虛函數是靠著與對象捆綁加上虛函數列表才實現了動態捆綁,所以沒有this指針虛函數無從談起。
——因為在一個類中聲明友元時,該友元不是自己的成員函數,自然不能把它聲明為虛函數。
但是友元本身可以是虛函數。這個類將她聲明為自己的友元時,只是讓它可以存取自己的私有變量。
9、純虛函數
在成員函數的形參後面寫上=0,則成員函數為虛函數,包含虛函數的類叫做抽象類(也叫接口類)抽象類不能實例化出對象,
純虛函數在派生類中重新定義以後,派生類才能實例化出對象。
class Test { virtual void FunTest() = 0;//沒有函數體,只是一個接口,必須在派生類中重新實現 }; int main() { Test t;//不允許使用抽象類類型的對象,會報錯 system( "pause"); return 0; } 10、虛表 class Base { public: virtual void FunTest(){}//有一個虛指針 virtual void FunTest1(){} virtual void FunTest2(){} virtual void FunTest3(){} virtual void FunTest4(){} }; int main() { Base base; base.FunTest(); base.FunTest1(); base.FunTest2(); base.FunTest3(); base.FunTest4(); system( "pause"); return 0; } class Drive :public Base { public: virtual void FunTest1() { ; } virtual void FunTest2() { ; } }; int main() { Drive d;//d裡面包含一個base,而base裡面同上,存的是一個虛指針,指向一個虛表,只不過在派生類中實現了的函數,它的函數地址會發生改變 d.FunTest(); d.FunTest1(); d.FunTest2(); d.FunTest3(); d.FunTest4();//在派生類中實現了的虛函數,就調用派生類中的,沒有在派生類中實現的就調用基類中的 system( "pause"); return 0; } int main() { Base base; Drive d; Base *pa = &base;//pa裡面會有(指向)一個虛指針,指向虛表,再來調動虛函數 pa->FunTest(); pa->FunTest1(); pa->FunTest2(); pa->FunTest3(); pa->FunTest4(); pa = (Base*)&d;//d中會有一個Base,Base裡面會包含一個虛指針,指向另一個虛表;;;這裡的類型轉換是不起作用的,並不會指向上一個虛表 pa->FunTest(); pa->FunTest1(); pa->FunTest2(); pa->FunTest3(); pa->FunTest4(); system( "pause"); return 0; }