使用包装函数调用malloc() / realloc() ... 这是一个好主意吗?

3

为了完成一个任务,我需要分配一个动态缓冲区,使用malloc()来分配初始缓冲区,如果需要扩展该缓冲区则使用realloc()。在我使用(re|m)alloc()的任何地方,代码看起来像下面这样:

char *buffer = malloc(size);

if (buffer == NULL) {
    perror();
    exit(EXIT_FAILURE);
}

该程序仅从文件中读取数据并输出,因此我认为在(re|m)alloc失败时退出程序是个好主意。现在,真正的问题是:
是否有益于对调用进行包装,例如像这样?
void *Malloc(int size) {
    void *buffer = malloc(size);

    if (buffer == NULL) {
        perror();
        exit(EXIT_FAILURE);
    }

    return buffer;
}

这是一个不好的主意吗?

1
我会将实用函数称为 checked_malloc - Vlad
调用perror而不带参数会导致未定义的行为。但是,空参数是有效的。下次请包含正确的头文件,这样编译器就会在您编写此类错误代码时给出错误提示。 - R.. GitHub STOP HELPING ICE
6个回答

4
以这种形式提供是个不好的主意,因为在除了为作业编写的微不足道的程序之外,你想要做的事情比退出更有用/优美。所以最好不要养成坏习惯。这并不是说分配器包装器本身就是坏的(集中处理错误可能是件好事),只是一个你没有检查返回值的包装器(例如,在失败时根本不返回)是个坏主意,除非你提供了某种机制来允许代码钩入退出逻辑。
如果您确实想按照您的方式进行操作,我强烈建议使用一个更清晰不同的名称而不是malloc,比如malloc_or_die。 :-)

3
除非是一个简单的程序,否则在操作系统因为分页活动而瘫痪之后才会出现内存错误。即使在这种情况下发生了内存溢出,往往也只能让程序崩溃,无法采取其他有效的措施。因此,最好的策略是在非简单程序中尽量避免内存耗尽的情况。 - Marcelo Cantos
当你内存耗尽时,还有哪些明智的选择呢? - Simone
是和不是... 是因为通常你可能有其他资源需要在 exit() 之前清理;不是因为内存不足不是大多数程序可以明智地恢复的错误类型,而且操作系统会自动清理大多数资源(除了可能的 IPC 资源)。 - j_random_hacker
1
@Marcelo:说得好,当然这完全取决于你正在处理哪个操作系统(例如嵌入式与桌面不同)。@Simone:释放一些内存。;-) - T.J. Crowder
有趣的是,一些操作系统(包括Darwin)甚至不会从malloc()返回NULL,除非分配的大小非常大。相反,您将获得一个有效的指针,然后如果遇到内存不足的情况,当您尝试访问其中一个指针时,您将遇到SIGSEGV或类似的错误。 - Justin Spahr-Summers
显示剩余8条评论

4
在大多数情况下,尝试使用空指针会很快导致程序崩溃,这使得调试更容易,因为你会得到一个漂亮的核心转储文件,但如果调用exit()则不会得到。 我唯一的建议是,在分配完内存后尽快对返回的指针进行取消引用,即使只是任意地这样做,以便核心转储可以直接将您带到错误的malloc调用。 由于内存耗尽后很少有很多事情可以做,所以退出通常是正确的选择。但要以使事后检查更容易的方式退出。 需要明确的是,内存耗尽通常发生在操作系统被页面交换活动破坏很久之后。 这个策略真的只有在捕获荒谬的分配(例如由于错误而尝试malloc(a_small_negative_number))时才有用。

如果您不立即正确使用指针,那么最好abort()程序,因为稍后发现它为空可能会很麻烦。 - Antoine Pelisse
@Antoine:因此,我的建议是尽快取消引用指针。正如在@T.J.Crowder的答案下讨论的那样,获取非NULL指针可能并不能保证该指针可用。没有什么比实际取消引用指针更好的了。 - Marcelo Cantos
@Marcelo,@Antoine:正如有人在另一个回答的评论中指出的那样,不幸的是,在现代系统上,您永远不会看到NULL指针,因为虚拟内存几乎是无限的。如果您回收太多内存,您的程序将会明显变慢,因为系统必须进行换页操作。直到所有的换页空间都被用完后,它才会崩溃,这通常会发生得非常晚。 - Jens Gustedt
@Marcelo:你说的“works :)”是什么意思啊?它不会造成太大的问题,但也不会检测出任何东西... - Jens Gustedt
@Marcelo:我认为你的观点不正确。你必须解除引用分配给每个页面,才能检测到这一点,如果你对每次调用malloc都这样做,这可能会非常昂贵。顺便说一下,这不仅适用于BSD,而且对于大型分配,Linux(我猜其他系统也是如此)也是这样做的。 - Jens Gustedt
显示剩余2条评论

