程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> C++入門知識 >> C++ New崩潰原理及解決方法

C++ New崩潰原理及解決方法

編輯:C++入門知識

C++ New崩潰原理及解決方法


大部分的C++開發者在他們的代碼中會廣泛的使用STL。如果你直接用STL和Visusal Studio 6.0,那麼你的程序將在內存很低的情況下極有可能崩潰掉。原因在於沒有對new操作的結果進行檢驗。更糟的是,若new操作確實失敗了,得到的反饋也沒有一個標准可言——有的編譯器會返回空指針,而有的會拋出異常。 總之,如果你在MFC的項目中用STL,請注意MFC有它自己的規則。這篇文章主要討論這些問題,解釋最新的Visual C++編譯器的默認行為有了怎樣的改變,並概述你在使用Visual C++ 6.0時必須要做出的一些修改,這樣即使在new操作失敗時你也能安全地使用STL。

 

 

背景

有多少程序員會檢查new操作是否失敗?是否有需要經常做這樣的檢查?我見過一些龐大而復雜的C++工程,它們是用Visual C++ 6.0寫的,但沒有看到一處對new的返回結果是否是NULL進行了檢查。請注意是對new返回NULL的檢查。Visual C++ 6.0中,new操作失敗時的默認行為是返回一個NULL指針而不是拋出異常。Visual C++ 2003中,C運行時庫(C Runtime Library)的new失敗時還是返回NULL,但標准C++庫(Standard C++ Library)中的new失敗時會拋出異常。New失敗時究竟是何種行為要看linker中是標准C++庫在前面還是C運行時庫在前面。若標准C++庫在前面,則會拋出異常;而C運行時庫在前面,則只返回NULL。要改寫這個行為並強制使用會拋異常的那個new,我們需要顯示的鏈接thrownew.obj。在Visual C++ 2005、2008及2010中,除非顯示鏈接nothrownew.obj,否則不管是C運行時庫還是標准C++庫,都會拋出異常。另外要注意的是,這裡描述的行為都不涉及托管代碼或.NET框架。若原有的Visual C++ 6.0風格的代碼沒有預料到new操作會丟出異常,將所有這些代碼移植到高版本編譯器後,若是其中的new會拋出異常,那麼產生的程序極有可能會在運行時意外終止。對這點我們必須要注意.

C++標准規定,new操作符必須在失敗時拋出異常,具體來說,這個異常得是std::bad_alloc。這只是標准而已,具體在Visual C++中的情形請見下表:

版本 純C++ MFC

Visual C++ 6.0 返回NULL CMemoryException > 6.0 std::bad_alloc CMemoryException

可見,在MFC環境下,拋出的異常並不是C++標准上要求的。如果你用的STL中用catch (std::bad_alloc)來處理內存分配失敗,那這個只能在沒有MFC的環境下才可以。Visual C++ 6.0中的STL用catch (…)來處理new失敗的情況,這種寫法可以在MFC中正常工作。

返回NULL的new操作符

通常兩種情形下不需要檢查new返回的指針是否是NULL:new永遠不會失敗或new會拋出異常。

即使你認為new永遠都不會失敗,但不檢查返回值是一個很差的編程習慣。桌面應用程序一般不太可能會遭受內存耗盡的窘境。但一些服務器上需要24小時運行的程序就比較有可能碰到內存耗盡的情況,尤其是在一台共享應用程序服務器上。如果你不能保證你的應用程序一直是一個字節都不洩露的,那由內存產生錯誤的幾率就會增加。

如果你不檢查返回的指針是否是NULL的原因是由於new會拋出異常,這也情有可原。畢竟,C++標准規定new在失敗時要拋出異常,但這不是Visual C++ 6.0的默認做法,它只會返回一個NULL指針。盡管之後的版本有支持C++標准,但6.0中的做法(尤其是在和STL一起使用時)會產生問題。STL中會假定new失敗時會拋出異常,不管使用的是何種編譯器。事實上,如果new沒有表現出這種行為並由於內存分配失敗而得到一個NULL指針,STL接下來的行為將是不可預測的,而程序也有很大的可能崩潰掉。

標准模板庫

開發人員在C++開發過程中越來越依賴於STL。STL在C++模板的基礎上提供了很多類及函數。用STL有幾個好處:首先,這個庫為各種通用任務提供了一個一致的接口;其次,這部分代碼被廣泛地測試過,因此可以認為它已經沒有bug了;最後,裡面的算法也是最佳的。

