重讀C++之一:封裝、繼承和多型

chenyufei1013發表於2011-12-01

  • 導讀

        前段時間重新看了一下C++,一是感覺清晰了許多,二是覺得若是換個角度看的話,會有不一樣的體會,並且也容易記住C++中的一些特性。本文就試圖將集合論中的相關知識引入到C++的封裝、繼承、多型上,讓我們對它有個重新的認識。
        從程式碼的角度而言,我認為計算機語言都不可不免的解決以下兩個問題:
        1.為了構建大型的程式,需要將程式碼模組化。C++中,由類的封裝來實現。
        2.為了減少程式碼的冗餘,需要實現程式碼共享。C++中,由類的繼承和多型來實現。

  • 封裝

        C語言中,程式碼之間的關係都是函式式的呼叫。這裡面牽扯到對資料的操作,若操作的都是區域性變數,那一切都太平了。但若是幾個函式操作同一個非區域性變數,考慮到模組化,那麼就要將變數和操作變數的函式整合在一起,這就是C++中的封裝。
        C++裡面引入了class的概念,目的是封裝資料和資料上的操作,使其成為一個獨立的模組。若是將這個獨立的模組(程式碼和資料)想象成集合,那個class A的集合為:


圖一

        此時若再引入一個class B,則有下面四種可能性,情況三、四實際上類似。


圖二

        情況一,只需要封裝就足夠了。處理情況二、三、四時,為了考慮程式碼共享,需要引入繼承機制。

  • 繼承

        我們先考慮情況二,由於A和B有公共程式碼(成員函式或者是成員變數),故通常考慮將公共的部分定義為class C,然後由A、B去繼承它。


圖三

        對於情況三、四,我們不需要演變,直接讓A繼承B,或者B繼承A即可。
        若,此時引入class D,那麼情況就會複雜很多。簡單期間,以情況二為擴充套件,考慮新增class D後的某一種。後續你會發現,情況三、四類似。


圖四

        此時,最合理的方式是引入四個類,class E, class F, class G, class H,如下圖。E為基類,F、G、H為一級子類、A、B、D為二級子類。


圖五

        但是,這種解決方案有問題:
        1.若是再新增class I,class J,那複雜度就可想而知了。
        2.雖然程式碼冗餘是消除了,但是引入了四個類,也著實有點多,更嚴重的話會導致“類氾濫”。
        為了能統一解決新增的類D,我們將圖四拆分成D和A,以及D和B的關係。這樣就轉化為圖二中的一種:情況二。


圖六

        圖六中,class H表示D和A的公共部分,class G表示D和B的公共部分。此種解法雖然有程式碼冗餘,但簡單了許多,事實上,我們很多時候處理類,就是這麼處理的。
        在這種情況下,若是新增class I,class J,都可以轉化為:新新增類和已有類之間的單獨關係,即圖二中的四種情況。
        同時,也可以發現,我們無法在類的繼承結構中完全消除程式碼冗餘,原因是多個類的情況下,實在是比較複雜。
        當我們在使用這些包含繼承結構的類的時候,考慮圖二的情況三,若B繼承自A,那麼實際上B也可以當A用的,這很好理解,本來A就是B的一部分。但若是,想讓A代表B呢(實際上就是B物件,只是用的時候當A用),為了完美解決這個問題,就要引入多型了。

  • 多型

        前面的分析可知,類之間的關係都可以簡化為圖二的情況。圖二的情況三中,A當B用(實際上只有B物件)又分為以下三種情況。第三種情況有點彆扭,可能是需求決定的吧。
        1.使用B中的A部分。直接使用A操作即可。
        2.使用B中的非A部分。需要將A轉化為B才可使用。
        3.B覆蓋定義A的公共介面或者成員變數。當B作為A使用的時候,A中的公共介面或者成員變數是在非A中的,實現這一機制的就是多型。
        C++中,基類定義虛擬函式,子類可以重新實現它,以實現多型。令人奇怪的是,沒有虛成員變數的概念,我覺得可能有以下幾個原因:
        1.沒必要提供虛成員變數。父類的成員變數屬於儲存空間,是可以直接用。不像函式,屬於程式碼無法直接替換。
        2.可能編譯器要實現這個會比較複雜吧。
        3.封裝的概念是少暴露成員變數,只暴露介面。因此,好的類的設計是沒有公共的成員變數的,也就不存在虛成員變數一說了。
        但是,從完整性的角度而言,應該提供虛成員變數的。

  • 總結

        上面的分析可以看出,引入集合,只是說明我試圖用一種簡單的方式來描述C++的封裝、繼承和多型。所有的源頭,都是因為在C++中引入了封裝機制,也就是傳說中的物件導向。繼承和多型都是隨之而來的,順著這條路徑走下去,你會發現C++裡面的變數的可視性(public, protected, private)等都是源自於上面描述的集合之間的關係。而所有的這些,只是用來解決兩個問題:模組化和程式碼共享。

  • 參考資料

        1.C++ Primer 中文版 第4版

相關文章