【问题标题】:Where should I deallocate memory within functions?我应该在哪里释放函数内的内存?
【发布时间】:2009-12-28 06:07:32
【问题描述】:

我正在用 C 编写一个 shell。虽然我不希望有很多其他人使用它,但我想练习编写可维护且组织良好的代码。我在我的一些函数中注意到了以下模式,所以在它固化之前,我希望它能够得到全面审查。

例如,考虑以下函数:

int foo(int param...) {
  // declare variables
  struct bar *a, *b, *c;

  // do some work
  a = bar_creator();
  b = bar_modifier(a);
  c = bar_modifier(b);

  // cleanup
  free(a);
  free(b);
  free(c);

  return 1;
}

注意事项:

  • 三个阶段:声明、启动/修改、清理

  • 新分配的结构通常作为其他对象的修改副本从函数返回

  • 不需要大量对象,因此内存使用不是问题

就目前而言,这三个部分相对不同。这使我可以匹配第一部分和最后一部分,并确保所有内容都得到考虑。现在我想知道更好的风格是否可能是在不需要时立即释放某些东西。这样做的动机可能是最小化代码段有意义的上下文。

您的资源重新分配方法是什么?给定策略的优势是什么?

编辑

消除对函数行为的任何混淆:

/**
* returns a newly created bar
*/
struct bar *bar_creator();

/**
* takes a bar, and returns a _new_ copy of it that may have been modified.
* the original is not modified.
*/
struct bar *bar_modifier(struct bar *param);

【问题讨论】:

  • 可能值得开发一种命名约定,以明确函数何时返回新对象,而不是修改现有对象。

标签: c coding-style


【解决方案1】:

就我个人而言,我的偏好是在使用完对象后直接释放它们,并且只在我需要它们之前直接分配。这迫使我了解我的程序实际使用的内存。这种技术的另一个好处是,如果在方法中释放内存后分配额外的内存,它会减少总内存消耗。

【讨论】:

  • “这迫使我了解我的程序实际使用的内存。”有趣的。我可以将其视为对可维护代码的好处。外部读者将有“更少的上下文/范围”来熟悉。谢谢。
【解决方案2】:

有两种不同的情况需要考虑:

(1) 对象是在本地范围内创建的,在本地范围之外不需要它。 在这种情况下,您可以使用 calloc alloca()RAII 方法分配存储空间。使用 calloc alloca() 有一个很大的优势,即您不必关心调用 free(),因为当本地范围离开时,分配的内存会自动释放。

(2) 在本地范围内创建一个对象,并且在该本地范围之外需要它。 在这种情况下,没有一般性建议。当不再需要对象时,我会释放内存。

已编辑:使用 alloca() 代替 calloc()

【讨论】:

  • calloc() 自动释放分配的内存是什么意思?我对calloc()的理解是,它可以在分配堆内存的同时进行一些初始化。
  • 你确定你不是指alloca()吗?根据前面的评论,calloc() 返回的内存 确实 需要释放。
  • UPS。你说的对。我的意思是 alloca() 当然。非常感谢您的指正!
【解决方案3】:

我倾向于在最后对释放进行分组,除非我要重用一个变量并且需要先释放它。这样可以更清楚地知道究竟需要销毁什么,如果您正在考虑早期的return 或者如果函数有点复杂,这将很有帮助。通常,您的函数会有几个不同的控制流,您希望确保它们都在最后进行清理,这样更容易看到清理代码何时结束。

【讨论】:

  • 谢谢。这与我一直在做的类似。对于您可能提前返回的部分中代码重复的可能性有什么建议吗?显然,在初始版本完成后进行小规模重构可以清理干净,那么在编写此控制流时您有什么要记住的吗?
【解决方案4】:

我通常倾向于尽可能小的范围,因此我尽可能晚地创建对象,并尽可能早地释放(释放)它们。

我会倾向于:

char * foo;
/* some work */
{
foo = create();
/* use foo */
destroy(foo);
}
/* some other work */
{
foo = create();
/* use foo */
destroy(foo);
}

即使我可以重用内存,我更喜欢分配两次并释放两次。大多数情况下,这种技术对性能的影响很小,因为大多数情况下这两个对象是不同的,如果这是一个问题,我倾向于在最近的开发过程中对其进行优化。

现在,如果您有 2 个具有相同范围的对象(或三个与您的示例相同),那就是同一件事:

{
foo1 = create();
foo2 = create();
foo3 = create();
/* do something */
destroy(foo1);
destroy(foo4);
destroy(foo3);
}

但这种特殊布局只有在三个对象具有相同范围时才相关。

我倾向于避免这种布局:

{
foo1 = create();
{
    foo2 = create();
    /* use foo2 */
}
destroy(foo1);
/* use foo2 again */
destroy(foo2);
}

我认为这很糟糕。

当然,这里的 {} 仅用于示例,但您也可以在实际代码、vim 折叠或任何表示范围的内容中使用它们。

当我需要更大的范围(例如全局或共享)时,我使用引用计数和保留释放机制(用保留替换创建,用释放替换销毁),这始终确保我有一个很好且简单的内存管理。

【讨论】:

  • 我喜欢这个使用括号的可视化。这似乎是一个可以在给定分布的样式部分中轻松描述的建议。谢谢你的解释。
