1. 程式人生 > >C++ 靜態多型和動態多型 淺析

C++ 靜態多型和動態多型 淺析

今天的C++已經是個多重泛型程式語言(multiparadigm programming lauguage),一個同時支援過程形式(procedural)、面向物件形式(object-oriented)、函式形式(functional)、範型形式(generic)、超程式設計形式(metaprogramming)的語言。 這些能力和彈性使C++成為一個無可匹敵的工具,但也可能引發使用者的某些迷惑,比如多型。在這幾種程式設計泛型中,面向物件程式設計、範型程式設計以及很新的超程式設計形式都支援多型的概念,但又有所不同。 C++支援多種形式的多型,從表現的形式來看,有虛擬函式、模板、過載等,從繫結時間來看,可以分成靜態多型和動態多型,也稱為編譯期多型和執行期多型。

本文即講述這其中的異同。注意範型程式設計和超程式設計通常都是以模板形式實現的,因此在本文中主要介紹基於面向物件的動態多型和基於模板程式設計的靜態多型兩種形式。另外其實巨集也可以認為是實現靜態多型的一種方式,實現原理就是全文替換,但C++語言本身就不喜歡巨集,這裡也忽略了“巨集多型”。

什麼是動態多型?
動態多型的設計思想:對於相關的物件型別,確定它們之間的一個共同功能集,然後在基類中,把這些共同的功能宣告為多個公共的虛擬函式介面。各個子類重寫這些虛擬函式,以完成具體的功能。客戶端的程式碼(操作函式)通過指向基類的引用或指標來操作這些物件,對虛擬函式的呼叫會自動繫結到實際提供的子類物件上去。

從上面的定義也可以看出,由於有了虛擬函式,因此動態多型是在執行時完成的,也可以叫做執行期多型,這造就了動態多型機制在處理異質物件集合時的強大威力(當然,也有了一點點效能損失)。

看程式碼:
namespace DynamicPoly
{
    class Geometry
    {
    public:
        virtual void Draw()const = 0;
    };


    class Line : public Geometry
    {
    public:
        virtual void Draw()const{    std::cout << "Line Draw()\n";    }
    };


    class Circle : public Geometry
    {
    public:
        virtual void Draw()const{    std::cout << "Circle Draw()\n";    }
    };


    class Rectangle : public Geometry
    {
    public:
        virtual void Draw()const{    std::cout << "Rectangle Draw()\n";    }
    };

    void DrawGeometry(const Geometry *geo)
    {
        geo->Draw();
    }


    //動態多型最吸引人之處在於處理異質物件集合的能力
    void DrawGeometry(std::vector<DynamicPoly::Geometry*> vecGeo)
    {
        const size_t size = vecGeo.size();
        for(size_t i = 0; i < size; ++i)
            vecGeo[i]->Draw();
    }
}

void test_dynamic_polymorphism()
{
    DynamicPoly::Line line;
    DynamicPoly::Circle circle;
    DynamicPoly::Rectangle rect;
    DynamicPoly::DrawGeometry(&circle);


    std::vector<DynamicPoly::Geometry*> vec;
    vec.push_back(&line);
    vec.push_back(&circle);
    vec.push_back(&rect);
    DynamicPoly::DrawGeometry(vec);
}

動態多型本質上就是面向物件設計中的繼承、多型的概念。
動態多型中的介面是顯式介面(虛擬函式),比如,

void DoSomething(Widget& w)
{
    if( w.size() > 0 && w != someNastyWidget)
    {
        Widget temp(w);
        temp.normalize();
        temp.swap(w);
    }
}

對於上面的程式碼,這要求:
由於w的型別被宣告為Widget,所以w必須支援Widget介面,且通常可以在原始碼中找出這些介面(比如Widget.h),因此這些介面也就是顯示介面;
Widget可能只是一個基類,他有子類,也就是說Widget的介面有可能是虛擬函式(比如上面的normalize),此時對介面的呼叫就表現出了執行時多型;

什麼是靜態多型?

靜態多型的設計思想:對於相關的物件型別,直接實現它們各自的定義,不需要共有基類,甚至可以沒有任何關係。只需要各個具體類的實現中要求相同的介面宣告,這裡的介面稱之為隱式介面。客戶端把操作這些物件的函式定義為模板,當需要操作什麼型別的物件時,直接對模板指定該型別實參即可(或通過實參演繹獲得)。

