• <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讀書筆記 條目9] 永遠不要在構造或析構的過程中調用虛函數(shù)

            讓我們直切正題:在程序進行構造或析構期間,你絕不能調用虛函數(shù),這是因為這樣的調用并不會按你所期望的執(zhí)行,即使能夠順利執(zhí)行,你也不會覺得十分舒服。如果你曾經(jīng)是一個Java或C#的程序員,并且在最近期望返回C++的懷抱,那么請你格外留意本條目,因為在這一問題上,C++與其他語言走的是完全不同的兩條路線。

            假設有一個股票交易模擬系統(tǒng),你為它編寫了一個類的層次化結構,其中包括實現(xiàn)購買、拋售等功能的類。這類交易應該是可以審計的,這一點很重要,所以說每創(chuàng)建一次交易時,都應該在日志中創(chuàng)建一條審計相關內容的記錄。下面是一個看似合理的解決方案:

            class Transaction {                      // 所有交易的基類

            public:

              Transaction();

              virtual void logTransaction() const = 0; // 作類型相關的記錄

              ...

            };

             

            Transaction::Transaction()               // 基類構造函數(shù)的實現(xiàn)

            {

              ...

              logTransaction();                       // 最后,記錄這次交易

            }

             

            class BuyTransaction: public Transaction { // 派生類

            public:

              virtual void logTransaction() const;   // 當前類型交易是如何記錄的

              ...

            };

             

            class SellTransaction: public Transaction {    // 派生類

            public:

              virtual void logTransaction() const;   // 當前類型交易是如何記錄的

              ...

            };

            請考慮一下在下邊的代碼運行時會發(fā)生什么:

            BuyTransaction b;

            很明顯的是此時BuyTransaction的構造函數(shù)將被調用,但是,首先必須調用Transaction的構造函數(shù)。對于一個派生的對象,其基類那一部分會首先得到構造,然后才是派生類的部分。Transaction的構造函數(shù)中最后一行調用了虛函數(shù)logTransaction,意外的事情就從這里發(fā)生了:此處調用的是Translation版本的logTransaction函數(shù),而不是BuyTransaction版本的——即使此處創(chuàng)建的對象是BuyTransaction類型的。在基類部分的構造過程中,虛函數(shù)永遠也不會嘗試去匹配派生類部分。取而代之的是,對象仍然保持基類的行為。更隨意一點的說法是,在基類部分構造的過程中,虛函數(shù)并不會被構造。

            這一行為看上去匪夷所思,但是這里有很充足的理由來解釋它。由于基類的構造函數(shù)先于派生類運行,在基類構造函數(shù)運行的時候,派生類的數(shù)據(jù)成員還沒有被初始化。如果在基類構造函數(shù)向下匹配派生類時調用了虛函數(shù),那么基類的函數(shù)幾乎一定會調用局部數(shù)據(jù)成員,但此時這些數(shù)據(jù)成員此時尚未得到初始化。你的程序將會出現(xiàn)無盡的未定義行為,你也會在整夜受到瑣碎的調試工作的折磨。當一個對象中某些部分尚未初始化的時候,此時對其進行調用會存在內在的危險,所以C++不允許你這樣做。

            實際情況比上文介紹的更為基礎。對于一個派生類的對象來說,在其進行基類部分構造工作的時候,這一對象的類型就是基類的。不僅僅虛函數(shù)會解析為基類的,而且C++中“使用運行時類型信息”的部分(比如dynamic_cast(參見條目27)和typeid)也會將其看作基類類型的對象。在我們的示例中,當調用Transaction的構造函數(shù)以初始化一個BuyTransaction對象的基類部分時,這一對象是Transaction類型的。C++的任何一部分都會這樣處理,這種處理方式是有意義的:由于這個對象的BuyTransaction部分尚未得到初始化,所以假定它們不存在才是最安全的處理方法。對于一個派生類對象來說,只有派生類的構造函數(shù)開始執(zhí)行,這個對象才會變成該派生類的對象。

            對于析構過程可以應用同樣的推理方式。一旦派生類的析構函數(shù)運行完畢,對象中派生類的那一部分數(shù)據(jù)成員將取得未定義的值,所以C++會認為它們不再存在。在進入基類的析構函數(shù)時,這個對象將成為一個基類對象,C++的所有部分——包括虛函數(shù)、dynamic_cast等等——都會這樣對待該對象。

            在上文的示例代碼中,Transaction的構造函數(shù)對一個虛函數(shù)進行了一次直接調用,很顯然這樣做是違背本條中的指導方針的。這樣的違規(guī)實在太容易發(fā)現(xiàn)了,一些編譯器都會對其做出警告。(其他一些則不會。參見條目53對編譯器警告信息的討論)即使沒有警告,問題也一定會在運行之前變得很明顯,這是因為Transaction中的logTransaction函數(shù)是純虛函數(shù),除非它得到了定義(不像是真的,但存在這種可能,參見條目34),程序才有可能會得到連接,其他情況都會報錯:連接器無法找到必要的Transaction::logTransaction的具體實現(xiàn)。

            查找構造或析構過程中對虛函數(shù)的調用并不總是一帆風順的。如果Transaction擁有多個構造函數(shù),它們所進行的工作中有一部分是相同的,那么可以將這些公共的初始化代碼(包括對logTransaction的調用)放入一個私有的非虛擬的初始化函數(shù)中,這樣做可以避免代碼重復,從軟件工程角度來講這似乎是一個很好的做法,我們將這一函數(shù)命名為init

            class Transaction {

            public:

              Transaction()

              { init(); }                      // 調用非虛函數(shù)...

             

              virtual void logTransaction() const = 0;

              ...

             

            private:

              void init()

              {

                ...

                logTransaction();              // ...而它卻調用一個虛函數(shù)!

              }

            };

            這樣的代碼與前文中的版本使用的是同一理念,但是這樣做所帶來的危害更為隱蔽和嚴重,這是因為這樣的代碼會得到正常的編譯和連接而不會報錯。這種情況下,由于logTransactionTransaction中的一個純虛函數(shù),大多數(shù)運行時系統(tǒng)將會在調用這個純虛函數(shù)時中止程序(通常情況下會針對這一結果顯示出一個消息)。然而如果logTransaction是一個“正常的”虛函數(shù)(也就是說,不是純虛的),并且在Transaction中給出了一些實現(xiàn),那么此時將調用這一版本的logTranscation,程序將會“愉快地一路小跑”下去,至于為什么在創(chuàng)建派生類對象時會調用錯誤的logTransaction版本,程序可就不管這一套了。避免這類問題的唯一途徑就是:在正在創(chuàng)建或銷毀的對象的構造函數(shù)和析構函數(shù)中,確保永遠不要調用虛函數(shù),對于構造函數(shù)和析構函數(shù)所調用的所有函數(shù)都應遵守這一約定。

            那么,每當創(chuàng)建一個Transaction層次結構中的對象時,如何確保去調用正確的logTransaction版本呢?顯然地,在Transaction的構造函數(shù)中調用一個虛函數(shù)是一個錯誤的做法。

            為解決這一問題我們可以另辟蹊徑。方案之一就是:將Transaction中的logTransaction變?yōu)橐粋€非虛函數(shù),然后要求派生類的構造函數(shù)把必要的日志記錄傳遞給Transaction的構造函數(shù)。這個構造函數(shù)對于非虛logTransaction的調用就是安全的。就像這樣:

            class Transaction {

            public:

              explicit Transaction(const std::string& logInfo);

             

              void logTransaction(const std::string& logInfo) const;

              // 現(xiàn)在logTransaction是非虛函數(shù)

              ...

            };

             

            Transaction::Transaction(const std::string& logInfo)

            {

              ...

              logTransaction(logInfo);         // 現(xiàn)在調用的是一個非虛函數(shù)

            }

             

            class BuyTransaction: public Transaction {

            public:

             BuyTransaction( parameters )

             : Transaction(createLogString( parameters ))

              { ... }                          // 將記錄傳遞給基類構造函數(shù)

             

               ...

             

            private:

              static std::string createLogString( parameters );

            };

            換句話說,你不能使用虛函數(shù)在基類構造過程中向下調用派生類的部分,作為一種補償,你可以讓派生類將一些必要的構造信息向上傳遞給基類的構造函數(shù)。

            請注意上述示例里BuyTransaction類中(私有的)靜態(tài)函數(shù)createLogString的使用。這里使用了一個輔助函數(shù)創(chuàng)建一個值來傳遞給基類的構造函數(shù),通常情況下這樣做更為方便(而且更具備可讀性),這樣做使為基類提供所需信息的成員初始化表變得更加直觀。這是因為這樣做解決了“為基類提供所需信息的成員初始化表”不直觀的問題。意外調用初生的BuyTransaction對象中那些尚未初始化的數(shù)據(jù)成員是十分危險的,由于createLogString是靜態(tài)的,此處便不存在這一危險。這一點很重要,因為這些數(shù)據(jù)成員正處于未定義的狀態(tài),這一事實便解釋了為什么“在基類部分構造或析構期間調用虛函數(shù),不會在第一時間向下匹配派生類”。

            時刻牢記

            不要在構造和析構的過程中調用虛函數(shù),因為這樣的調用永遠不會轉向當前執(zhí)行的析構函數(shù)或構造函數(shù)更深層的派生類中執(zhí)行。

            posted on 2007-04-27 22:37 ★ROY★ 閱讀(1510) 評論(4)  編輯 收藏 引用 所屬分類: Effective C++

            評論

            # re: 【翻譯】Effective C++ (第9條:永遠不要在構造或析構的過程中調用虛函數(shù))  回復  更多評論   

            不錯, 最近也在看Effective C++
            2007-04-28 13:01 | Galaxy

            # re: 【翻譯】Effective C++ (第9條:永遠不要在構造或析構的過程中調用虛函數(shù))[未登錄]  回復  更多評論   

            不是嚴格按照effective c++的那些item順序來翻譯的吧~

            是說怎么不對應呢
            2007-04-29 11:58 | recorder

            # re: 【翻譯】Effective C++ (第9條:永遠不要在構造或析構的過程中調用虛函數(shù))  回復  更多評論   

            我翻的是,Effective C++第三版,您看的可能是第二版。所以有些不一樣。第三版比第二版要難一些。像/**/和//哪個好這樣的問題第三版中就忽略了。Meyers先生可能還會寫第四版,估計那時候難度又要上一個臺階。
            2007-04-29 16:55 | ★ROY★

            # re: 【翻譯】Effective C++ (第9條:永遠不要在構造或析構的過程中調用虛函數(shù))[未登錄]  回復  更多評論   

            呵呵,是說呢。寫得不錯,繼續(xù)努力~ 偶會一直關注~
            2007-05-05 17:59 | recorder
            久久久久久精品免费看SSS| 91久久精品视频| 久久久久高潮综合影院| 青青热久久国产久精品| 久久综合九色综合网站| 久久青草国产精品一区| 亚洲国产天堂久久久久久| 久久精品无码专区免费青青| 久久99精品久久久久久不卡 | 久久久国产精品网站| 久久国产精品偷99| 综合久久国产九一剧情麻豆| 久久久久一区二区三区| 久久精品久久久久观看99水蜜桃| 久久青青草原精品影院| 久久久久久久波多野结衣高潮 | 亚洲精品乱码久久久久久自慰| 一本色道久久综合亚洲精品| 精品久久久久一区二区三区| 精品久久无码中文字幕| 久久人做人爽一区二区三区| 国产免费久久久久久无码| 久久精品人人做人人爽97| 久久久久av无码免费网| 伊人久久大香线蕉无码麻豆 | 久久精品国产免费| 一本一本久久A久久综合精品| 欧美一级久久久久久久大片| 99久久婷婷免费国产综合精品| 一本一道久久综合狠狠老| 久久精品国产男包| 午夜视频久久久久一区 | 伊人色综合九久久天天蜜桃| 久久www免费人成看国产片| 嫩草影院久久99| 久久99国产精品久久99果冻传媒| 久久亚洲国产成人精品性色| 亚洲中文字幕无码久久2017| 亚洲国产美女精品久久久久∴ | 99久久国产综合精品五月天喷水| 久久精品国产亚洲AV香蕉|