应该什么时候C函数返回新分配的内存?

时间:2009-12-29 01:09:00

标签: c memory-management coding-style

在其他地方的回复中,我找到了以下代码段:

  

一般来说,C更好   调用者分配内存,而不是   被调用者 - 因此为什么strcpy是“更好”   在我看来,比起strdup。

我可以看到这是一个有效的模式,但为什么它会被认为更好?遵循这种模式是否有优势?或者不是?

例如

最近我写了大量代码如下:

struct foo *a = foo_create();
// do something with a
foo_destroy(a);

如果foo不仅仅是一个扁平结构,那么我想我可以将所有初始化放在一个步骤中。另外,假设结构应该在堆上。为什么这样做会更好:

struct foo *a = malloc(sizeof(foo));
foo_init(a);
// do something with a
foo_destroy(a)

11 个答案:

答案 0 :(得分:12)

每当您想要一个不透明的结构并且不想在头文件中公开其内部时。您的foo_create()示例说明了这一点。

另一个例子是Windows API。例如。 CreateWindow为您提供HWND。您不知道实际的WND结构是什么样的,并且无法触及其字段。

与内核对象句柄相同。例如。 CreateEvent提供HANDLE。您只能使用定义良好的API对其进行操作,并使用CloseHandle()将其关闭。

回复:

struct foo *a = malloc(sizeof(foo));

这要求您在标题中定义struct foo,从而暴露其内部。如果您想在轨道上进行更改,则可能会破坏(错误地)直接依赖其成员的现有代码。

答案 1 :(得分:8)

让调用者分配内存的主要优点是它简化了界面,并且调用者拥有内存是完全明确的。正如您的create / destroy示例所示,简化并不是很好。

我更喜欢Dave Hanson在C Interfaces and Implementations中建立的创建/销毁惯例:

struct foo *foo_new(...);   // returns result of malloc()
void foo_free(struct foo **foop); // free *foop's resources and set *foop = NULL

您遵循惯例:

struct foo *a = foo_new();
...
foo_free(&a);
// now `a` is guaranteed to be NULL

这个约定使你不太可能留下悬垂的指针。

答案 2 :(得分:4)

你发布的任何一种方法都是好的形式;前者更接近C ++处理事物的方式,后者更像是Objective-C。重要的是在代码块中平衡创建和销毁。这种做法属于减少coupling的范畴。有一种不好的做法是创建一个能够创建执行其他任务的函数,就像strdup那样,这使得更难以知道调用者是否必须在不查阅文档的情况下处理任何事情。

答案 3 :(得分:4)

两种方法都很好。考虑所有FILE *操作函数,它们不允许您自己分配文件。

如果您使用C编程,您通常需要完全控制所有内容。这意味着让调用者可以控制在何处以及如何分配结构是一件好事。就个人而言,如果我不需要不透明的结构

,我通常会为初始化创建2个函数
int foo_init(struct foo *f); // allows the caller to allocate 'f' 
                             //however is suitable
struct foo * new_foo(void);  // mallocs and calls foo_init, for convenience.

如果需要,相应的

 void foo_free(struct foo *f );   //frees and destroys 'f'
 void foo_destroy(struct foo *f); //cleans up whatever internal stuff 'f' has,
                                  // but does not free 'f' itself

如果您希望调用者将结构视为不透明,则只提供struct foo * new_foo(void);不暴露struct foo实现有一些好处:

  • 通过直接访问会员,不允许来电者逛逛或执行潜在的危险捷径。
  • 你可以在不破坏现有二进制文件的情况下更改struct foo(你没有破坏ABI),如果你正在实现一个库,这可能是一个大问题。
  • 您的公共头文件不需要公开struct foo
  • 的实现和其他必需的头文件

缺点

  • 调用者无法控制struct foo的分配
  • 您将不得不通过函数调用
  • 来操作struct foo

答案 4 :(得分:3)

通过在同一个函数(或源文件)中分配和释放内存,您可以更轻松地识别潜在的内存泄漏(或说服自己没有),而无需跳到程序中的不同位置。如果被调用者分配内存,则应该进行重新分配的位置不明确。相反,通过让调用者这样做,该代码对内存负有“全部责任”。

最重要的是,一致性是关键。选择一种方法并坚持下去。

答案 5 :(得分:3)

