由于编译器、编译模式的不同,不同模块的内存结构与管理程序往往并不相同。因此,如果我们在一个模块申请内存,而在另一个模块中释放,这是一个不安全的做法。因为模块在释放内存的时候,并不会预料到需要释放的内存并非是自己管理的。
直观的说,假设我们有两个模块:Module1,Module2。它们有函数Module1.alloc,Module1.free;Module2.alloc, Module2.free。虽然同为alloc和free,但是你不能假设Module1.alloc/free和Module2.alloc/free是同一份代码。因此,Module1.alloc申请的资源,只有由Module1.free去释放才是确保安全的。
展开来讲,我们通常有以下跨模块调用约定:
1) 不能够在一个模块的引出函数、接口中申请内存并且返回出去,让另一模块释放它。
如果有此需求,尝试用以下几种解决方案:
- 考虑提供一个函数,让外部模块获得所需的内存大小,让外部模块申请内存并传入。这是一个比较典型的解决方案,Windows的API均采用此解决方案。
WWW_PLCJS※COM-PLC-技×术_网(可编程控※制器技术门户)
- 考虑使用一个接口包装此内存的访问,让外部模块获得接口指针,并以此访问内存。内存是通过接口的Release()函数释放的。这样就保证了内存释放的正确性。
WWW_PLCJS※COM-PLC-技.术_网(可※编程控※制器技术门户)
- 考虑使用CoTaskMemAlloc/CoTaskMemFree申请、释放内存。因为内存的申请、释放由系统完成,故可以保证其一致性。
WWW_PLC※JS_COM-PLC-技.术_网(可编程控※制器技术门户)
- 作为条目3. 的特殊情形,如果返回的是字符串,可考虑用BSTR。此时资源管理由系统调用SysAllocString/SysFreeString 实现。
P_L_C_技_术_网——可——编——程——控-制-器-技——术——门——户
- 仍然在内部申请内存并返回出去,同时将该内存的释放函数也作为引出函数引出去。外部模块使用完该内存后,用我们引出的释放函数释放它。这是可行的方案,虽然比较少见。你可以认为其实CoTaskMemAlloc/CoTaskMemFree、SysAllocString/SysFreeString也是基于这条规则提供的,只不过它没有特定目的而已。
◆注意◆
有时候出于某种考虑(例如检测内存资源泄漏),我们可能提供一个自己实现的Win32 API版本来取代Windows的系统调用。
WWW※PLCJS_COM-PL#C-技.术_网(可编※程控※制器技术门户)
我们知道,如果你使用CoTaskMemAlloc/CoTaskMemFree、SysAllocString/SysFreeString来申请、释放内存,那么哪怕存在内存泄漏,我们在《最快速度找到内存泄漏》中介绍的方法并不能检测出来。
WWW_PLCJ-S_COM-PLC-技.术_网(可-编程控-制器技术-门户)
除了使用一些系统资源泄漏的检测工具(其实它们的方法和我们这里介绍的肯定也类似)外,一种方法,就是提供这些API的替换版本。这些替换版本中,我们提供了泄漏检测的能力。
WWW_PLCJS※COM-PLC-技×术_网(可编程控※制器技术门户)
我们这里并不准备详细讨论这个技巧。但是请注意,这里存在的潜在危险是,有可能出现这样的情形:设想我们的某DLL使用了替换版本的SysAllocString,其中申请了一个BSTR返回给另一DLL,而该DLL并不使用替换版本的SysFreeString,而是调用系统的SysFreeString释放这个BSTR。这里存在的问题是显然的,因为系统并没有分配过这样一个BSTR。
——可——编——程——控-制-器-技——术——门——户
2) 不能够在参数列表或返回值中用到类。
这是因为:
WWW※PLCJS_COM-PL#C-技.术_网(可编※程控※制器技术门户)
- 同一个类,相同的声明,在不同的编译器、甚至不同的编译模式下,会有不同的内存布局。也就是说,看似是同一个类,但是其实在不同的模块中,理解上根本不同。例如,你用VC++写一个DLL,该DLL返回一个std::string,而DLL的客户程序是C++ Builder写的。你能够保证C++ Builder的std::string与VC++的内存布局一致吗?
WWW_P※LCJS_CO※M-PLC-技-.术_网
- 类存在成员函数(特别是构造、析构),这些成员函数对我们来说是个黑箱操作。对他们的调用同样容易产生这样的情况,就是在我们的模块中申请了内存,而在外部模块中(由析构函数)释放。仍然以std::string为例。我们往往为了方便返回一个字符串,而将函数声明为:
WWW_PLCJS@_COM%-PLC-技.术_网
⑴ std::string getXXX();WW.W_PLC※JS_C,OM-PL,C-技.术_网
或者:WWW※PLCJS_COM-PL#C-技.术_网(可编※程控※制器技术门户)
⑵ getXXX(std::string& str);WW.W_PLCJS_COM-PLC-技.术_网
这种方式在同一模块中是可行的,而且是相对比较高效的方式。但是如果用于跨模块的字符串传递,则存在风险(并不一定会出问题,关于什么时候不出问题,我们下一回讨论)。
遇到这种要用类的情形。请尝试采用以下方案:
W1WW_P4LCJS_COM-PLC-技.术_网
- 考虑采用纯结构体。
- 考虑使用一个接口包装该类,将该类实现为COM组件。
- 如果返回的是字符串,考虑用BSTR。
WWW_P※LCJS_CO※M-PLC-技-.术_网
纯结构体
所谓“纯结构体”,是指该结构体:
- 没有任何虚拟的成分。如虚函数、虚拟继承等。
WWW_PL※CJS_COM-PLC-技.术_网
- 它的所有成员变量,均为简单数据类型(C标准数据类型,不包括指针),或者是另一个“纯结构体”。
WWW_PLCJS_COM-PLC-技.术_网
- 如果成员变量是一个指针,那么要么作为输入参数,指向的内容是一个纯结构体或C标准字符串;要么作为输出参数,指向的内容是一系统分配的资源。
总的说来一句话,就是“纯结构体”成员的类型要求,完全等同模块的引出函数参数类型的要求。
纯结构体在接口定义中比较广泛,往往用于取代在接口使用类的需求。对于我们规范中的“不允许使用类”,有一个误区是,使用了一个struct关键字定义的,本质上还是类的东西。例如:
struct AStructW1WW_P4LCJS_COM-PLC-技.术_网
{WWW_PLCJS@_COM%-PLC-技.术_网
std::string strA;P_L_C_技_术_网——可——编——程——控-制-器-技——术——门——户
std::string strB;WWW※PLCJS_COM-PL#C-技.术_网(可编※程控※制器技术门户)
};
这个struct有构造、析构(尽管没有显式写出,编译器帮你生成的),析构中有内存释放操作,是一个标标准准的“类”。
另外,结构体需要显式指定字节对齐方式。例如:
#pragma pack(push, 1)WWW_P※LCJS_CO※M-PLC-技-.术_网
struct XXXXWWW_PLC※JS_COM-PLC-技.术_网(可编程控※制器技术门户)
{P.L.C.技.术.网——可编程控制器技术门户
...WWW_PLCJS※COM-PLC-技×术_网(可编程控※制器技术门户)
};WWW_PL※CJS_COM-PLC-技.术_网
#pragam pack(pop)