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

14

如果按照以下步骤进行:

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

然后我使用realloc:

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

在第二行(且仅在第二行),它能返回NULL吗?


有点相关:https://dev59.com/xXI-5IYBdhLWcg3wqqXh?rq=1 - user166390
7个回答

15

可以的。 关于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 <= 6,它分配32字节,对于7-10,则为48字节。
因此,如果将int[10]收缩到int[5],则分配的大小将从48缩小到32,有效地提供16个空闲字节。由于(正如刚才指出的那样)它不会分配少于32字节的任何东西,所以这16个字节将丢失。
如果将块移动到其他位置,则整个48字节将被释放,实际上可以在其中放置某些内容。当然,那只是一个科幻故事,而不是真正的实现;)。
C99标准中最相关的引用(7.20.3.4 realloc函数):
“返回”: realloc函数返回指向新对象的指针(可能与指向旧对象的指针具有相同的值),或者如果无法分配新对象,则返回空指针。
这里的“可能性”是关键词。它没有提到任何特定情况下可能发生的情况,因此即使它们一开始看起来很明显,您也不能依赖它们。
顺便说一下,我认为你可以认为realloc()有点过时了。如果您看一下C ++,则较新的内存分配接口(new / delete和分配器)甚至不支持这种东西。它们总是期望您分配一个新块。但那只是一个宽松的评论。

8
我必须反对将 realloc 称为弃用函数,只是因为 C++ 在 new/delete 中没有类似的函数。C++ 是一种与 C 非常不同的语言,特别是支持移动对象需要一些方法来通知对象正在被重新定位,并允许其更新自己的内部引用。另一方面,C 并没有自动化或封装任何这些操作,因此由调用者(因此完全可以)负责确定在 realloc 后对象的内容是否需要更改。 - R.. GitHub STOP HELPING ICE
13
йҖҡеёёжғ…еҶөдёӢпјҢжҲ‘и§үеҫ—дҪҝз”ЁC++д»Јз Ғе’Ңжңүе…іејғз”Ёзҡ„C++жҖқжғіжқҘеӣһзӯ”CиҜӯиЁҖй—®йўҳжңүзӮ№еҘҮжҖӘгҖӮ - Jens Gustedt
1
事实上,我甚至都没有费心去阅读代码...这个问题确实需要解决,因为它是关于C而不是C++的。 - R.. GitHub STOP HELPING ICE
1
我更喜欢使用malloc_usable_size而不是负索引。 - Alcaro
这个看起来对我来说是不正确的,因为有可用的内存,这是当前分配的。如果realloc()失败只是因为它想将内存移动到其他地方,那么它返回ENOMEM是不正确的。这不是缺乏内存,而是其他原因。 - hdante

7
其他回答已经回答得很好了,但是假设您知道 realloc 调用是“删减内存”的话,您可以使用以下方法包装它:
void *safe_trim(void *p, size_t n) {
    void *p2 = realloc(p, n);
    return p2 ? p2 : p;
}

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

无论如何,由于realloc的实现知道对象的大小并且因此可以确定它正在“修剪”,从实现质量的角度来看,不执行上述逻辑在病态情况下是非常糟糕的。但是,由于realloc不需要这样做,您应该自己执行此操作,使用上面的包装器或类似的内联逻辑调用realloc时执行。


3
是的,我相信这是正确的。为现有结果修剪存储空间的代码在发生失败时可能无法“撤消”其进度,并以有意义的方式向更高级别的代码报告失败。因此,能够以一种不会失败的方式编写代码非常有价值。即使下一次对malloc的调用会在其他地方失败,在一个健壮的程序中,也将在程序可以处理失败情况、撤销任何部分工作并报告错误的点上进行。 - R.. GitHub STOP HELPING ICE
3
当然是这样。如果不是这样,那么在健壮程序中,realloc将毫无用处。这实际上是一种极为常见的内存泄漏形式(即p = realloc(p,newsize);,如果realloc失败,则会丢失旧内存)。 - R.. GitHub STOP HELPING ICE
1
@R..:是否有理由说,即使是质量稍微好一点的实现也无法有用地减少分配大小,这些实现就应该简单地忽略这个请求呢?标准并不试图禁止“符合规范”的实现,这些实现质量非常差以至于毫无用处,但我看不出程序员针对良好实现的应用为什么要迎合悲惨实现的怪癖。 - supercat
1
@supercat:再次考虑,如果实现唯一能够减小大小的方法是通过永久地使剩余部分无法使用,或者在对象的生命周期内无法使用,那么报告失败并让调用者知道它仍然可以使用完整的原始大小而不是隐藏额外的空间,这可能是更高质量的行为。 - R.. GitHub STOP HELPING ICE
1
@supercat:这不是一个现实的情况,而是一个荒谬的情况。如果分配粒度为32字节,尝试将大小为32的对象调整为大小为31不应该失败。这样做没有任何好处。另一方面,如果一个对象的大小为10000000,你想将其调整为大小为10,但在支持分区成小尺寸的区域中没有可用内存,报告失败而不是成功地留下一个浪费约10MB不可用空间的对象对调用者有价值。 - R.. GitHub STOP HELPING ICE
显示剩余13条评论

