【问题标题】:What is the best way to free memory after returning from an error?从错误返回后释放内存的最佳方法是什么?
【发布时间】:2010-10-08 21:23:45
【问题描述】:

假设我有一个为调用者分配内存的函数:

int func(void **mem1, void **mem2) {
    *mem1 = malloc(SIZE);
    if (!*mem1) return 1;

    *mem2 = malloc(SIZE);
    if (!*mem2) {
        /* ... */
        return 1;
    }

    return 0;
}

我想听听您对释放()分配内存的最佳方式的反馈,以防第二个 malloc() 失败。你可以想象一个更复杂的情况,有更多的错误退出点和更多的分配内存。

【问题讨论】:

    标签: c memory free malloc


    【解决方案1】:

    我知道人们不愿意使用它们,但这是 C 语言中 goto 的完美情况。

    int func( void** mem1, void** mem2 )
    {
        int retval = 0;
        *mem1 = malloc(SIZE);
        if (!*mem1) {
            retval = 1;
            goto err;
        }
    
        *mem2 = malloc(SIZE);
        if (!*mem2) {
            retval = 1;
            goto err;
        }
    // ...     
        goto out;
    // ...
    err:
        if( *mem1 ) free( *mem1 );
        if( *mem2 ) free( *mem2 );
    out:
        return retval;
    }      
    

    【讨论】:

    • goto 语句在某些情况下很好。我们在 linux 内核中经常看到这种类型的清理。它运行良好且易于理解。
    • @Steve Lazaridis:没错。使用 Linux 内核告诉我,有时 goto 是可以的。 :)
    • 除非您将 *mem1 和 *mem2 预置零,否则它不会可靠地工作。
    • @Jonathtan Leffler:我假设调用者是“优秀的 C 公民”。要么,要么,是的,你必须将它们归零。
    • 如果您将 *mems 预置零,则无需测试 if(*mem)。
    【解决方案2】:

    在我看来,这是适合使用 goto 的地方。我曾经遵循反 goto 教条,但是当有人向我指出 do { ... } while (0); 时,我改变了这一点。编译成相同的代码,但不那么容易阅读。只需遵循一些基本规则,例如不要使用它们,将它们保持在最低限度,仅将它们用于错误条件等......

    int func(void **mem1, void **mem2)
    {
        *mem1 = NULL;
        *mem2 = NULL;
    
        *mem1 = malloc(SIZE);
        if(!*mem1)
            goto err;
    
        *mem2 = malloc(SIZE);
        if(!*mem2)
            goto err;
    
        return 0;
    err:
        if(*mem1)
            free(*mem1);
        if(*mem2)
            free(*mem2);
    
        *mem1 = *mem2 = NULL;
    
        return 1;
    }
    

    【讨论】:

      【解决方案3】:

      这有点争议,但我认为 Linux 内核中使用的goto 方法实际上在这种情况下效果很好:

      int get_item(item_t* item)
      {
        void *mem1, *mem2;
        int ret=-ENOMEM;
        /* allocate memory */
        mem1=malloc(...);
        if(mem1==NULL) goto mem1_failed;
      
        mem2=malloc(...);
        if(mem2==NULL) goto mem2_failed;
      
        /* take a lock */
        if(!mutex_lock_interruptible(...)) { /* failed */
          ret=-EINTR;
          goto lock_failed;
        }
      
        /* now, do the useful work */
        do_stuff_to_acquire_item(item);
        ret=0;
      
        /* cleanup */
        mutex_unlock(...);
      
      lock_failed:
        free(mem2);
      
      mem2_failed:
        free(mem1);
      
      mem1_failed:
        return ret;
      }
      

      【讨论】:

      • 你完全可以在那里使用 if-else。只需将其写为:if( mem != null ) {code...} free(mem); 您可以更快地阅读代码 + 编译器确切地知道您想要做什么。
      【解决方案4】:

      这是一个可读的替代方案:

      int func(void **mem1, void **mem2) {
        *mem1 = malloc(SIZE);
        *mem2 = malloc(SIZE);
        if (!*mem1 || !*mem2) {
          free(*mem2);
          free(*mem1);
          return 1;
        }
        return 0;
      }
      

      【讨论】:

        【解决方案5】:

        个人;我有一个资源跟踪库(基本上是一个平衡二叉树),并且我有所有分配函数的包装器。

        资源(例如内存、套接字、文件描述符、信号量等 - 您分配和释放的任何东西)都可以属于一个集合。

        我还有一个错误处理库,其中每个函数的第一个参数是一个错误集,如果出现问题,遇到错误的函数会将错误提交到错误集中。

        如果错误集包含错误,不执行任何函数。 (我在每个函数的顶部都有一个宏,它会导致它返回)。

        所以多个 malloc 看起来像这样;

        mem[0] = malloc_wrapper( error_set, resource_set, 100 );
        mem[1] = malloc_wrapper( error_set, resource_set, 50 );
        mem[2] = malloc_wrapper( error_set, resource_set, 20 );
        

        不需要检查返回值,因为如果发生错误,后面的函数都不会执行,例如以下 malloc 永远不会发生。

        所以,当我需要释放资源时(比如在函数结束时,该函数内部使用的所有资源都已放入一个集合中),我会释放该集合。这只是一个函数调用。

        res_delete_set( resource_set );
        

        我不需要专门检查错误 - 在我的代码检查返回值中有 no if()s,这使得它可以维护;我发现内联错误检查的泛滥破坏了可读性和可维护性。我只有一个简单的函数调用列表。

        这是艺术,伙计:-)

        【讨论】:

          【解决方案6】:

          调用者是否对失败前已正确分配的内存块做任何有用的事情?如果没有,被调用者应该处理释放。

          一种有效地进行清理的可能性是使用do..while(0),它允许break,您的示例returns:

          int func(void **mem1, void **mem2)
          {
              *mem1 = NULL;
              *mem2 = NULL;
          
              do
              {
                  *mem1 = malloc(SIZE);
                  if(!*mem1) break;
          
                  *mem2 = malloc(SIZE);
                  if(!*mem2) break;
          
                  return 0;
              } while(0);
          
              // free is NULL-safe
              free(*mem1);
              free(*mem2);
          
              return 1;
          }
          

          如果您进行大量分配,您可能还想使用 freeAll() 函数在此处进行清理。

          【讨论】:

          • 不,调用者只是在需要时释放并返回。这段代码不是隐蔽的 goto 吗?并不是说我反对为此目的使用 goto...
          • @FunkyMo:是的,它基本上是goto,但有限制,你只能跳过其余部分,即不可能做坏事[tm];我个人觉得这比使用goto 更干净,我只用它来打破新闻循环......
          • @FunkyMo:我经常使用这种模式:它允许一些漂亮的宏魔法:stackoverflow.com/questions/569573/…
          【解决方案7】:

          我自己的倾向是创建一个可变参数函数来释放所有非 NULL 指针。然后调用者可以处理错误情况:

          void *mem1 = NULL;
          void *mem2 = NULL;
          
          if (func(&mem1, &mem2)) {
              freeAll(2, mem1, mem2);
              return 1;
          }
          

          【讨论】:

          • 实际上,在 NULL 指针上调用 free 没有任何惩罚。您不必检查 NULL。但初始化为 NULL 很重要。
          • @ypnos:您正在考虑 C++ 删除。将 NULL 传递给 free() 是无效的。
          • 我查了 K&R,万能的词说 free(NULL) 什么都不做。
          • 如果你在谈论使用可变参数,你会遇到两个问题。 1.变量参数列表前的最后一个参数是什么? 2.你怎么知道变量参数列表的末尾在哪里? (像 execl() 这样在 NULL 上停止不会让您传递多个 NULL 指针。)
          • @bk1e 这实际上是我第一次使用可变参数函数,所以当我开始写它时(在我的帖子之后)我必须包括要释放的元素的数量。
          【解决方案8】:

          如果上面的 goto 语句由于某种原因让你感到恐惧,你总是可以这样做:

          int func(void **mem1, void **mem2)
          {
              *mem1 = malloc(SIZE);
              if (!*mem1) return 1;
          
              *mem2 = malloc(SIZE);
              if (!*mem2) {
                  /* Insert free statement here */
                  free(*mem1);
                  return 1;
              }
          
              return 0;
          }
          

          我经常使用这种方法,但只有在非常清楚发生了什么时才使用。

          【讨论】:

            【解决方案9】:

            我对 goto 语句的所有建议感到有点害怕!

            我发现使用 goto 会导致代码混乱,这更有可能导致程序员错误。我现在的偏好是完全避免使用它,除非在最极端的情况下。我几乎从不使用它。不是因为学术完美主义,而是因为一年或更长时间后,回忆整体逻辑似乎总是比我建议的替代方案更难。

            作为一个喜欢重构事物以尽量减少忘记事物的选项(例如清除指针)的人,我会先添加一些函数。我假设我很可能会在同一个程序中重用这些。函数 imalloc() 将使用间接指针执行 malloc 操作; ifree() 将撤消此操作。 cifree() 将有条件地释放内存。

            有了这个,我的代码版本(带有第三个参数,为了演示)将是这样的:

            // indirect pointer malloc
            int imalloc(void **mem, size_t size)
            {
               return (*mem = malloc(size));
            }
            
            // indirect pointer free
            void ifree(void **mem)
            {
               if(*mem)
               {
                 free(*mem);
                 *mem = NULL;
               }
            }
            
            // conditional indirect pointer free
            void cifree(int cond, void **mem)
            {
              if(!cond)
              {
                ifree(mem);
              }
            }
            
            int func(void **mem1, void **mem2, void **mem3)
            {
               int result = FALSE;
               *mem1 = NULL;
               *mem2 = NULL;
               *mem3 = NULL;
            
               if(imalloc(mem1, SIZE))
               {
                 if(imalloc(mem2, SIZE))
                 {
                   if(imalloc(mem3, SIZE))
                   {
                     result = TRUE;
                   }            
                   cifree(result, mem2);
                 }
                 cifree(result, mem1);
               }
              return result;
            }
            

            我更喜欢最后只从一个函数返回一个。在两者之间跳出很快(在我看来,有点脏)。但更重要的是,允许您在无意中轻松绕过相关的清理代码。

            【讨论】:

            • 此函数在返回 FALSE 时会泄漏内存,并在返回 TRUE 时释放内存,而不是提问者所要求的。
            • 更正了错字 - 条件比较应该是 (!cond)。
            • Dour High Arch - 如果您撤回评论会很好,因为它不正确。我提出的基本方案实际上运行良好,不会泄漏,也不会返回无效指针。
            • 我还要补充一点,这直接准确地解决了原始提问者的问题。
            猜你喜欢
            • 1970-01-01
            • 2011-01-12
            • 2011-11-01
            • 2013-03-30
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2021-05-25
            相关资源
            最近更新 更多