Friendlier 中更好。让调用者分配内存,调用者可以决定如何分配内存。他们可以明显地选择堆栈和堆栈。但在某些情况下也有多个堆之间。他们可以将多个分配打包到单个malloc调用中(当您需要将数据封送到另一个地址空间时很有用)。

在Win32中,有GlobalAlloc(),这是分配可以在DDE消息中传递给其他应用程序的内存的唯一方法。 (不是任何人都在乎;)

在Win32中我们也有VirtualAlloc,它不经常使用,但有一些属性使它对某些特殊情况非常有用。 (初始化后,您可以将内存从读写更改为只读。)

还有CreateFileMapping/MapViewOfFile,它可以获取由特定文件支持的内存 - 写入内存,最后写入文件。

我确信Unix具有相同的专用内存分配功能。

答案 6 :(得分:3)

我对此的看法是 - 有两种方法可以解决这个问题:

如果编写一个分配内存的函数,在函数上面写一条注释,表明内存管理的责任在于程序员,即明确释放内存,从而将内存管理的负担传递给负责的程序员

或者,编写一个类似于此的包装函数,以_alloc结尾,以及以_free结尾的相应包装函数,这样,您将定义一组记录良好的例程,使其成为程序员更容易阅读。

简单的优点是:如果程序员非故意引入了内存泄漏,那么警告是存在的,因为C中的格言就是这个'对于每个malloc,如果你没有,应该有相应的免费拥有它,然后你有泄漏'。程序员反过来可以提示并说“Aha ..我称这个包装函数something_alloc但没有调用something_free”。你得到了要点?无论如何,程序员会感谢你的!

真的,这取决于代码API的定义。如果你想编写代码来管理内存,从而让程序员免于管理内存的责任,最好包装它并给它一个特殊的含义,正如我所建议的那样使用下划线后跟'alloc'和'free ”。

这将赢得你的荣誉和尊重,因为将会阅读和使用你的代码的程序员会说 - '谢谢芽',最终结果是每个人都会很高兴。

希望这有帮助, 最好的祝福, 汤姆。

答案 7 :(得分:2)

这一切都取决于建立记忆的所有权。

当项目变得非常大时,很难弄清楚所有内存的去向。

在C ++中,我们经常使用诸如foo_create()示例之类的工厂来解决这个问题。该工厂知道如何设置foo对象,并且可以轻松跟踪分配的内存量和释放量。

虽然可以在C中完成类似的操作,但通常我们只需确保程序的每一层都清理它使用的内存。因此,审阅者可以浏览代码以确保每个malloc具有匹配的免费。当分配过于嵌套时,很快就会发现内存泄漏发生的地方很快。

顺便说一句,为了从初始化程序返回错误值,我倾向于倾向于使用与分配分开的初始化程序。如果你只是调用foo_create(),并返回一个空指针,则不清楚创建是否因内存不足或其他原因而失败。养成在init函数上使用返回值的习惯可以节省大量的调试时间。

答案 8 :(得分:1)

我更喜欢GLib风格(你提到的第一种)。对我来说,选择这种风格使其更像面向对象。 您的方法负责创建和销毁结构,因此您不必与结构的内部进行对抗。这种方法使您的代码也可以减少错误。

一个GString示例:

GString *s;
s = g_string_new();
// Not in this case, but sometimes you can
// find this:
if (s == NULL)
    printf("Error creating the object!");

答案 9 :(得分:1)

让调用者分配内存更好,因为您可以通过手动回收旧数据结构来节省内存分配。当你有很多N大小的数组时,这在mathy应用程序中很有用。记住,内存分配很慢。

另一方面,如果数组的大小只能由函数确定(即结果的大小未知),那么被调用者应该分配。

无论你做什么,都要使用惯例告诉人们发生了什么。像pre_allocated_N_arraynew_result_array这样的大愚蠢的名字(对不起,我不是C专家,但应该有C约定)对于那些在没有阅读文档的情况下使用你的功能的人来说非常方便。这一切都归结为一致性。

答案 10 :(得分:0)

如果您自己分配记忆,则可以控制自己的记忆方式。在堆栈,标准malloc或您在应用程序中使用的16个内存管理器之一。

如果为你分配了内存,你不仅无法控制它的完成方式,而且你应该知道如何释放内存。好吧,大多数图书馆都会免费为您提供“免费”功能。

话虽如此,我仍然认为没有一种“更好”的方法。什么更适合您的使用。