《Effective C++》:條款34:區分介面繼承和實現繼承,《effective34

來源:互聯網
上載者:User

《Effective C++》:條款34:區分介面繼承和實現繼承,《effective34

    public繼承的概念,由2部分構成:函數介面(function Interface)繼承和函數實現(function implementation)繼承。這兩種繼承的差異有點像函數的聲明和函數的定義之間的差異。

    我們在設計class時,有時希望derived class只繼承函數的介面(即函式宣告);有時候希望derived class繼承函數介面和實現,但又覆寫它們所繼承的實現;又有時候希望derived class同時繼承函數的介面和實現,但不覆寫任何東西。

    為了更好理解上述差異,用一個繪圖程式來說明:

        class Shape{    public:        virtual void draw() const=0;        virtual void error(const std::string& msg);        int objectID() const;        ……    };    class Rectangle: public Shape{……};    class Ellipse:public Shape{……};

    Shape中有pure virtual函數,所以它是個抽象類別,不能建立Shape對象,但Shape強烈影響了所有以public繼承它的derivedclass,因為
    - 成員函數的介面總會被繼承。條款32所說,public繼承意味著is-a

    Shape class有三個函數。draw是pure virtual函數;error是impure pure函數;objectID是non-virtual函數。

    pure virtual函數有兩個特點:它們必須被繼承了它們的具體class重新聲明,而且在抽象class中通常沒有定義。這也就是說明:

    • 聲明一個pure virtual函數的目的是為了讓derived class只繼承函數介面。

    這也是合情合理的,因為Shape::draw並不知道我們要畫什麼映像,當然無法給出實現了。但是我們可以為pure virtual函數提供定義,即為Share::draw提供一份實現,C++不會發出怨言,但是調用這個函數的唯一途徑是調用時指明其class名稱:

     Shape* ps=new Shape; ps->draw(); ps->Share::draw();

    impure virtual函數和pure virtual函數有所不同,derived classes繼承其函數介面,但impure virtual函數會提供一份實現代碼,derived class可能覆寫(override)它。

    • 聲明簡樸的(非純)impure virtual函數的目的,是讓derived classes繼承該函數的介面和預設實現。

    考慮Shape::error這個例子,error介面表示,每個class都必須支援一個遇上錯誤時可調用的函數,但每個class可以自由處理錯誤。如果某個class不想針對錯誤做出特殊行為,可以退回到Shape class提供的預設錯誤處理行為。也就是說Shape::error的聲明式告訴derived class設計者:你必須支援一個error函數,但如果你不想自己寫,可以使用Shape class提供的預設版本。

    如果允許impure virtual函數同時指定函式宣告和函數預設行為,有可能造成危險。考慮一個具體例子,一個XYZ航空公司設計飛機繼承體系,該公司有A型和B型兩種飛機,都以相同方式飛行,可以考慮這樣設計繼承體系:

        class Airport{ ……};    class Airplane{    public:        virtual void fly(const Airport& destation);        ……    };    void Airplane::fly(const Airport& destation)    {        //將飛機飛到指定的destination    }    class ModelA: public Airplane{……};    class ModelB: public Airplane{……};

    因為不同型飛機不需要不同的fly實現,Airplane::fly被聲明為virtual;為了避免在ModelA和ModelB重新撰寫相同代碼,預設的飛行行為有Airplane::fly提供。

    上面這種設計方式是典型的物件導向設計。兩個classes共用的性質放到base class中,然後被這兩個class繼承。這樣可以突出共同性質,避免代碼重複。

    但是如果XYZ要購買一種新型飛機C,C和A、B飛行方式不同。XYZ公司程式員給C型飛機添加了一個class,但是沒有重新定義fly函數

    class ModelC: public Airplane{……};

    然後又寫了如下代碼

        Airport PDX();//某個機場    Airplane* pa=new ModelC;    ……    pa->fly(PDX);//調用了Airplane::fly

    這會造成大災難,因為程式員試圖以ModelA或ModelB的方式來飛ModelC。問題不在於Airplane::fly有預設行為,在於ModelC在未搞清楚的情況下就使用了這個預設行為。幸運的是可以做到:提供預設實現給derived classes,但是除非derived classes真的要用。這個做法是切斷virtual函數介面和其預設實現之間的串連。

        class Airplane{    public:        virtual void fly(const Airport& destation)=0;    protected:        void defaultFly(const Airport& destation);    };    void Airplane::deFaultFly(const Airport& destation)    {        //將飛機飛到指定目的地    }

    這裡將Airplane::fly改為pure virtual函數,只提供介面。但是預設的行為在Airplane::defaultFly函數中出現。如果要使用其預設行為,可以在fly函數調用defaultFly函數。

        class ModelA: public Airplane{    public:        virtual void fly(const Airport& destation)        { defaultFly(destation)}    };    class ModelB: public Airplane    ……    class ModelC: public Airplane{    public:        virtual void fly(const Airport& destination);    };    void ModelC:fly(const Airport& destination)    {        //將C型飛機飛到指定目的地    }

    上面設計中,Airplane::defaultFly是個non-virtual,derived classes不用重新定義(**條款**36)。如果Airplane::defaultFly是virtual函數,就會出現迴圈問題:萬一derived classes忘記重新定義defaultFly函數會怎樣?

    有的人返回以不同的函數分別將提供介面和預設實現,這樣會因為過度雷同的函數名稱引起class命名空間汙染問題;但是他們同意介面和預設實現應該分開。我們可以利用“pure virtual函數必須在derived classes中重新聲明,但它們可以擁有自己的實現”這個特點

        class Airplane{    public:        virtual void fly(const Airport& destination)=0;        ……    };    void Airplane::fly(const Airport& destination)//pure virtual函數實現    {        //預設實現    }    class ModelA: public Airplane{    public:         virtual void fly(const Airport& destination)        {Airplane::fly(destination);}        ……    };    class ModelB:public Airplane    ……    class ModelC: public Airplane    {    public:        virtual void fly(const Airport& destination);        ……    };    void ModelC::fly(const Airport& destination)    {        //ModelC的實現    }

    這個實現和上一個不同之處在於,用pure virtual函數Airplane::fly替換了獨立函數Airplane::defaultFly。現在的fly被分割為兩個基本要素:其聲明部分表現為介面(derived classes必須使用),定義部分表現為預設行為(derived classes明確提出申請才可以用)。

    最後來看看Shape的non-virtual函數objectID;Shape::objectID是個non-virtual函數,這意味著它不打算在derived class中有不同行為。

    • 聲明non-virtual函數的目的是為了令derived classes繼承函數 的介面及一份強制性實現。

    可以把Shape::objectID看做“每個Shape對象都有一個用來產生識別碼的函數,這個識別碼採用相同計算方法。non-virtual函數代表的意義是不變性(invariant)淩駕特異性(specialization),所以不應該在derived classes中被重新定義,這個**條款**36討論的重點。

    pure virtual函數對應只繼承介面;simple(impure) virtual函數對應繼承介面和一份預設實現;non-virtual函數對應繼承介面和一份強制實現。在設計classes時,要分清這些區別和聯絡,否則容易犯兩個錯誤:

    • 第一個錯誤是將所有函式宣告為non-virtual。這會使derived classes沒有空間進行特化工作;non-virtual函數會給解構函式帶了問題(條款**7)。如果關心virtual函數的成本問題,可以參考條款**30的80-20法則。典型的程式有80%時間在執行20%代碼,函數中有80%的virtual函數不一定會給程式帶了多大效率損失,將心力花在20%代碼上才是關鍵。
    • 第二個錯誤是將所有成員函式宣告為virtual。有時候這樣是正確的,例如**條款**31的Interface classes。如果有些函數在derived classes中不應該被重新定義,那麼就應該將這些函式宣告為non-virtual。

    總結

    • 介面繼承和實現繼承不同。在public繼承下,derived classes總是繼承base classes的介面。
    • pure virtual函數只具體指定介面繼承。
    • impure virtual函數具體指定介面繼承和預設實現繼承。
    • non-virtual函數具體指定介面繼承和強制性實現繼承。

    聯繫我們

    該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

    如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

    A Free Trial That Lets You Build Big!

    Start building with 50+ products and up to 12 months usage for Elastic Compute Service

    • Sales Support

      1 on 1 presale consultation

    • After-Sales Support

      24/7 Technical Support 6 Free Tickets per Quarter Faster Response

    • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.