何时应该在C函数中返回新分配的内存?

15
在其他地方的一个回答中,我找到了以下代码片段:

通常在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)

如果foo_create()出现错误会发生什么?如果foo_init(foo*)出现错误会发生什么? - Andres
3
“nicer”指的是对恶意软件编写者更友好吗?strcpy不比其他任何东西更友善。你不应该在函数中分配内存的原因是,如果不读取源代码,可能很难看出它们分配了内存。 - user14554
1
我不同意你最后一个观点,@jbcreix。虽然我没有看过源代码,也没有在背地里分配内存,但我从来没有遇到过strdup的任何问题。这是因为文档说明了它的工作原理。那些懒得阅读所使用产品文档的人是我非常讨厌的一类人(当然不包括你,只是一般而言)。如果你不能在函数中分配内存,ISO WG会将其定为违法操作。既然标准规定可以这样做,那么这样做就是完全可以的。 - paxdiablo
11个回答

14
无论何时你需要一个不透明的结构体,并且不想在头文件中暴露它的内部实现,你的foo_create()示例就阐述了这一点。
另一个例子是Windows API,例如CreateWindow会给你一个HWND, 你不知道实际的WND结构体长什么样子,也不能触及其字段。
同样,内核对象句柄也是如此。例如CreateEvent会给你一个HANDLE,你只能使用定义良好的API来操作它,并使用CloseHandle()关闭它。
struct foo *a = malloc(sizeof(foo));

这需要在头文件中定义struct foo,因此需要暴露其内部。如果您想在后续更改它,您可能会破坏直接依赖其成员的现有代码。


10
由调用者分配内存的主要优点是简化了接口,并且清楚地表明了调用者拥有内存。正如你的创建/销毁示例所显示的那样,这种简化并不是非常大。
我更喜欢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

这个约定使得你遗留悬空指针的可能性稍微降低了一些。

6

无论您采用哪种方法,都是良好的形式;前者更接近C++处理方式,后者更类似于Objective-C。重要的是在代码块内平衡创建和销毁。这种做法属于减少耦合的范畴。不良实践是有一个函数同时创建某些内容并且执行其他任务,就像strdup一样,这使得难以知道调用者是否需要咨询文档来处理任何事情。


2
strdup() 只有一个功能 - 在分配的内存中创建字符串的副本。如果它还向标准错误写入消息或打电话给其作者,那就糟糕了。 - Jonathan Leffler
就C语言的习惯而言,你可以认为strdup有两个作用,因为它既分配内存又复制内容(克隆不是常见的C语言习惯)。你还应该认为克隆习惯独立于语言存在,而strdup执行克隆操作,无论名称如何。我只是用它作为例子,因为它出现在问题中。 - outis
我同意一个函数只做一件事的原则,但是“返回输入字符串的新分配副本”对我来说看起来就像是一个任务。我认为这个分配操作更像是一个形容词而不是一个单独的子句。 - David Thornley
就我个人而言,我认为“克隆”(以及浅复制和深复制)是一个单一的任务,但我不想争论这一点,因为提出原始观点的人不参与这次讨论。 - outis

5

这两种方法都是完全可行的。考虑到所有FILE*操作函数,它们不允许您自己分配一个FILE。

如果您正在使用C编程,通常希望对一切都有完全控制。这意味着将结构体的分配位置和方式交给调用者是一件好事。个人而言,如果我不需要一个不透明的结构体,我通常会创建两个初始化函数。

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

在同一个函数(或源文件)中分配和释放内存,您将更容易识别潜在的内存泄漏(或说服自己没有),而不必在程序的不同位置跳来跳去。如果被调用者分配内存,则不清楚应该在哪里进行解除分配。相反,让调用者完成,那段代码就要对内存“全责”。

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


4
我的看法是这样的 - 处理这个问题有两种方法:
如果你编写了一个分配内存的函数,请在函数上面写一个注释,说明内存管理的责任在程序员身上,即显式释放内存,将内存管理的负担传递给程序员,由程序员负责。
或者,编写一个类似于以下结尾为_alloc和相应的结尾为_free的包装函数,这样,您定义了一组文档良好的例程,使程序员更容易阅读。
简单的优点是:如果程序员无意中引入了内存泄漏,则会出现警告,因为C语言的格言是“每个malloc都应该有一个相应的free,如果没有,则会泄漏”。程序员可以打开提示并说:“啊哈……我调用了这个包装函数something_alloc但没有调用something_free”。你明白吗?不管怎样,程序员都会感谢你!
实际上,这取决于代码API的定义有多好。如果你想编写管理内存的代码,从而解放程序员的内存管理责任,最好将其包装并赋予特殊含义,就像我建议的那样,使用下划线后跟“alloc”和“free”。
这将赢得你的赞誉和尊重,因为将阅读和使用你的代码的程序员会说 - “谢谢,伙计”,最终结果是每个人都会感到满意。

顺便提一下,上述替代方案的另一个优点是保护指针不被搞乱。确保返回指针并在函数中传递它。将其视为黑盒或封装的形式。 - t0mm13b

3
更友好,指的是更加舒适。让调用者分配内存,并且决定如何分配内存。他们可以在栈和堆之间进行选择,有时还可以在多个堆之间进行选择。他们可以将多个分配打包成单个malloc调用(在需要将数据编组到另一个地址空间时非常有用)。
在Win32中,有GlobalAlloc(),这是传递DDE消息给其他应用程序的唯一方式。(现在似乎已经没有人关心了;)
在Win32中,我们还有VirtualAlloc,它并不经常使用,但具有使其在某些特殊情况下无价的某些属性。(您可以在初始化后将内存从可读写更改为只读)。
还有CreateFileMapping/MapViewOfFile,它可以让您获得由特定文件支持的内存-对内存的写入最终会写入文件。
我相信Unix有与之等效的专业内存分配函数。

1
在Win32中,我们有一些需要通过调用函数来分配内存的结构体,以确定所需内存量、分配内存并再次调用函数。如果内存是可变的,我更希望函数能够直接分配内存。 - David Thornley

2
这一切都归结于建立内存所有权。
当项目变得非常庞大时,很难弄清楚所有内存的使用情况。
在C++中,我们通常通过使用类似foo_create()工厂来解决这个问题。该工厂知道如何设置foo对象,并可以轻松跟踪其分配和释放的内存量。
虽然在C语言中也可以做类似的事情,但通常我们只是确保程序的每个层次清理所使用的内存。因此,代码审查员可以快速查看代码以确保每个malloc都有相应的free。当嵌套太深时,内存泄漏发生的位置很快就变得不清楚了。
顺便说一句,我倾向于使用与分配内存分开的初始化函数,以便从初始化器返回错误值。如果你仅调用foo_create(),并得到一个空指针,则不清楚创建失败是否由于缺少内存或其他原因造成的。养成在init函数上有返回值的习惯可节省大量调试时间。

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!");

1

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

另一方面,如果只有函数可以确定数组的大小(即结果的大小未知),则调用者应该分配。

无论您做什么,请使用约定告诉人们发生了什么。像pre_allocated_N_arraynew_result_array这样的大而愚蠢的名称(抱歉,我不是C专家,但应该有C约定)对于那些在不阅读文档的情况下使用您的函数的人非常有用。这归结为一致性。


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