第五章 C++多態(Polymorphism)與虛函數(Virtual Function)

一、多態與虛函數

多態

定義

  • 自然中:多態指的是同一名字的事物可以完成不同的功能。

  • C++中:有了虛函數,基類指針指向基類對象時就使用基類的成員(包括成員函數和成員變量),指向派生類對象時就使用派生類的成員。換句話說,基類指針可以按照基類的方式來做事,也可以按照派生類的方式來做事,它有多種形態,或者說有多種表現方式,我們將這種現象稱爲多態(Polymorphism)

分類

多態可以分爲編譯時的多態和運行時的多態。前者主要是指函數的重載(包括運算符的重載)、對重載函數的調用,在編譯時就能根據實參確定應該調用哪個函數,因此叫編譯時的多態;而後者則和繼承、虛函數等概念有關。

C++提供多態的目的

可以通過基類指針對所有派生類(包括直接派生和間接派生)的成員變量和成員函數進行“全方位”的訪問,尤其是成員函數。如果沒有多態,我們只能訪問成員變量。

構成多態的條件

  • 必須存在繼承關係;
  • 繼承關係中必須有同名的虛函數,並且它們是覆蓋關係(函數原型相同)。
  • 存在基類的指針,通過該指針調用虛函數。

虛函數

虛函數在C++中唯一用途:構成多態,虛函數是構成多態的前提條件之一。

普通成員函數與虛函數的區別

通過指針調用普通的成員函數時會根據指針的類型(通過哪個類定義的指針)來判斷調用哪個類的成員函數,虛函數是根據指針的指向來調用的,指針指向哪個類的對象就調用哪個類的虛函數。

虛函數注意事項

  1. 只需要在虛函數的聲明處加上 virtual 關鍵字,函數定義處可以加也可以不加。
  2. 爲了方便,你可以只將基類中的函數聲明爲虛函數,這樣所有派生類中具有遮蔽關係的同名函數都將自動成爲虛函數。
  3. 當在基類中定義了虛函數時,如果派生類沒有定義新的函數來遮蔽此函數,那麼將使用基類的虛函數。
  4. 只有派生類的虛函數覆蓋基類的虛函數(函數原型相同)才能構成多態(通過基類指針訪問派生類函數)。
  5. 構造函數不能是虛函數。對於基類的構造函數,它僅僅是在派生類構造函數中被調用,這種機制不同於繼承。也就是說,派生類不繼承基類的構造函數,將構造函數聲明爲虛函數沒有什麼意義。
  6. 析構函數可以聲明爲虛函數,而且有時候必須要聲明爲虛函數。

虛析構函數的必要性

例:

#include <iostream>
using namespace std;

//基類
class Base{
public:
    Base();
    ~Base();
protected:
    char *str;
};
Base::Base(){
    str = new char[100];
    cout<<"Base constructor"<<endl;
}
Base::~Base(){
    delete[] str;
    cout<<"Base destructor"<<endl;
}

//派生類
class Derived: public Base{
public:
    Derived();
    ~Derived();
private:
    char *name;
};
Derived::Derived(){
    name = new char[100];
    cout<<"Derived constructor"<<endl;
}
Derived::~Derived(){
    delete[] name;
    cout<<"Derived destructor"<<endl;
}

int main(){
   Base *pb = new Derived();
   delete pb;

   cout<<"-------------------"<<endl;

   Derived *pd = new Derived();
   delete pd;

   return 0;
}

運行結果:

Base constructor
Derived constructor
Base destructor
-------------------
Base constructor
Derived constructor
Derived destructor
Base destructor

由結果可以看出:

1、delete pb;僅調用基類的析構函數,沒有調用派生類的析構函數。因爲這裏的析構函數是非虛函數,通過指針訪問非虛函數時,編譯器會根據指針的類型來確定要調用的函數;也就是說,指針指向哪個類就調用哪個類的函數。pb 是基類的指針,所以不管它指向基類的對象還是派生類的對象,始終都是調用基類的析構函數。

2、delete pd;會同時調用派生類和基類的析構函數。pd 是派生類的指針,編譯器會根據它的類型匹配到派生類的析構函數,在執行派生類的析構函數的過程中,又會調用基類的析構函數。派生類析構函數始終會調用基類的析構函數,並且這個過程是隱式完成的。

更改上面的代碼,將基類的析構函數聲明爲虛函數:

class Base{
public:
    Base();
    virtual ~Base();
protected:
    char *str;
};

運行結果:

Base constructor
Derived constructor
Derived destructor
Base destructor
-------------------
Base constructor
Derived constructor
Derived destructor
Base destructor

在實際開發中,一旦我們自己定義了析構函數,就是希望在對象銷燬時用它來進行清理工作,比如釋放內存、關閉文件等,如果這個類又是一個基類,那麼我們就必須將該析構函數聲明爲虛函數,否則就有內存泄露的風險。也就是說,大部分情況下都應該將基類的析構函數聲明爲虛函數

二、抽象類與純虛函數

純虛函數

聲明形式(將虛函數聲明爲純虛函數):

virtual 返回值類型 函數名 (函數參數) = 0;

純虛函數沒有函數體,只有函數聲明,在虛函數聲明的結尾加上=0,表明此函數爲純虛函數。

最後的=0並不表示函數返回值爲0,它只起形式上的作用,告訴編譯系統“這是純虛函數”。

注意

  1. 一個純虛函數就可以使類成爲抽象基類,但是抽象基類中除了包含純虛函數外,還可以包含其它的成員函數(虛函數或普通函數)和成員變量。

  2. 只有類中的虛函數才能被聲明爲純虛函數,普通成員函數和頂層函數均不能聲明爲純虛函數。

抽象類

定義

包含純虛函數的類稱爲抽象類(Abstract Class)

之所以說它抽象,是因爲它無法實例化,也就是無法創建對象。原因很明顯,純虛函數沒有函數體,不是完整的函數,無法調用,也無法爲其分配內存空間。

抽象類通常是作爲基類,讓派生類去實現純虛函數。派生類必須實現抽象類的所有純虛函數才能被實例化。

其他

① 虛函數表 —> 多態實現機制

② typeid運算符:獲取類型信息

③ RTTI機制及其內存模型

  • 在程序運行後確定對象的類型信息的機制稱爲運行時類型識別(Run-Time Type Identification,RTTI)。

④ 靜態綁定和動態綁定

  • 將變量名和函數名統稱爲符號(Symbol),找到符號對應的地址的過程叫做符號綁定。
  • 找到函數名對應的地址,然後將函數調用處用該地址替換,這稱爲函數綁定
  • 一般情況下,在編譯期間(包括鏈接期間)就能找到函數名對應的地址,完成函數的綁定,程序運行後直接使用這個地址即可。這稱爲靜態綁定(Static binding)
  • 有時候在編譯期間想盡所有辦法都不能確定使用哪個函數,必須要等到程序運行後根據具體的環境或者用戶操作才能決定。這稱爲動態綁定(dynamic binding)
  • 動態綁定的本質:編譯器在編譯期間不能確定指針指向哪個對象,只能等到程序運行後根據具體的情況再決定。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章