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

            洛譯小筑

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

            [ECPP讀書筆記 條目7] 要將多態(tài)基類的析構(gòu)函數(shù)聲明為虛函數(shù)

            現(xiàn)在考慮一個計時器的問題,我們首先創(chuàng)建一個名為TimeKeeper的基類,然后在它的基礎(chǔ)上創(chuàng)建各種派生類,從而用不同手段來計時。由于計時有很多方式,所以這樣做是值得的:

            class TimeKeeper {

            public:

              TimeKeeper();

              ~TimeKeeper();

              ...

            };

             

            class AtomicClock: public TimeKeeper { ... };  // 原子鐘

            class WaterClock: public TimeKeeper { ... };   // 水鐘

            class WristWatch: public TimeKeeper { ... };   // 腕表

            許多客戶希望在訪問時間時不用關(guān)心計算的細(xì)節(jié),所以在此可以使用一個工廠函數(shù)來返回一個指向計時器對象的指針,工廠函數(shù)會返回一個基類指針,這個指針將指向一個新創(chuàng)建的派生類對象:

            TimeKeeper* getTimeKeeper();      // 返回一個繼承自TimeKeeper的動態(tài)分配的對象

            為了不破壞工廠函數(shù)的慣例,getTimeKeeper返回的對象將被放置在堆上,所以必須要在適當(dāng)?shù)臅r候刪除每一個返回的對象,從而避免內(nèi)存或者其他資源發(fā)生泄漏:

            TimeKeeper *ptk = getTimeKeeper(); // TimeKeeper層取得

                                               // 一個動態(tài)分配的對象

             

            ...                                // 使用這個對象

             

            delete ptk;                        // 釋放它,以防資源泄漏

            把釋放工作推卸給客戶將會帶來出錯的隱患,條目13中解釋了這一點。關(guān)于如何修改工廠函數(shù)的接口從而防止一般的客戶端錯誤發(fā)生,請參見條目18。但是這些議題在此都不是主要的,這一條目中我們主要討論的是上文中的代碼存在著的一個更為基本的弱點:即使客戶把每一件事都做得很完美,我們?nèi)詿o法預(yù)知程序會產(chǎn)生怎樣的行為。

            現(xiàn)在的問題是:getTimeKeeper返回一個指向某個派生類對象的指針(比如說AtomicClock),這個對象最終會通過一個基類指針得到刪除(比如說TimeKeeper*指針),而基類(TimeKeeper)有一個非虛析構(gòu)函數(shù)。這里埋藏著災(zāi)難,這是因為C++有明確的規(guī)則:如果希望通過一個指向基類的指針來刪除一個派生類對象,并且這一基類有一個非虛析構(gòu)函數(shù),結(jié)果將是未定義的。典型的后果就是,在運行時,派生類中新派生出的部分得不到銷毀。如果getTimeKeeper返回了一個指向AtomicClock對象的指針,那么這一對象中AtomicClock的部分(也就是AtomicClock類中新聲明的數(shù)據(jù)成員)有可能不會被銷毀掉,AtomicClock的析構(gòu)函數(shù)也可能不會得到運行。然而,這一對象中基類那一部分(也就是TimeKeeper的部分)很自然的會被銷毀掉,這樣便會產(chǎn)生一個古怪的“部分銷毀”的對象。用這種方法來泄漏資源、破壞數(shù)據(jù)結(jié)構(gòu)、浪費調(diào)試時間,實在是“再好不過”了。

            排除這一問題的方法很簡單:為基類提供一個虛擬的析構(gòu)函數(shù)。這時刪除一個派生類對象,程序就會精確地按你的需要運行了。整個對象都會得到銷毀,包括所有新派生的部分:

            class TimeKeeper {

            public:

              TimeKeeper();

              virtual ~TimeKeeper();

              ...

            };

             

            TimeKeeper *ptk = getTimeKeeper();

            ...

            delete ptk;                        // 現(xiàn)在,程序正常運轉(zhuǎn)

            通常情況下,像TimeKeeper這樣的基類會包含除析構(gòu)函數(shù)以外的虛函數(shù),這是因為虛函數(shù)的目的是允許派生類實現(xiàn)中對它們進(jìn)行自定義(參見條目34)。比如說,TimeKeeper類中可能存在一個虛函數(shù)getCurrentTime,它在不同的派生類中將有不同的實現(xiàn)方式。任何有虛函數(shù)的類幾乎一定都要包含一個虛析構(gòu)函數(shù)。

            如果一個類不包含虛函數(shù),通常情況下意味著它將不作為基類使用。當(dāng)一個類不作為基類時,將它的析構(gòu)函數(shù)其聲明為虛擬的通常情況下不是個好主意。請看下面的示例,這個類代表二維空間中的點:

            class Point {                      // 2D的點

            public:

              Point(int xCoord, int yCoord);

              ~Point();

             

            private:

              int x, y;

            };

            在一般情況下,如果一個int占用32比特,一個Point對象便可以置入一個64位的寄存器中。而且,這樣的一個Point對象可以以一個64位數(shù)值的形式傳給其他語言編寫的函數(shù),比如C或者FORTRAN。然而如果Point的析構(gòu)函數(shù)是虛擬的,那么就是另一種情況了。

            虛函數(shù)的實現(xiàn)需要它所在的對象包含額外的信息,這一信息用來在運行時確定本對象需要調(diào)用哪個虛函數(shù)。通常,這一信息采取一個指針的形式,這個指針被稱為“vptr”(“虛函數(shù)表指針”,virtual table pointer)。vptr指向一個包含函數(shù)指針的數(shù)組,這一數(shù)組稱為“vtbl”(“虛函數(shù)表”,virtual table),每個包含虛函數(shù)的類都有一個與之相關(guān)的vtbl。當(dāng)一個虛函數(shù)被一個對象調(diào)用時,就用到了該對象的vptr所指向的vtbl,在vtbl中查找一個合適的函數(shù)指針,然后調(diào)用相應(yīng)的實函數(shù)。

            虛函數(shù)實現(xiàn)的細(xì)節(jié)并不重要。重要的僅僅是,如果Point類包含一個虛函數(shù),這一類型的對象將會變大。在一個32位的架構(gòu)中,Point對象將會由64位(兩個int大小)增長至96位(兩個int加一個vptr);在64位架構(gòu)中,Point對象將由64位增長至128位。這是因為指向64位架構(gòu)的指針有64位大小。可以看到,為Point添加一個vptr將會使對象增大50-100%!這樣,一個64位的寄存器便容不下一個Point對象了。而且,此時C++版本的Point對象便不再與其它語言(比如C語言)有同樣的結(jié)構(gòu),這是因為其它語言很可能沒有vptr的概念。于是,除非你顯式增補(bǔ)一個vptr的等價物(但這是這種語言的實現(xiàn)細(xì)節(jié),而且不具備可移植性),否則Point對象便無法與其它語言編寫的函數(shù)互通。

            不得不承認(rèn),無故將所有的析構(gòu)函數(shù)聲明為虛擬的,與從不將它們聲明為虛函數(shù)一樣糟糕,這一點至關(guān)重要。實際上,許多人總結(jié)出一條解決途徑:當(dāng)且僅當(dāng)類中至少包含一個虛函數(shù)時,要聲明一個虛析構(gòu)函數(shù)。

            甚至在完全沒有虛函數(shù)的類里,你也可能會被非虛擬的構(gòu)造函數(shù)所糾纏。比如說,雖然標(biāo)準(zhǔn)的string類型不包含虛函數(shù),但是誤入歧途的程序員有些時候還是會將其作為基類:

            class SpecialString: public std::string {

                                               // 這不是個好主意!

                                               // std::string 有一個非虛擬的析構(gòu)函數(shù)

              ...

            };

            乍一看,這樣的代碼似乎沒什么問題,但是如果在某應(yīng)用程序里,你不知出于什么原因希望將一個SpecialString指針轉(zhuǎn)型為string指針,同時你又對這個string指針使用了delete,你的程序會立刻陷入未定義行為:

            SpecialString *pss =   new SpecialString("Impending Doom");

             

            std::string *ps;

            ...

             

            ps = pss;                          // SpecialString* std::string*

            ...

             

            delete ps;                         // 未定義行為!在實踐中*psSpecialString

                                               // 部分資源將會泄漏,這是因為SpecialString

                                               // 的析構(gòu)函數(shù)沒有被調(diào)用。

            對于任意沒有虛析構(gòu)函數(shù)的類而言,上面的分析都成立,包括所有的STL容器類型(比如vectorlistsettr1::unordered_map(參見條目54),等等)。如果你曾經(jīng)繼承過一個標(biāo)準(zhǔn)容器或者其他任何包含非虛析構(gòu)函數(shù)的類,一定要打消這種想法!(遺憾的是,C++沒有提供類似Java中的final類或C#中的sealed類那種防止繼承的機(jī)制)

            在個別情況下,為一個類提供一個純虛析構(gòu)函數(shù)是十分方便的。你可以回憶一下,純虛函數(shù)會使其所在的類成為抽象類——這種類不可以實例化(也就是說,你無法創(chuàng)建這種類型的對象)。然而某些時刻,你希望一個類成為一個抽象類,但是你有沒有任何純虛函數(shù),這時候要怎么辦呢?因為抽象類應(yīng)該作為基類來使用,而基類應(yīng)該有虛析構(gòu)函數(shù),又因為純虛函數(shù)可以造就一個抽象類,那么解決方案就顯而易見了:如果你希望一個類成為一個抽象類,那么在其中聲明一個純虛析構(gòu)函數(shù)。下邊是示例:

            class AWOV {                       // AWOV = "Abstract w/o Virtuals"

            public:

              virtual ~AWOV() = 0;             // 聲明純虛析構(gòu)函數(shù)

            };

            這個類有一個純虛函數(shù),所以它是一個抽象類,同時它擁有一個虛析構(gòu)函數(shù),所以你不需要擔(dān)心析構(gòu)函數(shù)的問題。然而這里還是有一個別扭的地方:你必須為純虛析構(gòu)函數(shù)提供一個定義:

            AWOV::~AWOV() {}                   // 純虛析構(gòu)函數(shù)的定義

            析構(gòu)函數(shù)的工作方式是這樣的:首先調(diào)用最后派生出的類的析構(gòu)函數(shù),然后依次調(diào)用上一層基類的析構(gòu)函數(shù)。由于當(dāng)調(diào)用一個AWOV的派生類的析構(gòu)函數(shù)時,編譯器會自動調(diào)用~AWOV,因此你必須為~AWOV提供一個函數(shù)體。否則連接器將會報錯。

            為基類提供虛析構(gòu)函數(shù)的原則僅對多態(tài)基類(這種基類允許通過其接口來操控派生類的類型)有效。我們說TimeKeeper是一個多態(tài)基類,這是由于即使我們手頭只有TimeKeeper指向它們的指針,我們?nèi)钥梢詫?span style="font-family:Consolas;Courier New";">AtomicClock和WaterClock進(jìn)行操控。

            并不是所有的基類都要具有多態(tài)性。比如說,標(biāo)準(zhǔn)string類型、STL容器都不用作基類,因此它們都不具備多態(tài)性。另外有一些類是設(shè)計用作基類的,但是它們并未被設(shè)計成多態(tài)類。這些類(例如條目6中的Uncopyable和標(biāo)準(zhǔn)類中的input_iterator_tag(參見條目47))不允許通過其接口來操控它的派生類。因此,它們并不需要虛析構(gòu)函數(shù)。

            時刻牢記

            應(yīng)該為多態(tài)基類聲明虛析構(gòu)函數(shù)。一旦一個類包含虛函數(shù),它就應(yīng)該包含一個虛析構(gòu)函數(shù)。

            如果一個類不用作基類或者不需具有多態(tài)性,便不應(yīng)該為它聲明虛析構(gòu)函數(shù)。

            posted on 2007-04-20 21:59 ★ROY★ 閱讀(1363) 評論(2)  編輯 收藏 引用 所屬分類: Effective C++

            評論

            # re: 【翻譯】Effective C++ (第7條:要將多態(tài)基類的析構(gòu)函數(shù)聲明為虛函數(shù))  回復(fù)  更多評論   

            謝謝,對我非常有幫助~
            2008-09-25 16:13 | wb

            # re: 【翻譯】Effective C++ (第7條:要將多態(tài)基類的析構(gòu)函數(shù)聲明為虛函數(shù))  回復(fù)  更多評論   

            3ks
            2009-06-24 16:14 | Felix021
            久久久久亚洲AV成人网| 久久精品国产99久久丝袜| 久久综合狠狠综合久久| 精品综合久久久久久97超人 | 久久久久亚洲AV无码专区首JN| 国产成人综合久久精品红| 精品永久久福利一区二区| 99久久免费国产精品| 欧美日韩精品久久久免费观看| 久久久一本精品99久久精品88 | 久久99热这里只频精品6| 亚洲精品乱码久久久久久久久久久久 | 久久亚洲精品成人av无码网站| 777久久精品一区二区三区无码 | 国产午夜福利精品久久2021| 精品视频久久久久| 日韩精品久久久久久免费| 久久久精品日本一区二区三区| 亚洲AV日韩AV永久无码久久| 品成人欧美大片久久国产欧美 | 久久99中文字幕久久| 亚洲伊人久久成综合人影院| 国产成人无码久久久精品一| 污污内射久久一区二区欧美日韩 | 99久久免费国产精品| 久久精品蜜芽亚洲国产AV| 欧美久久久久久午夜精品| 亚洲国产精久久久久久久| 伊人久久大香线蕉av不变影院| 久久国产福利免费| 国产精久久一区二区三区| 久久久青草久久久青草| 久久久久久久久无码精品亚洲日韩 | 日本强好片久久久久久AAA | 亚洲精品蜜桃久久久久久| 亚洲国产香蕉人人爽成AV片久久| 亚洲成色999久久网站| 99久久精品影院老鸭窝| 国产99精品久久| 国产精品久久久久久福利69堂| 亚洲精品白浆高清久久久久久|