C++多型之虛擬函式

Koma_Wong發表於2018-06-17
  • Copyright(C)原文來自GitHub賬號,此為副本,僅為學習,如有侵權請聯絡刪除!

C++多型之虛擬函式

多型性指的是相同物件在收到不同訊息或者不同物件收到相同訊息產生不同的實現動作。 C++中多型可以分為兩種:靜態多型動態多型。其中靜態多型主要的實現方式就是通過過載,過載又可以分為函式過載和運算子過載,內容比較多,這篇文章裡不會做過分詳細的描述。而動態過載的主要實現方式就是虛擬函式,因為虛擬函式體現了繼承和多型兩大特性, 可以說虛擬函式是C++中最重要的概念之一,正因有了虛擬函式我們才可以做到通過統一的訪問方式動態呼叫不同類中相同的函式。 雖然本文不會去描述過載的細節,但是為了對C++三大特性之一的多型性有比較深刻的認識,還是很有必要好好比較一番過載和虛擬函式(重寫)的異同的,接下來我將逐條羅列二者的區別。

虛擬函式(重寫):

  • 1.範圍:有繼承關係的多個類中的同名函式。
  • 2.引數:函式的函式名、引數均相同。
  • 3.修飾符:被過載的函式必須有 virtual 修飾。

函式過載

  • 1.範圍:位於同一個類中的同名函式。
  • 2.引數:函式引數列表必須不同。
  • 3.修飾符:virtual 修飾符可有可無。

比較完二者的區別,接下來把重心回到虛擬函式中來,我們必須需要知道什麼是虛擬函式, 虛擬函式是怎麼使用的,虛擬函式的實現原理以及虛擬函式和純虛擬函式的比較。

虛擬函式的主要工作

無論是基類還是派生類,其中同名但又體現不同特性的成員函式可以通過統一的基類指標訪問。我們舉個簡單的例子,定義賬單類(作為基類),類中成員函式 print 可以列印出每個月的賬單,現有計算機學院賬單類繼承了賬單類,同樣定義了成員函式 print,且這個函不僅僅只有函式名,連返回值和引數列表均和賬單類中的 print 函式完全相同,唯一的區別就是要額外列印出計算機學院的名字。若定義指標指向基類物件(賬單類)的指標 p,之後用這個指標取計算機學院物件的地址,之後用 p 呼叫 print 函式,這裡本意是取得派生類物件的地址,之後呼叫可以列印出計算機學院名字的那個 print 函式。但事與願違,這裡只能呼叫基類的 print 函式,確實是使用了基類的函式列印了計算機類賬單的變數,但很顯然不是我們所想的樣子。

  • 最簡單的解決方法就是重新定義一個指標,指標是指向計算機學院賬單物件的,這樣當我們使用這個指標訪問 print 函式時,就能精確的呼叫該派生類中的 print 函式了(即可以列印出計算機學院名字)。但是這種方式在程式很大的時候會造成管理十分混亂,程式設計師需要同時記得很多指標以及指標型別,這樣給程式開發維護帶來了很大的困難。
  • 另一種解決方案更加簡單粗暴,可以直接將各個函式中同名的函式換成不同的名字,每次呼叫時候都精確給出呼叫函式的名字,從而保證準確呼叫函式。但這種方式沒能發揮物件導向程式設計統一介面的思想,同樣給開發帶來了困難。

我們渴望有一種方式,只需要定義一個指向基類的指標,之後通過這個指標呼叫任何派生類中的函式時,都能準確通過定義的物件的型別自動去找到相應的同名函式。這就是我們所要講的虛擬函式

繼續使用之前的例子,我們可以在基類(賬單類)中將 print 成員函式前加上 virtual, 這樣 print 就成了虛擬函式。之後我們在通過派生出計算機賬單類的時候,該子類中的 print 函式也會是虛擬函式(無論是否顯式的加上 virtual 關鍵字)。 之後當我們使用指向基類的指標重新取得派生類的物件地址時,使用這個地址呼叫print 函式就可以列印出計算機學院的名字了,即可以立即為指標雖然是指向基類型別的,但因為重新取了新物件的地址,當執虛擬函式時就會準確呼叫取得地址的那個物件中的虛擬函式。

接下來說一下虛擬函式的原理

虛擬函式的實現主要是因為一個指標和一張指向函式入口的表。每一個包含虛擬函式的類都包含一個指標變數(系統自動插入類的低地址處),我們叫它vptr,所以包含虛擬函式的類是要比實際可見的大小多出一個指標變數大小的。每個類的這個指標指向一張虛擬函式表,虛擬函式表的每一個記錄是一個虛擬函式的指標。在呼叫類的建構函式時,指向此基礎類的指標此時已經變成了指向具體類的 this 指標,這樣靠此 this 指標即可得到正確的 vtable,如此才能真正與函式進行連線,這就是動態聯編,也是虛擬函式實現多型的基本原理。

最後,比較一下虛擬函式和純虛擬函式的區別

虛擬函式之前已經說得比較明白了,我們也看到之前的例子中,所有的基類都可以定義物件,並且基類中的虛擬函式具有完整的定義,只不過它和其派生類中的其他同名函式在函式體部分略有不同。但是在現實生活中,並不是所有的基類建立出的物件都是有意義的,比如動物作為基類,其可以派生出的老虎大象類,然後用老虎大象類去定義物件是有意義的,但是動物作為物件卻沒有什麼意義。這種情況我們可以使用純虛擬函式,純虛擬函式的實現方法是在函式原型後面加“=0”

純虛擬函式虛擬函式之前最大的區別

純虛擬函式在基類中只有函式宣告卻沒有函式定義,它起到的僅僅是一個介面的作用,其具體實現部分在其派生類中實現。正因純虛擬函式沒有完整的定義,所以包含純虛擬函式的基類是不能例項化的,它只能派生,這種不能例項化的類也被稱為抽象類。如果其派生出的派生類中依舊沒有將純虛擬函式改寫的話,那麼它的派生類依舊是抽象類。

相關文章