【解决方案5】:
  1. 通常动态分配的内存有很长的生命周期(比函数调用长),因此谈论它在函数中的哪个位置被释放是没有意义的。

  2. 如果仅在函数范围内需要内存,则根据语言,如果适当,则应在堆栈上静态分配(在函数中声明为局部变量,将在函数执行时分配)函数退出时调用并释放,如另一张海报的示例所示)。

  3. 就命名而言,只有分配内存并返回它的函数需要特别命名。其他任何东西都不要费心说“modfiier”——使用那个字母空间来描述函数的作用。 IE。默认情况下,假设它不分配内存,除非特别命名(即 createX、allocX 等)。

  4. 在静态分配不合适的语言或情况下(即与程序中其他地方的代码保持一致),然后通过在函数调用开始分配并在结束时释放来模拟堆栈分配模式.

  5. 为清楚起见,如果您的函数只是修改对象,则根本不要使用函数。使用程序。这清楚地表明没有分配新的内存。换句话说,消除你的指针 b 和 c - 它们是不必要的。他们可以修改 a 指向的内容而不返回值。

  6. 从代码的外观来看,要么释放已释放的内存,要么 bar_modifier 具有误导性,因为它不是简单地修改 a 指向的内存,而是创建全新的动态分配的内存。在这种情况下,它们不应命名为 bar_modifier,而应命名为 create_SomethingElse。

【讨论】:

  • Re: 第 5 点,在我看来 OP 正在尝试在 C 中使用函数式编程技术 - 如果是这样,他不想修改结构,而是使用新结构包含原件的修改副本。
  • 这种事情只有在你有垃圾收集的情况下才能很好地工作,因为你最终会得到一些非常复杂的结构共享。 IE。从第三个节点向下的左子树与 x, y, znd z 共享,从第二个节点向下的右子树与 y 共享,...在 c 中忘记它!
【解决方案6】:

为什么要释放 3 次?

如果bar_creator() 是唯一动态分配内存的函数,您只需释放一个指向该内存区域的指针。

【讨论】:

  • 是什么让你认为 a、b 和 c 是同一个内存区域?
  • 抱歉,更新了原帖。想象bar_modifier 返回一个可能已被修改的新副本。这只是一个人为的例子。
【解决方案7】:

当你完成它时!

不要让廉价的内存价格助长了懒惰的编程。

【讨论】:

    【解决方案8】:

    您需要注意内存分配失败时会发生什么。因为 C 不支持异常,所以我使用 goto 来管理错误时的展开动态状态。这是对原始函数的简单操作,展示了该技术:

    int foo(int param...) {
      // declare variables
      struct bar *a, *b, *c;
    
      // do some work
      a = bar_creator();
      if(a == (struct bar *) 0)
        goto err0;
      b = bar_modifier(a);
      if(b == (struct bar *) 0)
        goto err1;
      c = bar_modifier(b);
      if(c == (struct bar *) 0)
        goto err2;
    
      // cleanup
      free(a);
      free(b);
      free(c);
    
      return 1;
    
    err2:
      free(b);
    err1:
      free(a);
    err0:
      return -1;
    }
    

    在使用这种技术时,我总是希望在错误标签之前有一个return 语句,以便在视觉上区分正常返回情况和错误情况。现在,这假定您对动态分配的内存使用风/展开范式......您正在做的事情看起来更连续,所以我可能会更接近以下内容:

      a = bar_creator();
      if(a == (struct bar *) 0)
        goto err0;
      /* work with a */
      b = bar_modifier(a);
      free(a);
      if(b == (struct bar *) 0)
        goto err0;
      /* work with b */
      c = bar_modifier(b);
      free(b);
      if(c == (struct bar *) 0)
        goto err0;
      /* work with c */
      free(c);
    
      return 1;
    err0:
      return -1;
    

    【讨论】:

      【解决方案9】:

      让编译器帮你清理栈?

      int foo(int param...) {
        // declare variables
        struct bar a, b, c;
      
        // do some work
        bar_creator(/*retvalue*/&a);
        bar_modifier(a,/*retvalue*/&b);
        bar_modifier(b,/*retvalue*/&c);
      
        return 1;
      }
      

      【讨论】:

        【解决方案10】:

        对于复杂的代码,我会使用结构图来显示子例程协同工作的方式,然后对于分配/解除分配,我会尝试使这些在给定对象的图表中大致相同的级别发生。

        在你的情况下,我可能想定义一个名为 bar_destroyer 的新函数,在函数 foo 的末尾调用它 3 次,然后在其中执行 free()。

        【讨论】:

          【解决方案11】:

          考虑使用不同的模式。如果这样做是合理的(使用声明,而不是 alloca),则在堆栈上分配变量。考虑让你的 bar_creator 成为一个 bar_initialiser,它接受一个结构 bar *。

          然后你可以让你的 bar_modifier 看起来像

          void bar_modifier(const struct bar * source, struct bar *dest);
          

          那你就不用太担心内存分配了。

          一般来说,在 C 中让调用者而不是被调用者分配内存会更好——因此在我看来,为什么 strcpy 比 strdup 是一个“更好”的函数。

          【讨论】:

            猜你喜欢
            • 2014-04-27
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2015-03-11
            • 2017-10-20
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多