实际问题
在工作中,需要做一个网络通讯功能,领导要求封装为动态库;
动态库的简单工作流程和接口:
- Init: 初始化
- 读取对应文件中的网络配置参数;
- Open: 打开
- Open接口有个参数为回调函数,打开成功后网络库开始子线程工作,若接收到数据,调用回调函数,通知上层代码;
- Read: 读取数据
- 在工作线程接收到数据后,会将数据缓存起来,通知上层代码,此时上层代码可调用Read接口读取到缓存的数据;
- Close: 关闭网络
- 停止工作;
初版完成之后,Read接口类型为:
bool Read(char*& buffer, int& nBufferSize);
由于上层代码不能确认当前缓存中的数据大小,所以需要在接口Read内部进行内存分配,故传递指针的引用(char*&);
Read接口内会根据当前缓存中的数据大小为buffer分配内存,并将缓存数据拷贝到分配的内存中;
正常使用时,其他功能一切正常,但在释放Read接口中分配的内存时出现问题:
(为简化代码省略一些有效性判断)
{
char* buffer = NULL;
int nSize = 0;
net.Read(buffer, nSize);
//do something
...
delete[] buffer;
buffer = NULL;
}
问题出现在上述第九行,在释放buffer时程序崩溃;
问题分析
经查阅资料,此问题为cross-dll问题,即:
一个模块分配的内存不应该由另一个模块释放,因为两个模块可能使用不同版本的C运行期库,甚至不使用C运行期库。
malloc/free(引用自:https://blog.csdn.net/wangqing_199054/article/details/19402767)
这两个函数是使用频率最高的两个函数,由于他们是标准C库中的一部分,所以具有极高的移植性。这里的"移植性"指的是使用他们的代码可以在不同的平台下编译通过,而不同的平台下的C Run-Time Library的具体实现是平台相关的,在Windows平台的C Run-Time Library中的malloc()和free()是通过调用Heap Memory API来实现的。值得注意的是C Run-Time Library拥有独立的Heap对象,我们知道,当一个应用程序初始化的时候,首先被初始化的是C Run-Time Library,然后才是应用程序的入口函数,而Heap对象就是在C Run-Time Library被初始化的时候被创建的。对于动态链接的C Run-Time Library,运行库只被初始化一次,而对于静态连接的运行库,每链接一次就初始化一次,所以对于每个静态链接的运行库都拥有彼此不同的Heap 对象。这样在某种情况下就会出问题,导致程序崩溃,例如一个应用程序调用了多个DLL,除了一个DLL外,其他的DLL,包括应用程序本身动态连接运行库,这样他们就使用同一个Heap对象。而有一个DLL使用静态连接的运行库,它就拥有一个和其他DLL不同的Heap 对象,当在其他DLL中分配的内存在这个DLL中释放时,问题就出现了。
也就是说,两个都使用动态运行期库的模块(exe或dll)之间不存在cross-dll问题,而使用静态运行期库的模块,与其它模块之间,即使也是使用静态运行期库的模块,总是存在cross-dll问题的。
解决方法
- 导出一个内存释放接口,专门用来在库中释放由Read接口中分配的内存
- 导出一个获取缓存大小的接口,在调用Read前先使用此接口,获取当前缓存的大小,并在库外分配好buffer的大小,Read接口内仅进行内存拷贝操作;
//释放内存接口
void DeleteStack(char* buffer) {
if (buffer) delete buffer;
}
//缓存大小获取接口
int BufferSize() {
return size;
}
以上两种方法都可解决当前问题,比较有针对性和单一性。
使用智能指针
C++中的智能指针也可以解决此问题;
智能指针在创建对象时记录了对象的析构函数指针,所以如果以智能指针作为接口参数用来传递数据,那么在库内分配的内存以智能指针释放时,不管在库内库外,都是调用了库内的释放操作;
自定义数据管理类
由于有些旧的C++版本还不支持智能指针或不支持智能数组指针,所以可以自己根据智能指针的原理做一个简单的数据管理类,代码如下:
#ifndef NETBUFFER_H
#define NETBUFFER_H
class NetBuffer
{
typedef void (*funDestoryBuffer)(char*);
static void DestoryBuffer(char* buffer) {
if (buffer)
delete[] buffer;
}
public:
NetBuffer(const char* d = NULL, int l = 0)
: buffer(NULL)
, len(0)
, fdb(&NetBuffer::DestoryBuffer)
{
load(d, l);
}
NetBuffer(const NetBuffer& right)
{
load(right.buffer, right.len);
}
NetBuffer& operator=(const NetBuffer& right)
{
if (this == &right) {
return *this;
}
load(right.buffer, right.len);
}
~NetBuffer()
{
release();
}
public:
operator const char*() { return len > 0 ? buffer : NULL; }
const char* data() { return len > 0 ? buffer : NULL; }
int size() { return len; }
void swap(NetBuffer& right) //数据交换
{
if (this == &right) {
return;
}
char* tempB = right.buffer;
int tempL = right.len;
funDestoryBuffer tempF = right.fdb;
right.buffer = buffer;
right.len = len;
right.fdb = fdb;
buffer = tempB;
len = tempL;
fdb = tempF;
}
private:
void release()
{
if (fdb) {
fdb(buffer);
}
buffer = NULL;
len = 0;
}
void load(const char* d, int s)
{
release();
if (d && (s > 0))
{
len = s;
buffer = new char[len]();
memcpy(buffer, d, len);
//调用了new char[],需要更新fdb为当前模块的函数指针
fdb = &NetBuffer::DestoryBuffer;
}
}
private:
char* buffer; //数据
int len; //数据大小
funDestoryBuffer fdb; //数据释放函数
};
#endif //NETBUFFER_H
以上代码只实现了最基本的内存管理;
与普通的内存管理最主要的不同点为:
- release()方法内使用提前保存的内存释放函数进行释放内存(funDestoryBuffer fdb);而不是直接调用delete[] buffer;
- fdb在load方法内分配内存时,更新为当前模块内的数据释放函数;
- 若load在上层代码中调用,则load方法内第85行buffer = new char[len]()的new操作符为上层代码中的new,所以第89行更新fdb为上层代码中的内存释放函数;
- 若load在库内调用,则load方法内第85行buffer = new char[len]()的new操作符为库中的new,所以第89行更新fdb为库中的内存释放函数;
- swap方法中,数据交换,fdb也需要同步交换;
修改后的Read接口为:
bool Read(NetBuffer& buffer)
{
//内存拷贝 srcData和srcSize为实际缓存数据
//此时temp会拷贝数据,并记录库内的内存释放函数指针
NetBuffer temp(srcData, srcSize);
//交换数据,将temp中的数据和内存释放函数替换到buffer中,此时buffer在释放时调用的即为库内的内存释放函数;
buffer.swap(temp);
}
//接口调用:
void test()
{
NetBuffer buffer;
net.Read(buffer);
//do something
...
}
End