在C语言中如何释放内存?

6
我目前正在开发一个基于C语言的应用程序,但在释放内存时遇到了一些问题。我的主要问题是我在各种不同范围内声明了内存结构,并将这些结构通过引用传递给其他函数。其中一些函数可能会抛出错误并退出()。
如果我在某个作用域中退出(),但并不是所有的数据结构都在该作用域中,那么我该如何释放我的结构?
我感觉我需要将所有内容包装在一个伪异常处理程序中,并让处理程序处理释放,但这仍然看起来很丑陋,因为它必须知道我可能需要或不需要释放的所有内容...
8个回答

4

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

该过程不需要您手动释放内存。

1
这并不总是正确的。在退出之前,释放malloc分配的结构被认为是良好的卫生实践。当您需要查找实际内存泄漏时,这也会有所帮助。 - Edward Z. Yang
这仅适用于好的操作系统。做完后,释放内存是很好的习惯,即使只是为了养成这个习惯。 - Chris Lutz
对,我猜他在讨论关键故障;即进程正在崩溃并且崩溃得很快。 - Michael
@Chris Lutz:如果你正在为一个不好的操作系统编程,你会知道并且通常在问题中提到这个事实。哪些操作系统不好?AmigaDOS曾经是其中之一,在几十年前的某个时候。 - Jonathan Leffler
@Anthony - 有例子吗?这适用于Windows或*nix操作系统。 - Michael
@Michael:我曾经在一些嵌入式设备上工作过,其中一些系统由于明显的原因被保持最小化。它们为应用程序开发人员提供了一些方便的服务,但是除此之外,它们期望开发人员负责清理。 - anon

4
考虑使用malloc的包装器,并以有纪律的方式使用它们。跟踪您分配的内存(可能是在链表中),并使用包装器退出以枚举您的内存以释放它。您还可以使用链表结构的附加参数和成员来命名内存。在分配的内存高度依赖于范围的应用程序中,您将发现自己泄漏内存,这可以是卸载内存并对其进行分析的好方法。
更新: 在应用程序中进行线程处理会使此过程非常复杂。请参阅其他答案,了解线程问题。

你甚至可以使用 atexit() 函数编写一个释放所有在链表上分配的内存的函数(在这种情况下,它必须是全局变量),并在调用 exit() 时直接调用它 - 这样你就不必记住不要使用 plain exit()。 - Chris Lutz
但是为什么呢?操作系统已经在维护一个分配内存的列表。你复制这个功能只会拖慢关机过程,除了一点点自我膨胀之外没有任何好处。除非你计划将应用程序移植到MSDOS上,否则让操作系统完成它的工作。 - Eclipse
为什么这样做呢?因为最终的纪律和功能将帮助您追踪内存泄漏。如果您正确编写代码,可以通过DEFINE或MACRO定义关闭它。 - ojblass

4
为了恰当地回答这个问题,我们需要了解整个程序(或系统等)的架构。
答案是:这取决于具体情况,你可以采用多种策略。
正如其他人指出的那样,在现代桌面或服务器操作系统上,你可以使用 exit() 而不必担心程序已分配的内存。
但如果你在嵌入式操作系统上开发,情况就会有所不同。在这种情况下,exit() 可能无法清除所有东西。通常我看到的情况是当单个函数由于错误而返回时,它们会确保清理任何它们自己分配的东西。在调用 10 个函数后,你不会看到任何 exit() 调用。每个函数都会在返回时指示错误,并清理自己分配的内存。原始的 main() 函数(如果你愿意的话 - 它可能不叫 main())会检测错误、清理任何它已分配的内存并采取适当的措施。
当你只有作用域嵌套时,这并不难。困难之处在于如果你有多个执行线程和共享数据结构,那么你可能需要一个垃圾收集器或一种计算引用计数并在最后一个使用结构的用户完成时释放内存的方法。例如,如果你查看 BSD 网络堆栈的源代码,你会发现它在某些需要长时间保持“活动”且在不同用户之间共享的结构中使用了一个 refcnt(引用计数)值。(垃圾收集器基本上也是这样做的。)

