我有一个任务要用C实现realloc,这是我的代码,请检查一下。

void    *my_realloc(void *ptr, size_t len)
{
    void    *real;

    real = malloc(len);
    memset(real, 0, len);
    if (real)
        memcpy(real, ptr, len);
    free(ptr);
    return (real);
}


评论

我想说这是不可能的,因为您仅通过提供指针就无法知道分配区域的大小。分配是没有意义的,除非您还重写了malloc和free,以便可以存储分配的大小,例如,在该块的第一个字节中。但是单独重新实现realloc是不可能的。

在开始编写代码之前,您是否阅读了realloc规范?那应该是您的第一步。

您的分配包括重新实现malloc,free和calloc吗?您的realloc是否打算在平台的malloc返回的块上工作?如果是这样,下面的答案告诉您您被卡住了!

#1 楼


由于格式问题,代码给人留下了不好的印象。特别是,返回类型和函数名称之间的多余空格,以及变量的类型与其名称之间的多余空格看起来很奇怪。
除非编写的代码需要与C99之前的编译器兼容,否则应打破在块顶部声明变量并稍后对其进行初始化的习惯。而是将变量的声明推迟到可以初始化的时候(在可能的范围内)。这有助于最大程度地减少由未初始化的变量引起的错误。
我建议始终使用大括号在if语句之后创建块作用域,即使您当前只有一个语句也是如此。这样,当您稍后返回并添加一些其他逻辑时,可将发生严重错误的可能性降至最低。
必须在调用malloc之后立即检查malloc的返回值!如果malloc失败,它将返回空指针,并且任何使用空指针的尝试都将导致未定义的行为。因此,在检查memset的结果之后,需要将对malloc的调用移至,否则可能会出现不确定的行为。
实际上,您根本不需要调用memset。如果检查C语言规范以了解有关realloc函数的信息,则在新大小大于旧大小的情况下,它不会初始化附加内存。

realloc规范也可以通过其他几种方式使用。如果您的目标是模拟/重新发明realloc,那么您绝对必须确保您的实现行为与realloc完全相同。

实际上,realloc实际上是包装在单个函数中的完整内存管理子系统,它完成了mallocfree所做的所有工作,甚至更多,这使得其语义非常复杂且令人困惑。以下是该标准中对realloc的相关要求的摘要:如果请求的块大小小于块的原始大小,则realloc要么在末尾释放多余的内存,要么该块并返回输入指针不变,或分配一个适当大小的新块,释放原始块,然后将指针返回此新分配的块。
如果请求的块大小大于原始大小对于该块,realloc可以在新地址处分配一个扩展块,然后将原始块的内容复制到新位置。在这种情况下,将返回指向扩展块的指针,并且未初始化该块的扩展部分。或者,如果可能的话,它可以就地扩展原始块并返回原样的输入指针。
如果realloc不满足扩展块的请求,则它返回空指针,并且不释放原始块。 (如果请求缩小一个块,则realloc总是会成功。)
如果输入指针为null,则realloc的行为就像您调用malloc(size)一样,将指针返回到请求大小的新分配块,如果无法满足请求,则返回null指针。
如果请求的大小为0并且输入指针为非null,则realloc的行为与调用free(ptr)的行为完全相同,并且始终返回null指针。 br />如果输入指针为null且请求的大小为0,则结果不确定!


为什么!为正确起见,您的代码需要准确地实现所有这些条件。相反,如果您不模拟realloc(这很好,也许是因为您希望在程序中使用不同的行为而重新发明了轮子),则需要使用不同的名称,以免其他程序员误以为它是与realloc相同,并且不会期望它具有相同的行为。


实际上,正如您从上述需求列表中看到的那样,实际上不可能重新发明realloc根据规范的要求,因为您不具备有关标准库如何实现内存管理的内幕知识。您需要能够确定ptr指向的内存块的原始大小,并且没有可移植的方法。

为什么需要此信息?有两个原因。首先,您需要知道调用方是否正在请求缩小或扩展内存块,以便您可以遵循正确的语义。其次,您需要知道调用memcpy时要复制多少个字节—现在,您正在复制原始缓冲区的末尾,这是未定义的行为!

第一个问题可以通过解决更改函数的语义,以使其仅增加块的大小,将其重命名为GrowBlockExpandMemory之类的名称,以表明其与realloc不同。不幸的是,没有可靠的方法来实现此要求,即assert大于或等于函数体内len所指向的内存块的当前大小,因此仅凭文档就足够了,这非常薄弱的保证。更不幸的是,这将无法解决第二个问题-我们仍然无法正确调用ptr

因此,我们唯一的真实选择是修改函数的签名以接受memcpy的原始大小作为参数(除了您已经接受作为参数的所需新大小之外)。