相對於面向物件程式設計中,以顯式介面和執行期多型(虛擬函式)實現動態多型,在模板程式設計及泛型程式設計中,是以隱式介面和編譯器多型來實現靜態多型。

看程式碼:
namespace StaticPoly
{
    class Line
    {
    public:
        void Draw()const{    std::cout << "Line Draw()\n";    }
    };


    class Circle
    {
    public:
        void Draw(const char* name=NULL)const{    std::cout << "Circle Draw()\n";    }
    };


    class Rectangle
    {
    public:
        void Draw(int i = 0)const{    std::cout << "Rectangle Draw()\n";    }
    };


    template<typename Geometry>
    void DrawGeometry(const Geometry& geo)
    {
        geo.Draw();
    }


    template<typename Geometry>
    void DrawGeometry(std::vector<Geometry> vecGeo)
    {
        const size_t size = vecGeo.size();
        for(size_t i = 0; i < size; ++i)
            vecGeo[i].Draw();
    }
}


void test_static_polymorphism()
{
    StaticPoly::Line line;
    StaticPoly::Circle circle;
    StaticPoly::Rectangle rect;
    StaticPoly::DrawGeometry(circle);


    std::vector<StaticPoly::Line> vecLines;
    StaticPoly::Line line2;
    StaticPoly::Line line3;
    vecLines.push_back(line);
    vecLines.push_back(line2);
    vecLines.push_back(line3);
    //vecLines.push_back(&circle); //編譯錯誤,已不再能夠處理異質物件
    //vecLines.push_back(&rect);    //編譯錯誤,已不再能夠處理異質物件
    StaticPoly::DrawGeometry(vecLines);


    std::vector<StaticPoly::Circle> vecCircles;
    vecCircles.push_back(circle);
    StaticPoly::DrawGeometry(circle);
}
靜態多型本質上就是模板的具現化。靜態多型中的介面呼叫也叫做隱式介面,相對於顯示介面由函式的簽名式(也就是函式名稱、引數型別、返回型別)構成,隱式介面通常由有效表示式組成, 比如,
template<typename Widget,typename Other>
void DoSomething(Widget& w, const Other& someNasty)
{
    if( w.size() > 0 && w != someNasty) //someNastyT可能是是T型別的某一例項,也可能不是
    {
        Widget temp(w);
        temp.normalize();
        temp.swap(w);
    }
}
這看似要求:型別T需要支援size、normalize、swap函式,copy建構函式,可以進行不等比較
型別T是在編譯期模板進行具現化時才表現出呼叫不同的函式,此時對介面的呼叫就表現出了編譯期時多型。
但是,size函式並不需要返回一個整型值以和10比較,甚至都不需要返回一個數值型別,唯一的約束是它返回一個型別為X的物件,且X物件和int型別(數值10的型別)可以呼叫一個operator >,這個operator>也不一定非要一個X型別的引數不可,它可以通過隱式轉換能將X型別轉為Y型別物件,而只需要Y型別可以和int型別比較即可(好繞口,請看,這也側面印證了模板程式設計編譯錯誤很難解決)。

同樣型別T並不需要支援operator!=,而只需要T可以轉為X型別物件,someNastyT可以轉為Y型別物件,而X和Y可以進行不等比較即可。

動態多型和靜態多型的比較

靜態多型

優點:

由於靜多型是在編譯期完成的,因此效率較高,編譯器也可以進行優化;
有很強的適配性和鬆耦合性,比如可以通過偏特化、全特化來處理特殊型別;
最重要一點是靜態多型通過模板程式設計為C++帶來了泛型設計的概念,比如強大的STL庫。
缺點:
由於是模板來實現靜態多型,因此模板的不足也就是靜多型的劣勢,比如除錯困難、編譯耗時、程式碼膨脹、編譯器支援的相容性,不能夠處理異質物件集合。

動態多型

優點:
OO設計,對是客觀世界的直覺認識;
實現與介面分離,可複用;
處理同一繼承體系下異質物件集合的強大威力;
缺點:

執行期繫結,導致一定程度的執行時開銷;

編譯器無法對虛擬函式進行優化;
笨重的類繼承體系,對介面的修改影響整個類層次;

不同點:

