大部分的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都已修正了这些问题。
实例: