1. 程式人生 > >C++的多型與虛擬函式

C++的多型與虛擬函式

多型的作用:繼承是子類使用父類的方法,而多型則是父類使用子類的方法

在C++中,多型有兩種,一種是函式過載,一種是虛擬函式。函式過載發生在編譯的時候,它的函式引數是不一樣的。而虛擬函式是發生在執行的時候,它的函式原型是一樣的,依靠的是指標的指向。

有一篇非常好的文章介紹多型與虛擬函式。發一個連結在這裡。

http://blog.csdn.net/augusdi/article/details/38271009

這麼一大堆名詞,實際上就圍繞一件事展開,就是多型,其他三個名詞都是為實現C++的多型機制而提出的一些規則,下面分兩部分介紹,第一部分介紹【多型】,第二部分介紹【虛擬函式,純虛擬函式,抽象類】

一 【多型】

多型的概念 :關於多型,好幾種說法,好的壞的都有,分別說一下:

1指同一個函式的多種形態。

       個人認為這是一種高手中的高手喜歡的說法,對於一般開發人員是一種差的不能再差的概念,簡直是對人的誤導,然人很容易就靠到函式過載上了。

       以下是個人認為解釋的比較好的兩種說法,意思大體相同:

2多型是具有表現多種形態的能力的特徵,在OO中是指,語言具有根據物件的型別以不同方式處理之,特別是過載方法和繼承類這種形式的能力。

       這種說法有點繞,仔細想想,這才是C++要告訴我們的。

3多型性是允許你將父物件設定成為和一個或更多的他的子物件相等的技術,賦值之後,父物件就可以根據當前賦值給它的子物件的特性以不同的方式運作。簡單的說,就是一句話:允許將子類型別的指標賦值給父類型別的指標。多型性在

Object PascalC++中都是通過虛擬函式(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就多型了呢,我們還要介紹一個概念:聯編

      函式的聯編:在編譯或執行將函式呼叫與相應的函式體連線在一起的過程。

先期聯編或靜態聯編:在編譯時就能進行函式聯編稱為先期聯編或靜態聯編。

遲後聯編或動態聯編:在執行時才能進行的聯編稱為遲後聯編或動態聯編。

     那麼聯編與虛擬函式有什麼關係呢,當然,造成上面例子中的矛盾的原因就是程式碼的聯編過程採用了先期聯編,使得編譯時系統無法確定究竟應該呼叫基類中的函式還是應該呼叫派生類中的函式,要是能夠採用上面說的遲後聯編就好了,可以在執行時再判斷到底是哪個物件,所以,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指標直接呼叫更好,那純虛擬函式就沒有意義了,多型也就沒有意義了,瞭解一下多型的好處,再決定是否用純虛擬函式吧。

【注意】

抽象類並不能直接定義物件,只可以如上例那樣宣告指標,用來指向基類派生的子類的物件,上例中的A *a1,*a2;該為 A a1,a2;是錯誤的。

從一個抽象類派生的類必須提供純虛擬函式的程式碼實現或依舊指明其為派生類,否則是錯誤的。

當一個類打算被用作其它類的基類時,它的解構函式必須是虛的。

【例三】

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;

}