- 注册时间
- 2011-3-6
- 最后登录
- 1970-1-1
该用户从未签到
|
背景
有多少程序员会检查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都已修正了这些问题。 |
|