在C中释放内存的模式?

时间:2022-09-06 12:13:40

I'm currently working on a C based application am a bit stuck on freeing memory in a non-antipattern fashion. I am a memory-management amateur.

我目前正在研究一个基于C的应用程序,因为它以一种非反模式方式释放内存。我是一名记忆管理爱好者。

My main problem is I declare memory structures in various different scopes, and these structures get passed around by reference to other functions. Some of those functions may throw errors and exit().

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

How do I go about freeing my structures if I exit() in one scope, but not all my data structures are in that scope?

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

I get the feeling I need to wrap it all up in a psuedo exception handler and have the handler deal with freeing, but that still seems ugly because it would have to know about everything I may or may not need to free...

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

8 个解决方案

#1


Consider wrappers to malloc and using them in a disciplined way. Track the memory that you do allocate (in a linked list maybe) and use a wrapper to exit to enumerate your memory to free it. You could also name the memory with an additional parameter and member of your linked list structure. In applications where allocated memory is highly scope dependent you will find yourself leaking memory and this can be a good method to dump the memory and analyze it.

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

UPDATE: Threading in your application will make this very complex. See other answers regarding threading issues.

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

#2


You don't need to worry about freeing memory when exit() is called. When the process exits, the operating system will free all of the associated memory.

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

#3


I think to answer this question appropriately, we would need to know about the architecture of your entire program (or system, or whatever the case may be).

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

The answer is: it depends. There are a number of strategies you can use.

答案是:这取决于。您可以使用许多策略。

As others have pointed out, on a modern desktop or server operating system, you can exit() and not worry about the memory your program has allocated.

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

This strategy changes, for example, if you are developing on an embedded operating system where exit() might not clean everything up. Typically what I see is when individual functions return due to an error, they make sure to clean up anything they themselves have allocated. You wouldn't see any exit() calls after calling, say, 10 functions. Each function would in turn indicate an error when it returns, and each function would clean up after itself. The original main() function (if you will - it might not be called main()) would detect the error, clean up any memory it had allocated, and take the appropriate actions.

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

When you just have scopes-within-scopes, it's not rocket science. Where it gets difficult is if you have multiple threads of execution, and shared data structures. Then you might need a garbage collector or a way to count references and free the memory when the last user of the structure is done with it. For example, if you look at the source to the BSD networking stack, you'll see that it uses a refcnt (reference count) value in some structures that need to be kept "alive" for an extended period of time and shared among different users. (This is basically what garbage collectors do, as well.)

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

#4


You can create a simple memory manager for malloc'd memory that is shared between scopes/functions.

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

Register it when you malloc it, de-register it when you free it. Have a function that frees all registered memory before you call exit.

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

It adds a bit of overhead, but it helps keep track of memory. It can also help you hunt down pesky memory leaks.

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

#5


Michael's advice is sound - if you are exiting, you don't need to worry about freeing the memory since the system will reclaim it anyway.

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

One exception to that is shared memory segments - at least under System V Shared Memory. Those segments can persist longer than the program that creates them.

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

One option not mentioned so far is to use an arena-based memory allocation scheme, built on top of standard malloc(). If the entire application uses a single arena, your cleanup code can release that arena, and all is freed at once. (APR - Apache Portable Runtime - provides a pools feature which I believe is similar; David Hanson's "C Interfaces and Implementations" provides an arena-based memory allocation system; I've written one that you could use if you wanted to.) You can think of this as "poor man's garbage collection".

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

