如果程序因错误而提前退出,那么释放动态分配的内存的正确方法是啥?

Posted

技术标签:

【中文标题】如果程序因错误而提前退出,那么释放动态分配的内存的正确方法是啥?【英文标题】:What is the correct way of freeing dynamically allocated memory if the program can exit early because of errors?如果程序因错误而提前退出,那么释放动态分配的内存的正确方法是什么? 【发布时间】:2018-11-01 09:28:13 【问题描述】:

我一直都知道,在代码中慷慨地进行错误检查并在满足某些条件时提供错误消息是很好的。考虑以下结构的程序:

int main(const int argc, const char *argv[]) 

// read two integers M and N from input file 1 provided in argv[]
// read two integers K and L from input file 2 provided in argv[]

if (M*N < 1000000) 
    allocate array A
 else 
    printf("With your input values, the matrix will be too large!");
    return 1;


if (K*L < 1000000) 
    allocate array B
 else 
    printf("With your input values, the matrix will be too large!");
    return 1;


// multiply arrays elementwise
// free memory
return 0;

暂时忽略此代码可以重新组织,以便在分配发生之前检查输入参数的有效性(这将是这个简单示例中的直接解决方案)。我的实际问题将在下面为有兴趣的人详细说明。

如果在上面的伪代码中 A 被成功分配,但 B 没有因为 K*L 超过限制,程序末尾的内存空闲语句没有到达,与 A 相关联的内存泄漏。如果无法如上所述重组代码,是否是避免此问题的最佳方法?删除return 1; 可能会进一步导致其他错误。我能想到的另外两个选项是使用臭名昭著的 goto 语句(我不敢),或者确实在条件 as 中包含所有免费调用;

if (K*L < 1000000) 
    allocate array B
 else 
    printf("With your input values, the matrix will be too large!");
    free(A);
    return 1;

,但是当处理大量动态分配的数组时,这将意味着大量的代码重复。那么:对于中间错误检查和内存处理的组合,推荐什么样的程序结构?

我的实际情况: 我正在使用 CUDA,它(对于那些不熟悉它的人)允许人们编写要在 GPU 上执行的代码。与此 API 关联的所有(或大多数)代码都返回一个错误标志,建议在程序继续之前对其进行检查。就我而言,在我的程序中的某个时刻,我会像这样在 GPU 上分配内存(对于熟悉 C 的人来说应该是可读的):

double *dev_I;
cudaError_t err;
err = cudaMalloc(&dev_I, N*M, sizeof(*dev_I)); // note: cudaMalloc takes a double pointer
if (err != cudaSuccess)  printf("Error allocating dev_I.\n"); return 1; 

重要的部分来了:下一步是将内存从主机复制到 GPU 可以访问的地方。这看起来有点像这样:

err = cudaMemcpy(dev_I, host_I, M*N * sizeof(*host_I), cudaMemcpyHostToDevice);
if (err != cudaSuccess)  printf("Error copying dev_I"); return 1; 

这是我关心的部分。如果分配成功但内存复制失败,程序将(应该)退出,我将留下内存泄漏。因此,我的问题是如何最好地处理。

【问题讨论】:

goto 的经典案例...为退出状态添加变量,初始化所有指向 0 的指针(所以 free() 不会受到伤害)并使用类似 goto cleanup; on错误。 goto 在 C 语言中的名声很差。今天关于它的许多态度更多地符合宗教信仰,而不是知情理解。 大多数 情况都有更好的替代方案,但有时goto 是产生最干净、最清晰代码的工具。跳转到远处的错误处理代码通常是其中一种情况。 退出前是否强制释放所有内存是一个见仁见智的问题。正如这个问题所证明的那样,它可能很棘手的事实是支持担心在退出之前释放所有内存的有力论据之一。有关这一点的讨论,请参阅 this question。 不,您不会出现内存泄漏。所有 GPU 内存分配都由 GPU 上属于终止进程的 CUDA 运行时自动释放。从编译的角度来看,IMO 使用 goto 非常烦人,您很快就会发现。 感谢您的回复,罗伯特。最后一句话,你的意思是它大大减慢了编译速度吗?那么你怎么做呢,你根本不使用 cudaFree() 吗?还是您使用我在第一篇文章中提到的一种方式? 【参考方案1】:

