C++(十五)虛函數和純虛函數(抽象類)


關於C++中面向對象的多態特性,多態:即多種形態。在C++中一般是這麼解釋的:向不同的對象發送同一個消息,不同的對象在接收時會產生不同的行爲。
成員函數的重載,或者運算符的重載,模板函數還有下面要講的虛函數,都算是多態性的一種體現。

多態性指相同對象收到不同消息或不同對象收到相同消息時產生不同的實現動作。C++支持兩種多態性:編譯時多態性,運行時多態性。
a.編譯時多態性:通過重載函數實現
b 運行時多態性:通過虛函數實現。

舉個現實中的例子,我們還拿學生來做舉例:說上課了,那麼不同的學生可能會走進不同的教室,上課了,有的學生上語文課,有的學生上數學課,等等。所以針對同一個上課的消息,不同的學生產生的行爲是不一樣的。

一、虛函數

虛函數這個東西可以說是C++面向對象的一個非常重要的概念。
他的核心思想就是一句話:使用基類之指針,指向派生類的對象,調用虛函數的時候,最後調用的是派生類的函數!
虛函數:
①、分別給 CStudent、CXiaoStudent、CZhongStudent 三個類添加 shangke() 這麼個成員函數;
②、分別在三個類的 shangke 函數中打印出不同的文本內容,以示區別;
③、測試如下代碼:

int main(int argc, char* argv[])
{
    CZhongStudent stud_zhong;
    stud_zhong.shangke();     // 使用的是 CZhongStudent::shangke()函數

    CStudent *pStud = &stud_zhong;
    pStud->shangke();        // 使用的是 CStudent::shangke()函數
    return 0;
}

上述基類的指針 pStud 指向了派生類的對象,但是本身基類也有同名該函數,所以就調用了跟指針同類型的基類的 CStudent::shangke() 的函數了。
④、把 CStudent 的 shangke 函數聲明成 virtual 虛函數,如下:
virtual void shangke()
{
cout << “CStudent::shangke called.” << endl;
}
兩次調用的都是 CZhongStudent::shangke 函數。這就是 virtual 虛函數的妙用!

虛函數的注意事項:
①、virtual 只能用來聲明類的成員函數,把它作爲虛函數,而不能將類作用域外的普通函數聲明成虛函數。因爲虛函數的作用是允許在派生類中對基類的虛函數重新定義。顯然,他只能用於類的繼承層次結構中;
②、一個類中的某個函數被聲明成虛函數之後,同一類中就不能再定義一個非virtual的參數和返回值類型都相同的成員函數了。

使用虛函數
一般情況下是某個函數所在的類可能會作爲父類/基類,而且該函數有可能會被派生類重寫,並被派生類使用,那麼這個時候就可以考慮將該函數聲明爲 virtual 虛函數。否則就不用!因爲聲明成虛函數之後是有開銷的,所以不要隨隨便便的想聲明成虛函數就聲明。

二、純虛函數

純虛函數就是沒有函數體的虛函數。包含純虛函數的類就叫抽象類不能生成獨立的對象)。下面的類 A 就是一個抽象類:

class A {
private:
    int a;
public:
    virtual void Print() = 0;       //純虛函數
    void fun1() { cout << "fun1"; }
};

Print 就是純虛函數。純虛函數的寫法就是在函數聲明後面加=0,不寫函數體。純虛函數實際上是不存在的,引入純虛函數是爲了便於實現多態。
既然抽象類不能用來生成獨立對象,但是抽象類可以作爲基類,用來派生新類。可以定義抽象類的指針或引用,並讓它們指向或引用抽象類的派生類的對象,這就爲多態的實現創造了條件。獨立的抽象類的對象不存在,但是被包含在派生類對象中的抽象類的對象是可以存在的。

我們設計了一款“魔法門”遊戲,其中 CCreature 類的寫法如下:
實際上不需要獨立的 CCreature 對象,因爲一個怪物對象要麼代表“狼”,要麼代表“龍”,要麼代表“雷鳥”,總之是代表一種具體的怪物,而不會只是一個抽象的、什麼都不是的“怪物”類的對象。所以,上面的 CCreature 類中的 Attack、Hurted、FightBack 成員函數也都沒有實際操作。
既然如此,將上面三個成員函數聲明爲純虛函數,從而把 CCreature 類變成一個抽象類,就是很恰當的了。因此,CCreature 類的改進寫法如下:

class CCreature {                      //“怪物”類
protected:
    int lifeValue, power;
public:
    virtual void Attack(CCreature* p) = 0;
    virtual void Hurted(int nPower) = 0;
    virtual void FightBack(CCreature* p) = 0;
};

幾何形體對象要麼是圓形,要麼是三角形,要麼是矩形,等等,也不存在抽象的 CShape 類的對象,因此 CShape 類應該如下改寫爲抽象類:

