【问题标题】:Does realloc mutate its argumentsrealloc 是否改变了它的论点
【发布时间】:2019-12-21 05:42:32
【问题描述】:
  1. realloc 是否改变了它的第一个参数?
  2. 是否改变第一个参数取决于实现?
  3. 有什么理由不应该是const?作为一个反例,memcpy 将其 src 参数设为 const

ISO C 标准,第 7.20.3 节内存管理功能,未指定。 realloc 的 Linux 手册页未指定。

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

int main() {
  int* list = NULL;
  void* mem;
  mem = realloc(list, 64);
  printf("Address of `list`: %p\n", list);
  list = mem;
  printf("Address of `list`: %p\n", list);
  mem = realloc(list, 0);
  printf("Address of `list`: %p\n", list);
  // free(list);  // Double free
  list = mem;
  printf("Address of `list`: %p\n", list);
}

当我在我的 Debian 笔记本电脑上运行上述代码时:

  • 第一个printfnull
  • 第二个printf 有一个地址。
  • 第三个printf 与第二个地址相同。
  • 根据规范,尝试释放地址会导致双重free 错误。
  • 第四个printfnull

【问题讨论】:

  • C 是传值方式,所以函数调用本身永远不会改变变量列表的值。
  • @ThomasJager 我认为他的意思是它指向的内存,因为他将源参数与memcpy() 进行比较。
  • OT:转换说明符 p 仅针对 void-指针定义,因此您需要将所有指针强制转换为 (void*) t 打印它们的值,否则调用 UB。
  • 考虑传递给realloc的参数与传递给free类似。只有在realloc failure 情况下(无论如何您都应该检查)它的值 prior 调用仍然有意义。
  • 我想我想问的问题是“如果realloc 的第一个参数是void** ptr_to_ptr 会更好吗?”然后它将能够根据情况更改地址。我不确定是否应该将上述问题作为一个单独的问题提出。

标签: c pointers memory-management dynamic-memory-allocation realloc


【解决方案1】:

首先,正式地,realloc 在分配新对象并复制内容后释放其第一个参数指向的内存。因此,从语义上讲,指向的类型不是const 限定的,这是绝对正确的。在有限的情况下,新对象的地址可能与旧对象的地址相同,但正确的程序在很大程度上甚至看不到这一点(与旧指针相比是未定义的行为),更不用说依赖它了。

其次,我认为您混淆了参数类型的const-ness 和指向类型。 const 在参数类型上没有任何意义(并且被语言忽略,除非在被调用函数的实现中,它使局部变量接收参数常量)因为参数始终是,不引用调用者中的某些对象。当然realloc 不能改变你传递给它的调用者指针变量的。然而,由于任何无效指针的使用都是未定义的行为,您的程序可以(因为 UB 允许任何事情)表现出行为就好像调用者的副本已被修改。例如,比较它与新指针是否相等可能会产生不一致的结果。 memcpysrc 上的 const 是指向 const 类型的指针,而不是 const 类型。

【讨论】:

  • 我不明白const 指向的空间在语义上应该被认为是正确的,因为你不能用指向const 的指针做的事情之一就是释放它。跨度>
  • @JohnBollinger:对不起,我的意思是没有 const的状态。我会澄清的。
【解决方案2】:

realloc() 可以释放其参数指向的内存,如果它不能重用相同的内存。我认为这被认为是一种突变(因为它有效地完全破坏了它)。

语义上,realloc() 等价于:

void *realloc(void *ptr, size_t size) {
  void *result = malloc(size);
  if (result && ptr) {
    memcpy(result, ptr, min(size, _allocation_size(ptr)));
    free(ptr);
  }
  return result;
}

其中_allocation_size() 是 C 运行时的某个内部函数,用于确定动态内存分配的大小。

由于free() 的参数未声明为const void *,因此realloc() 的第一个参数也未声明。

