1. 多態
在物件導向語言中,介面的多種不同實現方式即為多態。多態是指,用父類的指標指向子類的執行個體(對象),然後通過父類的指標調用實際子類的成員函數。
多態性就是允許將子類類型的指標賦值給父類類型的指標,多態是通過虛函數實現的。
多態可以讓父類的指標有“多種形態”,這是一種泛型技術。(所謂泛型技術,就是試圖使用不變的代碼來實現可變的演算法)。
2. 虛函數
2.1虛函數定義
在基類的類定義中,定義虛函數的一般形式:
Virtual 函數傳回值類型 虛函數名(形參表)
{函數體}
虛函數必須是類的非靜態成員函數(且非建構函式),其存取權限是public。
2.2 虛函數的作用
虛函數的作用是實現動態聯編,也就是在程式的運行階段動態地選擇合適的成員函數,在定義了虛函數後,可以在基類的衍生類別中對虛函數進行重新定義(形式同上)。在衍生類別中定義的函數應與虛函數具有相同的形參個數和形參類型(覆蓋),以實現統一的介面,不同定義過程。如果在衍生類別中沒有對虛函數重新定義,則它繼承其基類的虛函數。
虛函數可以讓成員函數操作一般化,用基類的指標指向不同的衍生類別的對象時,基類虛成員函數調用基類指標,則會調用其真正指向的對象的成員函數,而不是基類中定義的成員函數(只要衍生類別改寫了該成員函數)。若不是虛函數,則不管基類指標指向哪個衍生類別對象,調用時都會調用基類中定義的那個函數。
2.3 實現動態聯編需要三個條件:
1)必須把需要動態聯編的行為定義為類的公用屬性的虛函數;
2)類之間存在子類型關係,一般表現為一個類從另一個類公有派生而來;
3)必須先使用基類指標指向子類型的對象,然後直接或者間接使用基類指標調用虛函數。
2.4 定義虛函數的限制
1)非類的成員函數不能定義為虛函數,類的成員函數中靜態成員函數和建構函式也不能定義為虛函數,但可以將解構函式定義為虛函數。
2)只需要在聲明函數的類體中使用關鍵字“virtual”將函式宣告為虛函數,而定義函數時不需要使用關鍵字“virtual”。
3)如果聲明了某個成員函數為虛函數,則在該類中不能出現和這個成員函數同名並且傳回值、參數個數、參數類型都相同的非虛函數。在以該類為基類的衍生類別中,也不能出現這種非虛的同名同傳回值同參數個數同參數類型函數。
2.5
1)為什麼類的靜態成員函數不能為虛函數:
如果定義為虛函數,那麼它就是動態綁定的,也就是在衍生類別中可以被覆蓋的,這與靜態成員函數的定義(在記憶體中只有一份拷貝,通過類名或對象引用訪問靜態成員)本身就是相矛盾的。
2)為什麼建構函式不能為虛函數:
因為如果建構函式為虛函數的話,它將在執行期間被構造,而執行期則需要對象已經建立,建構函式所完成的工作就是為了建立合適的對象,因此在沒有構建好的對象上不可能執行多態(虛函數的目的就在於實現多態性)的工作。在繼承體系中,構造的順序就是從基類到衍生類別,其目的就在於確保對象能夠成功地構建。建構函式同時承擔著虛函數表的建立,如果它本身都是虛函數的話,如何確保vtbl的構建成功呢?
3)虛解構函式
C++開發的時候,用來做基類的類的解構函式一般都是虛函數。當基類中有虛函數的時候,解構函式也要定義為虛解構函式。如果不定義虛解構函式,當刪除一個指向衍生類別對象的指標時,會調用基類的解構函式,衍生類別的解構函式未被調用,造成記憶體泄露。
虛解構函式工作的方式是:最底層的衍生類別的解構函式最先被調用,然後各個基類的解構函式被調用。這樣,當刪除指向衍生類別的指標時,就會首先調用衍生類別的解構函式,不會有記憶體泄露的問題了。
一般情況下,如果類中沒有虛函數,就不用去聲明虛解構函式。若且唯若類裡包含至少一個虛函數的時候才去聲明虛解構函式。
只有當一個類被用來作為基類的時候,才把解構函式寫成虛函數。
2.6虛函數的實現——虛函數表
虛函數是通過一張虛函數表來實現的,簡稱V-Table。類的虛函數表是一塊連續的記憶體,每個記憶體單元中記錄一個JMP指令的地址。編譯器會為每個有虛函數的類建立一個虛函數表,該虛函數表將被該類的所有對象共用,類的每個虛函數成員佔據虛函數表中的一行。
在這個表中,主要是一個類的虛函數的地址表。這張表解決了繼承、覆蓋的問題,保證其真實反應實際的函數。在有虛函數的類的執行個體中,分配了指向這個表的指標的記憶體,所以,當用父類的指標來操作一個子類的時候,這張虛函數表就指明了實際所應該調用的函數。
3. 純虛函數
許多情況下,在基類中不能對虛函數給出有意義的實現,則把它聲明為純虛函數,它的實現留給該基類的衍生類別去做。
純虛函數的聲明格式:virtual <函數傳回型別說明符> <函數名> ( <參數表> )=0;
純虛函數的作用是為衍生類別提供一個一致的介面。
4.抽象類別(abstract class)
抽象類別是指含有純虛函數的類(至少有一個純虛函數),該類不能建立對象(抽象類別不能執行個體化),但是可以聲明指標和引用,用於基礎類的介面聲明和運行時的多態。
抽象類別中,既可以有抽象方法,也可以有具體方法或者叫非抽象方法。抽象類別中,既可以全是抽象方法,也可以全是非抽象方法。一個繼承於抽象類別的子類,只有實現了父類所有的抽象方法才能夠是非抽象類別。
5.介面
介面是一個概念。它在C++中用抽象類別來實現,在C#和Java中用interface來實現。
介面是專門被繼承的。介面存在的意義也是被繼承。和C++裡的抽象類別裡的純虛函數是相同的。不能被執行個體化。
定義介面的關鍵字是interface,例如:
public interface MyInterface{
public void add(int x,int y);
public void volume(int x,int y,int z);
}
繼承介面的關鍵字是implements,相當於繼承類的extends。需要注意的是,當繼承一個介面時,介面裡的所有函數必須全部被覆蓋。
當想繼承多個類時,開發程式不允許,報錯。這樣就要用到介面。因為介面允許多重繼承,而類不允許(C++中可以多重繼承)。所以就要用到介面。
6.虛基類
在衍生類別繼承基類時,加上一個virtual關鍵詞則為虛擬基類繼承,如:
class derive : virtual public base
{
};
虛基類是相對於它的衍生類別而言的,它本身可以是一個普通的類。只有它的衍生類別虛繼承它的時候,它才稱作虛基類,如果沒有虛繼承的話,就稱為基類。比如類B虛繼承於類A,那類A就稱作類B的虛基類,如果沒有虛繼承,那類B就只是類A的基類。
虛繼承主要用於一個類繼承多個類的情況,避免重複繼承同一個類兩次或多次。
例如 由類A衍生類別B和類C,類D又同時繼承類B和類C,這時候類D就要用虛繼承的方式避免重複繼承類A兩次。
7. 抽象類別VS介面
一個類可以有多個介面,只能繼承一個父類??
抽象類別可以有構造方法,介面中不能有構造方法;
抽象類別中可以有普通成員變數,介面中沒有普通成員變數;
介面裡邊全部方法都必須是abstract的,抽象類別的可以有實現了的方法;
抽象類別中的抽象方法的訪問類型可以是public,protected,但介面中的抽象方法只能是public類型的,並且預設即為public abstract類型;
抽象類別中可以包含靜態方法,介面中不能包含靜態方法;
抽象類別和介面中都可以包含靜態成員變數,抽象類別中的靜態成員變數的訪問類型可以任意,但介面中定義的變數只能是public static final類型,並且預設即為public static final類型。
8. 虛函數VS純虛函數
虛函數
引入原因:為了方便使用多態特性,我們常常需要在基類中定義虛函數。
純虛函數
引入原因:
1)同“虛函數”;
2)在很多情況下,基類本身產生對象是不合情理的。例如,動物作為一個基類可以派生出老虎、孔雀等子類,但動物本身產生對象明顯不合常理。
純虛函數就是基類只定義了函數體,沒有實現過程。
純虛函數相當於介面,不能直接執行個體話,需要衍生類別來實現函數定義;
有的人可能在想,定義這些有什麼用?
比如你想描述一些事物的屬性給別人,而自己不想去實現,就可以定義為純虛函數。說的再透徹一些,比如蓋樓房,你是老闆,你給建築公司描述清楚你的樓房的特性,多少層,樓頂要有個花園什麼的,建築公司就可以按照你的方法去實現了,如果你不說清楚這些,可能建築公司不太瞭解你需要樓房的特性。用純需函數就可以很好的分工合作了。
二者的區別:
1> 類裡聲明為虛函數的話,這個函數是實現的,哪怕是空實現,它的作用就是為了能讓這個函數在它的子類裡面可以被重載,這樣的話,編譯器就可以使用後期綁定來達到多態了;
純虛函數只是一個介面,是個函數的聲明而已,它要留到子類裡去實現。
2>虛函數在子類裡面也可以不重載的;但純虛必須在子類去實現,這就像Java的介面一樣。通常我們把很多函數加上virtual,是一個好的習慣,雖然犧牲了一些效能,但是增加了物件導向的多態性,因為你很難預料到父類裡面的這個函數不在子類裡面不去修改它的實現;
3>虛函數的類用於“實作繼承”,繼承介面的同時也繼承了父類的實現。當然我們也可以完成自己的實現。純虛函數的類用於“介面繼承”,主要用於通訊協定方面。關注的是介面的統一性,實現由子類完成。一般來說,介面類中只有純虛函數的;
4>帶純虛函數的類叫抽象類別,這種基類不能直接產生對象,而只有被繼承,並重寫其虛函數後,才能使用。