12. 多態與重載
- 掌握如何實現
多態性
- 掌握
虛函數
的定義以及用法 - 熟悉
虛析構函數
- 熟悉
抽象基類
- 熟悉並掌握
運算符重載
12.1 多態概述
多態性
就相當於具有不同功能的函數可以共用同一個函數名,這樣就可以用一個函數名調用具有不同功能的函數。
12.1.1 認識多態行爲
- 所謂
消息
,就是調用函數,不同的行爲就是指不同的實現,即執行不同的函數。
靜態多態性和動態多態性
- 函數重載和運算符重載實現的多態性就屬於
靜態多態性
,在程序編譯時系統就能決定調用的是哪個函數,因此靜態多態性
又稱編譯時的多態性
。 靜態多態性
是通過函數重載實現的。動態多態性
是在程序運行過程中才動態地確定操作所針對的對象,它又稱運行時的多態性
。動態多態性
是通過虛函數實現的。
12.1.2 實現多態性
- 在基類的函數前加上
virtual
關鍵字,在派生類中重寫該函數,運行時將會根據對象的實際類型來調用相應的函數。 C++
編譯器在編譯的時候,要確定每個對象調用的函數的地址,這稱爲早期綁定
。- 當編譯器使用
晚期綁定
時,就會在運行時再去確定對象的類型以及正確的調用函數,而要讓編譯器採用晚期綁定,就要在基類中聲明函數時使用virtual
關鍵字,這樣的函數稱之爲虛函數
。 - 一旦某個函數在基類中聲明爲
virtual
,那麼在所有的派生類中該函數都是virtual
,而不需要再顯示地聲明爲virtual
。
12.2 虛函數
虛函數
是在基類中使用關鍵字virtual
聲明的函數。- 在派生類中重新定義基類中定義的虛函數時,會告訴編譯器不要靜態連接到該函數。用戶想要的是在程序中任意點可以根據所調用的對象類型來選擇調用的函數,這種操作被稱爲
動態聯編
,或晚期綁定
。
12.2.1 虛函數的定義
- 一種是基類希望其派生類進行覆蓋的函數;
- 另一種是基類希望派生類直接繼承而不要改變的函數。
- 任何構造函數之外的非靜態函數都可以是虛函數。派生類經常覆蓋它繼承的虛函數,如果派生類沒有覆蓋其基類中某個虛函數,則該虛函數的行爲類似於其他的普通成員,派生類會直接繼承其在基類的版本。
12.2.2 認識虛函數表
- 編譯器在編譯的時候,發現Base類中有虛函數,此時編譯器會爲每個包含虛函數的類創建一個
虛表
即vtable
,該表是一個一維數組,在這個數組中存放每個虛函數的地址。
- 那麼如何讓定位虛表呢?
編譯器另外還爲每個對象提供了一個虛表指針
即vptr
,這個指針指向了對象所屬類的虛表,在程序運行時,根據對象的類型去初始化vptr
,從而讓vptr
正確地指向了所屬類的虛表,從而在調用虛函數的時候,能夠找到正確的函數。 - 正是由於每個對象調用的虛函數都是通過虛表指針來索引的,也就決定了虛表指針的正確初始化是非常重要的,換句話說,
在虛表指針沒有正確初始化之前,用戶不能夠去調用虛函數,那麼虛表指針是在什麼時候,或者什麼地方初始化呢?
答案:在構造函數中進行虛表的創建和虛表指針的初始化,在構造子類對象時,要先調用父類的構造函數,此時編譯器只看到了父類,並不知道後面是否還有繼承者,它初始化父類對象的虛表指針,該虛表指針指向父類的虛表,當執行子類的構造函數時,子類對象的虛表指針被初始化,指向自身的虛表。 - 虛函數表有以下特點:
(1)每一個類都有虛表。
(2)虛表可以繼承,如果子類沒有重寫虛函數,那麼子類虛表中仍然會有該函數的地址,只不過這個地址指向的是基類的虛函數實現,如果基類有3個虛函數,那麼基類的虛表中就有三項(虛函數地址),派生類也會創建虛表,至少有三項,如果重寫了相應的虛函數,那麼虛表中的地址就會改變,指向自身的虛函數實現,如果派生類有自己的虛函數,那麼虛表中就會添加該項。
(3)派生類的虛表中虛地址的排列順序和基類的虛表中虛函數地址排列順序相同。
12.2.3 虛函數的用法
- 虛函數的作用是允許在派生類中重新定義與基類同名的函數,並且可以通過基類指針或引用來訪問基類和派生類中的同名函數。
- 經常會用到類的繼承,目的是保留基類的特性,以減少新類開發的時間。但是,從基類繼承來的某些成員函數不完全滿足派生類的需要。
- 當把基類的某個成員函數聲明爲虛函數後,允許在其派生類中對該函數重新定義,賦予它新的功能,並且可以通過指向基類的指針指向同一類族中不同類的對象,從而調用其中的同名函數。
由虛函數實現的動態多態性就是:同一類族中不同類的對象,對同一函數調用作出不同的響應。 - 在派生類中重新定義此函數,要求函數名、函數類型、函數參數個數和類型全部與基類的虛函數相同,並根據派生類的需要重新定義函數體。
- 但習慣上一般在每一層聲明該函數時都加
virtual
。 - 定義一個指向基類對象的指針變量,並使它指向同一類族中需要調用該函數的對象。
- 通過該指針變量調用此虛函數,此時調用的就是指針變量指向的對象的同名函數。
12.2.4 動態關聯與靜態關聯
- 確定調用的具體對象的過程稱爲
關聯
(binding)。 binding
原意是捆綁或連接
,即把兩樣東西捆綁(或連接)在一起。在這裏是指把一個函數名與一個類對象捆綁在一起,建立關聯
。- 一般地說,
關聯
指把一個標識符和一個存儲地址聯繫起來。
多態性
是指爲一個函數名關聯多種含義的能力,即同一種調用方式可以映像到不同的函數。
- 這種把函數的調用與適當的函數體對應的活動又稱爲
綁定
。根據綁定所進行階段的不同,可分爲早期綁定
(early binding
)、晚期綁定
(late binding
)。 早期綁定
發生在程序的編譯階段,稱爲靜態關聯
(static binding
)。晚期綁定
發生在程序的運行階段,稱爲動態關聯
(dynamic binding
)。
- 可以是通過基類指針與虛函數的結合來實現多態性的。
- 因爲編譯只作靜態的語法檢查,光從語句形式是無法確定調用對象的。
- 如果編譯系統把它放到運行階段處理,在運行階段確定關聯關係。在運行階段,基類指針變量先指向了某一個類對象,然後通過此指針變量調用該對象中的函數。此時調用哪一個對象的函數無疑是確定的。
- 由於是在運行階段把虛函數和類對象“綁定”在一起的,因此,此過程稱爲
動態關聯
(dynamic binding
)。這種多態性是動態的多態性
,即運行階段的多態性
。 - 在運行階段,指針可以先後指向不同的類對象,從而調用同一類族中不同類的虛函數。由於
動態關聯
是在編譯以後的運行階段進行的,因此也稱爲滯後關聯
(late binding
)。
12.2.5 純虛函數
虛函數
是在基類中用virtual
進行聲明定義,然後在子類中重寫這個函數後,基類的指針指向子類的對象,可以調用這個函數。純虛函數
可以不用在基類定義,只需要聲明就可以了。- 因爲是純虛函數,是不能產生基類的對象的,但是可以產生基類的指針。
純虛函數
和虛函數
最主要的區別在於:
純虛函數所在的基類是不能產生對象的,而虛函數的基類是可以產生對象的。- 純虛函數是在聲明虛函數時被“初始化”爲0的函數。
關於純虛函數需要注意以下幾點:
(1)純虛函數沒有函數體。
(2)最後面的“=0”並不表示函數返回值爲0,它只起形式上的作用,告訴編譯系統“這是純虛函數”。
(3)這是一個聲明語句,最後應有分號。
(4)純虛函數只有函數的名字而不具備函數的功能,不能被調用。它只是通知編譯系統:“在這裏聲明一個虛函數,留待派生類中定義”。在派生類中對此函數提供定義後,它才能具備函數的功能,可被調用。
(5)純虛函數的作用是在基類中爲其派生類保留一個函數的名字,以便派生類根據需要對它進行定義。
(6)如果在基類中沒有保留函數名字,則無法實現多態性。如果在一個類中聲明瞭純虛函數,而在其派生類中沒有對該函數定義,則該虛函數在派生類中仍然爲純虛函數。
12.3 虛析構函數
析構函數
的作用是在對象撤退之前做必要的“清理現場”的工作。- 當派生類的對象從內存中撤銷時一般先調用派生類的析構函數,然後再調用基類的析構函數。
- 但是,如果用
new
運算符建立了臨時對象,若基類中有析構函數,並且定義了一個指向該基類的指針變量,在程序中用帶指針參數的delete
運算符撤銷對象時,會發生一個情況:
系統會只執行基類的析構函數,而不執行派生類的析構函數。 - 如果
delete
後邊跟父類的指針則只會執行父類的析構函數,如果delete
後面跟的是子類的指針,那麼它即會執行子類的析構函數,也會執行父類的析構函數。面對這種情況則需要引入虛析構函數。 virtual
在函數中的使用限制:
(1)普通函數不能是虛函數,也就是說這個函數必須是某一個類的成員函數,不可以是一個全局函數,否則會導致編譯錯誤。
(2)靜態成員函數不能是虛函數,static成員函數
是和類同生共處的,它不屬於任何對象,使用virtual
也將導致錯誤。
(3)內聯函數不能是虛函數,如果內聯函數被virtual
修飾,計算機會忽略inline
而使它變成純粹的虛函數。
(4)構造函數不能是虛函數,否則會出現編譯錯誤。
12.4 抽象基類
- 包含純虛成員函數的類即爲
抽象基類
,之所以說它抽象,是因爲它無法實例化,也即無法用於創建對象。 - 抽象基類是無法用於創建對象的,而主函數中我們嘗試創建
Base
類的對象,這是不允許的,編譯提示語法錯誤。 - 純虛成員函數可以被派生類繼承,如果派生類不重新定義抽象基類中的所有純虛成員函數,則派生類同樣會成爲抽象基類,因而也不能用於創建對象。
- 抽象基類可以用於實現公共接口,在抽象基類中聲明的純虛成員函數,派生類如果想要能夠創建對象,則必須全部重新定義這些純虛成員函數。