最近兩次面試都問到了多態,我也不得不重視起來了,最近最大的收穫就是:基礎知識很重要,就算你很會寫代碼,但是面試官問你基礎知識答不上來的話,也很難被人賞識和錄用,所以還是要多補補基礎概念,這一篇就說多態。
之前第一篇提到過,多態是指同樣的訊息被不同的對象接受時導致不同的行為。分四類:重載多態,強制多態,包含多態和多重參數變形。
多態從實現的角度分為 編譯時間多態 運行時多態。不同處就是確定操作針對的具體對象的時間是編譯的時候還是啟動並執行時候。
重載多態:
我們知道的普通函數及類的成員函數的重載都屬於重載多態。
函數的重載因為很常用相信一般都很熟悉了,就是指函數名相同而形參的個數和類型不同,編譯器調用的時候根據參數類型個數來判斷調用哪一個函數。
運算子的重載也是重載多態,比如我們定義了一個類Counter,希望這個類內部可以實現'+'的運算,比如Counter a,b,c; 初始化後可以有c=a+b;或類似的運算,那麼我們需要對‘+’進行重載,給它以運算規則,來保證這句話能通過編譯。
(有5個運算子不可以被重載,分別是: . .* :: sizeof ?:)
運算子多載分兩種:作為成員函數重載 作為友元函數重載。
文法形式為:
| 作為成員函數重載運算子: |
作為友元函數重載運算子: |
函數類型 operator 運算子(形參表){ ............; } |
friend 函數類型 operator 運算子(形參表){ ............; } |
之前也說過,友元函數其實是定義在函數外的,所以跟函數成員的區別就是友元函數需要兩個形參作為運算子前後的值。
比如我要重載上面的Counter 類型的+ - 運算:
Counter operator +(Counter c2){
return Counter(this.number+c2.number);
}
//或者友元重載減號:
friend Counter operator -(Counter c1, Counter c2){
return Counter(c1.number-c2.number);
}
//調用時:
Counter a(5),b(3),c,d;
c=a+b;
d=a-b;
//都是可以通過的。
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
除了這種加減等常用的外,還有比如-- ++這種前置或後置運算子,比如我們要定義一個counter++; 那麼運算子多載為Counter的成員函數,同時函數要帶有一個整數形參(int),它的作用就是區別說明它是後置的++ --。如果是前置,則沒有int形參。如下:
//前置++的重載
Counter Counter::operator ++(){
.........;
}
//後置++的重載
Counter Counter::operator ++(int){
...........;
}
//這樣,當出現++符號時,系統就可以根據位置知道調用哪個函數了:
Counter a(5);
a++; // 相當於調用了 operator ++(0);
++a; // 相當於調用了 operator ++();
強制多態:
就是指講一個變元的類型加以變化,符合一個函數或者操作的具體要求。
比如加法符號,在進行整型和浮點型的運算時,會先強制將整型轉為浮點型,然後再進行運算。
包含多態:
包含多態是研究類族中定義於不同類中的同名函數成員的多態行為,主要通過虛函數來實現.
虛函數必須是非靜態成員,經過多次派生之後,族類中可以實現運行過程中的多態。
| 一般虛函數成員聲明文法: |
virtual 函數類型 函數名(形參表){ .......................; } |
虛函數的聲明只能在類定義中的函數原型聲明時就寫清楚,而不能在寫函數體時才聲明。運行過程中要滿足三個規則:
1. 類之間要滿足類型相容規則。
2. 聲明時虛函數。
3*. 要由成員函數來調用,或者是通過指標,引用來訪問虛函數。
理論瞭解了之後,我們寫個小例子就懂了。
以下是完整代碼:
虛函數的使用
#include <iostream>
using namespace std;
class Father{
public:
virtual void outputTest(){
cout<<"Father"<<endl;
}
};
class Son1:public Father{
public:
void outputTest(){
cout<<"Son1"<<endl;
}
};
class Son2:public Son1{
public:
void outputTest(){
cout<<"Son2"<<endl;
}
};
// 調用時
int main(){
Father f,*p;
Son1 s1;
Son2 s2;
p=&f;
p->outputTest();
p=&s1;
p->outputTest();
p=&s2;
p->outputTest();
}
話不多說,我們為證:
可見,通過使用virtual這個關鍵詞,我們實現了outputTest這個函數的包含多態。這三個類中,outputTest都是虛函數。
添一句話:如果子類中的一個函數fun,跟父類中的一個虛函數fun,函數名相同,參數表完全一致,傳回值也相同,那麼這函數就自動被判定為了虛函數。
虛解構函式:它的存在時為了防止某些情況下的空間泄露。所以虛建構函式是不允許存在的。同時,如果一個父類的解構函式是虛函數,那麼子類的析構也同樣是虛函數。
下面就說說空間泄露的情況:假如我定義了一個父類Father,裡面沒有資料成員,然後我定義了一個子類Son公有繼承父類,並且有一個私人資料成員*int t; Son的建構函式裡有 t=new int(0);,解構函式裡會delete t;,但是父類自然沒有這一句。這樣我們在定義對象的時候如果有這樣的情況: Father *f=new Son(); delete *f; 那麼,啊~非常不意外的,可憐的t就被這樣無情的丟在了角落,不使用也不釋放。這樣如果在一個較大成程式裡發生的話,很可能會出現記憶體不足的情況。
解決的辦法,就是在Father的解構函式前面加virtual,具體形式為: virtual ~Father(); 因為很簡單,就再給一次代碼好了,三分鐘搞定:
虛解構函式代碼
#include <iostream>
using namespace std;
class Father{
public:
Father(){};
virtual ~Father(){cout<<"Father delete;"<<endl;};
};
class Son:public Father{
public:
Son(){t=new int(0);};
~Son(){delete t; cout<<"Son delete;"<<endl;};
private:
int *t;
};
// 調用時
int main(){
Father *f=new Son();
delete f;
}
運行結果:
可見,子類的解構函式被調用了,這樣就避免了記憶體泄露帶來的危害。所以虛析構還是很有用的~!
除了上述兩種虛函數之外,我們還應該知道一種類 叫做:抽象類別。
抽象類別處於類的上一層,它本身無法被執行個體化,只能通過繼承機制,由抽象類別產生非抽象的衍生類別,再對之執行個體化。
注意:帶有純虛函數的類就是抽象類別。但是什麼是純虛函數呢?
純虛函數就是在一個基類中聲明的虛函數,它在這個基類裡面沒有具體要操作的內容,它存在的意義就是為了讓子類根據自己的需求對這個函數實現不同的介面。
純虛類的定義文法是: virtual 函數傳回型別 函數名(參數表)=0; 其實就是有了一個‘=0’。這樣就不需要給它定義函數體了,所以純虛函數是沒有函數體的。
多重參數變形:
多重參數變形,就是將程式所處理對象的型別參數化,使得一段程式可以用於處理多種不同類型的對象。
參數的多態,可以通過函數模板或類模板實現。
所謂函數模板,我們可以想一個例子:我們通常使用math.h裡的很多函數比如絕對值函數abs()的時候,參數可以有int, double等不用類型的參數,那麼如果為了每一種類型建立一個重載函數的話,就太麻煩了,因為函數體本身是相同的,所以我們就用一個模板來代替之,如下:
template <typename T>
T abs(T x){
return x<0?-x:x;
}
這樣,我們在調用的時候,如果給的參數是int,那麼編譯器就會把typename裡的T變為int,這個函數就是個返回int,參數為int的函數了,double等類型也一樣。
函數模板的文法定義: |
template <typename T> 或 template <class T> //表示T是一個類型名或者類名,可以更換為int 、 double、 某個類 等等 傳回型別 函數名(參數表){ .........; }(注意傳回型別不一定為T的~。) |
類模板:使用類模板可以為類聲明一種模式,是的類中某些資料成員、某些成員函數的參數、某些成員函數的傳回值能取任意類型。
| 類模板的文法定義: |
在類的外部定義成員函數的函數體時: |
template <模板參數表> class 類名{ ...........; } |
template <模板參數表> 傳回型別 類名<模板參數表成員>::函數名(參數表){ ........; } |
同樣,我們用一個小例子,一秒搞懂類模板:
#include <iostream>
using namespace std;
template<class Input, class Output>
class Test{
public:
Test(Input in){input=in;}
Output fun();
private:
Input input;
Output output;
};
// 類之外定義函數時的格式:
template<class Input, class Output>
Output Test<Input,Output>::fun(){
return input>0?input:-input;
}
int main(){
Test<int,int> test(-30);
cout<<test.fun()<<endl;
}
查看輸出:
成功~! 呵呵 雖然沒有含金量,但是該用到的格式這個小程式都用到了,不錯吧~