class CShape                         //基類:形體類
{
public:
    virtual double Area() = 0;       //求面積
    virtual void Printlnfo() = 0;    //顯示信息
};

三、虛析構函數

#include <iostream>
using namespace std;
class CShape                       //基類
{
public:
    ~CShape() { cout << "CShape::destrutor" << endl; }
};
class CRectangle : public CShape    //派生類
{
public:
    int w, h;                       //寬度和高度
    ~CRectangle() { cout << "CRectangle::destrutor" << endl; }
};
int main()
{
    CShape* p = new CRectangle;
    delete p;
    return 0;
}

==========輸出結果===============
CShape::destrutor

輸出結果說明,delete p;只引發了 CShape 類的析構函數被調用,沒有引發 CRectangle 類的析構函數被調用。這是因爲該語句是靜態聯編的,編譯器編譯到此時,不可能知道此時 p 到底指向哪個類型的對象,它只根據 p 的類型是 CShape * 來決定應該調用 CShape 類的析構函數。

按理說,delete p;會導致一個 CRectangle 類的對象消亡,應該調用 CRectangle 類的析構函數才符合邏輯,否則有可能引發程序的問題。
例如,假設程序需要對 CRetangle 類的對象進行計數,如果此處不調用 CRetangle 類的析構函數,就會導致計數不正確。
再如,假設 CRectangle 類的對象在存續期間進行了動態內存分配,而釋放內存的操作都是在析構函數中進行的,如果此處不調用 CRetangle 類的析構函數,就會導致被釋放的對象中動態分配的內存以後再也沒有機會回收。

綜上所述,人們希望delete p;這樣的語句能夠聰明地根據 p 所指向的對象執行相應的析構函數。實際上,這也是多態。爲了在這種情況下實現多態,C++ 規定,需要將基類的析構函數聲明爲虛函數,即虛析構函數。

改寫上面程序中的 CShape 類,在析構函數前加 virtual 關鍵字,將其聲明爲虛函數:
class CShape{
public:
virtual ~CShape() { cout << “CShape::destrutor” << endl; }
};
則程序的輸出變爲:
CRectangle::destrutor
CShape::destrutor

說明 CRetangle 類的析構函數被調用了。實際上,派生類的析構函數會自動調用基類的析構函數。
只要基類的析構函數是虛函數,那麼派生類的析構函數不論是否用virtual關鍵字聲明,都自動成爲虛析構函數。

一般來說,一個類如果定義了虛函數,則最好將析構函數也定義成虛函數。
析構函數可以是虛函數,但是構造函數不能是虛函數。

四、虛函數與純虛函數用法與區別

虛函數與純虛函數不同之處

  1. 虛函數和純虛函數可以定義在同一個類(class)中,含有純虛函數的類被稱爲抽象類(abstract class),而只含有虛函數的類(class)不能被稱爲抽象類(abstract class)。

  2. 虛函數可以被直接使用,也可以被子類(sub class)重載以後以多態的形式調用,而純虛函數必須在子類(sub class)中實現該函數纔可以使用,因爲純虛函數在基類(base class)只有聲明而沒有定義。

  3. 虛函數的定義形式:virtual {method body}
    純虛函數的定義形式:virtual { } = 0;

  4. 虛函數必須實現,如果不實現,編譯器將報錯,錯誤提示爲:
    error LNK****: unresolved external symbol “public: virtual void __thiscall
    ClassName::virtualFunctionName(void)”

  5. 虛函數是C++中用於實現多態(polymorphism)的機制。核心理念就是通過基類訪問派生類定義的
    函數

虛函數與純虛函數相同之處
6. 虛函數和純虛函數都可以在子類(sub class)中被重載,以多態的形式被調用。
7. 虛函數和純虛函數通常存在於抽象基類(abstract base class -ABC)之中,被繼承的子類重載,目的是提供一個統一的接口。
8. 在虛函數和純虛函數的定義中不能有static標識符,原因很簡單,被static修飾的函數在編譯時候要求前期bind,然而虛函數卻是動態綁定(run-time bind),而且被兩者修飾的函數生命週期(life recycle)也不一樣。
9. 對於虛函數來說,父類和子類都有各自的版本。由多態方式調用的時候動態綁定。
10. 實現了純虛函數的子類,該純虛函數在子類中就編程了虛函數,子類的子類即孫子類可以覆蓋
該虛函數,由多態方式調用的時候動態綁定。
11. 如果一個類中含有純虛函數,那麼任何試圖對該類進行實例化的語句都將導致錯誤的產生,因爲抽象基類(ABC)是不能被直接調用的。必須被子類繼承重載以後,根據要求調用其子類的方法。

虛函數和純虛函數的區別:https://blog.csdn.net/hackbuteer1/article/details/7558868

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章