VC6下使用STL注意:不要让内存分配失败导致您的旧版STL 应用程序崩溃
2010-07-15 20:45:37 来源:WEB开发网修复运算符 New
前面我提到不应按照知识库文章 167733 中给出的实现来修复引发异常的运算符 new。本文给出两个代码示例,如图 3 和图 4 所示。第一个示例正确地安装了一个新的(用于运算符 new 失败)处理程序,我将改进该处理程序以给出一个自动安装的处理程序。第二个示例是我为什么给出警告。该示例安装了一个新的处理程序,并调用 _set_new_mode(1) 以表明 malloc 应在失败时引发异常。
如果设置 malloc 在失败时引发异常,则使用 malloc 的所有代码都会对此行为“感到惊讶”。运算符 new(std::nothrow) 也可以按照 malloc 来实现(至少对于调试版本是这样),这种变化将导致运算符 new(std::nothrow) 在失败时引发异常。这肯定不是您想要的行为。
图 5 显示了自动安装的新处理程序的代码,这些代码包含在随本文一起提供的 NewHandler.cpp 源文件中(参见本文顶部的链接)。本质上,它与知识库文章(如图 4 所示)中列出的第二个示例代码相同,语法已修复,并且删除了 _set_new_mode 调用。通过将 NewHandler.cpp 文件添加到您的项目中可使用这些代码。
该处理程序的测试工具程序非常简短,如图 6 所示。在大多数计算机上,该测试代码的分配将会引起即时失败。示例代码导致非法的堆分配大小,无需实际执行分配即导致立即失败。遗憾的是,如果您持续分配了大块的内存,直到实际堆分配发生失败,您将不能对代码进行调试,因为 Visual C++ IDE 在内存不足的情况下总是要崩溃的。为了构建该测试程序,下载示例文件。在 Visual C++ 6.0 中打开 Testnew_throw.cpp 文件,从 Build 菜单选择 Build。接受创建一个默认工作区的提示。如果逐句调试代码,您会验证现在在失败时运算符 new 将引发 std::bad_allocc 类型的异常。
不管怎样,当运算符 new(std::nothrow) 引发异常时
在知识库文章的结尾,关于运算符 new(std::nothrow) 和 Visual C++ 5.0 的注意事项中指出:如果安装新的处理程序,则 new(std::nothrow) 将引发异常。在 Visual C++ 6.0 中仍然存在这个问题,但其行为更微妙。利用 Visual C++ 6.0,当只与 Debug 运行库链接时,运算符 new(std::nothrow) 在失败时的行为才和期望的一样,并返回 NULL。如果您链接运行库的 Release 版,那么运算符 new(std::nothrow) 总会引发异常。这当然不是应用程序所想要的行为。运算符 new(std::nothrow) 的测试工具程序非常简短,但由于另外的突发事件(这里是优化的编译器),对全部行为的演示并不是那么简单明了。该测试程序最主要的部分是 try 块中仅有的调用运算符 new(std::nothrow) 的代码,如图 7 所示。
利用与 Testnew_throw.cpp 相同的方法,构建该测试程序的 Win32® Debug 配置。运行产生的可执行文件,得到下面的期望结果:
p= 00000000
现在构建 Win32 Release 配置,运行产生的可执行文件。输出结果可能会出人意料:
abnormal program termination
这里有一件事是确定的:运算符 new(std::nothrow) 肯定不返回 NULL。究竟发生什么不是那么清楚。试着将该行移到 try 块内:
std::cout << "p= " << p << "n";
结果发生了变化:
Error bad allocation
现在调用 catch 处理程序,来证实我前面提到的发布构建行为。问题仍然存在:为什么以前得到一个异常的程序终止?为了看清楚正在发生的一切,首先恢复到如图 7 所示的原始代码(只有对 try 块中运算符 new(std::nothrow) 的调用)。接着,更改 Win32 Release 配置的项目设置以禁用编译器优化操作 (Project | Settings | C/C++ | General | Optimizations = Disable (Debug))。构建并运行该可执行文件。程序的输出结果是预期的,但仍不是您想要的:
Error bad allocation
该行为可归于优化的编译器,它实际上生成有效的编译代码。Visual C++ 6.0 文档规定:尽管编译器支持异常规范的语法,它仍将忽略它们。因此,这不严格为真。要看清楚正在发生的一切,必须深入研究生成的程序集代码(非常少)。保留禁用优化设置,并确保 try 块中的唯一一行代码是对运算符 new(std::nothrow) 的调用,打开混合的程序集代码 (Project | Settings | C/C++ | Listing Files | Listing file type = Assembly with Source Code)。构建可执行文件,打开位于 Release 文件夹中的生成文件 Testnew_nothrow.asm。在该文件中,搜索字符串“try”,确保选中了“Find whole word only”复选框,避免与其他实例的部分匹配。应该能够查看 try 块的混合源程序/程序集,包括一个对名称为 __$EHRec$ 变量的引用。这是为 try/catch 异常机制生成的部分代码。
接下来,重新打开优化设置,重新生成并定位 Testnew_nothrow.asm 文件中的“try”源程序行。对 __$EHRec$ 变量的引用没有了。已经发生的事情是优化编译器检测到运算符 new(std::nothrow) 被声明为不引发异常,并正确地推断出整个 try/catch 块是冗余代码。结果是整个 try/catch 块被优化,使得由 try 块包装的代码无需多余的异常处理支持就可运行。虽然这在技术上是正确的,但会与随后编译器允许非引发函数引发一个异常相矛盾,而非引发函数是不可能捕获异常的。
已经发现了运算符 new(std::nothrow) 所进行的操作,现在我以用户提供的运算符 new(std::nothrow) 版本来给出该问题的修复方法。该方法取自知识库文章 167733,并包括在 NewNoThrow.cpp 源文件中,该文件可从本文下载的代码得到。该版本在失败时将正确地返回 NULL:
void *__cdecl operator new(size_t cb, const std::nothrow_t&) throw()
{
char *p;
try
{
p = new char[cb];
}
catch (std::bad_alloc)
{
p = 0;
}
return p;
}
但这需要警告语句。如果链接 DLL 版本的运行库(调试多线程 DLL 或多线程 DLL),则新的处理程序会有效地安装到运行库 DLL 中。这意味着加载到进程地址空间并与匹配版本的运行库 DLL 链接的任一 DLL 文件,都将受到该处理程序的影响(new 在失败时将引发异常)。这其中的含意完全取决于客户端 DLL 是否希望 new 返回 NULL 或认为它将引发异常(ATL 对两种模式都支持)。
只有在 new 更改为引发异常时才需要修复运算符 new(std:nothrow),这样的更改对正确使用 STL 是强制性的。但是,这种修复对于源文件要插入到的项目来说是局部的。在这种情况下,任何使用运算符 new(std::nothrow) 并构建兼容版本的运行库 DLL 的第三方 DLL(如我前面所显示的)都将存在失败时 new(std::nothrow) 引发异常的危险。这之所以发生是因为全局范围的新处理程序与局部范围的替换运算符 new(std::nothrow) 不匹配。唯一可行的解决方案是链接一个静态运行时库,或者验证第三方代码不会调用运算符 new(std::nothrow),或者不链接 DLL 版本的运行库。如果说这种不幸的事件状态有可能补救,那一定是运算符 new(std::nothrow) 很少重载使用。
最后的补救就是求助于 Visual C++ 6.0 提供的 STL,实际上只在一个地方使用这种重载,也就是 get_temporary_buffer 模板函数。您的代码不太可能直接调用该函数。但是,通过对 STL 源代码进行的搜索显示,internal_Temp_iterator 模板类调用 get_temporary_buffer 模板函数。下列在算法中定义的公共函数间接调用 _Temp_iterator 模板类自身:stable_partition、stable_sort 和 inplace_merge。如果运算符 new(std::nothrow) 确实引发了异常,则这些函数的行为没有被定义。如果使用这些函数并且安装了新处理程序,那么可以考虑使用不同的 STL 实现,其中有很多实现都可以使用。我在工作中成功地使用了 STLPort (http://www.stlport.org)。据我所知,该实现在其实现中的任何地方都没有调用运算符 new(std::nothrow)。
小结
我已经说明了如果您正在非 MFC 项目中使用 Visual C++(最高至 6.0 版本)中的 STL,其即装即用的行为可能导致在内存不足的情况下 STL 使您的应用程序崩溃。Visual C++ 6.0 提供的运算符 new 版本与 STL 不兼容。即使这里提供了修复方法,但当使用第三方代码或者 Visual C++ 6.0 提供的 STL 版本中的某些函数时,仍有可能出现麻烦。目前 Visual C++ 6.0 中运算符 new、运算符 new(std::nothrow) 和 STL 之间的不匹配不能完全被修复。但是,如果您在代码中使用 STL,而且没有包含我在本文中推荐的修复方法,您的应用程序在内存不足的情况下就会处于 STL 代码崩溃的真实危险中。
对于基于 MFC 的项目,STL 是否会在运算符 new 内幸免于难,完全取决于您使用的 STL 实现如何处理该运算符的异常。在处理失败的分配时,大多数实现好像都使用 catch(...),而并不使用 catch(std::bad alloc),但并不是必需这样。
最后,正如我在本文开始部分所述,两个目前使用的 Visual C++ .NET 版本都已解决了我所提到的所有问题,除了 MFC 行为之外。
更多精彩
赞助商链接