一、多態與虛函數
多態
定義
-
自然中:多態指的是同一名字的事物可以完成不同的功能。
-
C++中:有了虛函數,基類指針指向基類對象時就使用基類的成員(包括成員函數和成員變量),指向派生類對象時就使用派生類的成員。換句話說,基類指針可以按照基類的方式來做事,也可以按照派生類的方式來做事,它有多種形態,或者說有多種表現方式,我們將這種現象稱爲多態(Polymorphism)。
分類
多態可以分爲編譯時的多態和運行時的多態。前者主要是指函數的重載(包括運算符的重載)、對重載函數的調用,在編譯時就能根據實參確定應該調用哪個函數,因此叫編譯時的多態;而後者則和繼承、虛函數等概念有關。
C++提供多態的目的
可以通過基類指針對所有派生類(包括直接派生和間接派生)的成員變量和成員函數進行“全方位”的訪問,尤其是成員函數。如果沒有多態,我們只能訪問成員變量。
構成多態的條件
- 必須存在繼承關係;
- 繼承關係中必須有同名的虛函數,並且它們是覆蓋關係(函數原型相同)。
- 存在基類的指針,通過該指針調用虛函數。
虛函數
虛函數在C++中唯一用途:構成多態,虛函數是構成多態的前提條件之一。
普通成員函數與虛函數的區別
通過指針調用普通的成員函數時會根據指針的類型(通過哪個類定義的指針)來判斷調用哪個類的成員函數,虛函數是根據指針的指向來調用的,指針指向哪個類的對象就調用哪個類的虛函數。
虛函數注意事項
- 只需要在虛函數的聲明處加上 virtual 關鍵字,函數定義處可以加也可以不加。
- 爲了方便,你可以只將基類中的函數聲明爲虛函數,這樣所有派生類中具有遮蔽關係的同名函數都將自動成爲虛函數。
- 當在基類中定義了虛函數時,如果派生類沒有定義新的函數來遮蔽此函數,那麼將使用基類的虛函數。
- 只有派生類的虛函數覆蓋基類的虛函數(函數原型相同)才能構成多態(通過基類指針訪問派生類函數)。
- 構造函數不能是虛函數。對於基類的構造函數,它僅僅是在派生類構造函數中被調用,這種機制不同於繼承。也就是說,派生類不繼承基類的構造函數,將構造函數聲明爲虛函數沒有什麼意義。
- 析構函數可以聲明爲虛函數,而且有時候必須要聲明爲虛函數。
虛析構函數的必要性
例:
#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,它只起形式上的作用,告訴編譯系統“這是純虛函數”。
注意
-
一個純虛函數就可以使類成爲抽象基類,但是抽象基類中除了包含純虛函數外,還可以包含其它的成員函數(虛函數或普通函數)和成員變量。
-
只有類中的虛函數才能被聲明爲純虛函數,普通成員函數和頂層函數均不能聲明爲純虛函數。
抽象類
定義
包含純虛函數的類稱爲抽象類(Abstract Class)。
之所以說它抽象,是因爲它無法實例化,也就是無法創建對象。原因很明顯,純虛函數沒有函數體,不是完整的函數,無法調用,也無法爲其分配內存空間。
抽象類通常是作爲基類,讓派生類去實現純虛函數。派生類必須實現抽象類的所有純虛函數才能被實例化。
其他
① 虛函數表 —> 多態實現機制
② typeid運算符:獲取類型信息
③ RTTI機制及其內存模型
- 在程序運行後確定對象的類型信息的機制稱爲運行時類型識別(Run-Time Type Identification,RTTI)。
④ 靜態綁定和動態綁定
- 將變量名和函數名統稱爲符號(Symbol),找到符號對應的地址的過程叫做符號綁定。
- 找到函數名對應的地址,然後將函數調用處用該地址替換,這稱爲函數綁定。
- 一般情況下,在編譯期間(包括鏈接期間)就能找到函數名對應的地址,完成函數的綁定,程序運行後直接使用這個地址即可。這稱爲靜態綁定(Static binding)。
- 有時候在編譯期間想盡所有辦法都不能確定使用哪個函數,必須要等到程序運行後根據具體的環境或者用戶操作才能決定。這稱爲動態綁定(dynamic binding)。
- 動態綁定的本質:編譯器在編譯期間不能確定指針指向哪個對象,只能等到程序運行後根據具體的情況再決定。