用 dependency walker (depends) 跟了一下,发现 operator new/delete 函数是从 msvcr[ver].dll 中导出的(如图),其中 ver 是 VC 运行时库 (CRT) 的版本,例如:VC 2005 (VC8) 环境下,Release 版本为 80,Debug 版本为 80d。本以为 operator new/delete 是从另一个 msvcp[ver].dll 导出的,其实不是,msvcp[ver].dll 有自己导出的 operator new/delete,但并不是我们编程常规用的 new/delete 操作符
VC 的运行时库,通常简称 CRT。特指时,它表示 msvcr[ver].dll 这个动态链接库,但在泛义上它和其它几个动态链接库有着紧密的联系,概念上的划分也有共享的部分。这几个都含 Runtime Library 语义的 dll 分别是:msvcr[ver].dll、msvcp[ver].dll、msvcm[ver].dll 和 msvcrt.dll
看看这些 dll 名字的都代表了什么意思:
msvcr[ver].dll
全称为 Microsoft C Runtime Library。msvcr[ver].dll 导出所有的标准 C 库 API,和微软的标准 C 库扩展 API,以及一些 C++ 基本语言特性需要的 API
msvcp[ver].dll
全称 Microsoft C++ Runtime Library。msvcp[ver].dll 导出标准 C++ 库中的 STL 和 iostream 类
msvcm[ver].dll
它也叫做 Microsoft C Runtime Library,不过是给托管代码用的 CRT,所以后缀 m 的含义可以理解为 managed(托管)。msvcm 没有任何对应的静态库,也就是说要使用托管 CRT,只能动态链接到 msvcm[ver].dll。有两种方式指定使用托管 CRT:
使用 /clr 编译选项,这时客户程序可以使用 managed/native 混合代码,并且使用 msvcmrt.lib 导入库
使用 /clr:pure 编译选项,这时客户程序使用纯粹的 MSIL 托管代码
由于 CLR 的 COM 实质,msvcm[ver].dll 依赖这些 dll:Native CRT:msvcr[ver].dll,OLE Library:ole32.dll,以及 .NET Runtime Execution Engine:mscoree.dll
上面的 msvcr、msvcp、msvcm 是 VC 引入的 dll,在部署应用程序时可以使用微软提供的 VC Redistributable Package 包来安装这些 dll,安装使用 side by side 方式,dll 拷贝到 %SystemRoot%winsxs 目录下,并且这些 dll 都是以 Release 方式编译的
另外,还有一种直接拷贝这些 dll 到目标系统的部署方法,参考 MSDN 的 How to: Deploy using XCopy (VC8) 的 Deploying Visual C++ library DLLs as private assemblies 章节。这些要拷贝的 dll 保存在:Path-to-VSVCredist(Release 版)和 Path-to-VSVCredistDebug_NonRedist(Debug 版)
msvcrt.dll
全称 Windows NT CRT DLL,它的名字是固定不带版本号的,位置总在 %SystemRoot%system32 下。msvcr[ver].dll 和 msvcrt.dll 的作用区别借用 MSDN 的话说为:
from: C Run-Time Libraries (VC8)
What is the difference between msvcrt.dll and msvcr80.dll?
The msvcrt.dll is now a "known DLL", meaning that it is a system component owned and built by Windows. It is intended for future use only by system-level components.
msvcr[ver].dll 依赖 msvcrt.dll
下面说下用 VC 编译的 C++ 程序中,常规使用的 new/delete 操作符,也就是 msvcr[ver].dll 导出的 operator new/delete 函数,它们的调用和二进制模块 msvcr[ver].dll 的导出函数的对应关系,以及申请内存失败的处理机制
声明:下面程序的编译、测试环境均为 VC8,用模块名 msvcr80[d].dll 表示无差别情况下的 Debug 或 Release 版 CRT 动态链接库;当提到 VC 的头文件、源文件、静态库、导入库、对象文件等路径时,均是相对于 Visual Studio 2005 的安装目录;给出的 MSDN 参考,若非指明,也是适用于 VC8 版本
工具:用 depends 查找模块导出符号时,有些不方便。可以使用 VC 的附带工具 dumpbin,用法参考 DUMPBIN Reference (VC9),例如:dumpbin /exports msvcp80d.dll > res.txt
有两个关于 new/delete 的头文件:标准 C++ 库的头文件 <new>(没有 .h 后缀),和 CRT 的头文件 <new.h>。<new.h> 对应的模块是 msvcr80[d].dll;而 <new> 中声明的大部分函数对应于 msvcr80[d].dll,如常规的 new/delete 函数,而有些函数,例如下面会讲的 set_new_handler(),则对应 msvcp80[d].dll
msvcr80[d].dll 导出的 new/delete 有:
这些就是编程中常规用的 new/delete 的二进制接口
在 C++ 源码级别,总共有 6 个 new 函数声明,上面的第一个导出函数 void* operator new(unsigned int) 对应其中的 4 个:
为什么二进制的 void* operator new(unsigned int) 也会对应声明的 operator new[] 原型?后面会慢慢道来
另外 2 个 new 函数的声明是:
这两个被称为放置式的 new,不对应任何运行时库的动态链接库,它们的二进制代码会编译进客户程序中
上面 6 个 new 的使用语法参考:operator new (CRT) 和operator new[] (CRT)
下面就说说这几个 new 的区别
标量 new(scalar new),即 operator new;矢量 new(vector new),operator new[]
按照 MSDN operator new[] (CRT) 的说法,当使用 new 申请数组块内存时,就调用矢量的 operator new[]
在实际的 VC8 环境测试中,发现下列客户代码均会调用 msvcr80[d].dll 导出的 void* operator new(unsigned int),而非导出的 void* operator new[](unsigned int):
反汇编调试后发现,在客户程序中 VC8 编译器已经将申请数组块的整体大小计算出来了,比如 sizeof(TestObj) = 40,则编译器就会计算出 new TestObj[10] 申请的大小为 40 * 10 = 400
如果此时仅包含 <new.h>,或者不包含 <new> 和 <new.h> 任一个,则会调用 msvcr80[d].dll 中导出的 void * operator new(unsigned int)
如果仅包含 <new>,或者 <new>、<new.h> 两个都包含(顺序无关),则会调用 VCcrtsrcnewaop.cpp 中定义的 operator new[](aop 的含义是 array operator)。那么 newaop.cpp 中的 operator new[] 是否是 msvcr80[d].dll 中导出的 void* operator new[](unsigned int)?答案不是,从 VC 的 Call Stack 中看出 newaop.cpp 对应的二进制模块是客户程序的模块而非 msvcr80[d].dll。newaop.cpp 定义的 operator new[] 是 msvcr80[d].dll 导出的 void* operator new(unsigned int) 的简单包裹,最后将调用传给它
delete/delete[] 的调用也有这种受包含 <new> 还是 <new.h> 影响的问题,见下面对 delete 的讨论
所以问题是,虽然在 msvcr80[d].dll 中导出了 operator new[],但似乎不能通过标准的方法使用它
按 new 的行为是申请存储位置,还是将对象放置到某个存储位置,可以分为:非放置式(nonplacement)new,和 放置式(placement)new
放置式 new 语法参考:《C++ 程序设计语言》特别版(Bjarne)章节 10.4.11 对象的放置。有两种惯用的放置手法:1. 放置到已有存储位置。2. 使用 Arena(场地)分配存储位置(自定义放置式 new)
VC CRT 中提供了第一种放置式 new,在 <new> 和 <new.h> 中都有声明:
这些放置 new 都是 inline 函数,声明的同时给出定义,函数体都是一句简单的 return (_Where),所以不存在 msvcr80[d].dll 中导出的放置式 new
测试放置式 new 的代码:
还有一种 MSDN 上称为 placement, no-throw 的 operator new/new[],在 <new.h> 和 <new> 中都有声明:
不过它和放置位置没有关系,而是影响内存申请失败时的报告机制,如果使用这种 new,则申请失败时将以 operator new 返回 0 值表示失败,否则使用默认抛出异常的方式表示申请失败
以调用 new(std::nothrow) char[BIG_SIZE] 为例,调用顺序如下:
客户程序模块: newaopnt.cpp: void* __CRTDECL operator new[](::size_t count, const std::nothrow_t& x)
客户程序模块: newopnt.cpp: void* __CRTDECL operator new(size_t count, const std::nothrow_t&)
msvcr80[d].dll: 导出的 void* operator new(unsigned int)
newaopnt.cpp 和 newopnt.cpp 在 VCcrtsrc 目录下,后缀 nt 表示 no-throw
更详细的 new 申请失败报告机制在后面叙述
在 msvcr80[d].dll 中有两个 4 个参数的 operator new 函数的导出:
这两个函数的声明在 <crtdbg.h> 中,实现源码在 <dbgnew.cpp>,用法参考 The Debug Heap from C++。示例:
在工程公共头文件中:
在源文件中:
上面申请数组块内存时,会调用 msvcr80d.dll 导出的 void* operator new[](unsigned int, int, char const*, int)
_CrtMemDumpAllObjectsSince(NULL) 将从程序开始到其调用点的所有堆对象调试信息,转储到调试输出,比如上面用 operator new[](unsigned int, int, char const*, int) 申请内存时,就会产生调试信息。_CrtMemDumpAllObjectsSince() 只在调试版起作用(有 _DEBUG 定义),当没有 _DEBUG 定义时,_CrtMemDumpAllObjectsSince() 就被替换成一个空操作 ((void)0),如同 _ASSERT() 的实现一样(_ASSERT() 也在 <crtdbg.h> 中定义)。可以用 VC 的调试 Output 窗口,或 DebugView 工具查看 _CrtMemDumpAllObjectsSince() 的输出
参考:The new and delete Operators
msvcr80[d].dll 中导出的 new 的报告申请失败的默认方式是抛出 std::bad_alloc 异常。测试例子:
上面的代码编译后不会链接到 msvcp80[d].dll,因为标准 C++ 异常类(即 std::bad_alloc)和 C++ RTTI 类(type_info),均在 msvcr80[d].dll 中有导出
如果想使用返回 0 来表示 new 申请内存失败,除了使用上面提到的 placement, no-throw 的 new 外,调试版的 operator new/new[](unsigned int, int, char const*, int) 也是以返回 0 表示失败,并不抛出异常
另外,还有一种用返回 0 来表示失败的方法,就是和 VClibnothrownew.obj 链接,此时便不会调用 msvcr80[d].dll 中导出的 new(用 depends 可以观察到),而调用 nothrownew.obj 中包含的 new
VC 编译出代码的异常处理方式(Exception Handling Model)和编译选项 /EH 有关系,它会影响 C 和 C++ 两种语言中的异常处理,以及标准 C++ 规范中的异常(try-catch 结构)和 Windows 特有的异常处理方式 SEH (Structured Exception Handling)(__try-__except-__finally 结构)
关于 VC 中 C/C++ 的异常处理,和更多 new/delete 操作的内容,请查阅文章最后的参考
std::set_new_handler
参考:
set_new_handler:set_new_handler 的用法
operator new (<new>):包含 operator new() 的循环尝试申请工作机制,和自定义 new_handler 应该完成的功能
set_new_handler() 是 C++ 标准中定义的 new 申请失败处理设定函数,使用的失败处理函数类型为:typedef void (*new_handler)()。相关函数、类型在 <new> 中声明,set_new_handler() 在 msvcp80[d].dll 中导出
例子:
上面代码会链接到 msvcp80[d].dll
自定义的错误处理函数 new_handler,必需完成 3 种功能之一:
产生更多可用的内存以供申请,例如使用垃圾回收、不常用对象交换到文件等手段,具体方式和应用有关,此时该函数可以用直接返回的方式离开,随后控制返回给 operator new(),并再次尝试申请内存
调用 abort 或 exit 函数,让 CRT 负责并终止程序执行
抛出一个异常,通常是 std::bad_alloc,该异常会穿过 operator new() 一直上抛到客户程序,此时 new_handler 通过异常方式离开 operator new()
所以用户的 new_handler 实现中,如果即没有产生更多可用内存的工作,又不通过异常或 abort/exit 方式离开 operator new(),则 operator new() 就会陷入一直调用 new_handler 的死循环
_set_new_handler
参考:_set_new_handler
_set_new_handler() 是 CRT 提供的 new 申请失败处理设定函数,使用的失败处理函数类型为:typedef int (*_PNH)(size_t)。相关函数、类型在 <new.h> 中声明,_set_new_handler() 在 msvcr80[d].dll 中导出
例子:
上面代码会仅会链接到 msvcr80[d].dll,而不会链接 msvcp80[d].dll
和标准 C++ 库中的 new 失败处理函数不同,CRT 中规定的失败处理函数 int (*_PNH)(size_t),有返回值和参数。_PNH 的参数 size_t,表示请求申请但失败的内存大小,而 int 型返回值表示:
返回非 0 值,表示 _PNH 做过一些产生更多可用内存的工作,控制返回给 operator new() 后,会再次尝试申请内存。该情况和 C++ new_handler 的直接返回类似
返回 0,表示无需让 operator new() 再次尝试申请内存,申请操作已经彻底失败,最终控制会以默认抛出异常方式回到客户程序。该情况和 C++ new_handler 的以抛出异常方式结束申请类似
所以类似标准 C++ 库的 new_handler,_PNH 如果即没有产生更多可用内存的工作,又返回了非 0 值,则 operator new() 就会陷入一直调用 _PNH 的死循环
_PNH 是作用于 CRT 提供的全局 operator new() 的,要想 _PNH 也作用于 malloc(),使得 malloc() 申请失败时也调用 _PNH 处理,可以调用 _set_new_mode(1) 激活 malloc() 的失败处理机制
在二进制层次,_PNH 函数是在所有模块间共享的,在 dll 中设定的 _PNH 会影响到主 exe 和其它 dll 中的 operator new() 行为
在客户代码中使用 delete[] 时,如果仅包含 <new>,或者 <new>、<new.h> 两个都包含(顺序无关),则调用 msvcr80[d].dll 导出的 void operator delete[](void*)
如果仅包含 <new.h>,或者不包含 <new> 和 <new.h> 任一个,则调用 msvcr80[d].dll 导出的 void operator delete(void*)
实际上 msvcr80[d].dll 的 void operator delete[](void*) 实现,仅仅是对 void operator delete(void*) 做了一个简单的包裹。delete[] 的源码在 VCcrtsrcdelete2.cpp,Release 版的 delete 的源码在 VCcrtsrcdelete.cpp,Debug 版的 delete 的源码在 VCcrtsrcdbgdel.cpp
示例:
在 Debug 配置下,即链接到 msvcr80d.dll,dbgdel.cpp 中定义的 delete 在程序运行时会检查到这种情况,并报 assert 诊断错误
在 Release 配置下,即链接到 msvcr80.dll,如果程序运行时 attach 到 VC 的调试器,则在重复 delete 的位置会给出警告,中止运行并报错 "This may be due to a corruption of the heap, and indicates a bug in [program-name.exe] or any of the DLLs it has loaded.",可以让调试器继续运行程序。如果没有 attach 到调试器,而是独立运行程序,则不会看到任何警告提示,程序运行直到结束,这可能会造成复杂程序中 bug 的潜藏点
在上面重复 delete 的代码中加上一句,如下:
和重复 delete 不同,上述 delete 空指针无论是从 C++ 语法标准的角度(参考《C++ 程序设计语言》章节 6.2.6 自由存储),还是在实际的 VC8 环境中都是正确的,Debug 和 Release 版的 CRT 库均会正常运行,不会报出错误或警告
参考:The Debug Heap from C++
和使用调试版的 void* operator new(unsigned int, int, char const*, int) 不同,不需用户写 delete 操作的替换宏和更改任何代码,只需用 Debug 配置方式编译程序即可使用调试版的 delete,而换到 Release 方式编译就可使用一般的 delete
最后,不妨看看 msvcp80.dll 导出的 operator new/delete 函数,大家想想这些接口在何种情况下被调用呢?
除了上述全局的 new/delete 外,msvcp80[d].dll 中还导出了 std::locale::facet 类定义的 new/delete,不过这些跟用户常规的 new/delete 操作都没有关系
CRT 的模块 msvcr80[d].dll 中并非只含 C API,下面标准 C++ 库的 API 也在其中:
常规使用的 operator new/delete,包括调试版 void* operator new(unsigned int, int, char const*, int),对应头文件 <new> 和 <new.h>
标准 C++ 异常类,对应头文件 <exception>
标准 C++ RTTI 类,对应头文件 <type_info> 和 <type_info.h>
而 msvcp80[d].dll 则侧重于标准 C++ 库中的 STL 和 iostream 类的实现,basic_string、vector、basic_ostream 等均在这里导出,另外还包括 <new> 中声明的 std::set_new_handler()
VC 的这种混合 C API 和标准 C++ 库的模块管理方式,让人感觉很混乱并摸不着头脑。其实,微软这么做是有考虑的,试想一下,如果你即想使用 C++ 的基本语言特性,如 new/delete、RTTI 等,又不想依赖额外的标准 C++ 库,如一大堆 STL 模板类和 iostream 类,此时就可以仅链接 msvcr80[d].dll。总之,只要记住 msvcr80[d].dll 包括所有 VC 基本的 C/C++ 语言支持就好了,这也是 CRT“运行时”库的内涵所在