所謂多態性就是不一樣對象收到相同的消息產生不一樣的動做。通俗的說,多態性是指一個名字定義不一樣的函數,這些函數執行不一樣但又相似的操做,即用一樣的接口訪問功能不一樣的函數,從而實現「一個接口,多種方法」。函數
多態性又分爲兩種:一種是編譯時的多態性,主要經過函數重載和運算符重載實現。一種是運行時的多態性,主要經過繼承和虛函數來實現的。spa
這一部分,咱們主要講函數重載和繼承與虛函數,運算符的重載我準備單獨作一部分來記載。指針
在同一範圍中聲明幾個功能相似的同名函數,可是這些同名函數的形參(參數的個數、類型或者順序)必須不一樣,這就是函數重載。函數重載一般被用來實現功能相似而所處理的數據類型不一樣的問題。code
1 // base class 2 class Base 3 { 4 public: 5 void print(int a, int b) { 6 printf("a = %d, b = %d\n", a, b); 7 } 8 9 void print(int c) { 10 printf("c = %d\n", c); 11 } 12 }; 13 // main 14 int _tmain(int argc, _TCHAR* argv[]) 15 { 16 Base base; 17 base.print(1); 18 base.print(2, 3); 19 20 system("pause"); 21 return 0; 22 }
輸出結果:對象
c = 1blog
a = 2, b = 3繼承
幾個注意點:接口
a) 函數的類型不在參數匹配檢查之列。若兩個函數除了返回類型不一樣,其餘的所有相同,這是非法的。如:原型
int Print(int a);編譯器
double Print(int a);
雖然這兩個函數的返回類型不一樣,可是因爲參數個數和參數類型徹底相同,編譯程序將沒法區分這兩個函數。由於在肯定調用哪個函數以前,返回類型是不知道的。
b) 函數的重載與帶默認值的函數一塊兒使用時,須要注意有可能會引發二義性。如:
void Print(int a = 0, int b = 0);
void Print(int a);
當咱們調用Print(1)時,程序沒法肯定調用的是哪個。
c) 函數調用時,注意輸入的實參要與形參的類型相符,不然容易形成編譯器自動作類型轉換是致使的錯誤。
網上看到不少博客和文章提到這一個概念,在有的書上面,也發現這一律念被稱做爲在基類和派生類中的函數重載。不過這裏爲了和函數重載區分,咱們仍是把它叫作隱藏。
咱們來根據兩個例子來講明:
一:派生類的函數與基類的函數同名,而且參數也相同,可是基類函數沒有virtual關鍵字(有關鍵字virtual的話就是虛函數了,這就是後面要說的重寫(覆蓋)的概念了)。此時,基類的函數被隱藏。
1 // 基類 2 class Base 3 { 4 public: 5 // 不能有關鍵字virtual 6 void print(int a) { 7 printf("Base::a = %d\n", a); 8 } 9 }; 10 // 派生類 11 class Derivel : 12 public Base 13 { 14 public: 15 // 隱藏 16 void print(int a) { 17 printf("Derivel::a = %d\n", a); 18 } 19 }; 20 // main 21 int _tmain(int argc, _TCHAR* argv[]) 22 { 23 Base base; 24 base.print(1); 25 26 Derivel derivel; 27 derivel.print(1); // 隱藏了基類的print函數,調用的是派生類中的print函數 28 29 system("pause"); 30 return 0; 31 }
輸出結果:
Base::a = 1
Derivel::a = 1
說明:
派生類Derivel公有繼承了基類Base,對於它的公有函數print。派生類的對象derivel是能夠直接訪問的。可是派生類中本身也定義了一個同名且同參數的print函數,咱們能夠看到對象derivel調用的print函數時派生類本身定義的print函數,而不是基類繼承下來的print函數,由此可知,基類的print函數被隱藏了。
二:派生類的函數與基類的函數同名,可是參數不一樣。此時,不論有無virtual關鍵字,基類的函數將被隱藏。
1 // 基類 2 class Base 3 { 4 public: 5 // 關鍵字virtual有無不影響 6 virtual void print(int a) { 7 printf("Base::a = %d\n", a); 8 } 9 }; 10 // 派生類 11 class Derivel : 12 public Base 13 { 14 public: 15 // 隱藏 16 void print(int a,int b) { 17 printf("Derivel::a = %d, b = %d\n", a, b); 18 } 19 }; 20 // main 21 int _tmain(int argc, _TCHAR* argv[]) 22 { 23 Base base; 24 base.print(1); 25 26 Derivel derivel; 27 // derivel.print(1); // 錯誤,基類的print已經被隱藏 28 derivel.print(1,2); // 正確 29 30 system("pause"); 31 return 0; 32 }
輸出結果:
Base::a = 1
Derivel::a = 1, b = 2
說明:這裏與上面的不一樣是派生類中定義了一個名字相同,參數不一樣的函數,這個函數一樣把繼承的print函數給隱藏了。同時,因爲參數不一樣,就不用考慮基類的print函數有沒有virtual關鍵字定義了。
延伸:
重載與隱藏的區別:
(1) 範圍不一樣,重載的函數是在一個類裏面,隱藏和被隱藏的函數是在基類和派生類兩個類裏面。
(2) 參數的區別,重載的函數之間參數確定是不同的,隱藏和被隱藏的函數參數能夠同樣也能夠不同。
上面說的函數重載和隱藏以及沒有說明的運算符重載都是編譯時的多態,也被稱爲靜態多態。下面要說的虛函數和重寫就是多態的另一種,運行時的多態,也被稱爲動態多態。
要了解重寫,咱們首先得了解虛函數的概念。
虛函數是函數重載的另外一種表現形式,這是一種動態的重載方式,它提供了一種更爲靈活的多態性機制。這裏也就離不開派生類的對象指針了,咱們來看一個例子。
1 // 基類 2 class Base 3 { 4 public: 5 void print(int a) { 6 printf("Base::a = %d\n", a); 7 } 8 }; 9 // 派生類 10 class Derivel : 11 public Base 12 { 13 public: 14 void print(int a) { 15 printf("Derivel::a = %d", a); 16 } 17 18 void print1() { 19 printf("Derivel::print1"); 20 } 21 }; 22 // main 23 int _tmain(int argc, _TCHAR* argv[]) 24 { 25 Base base; 26 Derivel derivel; 27 Base *b; 28 29 b = &base; 30 b->print(1); 31 b = &derivel; 32 b->print(1); 33 34 system("pause"); 35 return 0; 36 }
輸出結果:
Base::a = 1
Base::a = 1
對於對象指針,應該注意幾個問題:
(1). 聲明爲指向基類對象的指針能夠指向它的公有派生類對象,但不容許指向它的私有派生類對象和保護派生類對象。
(2). 容許將一個聲明爲指向基類對象的指針指向它的派生類對象,可是不能將一個指向派生類對象的指針指向它的基類對象。
(3). 聲明爲指向基類對象的指針,當其指向公有派生類對象時,只能用它來直接訪問派生類從基類繼承來的成員,而不能直接訪問公有派生類中本身定義的成員。如上面例子中若是你寫這句話:b->print1();,那它將報錯。正確的應該寫成((Derivel*)b)->print1();。
從輸出結果能夠看出,雖然b以及指向了派生類對象derivel,可是它所調用的print函數仍是基類對象的print函數。這就是咱們的問題,無論指針指向那個對象(基類對象或者派生類對象),它所調用的print函數一直都是基類的print函數。緣由是普通成員函數的調用是在編譯時靜態聯編的。在這種狀況下,若要調用派生類中的成員函數,必須採用顯示的調用方法,如:derivel.print(1),或者指針強制類型轉換((Derivel*)derivel)->print();,咱們使用對象指針的目的就是爲了表達一種動態的特性,即指針指向的對象不一樣時執行的操做不一樣。若是採用以上的兩種顯示調用方法也就起不到這種做用了。其實真正的解決方法就是把基類的print函數說明爲虛函數,這就是咱們爲何要引入虛函數的緣由了。
因此,若是咱們把上面那段代碼裏面基類中的void print(int a)函數前面加上一個virtual關鍵字,把它聲明爲虛函數,那麼輸出結果就是這樣:
Base::a = 1
Derivel::a = 1
在基類中的某個成員函數被聲明爲虛函數後,此虛函數就能夠在一個或者是多個派生類中被從新定義。在派生類中從新定義時,其函數原型,包括返回類型,函數名,參數個數,參數類型和參數順序都必須和基類徹底相同。
虛函數定義的幾點說明:
(1). 在基類中,用virtual關鍵字能夠將其public或者protected部分的成員函數聲明爲虛函數,建議在public部分聲明。
(2). 在派生類對基類聲明的虛函數進行從新定義的時候,關鍵字virtual是可寫可不寫的,可是不寫的時候有的狀況下容易引發混亂,因此建議在派生類從新定義的是時候也寫上virtual關鍵字。
(3). 虛函數在派生類中被從新定義時,其函數的原型與基類中的函數原型必須徹底相同。
(4). 一個虛函數不管被公有繼承多少次,他仍然保持他的虛函數特性。
(5). 虛函數必須是其所在類的成員函數,而不能是友元函數,也不能是靜態成員函數。
(6). 構造函數不能被聲明爲虛函數,可是析構函數能夠是虛函數。
經過一個例子,咱們來最後總結一下虛函數下的重寫(覆蓋)。
1 // 基類 2 class Base 3 { 4 public: 5 virtual void print(int a) { 6 printf("Base::a = %d\n", a); 7 } 8 }; 9 // 基類1 10 class Base1 11 { 12 public: 13 void print(int a) { 14 printf("Base1::a = %d\n", a); 15 } 16 }; 17 // 派生類 18 class Derivel : 19 public Base,public Base1 20 { 21 public: 22 void print(int a) { 23 printf("Derivel::a = %d\n", a); 24 } 25 }; 26 // main 27 int _tmain(int argc, _TCHAR* argv[]) 28 { 29 Derivel derivel; // 定義派生類的對象derivel 30 Base *b; // 定義基類base的指針b 31 Base1 *b1; // 定義基類base1的指針b1 32 33 b = &derivel; 34 b->print(1); // 基類Base中print是虛函數,派生類重寫了print函數 35 // 所以此處調用派生類的print函數 36 b1 = &derivel; 37 b1->print(1); // 基類Base1中print不是虛函數,b1是Base1的指針 38 // 所以此處調用基類Base1的print函數 39 40 system("pause"); 41 return 0; 42 }
輸出結果:
Derivel::a = 1
Base1::a = 1
延伸:
重寫和重載的區別:
1) .範圍不一樣,重載的函數是在一個類裏面,重寫的函數是在基類和派生類兩個類裏面。
2) .參數的區別,重載的函數之間參數確定是不同的,重寫的函數的函數類型(函數名,函數參數,參數個數,參數順序)必須與基類的函數徹底相同。
3) .virtual修飾的區別,重載函數virtual修飾無關緊要,重寫的基類函數必須有virtual修飾。
重寫和隱藏的區別:
1) .參數的區別,隱藏和被隱藏的的函數之間參數是能夠同樣也能夠不同,重寫函數的函數類型(函數名,函數參數,參數個數,參數順序)必須與基類函數徹底相同。
2) .virtual修飾的區別,當隱藏和被隱藏的函數之間參數同樣時,不能有virtual(有virtual修飾其實就是重寫了),參數不同是,virtual能夠可無。重寫的基類函數必須有virtual修飾。