[深入理解C++(二)]理解介面繼承規則

來源:互聯網
上載者:User
文章目錄
  • 一,前言
  • 二,引子:重載(overload),重寫(override),屏蔽(hide)
  • 三,函數繼承規則:
  • 四,後記
  • 五,引用
[深入理解C++(二)]理解介面繼承規則

 羅朝輝 ( http://www.cnblogs.com/kesalin/ )

CC許可,轉載請註明出處一,前言

在前一篇《[深入理解C++(一)]類型轉換(Type Casting)》中,我詳細講述了 C++ 中轉型動作,以及使用規則。有網友說應該提及下《深度探索 C++ 物件模型》一書中的內容,其實他的意思是,要是對 C++ 對象的記憶體布局不甚瞭解,就想要徹悟C++中的類型轉型,對象切割,虛函數調用等,猶如脫離了堅實的根基,想去建空中閣樓。理解 C++ 對象的記憶體布局對學會 C++來說至關重要,但我不打算寫 C++ 對象的記憶體布局相關的文章,因為要站在前人的肩膀上,大牛陳皓 已經就這個主題寫了三篇圖文並茂的文章:

(一),C++ 虛函數表解析

(二),C++ 對象的記憶體布局(上)

(三),C++ 對象的記憶體布局(下)

 

在繼續閱讀本文之前,建議先閱讀這三篇文章,以更好地理解本系列文章。在接下來的內容中,我將從重載,重寫,屏蔽等概念入手,引入眾多介面繼承規則。

 

二,引子:重載(overload),重寫(override),屏蔽(hide)

重載(overload):在相同範圍內,函數名稱相同,參數或常量性(const)不同的相關函數稱為重載。重載函數之間的區分主要在參數和常量性(const)的不同上,若僅僅是傳回值或修飾符 virtual,public/protected/private的不同不被視為重載函數(無法通過編譯)。不同參數是指參數的個數或類型不同,而類型不同是指各類型之間不能進行隱藏類型轉換或不多於一次的使用者自訂類型轉換(關於類型轉換,請參考前文:類型轉型(Type Casting))。當調用發生時,編譯器在進行重載決議時根據調用所提供的參數來選擇首選的函數。

重寫(override):衍生類別重寫基類中同名同參數同傳回值的函數(通常是虛函數,這是推薦的做法)。同樣重寫的函數可以有不同的修飾符virtual,public/protected/private。

屏蔽(hide):一個內部範圍(衍生類別,嵌套類或名字空間)內提供一個同名但不同參數或不同常量性(const)的函數,使得外圍範圍的同名函數在內部範圍不可見,編譯器在進行名字尋找時將在內部範圍找到該名字從而停止去外圍範圍尋找,因而屏蔽外圍範圍的同名函數。

(註:編譯器在決定哪一個函數應該被調用時,依次要做三件事:名字尋找,重載決議,訪問性檢查。後續文章將詳細介紹這個決定過程。)

下面來分析樣本:

class Base{public:    virtual void f() { cout << "Base::f()" << endl; }    void f(int) { cout << "Base::f(int)" << endl; }    virtual void f(int) const { cout << "Base::f(int) const" << endl; }    virtual void f(int *) { cout << "Base::f(int *)" << endl; }};class Derived : public Base{public:    virtual void f() { cout << "Derived::f()" << endl; }    virtual void f(char) { cout << "Derived::f(char)" << endl; }};const Base b;b.f(10);Derived d;int value = 10;d.f();d.f('A');d.f(10);//d.f(&value);//編譯報錯

 

在上面代碼中,Base 中的一系列名為 f 的函數在同一範圍內,且同名不同參或不同常量性,故為重載函數;而 Derived 中的 f() 則是重寫了基類同名同參的 f();而 Derived 中的 f(char) 則屏蔽了 Base 中所有的同名函數。

所以上面代碼的執行結果是:

Base::f(int) const
Derived::f()
Derived::f(char)
Derived::f(char)

對 d.f(10); 這兩個調用,看似基類 Base 中有更好的匹配,但實際上由於編譯器在進行名字尋找時,首先在 Derived 類範圍中進行尋找,找到  f(char) 就停止去基類範圍中尋找,因而基類的所有同名函數沒有機會進入重載決議,因而被屏蔽了。因此編譯器將 10 隱式轉型為 char 調用 Derived 中的 f(char)。至此,聰明的你應該很容易明白為什麼 d.f(&value);  無法通過編譯了吧(VS編譯器的提示資訊很給力)。

 

三,函數繼承規則:

鑒於繼承基類的函數有如此隱晦的概念需要弄懂,再加上 virtual 函數,public/protected/private 繼承等等,更是增加了理解一個類介面的難度(因為你不僅要看類自身的介面,還有向上追溯所有基類的介面,以及是以何種方式繼承基類的介面等等)。因此,C++裡面有很多針對類介面繼承的慣用法:

1,優先使用組合而非繼承。既然繼承代價如此之大,那麼最好的就是不繼承唄。當然不是說完全不用繼承,只有在存在明確的“IS-A”關係時,繼承的好處才會顯現出來(可以用多態-但要遵循 Liskov 替換原則);而其他情況下(”HAS-A”或“Is-implemented-in-terms-of”)應毫不猶豫地使用組合,而且要優先使用 PIMPL(Point to implementation) 手法(後續文章會介紹這個慣用法)來使用組合。

2,純虛函數繼承規則-聲明純虛函數的目的是讓衍生類別來繼承函數介面而非實現,使得純虛函數就像Java或C#中的 interface 一樣。唯一的例外就是需要純解構函式提供實現(避免資源泄漏)。

3,非純虛函數繼承規則-聲明非純虛函數的目的是讓衍生類別繼承函數介面及預設實現。但這是一種欠佳的做法,因為預設實現能讓新加入的沒有重寫該實現的衍生類別通過編譯並運行,而預設實現有可能並不適用於新加入的衍生類別,對此編譯器並不會提供任何資訊(警告都沒一個)。為了應對這一潛在的陷阱,誕生了另一個規則:”純虛函數的聲明提供介面,純虛函數的實現提供預設實現;衍生類別必須重寫該介面,但在實現時可以調用基類的預設實現。“

如下代碼所示:

class Base{public:    virtual void f() = 0;};void Base::f(){    cout << "Base::f() default implement." << endl;}class DerivedA : public Base{public:    virtual void f()    {        Base::f();    }};class DerivedB : public Base{public:    virtual void f()    {        cout << "DerivedB::f() override." << endl;    }};

 

4,非虛函數繼承規則-永遠也不要重寫基類中的非虛函數。非虛函數的目的就是為了讓衍生類別繼承基類的強制性實現,它並不希望被衍生類別改寫。

5,盡量不要屏蔽外圍範圍(包括繼承而來的)名字。屏蔽所帶來的隱晦難以理解等問題在前面已有描述。

如果沒得選擇(我還真沒想到有什麼情境會出現這種情況,通常換個名字都是可行的)必須重新定義或重寫基類中同名函數,那麼你應該為每一個原本會被隱藏的名字引入一個 using 聲明或使用轉交函數(衍生類別定義同名同參函數,在該函數內部調用基類的同名同參函數)來使這些名字在衍生類別的範圍中可見。(Effective C++ 條款33)。

該規則應用如下:

class Base{public:    virtual void f() { cout << "Base::f()" << endl; }    void f(int) { cout << "Base::f(int)" << endl; }    virtual void f(int) const { cout << "Base::f(int) const" << endl; }    virtual void f(int *) { cout << "Base::f(int *)" << endl; }};class Derived : public Base{public:    using Base::f;    virtual void f() { cout << "Derived::f()" << endl; }    //virtual void f(char) { cout << "Derived::f(char)" << endl; }};const Base b;b.f(10);Derived d;int value = 10;d.f();d.f('A');d.f(10);d.f(&value);

運行得到的結果為:

Base::f(int) const
Derived::f()
Base::f(int)
Base::f(int)
Base::f(int *)

在這裡,因為使用了 using Base::f; ,因此基類中的所有名字 f 對子類來說都是可見的,所有 d.f(&value); 等均可通過編譯運行了。再次提醒:這是一種非常不好的做法。

6,基類的解構函式應當為虛函數,以避免資源泄漏。

假設有如下情況,帶非虛解構函式的基類指標 pb 指向一個衍生類別對象 d,而衍生類別在其解構函式中釋放了一些資源,如果我們 delete pb; 那麼衍生類別對象的解構函式就不會被調用,從而導致資源泄漏發生。因此,應該聲明基類的解構函式為虛函數。

7,避免 private 繼承 – private 繼承通常意味著根據某物實現出(Is-implemented-in-terms-of),此種情況下使用基類與衍生類別這樣的術語並不太合適,因為它不滿足 Liskov 替換原則,並且從基類繼承而來的所有介面均為私人的,外部不可訪問。private 繼承可用 PIMPL 手法取代。

文中已經兩次提到 PIMPL 利器,在這裡就 private 繼承先給出一個樣本,以後再詳述 PIMPL 的好處。

原先使用 private 繼承:

class SomeClass{public:    void DoSomething(){}};class OtherClass : private SomeClass{private:    void DoSomething(){}};

使用 PIMPL 手法替代:

class SomeClass{public:    void DoSomething(){}};class OtherClass{public:    OtherClass();    ~OtherClass();    void DoSomething();private:    SomeClass * pImpl;};OtherClass::OtherClass(){    pImpl = new SomeClass();}OtherClass::~OtherClass(){    delete pImpl;}void OtherClass::DoSomething(){    pImpl->DoSomething();}

 

8,不要改寫繼承而來的預設參數值。前面已經說到非虛函數繼承是種不好的做法,所以在這裡的焦點就放在繼承一個帶有預設參數值的虛函數上了。為什麼改寫繼承而來的預設參數值不好呢?因為虛函數是動態綁定的,而預設參數值卻是靜態繫結的,這樣你在進行多態調用時:函數是由動態類型決定的,而其預設參數卻是由靜態類型決定的,違反直覺。

有代碼有真相:

class Base{public:    // 前面的樣本為了簡化代碼沒有遵循虛解構函式規則,在這裡說明下    virtual ~Base() {};     virtual void f(int defaultValue = 10)    {        cout << "Base::f() value = " << defaultValue << endl;    }};class Derived : public Base{public:    virtual void f(int defaultValue = 20)    {        cout << "Derived::f() value = " << defaultValue << endl;    }};

這段代碼的輸出為:

Derived::f() value = 10

調用的是動態類型 d -衍生類別 Derived的函數介面,但預設參數值卻是由靜態類型 pb-基類 Base 的函數介面決定的,這等隱晦的細節很可能會浪費你一下午來調試,所以還是早點預防為好。

9,還有一種流派認為不應公開(public)除虛解構函式之外的虛函數介面,而應公開一個非虛函數,在該非虛函數內 protected/private 的虛函數。這種做法是將介面何時被調用(非虛函數)與介面如何被實現(虛函數)分離開來,以達到更好的隔離效果。在設計模式上,這是一種策略模式。通常在非虛函數內內聯調用(直接在標頭檔函數申明處實現就能達到此效果)虛函數,所以在效率上與直接調用虛函數相比不相上下。

譬如:

class Base{public:    virtual ~Base() {}        void DoSomething()    {        StepOne();        StepTwo();    }private:    virtual void StepOne() = 0;    virtual void StepTwo() = 0;};class Derived : public Base{private:    virtual void StepOne()    {        cout << "Derived StepOne: do something." << endl;    }    virtual void StepTwo()    {        cout << "Derived StepTwo: do something." << endl;    }};

 

四,後記

C++ 陷阱特別多,學好用好 C++ 不容易,但只要把 OO 設計原則牢記在心頭,多見識些 C++ 慣用手法,C++ 的威力就能很好的展現出來。

 

五,引用

Effective C++ 條款 32 ~ 39

More Effective C++ 條款20 ~ 25

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在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.