基類
class Shape {public: Shape() { cout << "construct Shape" << endl; } ~Shape() { cout << "deconstruct Shape" << endl; } virtual double calArea() { cout << "Shape calArea" << endl; return 0; }};
子類
class Circle : public Shape {public: Circle() { cout << "construct Circle" << endl; } ~Circle() { cout << "deconstruct Circle" << endl; } //此處的virtual不是必須的,如果不加,系統會自動加上, //為了代碼的易讀性,推薦加上 virtual double calArea(){ cout << "Circle calArea" << endl; return 0; }};
類對象與類指標對比
類的對象: Circle circle;
類的指標: Circle* p_circle = new Circle();
類對象通過 “ . ”調用函數:
circle.calArea();
類指標通過“ -> ”調用函數:
p_circle->calArea();
類的對象用的是棧記憶體,是個局部的臨時變數。
類的指標:用的是堆記憶體,是個永久變數,除非你釋放它。
為什麼有了類的對象還要存在類的指標這種用法呢。 指標可以實現多態,類的對象不行 在函數調用,盡量傳引用或是指標參數。不管你的對象或結構參數多麼龐大,用指標,傳過去的是一個地址,就是4個位元組。如果用對象,參數傳遞佔用的資源就太大了。
什麼是多態?
相同對象收到不同訊息或不同對象收到相同訊息時產生的不同的動作。
類的對象調用的函數為其本身的成員函數:
// main1int main() { Circle c; c.calArea(); cout << endl; Shape s; s.calArea(); cout << enld;}
******* 執行結果 ***********
construct Shape
construct Circle
Circle calArea
construct Shape
Shape calArea
deconstruct Shape //析構Shape的對象s
deconstruct Circle
deconstruct Shape
注意,上述結果說明: 構造子函數前先調用父類的建構函式(析構過程相反,先調用子類的解構函式再調用父類的) 類的對象調用的函數為其本身的成員函數:
注意,基類Shape中的calArea( )函數是虛函數(virtual),如果不聲明為虛函數,執行以下代碼的結果:
// main2int main() { Circle c; c.calArea(); cout << endl; Shape* p = new Circle(); p->calArea(); delete p; //調用delete才析構,否則不析構 cout << endl;}
******* 執行結果 ***********
construct Shape
construct Circle
Circle calArea
construct Shape
construct Circle
Shape calArea // 指標p調用的是父類的函數
deconstruct Shape // 【1】 delete p時,指標p只調用了父類的解構函式,沒有析構子類,記憶體流失
deconstruct Circle
deconstruct Shape
父類Shape中的calArea函式宣告為virtual後,再執行main2 代碼,結果如下:
******* 執行結果 ***********
construct Shape
construct Circle
Circle calArea
construct Shape
construct Circle
Circle calArea // 指標p調用的是子類的函數
deconstruct Shape
deconstruct Circle
deconstruct Shape
為避免【1】中所說的記憶體流失問題,把父類的解構函式聲明為虛函數即可:
~virtual Shape(){ cout << "deconstruct Shape" << endl;}
這樣父類指標指向的是哪個對象,哪個對象的建構函式就會先執行,然後執行父類的建構函式。銷毀的時候子類的解構函式也會執行。
為什麼virtual可以實現多態
靜態繫結:編譯時間綁定,通過對象調用。程式執行之前,編譯器就已經解析出該調用哪個函數。
動態綁定:運行時綁定,通過地址實現(虛函數表)。程式在啟動並執行時候才解析該調用哪個函數。
虛函數可以實現動態綁定,要觸發動態綁定,需滿足兩個條件: 只有虛函數才能進行動態綁定,非虛函數不進行動態綁定。 必須通過基類類型的引用或指標進行函數調用。
virtual在函數中的使用限制 普通函數不能是虛函數,也就是說這個函數必須是某個類的成員函數,不可以是一個全域函數,否則會導致編譯錯誤。 靜態成員函數不能是虛函數 static成員函數是和類同生共處的,他不屬於任何對象,使用virtual也將導致錯誤。 內嵌函式不能是虛函數 如果修飾內嵌函式 如果內嵌函式被virtual修飾,電腦會忽略inline使它變成存粹的虛函數。 建構函式不能是虛函數,否則會出現編譯錯誤。
多態的實現原理
虛函數表指標:類中除了定義的函數成員,還有一個成員是虛函數表指標(佔四個基本記憶體單元),這個指標指向一個虛函數表的起始位置,這個表會與類的定義同時出現,這個表存放著該類的虛函數指標,調用的時候可以找到該類的虛函數表指標,通過虛函數表指標找到虛函數表,通過虛函數表的位移找到函數的入口地址,從而找到要使用的虛函數。
當執行個體化一個該類的子類對象的時候,(如果)該類的子類並沒有定義虛函數,但是卻從父類中繼承了虛函數,所以在執行個體化該類子類對象的時候也會產生一個虛函數表,這個虛函數表是子類的虛函數表,但是記錄的子類的虛函數地址卻是與父類的是一樣的。所以通過子類對象的虛函數表指標找到自己的虛函數表,在自己的虛函數表找到的要執行的函數指標也是父類的相應函數入口的地址。
如果我們在子類中定義了從父類繼承來的虛函數,對於父類來說情況是不變的,對於子類來說它的虛函數表與之前的虛函數表是一樣的,但是此時子類定義了自己的(從父類那繼承來的)相應函數,所以它的虛函數表當中管於這個函數的指標就會覆蓋掉原有的指向父類函數的指標的值,換句話說就是指向了自己定義的相應函數,這樣如果用父類的指標,指向子類的對象,就會通過子類對象當中的虛函數表指標找到子類的虛函數表,從而通過子類的虛函數表找到子類的相應虛函數地址,而此時的地址已經是該函數自己定義的虛函數入口地址,而不是父類的相應虛函數入口地址,所以執行的將會是子類當中的虛函數。這就是多態的原理。
函數的覆蓋和隱藏
父類和子類出現同名函數稱為隱藏。 父類對象.函數函數名(…); //調用父類的函數 子類對象.函數名(…); //調用子類的函數 子類對象.父類名::函數名(…); //子類調用從父類繼承來的函數。
父類和子類出現同名虛函數稱為覆蓋
- 父類指標=new 子類名(…); \quad父類指標->函數名(…);//調用子類的虛函數
純虛函數
純虛函數定義:
\quad純虛函數沒有函數體,同時在定義的時候函數名後面要加“=0”。
class Shape{public: virtual double calcArea()//虛函數 {} virtual double calcPerimeter()=0;//純虛函數};
含有純虛函數的類成為抽象類別抽象類別不能被執行個體化,繼承該抽象類別的子類也可以是抽象類別,抽象類別子類只有把抽象類別當中的所有的純虛函數都做了實現才可以執行個體化對象。
如果在抽象類別當中僅含有純虛函數而不含其他任何東西,我們稱之為介面類。 沒有任何資料成員 僅有成員函數 成員函數都是純虛函數