跨模块内存管理的陷阱
许式伟
2004年6月21日
1) 不能够在一个模块的引出函数、接口中申请内存并且返回出去,让另一模块释放它。
- 考虑提供一个函数,让外部模块获得所需的内存大小,让外部模块申请内存并传入。这是一个比较典型的解决方案,Windows的API均采用此解决方案。
- 考虑使用一个接口包装此内存的访问,让外部模块获得接口指针,并以此访问内存。内存是通过接口的Release()函数释放的。这样就保证了内存释放的正确性。
- 考虑使用CoTaskMemAlloc/CoTaskMemFree申请、释放内存。因为内存的申请、释放由系统完成,故可以保证其一致性。
- 作为条目3. 的特殊情形,如果返回的是字符串,可考虑用BSTR。此时资源管理由系统调用SysAllocString/SysFreeString 实现。
- 仍然在内部申请内存并返回出去,同时将该内存的释放函数也作为引出函数引出去。外部模块使用完该内存后,用我们引出的释放函数释放它。这是可行的方案,虽然比较少见。你可以认为其实CoTaskMemAlloc/CoTaskMemFree、SysAllocString/SysFreeString也是基于这条规则提供的,只不过它没有特定目的而已。
◆注意◆
有时候出于某种考虑(例如检测内存资源泄漏),我们可能提供一个自己实现的Win32 API版本来取代Windows的系统调用。
我们知道,如果你使用CoTaskMemAlloc/CoTaskMemFree、SysAllocString/SysFreeString来申请、释放内存,那么哪怕存在内存泄漏,我们在《最快速度找到内存泄漏》中介绍的方法并不能检测出来。
除了使用一些系统资源泄漏的检测工具(其实它们的方法和我们这里介绍的肯定也类似)外,一种方法,就是提供这些API的替换版本。这些替换版本中,我们提供了泄漏检测的能力。
我们这里并不准备详细讨论这个技巧。但是请注意,这里存在的潜在危险是,有可能出现这样的情形:设想我们的某DLL使用了替换版本的SysAllocString,其中申请了一个BSTR返回给另一DLL,而该DLL并不使用替换版本的SysFreeString,而是调用系统的SysFreeString释放这个BSTR。这里存在的问题是显然的,因为系统并没有分配过这样一个BSTR。
2) 不能够在参数列表或返回值中用到类。
这是因为:
- 同一个类,相同的声明,在不同的编译器、甚至不同的编译模式下,会有不同的内存布局。也就是说,看似是同一个类,但是其实在不同的模块中,理解上根本不同。例如,你用VC++写一个DLL,该DLL返回一个std::string,而DLL的客户程序是C++ Builder写的。你能够保证C++ Builder的std::string与VC++的内存布局一致吗?
- 类存在成员函数(特别是构造、析构),这些成员函数对我们来说是个黑箱操作。对他们的调用同样容易产生这样的情况,就是在我们的模块中申请了内存,而在外部模块中(由析构函数)释放。仍然以std::string为例。我们往往为了方便返回一个字符串,而将函数声明为:
⑴ std::string getXXX();
或者:
⑵ getXXX(std::string& str);
这种方式在同一模块中是可行的,而且是相对比较高效的方式。但是如果用于跨模块的字符串传递,则存在风险(并不一定会出问题,关于什么时候不出问题,我们下一回讨论)。
遇到这种要用类的情形。请尝试采用以下方案:
- 考虑采用纯结构体。
- 考虑使用一个接口包装该类,将该类实现为COM组件。
- 如果返回的是字符串,考虑用BSTR。
纯结构体
- 没有任何虚拟的成分。如虚函数、虚拟继承等。
- 它的所有成员变量,均为简单数据类型(C标准数据类型,不包括指针),或者是另一个“纯结构体”。
- 如果成员变量是一个指针,那么要么作为输入参数,指向的内容是一个纯结构体或C标准字符串;要么作为输出参数,指向的内容是一系统分配的资源。
{
std:: string strA;
std:: string strB;
};
struct XXXX
{
...
};
#pragam pack(pop)
附加说明:
对“内存管理”相关的技术感兴趣?这里可以看到我的所有关于内存管理的文章。