今天的C++已經是個多重泛型編程語言(multiparadigm programming lauguage),一個同時支持過程形式(procedural)、面向對象形式(object-oriented)、函數形式(functional)、泛型形式(generic)、元編程形式(metaprogramming)的語言。 這些能力和彈性使C++成為一個無可匹敵的工具,但也可能引發使用者的某些迷惑,比如多態。在這幾種編程泛型中,面向對象編程、泛型編程以及很新的元編程形式都支持多態的概念,但又有所不同。 C++支持多種形式的多態,從表現的形式來看,有虛函數、模板、重載等,從綁定時間來看,可以分成靜態多態和動態多態,也稱為編譯期多態和運行期多態。 本文即講述這其中的異同。注意泛型編程和元編程通常都是以模板形式實現的,因此在本文中主要介紹基於面向對象的動態多態和基於模板編程的靜態多態兩種形式。另外其實宏也可以認為是實現靜態多態的一種方式,實現原理就是全文替換,但C++語言本身就不喜歡宏,這裡也忽略了“宏多態”。 什麼是動態多態? 動態多態的設計思想:對於相關的對象類型,確定它們之間的一個共同功能集,然後在基類中,把這些共同的功能聲明為多個公共的虛函數接口。各個子類重寫這些虛函數,以完成具體的功能。客戶端的代碼(操作函數)通過指向基類的引用或指針來操作這些對象,對虛函數的調用會自動綁定到實際提供的子類對象上去。 從上面的定義也可以看出,由於有了虛函數,因此動態多態是在運行時完成的,也可以叫做運行期多態,這造就了動態多態機制在處理異質對象集合時的強大威力(當然,也有了一點點性能損失)。 看代碼: 復制代碼 1 namespace DynamicPoly 2 { 3 class Geometry 4 { 5 public: 6 virtual void Draw()const = 0; 7 }; 8 9 class Line : public Geometry 10 { 11 public: 12 virtual void Draw()const{ std::cout << "Line Draw()\n"; } 13 }; 14 15 class Circle : public Geometry 16 { 17 public: 18 virtual void Draw()const{ std::cout << "Circle Draw()\n"; } 19 }; 20 21 class Rectangle : public Geometry 22 { 23 public: 24 virtual void Draw()const{ std::cout << "Rectangle Draw()\n"; } 25 }; 26 27 void DrawGeometry(const Geometry *geo) 28 { 29 geo->Draw(); 30 } 31 32 //動態多態最吸引人之處在於處理異質對象集合的能力 33 void DrawGeometry(std::vector<DynamicPoly::Geometry*> vecGeo) 34 { 35 const size_t size = vecGeo.size(); 36 for(size_t i = 0; i < size; ++i) 37 vecGeo[i]->Draw(); 38 } 39 } 40 41 void test_dynamic_polymorphism() 42 { 43 DynamicPoly::Line line; 44 DynamicPoly::Circle circle; 45 DynamicPoly::Rectangle rect; 46 DynamicPoly::DrawGeometry(&circle); 47 48 std::vector<DynamicPoly::Geometry*> vec; 49 vec.push_back(&line); 50 vec.push_back(&circle); 51 vec.push_back(&rect); 52 DynamicPoly::DrawGeometry(vec); 53 } 復制代碼 動態多態本質上就是面向對象設計中的繼承、多態的概念。動態多態中的接口是顯式接口(虛函數),比如, 復制代碼 1 void DoSomething(Widget& w) 2 { 3 if( w.size() > 0 && w != someNastyWidget) 4 { 5 Widget temp(w); 6 temp.normalize(); 7 temp.swap(w); 8 } 9 } 復制代碼 對於上面的代碼,這要求: 由於w的類型被聲明為Widget,所以w必須支持Widget接口,且通常可以在源碼中找出這些接口(比如Widget.h),因此這些接口也就是顯示接口; Widget可能只是一個基類,他有子類,也就是說Widget的接口有可能是虛函數(比如上面的normalize),此時對接口的調用就表現出了運行時多態; 什麼是靜態多態? 靜態多態的設計思想:對於相關的對象類型,直接實現它們各自的定義,不需要共有基類,甚至可以沒有任何關系。只需要各個具體類的實現中要求相同的接口聲明,這裡的接口稱之為隱式接口。客戶端把操作這些對象的函數定義為模板,當需要操作什麼類型的對象時,直接對模板指定該類型實參即可(或通過實參演繹獲得)。 相對於面向對象編程中,以顯式接口和運行期多態(虛函數)實現動態多態,在模板編程及泛型編程中,是以隱式接口和編譯器多態來實現靜態多態。 看代碼: 復制代碼 1 namespace StaticPoly 2 { 3 class Line 4 { 5 public: 6 void Draw()const{ std::cout << "Line Draw()\n"; } 7 }; 8 9 class Circle 10 { 11 public: 12 void Draw(const char* name=NULL)const{ std::cout << "Circle Draw()\n"; } 13 }; 14 15 class Rectangle 16 { 17 public: 18 void Draw(int i = 0)const{ std::cout << "Rectangle Draw()\n"; } 19 }; 20 21 template<typename Geometry> 22 void DrawGeometry(const Geometry& geo) 23 { 24 geo.Draw(); 25 } 26 27 template<typename Geometry> 28 void DrawGeometry(std::vector<Geometry> vecGeo) 29 { 30 const size_t size = vecGeo.size(); 31 for(size_t i = 0; i < size; ++i) 32 vecGeo[i].Draw(); 33 } 34 } 35 36 void test_static_polymorphism() 37 { 38 StaticPoly::Line line; 39 StaticPoly::Circle circle; 40 StaticPoly::Rectangle rect; 41 StaticPoly::DrawGeometry(circle); 42 43 std::vector<StaticPoly::Line> vecLines; 44 StaticPoly::Line line2; 45 StaticPoly::Line line3; 46 vecLines.push_back(line); 47 vecLines.push_back(line2); 48 vecLines.push_back(line3); 49 //vecLines.push_back(&circle); //編譯錯誤,已不再能夠處理異質對象 50 //vecLines.push_back(&rect); //編譯錯誤,已不再能夠處理異質對象 51 StaticPoly::DrawGeometry(vecLines); 52 53 std::vector<StaticPoly::Circle> vecCircles; 54 vecCircles.push_back(circle); 55 StaticPoly::DrawGeometry(circle); 56 } 復制代碼 靜態多態本質上就是模板的具現化。靜態多態中的接口調用也叫做隱式接口,相對於顯示接口由函數的簽名式(也就是函數名稱、參數類型、返回類型)構成,隱式接口通常由有效表達式組成, 比如, 復制代碼 1 template<typename Widget,typename Other> 2 void DoSomething(Widget& w, const Other& someNasty) 3 { 4 if( w.size() > 0 && w != someNasty) //someNastyT可能是是T類型的某一實例,也可能不是 5 { 6 Widget temp(w); 7 temp.normalize(); 8 temp.swap(w); 9 } 10 } 復制代碼 這看似要求: 類型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設計,對是客觀世界的直覺認識; 實現與接口分離,可復用 處理同一繼承體系下異質對象集合的強大威力 缺點: 運行期綁定,導致一定程度的運行時開銷; 編譯器無法對虛函數進行優化 笨重的類繼承體系,對接口的修改影響整個類層次; 不同點: 本質不同,靜態多態在編譯期決定,由模板具現完成,而動態多態在運行期決定,由繼承、虛函數實現; 動態多態中接口是顯式的,以函數簽名為中心,多態通過虛函數在運行期實現,靜態多台中接口是隱式的,以有效表達式為中心,多態通過模板具現在編譯期完成 相同點: 都能夠實現多態性,靜態多態/編譯期多態、動態多態/運行期多態; 都能夠使接口和實現相分離,一個是模板定義接口,類型參數定義實現,一個是基類虛函數定義接口,繼承類負責實現;