模块、库等等这间动态内存的申请与释放问题

冷风1023 2020-04-14 10:07:40
最近项目同事的模块之间遇到个问题。
A调用B的模块,但B申请的动态内存数据要返回给A用,A使用完忘了释放,导致内存泄露了。类似于这样的问题之前也出现过很多次。现在的解决办法有
1、谁申请谁释放原则,A申请然后把指针传给B,B返回给A,A释放。
2、B申请,然后接口文档中说明,A用完以后释放。
总觉得这两种都不是完美的方法,请教大家有没有好的方案。模块之间的调用不限制于C/C++,也有可能是java调用C等等。
...全文
269 11 打赏 收藏 转发到动态 举报
写回复
用AI写文章
11 条回复
切换为时间正序
请发表友善的回复…
发表回复
蓬 蒿 人 2020-04-14
  • 打赏
  • 举报
回复
我的想法:在B模块实现两个函数Init()和Uninit()分别分配和释放内存,然后再A模块中调用B::Init()和B::Uninit(),Init和Uninit函数中还能处理一些其他的准备工作和清理工作
Michael阿明 2020-04-14
  • 打赏
  • 举报
回复
一般你说的方案1可能稍好点吧,方案2可能会忘记释放。 另外最好用valgrind检测下有没有内存泄漏。
冷风1023 2020-04-14
  • 打赏
  • 举报
回复
忘了说了,是linux系统。
冷风1023 2020-04-14
  • 打赏
  • 举报
回复
大家可以讨论下。
冷风1023 2020-04-14
  • 打赏
  • 举报
回复
引用 10 楼 _mervyn 的回复:
[quote=引用 8 楼 冷风1023 的回复:]
回复5楼,我可以把你的函数简化为如下,如有不对还请指明,谢谢。


int ExampleFunc( const char* params, int size_in ,void* usr)
{
//...
std::string* p_data_out = (std::string*)usr;
p_data_out->append(data, size);
//...
}

这是完全不一样的,如果这样简化,p_data_out->append(data, size);这一句,已经相当于在B模块内部修改A模块地址内容(B模块真正申请了字符串内存)。这样返回到A模块,A模块的string不知道自己的实际内容是在B模块申请的,string释放时是A模块去释放的,如果两个模块运行时库不一致,这会引起程序崩溃。[/quote]

非常感谢你的回复,我明白你的意思了。
_mervyn 2020-04-14
  • 打赏
  • 举报
