青青草原综合久久大伊人导航_色综合久久天天综合_日日噜噜夜夜狠狠久久丁香五月_热久久这里只有精品

羅朝輝(飄飄白云)

關注嵌入式操作系統,移動平臺,圖形開發。-->加微博 ^_^

  C++博客 :: 首頁 :: 新隨筆 :: 聯系 :: 聚合  :: 管理 ::
  85 隨筆 :: 0 文章 :: 169 評論 :: 0 Trackbacks

[深入理解C++(二)]理解接口繼承規則

 羅朝輝 ( http://m.shnenglu.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(intconst { 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(intconst { 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







posted on 2012-11-06 21:21 羅朝輝 閱讀(3260) 評論(5)  編輯 收藏 引用 所屬分類: C/C++

評論

# re: [深入理解C++(二)]理解接口繼承規則 2012-11-07 13:47 3tgame
"在上面代碼中,Base 中的一系列名為 f 的函數在同一作用域內,且同名不同參或不同常量性,故為重載函數;而 Derived 中的 f() 則是重寫了基類同名同參的 f();而 Derived 中的 f(char) 則屏蔽了 Base 中所有的同名函數。"

我怎么都找不到本文第一段程序哪里出現這個f(char) 函數?  回復  更多評論
  

# re: [深入理解C++(二)]理解接口繼承規則 2012-11-07 15:42 羅朝輝
@3tgame

多謝提醒,已經修改了。  回復  更多評論
  

# re: [深入理解C++(二)]理解接口繼承規則[未登錄] 2012-11-08 14:28 歲月漫步
C++中細節太多了  回復  更多評論
  

# re: [深入理解C++(二)]理解接口繼承規則 2013-09-13 16:39 李明
mark下!  回復  更多評論
  

# re: [深入理解C++(二)]理解接口繼承規則 2014-01-15 16:43 ss
最后一個例子, " 在設計模式上,這是一種策略模式 ".
我怎么看怎么像是 Template Method.

現在學著學著, 很多設計模式我都已經分辨不出來了... :(  回復  更多評論
  

青青草原综合久久大伊人导航_色综合久久天天综合_日日噜噜夜夜狠狠久久丁香五月_热久久这里只有精品
  • <ins id="pjuwb"></ins>
    <blockquote id="pjuwb"><pre id="pjuwb"></pre></blockquote>
    <noscript id="pjuwb"></noscript>
          <sup id="pjuwb"><pre id="pjuwb"></pre></sup>
            <dd id="pjuwb"></dd>
            <abbr id="pjuwb"></abbr>
            欧美日韩和欧美的一区二区| 亚洲国产合集| 美国十次了思思久久精品导航| 久久精品夜色噜噜亚洲a∨| 欧美亚洲一区二区三区| 久久成人精品无人区| 亚洲国产综合在线看不卡| 亚洲视频网在线直播| 欧美成人免费在线| 久久久久一区二区三区| 国产综合色一区二区三区 | 亚洲天堂第二页| 亚洲国产成人av好男人在线观看| 久久福利影视| 一区二区三区在线视频播放 | 免费欧美在线视频| 亚洲第一在线| 亚洲国产精品黑人久久久| 可以看av的网站久久看| 影音先锋亚洲一区| 亚洲第一天堂无码专区| 美女网站久久| 99re热这里只有精品免费视频| 亚洲国产你懂的| 欧美日韩国产限制| 亚洲一区免费在线观看| 亚洲一区二区三区高清不卡| 国产精品日韩在线| 欧美在线观看一区| 亚洲欧美日韩在线高清直播| 激情亚洲网站| 欧美大片在线观看| 欧美日韩亚洲一区| 亚洲欧美在线一区二区| 性色av香蕉一区二区| 一色屋精品视频在线看| 亚洲国产欧美一区二区三区丁香婷| 欧美激情va永久在线播放| 亚洲一区二区三区乱码aⅴ| 亚洲欧美在线免费| 亚洲第一区在线| 中文日韩在线视频| 尤物yw午夜国产精品视频明星 | 亚洲欧美一区二区三区久久| 激情欧美国产欧美| 亚洲国产精选| 国产精品亚洲а∨天堂免在线| 久久精品99久久香蕉国产色戒| 久久久噜噜噜久久中文字幕色伊伊| 另类亚洲自拍| 亚洲免费av电影| 久久精品二区亚洲w码| 日韩一级黄色片| 久久久国产精品一区二区三区| 一区二区欧美视频| 巨胸喷奶水www久久久免费动漫| 一本色道久久综合亚洲精品婷婷| 亚洲国产精品久久| 亚洲电影免费| 欧美日韩精选| 久久久久久网址| 欧美日韩精品二区第二页| 久久蜜桃香蕉精品一区二区三区| 欧美大学生性色视频| 欧美一区二区三区久久精品| 欧美.com| 久久久精品日韩| 欧美午夜精品电影| 欧美大片在线看免费观看| 国产女人18毛片水18精品| 亚洲国产免费| 亚洲成人原创| 欧美在线黄色| 性伦欧美刺激片在线观看| 欧美精品一区二区三区很污很色的| 久久精品国产精品亚洲| 欧美日本中文字幕| 亚洲高清自拍| 亚洲国产精品va在线观看黑人| 亚洲欧美亚洲| 亚洲欧美国产不卡| 欧美三级在线播放| 亚洲黄一区二区| 又紧又大又爽精品一区二区| 午夜欧美精品| 亚洲天堂免费在线观看视频| 欧美电影免费观看网站| 嫩草影视亚洲| 久久精品国产一区二区三区| 日韩一级精品| 欧美国产亚洲另类动漫| 欧美激情第六页| 亚洲国产精品嫩草影院| 久久艳片www.17c.com| 男人天堂欧美日韩| 在线高清一区| 久久精品一区二区三区不卡| 久久www免费人成看片高清 | 亚洲日本成人| 影音先锋中文字幕一区二区| 久久精品二区三区| 你懂的亚洲视频| 狠狠干综合网| 久久免费视频在线| 欧美韩日一区二区三区| 亚洲激情图片小说视频| 欧美电影电视剧在线观看| 国产精品女主播在线观看 | 男女激情视频一区| 性一交一乱一区二区洋洋av| 一本色道久久88综合亚洲精品ⅰ| 亚洲日本中文字幕| 亚洲美女视频在线免费观看| 日韩视频中午一区| 99热免费精品| 亚洲系列中文字幕| 亚洲精品久久久久久一区二区 | 久久久一本精品99久久精品66| 99精品热6080yy久久| 亚洲国产一区二区三区在线播| 久久久欧美精品sm网站| 乱中年女人伦av一区二区| 农夫在线精品视频免费观看| 美女精品在线观看| 欧美激情一区二区三区| 亚洲精品女av网站| 亚洲人成网站777色婷婷| 一区二区三区久久网| 亚洲自拍偷拍网址| 欧美高清hd18日本| 亚洲日韩欧美一区二区在线| 18成人免费观看视频| 亚洲免费视频网站| 国产欧美一区二区在线观看| 一区二区三区不卡视频在线观看| 亚洲欧美一区二区激情| 亚洲国产精品精华液2区45 | 一本色道久久88综合亚洲精品ⅰ| 欧美性大战久久久久| 亚洲欧美另类国产| 老色批av在线精品| 亚洲视频一区二区在线观看| 国际精品欧美精品 | 亚洲美女黄色| 国产一区二区三区四区三区四| 性欧美精品高清| 国模一区二区三区| 欧美r片在线| 国产精品久久久久久一区二区三区| 国产亚洲欧美中文| 日韩一区二区免费高清| 亚洲国产综合在线| 欧美日本三区| 亚洲在线国产日韩欧美| 欧美伊人久久久久久午夜久久久久| 一本一本a久久| 国产日韩欧美精品一区| 亚洲精品久久久久久久久| 国产精品高清免费在线观看| 欧美大片在线看| 久久久久久日产精品| 亚洲尤物视频网| 亚洲人成网站影音先锋播放| 男女激情久久| 久久精品中文字幕一区二区三区 | 欧美电影资源| 久久天堂成人| 久久久久久久久伊人| 亚洲综合色激情五月| 一本色道久久综合狠狠躁篇怎么玩| 精品不卡在线| 国产一区二区三区久久久| 国产精品丝袜白浆摸在线| 国产精品爱久久久久久久| 欧美护士18xxxxhd| 免费在线观看精品| 快播亚洲色图| 久久久久久久久久码影片| 久久成人av少妇免费| 欧美在线亚洲一区| 欧美一级免费视频| 欧美伊人久久| 久久久久久亚洲精品中文字幕 | 亚洲人成在线观看| 亚洲欧洲在线看| 亚洲欧洲另类国产综合| 日韩亚洲欧美一区二区三区| 亚洲欧洲在线观看| 亚洲精品视频在线看| 亚洲精品一区二区网址| 99国产一区| 国产精品99久久久久久www| 一区二区三区.www| 亚洲在线免费| 欧美一区二区三区免费视频| 久久人人97超碰国产公开结果 | 一区二区毛片| 亚洲免费在线视频一区 二区| 久久狠狠久久综合桃花| 久久婷婷色综合|