我想,有一条编程铁律已经深深的刻入到你的头脑中了:使用成套的函数来分配和释放内存,例如,如果使用 LocalAlloc 分配内存,则应该使用 LocalFree,类似的例子还有:GlobalAlloc 对应 GlobalFree,new [] 对应 delete []。
但这条规则还有更深层次的潜规则。
如果你提供了一个函数分配了一段内存并返回了数据,则调用者必须知道如何释放该内存。
实现这个目标有很多不同的方法,其中一个是在函数的文档中显式的声明内存应该如何释放。例如,FormatMessage 这个函数在文档中清楚的表达了调用者应该使用 LocalFree 来释放已分配的内存(如果你传递了
FORMAT_MESSAGE_ALLOCATE_BUFFER 标志的话)。
所有的 BSTR 变量必须使用 SysFreeString 来释放。跨 COM 接口边界返回的所有内存必须使用 COM 内存分配器分配和释放。
但是,请注意,如果你决定使用 C 运行时(例如使用 free)释放内存块,或者通过 C++ 运行时的 delete 或 delete[] 释放 ,则会遇到一个新问题:哪个运行时?
如果选择与静态运行时库链接,则模块具有自己的 C/C++ 运行时专用副本。当模块调用 new 或 malloc 时,只能通过模块调用 delete 或 free 来释放内存。如果另一个模块调用 delete 或 free,这将使用与你的模块不同的其他模块的 C/C++ 运行时。
事实上,即使你选择与 C/C++ 运行时库的 DLL 版本链接,你仍然必须同意使用哪个版本的 C/C++ 运行时。如果 DLL 使用 MSVCRT20.DLL 来分配内存,则任何想要释放该内存的人也必须使用 MSVCRT20.DLL。
如果你仔细观察,你可能会发现一个迫在眉睫的问题。如果你控制所有客户端并愿意在每次编译器更改时重新编译所有客户端,则要求所有客户端使用特定版本的 C/C++ 运行时似乎是合理的。
但在现实生活中,人们往往不想冒这种风险。”如果它程序还能跑,就不要管它。” 切换到新的编译器可能会暴露一个微妙的错误,例如,忘记将变量声明为易失性(volatile)变量,或者无意中依赖于具有特定生存期的临时变量。
在实践中,你可能希望仅将程序的一部分转换为新的编译器,而不理会旧模块。(例如,你可能希望利用新的语言功能,如模板,这些功能仅在新编译器中可用。) 但是,如果这样做,则无法释放旧 DLL 分配的内存,因为该 DLL 希望你使用 MSVCRT20.DLL,而新编译器使用 MSVCR71.DLL。
解决这个问题的方法是:提前做好规划。
一种方法是:使用固定的外部分配器,例如 LocalAlloc 或 CoTaskMemAlloc。这些都是通用的内存分配器,不依赖于所使用的编译器版本。
另一种方法:将内存分配器封装在管理分配的导出函数中。这是 NetApi 系列函数使用的机制。例如,NetGroupEnum 函数分配内存并通过 bufptr 参数返回内存。调用方使用完内存后,它会使用 NetApiBufferFree 函数释放内存。以这种方式,内存分配方法与调用方隔离。在内部,NetApi 函数可能使用 LocalAlloc 或 HeapAllocate 或 HeapAllocate 函数,甚至可能是 new 或者 free。这不重要; 只要 NetApiBufferFree 使用最初用于分配内存的 NetGroupEnum 所用的相同分配器释放内存。
虽然我个人更喜欢使用固定的外部分配器,但许多人发现使用包装器技术更方便。
这样,他们就可以在整个模块中使用自己喜欢的分配器。无论哪种方式都可以正常工作。关键是,当内存离开 DLL 时,你提供内存的代码必须知道如何释放它,即使它使用的编译器与用于生成 DLL 的编译器不同。
总结
作为一名 C++ 程序员,你不能像其他带 GC 语言的程序员那样粗心大意。
对内存的管理,需要你的头脑十分冷静和清醒,一旦出现内存方面的问题,你不得不花费巨量的时间来追踪。
但这个解决问题的过程也是十分有乐趣的,这是对 C++ 程序员的馈赠。
最后
Raymond Chen的《The Old New Thing》是我非常喜欢的博客之一,里面有很多关于Windows的小知识,对于广大Windows平台开发者来说,确实十分有帮助。
本文来自:《Allocating and freeing memory across module boundaries》