考虑到所有这些,这就是我的写法:的ptr!这是关于为什么realloc实际上是不良设计的一个很好的教训。您不应该编写大量的多功能函数,当然也不应该在单个函数中实现整个子系统!而是将重要行为分解为单独的功能。在这种情况下,您可能具有四个单独的函数,这些函数分配一个块(realloc),释放一个块(AllocateMemory),展开一个块(FreeMemory)和缩小一个块(ExpandMemory)。这种分工使得实现更容易编写,更易于推理,更容易包含错误检查,因此更易于维护且更不可能包含错误。我知道您的任务是写ShrinkMemory,但是这里有一个更广泛的(也许是无意的)课,您不要错过。

评论


\ $ \ begingroup \ $
评论不用于扩展讨论;此对话已移至聊天。
\ $ \ endgroup \ $
– Jamal♦
16 Dec 31'6:16

\ $ \ begingroup \ $
该答案已被选为2016最佳代码评论-最佳新人(答案)的获奖者。
\ $ \ endgroup \ $
– 200_success
17年1月18日在19:10

\ $ \ begingroup \ $
哇,谢谢您来之不易的声誉@ 200的认可和捐赠!
\ $ \ endgroup \ $
–科迪·格雷
17年1月18日在19:20

\ $ \ begingroup \ $
当调用realloc的大小小于原始大小时,则完全不需要返回相同的指针。根据实现的不同,很可能会返回不同的指针。甚至有可能失败。
\ $ \ endgroup \ $
– gnasher729
17年1月24日在11:24

\ $ \ begingroup \ $
是的,@ gnasher。我已经说了很多:“我们实际上无法就地缩小一个内存块,因此我们要么必须返回不变的内存块(这是合法的,因为始终允许内存块大于所需的大小),或者分配一个新的较小的块,复制原始数据中适合的部分,并返回指向这个新的缩小块的指针。后者实际上会变慢,因此,在当前实现中,我们将避免做这些额外的工作。”哦,我明白你在说什么。该要点表示返回了相同的指针。我会解决的。
\ $ \ endgroup \ $
–科迪·格雷
17年1月24日,11:42



#2 楼

通常,mallocreallocfree都是同一库的一部分。允许这样做的一件事是一些“幕后”元数据修饰。例如,如果您要调用malloc(16),则内存库可能会分配20个字节的空间,其中前4个字节包含分配的长度,然后返回指向块开头的4个字节的指针。

调用free时,库将从传入的指针中减去4个字节,以找到指向的指针。分配的长度。

realloc也可以使用此技巧来找出原始分配的时间。

因此,在回答您的问题时,您不仅可以实现realloc,还必须实现mallocfree

此外,如果您有原始的K&R,我相信您也可以在其中找到一些realloc来源。

评论


\ $ \ begingroup \ $
元数据不是便携式解决方案。 C没有指定“该库将从传入的指针中减去4个字节”。
\ $ \ endgroup \ $
–chux-恢复莫妮卡
16年12月31日在6:04

\ $ \ begingroup \ $
谁说便携式的?注意,我说“可能”分配。当然,C规范没有指定此特殊技巧,但是许多实现确实使用此思想来包括内存分配元数据。例如,Microsoft的malloc的调试版本包括诸如分配内存的文件/行之类的内容,因此,如果您忘记释放它,则在程序退出时,它将列出所有具有挂起分配的代码。一个特定的库将以自己的方式实现它,唯一可移植的位是接口。
\ $ \ endgroup \ $
–尼尔
16年12月31日在10:39

\ $ \ begingroup \ $
我刚刚检查了K&R的第8章,他们对malloc的实现确实使用了这种技巧,但是他们的元数据是一个指针(指向链中的下一个块)和一个无符号的(块的大小)。 ime.usp.br/~pf/Kernighan-Ritchie/C-Programming-Ebook.pdf
\ $ \ endgroup \ $
–尼尔
16 Dec 31'在10:46

\ $ \ begingroup \ $
作为示例,MacOS X和iOS具有将大小四舍五入到下一个16的倍数并返回该大小的块的实现,而没有任何开销,并且在该块附近不存储任何元数据。
\ $ \ endgroup \ $
– gnasher729
17年1月24日,11:25

#3 楼

您在调用malloc之前检查memcpy是否成功,但在调用memset之前没有检查。

您要将len字节从ptr复制到新的内存位置,但不知道ptr实际指向多少字节。它可能指向的更少,这是一个为什么要首先调用realloc的主要原因。

#4 楼

以下是一些简单的注释来突出显示一个主要问题:

real = malloc(len);    // This may or may not succeed.  On failure, real is NULL
memset(real, 0, len);  // This uses real on the assumption that it succeeded!
if (real)              // Here, you acknowledge that it may have failed, even though you already used it!
    memcpy(real, ptr, len);


#5 楼

有一个主要的错误:当realloc失败并返回空指针时,原始指针必须保持不变。而且当然不能被释放。因此,当重新分配失败时,调用方至少具有原始数据。没有那个保证,重新分配基本上是没有用的。