C++ 虛函數&純虛函數&抽象類&接口&虛基類

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>帶純虛函數的類叫抽象類,這種基類不能直接生成對象,而只有被繼承,並重寫其虛函數後,才能使用。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章