顯示具有 [C加加]物件導向 標籤的文章。 顯示所有文章
顯示具有 [C加加]物件導向 標籤的文章。 顯示所有文章

2014年7月18日 星期五

純虛擬函式、抽象類別(Abstract class)

C++預設函式成員都不是虛擬函式,如果要將某個函式成員宣告為虛擬函式,則要加上"virtual"關鍵字,然而C++提供一種語法定義「純虛擬函式」 (Pure virtual function),指明某個函式只是提供一個介面,要求繼承的子類別必須重新定義該函式,定義純虛擬函式除了使用關鍵字"virtual"之外,要在函 式定義之後緊跟著'='並加上一個0,例如:
class Some {
public:
    // 純虛擬函式
    virtual void someFunction() = 0;

    ....
};

一個類別中如果含有純虛擬函式,則該類別為一「抽象類別」(Abstract class),該類別只能被繼承,而不能用來直接生成實例,如果試圖使用一個抽象類別來生成實例,則會發生編譯錯誤。

以下舉個實際的例子,先假設您設計了兩個類別:ConcreteCircle與HollowCircle:

class ConcreteCircle {
public:
    void radius(double radius) {
         _radius = radius;
    }
    double radius() {
        return _radius;
    }
    void render() {
         cout << "畫一個半徑 "
              << _radius
              << " 的實心圓"
              << endl;
    }
private:
    double _radius;
};

class HollowCircle {
public:
    void radius(double radius) {
         _radius = radius;
    }
    double radius() {
        return _radius;
    }
    void render() {
         cout << "畫一個半徑 " 
              << _radius 
              << " 的空心圓"
              << endl;
    }
private:
    double _radius;
};

顯然的,這兩個類別除了render()方法的實作內容不同之外,其它的定義是一樣的,而且這兩個類別所定義的顯然都是「圓」的一種類型,您可以定義一個 抽象的AbstractCircle類別,將ConcreteCircle與HollowCircle中相同的行為與定義提取(Pull up)至抽象類別中:

  • AbstractCircle.h
#ifndef ABSTRACTCIRCLE
#define ABSTRACTCIRCLE

class AbstractCircle {
public:
    void radius(double radius) {
        _radius = radius;
    }
    double radius() {
        return _radius;
    }
    // 宣告虛擬函式
    virtual void render() = 0;
 
protected:
    double _radius;
};

#endif

注意到在類別宣告了虛擬函式render(),所以AbstractCircle是個抽象類別,它只能被繼承,繼承了AbstractCircle的類別 必須實作render()函式,接著您可以讓ConcreteCircle與HollowCircle類別繼承AbstractCircle方法並實作 render()函式:

  • HollowCircle.h
#include <iostream> 
#include "AbstractCircle.h"
using namespace std; 

class HollowCircle : public AbstractCircle {
public:
    void render() {
        cout << "畫一個半徑 " 
             << _radius 
             << " 的空心圓"
             << endl;
    }
};
  • ConcreteCircle.h
#include <iostream> 
#include "AbstractCircle.h"
using namespace std; 

class ConcreteCircle : public AbstractCircle {
public:
    void render() {
        cout << "畫一個半徑 " 
             << _radius 
             << " 的實心圓"
             << endl;
    }
};

由於共同的定義被提取至AbstractCircle類別中,並於衍生類別中繼承了下來,所以在ConcreteCircle與HollowCircle 中無需重覆定義,只要定義個別對render()的處理方式就行了,而由於ConcreteCircle與HollowCircle都是 AbstractCircle的子類別,因而可以使用AbstractCircle上所定義的虛擬操作介面,來操作子類別實例上的方法,如下所示:

  • main.cpp
#include <iostream> 
#include "AbstractCircle.h"
#include "ConcreteCircle.h"
#include "HollowCircle.h"
using namespace std; 

void render(AbstractCircle &circle) {
    circle.render();
}

int main() {
    ConcreteCircle concrete;
    concrete.radius(10.0);
    render(concrete);
 
    HollowCircle hollow;
    hollow.radius(20.0);
    render(hollow);
 
    return 0;
}

執行結果:

畫一個半徑 10 的實心圓
畫一個半徑 20 的空心圓


Gossip: 虛擬函式(Virtual function)

之前曾經介紹過函式與運算子的重載(Overload),重載可以使用一個函式名稱來執行不同的實作,這是一種「編譯時期」就需決定的方式,這是「早期繫 結」(Early binding)、「靜態繫結」(Static binding),因為在編譯時就可以決定函式的呼叫對象,它們的呼叫位址在編譯時就可以得知。

