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

本文探讨了在Visual C++ 6.0中使用STL时,new操作符在内存分配失败时返回NULL的问题,并提出了解决方案。文章强调了此行为与C++标准不符,可能导致程序崩溃。
部署运行你感兴趣的模型镜像



作者: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都已修正了这些问题。

您可能感兴趣的与本文相关的镜像

PyTorch 2.6

PyTorch 2.6

PyTorch
Cuda

PyTorch 是一个开源的 Python 机器学习库,基于 Torch 库,底层由 C++ 实现,应用于人工智能领域,如计算机视觉和自然语言处理

<think>嗯,用户这次提出的问题相当专业啊——关于内存中运行的DLL调用STL函数与主进程冲突导致崩溃的解决方案。这属于C++开发中比较棘手的运行时问题,看来用户应该是位有经验的开发者,正在调试复杂的模块冲突。 从用户引用的两段资料来看,ta显然已经掌握了基础的调试手段:知道用Dependency Walker检查依赖,会用Windbg附加进程并查看堆栈。但问题进阶到了更隐蔽的运行时冲突层面,特别是STL实现的版本不一致性问题,这往往需要更深入的二进制分析。 用户提到的"内存运行的DLL"这个表述值得注意,可能是指通过LoadLibrary动态加载的模块?或者是内存映射加载的特殊场景?需要重点考虑跨模块边界时STL对象传递的风险。这类问题最典型的症状就是堆损坏或虚表指针异常,崩溃点往往在看似无关的STL操作中突然发生。 我回忆了下Windows环境下常见的冲突点:首先是CRT堆内存管理器的隔离问题,不同模块使用独立堆时,在某个模块分配的内存在另一模块释放必然崩溃;其次是STL容器底层allocator的差异,比如DLL使用自定义allocator而主程序使用默认allocator;最麻烦的是不同编译器版本生成的STL符号冲突,比如DLL用VC2019的std::string而主程序用VC2022的。 应该建议用户先用Windbg的!heap命令验证内存分配源,然后用x命令检查STL函数的符号修饰差异。如果发现msvcp140.dll和vcruntime140.dll存在多个版本混用,基本就能锁定问题。解决方案必须同时处理编译期设置(如_AFXEXT宏)和运行时配置(如设置链接器/BIND选项)。 用户可能还忽略了另一个关键点:异常处理机制的一致性。如果DLL使用SEH而主程序使用C++异常,STL容器在跨模块传递异常时必然崩溃。这点在引用资料里没提到,但实际调试中经常遇到,需要补充说明。 最后要注意给出可落地的操作步骤,比如如何设置项目属性里的"代码生成→运行时库"选项,毕竟理论分析再完整也不如直接告诉用户在VS哪个菜单里修改配置。对于紧急情况下的workaround,可以考虑建议用户使用接口隔离技术,用PIMPL模式彻底避免STL对象跨模块暴露。 </think>内存中 DLL 调用 STL 函数与主进程冲突导致崩溃的常见原因是 **STL 实现版本不一致**或 **堆内存管理冲突**,可通过以下方案逐步排查解决: --- ### **核心解决方案** #### 1. **统一 STL 实现版本** - **编译器与 CRT 一致性** 确保 DLL 和主程序使用**相同编译器版本**(如 VS2019/VS2022)且**相同运行时库设置**(MT/MD): - **/MT**:静态链接 CRT(每个模块独立堆) - **/MD**:动态链接 CRT(共享 `msvcrXXX.dll`) > 📌 若主程序用 `/MD` 编译,DLL 必须同样使用 `/MD`,否则导致堆内存跨模块释放崩溃[^1]。 - **验证方法** 用 **Dependency Walker** 检查主程序和 DLL 依赖的 CRT 库(如 `msvcp140.dll`, `vcruntime140.dll`)**版本是否一致**。若存在多个版本,需统一编译环境[^1]。 #### 2. **隔离堆内存分配** - **禁止跨模块传递 STL 对象** STL 容器(如 `std::string`, `std::vector`)若在 DLL 内部分配内存,在主程序中释放(或反之),必然崩溃。解决方案: - **接口隔离**:DLL 导出函数使用原始指针(`char*`, `int*`)或 C 风格结构体,避免直接暴露 STL 对象。 - **内存自治**:DLL 内分配的内存由 DLL 内部释放,主程序仅通过接口传递数据副本。 ```cpp // DLL 导出接口示例(安全) extern "C" __declspec(dllexport) void GetStringData(char* buffer, int size); ``` - **使用共享堆管理器** 若必须跨模块传递 STL 对象,可让主程序和 DLL 使用**同一堆分配器**: ```cpp // 主程序定义全局堆分配器 HANDLE hSharedHeap = HeapCreate(0, 0, 0); // DLL 中使用该堆分配内存 void* pMem = HeapAlloc(hSharedHeap, 0, size); ``` #### 3. **符号可见性与边界控制** - **显式声明接口** 使用 `extern "C"` 消除 C++ 符号修饰,避免名称改编(Name Mangling)冲突: ```cpp extern "C" { __declspec(dllexport) int SafeAPI(); } ``` - **隔离 STL 实现** 在 DLL 头文件中隐藏 STL 类型: ```cpp // 不暴露 STL 细节(使用前置声明或 opaque pointer) class ImplClass; // DLL 内部实现 __declspec(dllexport) ImplClass* CreateObject(); ``` --- ### **调试与验证** #### 1. **Windbg 动态分析** - 附加进程后复现崩溃,执行 `kn` 查看调用堆栈,定位崩溃点是否在 STL 函数内(如 `std::vector::push_back`)[^2]。 - 用 `!heap -p -a <address>` 检查内存块所属堆,确认是否跨模块释放。 #### 2. **DLL 加载诊断** - 通过 **Process Monitor** 监控 DLL 加载行为,过滤 `LoadImage` 事件,确认 CRT 库加载路径是否冲突。 - 使用 **Dependency Walker** 的 Profile 模式运行程序,检查运行时加载的模块依赖树。 --- ### **预防措施** 1. **编译规范** - 项目组统一编译器版本及运行时库配置(强制 `/MD` 或 `/MDd`)。 - 开启 `/Zc:dllexportInlines-` 禁止导出内联 STL 函数。 2. **接口设计** - **DLL 边界传递 POD 类型**(基本类型、结构体)。 - 提供明确的资源管理接口(如 `CreateResource()`/`ReleaseResource()`)。 3. **测试验证** - 在 Debug 模式下启用 **Page Heap**(`gflags /p /enable <exe>`)检测堆损坏。 > 💡 **关键总结**:崩溃本质是内存管理边界被破坏。**统一运行时环境 + 隔离 STL 对象传递** 可解决 90% 此类问题。对于复杂系统,建议改用 COM 或抽象接口层彻底隔离实现细节[^1][^2]。 ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值