3

语言规范(和库)不做此类保证,就像它不保证“trimming”realloc将保留指针值一样。

实现可能决定以最“原始”的方式实现realloc:通过对新的内存块进行无条件malloc,复制数据并释放旧块。显然,这种实现在低内存情况下可能会失败。


3
不要指望它。标准没有这样的规定,它只是陈述:“如果无法分配新对象,则返回空指针”。虽然很难找到这样的实现,但按照标准仍然可以兼容。

我认为你不应该称这样的实现为“脑残”。它可能实际上是“更优化”的。 - Michał Górny
@MichałGórny 在我的语言中,“更优化”被认为是多余的,所以我会避免说它。但是,是的,我已经编辑了 :-) - cnicutar
@cnicutar:由于某种原因,一些编写实现的人似乎认为“聪明”和“愚蠢”是反义词。 - supercat

2

我怀疑在你描述的情况下存在一个理论上的可能失败的情况。

根据堆实现的不同,可能不存在修剪现有分配块的情况。相反,首先分配一个较小的块,然后将数据从旧块复制过去,最后再释放它。

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


在这种情况下,仅返回原始指针仍将是有效的。是否更有帮助,我不确定。报告错误更具信息性,并允许调用者选择使用现有的超大分配,但它也有很高的可能性破坏假定“修剪”realloc永远不会失败的糟糕代码。 - R.. GitHub STOP HELPING ICE
1
如果tcmalloc缩小,可能会失败,请检查源代码tcmalloc.cc中的函数do_realloc(),该函数用于tc_realloc()。(https://github.com/gperftools/gperftools/blob/master/src/tcmalloc.cc#L1446) - 12431234123412341234123
@R..:标准应该定义一些标准宏来指示实现在各种边角情况下的行为,并允许代码拒绝在古怪的实现上运行,而不是要求额外的代码来处理在高质量实现中不应出现的情况。更好的做法可能是有一个更通用的分配控制函数,带有一个参数来指示分配是否预计增长或缩小,并指示是否接受重定位。实现将不被允许忽略关于期望的信息,并具有请求... - supercat
尝试在不进行重定位的情况下扩展块始终会失败,但是优化基于信息和请求的高质量实现可能会胜过那些没有这样做的实现。无论如何,针对良好实现的程序员不应该为了适应最差的实现而弯腰。 - supercat

2
略晚了一些,但至少有一个流行的实现方式,即使用较小的大小进行realloc()可能会失败:TCMalloc。(至少就我理解代码而言)
如果您阅读文件tcmalloc.cc,在函数do_realloc_with_callback()中,您将看到如果收缩足够(分配内存的50%,否则将被忽略),TCMalloc将首先分配新内存(并可能失败),然后复制它并删除旧内存。
我不会复制源代码,因为我不确定版权(TCMalloc和Stackoverflow)是否允许,但是这里有一个链接到源代码(截至2019年5月17日的版本)。

-1

realloc 在缩小现有内存时不会失败,因此它不会返回NULL。只有在扩展期间失败时才会返回NULL

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

但为了更安全起见,在缩小内存后也最好进行NULL检查。


这个实现是否有保障?或者在重新分配内存时,实现是否仍然可以尝试“移动”已分配的内存(例如,“free”和“malloc”),从而导致失败? - user166390
2
那么语句“不会失败”是不正确/误导的 :) - user166390
在某些RTOS架构中,realloc可以通过使用free和malloc(较小的大小)来实现,以避免内存碎片化。 - rashok
2
我只是指出你的前两句话和回答的其余部分不一致。这就是为什么它没有任何赞成票的原因...它可能会失败或永远不会失败。选择一个。 - user166390
如果一个架构试图缩小,则不会失败,但如果它执行malloc(较小的大小)并释放旧块,则可能会失败(但这种实现方式非常罕见)。 - rashok
如果你将它缩小到零会怎样?那么它将返回NULL。 - Trevor Hickey

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