【问题标题】:Please explain this *char malloc/realloc/free behavior in C请在 C 中解释这个 *char malloc/realloc/free 行为
【发布时间】:2014-04-27 12:21:42
【问题描述】:

在 C 语言中使用链表时,我注意到了我不理解的这种行为。下面的示例代码说明了声明一个简单列表并填充包含*char 名称的节点的情况。 theName 字符串是通过在命令行中给定的每个参数附加_ 生成的,因此charNum 比argv[i] 大2 以容纳_\0。每个argv 元素都会生成一个节点,该节点将添加到main 函数的for 循环中的列表中。

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

struct node {
  char* name;
  struct node* next;
};

struct node*
nalloc(char* name)
{
  struct node* n = (struct node*) malloc(sizeof(struct node));
  if (n)
  {
    n->name = name;
    n->next = NULL;
  }
  return n;
}

struct node*
nadd(struct node* head, char* name)
{
  struct node* new = nalloc(name);
  if (new == NULL) return head;
  new->next = head;
  return new;
}

void
nprint(struct node* head)
{
  struct node* n = NULL;
  printf("List start: \n");
  for(n = head; n; n=n->next)
  {
    printf("  Node name: %s, next node: %p\n", n->name, n->next);
  }
  printf("List end. \n");
}

void
nfree(struct node* head)
{
  struct node* n = NULL;
  printf("Freeing up the list: \n");
  while (head)
  {
    n = head;
    printf("  Freeing: %s\n", head->name);
    head = head->next;
    free(n);
  }
  printf("Done.\n");
}

int
main(int argc, char** argv)
{
  struct node* list = NULL;
  char* theName = (char*) malloc(0);
  int i, charNum;
  for (i=0; i < argc; i++)
  {
    charNum = strlen(argv[i]) + 2;
    theName = (char*) realloc(NULL, sizeof (char)*charNum);
    snprintf(theName, charNum, "%s_", argv[i]);
    list = nadd(list, theName);
  }
  nprint(list);
  nfree(list);
  free(theName);
  return 0;
}

上面的代码按预期工作:

$  ./a.out one two three
List start: 
  Node name: three_, next node: 0x1dae0d0
  Node name: two_, next node: 0x1dae090
  Node name: one_, next node: 0x1dae050
  Node name: ./a.out_, next node: (nil)
List end. 
Freeing up the list: 
  Freeing: three_
  Freeing: two_
  Freeing: one_
  Freeing: ./a.out_
Done.

但是,当我修改此代码并在打印列表之前调用 free(theName) 时:

  ...
  free(theName);
  nprint(list);
  nfree(list);
  return 0;
  ...

缺少最后一个列表项的名称:

$  ./a.out one two three
List start: 
  Node name: , next node: 0x3f270d0
  Node name: two_, next node: 0x3f27090
  Node name: one_, next node: 0x3f27050
  Node name: ./a.out_, next node: (nil)
List end. 
Freeing up the list: 
  Freeing: 
  Freeing: two_
  Freeing: one_
  Freeing: ./a.out_
Done.

所以释放theName 指针会影响使用它作为名称的列表节点,但为什么更早reallocs 没有影响其他节点?如果free(theName) 破坏了最后一个节点的名称,我猜realloc 会这样做,并且列表中的所有节点都将具有空白名称。


感谢大家的 cmets 和回答。我修改了代码以删除 malloc 结果的强制转换,添加了 node->name 的释放并将名称的 'malloc -> multiple reallocs -> free' 更改为 'multiple mallocs -> free'。所以这是新代码:

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

struct node {
  char* name;
  struct node* next;
};

struct node*
nalloc(char* name)
{
  struct node* n = malloc(sizeof(struct node));
  if (n)
  {
    n->name = name;
    n->next = NULL;
  }
  return n;
}

struct node*
nadd(struct node* head, char* name)
{
  struct node* new = nalloc(name);
  if (new == NULL) return head;
  new->next = head;
  return new;
}

void
nprint(struct node* head)
{
  struct node* n = NULL;
  printf("List start: \n");
  for(n = head; n; n=n->next)
  {
    printf("  Node name: %s, next node: %p\n", n->name, n->next);
  }
  printf("List end. \n");
}

void
nfree(struct node* head)
{
  struct node* n = NULL;
  printf("Freeing up the list: \n");
  while (head)
  {
    n = head;
    printf("  Freeing: %s\n", head->name);
    head = head->next;
    free(n->name);
    free(n);
  }
  printf("Done.\n");
}

int
main(int argc, char** argv)
{
  struct node* list = NULL;
  char* theName;
  int i, charNum;
  for (i=0; i < argc; i++)
  {
    charNum = strlen(argv[i]) + 2;
    theName = malloc(sizeof (char)*charNum);
    snprintf(theName, charNum, "%s_", argv[i]);
    list = nadd(list, theName);
  }
  nprint(list);
  nfree(list);
  free(theName);
  return 0;
}

上述工作按预期工作:

$  ./a.out one two three
List start: 
  Node name: three_, next node: 0x1826c0b0
  Node name: two_, next node: 0x1826c070
  Node name: one_, next node: 0x1826c030
  Node name: ./a.out_, next node: (nil)
