递归函数中的malloc和free

4

我有一些代码需要你翻译,希望有人能告诉我哪里出了问题。目前,我正在将我的编程难题移植到其他编程语言中,以便更好的实践。

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

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
const char *dummy = "1234567890";
const char* inlet = "_";

void dosomething(int c, char* s){
  printf("%s\n", 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()调用。这就是我不理解的地方。在我的看来,在递归调用返回后,释放内存绝对没有问题,因为它已经不再使用,但程序的输出结果却不同。
没有free的输出结果如预期:
...
1_34567890
1_34567890
...

使用第二个免费选项时,只会产生一个结果,然后程序停止并显示:

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

更新: 我根据评论和答案更改了代码,但问题仍然存在。如果dosomething()方法内的free()调用处于注释状态,使用malloc分配更多内存也不能防止内存错误。递归的第一次迭代生成正确的输出,第二次显示不同的结果,第三次也是如此,然后程序崩溃(请参见函数顶部的新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 %s\n", 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);
  }
}

5
在C语言中,不要将malloc等函数的结果转换为其他类型。 - too honest for this site
为什么在 ns = strcat( ns, &s[2] ) 中需要赋值?只有 strcat 就足够了 - 可以查看函数说明。此外,&s[2]s + 2 是相同的(我发现这种语法更短)。 - i486
你在dosomething函数内忘记了使用free释放内存;建议使用valgrind进行检查,并使用所有警告和调试信息进行编译(gcc -Wall -Wextra -g)。 - Basile Starynkevitch
malloc(sizeof(char)*11); 更改为 malloc(11);,因为 sizeof(char)==1,这意味着您的代码看起来像这样:malloc(1 * 11) - Michi
更改问题中显示的代码不是好的风格;它会使这里已经给出的答案毫无意义。 - DevSolar
显示剩余5条评论
6个回答

9

原因在于malloc函数分配了10个字符,而需要11个(以\0结尾)。

虽然这取决于不同的实现方式,但malloc函数为了效率可能会使用分配区域内部和外部的一些字节来设置一些内部信息。当这个内部区域被改变(多一个字符),free可能会使用这些字节,最终结果是未定义行为。

无论如何,超出边界读取或修改数组都是未定义行为。

建议使用:

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

不要将malloc的结果强制转换为指针。

1
为了提高效率,malloc函数使用一些字节……这不是标准要求的实现细节。访问数组越界是未定义行为(而不是意外行为)。这就是所有需要说的。 - too honest for this site
1
可以,但你应该清楚区分标准要求和实现细节。此外,你暗示这总是正确的。事实上,并非如此。还有其他有效的实现方式,不使用带内数据。顺便说一下:那些数据可能在返回地址之前。原因是:这样free就知道在哪里找到它,而且正向越界比负向越界更容易出现损坏。 - too honest for this site
1
我同意@BeowulfOF的观点。我们确实需要分配11个字节,但即使注释掉free,我仍然会遇到崩溃问题。 - Jabberwocky
2
这里至少还有一个问题:在第一次迭代中,c 包含 1。因此 strncpy(ns, s, c-1) 拷贝了 0 字节并且未改变 ns 缓冲区。因此,ns 缓冲区包含垃圾。然后 strncat(ns, inlet, 1); 导致未定义的行为,因为 ns 指向未初始化的内存。您还可以考虑到如果要复制的长度小于源字符串的长度,则 strncpy 可能会使目标字符串没有终止零。 - Jabberwocky
BeowulfOF,记住strcat是将两个字符串连接到目标字符串中,这两个字符串在哪里?这就是为什么你需要使用calloc的原因。 - Michi
显示剩余5条评论

5

malloc()通常不会初始化分配的内存。 你应该使用memset()来初始化分配的内存或者 使用calloc(),它可以将分配的内存清零。

你应该为11个字符(10个+1个'\0')分配内存,并且不要强制转换malloc()返回的指针。

在某些系统上,你可以配置malloc来初始化内存, 但这只是一个非常不好的调试辅助工具,你永远不应该依赖它。


1
可能这个答案可以解决90%的问题,这里需要使用calloc而不是malloc。 - Michi
不,即使你键入“malloc(1000);”也无法解决这个问题。这段代码的编写方式只有“calloc”才能解决它。阅读更多关于“strcat”的内容。 - Michi
单独使用malloc(1000)无法解决问题。在使用malloc时避免类型转换是一个可选的做法,但这总是一个好主意。 - CodeWarrior101

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

问题就在这里。
每当你从 s 复制 c-1 个字节到 ns 时,你都没有复制终止符 \0,因此在随后的 strncat 调用中,由于缺少 \0,无法确定 ns 的结尾位置,因此来自 inlet 的 _ 可能会被复制到超出 ns 分配的 11 个字节的内存范围之外,覆盖了内存。
来自 man strncpy 的警告:
如果 src 的前 n 个字节中没有 null 字节,则放置在 dest 中的字符串将不以 null 结尾。
而这正是每个 strncpy(ns, s, c-1); 调用中发生的事情,导致 strncat 进一步破坏内存,因为 strncat 可能在 ns 的 11 个字节之外找到垃圾 \0 字节。
如另一个答案中所指出的,使用 calloc 而不是 malloc 将使用 \0 字节填充 ns 指向的缓冲区,因此在 strncpy(ns, s, c-1); 后,strncat 将始终在 ns 的 c-1 个字符之后找到终止的 \0 字节,其位于 ns 的 11 个字节之内。
另一种方法是在 strncpy(ns, s, c-1); 后,在 strncat(ns, inlet, 1); 之前明确在 ns 的第 c-1 个位置存储 \0 字符,保持使用 malloc。
因此,工作代码片段如下:
char *ns = malloc(sizeof(char)*11);
strncpy(ns, s, c-1);
ns[c-1] = '\0';
strncat(ns, inlet, 1);
...

1
问题在于这里使用了 malloc应该使用 calloc,因为涉及到 strcat - Michi
我没有看到更新,现在真的更好了(+1)! - terence hill
@Michi 不,malloc不是问题。问题在于strncpy导致的非空结尾字符串。使用malloc编写相同的代码没有任何问题。请参见我的更新答案。 - rootkea
@rootkea 在另一个回答中,我也评论了这个问题。问题在于代码的编写方式,这就是为什么需要使用 calloc 的原因。当然,你可以进行更改(http://ideone.com/WkowIi)。 - Michi

3

标准的代码异味,目前还没有任何答案解决:

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

这个函数有两个比较意外的特性,许多(大部分?)程序员并不知道:

  • 如果复制的字符串比count短,dest将填充零字节。(通常不是问题。)

但是:

  • 如果复制的字符串(包括终止的零字节)比count长,dest 将不会以零结尾

查看了您程序的流程后,我发现代码异味得到了确认:

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

strncpy(ns, s, c-1);

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

因此,为此原因,

strncat(ns, inlet, 1);

这将导致未定义的行为。

由于溢出的影响,始终在继续之前断言 dest[count - 1] == '\0'


@BeowulfOF:感谢您接受我的答案。请注意编辑——当我称strncpy()无用时,我没有真正考虑清楚。由于其他副作用,您可以轻松检查它是否成功。 - DevSolar
@DevSolar *"不要像其他地方建议的那样使用calloc()(这将初始化所有内存),我建议在malloc()之后设置ns[0] = '\0'"*。这将从第一个递归调用dosomething()开始引入相同的错误。 - rootkea
@rootkea:我没看到那个。你是怎么想出来的? - DevSolar
1
从第一个递归调用开始,c 将从 2 增长到 9,通过 strncpy(ns, s, c-1); 覆盖了你的 ns[0] = '\0' - rootkea
如果您仍然想避免使用 calloc 并希望坚持使用 malloc,那么您可以在 strncpy(ns, s, c-1); 之后,在 strncat(ns, inlet, 1); 之前加上 ns[c-1] = '\0'; - rootkea
@rootkea:我对于 OP 采取什么措施来解决问题并没有偏好。我只是在这里指出 strncpy() 的缺陷。;-) 不过还是谢谢你提醒我。 - DevSolar

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("%s\n", 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);
    }
}

1

解决使用strcat/strncat的问题

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

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

==4722== Conditional jump or move depends on uninitialised value(s)

(在strncat的那一行)

我进行了一些研究,发现 strcatstrncat 需要正确设置 nul 结尾符才能正常工作(例如,请参见此 post)。在调用 malloc 后,内存未初始化,此外,调用 strncpy 时不会添加终止字符,因为你总是拷贝 (c-1) 个字符,因此不包括空字节(请参见 man 页面的 strncpy,特别是 Notes 部分的示例)。因此,调用 strncat 可能会导致 未定义行为
为解决此问题,在调用 strncat 函数之前,我们必须正确设置终止字符,就像以下代码片段中一样:
void dosomething(int c, char* s){
  printf("%d %s\n", 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]='\0';        
    // ---- ---- ---- ---- ---- ---- //
    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);

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