浅谈C++跨模块释放内存

     在开发主程序和动态库时,首要原则就是:避免跨模块申请和释放内存。这一点,我们在很多开源库或者平常项目中也都碰到过,对于动态库中的堆内存申请与释放,动态库总是会提供两个接口分别实现new和delete操作,而不会让调用方自己去操作。但有时候如果违背了这个原则呢,在linux平台上不会存在这样的忧虑,因为在linux下,每个进程只有一个heap,在任何一个动态库模块so中通过new或者malloc来分配内存的时候都是从这个唯一的heap中分配的,那么自然你在其它随便什么地方释放都是没问题的。这个模型是简单的。而windows下就变得复杂了,下面主要介绍一下windows下的主程序和dll之间跨模块内存释放的问题。

     windows允许一个进程中有多个heap,那么当需要在堆上分配内存时就要指明在哪个heap上分配,win32提供了HeapAlloc函数可以在指定的堆上分配内存。这样的设计虽然比较灵活,但是问题在于,每次分配内存的时候就必须要显式的指定一个heap,对于crt中的new/malloc,显然需要特殊处理。那么如何处理就取决于crt的实现了。vc的crt是创建了一个单独的heap,叫做__crtheap,它对于用户是看不见的,但是在new/malloc的实现中,都是用HeapAlloc在这个__crtheap上分配的,也就是说malloc(size)基本上可以认为等同于HeapAlloc(__crtheap, size)(当然实际上crt内部还要维护一些内存管理的数据结构,所以并不是每次malloc都必然会触发HeapAlloc),这样new/malloc就和windows的heap机制吻合了。

     如果一个进程需要动态库支持,系统在加载dll的时候,在dll的启动代码_DllMainCRTStartup中,会创建这个__crtheap,所以理论上有多少个dll,就有多少个__crtheap。最后主进程的mainCRTStartup 中还会创建一个为主进程服务的__crtheap。(由于顺序总是先加载dll,然后才启动main进程,所以你可以看到各个dll的__crtheap地址比较小,而主进程的__crtheap比较大,当然排在最前面的堆是每个进程的主heap。)

     由此可见,对于crt来说,由于每个dll都有自己的heap,所以每个dll通过new/malloc分配的内存都是在自己dll内部的那个heap上用HeapAlloc来分配的,而如果你想在其它模块中释放,那么在释放的时候HeapFree就会失败了,因为各个模块的__crtheap是不一样的。

     那么如果有非要用到跨模块释放的场景呢,可以使用以下几种方式来解决:

一, MT改MD

     一个进程的地址空间是由一个可执行模块和多个DLL模块构成的,这些模块中,有些可能会链接到C/C++运行库的静态版本,有些可能会链接到C/C++运行库的DLL版本。当使用运行库的DLL版本时,由于dll加载到进程中只会在地址空间中存有一份,因此共用的是同一个堆。所以将可执行模块和DLL模块统一修改为MD编译,则可以直接实现跨模块之间的内存申请和释放,而不会存在任何问题。
     更多MT和MD,以及DLL和进程地址空间的知识可以参见博客:DLL和进程的地址空间

二, DLL提供释放接口

     DLL提供统一的对外接口,供外部模块(可执行模块或其它DLL模块)调用,由该DLL内部来进行内存的释放。简单实现如下:

void __stdcall MyFree(void *ptr)
{
	if (ptr)
	{
		free(ptr);
	}
}
void __stdcall MyDelete(void *ptr)
{
	if (ptr)
	{
		delete ptr;
	}
}
void  __stdcall MyDeleteArray(void *ptr)
{
	if (ptr)
	{
		delete[] ptr;
	}
}

三, 使用进程堆申请内存

     在一个进程中,可执行模块和DLL模块都属于同一个进程地址空间,而每个进程又都有一个为主进程服务的堆(一般也称为进程的默认堆),当我们需要跨模块进行内存申请和释放时,可以在进程主堆上进行申请,同样地,释放时,也直接在进程主堆上进行释放,这样就可以不用考虑MT导致的跨进程释放的问题。API的使用此处不讲解,直接附上简易代码:
在DLL中:

void* __stdcall Test(int *len)
{
	void* pData = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, 100);
	if (pData == NULL)
		return NULL;
	//使用的是HEAP_ZERO_MEMORY,会自动把内存块的内容都清零
	//下面这行代码可以不要的
	memset(pData, 0, 100);

	char pBuf[] = "十点十分十分十分";
	memcpy(pData, pBuf, sizeof(pBuf));
	*len = 100;
	return pData;
}

在可执行模块中:

int main()
{
	HMODULE hLib = LoadLibraryA("Dll1.dll");
	if (nullptr == hLib)
	{
		std::cout << "LoadLibraryA fail, error:" << GetLastError() << std::endl;
		return 0;
	}

	Fun fun = (Fun)GetProcAddress(hLib, "Test");
	if (nullptr == fun)
	{
		std::cout << "GetProcAddress fail, error:" << GetLastError() << std::endl;
		return 0;
	}

	int nLen = 0;
	char *pData = (char*)fun(&nLen);

	std::string strTemp(pData, nLen);

	HeapFree(GetProcessHeap(), 0, pData);

	std::cout << strTemp << std::endl;

	return 0;
}

     使用默认的进程堆来申请内存还需要注意,很多Windows系统函数都用到了进程的默认堆,而且应用程序会可能有多个线程同时要调用各种windows函数,因此系统保证不管在什么时候,一次只让一个线程从默认堆中分配或者释放内存快。当两个线程同时想要从默认堆中分配一块内存,那么只有一个线程能够分配,另一个线程必须等待第一个线程的分配完成。这种依次访问对性能会有轻微影响,在一般的应用程序中可以忽略不计,对性能要求较高的程序需要注意。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Simple Simple

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值