不要让内存分配导致传统的STL程序崩溃

问题描述

大部分的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中正常工作。

解决方案

(一般应有具体的解决方案)

可见,在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的行为使其遇到错误时抛出异常,但这会带来更多的不规范。我们通过以下的代码来说明问题:

#include 
void Foo()
{
    std::string str("A very big string");
}
	  

在Visual C++ 6.0中,上面的代码最终会调用到STL中如下的函数(节选,为说明的方便多余的代码已拿掉):

void _Copy(size_type _N)
{
    ...
    _E *_S;
    _TRY_BEGIN 
        _S = allocator.allocate(_Ns + 2, (void *)0);
    _CATCH_ALL 
        _Ns = _N; 
        _S = allocator.allocate(_Ns + 2, (void *)0); 
    _CATCH_END 
    ...
    _Ptr = _S + 1;
    // ACCESS VIOLATION
    _Refcnt(_Ptr) = 0;
    ...
}
	  

在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操作符

#include 
#include 
#pragma init_seg(lib)
namespace
{
int new_handler(size_t) 
{
    throw std::bad_alloc();
    return 0;
}

class NewHandler
{
public:
    NewHandler() 
    {
        m_old_new_handler = _set_new_handler(new_handler);
    }   
    ~NewHandler() 
    {
        _set_new_handler(m_old_new_handler);
    }
private:
    _PNH m_old_new_handler;
} g_NewHandler; 
}   // namespace
	  

将以上代码包含进我们的工程,那么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都已修正了这些问题。


你可能感兴趣的:(不要让内存分配导致传统的STL程序崩溃)