1. 程式人生 > >C++多型、介面和虛基類的深入理解

C++多型、介面和虛基類的深入理解

表述一:在面嚮物件語言中,介面的多種不同實現方式即為多型。多型是指,用父類的指標指向子類的例項(物件),然後通過父類的指標呼叫實際子類的成員函式

表述二:基類指標(或引用)的多種狀態,即基類指標所指物件的真實身份為基類則調基類中的函式表現基類的行為,為派生類則調派生類的函式表現為派生類的行為。作用是使基類指標或引用統一管理各類物件,是基於虛擬函式實現。

理解:多型性就是允許將子類型別的指標賦值給父類型別的指標,多型是通過虛擬函式實現的

多型可以讓父類的指標有“多種形態”,這是一種泛型技術。(所謂泛型技術,就是試圖使用不變的程式碼來實現可變的演算法)。

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++中用抽象類來實現,在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++中可以多重繼承),所以就要用到介面。

以下是一個簡單的例子,有助於理解C++的多型性和介面:

#include <iostream>
using namespace std;
/*抽象類作為介面類舉例*/
class Shape
{
public:
    virtual double area() = 0;//純虛擬函式,純虛擬函式首先與子類的對應函式屬過載函式,除了形參可不相同(相同為重寫),返回值型別等都相同。
protected:
private:
};//抽象類,不能直接宣告物件,僅作介面類使用。
class Trigon :public Shape
{
public:
    double area(){return a*b/2;}
    Trigon(double x, double y){a = x; b = y;}
protected:
    double a, b;
private:
    
};
class Square :public Trigon
{
public:
    double area(){return a*b;}
    //  Square():Trigon(0,0){cout<<"執行了"<<endl;};//自定義一個建構函式;可配合下一個註釋程式碼測試,驗證下一個註釋的內容。
    Square(double j, double k):Trigon(j,k){}//通過Trigon()為繼承而來的保護成員a\b賦值,使得條理更清晰。
protected:
private:
};
class Circle :public Square
{
public:
    //  Circle(double r){radius = r;}//未顯性指明呼叫父類的建構函式時,預設呼叫父類的沒有形參的預設建構函式。而父類Square已有自定義的建構函式,系統就不再為其提供任何構造函數了,故報錯。
    Circle(double r):Square(0,0){radius = r;}//顯性指明呼叫父類的建構函式。
    double area(){return radius*radius*3.14;}
protected:
private:
    double radius;
    
};
void main()
{
    Shape * p;
    bool quit = false;
    while (1)
    {
        int choice;
        cout<<"請選擇:(0)退出(1)三角形的面積(2)矩形的面積(3)圓的面積.\n";
        cin>>choice;
        switch (choice)
        {
            case 0:
                quit = true;
                break;
            case 1:
                p = new Trigon(2.58,3.69);//三角形
                cout<<p->area()<<endl;
                break;
            case 2:
                p = new Square(23.6,5.666);//矩形
                cout<<p->area()<<endl;
                break;
            case 3:
                p = new Circle(6.866);
                cout<<p->area()<<endl;
                break;
            default:
                cout<<"請輸入0到3的數字:"<<endl;
                break;
        }
        if (choice>0 && choice<=3)
        {
            delete p;//當輸入有效時釋放堆中記憶體空間
        }
        if (quit)  
        {  
            break;  
        }  
    }  
}


6.虛基類

在派生類繼承基類時,加上一個virtual關鍵詞則為虛擬基類繼承,如:
class derive : virtual public base
{
};

虛基類是相對於它的派生類而言的,它本身可以是一個普通的類。只有它的派生類虛繼承它的時候,它才稱作虛基類,如果沒有虛繼承的話,就稱為基類。比如類B虛繼承於類A,那類A就稱作類B的虛基類,如果沒有虛繼承,那類B就只是類A的基類。
虛繼承主要用於一個類繼承多個類的情況,避免重複繼承同一個類兩次或多次(菱形繼承問題)。
例如 由類A派生類B和類C,類D又同時繼承類B和類C,這時候類D就要用虛繼承的方式避免重複繼承類A兩次。

7. 抽象類VS介面

(注意以下的說法不針對C++)

一個類可以有多個介面,只能繼承一個父類;

抽象類可以有構造方法,介面中不能有構造方法;

抽象類中可以有普通成員變數,介面中沒有普通成員變數;

接口裡邊全部方法都必須是abstract的,抽象類的可以有實現了的方法;

抽象類中的抽象方法的訪問型別可以是public,protected,但介面中的抽象方法只能是public型別的,並且預設即為public abstract型別;

抽象類中可以包含靜態方法,介面中不能包含靜態方法;

抽象類和介面中都可以包含靜態成員變數,抽象類中的靜態成員變數的訪問型別可以任意,但介面中定義的變數只能是public static final型別,並且預設即為public static final型別。

8. 虛擬函式VS純虛擬函式

虛擬函式
引入原因:為了方便使用多型特性,我們常常需要在基類中定義虛擬函式。
純虛擬函式
引入原因:
1)同“虛擬函式”;
2)在很多情況下,基類本身生成物件是不合情理的。例如,動物作為一個基類可以派生出老虎、孔雀等子類,但動物本身生成物件明顯不合常理。
純虛擬函式就是基類只定義了函式體,沒有實現過程。
純虛擬函式相當於介面,不能直接例項hu,需要派生類來實現函式定義;
有的人可能在想,定義這些有什麼用?
比如你想描述一些事物的屬性給別人,而自己不想去實現,就可以定義為純虛擬函式。說的再透徹一些,比如蓋樓房,你是老闆,你給建築公司描述清楚你的樓房的特性,多少層,樓頂要有個花園什麼的,建築公司就可以按照你的方法去實現了,如果你不說清楚這些,可能建築公司不太瞭解你需要樓房的特性。用純需函式就可以很好的分工合作了。

二者的區別:

1> 類裡宣告為虛擬函式的話,這個函式是實現的,哪怕是空實現,它的作用就是為了能讓這個函式在它的子類裡面可以被過載,這樣的話,編譯器就可以使用後期繫結來達到多型了;
純虛擬函式只是一個介面,是個函式的宣告而已,它要留到子類裡去實現。

2>虛擬函式在子類裡面也可以不過載的;但純虛必須在子類去實現,這就像Java的介面一樣。通常我們把很多函式加上virtual,是一個好的習慣,雖然犧牲了一些效能,但是增加了面向物件的多型性,因為你很難預料到父類裡面的這個函式在子類裡面如何去修改它的實現;

3>虛擬函式的類用於“實作繼承”,繼承介面的同時也繼承了父類的實現。當然我們也可以完成自己的實現。純虛擬函式的類用於“介面繼承”,主要用於通訊協議方面。關注的是介面的統一性,實現由子類完成。一般來說,介面類中只有純虛擬函式的;

4>帶純虛擬函式的類叫抽象類,這種基類不能直接生成物件,而只有被繼承,並重寫其虛擬函式後,才能使用。