请解释C语言中的*char malloc/realloc/free行为

3
在使用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 指针会影响使用它作为名字的节点,但是为什么之前的 realloc 不会影响其他节点呢?如果 free(theName) 破坏了最后一个节点的名称,我会猜测 realloc 也会做同样的事情,所有列表中的节点都将具有空白名称。
感谢您所有的评论和答案。我修改了代码以删除 malloc 的结果强制转换,添加了 node->name 的释放,并将“malloc -> 多次 realloc -> free”更改为名称的“多次 malloc -> 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.

然而,当我在nprint(list);之前加上free(theName);

  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

当我在nprint(list);之后并在nfree(list);之前加上free(theName);时:
  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,那么当程序在nfree(list);之后调用free(theName);时,为什么不会引发双重释放错误(就像在工作代码中一样)?


这个 realloc(NULL, sizeof (char)*charNum); 等同于 malloc(sizeof (char)*charNum); - alk
这个 char* theName = (char*) malloc(0); 可能会泄漏内存,这取决于 libc 的实现。 - alk
1
在 C 语言中,不需要对 malloc/calloc/realloc 的结果进行强制类型转换,也不建议这样做。 - alk
2个回答

4
当你释放theName时,指针仍然指向列表中最近添加的名称部分。它不指向列表中较早的项目,因为指针由结构元素正确管理,而theName已移动以指向不同的值(最近添加的内容)。这就是为令名称被free()的原因。
你还未能正确释放每个结构元素内部的变量(即名称),导致内存泄漏。我个人建议获取valgrind(linux)或this(windows),并通过它运行程序。

你说得对,我忽略了那个 - 的确 nfree 不会释放名称!但是,你确定有内存泄漏吗?从概念上讲,难道不是只有一个内存块被分配,然后重新分配多次,最后被释放吗? - Codor
@Codor 记住,当你析构时,必须反向进行。首先释放最内层的内存指针,然后释放下一个更内部的级别,以此类推,否则你将失去跟踪内部指针的变量的引用,从而再次发生泄漏。 - ciphermagi
@Codor,你实际上是在像malloc()那样使用realloc()。如果它真的像你想象中那样重新分配内存,那么你的程序将以许多其他有趣的方式崩溃。 - ciphermagi
@ciphermagi 这是否意味着,当在每次迭代中重新分配theName时,每个节点的name指向已被释放的内存位置?如果是这样,那么由于程序运行期间的过程可能会覆盖这些内存,存在节点名称显示不同值的风险吗? - mac13k

3
据我理解,如果您在打印列表之前调用free(theName),则指向最后一个列表节点的内存将被释放。此外,我对使用realloc来分配新内存有些怀疑;在打印列表时,您可能会读取包含预期数据但已被realloc释放的内存。
请注意,realloc允许移动内存块的起始地址,这意味着即使写入realloc返回的地址,旧内容仍然可能存在。

realloc() 正确使用,不会有任何内存丢失。 - ciphermagi
嗯,我已经好几年没有用C了。我的理解是,没有内存泄漏,但在列表的输出中,名称很可能是从已经释放的内存中读取的? - Codor
我同意关于使用realloc()的观点,但我仍然相信没有内存泄漏。请帮助我理解或让我表达得更清楚一些。据我理解,列表的内存管理没有问题,节点以一致的方式分配和释放;name成员没有分配,也没有释放。然而,name成员的分配和释放也是一致的,意味着没有内存泄漏,但输出中的读取访问了已经被释放的内存? - Codor
我认为让你感到困惑的是realloc()的语法。第一个元素是NULL,这使它的行为类似于malloc(),因为没有传入指针。 - ciphermagi
哦,我明白了!确实有点微妙!感谢您的耐心解释,现在我完全同意您的解释。 - Codor
显示剩余2条评论

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