我真的需要 malloc 吗?

Do I really need malloc?

我了解到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 吗? 我声明这个数组的方式有什么问题吗?

TL;DR

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

Do I really need to use malloc here?

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

长版

Is there anything wrong with the way I am declaring this array?

视情况而定。 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有一些优点。您可以阅读有关它们的信息 但是在我看来,尽管它们具有这些优势,但并不值得。每当您发现 VLA:s 有用时,我会说您至少应该考虑切换到另一种语言。

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

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

简短摘要

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

    ,请使用固定数组
  • VLA:s 是 alloca 而不是 malloc 的语法糖(并非 100% 正确,尤其是在处理多维数组时)。所以不要用它们代替 malloc。除了 sizeof 如何在 VLA 上工作之外,它们绝对没有任何好处,除了更简单的声明。

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

  • 您无法检查 VLA 分配是否失败,因此最好提前检查大小是否太大。但是随后我们进行错误检查,就像检查 malloc returned NULL.

    时所做的一样
  • VLA 不能是全局的也不能是静态的。单独的静态部分可能不会引起任何问题,但如果你想要一个全局数组,那么你不得不使用 malloc 或固定大小的数组。

This function works fine.

不,它没有。它具有未定义的行为。正如 Jonathan Leffler 在评论中指出的那样,数组 fileName 太短了。包含 [=45=] 终止符至少需要 12 个字节。您可以通过更改为:

来使其更安全
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;
}

如果一个函数只有returns 0,那么return任何东西都没有意义。而是将其声明为无效。现在我使用 return 值使调用者可以检测错误和错误类型。

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