【问题标题】:Do I really need malloc?我真的需要malloc吗?
【发布时间】:2020-01-29 11:10:43
【问题描述】:

我了解 malloc 用于动态分配内存。在我的代码中,我有时会调用以下函数:

int memory_get_log(unsigned char day, unsigned char date, unsigned char month){

    char fileName[11];
    unsigned long readItems, itemsToRead;
    F_FILE *file;

    sprintf(fileName, "%s_%u%u%u%s", "LOG", day, date, month, ".bin");

    file = f_open(fileName , "r");

    itemsToRead = f_filelength( fileName );

    //unsigned char *fileData = (unsigned char *) malloc(itemsToRead);
    unsigned char fileData[itemsToRead]; //here I am not using malloc

    readItems = f_read(fileData, 1, itemsToRead, file);

    transmit_data(fileData, itemsToRead);

    f_close(file);

    return 0;
}

如您所见,我每次从文件中读取的项目数可能不同。线 unsigned char fileData[itemsToRead]; 用于读取这些可变大小的文件。我可以看到我正在以某种方式动态分配内存。此功能工作正常。我真的需要在这里使用 malloc 吗? 我声明这个数组的方式有什么问题吗?

【问题讨论】:

  • 对于较大的itemsToRead 值,您的代码可能会产生问题。带有变量的静态声明,更适合用于小型数组。只要您的价值很低,您的代码就可以了。但是,malloc/calloc 总是最安全的
  • 由于您没有在函数外部使用变量itemsToRead,因此您可以不使用 malloc,但是正如已经提到的,您应该只对较小的变量执行此操作。
  • 数组 fileName 太小而无法容纳 LOG_123456.bin — 即使所有数字都是个位数,它也太小了一个(可能你忘记了尾随的空字节),它们可能是三位数的值。
  • 在 Windows 上,默认情况下,堆栈大小为 1 MiB;在大多数类 Unix 系统上,它是 8 MiB。虽然您可以绕过限制,但通常最简单的方法是不需要这样做。 fileDataarray 需要足够小以适合堆栈 - 所以itemsToRead 值也需要足够小。使用名称 items 表示“字节”有点误导,但并非如此。
  • 主要问题是错误处理:当您向程序提供“垃圾”输入(例如非常大的文件或不适合您的场景的无休止的字符流)时,您的程序会如何反应?您可能想要执行一些清理操作,而不仅仅是崩溃。使用malloc 通常更容易做到这一点。

标签: c malloc mingw


【解决方案1】:

TL;DR

如果您不知道自己在做什么,请在所有情况下使用malloc 或固定大小的数组。 VLA:s 根本不需要。请注意,VLA:s 不能是静态的,也不能是全局的。

我真的需要在这里使用malloc吗?

是的。你正在读一个文件。它们通常比适合 VLA 的要大得多。它们应该只用于小型阵列。如果有的话。

加长版

我声明这个数组的方式有什么问题吗?

这取决于。 VLA:s 作为强制组件从 C11 中删除,所以严格来说,您正在使用编译器扩展,从而降低了可移植性。将来,VLA:s 可能(机会可能极低)从您的编译器中删除。也许您还想在不支持 VLA:s 的编译器上重新编译代码。关于此的风险分析取决于您。但我可能会提到alloca 也是如此。虽然普遍可用,但标准没有要求。

另一个问题是分配是否失败。如果您使用 malloc,您有机会从中恢复,但如果您只打算这样做:

unsigned char *fileData = malloc(itemsToRead);
if(!fileData)
    exit(EXIT_FAILURE);

也就是说,只是在失败时退出而不试图恢复,那么这并不重要。至少从纯粹的恢复角度来看不是。

而且,尽管 C 标准没有强制要求 VLA:s 最终位于堆栈或堆上,但据我所知,将它们放在堆栈上是很常见的。这意味着由于可用内存不足而导致分配失败的风险要高得多。在 Linux 上,堆栈通常为 8MB,在 Windows 上为 1MB。在几乎所有情况下,可用堆都高得多。声明 char arr[n]char *arr = alloca(n) 基本相同,只是 sizeof 运算符的工作方式不同。

虽然我可以理解您有时可能想在 VLA 上使用 sizeof 运算符,但我发现很难找到它的真正需求。毕竟,大小永远不会改变,并且在您进行分配时大小是已知的。所以而不是:

int arr[n];
...
for(int i=0; i<sizeof(arr), ...

只要做:

const size_t size = n;
int arr[size];
...
for(int i=0; i<size; ...

VLA:s 不能替代 malloc。它们是alloca 的替代品。如果您不想将 malloc 更改为 alloca,那么您也不应该更改为 VLA。

此外,在 VLA 似乎是个好主意的许多情况下,检查大小是否低于某个限制也是一个好主意,如下所示:

int foo(size_t n)
{
    if(n > LIMIT) { /* Handle error */ }
    int arr[n];
    /* Code */
}

这可行,但将其与此进行比较:

int foo(size_t n)
{
    int *arr = malloc(n*sizeof(*arr));
    if(!arr) { /* Handle error */ }
    /* Code */
    free(arr);
}

你并没有真正让事情变得那么容易。它仍然是一个错误检查,所以你真正摆脱的唯一一件事就是free 调用。我还可以补充一点,由于大小太大,VLA 分配失败的风险要高得多。因此,如果您知道大小很小,则无需进行检查,但话又说回来,如果您知道它很小,只需使用适合您需要的常规数组即可。

但是,我不会否认 VLA:s 有一些优点。您可以阅读有关它们的信息here. 但是 IMO,虽然它们具有这些优势,但它们并不值得。每当您发现 VLA:s 有用时,我会说您至少应该考虑切换到另一种语言。

此外,VLA:s(以及alloca)的一个优点是它们通常比malloc 更快。因此,如果您遇到性能问题,您可能希望切换到 alloca 而不是 mallocmalloc 调用涉及向操作系统(或类似的东西)请求一块内存。操作系统然后搜索它并在找到它时返回一个指针。另一方面,alloca 调用通常只是通过在一条 cpu 指令中更改堆栈指针来实现。

有很多事情需要考虑,但我会避免使用 VLA:s。如果你问我,它们最大的风险是,由于它们很容易使用,人们对它们变得粗心。对于我认为合适的少数情况,我会改用alloca,因为这样我就不会隐藏危险。

简短总结

  • C11 及更高版本不需要 VLA:s,因此严格来说,您依赖于编译器扩展。但是,alloca 也是如此。因此,如果这是一个非常大的问题,如果您不想使用malloc,请使用固定数组。

  • VLA:s 是 alloca 而不是 malloc 的语法糖(不是 100% 正确,尤其是在处理多维数组时)。所以不要用它们代替malloc。除了 sizeof 在 VLA 上的工作方式之外,它们完全没有任何好处,只是声明更简单一些。

  • VLA:s(通常)存储在堆栈中,而 malloc 完成的分配(通常)存储在堆中,因此大分配失败的风险要高得多。

  • 您无法检查 VLA 分配是否失败,因此最好提前检查大小是否太大。但是我们会进行错误检查,就像检查 malloc 是否返回 NULL 一样。

  • VLA 不能是全局的,也不能是静态的。单独的静态部分可能不会造成任何问题,但如果您想要一个全局数组,那么您将不得不使用malloc 或固定大小的数组。

这个功能很好用。

不,它没有。它具有未定义的行为。正如 Jonathan Leffler 在 cmets 中指出的那样,数组 fileName 太短了。它至少需要 12 个字节才能包含\0-终止符。您可以更改为:

snprintf(fileName, 
         sizeof(fileName), 
         "%s_%u%u%u%s", 
         "LOG", day, date, month, ".bin");

在这种情况下,数组太小的问题会通过创建扩展名为 .bi 而不是 .bin 的文件来表现出来,这比当前情况下的未定义行为更好。

您的代码中也没有错误检查。我会像这样重写它。对于那些认为 goto 不好的人来说,通常是这样,但是错误处理在经验丰富的 C 编码人员中既实用又普遍接受。另一个常见的用途是打破嵌套循环,但这不适用于这里。

int memory_get_log(unsigned char day, unsigned char date, unsigned char month){

    char fileName[12];
    unsigned long readItems, itemsToRead;
    int ret = 0;

    F_FILE *file;

    snprintf(fileName, 
             sizeof(fileName), 
             "%s_%u%u%u%s", "LOG", 
             day, date, month, ".bin");

    file = f_open(fileName , "r");
    if(!file) { 
        ret = 1; 
        goto END;
    }

    itemsToRead = f_filelength( fileName );

    unsigned char *fileData = malloc(itemsToRead);
    if(!fileData) { 
        ret=2;
        goto CLOSE_FILE;
    }
 
    readItems = f_read(fileData, 1, itemsToRead, file);
    // Maybe not necessary. I don't know. It's up to you.
    if(readItems != itemsToRead) {  
        ret=3;
        goto FREE;
    }

    // Assuming transmit_data have some kind of error check
    if(!transmit_data(fileData, itemsToRead)) {  
        ret=4;
    }

FREE:
    free(fileData);
CLOSE_FILE:
    f_close(file);
END:
    return ret;
}

如果一个函数只返回 0,那么返回任何东西都是没有意义的。改为将其声明为无效。现在我使用返回值让调用者能够检测错误和错误类型。

【讨论】:

  • 感谢您的翔实回复!我最初确实有错误检查,但我删除了它们以缩短 stackoverflow 的代码并帮助读者更快地了解代码中的要点。确实 fileName 应该是 12 个字节,但我似乎错过了它。
  • 使 VLA 成为可选项已经是 C11。但是不,具有此功能的编译器不会删除它。使其成为可选只是一种(失败的)策略来说服更多的编译器变得一致。
  • @Sarahcartenz 我怀疑您可能会删除错误检查以创建 MRE,但我认为这可能对未来的读者有所帮助。很高兴你喜欢这个答案。
  • @JensGustedt 是的。我稍微改变了答案。
【解决方案2】:

首先,'unsigned char fileData[itemsToRead]' 行要求堆栈内存,如果文件很大,这将是一个可怕的错误。您应该使用“malloc”来询问堆上的内存。 其次,如果文件大小真的足够大,你应该考虑使用虚拟内存或动态加载,例如'fseek'方法。

【讨论】:

    猜你喜欢
    • 2012-03-13
    • 2011-01-28
    • 1970-01-01
    • 2016-07-26
    • 1970-01-01
    • 1970-01-01
    • 2013-07-04
    • 2012-03-26
    • 1970-01-01
    相关资源
    最近更新 更多