C - 递归函数中的 malloc 和自由



我有一些代码给你,希望有人能告诉我,我做错了什么。目前,我正在将我的编程难题移植到其他编程语言中,以便动手。

C 语言中的代码抽象(更新(:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
const char *dummy = "1234567890";
const char* inlet = "_";
void dosomething(int c, char* s){
  printf("%sn", s);
  if (c < 10) {
    char *ns = malloc(sizeof(char)*11);
    strncpy(ns, s, c-1);
    strncat(ns, inlet, 1);
    strcat(ns, &s[c]);
    dosomething(c+1, ns);
    //free(ns);
  }
}
void main() {
  for(int i = 0; i < 100; i++) {
    char *s = malloc(sizeof(char)*11);
    strcpy(s, dummy);
    dosomething(1, s);
    free(s);
  }
}

代码运行良好,直到我在dosomething((方法中取消注释free((调用。这就是我不理解的。在我看来,释放内存绝对没有问题,因为它从递归调用返回后不再使用,但程序输出告诉不同的东西。

没有空闲的输出符合预期:

...
1_34567890
1_34567890
...

使用第二个免费,只产生一个结果,然后程序停止:

*** Error in `./a.out': malloc(): memory corruption (fast): 0x000000000164e0d0 ***
Abgebrochen (Speicherabzug geschrieben)

更新:我根据注释和答案更改了代码,但问题仍然存在。使用 malloc 分配更多内存并不能防止内存错误,如果 dosomething(( 方法中的 free(( 调用被注释。为递归的第一次迭代正确生成输出,第二次显示不同的结果,第三次也是如此,然后程序失败(有关新结果,请参阅函数顶部的新 printf:

输出:

1234567890
_234567890
__34567890
___4567890
____567890
_____67890
______7890
_______890
________90
_________0
1234567890
@@J_234567890
@@J_J_234567890
@@J__J_234567890
@@J___J_234567890
@@J___J_234567890
@@J___J_234567890
@@J____J_234567890
@@J____J_234567890
@@J_____0__234567890
1234567890
@@J_234567890
@@J_J_234567890
@@J__J_234567890
@@J___J_234567890
@@J___J_234567890
@@J___J_234567890
@@J____J_234567890
@@J____J_234567890
@@J_____0__234567890__234567890
*** Error in `./a.out': free(): invalid next size (fast): 0x00000000014a4130 ***
Abgebrochen (Speicherabzug geschrieben)

谁能向我解释一下,我在眨眼什么?

更新2:@Michi和@MichaelWalz已经弄清楚了这一点。它是使用 malloc - 因此在第一次迭代后处理内存中的垃圾(打印内存地址和字符串表明非常整洁(和使用 strcat 之间的组合。

在未初始化的内存上使用 strcat 会将内存中的字符串附加到内存中指针之后找到的下一个"\0"字符。如果未初始化内存,则可能远远超出该字符串的范围。

谢谢你们!

工作代码:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
const char *dummy = "1234567890";
const char* inlet = "_";
void dosomething(int c, char* s){
  printf("%p %sn", s, s);
  if (c < 10) {
    //char *ns = malloc(sizeof(char)*11);
    char *ns = calloc(11, sizeof(char));
    strncpy(ns, s, c);
    strncat(ns, inlet, 1);
    strncat(ns, &s[c+1],10-c);
    dosomething(c+1, ns);
    free(ns);
  }
}
void main() {
  for(int i = 0; i < 100; i++) {
    //char *s = malloc(sizeof(char)*11);
    char *s = calloc(11, sizeof(char));
    strcpy(s, dummy);
    dosomething(0, s);
    free(s);
  }
}

原因是因为 malloc 函数分配 10 个字符,而需要 11 个字符(以 结尾(。

虽然这依赖于实现,但为了高效起见,malloc 函数很可能在分配区域内和分配区域之外使用一些字节来设置一些内部信息。在此内部区域被更改(一个字符太多(后,free可能会使用这些字节,最终结果是未定义的行为。

无论如何,越界更改甚至读取数组都是 UB。

更好地使用

char *s = malloc(strlen(dummy) + 1);

并且不要强制转换 Malloc 的结果指针。

malloc()通常不会初始化分配的内存。您应该使用 memset() 来初始化分配的内存或使用 calloc() 它确实用零初始化分配的内存。

您应该为 11 个字符分配内存(10 + 1 表示"\0"(,不要投射 malloc() 返回的指针。

在某些系统上,您可以配置 malloc 以初始化内存,但这充其量是一个糟糕的调试辅助工具,你永远不应该指望它。

strncpy(ns, s, c-1);
strncat(ns, inlet, 1);

问题就在这里。

每当您将c-1字节从s复制到ns时,您永远不会复制终止因此在随后的strncat调用中,无法识别ns的结束(由于没有(,因此来自 inlet 的_可能会被复制远远超出分配的 11 字节ns覆盖内存。

man strncpy

警告:如果 src 的前 n 个字节中没有空字节,则放置在 dest 中的字符串将不会以空结尾。

这正是每个strncpy(ns, s, c-1);调用中发生的情况,这会导致strncat进一步的内存损坏,因为strncat可能会发现垃圾字节远远超过ns11字节。


正如另一个答案中所指出的那样,使用 calloc 而不是 malloc 将用 字节填充ns指向的缓冲区,因此在strncpy(ns, s, c-1);之后,strncat总是c-1会在 11 字节的 ns 字节内终止 ns


另一种方法是字符明确存储在strncpy(ns, s, c-1);之后和strncat(ns, inlet, 1);坚持malloc之前c-1 ns的第位置。

因此,工作代码片段将是:

char *ns = malloc(sizeof(char)*11);
strncpy(ns, s, c-1);
ns[c-1] = '';
strncat(ns, inlet, 1);
...

标准代码异味,尚未通过任何答案解决:

strncpy( char *dest, const char *src, size_t count )

这个函数有两个相当出乎意料的特性,许多(大多数?(程序员都不知道:

  • 如果复制的字符串短于 count ,则dest用零字节填充。(这通常不是问题。

但:

  • 如果复制的字符串(包括终止的零字节(长于 countdest不会以零结尾

查看您的程序流程后,我看到我的代码气味已确认:

在第一次调用 dosomething() 时,c 是 1,并且 ns 的内容(指向新malloc()的内存(是不确定的:

strncpy(ns, s, c-1);

这将复制零字节,包括终止零字节ns仍然指向完全不确定的内容。最重要的是,不能保证分配的内存中会有零字节。

出于这个原因,

strncat(ns, inlet, 1);

然后,这是未定义的行为。

由于上述对溢出的影响,请始终在继续之前断言该dest[count - 1] == ''

我对你的代码做了一些小的更改:

  1. void main(){}更改为int main(void){}

  2. int c参数更改为size_t c,因为 strncpy 期望 size_t

  3. malloc(sizeof(char)*11);更改为calloc(11,1);

  4. 注释掉了//free(ns);.

我得到了:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
const char *dummy = "1234567890";
const char* inlet = "_";
void dosomething(size_t c, char* s);
int main(void) {
    for(int i = 0; i < 100; i++) {
        char *s = calloc(11,1);
        strcpy(s, dummy);
        dosomething(1, s);
        free(s);
    }
}
void dosomething(size_t c, char* s){
    printf("%sn", s);
    if (c < 10) {
        char *ns = calloc(11,1);
        strncpy(ns, s, c-1);
        strncat(ns, inlet, 1);
        strcat(ns, &s[c]);
        dosomething(c+1, ns);
        free(ns);
    }
}

解决 strcat/strncat 的问题

首先,对 free 的调用与代码中的问题无关。

我使用 Valgrind 来了解正在发生的事情,输出显示存在依赖于未初始化值的条件跳转:

==4722== 条件跳转或移动取决于未初始化的值

(在STRNCAT的线上(

我做了一些研究,发现strcat和strncat需要nul终止字符才能正常工作(例如,请参阅这篇文章(。调用 malloc 后,内存未初始化,此外,对 strncpy 的调用不会添加终止字符,因为您总是复制 (C-1( 字符,因此不包括空字节(请参阅 strncy 的手册页,尤其是注释部分中的示例(。 因此,对srncat的调用可能会涉及未定义的行为
为了解决这个问题,在调用strncat函数之前,我们必须正确设置终止字符,如下面的代码片段所示:

void dosomething(int c, char* s){
  printf("%d %sn", c, s);
  if (c < 10) {
    char *ns = malloc(sizeof(char)*11);
    if(c-1) strncpy(ns, s, c-1);
    // ----  Set the nul character --- //
    ns[c-1]='';        
    // ---- ---- ---- ---- ---- ---- //
    strncat(ns, inlet, 2);
    strcat(ns, &s[c]);
    dosomething(c+1, ns);
    free(ns);
  }
}

我还添加了一个检查,仅当您实际必须复制某些内容时才执行第一个 strncat。更正随后对strcat的调用也会更安全(另请参阅@rootkea的答案(,因为您将太多字符附加到ns字符串,超出了界限(valgrind没有报告这一点(。

strcat(ns, &s[c]); ---> strncat(ns, &s[c], 10-c);

最新更新