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

            洛譯小筑

            別來(lái)無(wú)恙,我的老友…
            隨筆 - 45, 文章 - 0, 評(píng)論 - 172, 引用 - 0
            數(shù)據(jù)加載中……

            [ECPP讀書筆記 條目32] 確保公共繼承以“A是一個(gè)B”形式進(jìn)行

            威廉 德門特在他的著作《幾人留守幾人眠》中講述了這樣一個(gè)故事:他在課堂上嘗試讓他的學(xué)生記住課程中最重要的那一部分。他和他的學(xué)生講,據(jù)說(shuō)普通的英國(guó)學(xué)生僅僅能記得黑斯廷斯戰(zhàn)役發(fā)生于1066年。德門特強(qiáng)調(diào),如果有一個(gè)學(xué)生只記住了一點(diǎn)點(diǎn),他(她)記住的便是1066這個(gè)年號(hào)。德門特繼續(xù)講,對(duì)于他的課堂上的學(xué)生,只有幾條核心的信息,其中還包括一條有趣的結(jié)論——“安眠藥最終會(huì)導(dǎo)致失眠”。他請(qǐng)求他的學(xué)生們一定要記住這些核心信息,即使把課堂中討論的所有其他的內(nèi)容都忘光也可以,整個(gè)學(xué)期他都為學(xué)生反復(fù)重復(fù)這些核心信息。

            在學(xué)期末,期末考試的最后一道題是“請(qǐng)寫下這一學(xué)期中讓你銘記一生的一件東西。”當(dāng)?shù)麻T特閱卷的時(shí)候,差點(diǎn)兒沒昏過去。幾乎所有的學(xué)生不約而同地寫下了“1066”。

            因此,我“誠(chéng)惶誠(chéng)恐”地向各位講述C++面向?qū)ο缶幊讨凶顬橹匾]有之一)的一條原則:公共繼承意味著“A是一個(gè)B”關(guān)系。這條原則一定要深深的印在腦子里。

            如果你編寫了B類(Base,基類),并編寫了由其派生出的D類(Derived,派生類),那么你就告訴了C++編譯器(以及代碼的讀者),每一個(gè)D類型的對(duì)象同時(shí)也是B類型的,但是反過來(lái)不成立。你要表達(dá)的是:B表示比D更加一般化的內(nèi)容,而D則表示比B更加具體化的內(nèi)容。另外我們還強(qiáng)調(diào)如果一個(gè)B類型的對(duì)象可以在某處使用時(shí),那么D類型的對(duì)象一定可以在此使用。這是因?yàn)镈類型的每個(gè)對(duì)象一定是B類型的。反之,如果某一刻你需要一個(gè)D類型的對(duì)象,那么一個(gè)B類型的對(duì)象則不一定能滿足要求:每個(gè)D都是一個(gè)B,但反之不然。

            C++嚴(yán)格按上述方式解釋公共繼承。請(qǐng)參見下面的示例:

            class Person {...};

            class Student: public Person {...};

            我們從生活的經(jīng)驗(yàn)中可以得知:每個(gè)學(xué)生都是一個(gè)人,但是并不是每個(gè)人都是學(xué)生。上面的代碼精確的體現(xiàn)了這一層次結(jié)構(gòu)。我們期望“人”的每一條屬性對(duì)“學(xué)生”都適用(比如人有出生日期,學(xué)生也有)。但是對(duì)學(xué)生能成立的屬性對(duì)于一般的人來(lái)說(shuō)并不一定成立(比如一個(gè)學(xué)生被某所大學(xué)錄取了,但不是每個(gè)人都會(huì)去上大學(xué))。人的概念比學(xué)生的概念更加寬泛,而學(xué)生是一類特殊的人。

            在C++領(lǐng)域中,一切需要使用Person類型參數(shù)(或指向Person的指針或引用)的函數(shù)同樣能夠接受Student對(duì)象(或指向Student的指針或引用):

            void eat(const Person& p);        // 人人都會(huì)吃飯

             

            void study(const Student& s);     // 只有學(xué)生會(huì)學(xué)習(xí)

             

            Person p;                          // p是一個(gè)人

            Student s;                         // s是一個(gè)學(xué)生

             

            eat(p);                            // 正確,p是一個(gè)人

             

            eat(s);                            // 正確, s是一個(gè)學(xué)生,

                                               // 同時(shí)一個(gè)學(xué)生是一個(gè)人

             

            study(s);                          // 正確

             

            study(p);                          // 錯(cuò)誤!p不一定是學(xué)生

            這一點(diǎn)僅僅在公共繼承的情況下成立。只有在Student是由Person公共派生而來(lái)時(shí),C++才會(huì)按剛才描述的情景運(yùn)行。私有繼承將引入一個(gè)全新的議題(我們?cè)跅l目39中討論)。受保護(hù)的繼承我至今也沒有弄明白,暫且不談。

            公共繼承和“A是一個(gè)B”的等價(jià)性聽上去很簡(jiǎn)單,但是有時(shí)你的直覺會(huì)誤導(dǎo)你。比如說(shuō),“企鵝是鳥”這是千真萬(wàn)確的,同時(shí),“鳥會(huì)飛”也是不爭(zhēng)的事實(shí)。如果我們?cè)贑++中如此幼稚地表述這一情景,那么我們將得到:

            class Bird {

            public:

              virtual void fly();              // 鳥會(huì)飛

              ...

            };

             

            class Penguin:public Bird {        // 企鵝是鳥

              ...

            };

            瞬間我們陷入泥潭,因?yàn)檫@一層次結(jié)構(gòu)中,企鵝竟然會(huì)飛!這顯然是荒謬的。那么問題出在哪里呢?

            這種情況下,我們成為了一種不精確的語(yǔ)言——英語(yǔ)的受害者。當(dāng)我們說(shuō)“鳥類能夠飛行”時(shí),我們的意思并不是說(shuō)所有的鳥類都會(huì)飛。在一般情況下,只有擁有飛行能力的鳥類才能夠飛。假如我們的語(yǔ)言更加精確些,我們就能認(rèn)識(shí)到世界上還存在著一些不會(huì)飛的鳥類,我們也就能構(gòu)建出下面的層次結(jié)構(gòu),這樣的機(jī)構(gòu)才更加貼近真實(shí)世界:

            class Bird {

              ...                              // 不聲明任何飛行函數(shù)

            };

             

            class FlyingBird: public Bird {

            public:

              virtual void fly();

              ...

            };

             

            class Penguin: public Bird {

             

              ...                              // 不聲明任何飛行函數(shù)

             

            };

            這一層次結(jié)構(gòu)比原先設(shè)計(jì)的更加忠實(shí)于我們所了解的世界。

            到目前為止,上文的飛禽問題尚未徹底明了,因?yàn)樵谝恍┸浖到y(tǒng)中,區(qū)分鳥類是否可以飛行這項(xiàng)工作是沒有意義的,如果你的程序主要是關(guān)于鳥類的喙和翅膀,而與飛行沒有什么關(guān)系,那么原先的2個(gè)類的層次結(jié)構(gòu)就可以滿足要求了。這里也很清晰的反映出了這一哲理:凡事并不存在一勞永逸的解決方案。對(duì)軟件系統(tǒng)而言,最好的設(shè)計(jì)一定會(huì)考慮到這個(gè)系統(tǒng)是用來(lái)做什么的,無(wú)論是現(xiàn)在還是未來(lái),如果你的程序?qū)︼w行的問題一無(wú)所知,并且也不準(zhǔn)備去了解,那么忽略飛行特性的設(shè)計(jì)方案很可能就是完美的。事實(shí)上,這樣做要比將兩者區(qū)分開的設(shè)計(jì)方案更好些,因?yàn)槟阏谀M的世界中很可能不會(huì)存在這一機(jī)制。

            對(duì)于解決上文中的“白馬非馬”問題,還存在另外一個(gè)思考方法。那就是為企鵝重新定義fly函數(shù),從而讓其產(chǎn)生一個(gè)運(yùn)行時(shí)錯(cuò)誤:

            void error(const std::string& msg);    // 定義的內(nèi)容在其他地方

             

            class Penguin: public Bird {

            public:

              virtual void fly() { error("嘗試讓一只企鵝飛行!”);}

             

              ...

             

            };

            一定要認(rèn)識(shí)到:這樣做不一定能達(dá)到預(yù)期效果。因?yàn)檫@并不是說(shuō)“企鵝不會(huì)飛”,而是說(shuō)“企鵝會(huì)飛,但是在它嘗試飛行時(shí)出錯(cuò)了”。這一點(diǎn)很重要。

            如何找出兩者的區(qū)別呢?我們從捕獲錯(cuò)誤的時(shí)機(jī)入手:“企鵝不會(huì)飛”的指令可以由編譯器做出保證,但是對(duì)于“企鵝在嘗試飛行時(shí)出錯(cuò)”這一規(guī)則的違背只能夠在運(yùn)行時(shí)捕獲。

            為了表達(dá)這一約束,“企鵝不能飛行——句號(hào)”,你要確認(rèn)企鵝對(duì)象一定沒有飛行函數(shù)定義:

            class Bird 

              ...                              // 不聲明任何飛行函數(shù)

            };

             

            class Penguin: public Bird {

              ...                              // 不聲明任何飛行函數(shù)

            };

            現(xiàn)在,如果你嘗試讓一個(gè)企鵝飛行,那么編譯器將對(duì)你的侵犯行為做出抗議:

            Penguin p;

            p.fly();                           // 錯(cuò)誤!

            如果你適應(yīng)了“產(chǎn)生運(yùn)行時(shí)錯(cuò)誤”的方法,上文代碼的行為則與你所了解的大相徑庭。使用上文中的方法,編譯器不會(huì)對(duì)p.fly的調(diào)用做出任何反應(yīng)。條目18中解釋了好的接口設(shè)計(jì)能夠防止非法代碼得到編譯。因此你最好使用在編譯室拒絕企鵝嘗試飛行的設(shè)計(jì)方案,而不是僅僅在運(yùn)行時(shí)捕獲錯(cuò)誤。

            可能你承認(rèn)你的鳥類學(xué)知識(shí)并不豐富,但對(duì)于初級(jí)幾何你還是有信心的吧,讓我們拿長(zhǎng)方形和正方形再舉一個(gè)例子,這沒有什么復(fù)雜的吧?

            好,請(qǐng)回答一個(gè)簡(jiǎn)單的問題:Square(正方形)類是否應(yīng)該公共繼承自Rectangle(長(zhǎng)方形)類?


            你會(huì)說(shuō):“當(dāng)然可以了!正方形就是一個(gè)長(zhǎng)方形,這地球人都知道。但反過來(lái)就不成立了。”這一點(diǎn)至少在學(xué)校里是正確的,但我們都已經(jīng)不是小學(xué)生了,請(qǐng)考慮下面的代碼:

            class Rectangle {

            public:

              virtual void setHeight(int newHeight);

              virtual void setWidth(int newWidth);

             

              virtual int height() const;      // 返回當(dāng)前值

              virtual int width() const;

             

              ...

            };

             

            void makeBigger(Rectangle& r)     // 增加r面積的函數(shù)

            {

              int oldHeight = r.height();

             

              r.setWidth(r.width() + 10);      // r的寬增加10

             

              assert(r.height() == oldHeight); // 斷言r的高不變

            }

            顯然地,這里的判斷永遠(yuǎn)不會(huì)失敗,makeBigger僅僅改變了r的寬,它的高始終沒有改變。

            現(xiàn)在請(qǐng)觀察下面的代碼,其中使用了公共繼承,從而使得正方形得到與長(zhǎng)方形一致的待遇:

            class Square: public Rectangle {...};

             

            Square s;

            ...

             

            assert(s.width() == s.height());  // 這對(duì)所有的正方形都成立

             

            makeBigger(s);                     // 根據(jù)繼承關(guān)系,s是一個(gè)長(zhǎng)方形

                                               // 因此我們可以增加它的面積

             

            assert(s.width() == s.height());  // 對(duì)所有的正方形也應(yīng)成立

            第二次判斷同樣不應(yīng)該出錯(cuò),這也是十分明顯的。因?yàn)檎叫蔚亩x要求長(zhǎng)寬值永遠(yuǎn)相等。

            但是現(xiàn)在我們又遇到了一個(gè)問題,我們?nèi)绾螀f(xié)調(diào)下面的斷言呢?

            在調(diào)用makeBigger之前,s的高與寬相等;

            makeBigger內(nèi)部,s的寬值該變了,但是高沒有;

            makerBigger返回后,s的高與寬又相等了。(請(qǐng)注意:s是通過引用傳入makeBigger中的,因此makeBigger改變的是s本身,而不是s的副本。)

            歡迎來(lái)到公共繼承的美妙世界。在這里,你在其他領(lǐng)域(包括數(shù)學(xué))所積累的經(jīng)驗(yàn)也許不會(huì)按部就班地奏效。這種情況下最基本的問題就是:一些對(duì)長(zhǎng)方形可用的屬性(它的長(zhǎng)、寬可以分別修改),對(duì)于正方形而言并不適用(它的長(zhǎng)、寬必須保持一致)。但是,公共繼承要求對(duì)基類成立的一切對(duì)于派生類同樣應(yīng)該能夠成立——一切的一切!這種情況下,長(zhǎng)方形和正方形(以及條目38中集合、線性表)的實(shí)例都會(huì)遇到問題。因此,使用公共繼承來(lái)構(gòu)建它們之間的關(guān)系顯然是錯(cuò)誤的。編譯器會(huì)允許你這樣做,但是就像我們剛剛所看到的一樣,我們無(wú)法確保代碼是否能夠按要求運(yùn)行。這件事每一位程序員一定深有體會(huì)(往往比其他行業(yè)的人要深得多),因?yàn)樵S多情況下代碼能夠通過編譯并不意味著它能夠正常運(yùn)行。

            在我們投入面向?qū)ο蟪绦蛟O(shè)計(jì)的懷抱時(shí),多年積累的編程經(jīng)驗(yàn)難道成為了我們的絆腳石嗎?這一點(diǎn)你無(wú)需顧慮。舊有的知識(shí)依然是寶貴的,只是既然你已經(jīng)把繼承的概念添加進(jìn)你大腦中的設(shè)計(jì)方案庫(kù)中,你就應(yīng)該以全新的眼光來(lái)開拓自己的感官世界,從而使你在面對(duì)包含繼承的程序時(shí)不會(huì)迷失方向。假如有人向你展示了一個(gè)幾頁(yè)長(zhǎng)的程序,你也可以從企鵝繼承自鳥類、正方形繼承自長(zhǎng)方形這些示例所包含的理念中,找出同樣有趣的東西。這有可能是完成工作的正確途徑,只是這個(gè)可能性并不大。

            類間的關(guān)系并不僅限于“A是一個(gè)B”關(guān)系。另外還存在兩個(gè)內(nèi)部類關(guān)系,它們是:“A擁有一個(gè)B”、“A是以B的形式實(shí)現(xiàn)的”。這些關(guān)系將在條目38和條目39中講解。由于人們往往會(huì)將上面兩種關(guān)系其中之一錯(cuò)誤地構(gòu)造成“A是一個(gè)B”,因此隨之帶來(lái)的C++設(shè)計(jì)錯(cuò)誤比比皆是,所以你應(yīng)該確保對(duì)于這些關(guān)系之間的區(qū)別有著充分的理解,這樣你才能在C++中分別對(duì)這些關(guān)系做出最優(yōu)秀的構(gòu)造。

            時(shí)刻牢記

            公共繼承意味著A是一個(gè)B的關(guān)系。對(duì)于基類成立的一切都應(yīng)該適用于派生類,因?yàn)榕缮惖膶?duì)象就一個(gè)基類對(duì)象。

            posted on 2008-03-17 22:55 ★ROY★ 閱讀(2061) 評(píng)論(2)  編輯 收藏 引用 所屬分類: Effective C++

            評(píng)論

            # re: 【讀書筆記】[Effective C++第3版][第32條] 確保公共繼承以“A是一個(gè)B”形式進(jìn)行  回復(fù)  更多評(píng)論   

            終于更新了,好久沒更新哦。呵呵
            2008-03-18 18:32 | 酷勤網(wǎng)

            # re: 【讀書筆記】[Effective C++第3版][第32條] 確保公共繼承以“A是一個(gè)B”形式進(jìn)行  回復(fù)  更多評(píng)論   


            承蒙您關(guān)照
            期待您批評(píng)
            2008-03-18 22:03 | ★ROY★
            韩国无遮挡三级久久| 国产精品免费看久久久| 国产精品伊人久久伊人电影 | 亚洲欧洲久久久精品| 久久天天躁狠狠躁夜夜网站| 久久影院午夜理论片无码| 亚洲国产二区三区久久| 精品久久久无码21p发布 | 久久久久高潮综合影院| 久久精品中文闷骚内射| 国内高清久久久久久| 久久精品免费全国观看国产| 久久精品无码一区二区三区日韩 | 亚洲精品乱码久久久久久中文字幕| 国产精品VIDEOSSEX久久发布| 亚洲人成网站999久久久综合 | 亚洲精品无码久久久久sm| 97热久久免费频精品99| 亚洲精品无码久久千人斩| 91亚洲国产成人久久精品网址| 久久亚洲精品国产精品婷婷| segui久久国产精品| 久久精品无码一区二区无码| 亚洲欧美日韩精品久久亚洲区| 久久r热这里有精品视频| 色欲久久久天天天综合网| 内射无码专区久久亚洲| segui久久国产精品| 久久久精品一区二区三区| 狠狠色伊人久久精品综合网 | 久久综合久久美利坚合众国| 99精品久久精品一区二区| 国产精品热久久无码av| 久久久91精品国产一区二区三区| 精品久久久久久中文字幕大豆网| 亚洲天堂久久久| 精品久久久一二三区| 午夜精品久久久内射近拍高清| 思思久久精品在热线热| 日韩人妻无码一区二区三区久久99 | 久久久久久精品免费免费自慰|