修剪时 realloc 会失败(返回 NULL)吗?

Posted

技术标签:

【中文标题】修剪时 realloc 会失败(返回 NULL)吗?【英文标题】:Can realloc fail (return NULL) when trimming? 【发布时间】:2012-08-20 22:57:59 【问题描述】:

如果执行下一步:

int* array = malloc(10 * sizeof(int));

我使用 realloc:

array = realloc(array, 5 * sizeof(int));

在第二行(只有它),它可以返回NULL吗?

【问题讨论】:

有些相关:***.com/questions/1736433/… 【参考方案1】:

是的,它可以。realloc() 没有实现保证,即使在收缩时它也可以返回不同的指针。

例如,如果一个特定的实现为不同的对象大小使用不同的池,realloc() 实际上可能会在池中为较小的对象分配一个新块,并为较大的对象释放池中的块。因此,如果较小对象的池已满,它将失败并返回 NULL


或者它可能只是决定移动块更好

我刚刚使用以下程序通过 glibc 获取实际分配的内存大小:

#include <stdlib.h>                                                          
#include <stdio.h>                                                           

int main()                                                                   
                                                                            
    int n;                                                                   

    for (n = 0; n <= 10; ++n)                                                
                                                                            
        void* array = malloc(n * sizeof(int));                               
        size_t* a2 = (size_t*) array;                                        

        printf("%d -> %zu\n", n, a2[-1]);                                    
                                                                            

n

因此,如果将 int[10] 缩小到 int[5],分配的大小将从 48 缩小到 32,有效地提供 16 个空闲字节。因为(正如刚刚提到的)它不会分配小于 32 字节的任何内容,所以这 16 字节会丢失。

如果它将块移动到其他地方,整个 48 个字节将被释放,并且实际上可以在其中放入一些东西。当然,这只是一个科幻故事,并不是真正的实现;)。


C99 标准中最相关的引用(7.20.3.4 realloc 函数):

退货

4 realloc 函数返回一个指向新对象的指针(可能与旧对象的指针具有相同的值),或者如果新对象不能是空指针已分配。

'May' 是这里的关键词。它没有提及可能发生这种情况的任何具体情况,因此您不能依赖其中任何一种,即使它们乍一看听起来很明显。


顺便说一句,我认为您可以认为 realloc() 有点过时了。如果你看一下 C++,新的内存分配接口(@98​​7654331@/delete 和分配器)甚至不支持这样的事情。他们总是希望你分配一个新块。但这只是一个松散的评论。

【讨论】:

我必须反对调用 realloc 已弃用,因为 C++ 在新/删除机制中没有类似物。 C++ 是一种与 C 非常不同的语言,特别是,在 C++ 中支持移动对象需要实现某种方式来通知对象它正在被重定位并允许它更新自己的内部引用。另一方面,C 不会自动化或封装任何这些,因此由调用者(因此非常好)负责在 realloc 之后是否需要更改对象的内容。 一般来说,我觉得用 C++ 代码和 C++ 关于弃用的想法来回答 C 问题有点奇怪。 确实,我什至都懒得去阅读代码......这应该真的解决了,因为这个问题是关于 C,而不是 C++。 我更喜欢 malloc_usable_size 而不是负索引。 这对我来说看起来不正确,因为有可用的内存,这是当前分配的。如果 realloc() 仅仅因为它想将内存移动到其他地方而失败,那么它返回 ENOMEM 是不正确的。不是内存不足,而是其他原因。【参考方案2】:

其他答案已经解决了这个问题,但假设您知道 realloc 调用是“修剪”,您可以将其包装为:

void *safe_trim(void *p, size_t n) 
    void *p2 = realloc(p, n);
    return p2 ? p2 : p;

并且返回值将始终指向大小为n的对象。

无论如何,由于realloc 的实现知道对象的大小,因此可以确定它正在“修剪”,从实现质量的角度来看,如果不在内部执行上述逻辑,那将是病态的糟糕.但是由于不需要realloc 来执行此操作,因此您应该自己执行此操作,可以使用上述包装器,也可以在调用realloc 时使用类似的内联逻辑。

【讨论】:

是的,我相信是的。为现有结果修剪存储的代码可能无法“退出”失败的进度并以有意义的方式将失败报告给更高级别的代码。因此,能够以不会失败的方式编写代码是非常有价值的。即使对malloc 的下一次调用将在其他地方失败,这将(至少在一个健壮的程序中)处于程序可以处理失败情况、退出任何部分工作并报告错误的地步. 是的,当然是。如果不是这样,realloc 在健壮的程序中将毫无用处。这实际上是一种极其常见的内存泄漏形式(即p=realloc(p,newsize);,如果realloc 失败,则会丢失旧内存)。 @R..:为什么一个无法有效减少分配大小的均匀远程质量实现不应该简单地忽略请求?该标准没有试图禁止质量差到无用的“符合”实现,但我认为没有理由以良好实现为目标的程序员应该迎合糟糕实现的怪癖。 @supercat:再次考虑一下,如果实现可以减小大小的唯一方法是让剩余部分永久不可用,或者在对象的生命周期内不可用,那么它可以说是更高质量的行为向调用者报告失败并让它知道它仍然可以使用完整的原始大小而不是隐藏额外的空间。 @supercat:这不是一个现实的情况,这是一个荒谬的情况。例如,如果分配粒度为 32 字节,则尝试将大小为 32 的对象调整为大小为 31 应该不会失败。让它这样做没有任何好处。另一方面,如果一个对象的大小为 10000000,并且您想将其大小调整为 10,但在支持分区为小大小的区域中没有可用内存,则报告失败而不是仅仅离开调用者就成功是有价值的一个浪费了大约 10MB 不可用空间的对象。【参考方案3】:

