VC6.0在<new>頭文件中聲明了這兩種Operator new操作符:
void *__cdecl Operator new(size_t) _THROW1(std::bad_alloc);
void *__cdecl Operator new(size_t, const std::nothrow_t&) _THROW0();
並分別定義在newop.cpp和newop2.cpp中。而_THROW0和_THROW1則是兩個宏,在Include目錄的xstddef文件中定義:
#define _THROW0() throw ()
#define _THROW1(x) throw (x)
newop.cpp和newop2.cpp對應的目標模塊被打包進標准C++庫中。標准C++庫有若干個版本: libcp.lib(單線程靜態版)、libcpd.lib(單線程靜態調試版)、libcpmt.lib(多線程靜態版)、libcpmtd.lib(多線程靜態調試版)、msvcprt.lib(多線程動態版的導入庫),msvcprtd.lib(多線程動態調試版的導入庫),這些庫與相應版本的C標准庫一起使用,比如libcp.lib與libc.lib搭配。另外,VC6.0在new.cpp還定義了一個Operator new,原型如下 :
void * Operator new( unsigned int cb )
而new.cpp對應的目標模塊卻是被打包進C標准庫中的(是不是有點奇怪?)。
一般來說,程序員不會顯式指定鏈接C++標准庫,可是當程序中確實使用了標准C++庫時鏈接器卻能聰明地把相應的C++標准庫文件加進輸入文件列表,這是為什麼?其實任何一個C++標准頭文件都會直接或間接地包含use_ansi.h文件,打開它一看便什麼都清楚了(源碼之前,了無秘密) :
/***
*use_ansi.h - pragmas for ANSI Standard C++ librarIEs
*
* Copyright (c) 1996-1997, Microsoft Corporation. All rights reserved.
*
*Purpose:
* This header is intended to force the use of the appropriate ANSI
* Standard C++ librarIEs whenever it is included.
*
* [Public]
*
****/
#if _MSC_VER > 1000
#pragma once
#endif
#ifndef _USE_ANSI_CPP
#define _USE_ANSI_CPP
#ifdef _MT
#ifdef _DLL
#ifdef _DEBUG
#pragma comment(lib,"msvcprtd")
#else // _DEBUG
#pragma comment(lib,"msvcprt")
#endif // _DEBUG
#else // _DLL
#ifdef _DEBUG
#pragma comment(lib,"libcpmtd")
#else // _DEBUG
#pragma comment(lib,"libcpmt")
#endif // _DEBUG
#endif // _DLL
#else // _MT
#ifdef _DEBUG
#pragma comment(lib,"libcpd")
#else // _DEBUG
#pragma comment(lib,"libcp")
#endif // _DEBUG
#endif
#endif // _USE_ANSI_CPP
現在我們用實際代碼來測試一下new會不會拋出異常,建一個test.cpp源文件:
// test.cpp
#include <new>
#include <iOStream>
using namespace std;
class BigClass
{
public:
BigClass() {}
~BigClass(){}
char BigArray[0x7FFFFFFF];
};
int main()
{
try
{
BigClass *p = new BigClass;
}
catch( bad_alloc &a)
{
cout << "new BigClass, threw a bad_alloc exception" << endl;
}
BigClass *q = new(nothrow) BigClass;
if ( q == NULL )
cout << "new(nothrow) BigClass, returned a NULL pointer" << endl;
try
{
BigClass *r = new BigClass[1];
}
catch( bad_alloc &a)
{
cout << "new BigClass[1], threw a bad_alloc exception" << endl;
}
return 0;
}
根據VC6.0編譯器與鏈接器的做法(請參考《為什麼會出現LNK2005"符號已定義"的鏈接錯誤?》),鏈接器會首先在C++標准庫中解析符號,然後才是C標准庫,所以如果開發者沒有自定義Operator new的話最後程序鏈接的應該是C++標准庫中newop.obj和newop2.obj模塊裡的代碼。可是程序運行的結果卻是:
new(nothrow) BigClass, returned a NULL pointer
顯然程序始終未拋出bad_alloc異常。單步跟蹤觀察,發現第1個和第3個new實際上調用了new.cpp裡的Operator new,而第二個new(nothrow)則正確地調用了newop2.cpp定義的版本。很難理解是吧?但是當你用
dumpbin /SYMBOLS libcp.lib
dump出libcp.lib所有的符號信息時,你會發現其中的newop.obj模塊沒有定義任何符號(其它版本也一樣)。不可思議!newop.cpp的實現代碼明明寫在那兒,怎麼會....?讓我們再仔細看看newop.cpp,咦,Operator new的定義被包裹在一個#if...#endif塊中:
#if !defined(_MSC_EXTENSIONS)
...
...
void *__cdecl Operator new(size_t size) _THROW1(_STD bad_alloc)
{
...
...
}
#endif
哦,原來需要_MSC_EXTENSIONS宏未定義,實現代碼才是有效的啊。那麼這個宏是什麼意思?其實Visual C++在語言層面上對ANSI C標准做了一些特殊的擴展,定義_MSC_EXTENSIONS意味著編譯器支持這樣的擴展,沒有定義它編譯器就會嚴格按照ANSI C標准來編譯程序。實際上如果指定了編譯選項/Ze編譯器就會自動定義這個宏,指定/Za則不會,而且/Ze是缺省選項。作者猜想Visual Studio的開發人員在build標准C++庫時很可能沒有指定/Za,導致newop.cpp中的operator new定義被無情拋棄。是他們的疏漏嗎?我看未必,大家可以試試用/Za選項去編譯那些標准庫文件,看看有多少編譯不通過。VC標准庫的實現用了很多微軟擴展的語言特性,不指定/Za是情有可原的,我不明白的是newop.cpp的作者(好象是P.J. Plauger老人家)為什麼會加上一個如此愚蠢的"#if !defined(_MSC_EXTENSIONS)",因為實在看不出這個Operator new定義與_MSC_EXTENSIONS有什麼沖突的地方。
既然標准C++庫裡的newop.obj是個空殼,那我們就只好自己動手豐衣足食了。把newop.cpp和dbgint.h(都在VC98crtsrc目錄下)拷貝到test.cpp所在的目錄,並將newop.cpp中的
#include <dbgint.h>
改成
#include "dbgint.h"
然後用
cl /c /Za /D_CRTBLD newop.cpp
編譯它。/D_CRTBLD定義了_CRTBLD宏,為什麼這麼做呢?因為dbgint.h屬於內部頭文件,VC不希望應用程序用到它,便在文件中埋伏了這麼一段:
#ifndef _CRTBLD
/*
* This is an internal C runtime header file. It is used when building
* the C runtimes only. It is not to be used as a public header file.
*/
#error ERROR: Use of C runtime library internal header file.
#endif /* _CRTBLD */
可我們確確實實是想build標准庫(的一部分),所以只好強行突破這個限制了。然後編譯test.cpp:
cl /c /GX test.cpp
最後進行鏈接:
link test.obj newop.obj
這時再運行test.exe輸出的結果就是
new BigClass, threw a bad_alloc exception
new(nothrow) BigClass, returned a NULL pointer
new BigClass[1], threw a bad_alloc exception
值得慶幸的是雖然VC6.0如此弱智,但VC7.1卻表現良好,原因是VC7.1的newop.cpp和newaop.cpp(數組版)取消了那個愚的"#if !defined(_MSC_EXTENSIONS)",於是標准C++庫中的newop.obj和newaop.obj模塊都實實在在地有了相應代碼。另外,nothrow版的定義也分別轉移到了newopnt.cpp和newaopnt.cpp中。
後記: 作者在2001年便碰到過這個問題,百思不得其解,於是在CSDN論壇上發問,也不見答復。從此便擱置一旁,直到最近因探究LNK2005鏈接錯誤而徹底弄清楚VC鏈接器解析符號的規則後,才意識到二者或有聯系。於是重拾舊疑,順籐而上,果然問題就迎刃而解。此題雖小,功夫卻做足,最後總算水落石出,解除了4年的積惑。