【讨论】:

    【解决方案3】:

    该函数不会更改原始指针,因为它处理的是指针的副本。那就是指针不是通过引用传递的。

    考虑以下程序

    #include <stdio.h>
    #include <stdlib.h>
    
    int main(void) 
    {
        int *p = malloc( sizeof( int ) );
        *p = 10;
    
        printf( "Before p = %p\n", ( void * )p );
    
        char *q = realloc( p, 2 * sizeof( int ) );
    
        printf( "After  p = %p\n", ( void * )p );
    
        free( q );
    
        return 0;
    }
    

    它的输出是

    Before p = 0x5644bcfde260
    After  p = 0x5644bcfde260
    

    如你所见,指针 p 没有改变。

    但是,新指针q 可以具有与调用 realloc 之前指针 p 相同的值。

    来自 C 标准(7.22.3.5 realloc 函数)

    4 realloc 函数返回一个指向新对象的指针( 可能与指向旧对象的指针具有相同的值),或者为 null 无法分配新对象时的指针。

    当然,如果你会写

        p = realloc( p, 2 * sizeof( int ) );
    

    而不是

        char *q = realloc( p, 2 * sizeof( int ) );
    

    那么很明显,通常指针p 的新值可能与p 的旧值不同(尽管根据引用可以相同)。例如,如果函数无法重新分配内存。在这种情况下,如果指针p 的初始值不等于NULL,则会发生内存泄漏。因为在这种情况下(当指针的初始值不等于NULL时)早期分配的内存地址会丢失。

    如果新的内存范围不能被释放,旧的内存不会被释放 分配,因为函数需要将旧内容复制到 新的内存范围。

    来自 C 标准(7.22.3.5 realloc 函数)

    如果无法为新对象分配内存,则旧对象是 未解除分配,其值不变。

    注意这个调用

    mem = realloc(list, 0);
    

    不必返回 NULL。

    来自 C 标准(7.22.3 内存管理函数)

    如果请求的空间大小为零,则行为是 实现定义:要么返回一个空指针,要么 行为就好像大小是某个非零值,除了 返回的指针不得用于访问对象。

    【讨论】:

    • 请注意,oldptr = realloc(oldptr, newsize); 惯用语是一个初期的内存泄漏——如果realloc() 失败并且oldptr 包含对内存的唯一引用,则它刚刚被一个空指针删除。正如你所说,newptr = realloc(oldptr, newsize); 成语是正确的(没有真正解释为什么它是正确的)。
    【解决方案4】:

    我不完全确定“realloc 是否改变了它的第一个参数?”是什么意思。

    它当然不会改变调用者中指针的值——没有 C 函数可以做到这一点。

    但是它会改变指向内存的值吗?这是一个比较棘手的问题。

    就程序员而言,你给realloc一个指向M字节的指针,它返回给你一个(可能不同的)指向N字节的指针。

    如果它返回给你相同的指针(意味着它能够“就地”进行重新分配),并且如果 N ≥ M,它肯定不会触及前 M 个字节。

    如果它返回给你相同的指针但 N

    最后,如果realloc 给你一个不同的指针,前 M 个字节就“消失”了——同样,你不再被允许访问它们,所以很难说它们是否被修改了,但它们可能是,因为它们现在都可供将来分配。

    但无论如何:你交给realloc的指针是指向堆的指针,而realloc肯定会在堆工作时改变它,所以是的,我认为可以肯定地说realloc改变它的第一个参数,因此不应声明为const。 (即使在我讨论的第一种情况下,realloc“肯定没有触及前 M 个字节”,它可能仍然调整了一些附近的数据结构,以记录新的分配。)

    最后,如果“变异”是指 C++ 程序在声明成员变量时允许做的事情mutable——也就是说,在幕后发生了对某些数据结构的更改一个在其他方面被限定为 const 的指针——嗯,是的,这与 realloc 所做的相差不远。 如果 realloc's 第一个参数是constif realloc 所做的修改是对限定为mutable 的数据结构,那么我想这会起作用-- 还有如果我们在谈论 C++。

    当然,我们不是在谈论 C++。我们说的是 C,它甚至没有 mutable 限定符。

    (我想说memcpy 不是一个反例,因为它不会做任何闻起来像写入与其第二个参数相关的任何数据结构的事情。)

    【讨论】:

      【解决方案5】:

      realloc 会改变它的第一个参数吗?

      如果您的意思是更改作为参数传递的变量的值,答案是否定的。这一点与特定的realloc() 函数无关,而更普遍地与语言处理参数的方式有关。 C 语言在将参数传递给函数之前通常在堆栈上生成每个参数的私有副本,因此对它们的每次更改都被限制在本地,并且在函数返回和堆栈重用时丢失。形式上,C 语言通过值传递几乎所有类型(数组是一个众所周知的例外)。无论如何,我会回到下面的论点。

      是否改变第一个参数取决于实现?

      当然不是。如上所述,这取决于语言。

      它不应该是 const 有什么原因吗?作为一个反例 memcpy 使其 src 参数为 const。

      当然是有原因的。

      忘记与void* realloc (void* ptr, size_t size)没有联系的void * memcpy ( void * destination, const void * source, size_t num ),让我们考虑动态内存的管理取决于特定的本地实现,但基本上所有的分配例程都是基于内存池,通常分成小块,从哪里派生返回给我们程序的内存块。我们可以想象,当我们需要缩小块时,系统将删除一些块,返回一个较小的块,这些块偶然保留在同一地址,但如果我们需要扩展,可能我们块后面的块已经分配,​​我们无法继续.

      在嵌入式 8 位 micro 上可能会发生实际内存块无法扩展的情况,但另一个内存区域对于范围足够大,在这种情况下,我们可以将以前的块数据复制到新的块并返回。但在这种情况下,我们在内存中有不同的地址。

      但是malloc() 必须是通用的,独立于实现它的机器,从 8 位嵌入式应用程序到具有千兆字节可用内存和虚拟内存支持的 64 位桌面。出于这个原因,标准必须提供一个适合所有情况的定义。

      第二点是如何传递重新分配的结果,通过/失败,如果使用了对内存块指针的引用(即传递&amp;ptr),如果返回 NULL 失败,原始指针将丢失!。用户为了保存它,必须先复制指向realloc() 的指针,但这个过程很不自然,容易出错。

      出于这个原因,在标准库中,这个问题从不同的角度来解决:重新分配将正式返回一个新分配的内存块,其中已经复制了以前的内存块数据。程序员只需要在使用前检查结果(见下面的代码示例)。

      函数定义中的标准非常明确,正如其他答案中已经提到的那样,为了完整起见,我在下面报告。来自 ISO/IEC 9899:2017 §7.22.3.5 realloc 函数

      realloc 函数释放 ptr 指向的旧对象并 返回一个指向新对象的指针,该对象的大小由 size 指定。

      新对象的内容应与旧对象的内容相同 释放前的对象,直到新旧对象中的较小者 尺寸。

      新对象中超出旧对象大小的任何字节都有 不确定的值。

      如果 ptr 是空指针,realloc 函数的行为类似于 malloc 指定大小的函数。否则,如果 ptr 不匹配 内存管理函数早先返回的指针,或者如果 已通过调用 free 或 realloc 函数释放空间, 行为未定义。

      如果 size 不为零且未分配新对象的内存,则 旧对象没有被释放。

      如果 size 为零并且没有为新对象分配内存,则它是 实现定义是否释放旧对象。如果 旧对象未释放,其值应保持不变。

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

      因为你不知道realloc()是返回一个新对象还是以前的对象,甚至NULL在出错的情况下,你应该考虑realloc()作为总是返回一个新对象,因此代码:

        int* list = NULL;
        void* mem;
        mem = realloc(list, 64);
        printf("Address of `list`: %p\n", list);
      

      至少有两个原因是错误的:

      1. 显然因为如果realloc() 返回一个新对象并释放 旧内存,变量list 包含无效指针。此外,它可能无法返回 NULL,在这种情况下,前一个块仍然有效。
      2. 因为您不能期望以任何方式更改list,将其作为函数中的本地参数传递。当然list 将保留其以前的值NULL

      虽然将空指针传递给realloc() 是符合标准的,因为它明确表示在这种情况下行为将与malloc() 相同,传递零大小 行为是implementation-defined 暗示前一个块将被一些编译器释放,但不会被其他一些编译器释放。后者意味着行为可以在编译器的基础上改变,在你的机器上我们可以推断出编译器的行为显然是释放块,因为你得到了双重释放错误和realloc()返回的空指针。另请注意,在后一种情况下当将零大小传递给realloc()时,返回的NULL 并不意味着发生了故障,并且该函数是成功的,但如果失败你将无法正确理解是否有失败。这是功能的模糊性(或者据我所知,欢迎使用 cmets)。

      使用realloc() 时要遵循的黄金法则基本上如下:

      1. 请记住,从函数 返回的对象始终是 新对象,您必须保存它。
      2. 因为realloc() 可能会失败并返回NULL 指针,所以永远不要使用 代码如下,因为如果失败,我们将覆盖旧的 对象指针失去恢复数据或释放的可能性 以前的对象。始终使用临时变量来检查返回 价值。

      示例代码:

      void *p = malloc(SIZE);
      /* Wrong approach we overwrite anyway teh pointer */
      p = realloc(p, 2*SIZE);
      
      /** Correct approach */
      void *pTmp = realloc(p, 2*SIZE);
      if (NULL == pTmp)
      {
          //Error manage code
      }
      else
      {
          p = pTmp;    //assign value
      }
      

      现在您可能会问,为什么在许多机器上,如台式机、智能手机等具有虚拟内存管理功能,经常碰巧从realloc() 返回的内存地址不变。关键是,由于虚拟内存管理,可以将更多的物理不连续内存块添加到虚拟内存链中,然后可以操纵虚拟内存描述符,将相应的虚拟地址映射到每个物理块,用户可以看到平坦的连续虚拟内存空间。

      【讨论】:

        猜你喜欢
        • 2015-06-20
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2018-09-25
        • 1970-01-01
        • 2011-06-11
        • 2018-09-06
        相关资源
        最近更新 更多