5

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

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

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

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

4

8 回答 8

3

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

于 2009-03-28T03:26:03.740 回答
3

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

更新: 您的应用程序中的线程将使这非常复杂。请参阅有关线程问题的其他答案。

于 2009-03-28T04:08:03.140 回答
3

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

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

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

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

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

于 2009-03-28T04:23:05.850 回答
1

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

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

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

于 2009-03-28T04:13:17.293 回答
1

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

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

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

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

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

大多数剩余的“模式”都有些临时性

于 2009-03-28T05:39:06.773 回答
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(巧合的是,它是一个内存管理系统)。

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

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

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

于 2011-07-14T20:28:59.897 回答
0

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

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

于 2009-03-28T05:30:13.287 回答
0

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

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

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

于 2009-11-13T06:26:43.260 回答