3

1
实际上,我认为这个问题使得它成为了一个重复的问题。而那个答案中的分析是非常出色的。 - T.J. Crowder

3

在你的情况下是可以的。只需记得通过提供退出原因的消息,并指定行号,这会很好。类似于:

void* malloc2(int size, int line_num){
    void *buffer = malloc(size);
    if (buffer == NULL) {
        printf("ERROR: cannot alloc for line %d\n", line_num);
        perror();
        exit(EXIT_FAILURE);
        }
    return buffer;
};

#define Malloc(n) malloc2((n), __LINE__)

编辑:正如其他人所提到的,对于有经验的程序员来说这不是一个好习惯,但对于初学者而言,即使在“顺利”的情况下也难以跟踪程序流程,这是可以接受的。


1
+1 对于行号的想法表示赞同。不赞成直接放弃,但如果必须这样做,一定要告诉人们“何处”。 - T.J. Crowder

1
“检查malloc失败是否无用,因为过度提交”或“操作系统在malloc失败时已经崩溃”这些想法已经严重过时。强大的操作系统从来没有过度提交内存,而历史不太强大的操作系统(如Linux)现在有简单的方法来禁用过度提交并保护免受内存耗尽导致操作系统崩溃的影响-只要应用程序在malloc失败时不崩溃和烧毁!”
“现代系统中malloc失败的原因有很多:”
  • 物理资源不足以实例化内存。
  • 虚拟地址空间耗尽,即使有大量可用的物理内存。这在32位机器(或32位用户空间)上很容易发生,当ram+swap> 4gb时。
  • 内存碎片。如果您的分配模式非常糟糕,您可能会得到400万个16字节块,相距1000字节,无法满足malloc(1024)调用。
“如何处理内存耗尽取决于程序的性质。”

从整个系统的健康角度来看,程序崩溃是好事。这减少了资源匮乏的情况,可能使其他应用程序继续运行。但另一方面,如果这意味着失去数小时的视频编辑、打字、撰写博客文章、编码等工作,用户会非常沮丧。或者,如果他们的mp3播放器突然因内存不足而死机,这意味着磁盘停止抖动,他们可以回到文字处理器并点击“保存”,那么他们可能会感到高兴。

至于OP最初的问题,我强烈建议不要编写在失败时崩溃的malloc包装器,或者编写仅假定在使用空指针时立即出现段错误的代码,如果malloc失败。这是一个容易养成的坏习惯,一旦你编写了充满未经检查的分配的代码,将无法在任何需要鲁棒性的程序中重用该代码。

一种更好的解决方法是仅返回失败给调用函数,并让调用函数将失败返回给其调用函数,以此类推,直到回到main或类似位置,在那里您可以编写if(failure)exit(1);。这样,代码可以立即在其他情况下重复使用,您可能真正希望检查错误并采取某些恢复步骤以释放内存、保存/转储有价值的数据到磁盘等。

0

我认为这是一个不好的想法,因为首先检查malloc的返回值在现代系统上并没有什么用处,其次,这会给你错误的安全感,认为当你使用这样的调用时,所有的分配都是正确的。

(我假设你是在一个托管环境下编写代码,而不是嵌入式、独立的环境。)

具有大型虚拟地址空间的现代系统将从mallocrealloc中返回(void*)0几乎是不可能的,除非参数是虚假的。当你的系统开始疯狂地交换或甚至耗尽交换空间时,你会遇到更多的问题。

所以不要检查这些函数的返回值,这没有太多意义。相反,使用断言检查malloc的参数是否等于0(对于realloc,如果两个参数同时为0),因为这时问题不在mallocrealloc内部,而是你调用它们的方式有问题。


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