回复
引用 8 楼 冷风1023 的回复:
回复5楼,我可以把你的函数简化为如下,如有不对还请指明,谢谢。 int ExampleFunc( const char* params, int size_in ,void* usr) { //... std::string* p_data_out = (std::string*)usr; p_data_out->append(data, size); //... }
这是完全不一样的,如果这样简化,p_data_out->append(data, size);这一句,已经相当于在B模块内部修改A模块地址内容(B模块真正申请了字符串内存)。这样返回到A模块,A模块的string不知道自己的实际内容是在B模块申请的,string释放时是A模块去释放的,如果两个模块运行时库不一致,这会引起程序崩溃。
_mervyn 2020-04-14
  • 打赏
  • 举报
回复
引用 6 楼 冷风1023 的回复:
非常感谢你的回复,第一和第二种方法没什么问题,和我之前说的一样,关于第三种我有点疑问 //借用std::string接收内容,也可以是自己的数据类 std::string data_out; 这里和第一种自己申请自己释放的原则是一样的啊,只是你这里使用了栈变量,而我说的是申请内存变量一个道理啊
首先要明确一点的是,谁申请谁释放的原则是不能被破坏的。如果业务需要,你就不可能避开申请和释放内存。 方案1:调用方申请,调用方释放。缺点明显就能看到,调用方事先不知道应该申请多少内存。 方案2:被调方申请,被调方释放。缺点是要增加一个额外的接口用于给被调方释放。 以上两个方案,接口和内存的申请释放,是强相关的,强耦合的。 方案3:综合1和2的优缺点,还是采用被调方申请,被调方释放。但是被调方不需要额外提供释放接口。采用回调方式告知调用方结果。至于调用方你要不要申请内存,申请多少内存,跟被调方是完全无关的,是完全解耦的。只跟业务相关。调用方如果业务不需要,也完全可以不申请任何内存啊。 比如:

int ExampleCallback(int error,const char* data, int size,void* usr)
{
	if(error){
		return error;
	}
	//调用方直接在回调中使用data处理自己的业务。
	return 0;
}

//使用回调接收数据,直接在上面回调中处理结果
ExampleFunc(params, size_in, &ExampleCallback, NULL);
请理解我所说的方案3的 接口设计理念。 它做到了:在接口设计时,完全不用关心调用方如何如何,不用担心调用方知不知道该申请多少内存给我(方案1),也不用担心调用方会不会忘记调用我提供的另一个释放接口(方案2)。一旦担心的事情发生了,那我认为,被调方是需要承担责任的(接口设计不合理)。而方案3就不一样了,的确,在我之前说的例子中,调用方可能因业务需要,还是要在回调中申请内存保存结果,在回调外处理,那调用方还是可能忘记释放。但那又怎么样呢?关我什么事?那是你调用方自己的问题,我认为怪不到被调方的头上。 看下面这个例子:

int ExampleCallback(int error,const char* data, int size,void* usr)
{
	if(error){
		return error;
	}
	//接收data,无需关心data的释放。
	char** p_data_out = (char**)usr;
	*p_data_out = new char[size];
	memcpy(*p_data_out, data, size);
	return 0;
}

//回调外部,调用方代码:
char* data_out = NULL;
//使用回调接收数据
ExampleFunc(params, size_in, &ExampleCallback, &data_out);
//使用数据data_out
//...
//如果调用方忘记释放data_out,那就是调用方自己的责任。因为是调用方自己new的,new和delete都明确的在调用方模块内。
//delete data_out;
不过按上述例子写代码,是容易忘记delete的,因为new的代码和delete的代码虽然在同一模块内,但仍分割在了两个地方。所以我之前的例子是使用了string或者其他自己的数据结构,但即使不用,一旦出了问题,就不能怪罪到接口设计问题上了吧?接口设计的已经能够保证接口自己的内存一定被释放了,它的任务就完美完成了。剩下的是调用方自己代码问题了。 我们再来看回方案1和2: 方案1,接口直接不考虑申请内存,这个任务直接甩给调用方,当然不用担内存忘记释放的责任了,但是带来了另一个问题:调用方不知道申请多少内存。这一点是不合理的地方。 方案2,提供额外接口用于释放内存。这要求调用方明确知道这一规则,一个功能健壮的完成需要调用两个接口,这本身不太符合开发者习惯的。这一点是不合理的地方。开发者如果忘记调用释放接口,那么我认为,采用这种方案的接口设计者还是要承担一点责任的。 当然,方案3的设计就一定完美了吗?也不是。回调这种东西,多了之后,代码分割严重,会使得难以阅读。但我觉得相比较而言,我更喜欢方案3。
冷风1023 2020-04-14
  • 打赏
  • 举报
回复
回复5楼,我可以把你的函数简化为如下,如有不对还请指明,谢谢。
调用方调用例子:
//借用std::string接收内容,也可以是自己的数据类
std::string data_out;
//使用回调接收数据
ExampleFunc(params, size_in, &data_out);
//使用数据data_out
//...


int ExampleFunc( const char* params, int size_in ,void* usr)
{
//解析params
//...
//size_out = xxx;
//...

//根据params,生成结果data_out
char* data_out = new char[size_out];
//...
//memcpy(data_out,xxx,size_out);
//...

std::string* p_data_out = (std::string*)usr;
p_data_out->append(data, size);

//释放内存
delete data_out;
return ret;
}
冷风1023 2020-04-14
  • 打赏
  • 举报
回复
引用 4 楼 蓬 蒿 人 的回复:
我的想法:在B模块实现两个函数Init()和Uninit()分别分配和释放内存,然后再A模块中调用B::Init()和B::Uninit(),Init和Uninit函数中还能处理一些其他的准备工作和清理工作

这种方案在大型工程调用时经常使用,我所考虑的是模块功能很简单,例如只是一个字符串处理,如果采用这种初始化的方法反而会增加程序的复杂度和耦合度。
冷风1023 2020-04-14
  • 打赏
  • 举报
回复
引用 5 楼 _mervyn 的回复:
一般来说这种涉及跨模块间内存申请释放问题的接口,在设计上有3种方案。
方案1:接口明确接收内存地址和大小,由调用方申请内存,并传入内存地址和大小。被调方在接口内填充,返回调用方使用后由调用方释放。
例子:

/**
* @param const char* [in] 输入参数;
* @param int [in] 输入参数长度;
* @param char* [in] 输出参数,返回内容;
* @param int* [in,out] 输入输出参数,用户输入时指定data_out内存空间的长度,函数返回输出实际内容长度;
* @return 成功返回0,否则返回其他值;
*/
int ExampleFunc(const char* params,int size_in, char* data_out, int* out_size);

这种方案,需要调用方事先申请足够大的内存,所以接口一般还要设计成:接口因为调用方输入内存空间不够用时,告知调用方所需的内存大小。
所以,调用方可能需要调用两遍接口。例:

//第一遍确定所需内存空间
int out_size = 0;
ExampleFunc(params,size_in,NULL,&out_size);
//调用方申请内存
char* out_buf = new char[out_size];
//再次调用
ExampleFunc(params,size_in,out_buf,&out_size);
//...
//调用方释放
delete out_buf;


方案2:接口内部申请空间,并提供额外的接口用于释放这片内存。
例子:

/**
* @param const char* [in] 输入参数;
* @param int [in] 输入参数长度;
* @return 返回内容,使用完毕后需要调用FreeExample接口释放。
*/
char* ExampleFunc(const char* params, int size_in);

/**
* @param const char* [in] 输入参数;ExampleFunc的返回值
* @return 成功返回0,否则返回其他值;
*/
int FreeExample(const char* params);

该方案的缺点是,需要额外再设计一个接口供调用方使用,增加了调用方开发成本,且调用方很容易忘记这茬,甚至不知道还有个释放接口。
调用例子:

//调用功能,被调用方申请了内存并返回了结果。
char* out_buf = ExampleFunc(params,size_in);
//调用方使用
//...
//使用完毕后,调用专门的接口,由被调方释放
FreeExample(out_buf);


方案3:使用回调返回内容
例子:

/** @brief 功能接口调用完成通知;
* @param error [in] 错误码;
* @param data [in] 数据结果;
* @param size [in] 数据长度;
* @param usr [in] 用户数据;
* @return 成功返回0,失败返回其他值;
*/
typedef int (*COMPLETIONPROC)(int error,const char* data, int size,void* usr);

/**
* @param const char* [in] 输入参数;
* @param int [in] 输入参数长度;
* @param COMPLETIONPROC [in] 用于接收结果的回调函数地址;
* @param void* [in] 用户数据,会在在回调中原样返回;
* @return 成功返回0,否则返回其他值;
*/
int ExampleFunc( const char* params, int size_in, COMPLETIONPROC proc,void* usr);

个人比较推荐这种方案,该方案用户无需事先申请内存,就不需要事先知道需要多少内存大小。也不需要关心是否还有其他接口专门用来释放。并且该方案还能支持异步调用。
调用方调用例子:

//借用std::string接收内容,也可以是自己的数据类
std::string data_out;
//使用回调接收数据
ExampleFunc(params, size_in, &ExampleCallback, &data_out);
//使用数据data_out
//...

调用方实现的回调:

int ExampleCallback(int error,const char* data, int size,void* usr)
{
if(error){
return error;
}
//使用自己的数据结构接收data,无需关心data的释放。
std::string* p_data_out = (std::string*)usr;
p_data_out->append(data, size);
return 0;
}


该方案下,被调方内部实现的一个简单例子:

int ExampleFunc( const char* params, int size_in, COMPLETIONPROC proc,void* usr)
{
//解析params
//...
//size_out = xxx;
//...

//根据params,生成结果data_out
char* data_out = new char[size_out];
//...
//memcpy(data_out,xxx,size_out);
//...

//返回结果
if(proc)
{
ret = proc(0, data_out, size_out, usr);
}

//释放内存
delete data_out;
return ret;
}

非常感谢你的回复,第一和第二种方法没什么问题,和我之前说的一样,关于第三种我有点疑问
//借用std::string接收内容,也可以是自己的数据类
std::string data_out;
这里和第一种自己申请自己释放的原则是一样的啊,只是你这里使用了栈变量,而我说的是申请内存变量一个道理啊
_mervyn 2020-04-14
  • 打赏
  • 举报
回复
一般来说这种涉及跨模块间内存申请释放问题的接口,在设计上有3种方案。 方案1:接口明确接收内存地址和大小,由调用方申请内存,并传入内存地址和大小。被调方在接口内填充,返回调用方使用后由调用方释放。 例子:

/** 
 *  @param const char* [in]		输入参数;
 *  @param int [in]				输入参数长度;
 *  @param char* [in]			输出参数,返回内容;
 *  @param int* [in,out]		输入输出参数,用户输入时指定data_out内存空间的长度,函数返回输出实际内容长度;
 *  @return 成功返回0,否则返回其他值;
 */
int ExampleFunc(const char* params,int size_in, char* data_out, int* out_size);
这种方案,需要调用方事先申请足够大的内存,所以接口一般还要设计成:接口因为调用方输入内存空间不够用时,告知调用方所需的内存大小。 所以,调用方可能需要调用两遍接口。例:

//第一遍确定所需内存空间
int out_size = 0;
ExampleFunc(params,size_in,NULL,&out_size);
//调用方申请内存
char* out_buf = new char[out_size];
//再次调用
ExampleFunc(params,size_in,out_buf,&out_size);
//...
//调用方释放
delete out_buf;
方案2:接口内部申请空间,并提供额外的接口用于释放这片内存。 例子:

/** 
 *  @param const char* [in]		输入参数;
 *  @param int [in]				输入参数长度;
 *  @return 返回内容,使用完毕后需要调用FreeExample接口释放。
 */
char* ExampleFunc(const char* params, int size_in);

/** 
 *  @param const char* [in]		输入参数;ExampleFunc的返回值
 *  @return 成功返回0,否则返回其他值;
 */
int FreeExample(const char* params);
该方案的缺点是,需要额外再设计一个接口供调用方使用,增加了调用方开发成本,且调用方很容易忘记这茬,甚至不知道还有个释放接口。 调用例子:

//调用功能,被调用方申请了内存并返回了结果。
char* out_buf = ExampleFunc(params,size_in);
//调用方使用
//...
//使用完毕后,调用专门的接口,由被调方释放
FreeExample(out_buf);
方案3:使用回调返回内容 例子:

/** @brief 功能接口调用完成通知;
 *  @param error [in] 错误码;
 *  @param data [in] 数据结果;
 *  @param size [in]  数据长度;
 *  @param usr [in]  用户数据;
 *  @return 成功返回0,失败返回其他值;
 */
typedef int (*COMPLETIONPROC)(int error,const char* data, int size,void* usr);

/** 
 *  @param const char* [in]		输入参数;
 *  @param int [in]				输入参数长度;
 *  @param COMPLETIONPROC [in]	用于接收结果的回调函数地址;
 *  @param void* [in]			用户数据,会在在回调中原样返回;
 *  @return 成功返回0,否则返回其他值;
 */
int ExampleFunc( const char* params, int size_in, COMPLETIONPROC proc,void* usr);
个人比较推荐这种方案,该方案用户无需事先申请内存,就不需要事先知道需要多少内存大小。也不需要关心是否还有其他接口专门用来释放。并且该方案还能支持异步调用。 调用方调用例子:

//借用std::string接收内容,也可以是自己的数据类
std::string data_out;
//使用回调接收数据
ExampleFunc(params, size_in, &ExampleCallback, &data_out);
//使用数据data_out
//...
调用方实现的回调:

int ExampleCallback(int error,const char* data, int size,void* usr)
{
	if(error){
		return error;
	}
	//使用自己的数据结构接收data,无需关心data的释放。
	std::string* p_data_out = (std::string*)usr;
	p_data_out->append(data, size);
	return 0;
}
该方案下,被调方内部实现的一个简单例子:

int ExampleFunc( const char* params, int size_in, COMPLETIONPROC proc,void* usr)
{
	//解析params
	//...
	//size_out = xxx;
	//...	
	
	//根据params,生成结果data_out
	char* data_out = new char[size_out];
	//...
	//memcpy(data_out,xxx,size_out);
	//...
	
	//返回结果
	if(proc)
	{
		ret = proc(0, data_out, size_out, usr);
	}
	
	//释放内存
	delete data_out;
	return ret;
}

64,651

社区成员

发帖
与我相关
我的任务
社区描述
C++ 语言相关问题讨论,技术干货分享,前沿动态等
c++ 技术论坛(原bbs)
社区管理员
  • C++ 语言社区
  • encoderlee
  • paschen
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
  1. 请不要发布与C++技术无关的贴子
  2. 请不要发布与技术无关的招聘、广告的帖子
  3. 请尽可能的描述清楚你的问题,如果涉及到代码请尽可能的格式化一下

试试用AI创作助手写篇文章吧