As a general memory discipline, every time you allocate memory dynamically, you should understand which code is going to release it and when it can be released. There are a few standard patterns. The simplest is "allocated in this function; released before this function returns". This keeps the memory largely under control (if you don't run too many iterations on the loop that contains the memory allocation), and scopes it so that it can be made available to the current function and the functions it calls. Obviously, you have to be reasonably sure that the functions you call are not going to squirrel away (cache) pointers to the data and try to reuse them later after you've released and reused the memory.

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

The next standard pattern is exemplified by fopen() and fclose(); there's a function that allocates a pointer to some memory, which can be used by the calling code, and then released when the program has finished with it. However, this often becomes very similar to the first case - it is usually a good idea to call fclose() in the function that called fopen() too.

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

Most of the remaining 'patterns' are somewhat ad hoc.

大多数剩余的“模式”都是临时性的。

#6


People have already pointed out that you probably don't need to worry about freeing memory if you're just exiting (or aborting) your code in case of error. But just in case, here's a pattern I developed and use a lot for creating and tearing down resources in case of error. NOTE: I'm showing a pattern here to make a point, not writing real code!

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

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 */
}

I can bet I'm going to get some comments saying "this is bad because you use goto". But this is a disciplined and structured use of goto that makes code clearer, simpler, and easier to maintain if applied consistently. You can't achieve a simple, documented tear-down path through the code without it.

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

If you want to see this in real in-use commercial code, take a look at, say, arena.c from the MPS (which is coincidentally a memory management system).

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

It's a kind of poor-man's try...finish handler, and gives you something a bit like destructors.

这是一个穷人的尝试...完成处理程序,并给你一些有点像析构函数。

I'm going to sound like a greybeard now, but in my many years of working on other people's C code, lack of clear error paths is often a very serious problem, especially in network code and other unreliable situations. Introducing them has occasionally made me quite a bit of consultancy income.

我现在听起来像是一个灰胡子,但在我多年来研究其他人的C代码时,缺乏明确的错误路径通常是一个非常严重的问题,特别是在网络代码和其他不可靠的情况下。介绍他们偶尔会给我相当多的咨询收入。

There are plenty of other things to say about your question -- I'm just going to leave it with this pattern in case that's useful.

关于你的问题还有很多其他的事情要说 - 我只是假设这个模式是有用的。

#7


Very simply, why not have a reference counted implementation, so when you create an object and pass it around you increment and decrement the reference counted number (remember to be atomic if you have more than one thread).

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

That way, when an object is no longer used (zero references) you can safely delete it, or automatically delete it in the reference count decrement call.

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

#8


This sounds like a task for a Boehm garbage collector.

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

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

Depends on the system of course whether you can or should afford to use it.

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

#1


Consider wrappers to malloc and using them in a disciplined way. Track the memory that you do allocate (in a linked list maybe) and use a wrapper to exit to enumerate your memory to free it. You could also name the memory with an additional parameter and member of your linked list structure. In applications where allocated memory is highly scope dependent you will find yourself leaking memory and this can be a good method to dump the memory and analyze it.

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

UPDATE: Threading in your application will make this very complex. See other answers regarding threading issues.

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

#2


You don't need to worry about freeing memory when exit() is called. When the process exits, the operating system will free all of the associated memory.

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

#3


I think to answer this question appropriately, we would need to know about the architecture of your entire program (or system, or whatever the case may be).

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

The answer is: it depends. There are a number of strategies you can use.

答案是:这取决于。您可以使用许多策略。

As others have pointed out, on a modern desktop or server operating system, you can exit() and not worry about the memory your program has allocated.

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

This strategy changes, for example, if you are developing on an embedded operating system where exit() might not clean everything up. Typically what I see is when individual functions return due to an error, they make sure to clean up anything they themselves have allocated. You wouldn't see any exit() calls after calling, say, 10 functions. Each function would in turn indicate an error when it returns, and each function would clean up after itself. The original main() function (if you will - it might not be called main()) would detect the error, clean up any memory it had allocated, and take the appropriate actions.

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

When you just have scopes-within-scopes, it's not rocket science. Where it gets difficult is if you have multiple threads of execution, and shared data structures. Then you might need a garbage collector or a way to count references and free the memory when the last user of the structure is done with it. For example, if you look at the source to the BSD networking stack, you'll see that it uses a refcnt (reference count) value in some structures that need to be kept "alive" for an extended period of time and shared among different users. (This is basically what garbage collectors do, as well.)

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