「虛擬函式」(Virtual function)可以實現「執行時期」的多型支援,是一個「晚期繫結」(Late binding)、「動態繫結」(Dynamic binding),也就是指必須在執行時期才會得知所要調用的物件或其上的公開介面。

在談虛擬函式之前必須先知道,一個基底類別的物件指標,可以用來指向其衍生類別物件而不會發生錯誤,例如若基底類別是Foo1,而衍生類別是Foo2,則 下面這個指定是可以接受的: 

Foo1 *fptr; 
Foo2 f2; 
fptr = &f2;

多型與動態繫結的基礎從這開始,它們只有在使用指標或參考時才得以發揮它們的特性,然而由於fptr仍是Foo1類型的指標,它只能存取Foo1中有定義 的成員,目前來說也只能操作Foo1中的成員。

注意將衍生類別型態的指標指向基底類別的物件基本是不可行的(雖然可以使用型態轉換的方式來勉強達成,但並不鼓勵),衍生類別的指標並不能存取基底類別的 成員。

虛擬函式是一種成員函式,它在基底類別中使用關鍵字"virtual"宣告(定義),並在衍生類別中重新定義虛擬函式,這將成員函式的操作決議 (Resolution)推遲至執行時期再決定。

虛擬函式可以實現執行時期的「多型」,也就是「一個介面,多種函式」,一個含有虛擬函式的類別被稱為「多型的類別」(Polymorphic class),當一個基底類別型態的指標指向一個含有虛擬函式的衍生類別,您就可以使用這個指標來存取衍生類別中的虛擬函式,下面這個例子是個簡單的示 範:
 

#include <iostream> 
using namespace std; 

class Foo1 { 
public: 
    virtual void show() { // 虛擬函式 
        cout << "Foo1's show" << endl; 
    } 
}; 

class Foo2 : public Foo1 { 
public: 
    virtual void show() { // 虛擬函式 
        cout << "Foo2's show" << endl; 
    } 
}; 

void showFooByPtr(Foo1 *foo) {
    foo->show();
}

void showFooByRef(Foo1 &foo) {
    foo.show();
}

int main() { 
    Foo1 f1; 
    Foo2 f2; 

    // 動態繫結 
    showFooByPtr(&f1); 
    showFooByPtr(&f2);
    cout << endl;
 
    // 動態繫結 
    showFooByRef(f1); 
    showFooByRef(f2);
    cout << endl; 

    // 靜態繫結 
    f1.show(); 
    f2.show(); 

    return 0;
}

執行結果:

Foo1's show
Foo2's show

Foo1's show
Foo2's show

Foo1's show
Foo2's show

showFooByPtr()與showFooByRef()函式並無法事先知道要操作的是哪一個物件的哪一個公開介面,最後的操作要在執行時期才能決 定。

衍生類別中重新定義虛擬函式時,virtual可以根據需求加上,如果再接下來的衍生類別仍想進行多型操作,則加上virtual,如果不打算進行多型操 作,則可以不加上。

保護(protected)繼承、私用(private)繼承

在繼承時採公開(public)繼承的方式來繼承一個類別時,父類別與子類別為"is-a"的 關係,子類別繼承父類別的公開(public)介面及受保護(protected)的成員,子類別是父類別的細化型態。

保護(protected)繼承可以改變繼承下來的基底類別成員權限,保護的意思就是讓這些成員繼承下來之後,保護它們僅能在類別與衍生類別中使用,保護 繼承的語法如下所示: 

class B : protected A { 
    // 實作 
};

保護繼承時使用protected來繼承基底類別,繼承下來的成員在衍生類別中的權限變為如下:

基 底類別衍 生類別
private不繼承
protectedprotected
publicprotected

簡單的說,原來的權限在protected以下的保留其原來權限,而在protected以上的就降為protected,子類別protected繼承 的目的在只希望保留父類別中已實作的公開成員與受保護的成員為己用或接下來的衍生類別使用,並提供自己的公開介面。

您也可以在繼承基底類別之後,將它所有的成員一律改為私用(private),使用私用(private)繼承可以達到這個目的,其語法如下: 

class B : private A { 
    // 實作 
}; 


基底類別中的成員在被繼承之後,其權限如下所示: 

基 底類別衍 生類別
private不繼承
protectedprivate
publicprivate

private繼承被稱為「實作繼承」,意味著子類別只想保留父類別中已實作的公開與受保護的成員為己用,並提供自己的公開介面與接下來會被繼承的受保護 的成員。