為了使STL能使用,編譯器要支持C++標准。Visual C++編譯器預裝了一個STL,其他廠家的也是能使用的。

Visual C++ 6.0和new操作符

當new失敗時返回NULL,可以認為這個行為是Bug,因為它與標准不符。所有STL的實現,包括Visual C++自帶的,都預期new操作符在失敗時會拋出異常。盡管可以改變new的行為使其遇到錯誤時拋出異常,但這會帶來更多的不規范。我們通過以下的代碼來說明問題:

1.#include < string > 2.void Foo() 3.{ 4.std::string str(A very big string); 5.} 6.

在Visual C++ 6.0中,上面的代碼最終會調用到STL中如下的函數(節選,為說明的方便多余的代碼已拿掉):

01.void _Copy(size_type _N) 02.{ 03.... 04._E *_S; 05._TRY_BEGIN 06._S = allocator.allocate(_Ns + 2, (void *)0); 07._CATCH_ALL 08._Ns = _N; 09._S = allocator.allocate(_Ns + 2, (void *)0); 10._CATCH_END 11.... 12._Ptr = _S + 1; 13.// ACCESS VIOLATION 14._Refcnt(_Ptr) = 0; 15.... 16.} 17.

在try語句塊中,allocator.allocate的返回值賦給局部變量_S,而allocator.allocate會用到new。Visual C++ 6.0的默認行為是:new操作符失敗時會返回NULL,這就會使_S的值為NULL。接下來一行會將_S+1的值賦給_Ptr。若_S為NULL,_Ptr最終將為0x00000001。接下來一句_Refcnt(_Ptr) = 0事實上返回_Ptr-1(即_Ptr[-1]),即其實是在對最初返回的那個NULL在計算。_Refcnt返回一個NULL指針,接下來再將0賦值給它(*NULL = 0),這樣就會立即產生一個訪問沖突的錯誤。盡管這看起來似乎是一個Bug,但STL的代碼其實沒有問題,只是為了得到一個正確的行為,它需要new能拋出異常。

讓我們再看一下new失敗時拋出異常的執行流程。首先執行allocator.allocate,這其中的new失敗後會拋出std::bad_alloc異常,接著就進到_CATCH_ALL再試一次。如果第二次分配也失敗了,將會有另一個std::bad_alloc異常被拋出,這個會被一路傳播到我們的代碼中,最終導致std::stting對象雖然定義了卻還是空的這樣一個狀態。

修正new操作符

01.#include < new > 02.#include < new.h > 03.#pragma init_seg(lib) 04.namespace 05.{ 06.int new_handler(size_t) 07.{ 08.throw std::bad_alloc(); 09.return 0; 10.} 11. 12.class NewHandler 13.{ 14.public: 15.NewHandler() 16.{ 17.m_old_new_handler = _set_new_handler(new_handler); 18.} 19.~NewHandler() 20.{ 21._set_new_handler(m_old_new_handler); 22.} 23.private: 24._PNH m_old_new_handler; 25.} g_NewHandler; 26.} // namespace 27.

將以上代碼包含進我們的工程,那麼new失敗時的錯誤處理會被自動修改,例子中將會拋出std::bad_alloc。

new(std::nothrow)拋出錯誤

在Visual Studio 6.0中,如果將以上代碼包含進去,而分配內存時用new(std::nothrow),運行release時反而會報錯,顯示Abnormal program termination。這是個比較細節性的問題,是由於編譯器的優化造成的。可以到Project Settings | C/C++ | General | Optimizations將優化關掉以避免這個問題,或者還可以自己寫一個new(std::nothrow)(請參考源代碼NewNoThrow.cpp)。

總結

Visual C++ 6.0默認提供的new操作與STL並不兼容。即使前面提到了一些解決方法,仍有可能在用第三方的庫或STL中個別其他函數時會有麻煩。VC 6.0中new、new(std::nothrow)和STL的不相稱不能完全的解決掉,但如果不用上面的方法,肯定會有很到的麻煩。

MFC項目中,STL中用new的地方是否能經受異常的考驗完全取決於你用的STL中的錯誤處理時如何寫的。大多數都會用catch(…)而不是catch(std::bad_alloc),但這並不是必須的。

最後,正如最開始所提到的,Visual C++ 2005到2010都已修正了這些問題。


實例:

 

  \

 

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved