【问题标题】:malloc and free in recursive functions递归函数中的 malloc 和 free
【发布时间】:2016-01-08 08:23:35
【问题描述】:

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

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)

更新: 我根据cmets和answers更改了代码,但问题依然存在。如果对 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);
  }
}

【问题讨论】:

  • 变量dummy指向包含11个字符的数组的第一个元素。
  • 不要将malloc和朋友的结果投射到C中!
  • 为什么您需要在ns = strcat( ns, &amp;s[2] ) 中分配?只有strcat 就足够了 - 请参阅功能描述。 &amp;s[2] 也与 s + 2 相同(我发现它的语法更短)。
  • 你忘记在free里面dosomething;考虑使用valgrind 并使用所有警告和调试信息进行编译 (gcc -Wall -Wextra -g)
  • @i486: &amp;a[i] 如果你真的有一个数组会更清楚。相当多的编码标准(例如 MISRA)禁止显式指针算术语法。

标签: c pointers recursion


【解决方案1】:

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

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

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

更好的使用

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

并且不要转换 malloc 的结果指针。

【讨论】:

  • “malloc 函数,为了高效,使用一些字节......”不是必须的。这是一个可能的实现细节,标准不需要。越界访问数组是未定义(不是意外行为。这就是所有要说的。
  • @Olaf 同意,但是添加这部分的原因是为了暂时解释为什么在调用 free 时会发生奇怪的行为 only .
  • 没关系,但您应该清楚地区分标准要求和实施细节。你也暗示这总是正确的。事实上,事实并非如此。还有其他不使用带内数据的有效实现。仅供参考:该数据可能在返回地址之前。原因是:这样free 知道在哪里可以找到它并且它不太可能被破坏,因为积极的越界比消极的可能性更大。
  • 我同意@BeowulfOF。我们需要分配 11 个字节是正确的,但我仍然会崩溃即使将 free 注释掉
  • 这里至少还有一个问题:在第一次迭代中,c 包含 1。因此,strncpy(ns, s, c-1) 复制了 0 个字节并保持 ns 缓冲区不变。因此,ns 缓冲区包含垃圾。然后strncat(ns, inlet, 1); 产生未定义的行为,因为ns 指向未初始化的内存。您还可以考虑,如果要复制的长度小于源字符串的长度,strncpy 可能会离开目标字符串而不终止零。
【解决方案2】:

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

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

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

【讨论】:

  • 可能这个答案确实修复了 90%,这里需要 calloc 而不是 malloc
  • 不,即使您输入malloc(1000); 也不能解决问题。这段代码的编写方式只有calloc 修复它。阅读更多关于strcat
  • malloc(1000) 单独无法解决问题。这是可选的,但在 malloc 的情况下避免类型转换总是一个好主意。
【解决方案3】:
strncpy(ns, s, c-1);
strncat(ns, inlet, 1);

问题就在这里。

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

来自man strncpy

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

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


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


另一种方法是在strncpy(ns, s, c-1); 之后和strncat(ns, inlet, 1); 之前将\0 字符显式存储在nsc-1th 位置,并使用malloc

因此工作代码 sn-p 将是:

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

【讨论】:

  • 这里的问题是malloche need calloc instead,因为strcat
  • 我没看到更新,现在真的好多了(+1)!
  • @Michi No. malloc 不是问题。 由于strncpy 导致的非空终止字符串是问题。可以使用malloc 编写相同的代码,没有任何问题。请参阅我的更新答案。
  • @rootkea 在另一个答案中,我也对此发表了评论。编写方式是问题所在,这就是为什么需要callocOf course you can change it
【解决方案4】:

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

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

这个函数有两个出乎意料的特性,很多(大多数?)程序员都没有意识到:

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

但是:

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

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

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

strncpy(ns, s, c-1);

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

因此,

strncat(ns, inlet, 1);

这是未定义的行为。

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

【讨论】:

  • @BeowulfOF:感谢您接受我的回答。请注意编辑——我在调用strncpy() 无用时并没有真正考虑清楚。由于其他副作用,您可以轻松检查它是否成功。
  • @DevSolar "我建议在malloc()".这将从第一次递归调用dosomething()开始引入相同的错误
  • @rootkea:我没看到。你怎么看?
  • 从第一个递归调用开始 c 将从 2 to 9 覆盖你的 ns[0] = '\0'strncpy(ns, s, c-1);
  • @DevSolar 如果您仍然想避免使用calloc 并想坚持使用malloc,那么您可以在strncpy(ns, s, c-1); 之后和strncat(ns, inlet, 1); 之前说ns[c-1] = '\0';
【解决方案5】:

我对您的代码做了一些小改动:

  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);
    }
}

【讨论】:

    【解决方案6】:

    用 strcat/strncat 解决问题

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

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

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

    (在strncat的行)

    我做了一些研究,发现 strcatstrncat 需要 nul 终止字符才能正常工作(参见post,例子)。调用 ma​​lloc 后,内存未初始化, 此外,对 strncpy 的调用不会添加终止字符,因为您总是复制 (c-1) 字符,因此不包括空字节(请参阅 strncpy 的man 页面,尤其是部分注释)。因此,对 strncat 的调用可能会导致未定义的行为
    为了解决这个问题,在调用strncat函数之前,我们必须正确设置终止符,如下面的代码sn-p:

    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);
    

    【讨论】:

      猜你喜欢
      • 2019-11-15
      • 1970-01-01
      • 1970-01-01
      • 2018-05-01
      • 1970-01-01
      • 2011-11-24
      • 2014-05-21
      • 2013-06-16
      • 2021-12-11
      相关资源
      最近更新 更多