List end. 
Freeing up the list: 
  Freeing: three_
  Freeing: two_
  Freeing: one_
  Freeing: ./a.out_
Done.

但是,当我将free(theName); 放在nprint(list); 之前:

  free(theName);
  nprint(list);
  nfree(list);
  return 0;

在输出中缺少最后一个节点的名称,nfree(list); 抛出错误:

$  ./a.out one two three
List start: 
  Node name: , next node: 0x1cf3e0b0
  Node name: two_, next node: 0x1cf3e070
  Node name: one_, next node: 0x1cf3e030
  Node name: ./a.out_, next node: (nil)
List end. 
Freeing up the list: 
  Freeing: 
*** glibc detected *** ./a.out: double free or corruption (fasttop): 0x000000001cf3e0d0 ***
======= Backtrace: =========
...
======= Memory map: ========
...
Aborted

当我将free(theName); 放在nprint(list); 之后和nfree(list); 之前:

  nprint(list);
  free(theName);
  nfree(list);
  return 0;

在输出中所有节点都正确打印,但nprint(list); 仍然抛出错误:

$  ./a.out one two three
List start: 
  Node name: three_, next node: 0x19d160b0
  Node name: two_, next node: 0x19d16070
  Node name: one_, next node: 0x19d16030
  Node name: ./a.out_, next node: (nil)
List end. 
Freeing up the list: 
  Freeing: 
*** glibc detected *** ./a.out: double free or corruption (fasttop): 0x000000001cf3e0d0 ***
======= Backtrace: =========
...
======= Memory map: ========
...
Aborted

这在我的脑海中提出了另一个问题:我猜在任何情况下theName 指向的内存都被释放了两次:第一次作为 node->name,第二次作为 theName,那么为什么 free(theName); 不会引发在nfree(list); 之后在程序末尾调用时出现双释放错误(就像在工作代码中一样)?

【问题讨论】:

  • 这个realloc(NULL, sizeof (char)*charNum); 等价于malloc(sizeof (char)*charNum);
  • 这个char* theName = (char*) malloc(0); 可能会泄漏内存,具体取决于 libc 实现。
  • 在C中没有需要转换malloc/calloc/realloc的结果也不推荐

标签: c pointers malloc free realloc


【解决方案1】:

当您释放 theName 时,指针仍指向列表中最近添加的名称部分。它没有指向列表中较早的项目,因为指针由结构元素正确管理,并且 theName 被移动以指向不同的值(最近添加的)。这就是为什么名称是 free()d。

在释放结构元素本身之前,您还没有正确释放每个结构元素(即名称)内的变量,从而导致内存泄漏。我个人建议获取valgrind (linux) 或this (windows) 并通过它运行您的程序。

【讨论】:

  • 你说得对,我忽略了这一点——确实nfree 不会释放名称!但是,您确定会泄漏吗?从概念上讲,不是只有一块内存被分配,然后重新分配了几次,最后被释放了吗?
  • @Codor 请记住,当您解构时,您必须反向进行。 free() 首先是最内层的内存指针,然后是下一个最内层,等等,否则您将丢失对跟踪内部指针的变量的引用,并再次发生泄漏。
  • @Codor 您实际上是在使用 realloc(),如 malloc()。如果它真的按照您认为的方式重新分配,您的程序会以许多其他有趣的方式中断。
  • @ciphermagi 是不是说,当theName在每次迭代中被重新分配时,每个节点的name指向已经被释放的内存位置?如果是这样,是否存在由于程序运行过程而导致内存被覆盖的风险,并且节点的名称将显示不同的值?
【解决方案2】:

据我了解,如果您在打印列表之前调用free(theName),则会释放最后一个列表节点指向的内存。此外,我对使用realloc 分配新内存有点怀疑;打印列表时,您可能会读取包含预期数据但已被realloc 释放的内存。

注意realloc允许移动内存块的起始地址,这意味着即使写入realloc返回的地址,旧的内容可能仍然存在。

【讨论】:

  • realloc() 正在正确使用。不会有任何记忆丧失。
  • 好吧,我已经好几年没用过C了。我是否正确理解没有内存泄漏,但在列表的输出中,名称很可能是从已释放的内存中读取的?
  • 不,肯定有内存泄漏,但这与使用 realloc() 无关,它只是将分配的内存移动到适合新分配大小的连续块,然后正确释放原始内存,独立维护指针的完整性。
  • 我同意 realloc() 的使用,但是我仍然相信没有内存泄漏。请帮助我理解或让自己更清楚一点。据我了解,列表的内存管理没有问题,节点以一致的方式分配和释放; name 成员没有 alloc,name 成员没有 free。但是name成员的分配和释放在没有内存泄漏的意义上也是一致的,但是输出中的读取访问访问了已经释放的内存?
  • 名称成员保持分配状态。这是一种副作用分配,充其量是间接偶然的。我永远不会这样做,而是更喜欢在 malloc() 内部进行所有分配 nalloc() 帮助器。
猜你喜欢
  • 1970-01-01
  • 2019-09-19
  • 2012-02-22
  • 1970-01-01
  • 2018-10-22
  • 2012-07-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多