• <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>

            羅朝輝(飄飄白云)

            關(guān)注嵌入式操作系統(tǒng),移動(dòng)平臺(tái),圖形開(kāi)發(fā)。-->加微博 ^_^

              C++博客 :: 首頁(yè) :: 新隨筆 :: 聯(lián)系 :: 聚合  :: 管理 ::
              85 隨筆 :: 0 文章 :: 169 評(píng)論 :: 0 Trackbacks

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

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

            CC許可,轉(zhuǎn)載請(qǐng)注明出處

            一,前言

            在前一篇《[深入理解C++(一)]類型轉(zhuǎn)換(Type Casting)》中,我詳細(xì)講述了 C++ 中轉(zhuǎn)型動(dòng)作,以及使用規(guī)則。有網(wǎng)友說(shuō)應(yīng)該提及下《深度探索 C++ 對(duì)象模型》一書中的內(nèi)容,其實(shí)他的意思是,要是對(duì) C++ 對(duì)象的內(nèi)存布局不甚了解,就想要徹悟C++中的類型轉(zhuǎn)型,對(duì)象切割,虛函數(shù)調(diào)用等,猶如脫離了堅(jiān)實(shí)的根基,想去建空中閣樓。理解 C++ 對(duì)象的內(nèi)存布局對(duì)學(xué)會(huì) C++來(lái)說(shuō)至關(guān)重要,但我不打算寫 C++ 對(duì)象的內(nèi)存布局相關(guān)的文章,因?yàn)橐驹谇叭说募绨蛏希笈?a target="_blank">陳皓 已經(jīng)就這個(gè)主題寫了三篇圖文并茂的文章:

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

            (二),C++ 對(duì)象的內(nèi)存布局(上)

            (三),C++ 對(duì)象的內(nèi)存布局(下)

             

            在繼續(xù)閱讀本文之前,建議先閱讀這三篇文章,以更好地理解本系列文章。在接下來(lái)的內(nèi)容中,我將從重載,重寫,屏蔽等概念入手,引入眾多接口繼承規(guī)則。

             

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

            重載(overload):在相同作用域內(nèi),函數(shù)名稱相同,參數(shù)或常量性(const)不同的相關(guān)函數(shù)稱為重載。重載函數(shù)之間的區(qū)分主要在參數(shù)和常量性(const)的不同上,若僅僅是返回值或修飾符 virtual,public/protected/private的不同不被視為重載函數(shù)(無(wú)法通過(guò)編譯)。不同參數(shù)是指參數(shù)的個(gè)數(shù)或類型不同,而類型不同是指各類型之間不能進(jìn)行隱身類型轉(zhuǎn)換或不多于一次的用戶自定義類型轉(zhuǎn)換(關(guān)于類型轉(zhuǎn)換,請(qǐng)參考前文:類型轉(zhuǎn)型(Type Casting))。當(dāng)調(diào)用發(fā)生時(shí),編譯器在進(jìn)行重載決議時(shí)根據(jù)調(diào)用所提供的參數(shù)來(lái)選擇最佳匹配的函數(shù)。

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

            屏蔽(hide):一個(gè)內(nèi)部作用域(派生類,嵌套類或名字空間)內(nèi)提供一個(gè)同名但不同參數(shù)或不同常量性(const)的函數(shù),使得外圍作用域的同名函數(shù)在內(nèi)部作用域不可見(jiàn),編譯器在進(jìn)行名字查找時(shí)將在內(nèi)部作用域找到該名字從而停止去外圍作用域查找,因而屏蔽外圍作用域的同名函數(shù)。

            (注:編譯器在決定哪一個(gè)函數(shù)應(yīng)該被調(diào)用時(shí),依次要做三件事:名字查找,重載決議,訪問(wèn)性檢查。后續(xù)文章將詳細(xì)介紹這個(gè)決定過(guò)程。)

            下面來(lái)分析示例:

            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);//編譯報(bào)錯(cuò)

             

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

            所以上面代碼的執(zhí)行結(jié)果是:

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

            對(duì) d.f(10); 這兩個(gè)調(diào)用,看似基類 Base 中有更好的匹配,但實(shí)際上由于編譯器在進(jìn)行名字查找時(shí),首先在 Derived 類作用域中進(jìn)行查找,找到  f(char) 就停止去基類作用域中查找,因而基類的所有同名函數(shù)沒(méi)有機(jī)會(huì)進(jìn)入重載決議,因而被屏蔽了。因此編譯器將 10 隱式轉(zhuǎn)型為 char 調(diào)用 Derived 中的 f(char)。至此,聰明的你應(yīng)該很容易明白為什么 d.f(&value);  無(wú)法通過(guò)編譯了吧(VS編譯器的提示信息很給力)。

             

            三,函數(shù)繼承規(guī)則:

            鑒于繼承基類的函數(shù)有如此隱晦的概念需要弄懂,再加上 virtual 函數(shù),public/protected/private 繼承等等,更是增加了理解一個(gè)類接口的難度(因?yàn)槟悴粌H要看類自身的接口,還有向上追溯所有基類的接口,以及是以何種方式繼承基類的接口等等)。因此,C++里面有很多針對(duì)類接口繼承的慣用法:

            1,優(yōu)先使用組合而非繼承。既然繼承代價(jià)如此之大,那么最好的就是不繼承唄。當(dāng)然不是說(shuō)完全不用繼承,只有在存在明確的“IS-A”關(guān)系時(shí),繼承的好處才會(huì)顯現(xiàn)出來(lái)(可以用多態(tài)-但要遵循 Liskov 替換原則);而其他情況下(”HAS-A”或“Is-implemented-in-terms-of”)應(yīng)毫不猶豫地使用組合,而且要優(yōu)先使用 PIMPL(Point to implementation) 手法(后續(xù)文章會(huì)介紹這個(gè)慣用法)來(lái)使用組合。

            2,純虛函數(shù)繼承規(guī)則-聲明純虛函數(shù)的目的是讓派生類來(lái)繼承函數(shù)接口而非實(shí)現(xiàn),使得純虛函數(shù)就像Java或C#中的 interface 一樣。唯一的例外就是需要純析構(gòu)函數(shù)提供實(shí)現(xiàn)(避免資源泄漏)。

            3,非純虛函數(shù)繼承規(guī)則-聲明非純虛函數(shù)的目的是讓派生類繼承函數(shù)接口及默認(rèn)實(shí)現(xiàn)。但這是一種欠佳的做法,因?yàn)槟J(rèn)實(shí)現(xiàn)能讓新加入的沒(méi)有重寫該實(shí)現(xiàn)的派生類通過(guò)編譯并運(yùn)行,而默認(rèn)實(shí)現(xiàn)有可能并不適用于新加入的派生類,對(duì)此編譯器并不會(huì)提供任何信息(警告都沒(méi)一個(gè))。為了應(yīng)對(duì)這一潛在的陷阱,誕生了另一個(gè)規(guī)則:”純虛函數(shù)的聲明提供接口,純虛函數(shù)的實(shí)現(xiàn)提供默認(rèn)實(shí)現(xiàn);派生類必須重寫該接口,但在實(shí)現(xiàn)時(shí)可以調(diào)用基類的默認(rèn)實(shí)現(xiàn)。“

            如下代碼所示:

            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,非虛函數(shù)繼承規(guī)則-永遠(yuǎn)也不要重寫基類中的非虛函數(shù)。非虛函數(shù)的目的就是為了讓派生類繼承基類的強(qiáng)制性實(shí)現(xiàn),它并不希望被派生類改寫。

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

            如果沒(méi)得選擇(我還真沒(méi)想到有什么場(chǎng)景會(huì)出現(xiàn)這種情況,通常換個(gè)名字都是可行的)必須重新定義或重寫基類中同名函數(shù),那么你應(yīng)該為每一個(gè)原本會(huì)被隱藏的名字引入一個(gè) using 聲明或使用轉(zhuǎn)交函數(shù)(派生類定義同名同參函數(shù),在該函數(shù)內(nèi)部調(diào)用基類的同名同參函數(shù))來(lái)使這些名字在派生類的作用域中可見(jiàn)。(Effective C++ 條款33)。

            該規(guī)則應(yīng)用如下:

            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);

             

            運(yùn)行得到的結(jié)果為:

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

            在這里,因?yàn)槭褂昧?using Base::f; ,因此基類中的所有名字 f 對(duì)子類來(lái)說(shuō)都是可見(jiàn)的,所有 d.f(&value); 等均可通過(guò)編譯運(yùn)行了。再次提醒:這是一種非常不好的做法。

            6,基類的析構(gòu)函數(shù)應(yīng)當(dāng)為虛函數(shù),以避免資源泄漏。

            假設(shè)有如下情況,帶非虛析構(gòu)函數(shù)的基類指針 pb 指向一個(gè)派生類對(duì)象 d,而派生類在其析構(gòu)函數(shù)中釋放了一些資源,如果我們 delete pb; 那么派生類對(duì)象的析構(gòu)函數(shù)就不會(huì)被調(diào)用,從而導(dǎo)致資源泄漏發(fā)生。因此,應(yīng)該聲明基類的析構(gòu)函數(shù)為虛函數(shù)。

            7,避免 private 繼承 – private 繼承通常意味著根據(jù)某物實(shí)現(xiàn)出(Is-implemented-in-terms-of),此種情況下使用基類與派生類這樣的術(shù)語(yǔ)并不太合適,因?yàn)樗粷M足 Liskov 替換原則,并且從基類繼承而來(lái)的所有接口均為私有的,外部不可訪問(wèn)。private 繼承可用 PIMPL 手法取代。

            文中已經(jīng)兩次提到 PIMPL 利器,在這里就 private 繼承先給出一個(gè)示例,以后再詳述 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,不要改寫繼承而來(lái)的缺省參數(shù)值。前面已經(jīng)說(shuō)到非虛函數(shù)繼承是種不好的做法,所以在這里的焦點(diǎn)就放在繼承一個(gè)帶有缺省參數(shù)值的虛函數(shù)上了。為什么改寫繼承而來(lái)的缺省參數(shù)值不好呢?因?yàn)樘摵瘮?shù)是動(dòng)態(tài)綁定的,而缺省參數(shù)值卻是靜態(tài)綁定的,這樣你在進(jìn)行多態(tài)調(diào)用時(shí):函數(shù)是由動(dòng)態(tài)類型決定的,而其缺省參數(shù)卻是由靜態(tài)類型決定的,違反直覺(jué)。

            有代碼有真相:

            class Base
            {
            public:
                // 前面的示例為了簡(jiǎn)化代碼沒(méi)有遵循虛析構(gòu)函數(shù)規(guī)則,在這里說(shuō)明下
                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

            調(diào)用的是動(dòng)態(tài)類型 d -派生類 Derived的函數(shù)接口,但缺省參數(shù)值卻是由靜態(tài)類型 pb-基類 Base 的函數(shù)接口決定的,這等隱晦的細(xì)節(jié)很可能會(huì)浪費(fèi)你一下午來(lái)調(diào)試,所以還是早點(diǎn)預(yù)防為好。

            9,還有一種流派認(rèn)為不應(yīng)公開(kāi)(public)除虛析構(gòu)函數(shù)之外的虛函數(shù)接口,而應(yīng)公開(kāi)一個(gè)非虛函數(shù),在該非虛函數(shù)內(nèi) protected/private 的虛函數(shù)。這種做法是將接口何時(shí)被調(diào)用(非虛函數(shù))與接口如何被實(shí)現(xiàn)(虛函數(shù))分離開(kāi)來(lái),以達(dá)到更好的隔離效果。在設(shè)計(jì)模式上,這是一種策略模式。通常在非虛函數(shù)內(nèi)內(nèi)聯(lián)調(diào)用(直接在頭文件函數(shù)申明處實(shí)現(xiàn)就能達(dá)到此效果)虛函數(shù),所以在效率上與直接調(diào)用虛函數(shù)相比不相上下。

            譬如:

            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++ 陷阱特別多,學(xué)好用好 C++ 不容易,但只要把 OO 設(shè)計(jì)原則牢記在心頭,多見(jiàn)識(shí)些 C++ 慣用手法,C++ 的威力就能很好的展現(xiàn)出來(lái)。

             

            五,引用

            Effective C++ 條款 32 ~ 39

            More Effective C++ 條款20 ~ 25







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

            評(píng)論

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

            我怎么都找不到本文第一段程序哪里出現(xiàn)這個(gè)f(char) 函數(shù)?  回復(fù)  更多評(píng)論
              

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

            多謝提醒,已經(jīng)修改了。  回復(fù)  更多評(píng)論
              

            # re: [深入理解C++(二)]理解接口繼承規(guī)則[未登錄](méi) 2012-11-08 14:28 歲月漫步
            C++中細(xì)節(jié)太多了  回復(fù)  更多評(píng)論
              

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

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

            現(xiàn)在學(xué)著學(xué)著, 很多設(shè)計(jì)模式我都已經(jīng)分辨不出來(lái)了... :(  回復(fù)  更多評(píng)論
              

            久久综合综合久久综合| 久久精品99久久香蕉国产色戒 | 国产成人精品久久一区二区三区| 人人狠狠综合久久亚洲高清| 草草久久久无码国产专区| 精品久久久久久中文字幕| 国产精品久久久久久久久软件 | 日本WV一本一道久久香蕉| 欧美午夜A∨大片久久| 久久精品亚洲精品国产欧美| 久久精品成人欧美大片| 久久精品国产精品亚洲下载| 久久久久一本毛久久久| 中文国产成人精品久久亚洲精品AⅤ无码精品 | 亚洲精品无码久久久久久| 久久久久久午夜精品| 国产69精品久久久久久人妻精品| 麻豆av久久av盛宴av| 热re99久久精品国99热| 成人免费网站久久久| 久久久久久免费一区二区三区| 亚洲国产二区三区久久| 欧美精品福利视频一区二区三区久久久精品| 久久精品国产精品亚洲下载| 久久精品免费一区二区| 久久国产精品成人片免费| 香蕉久久夜色精品国产小说| 久久久99精品成人片中文字幕| 三级片免费观看久久| 成人午夜精品无码区久久| 国产精品久久久久无码av| 久久国产精品偷99| 亚洲精品国精品久久99热一| 青青草国产精品久久久久| 日本国产精品久久| 久久亚洲精精品中文字幕| 国产亚洲精午夜久久久久久| 久久午夜无码鲁丝片秋霞| 精品久久777| 无码人妻久久一区二区三区蜜桃 | 久久996热精品xxxx|