跨模块内存管理的陷阱

跨模块内存管理的陷阱

许式伟
2004年6月21日

由于编译器、编译模式的不同,不同模块的内存结构与管理程序往往并不相同。因此,如果我们在一个模块申请内存,而在另一个模块中释放,这是一个不安全的做法。因为模块在释放内存的时候,并不会预料到需要释放的内存并非是自己管理的。
 
直观的说,假设我们有两个模块:Module1,Module2。它们有函数Module1.alloc,Module1.free;Module2.alloc, Module2.free。虽然同为alloc和free,但是你不能假设Module1.alloc/free和Module2.alloc/free是同一份代码。因此,Module1.alloc申请的资源,只有由Module1.free去释放才是确保安全的。
  
展开来讲,我们通常有以下跨模块调用约定: 

1)    不能够在一个模块的引出函数、接口中申请内存并且返回出去,让另一模块释放它。

如果有此需求,尝试用以下几种解决方案:
  1. 考虑提供一个函数,让外部模块获得所需的内存大小,让外部模块申请内存并传入。这是一个比较典型的解决方案,Windows的API均采用此解决方案。
     
  2. 考虑使用一个接口包装此内存的访问,让外部模块获得接口指针,并以此访问内存。内存是通过接口的Release()函数释放的。这样就保证了内存释放的正确性。
     
  3. 考虑使用CoTaskMemAlloc/CoTaskMemFree申请、释放内存。因为内存的申请、释放由系统完成,故可以保证其一致性。
     
  4. 作为条目3. 的特殊情形,如果返回的是字符串,可考虑用BSTR。此时资源管理由系统调用SysAllocString/SysFreeString 实现。
     
  5. 仍然在内部申请内存并返回出去,同时将该内存的释放函数也作为引出函数引出去。外部模块使用完该内存后,用我们引出的释放函数释放它。这是可行的方案,虽然比较少见。你可以认为其实CoTaskMemAlloc/CoTaskMemFree、SysAllocString/SysFreeString也是基于这条规则提供的,只不过它没有特定目的而已。

◆注意◆

有时候出于某种考虑(例如检测内存资源泄漏),我们可能提供一个自己实现的Win32 API版本来取代Windows的系统调用。

我们知道,如果你使用CoTaskMemAlloc/CoTaskMemFree、SysAllocString/SysFreeString来申请、释放内存,那么哪怕存在内存泄漏,我们在《最快速度找到内存泄漏》中介绍的方法并不能检测出来。

除了使用一些系统资源泄漏的检测工具(其实它们的方法和我们这里介绍的肯定也类似)外,一种方法,就是提供这些API的替换版本。这些替换版本中,我们提供了泄漏检测的能力。

我们这里并不准备详细讨论这个技巧。但是请注意,这里存在的潜在危险是,有可能出现这样的情形:设想我们的某DLL使用了替换版本的SysAllocString,其中申请了一个BSTR返回给另一DLL,而该DLL并不使用替换版本的SysFreeString,而是调用系统的SysFreeString释放这个BSTR。这里存在的问题是显然的,因为系统并没有分配过这样一个BSTR。

2)    不能够在参数列表或返回值中用到类。

这是因为:

  1. 同一个类,相同的声明,在不同的编译器、甚至不同的编译模式下,会有不同的内存布局。也就是说,看似是同一个类,但是其实在不同的模块中,理解上根本不同。例如,你用VC++写一个DLL,该DLL返回一个std::string,而DLL的客户程序是C++ Builder写的。你能够保证C++ Builder的std::string与VC++的内存布局一致吗?
     
  2. 类存在成员函数(特别是构造、析构),这些成员函数对我们来说是个黑箱操作。对他们的调用同样容易产生这样的情况,就是在我们的模块中申请了内存,而在外部模块中(由析构函数)释放。仍然以std::string为例。我们往往为了方便返回一个字符串,而将函数声明为:
        ⑴ std::string getXXX();
    或者:
        ⑵ getXXX(std::string& str);
    这种方式在同一模块中是可行的,而且是相对比较高效的方式。但是如果用于跨模块的字符串传递,则存在风险(并不一定会出问题,关于什么时候不出问题,我们下一回讨论)。 

遇到这种要用类的情形。请尝试采用以下方案:

  1.  考虑采用纯结构体
  2. 考虑使用一个接口包装该类,将该类实现为COM组件。
  3. 如果返回的是字符串,考虑用BSTR。 
     

纯结构体

所谓“纯结构体”,是指该结构体:
  1. 没有任何虚拟的成分。如虚函数、虚拟继承等。
     
  2. 它的所有成员变量,均为简单数据类型(C标准数据类型,不包括指针),或者是另一个“纯结构体”。
     
  3. 如果成员变量是一个指针,那么要么作为输入参数,指向的内容是一个纯结构体或C标准字符串;要么作为输出参数,指向的内容是一系统分配的资源。
总的说来一句话,就是“纯结构体”成员的类型要求,完全等同模块的引出函数参数类型的要求。
 
纯结构体在接口定义中比较广泛,往往用于取代在接口使用类的需求。对于我们规范中的“不允许使用类”,有一个误区是,使用了一个struct关键字定义的,本质上还是类的东西。例如:
struct  AStruct
{
   std::
string  strA;
   std::
string  strB;
};
这个struct有构造、析构(尽管没有显式写出,编译器帮你生成的),析构中有内存释放操作,是一个标标准准的“类”。
 
另外,结构体需要显式指定字节对齐方式。例如:
#pragma  pack(push, 1)
struct  XXXX
{
    ...
};
#pragam pack(pop)

附加说明:

对“内存管理”相关的技术感兴趣?这里可以看到我的所有关于内存管理的文章。

你可能感兴趣的:(struct,String,Module,dll,vc++,编译器)