语言(和库)规范没有做出这样的保证,就像它不保证“修剪”realloc 将保留指针值一样。

实现可能决定以最“原始”的方式实现realloc:通过对新内存块执行无条件的malloc、复制数据和free-ing 旧块。显然,这种实现在内存不足的情况下可能会失败。

【讨论】:

【参考方案4】:

不要指望它。标准没有这样的规定;它只是声明“如果无法分配新对象,则为空指针”。

你很难找到这样的实现,但根据标准,它仍然是合规的。

【讨论】:

我相信你不应该调用这样的实现braindead。它实际上可能更优化 @MichałGórny 在我的语言中,“更优化”被认为是一个 pleonasm,所以我会避免这样说。但是,是的,我编辑了 :-) @cnicutar:出于某种原因,一些编写实现的人似乎将“聪明”和“愚蠢”视为反义词。【参考方案5】:

我怀疑在您描述的场景中可能存在理论上失败的可能性。

根据堆实现的不同,可能没有修剪现有分配块这样的事情。相反,首先分配一个较小的块,然后从旧块中复制数据,然后将其释放。

例如,桶堆策略可能就是这种情况(一些流行的堆使用,例如 tcmalloc)。

【讨论】:

在这种情况下只返回原始指针仍然是有效。这是否更有帮助,我不确定。报告错误信息更丰富,并允许调用者选择使用现有的超大分配,但它也很有可能破坏假定“修剪”realloc 永远不会失败的错误代码。 tcmalloc 如果收缩会失败,请检查源tcmalloc.cc 函数do_realloc() 用于tc_realloc(), (github.com/gperftools/gperftools/blob/master/src/…) @R..:标准确实应该定义一些标准宏来指示实现在各种极端情况下的行为方式,并允许代码拒绝在古怪的实现上运行,而不是需要额外的代码来执行处理质量实施中不应出现的情况。更好的可能是拥有一个更通用的分配控制函数,该函数带有一个参数来指示分配是预期增长还是缩小,并指示重定位是否可以接受。不允许实现忽略有关期望的信息,并且有请求...... ...如果可能的话,在不重新定位的情况下扩展一个块总是会失败,但是根据他们收到的信息和请求进行优化的质量实现可能会胜过那些不这样做的实现。在任何情况下,以良好实现为目标的程序员都不应该为了适应最底层的实现而向后弯腰。【参考方案6】:

有点晚了,但至少有一个流行的实现,realloc() 具有较小的大小可能会失败:TCMalloc。 (至少就我理解的代码而言)

如果你读取文件tcmalloc.cc,在函数do_realloc_with_callback()中,你会看到如果你收缩得足够多(分配的内存的50%,否则会被忽略),TCMalloc会先分配新的内存(并且可能失败)然后复制它并删除旧内存。

我不会复制源代码,因为我不确定(TCMalloc 和 *** 的)版权是否允许这样做,但这里有一个 link to the source(修订于 2019 年 5 月 17 日)。

【讨论】:

【参考方案7】:

realloc 不会在收缩现有内存时失败,因此它不会返回NULL。只有在扩展过程中失败,它才能返回NULL

但是在某些架构中,收缩可能会失败,realloc 可以以不同的方式实现,例如单独分配较小的内存并释放旧内存以避免碎片。在这种情况下,缩小内存可以返回 NULL。但它的实现非常罕见。

但最好是在更安全的一面,在缩小内存后保持NULL检查。

【讨论】:

这个实现有保证吗?或者实现仍然可以尝试在 realloc 上移动分配的内存(例如“free”和“malloc”)并因此失败? 那么声明 “不会失败” 是不正确/具有误导性的 :) 在某些 RTOS 架构中,realloc 可以通过 free 和 malloc(smallersize) 实现以避免碎片。 (我只是指出您的前两句话和其余的答案不同意。这就是为什么它没有任何赞成票的原因......它要么可以 失败或永远不会失败。选择一个。) 如果一个架构试图收缩,那么它不会失败,但如果它做 malloc(smallersize) 和 free(oldblock) 那么它可能会失败(但这种类型的实现非常罕见)。跨度>

以上是关于修剪时 realloc 会失败(返回 NULL)吗?的主要内容,如果未能解决你的问题,请参考以下文章

如果失败,realloc 会释放前一个缓冲区吗?

malloc realloc calloc的区别

realloc 会覆盖旧内容吗?

是真的吗,现代操作系统在调用 realloc 时可能会跳过复制

为啥我不能返回 realloc 的结果? (当你看到代码时你会得到问题......)

将 realloc() 返回的地址分配给同一个指针是一种好的编码习惯吗?