1
人们已经指出,如果您只是在出现错误时退出(或中止)代码,则可能无需担心释放内存。但以防万一,这里是我开发并经常用于创建和拆除资源的模式。注意:我在这里展示一个模式来说明一点,并非编写真正的代码!
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 */
}

我敢打赌,肯定会有人评论说“你使用goto是不好的”。但这是一种有纪律、有结构的goto使用方式,如果一贯地应用,可以使代码更清晰、更简单、更易于维护。如果没有它,你无法通过代码实现一个简单、文档化的拆除路径。

如果你想在真正的商业代码中看到这个例子,请看看MPS中的arena.c(巧合的是,它是一个内存管理系统)。

这是一种类似于析构函数的贫民版try...finish处理程序。

我现在听起来像一个老古董了,但在我多年的C代码开发经验中,缺乏明确的错误路径通常是一个非常严重的问题,特别是在网络代码和其他不可靠的情况下。引入它们有时会给我带来相当多的咨询收入。

关于你的问题还有很多其他的事情要说——我只是留下这个模式,以防有用。


这是不好的,因为你使用了goto。抱歉,忍不住开个玩笑 :D - Matt N.
如果你用NULL初始化了你的bazbar,并且使你的销毁函数能够优雅地处理NULL参数(就像free一样),那么你就可以只使用一个fail:标签,使它变得更加简洁。 - Patrick Schlüter
谢谢您的建议!在我所从事的高可靠性项目中,避免使用NULL几乎是一项编码规则。有了NULL会导致各种错误。我们特别避免使用NULL来指示应该执行的特殊操作。但是我还可以写一篇关于NULL的文章:P - rptb1

1
你可以为在不同作用域或函数之间共享的 malloc’d 内存创建一个简单的内存管理器。
在调用 malloc 函数后注册内存分配,释放内存时取消注册。在调用 exit 之前编写一个函数,以便释放所述内存管理器注册的所有内存。
这样做会增加一些开销,但可以帮助跟踪内存使用情况,也有助于查找难以捉摸的内存泄漏问题。

1

迈克尔的建议很好 - 如果您正在退出,您不需要担心释放内存,因为系统会自动回收它。

其中一个例外是共享内存段 - 至少在 System V 共享内存下。这些段可以比创建它们的程序更长时间地存在。

到目前为止未提及的一个选项是使用基于区域的内存分配方案,构建在标准的 malloc() 之上。如果整个应用程序使用单个区域,则清理代码可以释放该区域,并且所有内容都会一次性释放。(APR - Apache Portable Runtime - 提供了类似的池功能;David Hanson 的 "C Interfaces and Implementations" 提供了基于区域的内存分配系统;如果您愿意,我也写了一个您可以使用的。) 您可以将其视为“穷人的垃圾回收”。

作为一般的内存规则,每次动态分配内存时,您都应该了解哪些代码将释放它以及何时可以释放它。有几种标准模式。最简单的是“在此函数中分配;在此函数返回之前释放”。这使得内存大部分受控制(如果您不在包含内存分配的循环上运行太多迭代),并将其范围限定,以便可以将其提供给当前函数和调用它的函数。显然,您必须相当确定您调用的函数不会将指向数据的指针藏起来(缓存)并在您释放并重用内存后尝试重新使用它们。
下一个标准模式由fopen()和fclose()所示;有一个函数分配了指向某些内存的指针,可以被调用代码使用,然后在程序完成后释放。然而,这通常变得非常类似于第一种情况-通常最好在调用fopen()的函数中调用fclose()。
剩余的大部分“模式”都有点特别。

0

0
非常简单,为什么不使用引用计数实现,这样当您创建一个对象并将其传递时,您会增加和减少引用计数(如果有多个线程,请记得是原子操作)。
这样,当一个对象不再被使用(引用计数为零)时,您可以安全地删除它,或在引用计数减量调用中自动删除它。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接