【问题标题】:Patterns for freeing memory in C?在 C 中释放内存的模式?
【发布时间】:2009-03-28 03:23:30
【问题描述】:

我目前正在开发一个基于 C 的应用程序,有点卡在以非反模式方式释放内存。我是一个内存管理爱好者。

我的主要问题是我在各种不同的范围内声明内存结构,这些结构通过引用其他函数来传递。其中一些函数可能会抛出错误并退出()。

如果我 exit() 在一个范围内,但不是所有数据结构都在该范围内,我该如何释放我的结构?

我觉得我需要将它全部包装在一个伪异常处理程序中并让处理程序处理释放,但这仍然看起来很难看,因为它必须知道我可能需要或不需要释放的所有内容。 .

【问题讨论】:

    标签: c memory-management free


    【解决方案1】:

    考虑 malloc 的包装器并以规范的方式使用它们。跟踪您分配的内存(可能在链表中)并使用包装器退出以枚举您的内存以释放它。您可以还使用附加参数和链接列表结构的成员来命名内存。在分配的内存高度依赖于范围的应用程序中,您会发现自己正在泄漏内存,这可能是转储和分析内存的好方法。

    更新: 应用程序中的线程将使这变得非常复杂。查看有关线程问题的其他答案。

    【讨论】:

    • 你甚至可以使用 atexit() 函数并编写一个函数来释放链表上分配的所有内存(在这种情况下必须是一个全局变量),只需一个简单的退出调用() - 这样你就不必记住不要使用普通的 exit()。
    • 但是为什么呢?操作系统已经在维护分配的内存列表。您复制该功能只会减慢关闭过程,除了一些自我膨胀之外没有任何好处。除非您打算将应用程序移植到 MSDOS,否则请让操作系统完成它的工作。
    • 为什么是因为最终纪律和功能将帮助您追踪内存泄漏。如果您正确编码,它可以通过 DEFINE 或 MACRO 定义关闭。
    【解决方案2】:

    当调用 exit() 时,您无需担心释放内存。当进程退出时,操作系统将释放所有相关的内存。

    【讨论】:

    • 这并不总是正确的。在退出之前释放 malloc'ed 结构被认为是良好的卫生习惯。当您需要查找实际的内存泄漏时,它也会有所帮助。
    • 这仅适用于好的操作系统。用完 free() 内存是件好事,如果只是为了养成习惯。
    • 对,我假设他正在讨论严重的故障;即,该过程正在消亡并且消亡得很快。
    • @Chris Lutz:如果你正在为一个不合适的操作系统编程,你知道并且通常会在问题中提到这个事实。哪些操作系统不好?几十年前,AmigaDOS 就是其中之一。
    • @Anthony - 例子?对于 Windows 或 *nix 操作系统来说是这样。
    【解决方案3】:

    我认为要恰当地回答这个问题,我们需要了解您的整个程序(或系统,或任何情况)的架构。

    答案是:视情况而定。您可以使用多种策略。

    正如其他人所指出的,在现代桌面或服务器操作系统上,您可以exit() 而不必担心程序分配的内存。

    此策略会发生变化,例如,如果您在嵌入式操作系统上进行开发,exit() 可能无法清理所有内容。通常我看到的是,当个别函数由于错误而返回时,他们会确保清理他们自己分配的任何东西。在调用 10 个函数之后,您不会看到任何 exit() 调用。每个函数在返回时会依次指示错误,并且每个函数都会自行清理。原来的 main() 函数(如果你愿意的话 - 它可能不会被称为 main())会检测到错误,清理它分配的所有内存,并采取适当的措施。

    当您只有范围内的范围时,这不是火箭科学。如果您有多个执行线程和共享数据结构,则变得困难。然后,您可能需要一个垃圾收集器或一种方法来计算引用并在结构的最后一个用户完成它时释放内存。例如,如果您查看 BSD 网络堆栈的源代码,您会发现它在某些需要长时间保持“活动”并共享的结构中使用了refcnt(引用计数)值在不同的用户之间。 (这基本上也是垃圾收集器所做的。)

    【讨论】:

      【解决方案4】:

      您可以为作用域/函数之间共享的 malloc 内存创建一个简单的内存管理器。

      malloc 时注册,释放时注销。有一个函数可以在调用 exit 之前释放所有已注册的内存。

      它增加了一些开销,但它有助于跟踪内存。它还可以帮助您找出令人讨厌的内存泄漏。

      【讨论】:

        【解决方案5】:

        Michael 的建议是合理的 - 如果您要退出,则无需担心释放内存,因为无论如何系统都会回收它。

        共享内存段是一个例外——至少在 System V Shared Memory 下。这些段可以比创建它们的程序持续更长时间。

        目前未提及的一个选项是使用基于竞技场的内存分配方案,该方案建立在标准 malloc() 之上。如果整个应用程序使用单个区域,您的清理代码可以释放该区域,然后立即释放所有区域。 (APR - Apache Portable Runtime - 提供了一个池功能,我认为它是类似的;David Hanson 的“C 接口和实现”提供了一个基于竞技场的内存分配系统;我已经写了一个,如果你愿意,你可以使用它。)你可以认为这是“穷人的垃圾收集”。

        作为一般的内存规则,每次动态分配内存时,都应该了解哪些代码将释放它以及何时可以释放它。有一些标准模式。最简单的就是“在这个函数中分配;在这个函数返回之前释放”。这可以使内存在很大程度上受到控制(如果您不在包含内存分配的循环上运行太多迭代),并对其范围进行限定,以便它可以用于当前函数及其调用的函数。显然,您必须合理地确保您调用的函数不会隐藏(缓存)指向数据的指针,并在您释放和重用内存后尝试重用它们。

        下一个标准模式以fopen()fclose()为例;有一个函数分配一个指向某个内存的指针,调用代码可以使用它,然后在程序完成它时释放它。但是,这通常与第一种情况非常相似 - 在调用 fopen() 的函数中调用 fclose() 通常是个好主意。

        大多数剩余的“模式”有点临时

        【讨论】:

          【解决方案6】:

          人们已经指出,如果您只是在发生错误时退出(或中止)代码,您可能不需要担心释放内存。但以防万一,这是我开发的一种模式,并在出现错误时大量使用它来创建和拆除资源。注意:我在这里展示一个模式是为了说明一点,而不是编写真正的代码!

          int foo_create(foo_t *foo_out) {
              int res;
              foo_t foo;
              bar_t bar;
              baz_t baz;
              res = bar_create(&bar);
              if (res != 0)
                  goto fail_bar;
              res = baz_create(&baz);
              if (res != 0)
                  goto fail_baz;
              foo = malloc(sizeof(foo_s));
              if (foo == NULL)
                  goto fail_alloc;
              foo->bar = bar;
              foo->baz = baz;
              etc. etc. you get the idea
              *foo_out = foo;
              return 0; /* meaning OK */
          
              /* tear down stuff */
          fail_alloc:
              baz_destroy(baz);
          fail_baz:
              bar_destroy(bar);
          fail_bar:
              return res; /* propagate error code */
          }
          

          我敢打赌,我会得到一些 cmets 说“这很糟糕,因为你使用 goto”。但这是对 goto 的规范和结构化使用,如果一致应用,它会使代码更清晰、更简单且更易于维护。没有它,您将无法通过代码实现简单的、记录在案的拆卸路径。

          如果您想在实际使用中的商业代码中看到这一点,请查看arena.c from the MPS(巧合的是,它是一个内存管理系统)。

          这是一种穷人的 try...finish 处理程序,并为您提供了一些类似于析构函数的东西。

          我现在听起来像个白胡子,但在我多年从事其他人的 C 代码工作时,缺乏清晰的错误路径通常是一个非常严重的问题,尤其是在网络代码和其他不可靠的情况下。介绍他们偶尔会让我获得相当多的咨询收入。

          关于您的问题还有很多其他要说的——我将保留这种模式,以防万一有用。

          【讨论】:

          • 这很糟糕,因为您使用 goto。对不起,无法抗拒:D
          • 如果你用NULL 初始化你的bazbar 并让你的销毁函数能够优雅地处理NULL 参数(如free),那么你会只能使用一个fail: 标签,使其不那么拥挤。
          • 感谢您的建议!这是我的高可靠性项目中的一个编码规则,以避免在几乎任何事情上使用 NULL。周围有 NULL 会导致各种错误。我们特别避免使用 NULL 来指示您应该采取的特殊操作。但是我可以写一篇关于 NULL 的文章:P
          【解决方案7】:

          很简单,为什么不使用引用计数实现,所以当你创建一个对象并传递它时,你会递增和递减引用计数(如果你有多个线程,请记住是原子的)。

          这样,当一个对象不再被使用(零引用)时,您可以安全地删除它,或者在引用计数递减调用中自动删除它。

          【讨论】:

            【解决方案8】:

            这听起来像是 Boehm 垃圾收集器的任务。

            http://www.hpl.hp.com/personal/Hans_Boehm/gc/

            当然取决于您是否可以或应该负担得起使用它的系统。

            【讨论】:

              猜你喜欢
              • 2010-10-23
              • 2011-02-12
              • 1970-01-01
              • 1970-01-01
              • 2021-07-19
              • 2023-03-22
              • 1970-01-01
              • 1970-01-01
              • 2016-09-07
              相关资源
              最近更新 更多