大部分的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都已修正了這些問題。
實例: