C++中,實現多態有以下方法:虛函數,抽象類,重載,覆蓋
多態性在Object Pascal和C++中都是通過虛函數(Virtual Function) 實現的。
這麼一大堆名詞,實際上就圍繞一件事展開,就是多態,其他三個名詞都是爲實現C++的多態機制而提出的一些規則,下面分兩部分介紹,第一部分介紹【多態】,第二部分介紹【虛函數,純虛函數,抽象類】
一 【多態】
多態的概念 :關於多態,好幾種說法,好的壞的都有,分別說一下:
1 指同一個函數的多種形態。
個人認爲這是一種高手中的高手喜歡的說法,對於一般開發人員是一種差的不能再差的概念,簡直是對人的誤導,然人很容易就靠到函數重載上了。
以下是個人認爲解釋的比較好的兩種說法,意思大體相同:
2多態是具有表現多種形態的能力的特徵,在OO中是指,語言具有根據對象的類型以不同方式處理之,特別是重載方法和繼承類這種形式的能力。
這種說法有點繞,仔細想想,這纔是C++要告訴我們的。
3多態性是允許你將父對象設置成爲和一個或更多的他的子對象相等的技術,賦值之後,父對象就可以根據當前賦值給它的子對象的特性以不同的方式運作。簡單的說,就是一句話:允許將子類類型的指針賦值給父類類型的指針。多態性在Object Pascal和C++中都是通過虛函數(Virtual Function) 實現的。
這種說法看來是又易懂,又全面的一種,尤其是最後一句,直接點出了虛函數與多態性的關係,如果你還是不太懂,沒關係,再把3讀兩遍,有個印象,往後看吧。
二 【虛函數,純虛函數,抽象類】
多態才說了個概念,有什麼用還沒說就進入第二部分了?看看概念3的最後一句,虛函數就是爲多態而生的,多態的作用的介紹和虛函數簡直關係太大了,就放一起說吧。
多態的作用:繼承是子類使用父類的方法,而多態則是父類使用子類的方法。這是一句大白話,多態從用法上就是要用父類(確切的說是父類的對象名)去調用子類的方法,例如:
【例一】
class A {
public:
A() {}
(virtual) void print() {
cout << "This is A." << endl;
}
};
class B : public A {
public:
B() {}
void print() {
cout << "This is B." << endl;
}
};
int main(int argc, char* argv[]) {
B b;
A a; a = b;a.print;---------------------------------------- make1
// A &a = b; a->print();----------------------------------make2
//A *a = new B();a->print();--------------------------------make3
return 0;
}
這將顯示:
This is B.
如果把virtual去掉,將顯示:
This is A.
(make1,2,3分別是對應兼容規則(後面介紹)的三種方式,調用結果是一樣的)
加上virtual ,多態了,B中的print被調用了,也就是可以實現父類使用子類的方法。
對多態的作用有一個初步的認識了之後,再提出更官方,也是更準確的對多態作用的描述:
多態性使得能夠利用同一類(基類)類型的指針來引用不同類的對象,以及根據所引用對象的不同,以不同的方式執行相同的操作。把不同的子類對象都當作父類來看,可以屏蔽不同子類對象之間的差異,寫出通用的代碼,做出通用的編程,以適應需求的不斷變化。賦值之後,父對象就可以根據當前賦值給它的子對象的特性以不同的方式運作(也就是可以調用子對象中對父對象的相關函數的改進方法)。
那麼上面例子中爲什麼去掉virtual就調用的不是B中的方法了呢,明明把B的對象賦給指針a了啊,是因爲C++定義了一組對象賦值的兼容規則,就是指在公有派生的情況下,對於某些場合,一個派生類的對象可以作爲基類對象來使用,具體來說,就是下面三種情形:
Class A ;
class B:public A
1. 派生的對象可以賦給基類的對象
A a;
B b;
a = b;
2. 派生的對象可以初始化基類的引用
B b;
A &a = b;
3. 派生的對象的地址可以賦給指向基類的指針
B b;
A *a = &b;
或
A *a = new B();
由上述對象賦值兼容規則可知,一個基類的對象可兼容派生類的對象,一個基類的指針可指向派生類的對象,一個基類的引用可引用派生類的對象,於是對於通過基類的對象指針(或引用)對成員函數的調用,編譯時無法確定對象的類,而只是在運行時才能確定並由此確定調用哪個類中的成員函數。
看看剛纔的例子,根據兼容規則,B的對象根本就被當成了A的對象來使用,難怪B的方法不能被調用。
【例二】
#include <iostream>
using namespace std;
class A
{
public:
void (virtual) print(){cout << "A print"<<endl;}
private:
};
class B : public A
{
public:
void print(){cout << "B print"<<endl;}
private:
};
void test(A &tmpClass)
{
tmpClass.print();
}
int main(void)
{
B b;
test(b);
getchar();
return 0;
}
這將顯示:
B print
如果把virtual去掉,將顯示:
A print
那麼,爲什麼加了一個virtual以後就達到調用的目的了呢,多態了嘛~那麼爲什麼加上virtual就多態了呢,我們還要介紹一個概念:聯編
函數的聯編:在編譯或運行將函數調用與相應的函數體連接在一起的過程。
1 先期聯編或靜態聯編:在編譯時就能進行函數聯編稱爲先期聯編或靜態聯編。
2 遲後聯編或動態聯編:在運行時才能進行的聯編稱爲遲後聯編或動態聯編。
那麼聯編與虛函數有什麼關係呢,當然,造成上面例子中的矛盾的原因就是代碼的聯編過程採用了先期聯編,使得編譯時系統無法確定究竟應該調用基類中的函數還是應該調用派生類中的函數,要是能夠採用上面說的遲後聯編就好了,可以在運行時再判斷到底是哪個對象,所以,virtual關鍵字的作用就是提示編譯器進行遲後聯編,告訴連接過程:“我是個虛的,先不要連接我,等運行時再說吧”。
那麼爲什麼連接的時候就知道到底是哪個對象了呢,這就引出虛函數的原理了:當編譯器遇到virtual後,會爲所在的類構造一個表和一個指針,那個表叫做vtbl,每個類都有自己的vtbl,vtbl的作用就是保存自己類中虛函數的地址,我們可以把vtbl形象地看成一個數組,這個數組的每個元素存放的就是虛函數的地址.指針叫做vptr,指向那個表。而這個指針保存在相應的對象當中,也就是說只有創建了對象以後才能找到相應虛函數的地址。
【注意】
1爲確保運行時的多態定義的基類與派生類的虛函數不僅函數名要相同,其返回值及參數都必須相同,否則即使加上了virtual,系統也不進行遲後聯編。
2 虛函數關係通過繼承關係自動傳遞給基類中同名的函數,也就是上例中如果A中print有virtual,那麼 B中的print即使不加virtual,也被自動認爲是虛函數。
*3 沒有繼承關係,多態機制沒有意義,繼承必須是公有繼承。
*4現實中,遠不只我舉的這兩個例子,但是大的原則都是我前面說到的“如果發現一個函數需要在派生類裏有不同的表現,那麼它就應該是虛的”。這句話也可以反過來說:“如果你發現基類提供了虛函數,那麼你最好override它”。
純虛函數:
虛函數的作用是爲了實現對基類與派生類中的虛函數成員的遲後聯編,而純虛函數是表明不具體實現的虛函數成員,即純虛函數無實現代碼。其作用僅僅是爲其派生類提過一個統一的構架,具體實現在派生類中給出。
一個函數聲明爲純虛後,純虛函數的意思是:我是一個抽象類!不要把我實例化!純虛函數用來規範派生類的行爲,實際上就是所謂的“接口”。它告訴使用者,我的派生類都會有這個函數。
抽象類:
含有一個或多個純虛函數的類稱爲抽象類。
【例三】
#include <iostream>
using namespace std;
class A
{
public:
virtual float print() = 0;
protected:
float h,w;
private:
};
class B : public A
{
public:
B(float h0,float w0){h = h0;w = w0;}
float print(){return h*w;}
private:
};
class C : public A
{
public:
C(float h0,float w0){h = h0;w = w0;}
float print(){return h*w/2;}
private:
};
int main(void)
{
A *a1,*a2;
B b(1,2);
C c(1,2);
a1 = &b;
a2 = &c;
cout << a1->print()<<","<<a2->print()<<endl;
getchar();
return 0;
}
結果爲:
2,1
在這個例子中,A就是一個抽象類,基類A中print沒有確定具體的操作,但不能從基類中去掉,否則不能使用基類的指針a1,a2調用派生類中的方法(a1->print;a2->print就不能用了),給多態性造成不便,這裏要強調的是,我們是希望用基類的指針調用派生類的方法,希望用到多態機制,如果讀者並不想用基類指針,認爲用b,c指針直接調用更好,那純虛函數就沒有意義了,多態也就沒有意義了,瞭解一下多態的好處,再決定是否用純虛函數吧。
【注意】
1 抽象類並不能直接定義對象,只可以如上例那樣聲明指針,用來指向基類派生的子類的對象,上例中的A *a1,*a2;該爲 A a1,a2;是錯誤的。
2 從一個抽象類派生的類必須提供純虛函數的代碼實現或依舊指明其爲派生類,否則是錯誤的。
3 當一個類打算被用作其它類的基類時,它的析構函數必須是虛的。
【例三】
class A
{
public:
A() { ptra_ = new char[10];}
~A() { delete[] ptra_;} // 非虛析構函數
private:
char * ptra_;
};
class B: public A
{
public:
B() { ptrb_ = new char[20];}
~B() { delete[] ptrb_;}
private:
char * ptrb_;
};
void foo()
{
A * a = new B;
delete a;
}
在這個例子中,程序也許不會象你想象的那樣運行,在執行delete a的時候,實際上只有A::~A()被調用了,而B類的析構函數並沒有被調用!這是否有點兒可怕? 如果將上面A::~A()改爲virtual,就可以保證B::~B()也在delete a的時候被調用了。因此基類的析構函數都必須是virtual的。純虛的析構函數並沒有什麼作用,是虛的就夠了。通常只有在希望將一個類變成抽象類(不能實例化的類),而這個類又沒有合適的函數可以被純虛化的時候,可以使用純虛的析構函數來達到目的。
最後通過一個例子說明一下抽象類,純虛函數以及多態的妙用吧:
我們希望通過一個方法得到不同圖形面積的和的方式:
#include <iostream>
using namespace std;
class A //定義一個抽象類,用來求圖形面積
{
public:
virtual float area() = 0;//定義一個計算面積的純虛函數,圖形沒確定,當
//不能確定具體實現
protected:
float h,w; //這裏假設所有圖形的面積都可以用h和w兩個元素計算得出
//就假設爲高和長吧
private:
};
class B : public A //定義一個求長方形面積的類
{
public:
B(float h0,float w0){h = h0;w = w0;}
float area (){return h*w;}//基類純虛函數的具體實現
private:
};
class C : public A //定義一個求三角形面積的類
{
public:
C(float h0,float w0){h = h0;w = w0;}
float area (){return h*w/2;}//基類純虛函數的具體實現
private:
};
float getTotal(A *s[],int n)//通過一個數組傳遞所有的圖形對象
//多態的好處出來了吧,不是多態,不能用基類A調用
//參數類型怎麼寫,要是有100個不同的圖形,怎麼傳遞
{
float sum = 0;
for(int i = 0;i < n; i++)
sum = sum + s[i]->area();
return sum;
}
int main(void)
{
float totalArea;
A *a[2];
a[0] = new B(1,2); //一個長方形對象
a[1] = new C(1,2);//一個三角形對象
totalArea = getTotal(a , 2);//求出兩個對象的面積和
getchar();
return 0;
}