本質不同,靜態多型在編譯期決定,由模板具現完成,而動態多型在執行期決定,由繼承、虛擬函式實現;
動態多型中介面是顯式的,以函式簽名為中心,多型通過虛擬函式在執行期實現,靜態多臺中介面是隱式的,以有效表示式為中心,多型通過模板具現在編譯期完成。
相同點:
都能夠實現多型性,靜態多型/編譯期多型、動態多型/執行期多型;
都能夠使介面和實現相分離,一個是模板定義介面,型別引數定義實現,一個是基類虛擬函式定義介面,繼承類負責實現;
附上本次測試的所有程式碼:
namespace DynamicPoly
{
    class Geometry
    {
    public:
        virtual void Draw()const = 0;
    };


    class Line : public Geometry
    {
    public:
        virtual void Draw()const{    std::cout << "Line Draw()\n";    }
    };


    class Circle : public Geometry
    {
    public:
        virtual void Draw()const{    std::cout << "Circle Draw()\n";    }
    };


    class Rectangle : public Geometry
    {
    public:
        virtual void Draw()const{    std::cout << "Rectangle Draw()\n";    }
    };


    void DrawGeometry(const Geometry *geo)
    {
        geo->Draw();
    }


    //動態多型最吸引人之處在於處理異質物件集合的能力
    void DrawGeometry(std::vector<DynamicPoly::Geometry*> vecGeo)
    {
        const size_t size = vecGeo.size();
        for(size_t i = 0; i < size; ++i)
            vecGeo[i]->Draw();
    }
}


namespace StaticPoly
{
    class Line
    {
    public:
        void Draw()const{    std::cout << "Line Draw()\n";    }
    };


    class Circle
    {
    public:
        void Draw(const char* name=NULL)const{    std::cout << "Circle Draw()\n";    }
    };


    class Rectangle
    {
    public:
        void Draw(int i = 0)const{    std::cout << "Rectangle Draw()\n";    }
    };


    template<typename Geometry>
    void DrawGeometry(const Geometry& geo)
    {
        geo.Draw();
    }


    template<typename Geometry>
    void DrawGeometry(std::vector<Geometry> vecGeo)
    {
        const size_t size = vecGeo.size();
        for(size_t i = 0; i < size; ++i)
            vecGeo[i].Draw();
    }
}


void test_dynamic_polymorphism()
{
    DynamicPoly::Line line;
    DynamicPoly::Circle circle;
    DynamicPoly::Rectangle rect;
    DynamicPoly::DrawGeometry(&circle);


    std::vector<DynamicPoly::Geometry*> vec;
    vec.push_back(&line);
    vec.push_back(&circle);
    vec.push_back(&rect);
    DynamicPoly::DrawGeometry(vec);
}


void test_static_polymorphism()
{
    StaticPoly::Line line;
    StaticPoly::Circle circle;
    StaticPoly::Rectangle rect;
    StaticPoly::DrawGeometry(circle);


    std::vector<StaticPoly::Line> vecLines;
    StaticPoly::Line line2;
    StaticPoly::Line line3;
    vecLines.push_back(line);
    vecLines.push_back(line2);
    vecLines.push_back(line3);
    //vecLines.push_back(&circle); //編譯錯誤,已不再能夠處理異質物件
    //vecLines.push_back(&rect);    //編譯錯誤,已不再能夠處理異質物件
    StaticPoly::DrawGeometry(vecLines);


    std::vector<StaticPoly::Circle> vecCircles;
    vecCircles.push_back(circle);
    StaticPoly::DrawGeometry(circle);
}


/**無法編譯通過,因此Widget要求有顯式介面,但現在看不到*/
//void DoSomething(Widget& w)
//{
//    if( w.size() > 0 && w != someNastyWidget)
//    {
//        Widget temp(w);
//        temp.normalize();
//        temp.swap(w);
//    }
//}


/**可以編譯通過,因此此處只是要求了只有在模板具現時需保證下面可編譯(無呼叫,無具現)*/
template<typename Widget,typename Other>
void DoSomething(Widget& w, const Other& someNasty)
{
    if( w.size() > 0 && w != someNasty) //someNastyT可能是是T型別的某一例項,也可能不是
    {
        Widget temp(w);
        temp.normalize();
        temp.swap(w);
    }
}