C 中的一般结构化编程需要goto,如下:

int f() 
  void *p = alloc_resource();
  if (p == NULL) goto error_1;

  void* q = alloc_resource();
  if (q == NULL) goto error_2;

  return g(p, q);  // g takes ownership

error_2:
  dealloc_resource(p);
error_1:
  return -1;  // some error code

您可以看到这种模式如何推广到任意数量或所有权收购:您基本上必须保留一个清理操作列表,并在您的控制流遇到错误以及您已建立的所有状态时跳转到适当的操作到目前为止需要展开。

通常,人们会尽量避免过于复杂的函数,方法是让每个函数保持较小,这样清理就可以在没有gotos 的情况下以更特别的方式完成,但有时你需要这样的结构(例如,当你正在组成多个所有权位)。

(其他语言为多个出口提供语言级别的支持,例如在 C++ 中,此逻辑由所谓的“析构函数”提供。它们几乎只以更可组合和可重用的方式影响相同的事情,并生成所有跳转由编译器。)

【讨论】:

requires 被认为是有害的 ;) @500-InternalServerError:重复次数最多,上下文中最不理解? :-) 这听起来可能令人惊讶,但它确实是您以系统方式处理任意复杂性的唯一方法。但我认为大多数人更喜欢不具有任意复杂性,而是坚持使用非常小的功能(这是一件好事!),或者可能选择不完全处理所有权并接受“良性泄漏”。 (我认为应该被“视为有害”的是代码的猖獗意大利面条化,而不是结构化的退出。) 感谢Kerrek 提供此示例。我想这与 Felix Palmen 的建议很好地结合在一起,在程序开始时将所有指针初始化为 null,这可以在所有 free() 调用之前将goto 语句的数量减少到一个(可能还有其他文件关闭,等)。 @FlorisSA:我不确定这有什么关系。除非您使用 40 年前的 C89 进行编程,否则您可以在初始化时声明变量:void *p = f(); CHECK; void* q = g(); CHECK; 等。请注意,早期的错误会跳转到后面的标签,因此错误处理甚至不必考虑没有'还没有变得相关。并且即使您需要在开始时声明变量,同样的推理表明您的清理代码永远不会触及未初始化的变量。【参考方案2】:

除了@Kerrek SB好的答案,另一种方法是将所有对象分配/初始化为某个有效状态,测试所有资源的有效性,如果ok,则使用,然后免费。

关键思想是在对象的声明处(例如bar *A = NULL),对象A立即被赋予一个有效值-一些有效值。

int foo(void) 
  bar *A = NULL;
  bar *B = NULL;
  int error = 0;

  if (M*N < 1000000) 
    A = allocate(M, N);
   else 
    printf("With your input values, the matrix will be too large!");
    error = 1;
  

  if (K*L < 1000000) 
    B = allocate(K, L);
   else 
    printf("With your input values, the matrix will be too large!");
    error = 1;
  

  // Was resource allocation successful?
  if (A && B) 
   // multiply arrays
   mul(A,B);
  

  // free resources
  free(A);
  free(B);  
  return error;

伪代码大纲

Get resources for A
Get resources for B
Get resources for C
If (OK(A) && OK(B) && OK(C)) 
  Perform the operation(A,B,C)

Return resources(C)
Return resources(B)
Return resources(A)

我发现这种方法更容易测试。

【讨论】:

这种方法适用于freeAB 的成功和失败状态都允许的特殊情况。但更通用的资源可能有更严格的前提条件(例如fclose),您需要区分成功清理和失败清理。

以上是关于如果程序因错误而提前退出,那么释放动态分配的内存的正确方法是啥?的主要内容,如果未能解决你的问题,请参考以下文章

part03-动态内存分配

part03-动态内存分配

在 C 中释放内存需要啥?

在 C 中退出程序时释放所需的分配内存

退出程序后Crouton环境不释放内存?

C++程序的内存分区,为什么要使用动态内存,动态内存的分配使用释放