cross-dll C++在不同模块内分配和释放内存问题

实际问题

在工作中,需要做一个网络通讯功能,领导要求封装为动态库;

动态库的简单工作流程和接口:

  • 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问题的。

解决方法

  1. 导出一个内存释放接口,专门用来在库中释放由Read接口中分配的内存
  2. 导出一个获取缓存大小的接口,在调用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

以上代码只实现了最基本的内存管理;

与普通的内存管理最主要的不同点为:

  1. release()方法内使用提前保存的内存释放函数进行释放内存(funDestoryBuffer fdb);而不是直接调用delete[] buffer;
  2. fdb在load方法内分配内存时,更新为当前模块内的数据释放函数;
  3. 若load在上层代码中调用,则load方法内第85行buffer = new char[len]()的new操作符为上层代码中的new,所以第89行更新fdb为上层代码中的内存释放函数;
  4. 若load在库内调用,则load方法内第85行buffer = new char[len]()的new操作符为库中的new,所以第89行更新fdb为库中的内存释放函数;
  5. 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

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值