#4


You can create a simple memory manager for malloc'd memory that is shared between scopes/functions.

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

Register it when you malloc it, de-register it when you free it. Have a function that frees all registered memory before you call exit.

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

It adds a bit of overhead, but it helps keep track of memory. It can also help you hunt down pesky memory leaks.

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

#5


Michael's advice is sound - if you are exiting, you don't need to worry about freeing the memory since the system will reclaim it anyway.

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

One exception to that is shared memory segments - at least under System V Shared Memory. Those segments can persist longer than the program that creates them.

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

One option not mentioned so far is to use an arena-based memory allocation scheme, built on top of standard malloc(). If the entire application uses a single arena, your cleanup code can release that arena, and all is freed at once. (APR - Apache Portable Runtime - provides a pools feature which I believe is similar; David Hanson's "C Interfaces and Implementations" provides an arena-based memory allocation system; I've written one that you could use if you wanted to.) You can think of this as "poor man's garbage collection".

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

As a general memory discipline, every time you allocate memory dynamically, you should understand which code is going to release it and when it can be released. There are a few standard patterns. The simplest is "allocated in this function; released before this function returns". This keeps the memory largely under control (if you don't run too many iterations on the loop that contains the memory allocation), and scopes it so that it can be made available to the current function and the functions it calls. Obviously, you have to be reasonably sure that the functions you call are not going to squirrel away (cache) pointers to the data and try to reuse them later after you've released and reused the memory.

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

The next standard pattern is exemplified by fopen() and fclose(); there's a function that allocates a pointer to some memory, which can be used by the calling code, and then released when the program has finished with it. However, this often becomes very similar to the first case - it is usually a good idea to call fclose() in the function that called fopen() too.

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

Most of the remaining 'patterns' are somewhat ad hoc.

大多数剩余的“模式”都是临时性的。

#6


People have already pointed out that you probably don't need to worry about freeing memory if you're just exiting (or aborting) your code in case of error. But just in case, here's a pattern I developed and use a lot for creating and tearing down resources in case of error. NOTE: I'm showing a pattern here to make a point, not writing real code!

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

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 */
}

I can bet I'm going to get some comments saying "this is bad because you use goto". But this is a disciplined and structured use of goto that makes code clearer, simpler, and easier to maintain if applied consistently. You can't achieve a simple, documented tear-down path through the code without it.

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

If you want to see this in real in-use commercial code, take a look at, say, arena.c from the MPS (which is coincidentally a memory management system).

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

It's a kind of poor-man's try...finish handler, and gives you something a bit like destructors.

这是一个穷人的尝试...完成处理程序,并给你一些有点像析构函数。

I'm going to sound like a greybeard now, but in my many years of working on other people's C code, lack of clear error paths is often a very serious problem, especially in network code and other unreliable situations. Introducing them has occasionally made me quite a bit of consultancy income.

我现在听起来像是一个灰胡子,但在我多年来研究其他人的C代码时,缺乏明确的错误路径通常是一个非常严重的问题,特别是在网络代码和其他不可靠的情况下。介绍他们偶尔会给我相当多的咨询收入。

There are plenty of other things to say about your question -- I'm just going to leave it with this pattern in case that's useful.

关于你的问题还有很多其他的事情要说 - 我只是假设这个模式是有用的。

#7


Very simply, why not have a reference counted implementation, so when you create an object and pass it around you increment and decrement the reference counted number (remember to be atomic if you have more than one thread).

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

That way, when an object is no longer used (zero references) you can safely delete it, or automatically delete it in the reference count decrement call.

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

#8


This sounds like a task for a Boehm garbage collector.

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

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

Depends on the system of course whether you can or should afford to use it.

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