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

转载 2012年03月31日 12:19:55



作者:SleepSheep

概述

大部分的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的行为使其遇到错误时抛出异常,但这会带来更多的不规范。我们通过以下的代码来说明问题:
#include <string>
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 <new>
#include <new.h>
#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都已修正了这些问题。

应用待机时间过长导致内存被回收程序崩溃

android:alwaysRetainTaskState="true"  添加这句话就不蹦了   android:allowTaskReparenting      用法      是否允许a...

Linux服务器Cache占用过多内存导致系统内存不足最终java应用程序崩溃解决方案

问题描述 Linux内存使用量超过阈值,使得Java应用程序无可用内存,最终导致程序崩溃。即使在程序没有挂掉时把程序停掉,系统内存也不会被释放。 找原因的过程 这个问题已经困扰我好几个月...

Linux服务器Cache占用过多内存导致系统内存不足最终java应用程序崩溃解决方案

http://blog.csdn.net/u014740338/article/details/66975550 问题描述 Linux内存使用量超过阈值,使得Java应用程序无...
  • bluetjs
  • bluetjs
  • 2017年11月17日 20:44
  • 71

OC手动管理内存时出现程序崩溃

今天遇到了个之前没 在手动管理内存时应该注意,实例变量在定义为 retain 模式时,在调用初始化方法中不应该是自己手动赋值,而是调用setter方法。如: 在头文件中声明 @property (no...

windows 应用程序崩溃时的内存转储及dump文件的分析

1、在现场设置程序崩溃时的自动内存转储,得到dump文件        在windows 注册表如下项:      //HKEY_LOCAL_MACHINE/Software/Microsoft/...
  • SP_daiyq
  • SP_daiyq
  • 2012年09月11日 10:27
  • 12123

使用SetUnhandledExceptionFilter转储程序崩溃时内存DMP

关于程序崩溃时转储内存DMP,可以设置注册表,使程序崩溃时自动转储内存DMP,见程序崩溃时利用注册表自动转储内存DMP。本文要介绍的是使用SetUnhandledExceptionFilter函数在程...
  • BeanJoy
  • BeanJoy
  • 2012年07月09日 00:28
  • 7437

程序崩溃--内存访问及申请释放

程序的编写过程中,经常会出现程序崩溃的问题,而这类问题大多数是因为对内存的处理不当引起的,而且不好找出原因,现将遇到的问题总结如下,以便日后查找: 1、内存的访问过界 这类问题只要写程序时注意...

dll的内存申请和释放问题--Debug程序正常而Release程序崩溃

C++编程中经常遇到这样的需求:主函数需要调用一个dll库函数并返回一块大小不定的存储着处理结果的内存,这时容易想到两种选择:一是使用vector的引用作为形参,二是使用指针,在主函数中定义指针,而在...

[BUG分享]搜狗浏览器地址栏输入特殊字符导致程序崩溃

搜狗浏览器1.0时由程序崩溃引申出的数据库相关测试点及SQL注入语句
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:不要让内存分配导致传统的STL程序崩溃
举报原因:
原因补充:

(最多只允许输入30个字)