【问题标题】:Realloc and one function or malloc and two functions?realloc 和一个函数还是 malloc 和两个函数?
【发布时间】:2014-01-31 15:08:45
【问题描述】:

我有一个文件有num 行:每一行都包含一个数字。我想将每个数字保存到向量*vet 中。这两个版本哪个更好?

[版本 1] 我有两个功能:第一个用于计算 num,第二个用于将数字保存到 *vet。我在main() 中用malloc 分配内存。

#include <stdio.h>
#include <stdlib.h>

/* The first function counts lines number */
int count_line (int *num)
{
    FILE *fin;
    char buff[10];

    *num = 0;

    if ( !(fin = fopen("numbers.dat", "r")) )
        return 1;

    while ( fgets(buff, sizeof(buff), fin) )
        (*num)++;

    return fclose(fin);
}

/* The second function save numbers into a vector */
int save_numbers (int *vet)
{
    FILE *fin;
    int i=0;
    char buff[10];

    if ( !(fin = fopen("numbers.dat", "r")) )
        return 1;

    while ( fgets(buff, sizeof(buff), fin) )
    {
        sscanf (buff, "%d", &vet[i]);
        i++;
    }

    return fclose(fin);
}

int main ()
{
    int num, i, *vet;

    if ( count_line(&num) )
    {
        perror("numbers.dat");
        exit(1);
    }

    vet = (int *) malloc ( num * sizeof(int) );

    if ( save_numbers(vet) )
    {
        perror("numbers.dat");
        exit(2);
    }

    /* print test */
    for (i=0; i<num; i++)
        printf ("%d ", vet[i]);
    printf("\n");

    free(vet);

    return 0;
}

[版本 2] 我只有一个功能:它使用realloc 分配内存并将数字保存到*vet

#include <stdio.h>
#include <stdlib.h>

/* This function allocate memory
and save numbers into a vector */
int save_numbers (int **vet, int *num)
{
    FILE *fin;
    int i = 0;
    char buff[10];

    if ( !(fin = fopen("numbers.dat", "r")) )
        return 1;

    while ( fgets(buff, sizeof(buff), fin) )
    {
        *vet = (int *) realloc (*vet, (i+1) * sizeof(int) );
        sscanf (buff, "%d", &(*vet)[i]);
        i++;
    }

    *num = i;

    return fclose(fin);
}

int main ()
{
    int i, num, *vet = NULL;    

    if ( save_numbers(&vet, &num) )
    {
        perror("numbers.dat");
        exit(1);
    }

    /* print test */
    for (i=0; i<num; i++)
        printf ("%d ", vet[i]);
    printf("\n");

    free(vet);

    return 0;
}

此处的文件示例:http://pastebin.com/uCa708L0

【问题讨论】:

  • 最好尽量少读磁盘。所以,第 2 版。
  • 正如@APerson 所说,磁盘 I/O 很昂贵,所以版本 2 更好。但这并不好;一般来说,增量内存分配的成本是二次方的。您应该计划在每次分配时将分配的空间量加倍,以摊销分配的成本。
  • 并避免在为数字 N 分配空间时将前 N-1 个数字从一个地方复制到另一个地方的成本。这并不总是发生,但正式地,realloc() 释放了它当前分配的空间并分配新空间(但有时新旧指针会相同)。
  • @Jonathan Leffler 那么如果我能解决你所说的问题我应该怎么做?

标签: c file function malloc realloc


【解决方案1】:

由于A Personcommented,磁盘 I/O 开销很大,所以版本 2 更好,因为它读取文件一次。

虽然不好;一般来说,增量内存分配的成本是二次方的。您应该计划在每次分配时将分配的空间量加倍,以摊销分配的成本。这避免了在为数字 N 分配空间时将先前的 N-1 个数字从一个地方复制到下一个的成本。这并不总是发生,但正式地,realloc() 释放它当前分配的空间并分配新的空间空间(但有时新旧指针会相同)。

#include <stdio.h>
#include <stdlib.h>

/* This function allocate memory
and save numbers into a vector */
static int save_numbers(char const *file, int **vet, int *num)
{
    FILE *fin;
    int i = 0;
    int n_max = 0;
    char buff[10];

    if ((fin = fopen(file, "r")) == 0)
        return -1;

    while (fgets(buff, sizeof(buff), fin) != 0)
    {
        if (i >= n_max)
        {
            int n_new = (2 * n_max) + 2;
            void *v_new = (int *)realloc(*vet, n_new * sizeof(int));
            if (v_new == 0)
                return -1;
            *vet = v_new;
            n_max = n_new;
        }
        if (sscanf(buff, "%d", &(*vet)[i]) != 1)
            break;
        i++;
    }

    *num = i;

    /* Optionally release surplus space - if there is enough to warrant doing so */
    if (i + 8 < n_max)
    {
        void *v_new = realloc(*vet, i * sizeof(int));
        if (v_new == 0)
            return -1;
        *vet = v_new;
    }

    return fclose(fin);
}

int main(void)
{
    int i, num, *vet = NULL;    

    if (save_numbers("numbers.dat", &vet, &num))
    {
        perror("numbers.dat");
        exit(1);
    }

    /* print test */
    for (i = 0; i < num; i++)
        printf ("%d ", vet[i]);
    printf("\n");

    free(vet);

    return 0;
}

讨论

我正在尝试理解代码,但可能我还是太菜鸟。

有什么问题?我引入了新变量 n_max 来计算分配的行数;该数字最初为零。读取新行时,代码会检查数组中是否还有空槽(i >= n_max)。如果没有剩余空间,则 (2 * n_max) + 2 计算(我通常将其用于序列 2、6、14、30,或使用 (2 + n_max) * 2 用于序列 4、12、28、60 — 两者都确保经常执行重新分配以进行测试)提供了一个新的、非零的大小来分配。然后代码分配空间,在覆盖之前分配内存的指针之前检查分配是否成功,从而避免内存泄漏。如果一切正常,则分配新指针和新大小,并或多或少像以前一样继续,但检查sscanf() 是否有效。

但为什么是int n_new = (2 * n_max) + 2;?为什么是(2 * n_max) + 2;

因为 2 * 0 是 0,它不会分配比分配 0 更多的空间。

[1] 可能我不太清楚 realloc() 是如何工作的。为什么void *v_new = (int *)realloc(*vet, n_new * sizeof(int)); 中同时存在*v_new*vet

小号;要么它应该是int *v_new = (int *)realloc(*vet, n_new * sizeof(int));,要么它应该是void *v_new = realloc(*vet, n_new * sizeof(int));,尽管写的东西是有效的。至于为什么会有额外的变量,看看如果n_new 为6 时的内存分配会发生什么。变量*vet 持有指向原始数据的唯一指针,该原始数据持有2 个数字。如果分配失败但你写了*vet = (int *)realloc(*vet, n_new * sizeof(int));,那么你也失去了释放其他两个数字的机会——你不再有指向它们的指针。

一般来说,成语:

pointer = realloc(pointer, new_size);

是有风险的,因为它丢失了指向先前分配的指针。这就是为什么上面的代码将新指针保存到不同的变量中。

还要注意void *v_new 中的**vet 中的* 不同。在v_new的声明中,表示类型为指针。在赋值的RHS中,是解引用操作符。

[2] 为什么是*vet = v_new;

已经将新指针保存到v_new,一旦它被验证,那么分配给它是安全的

[3] 为什么是if (i + 8 &lt; n_max)?及其内容?

如果有足够多的过度分配的内存值得担心(8 个整数大约是在 64 位机器上有意义的最低限度),那么“可选地释放剩余空间”代码会在最后释放未使用的内存块的。标准并没有说realloc() 在收缩时不会移动数据,尽管realloc() 等人在收缩时确实会移动数据是非常罕见的实现。在该代码中省略“realloc() 返回 NULL”检查非常诱人。

如果in_max 的8 以内(即8 个整数,通常为32 个字节),则可能没有足够的可用空间值得释放。在 64 位系统上,最小分配通常是 16 字节,即使您分配连续的单个字符,返回的指针通常也会相隔 16 字节。因此,返回少于 16 个字节通常是完全无操作。返回 32 个字节更有可能是可用的。这是一个判断电话,但 4 或 8 是合理的数字,16 不会有问题——或者你可以完全忽略过度分配。 (另一方面,如果将分配从 1 GiB 增加到 2 GiB,然后使用第二个 GiB 中的 256 个字节,则返回第二个 GiB 数据的剩余部分可能是值得的。)

[4] 如果我理解,请告诉我:我的版本 2 没问题,但您的代码更好,因为它不会为每个数字重新分配内存,而是分配更多内存。在第一个周期它分配2*sizeof(int),在第二个周期它分配6*sizeof(int),在第三个周期它分配14*sizeof(int),等等。然后,如果分配的内存太多,它用if (i + 8 &lt; n_max) 释放它。正确的?我明白了吗?

这是非常正确的。当然,您所指的“循环”意味着代码不会在每次读取数字时分配。当代码读取第二个数字时,它不分配任何东西;它第一次为 2 个数字分配了足够的空间。当它读取第三个数字时,分配变为 6 个数字,因此它读取第四个到第六个数字而无需进行另一个分配。因此,它不需要 6 次分配来读取第六个数字,而是只进行了 2 次分配——节省了大量资金。

[5] 如何处理错误?例如,如果if (v_new == 0) 为真(v_new 为 0),则函数返回 -1,而 main 函数返回 perror("numbers.dat");,但这不是文件错误。

有多种方法可以做到这一点。我在对chux 的评论回复中提到,我通常会单独报告内存错误;实际上,我通常也会在函数中报告文件处理错误,而不是在main() 中使用报告。一个通常有用的约定是让检测错误的低级别报告它,但将它失败的信息传达回调用代码。除其他外,这意味着您可以区分打开文件的错误和关闭文件的错误,因为函数在检测到错误时知道正在尝试做什么,但调用函数只知道当它出现某种问题时正在处理文件。您可以使用或多或少复杂的方案来记录错误并在调用链上报告它们。

[6] 写if (sscanf(buff, "%d", &amp;(*vet)[i]) != 1)if (sscanf(buff, "%d", &amp;(*vet)[i]) == EOF)一样吗?但是文件控制的结束不是while (fgets(buff, sizeof(buff), fin))已经完成了吗?

如果字符串中没有数据(它是一个空字符串),sscanf() 调用将返回 EOF。如果有字符,它将返回 0,但不能将它们视为数字。如果有一系列字符组成一个数字,它将返回 1(但在有效数字之后可能有尾随的垃圾 - 字母或标点符号。同样,请参阅 cmets 以获取有关如何处理此问题的部分讨论 - 虽然该评论更多地是关于在一行中处理多个数字。

[7] 我还没弄清楚为什么if (i + 8 &lt; n_max)。你已经告诉我了,但我不明白。为什么我不能if (i &lt; n_max)

你可以if (i &lt; n_max)。但是,如果您通过realloc()(因为i + 1 == n_max)返回 4 个字节,那么系统可能无法对它做任何有用的事情,因此您通过调用没有取得任何成果。 OTOH,尝试释放每个字节并没有太大的危害。我的猜测是,如果您分配了几百个或更多数字并且文件在您将值读入最后一个之前结束,您通常会释放几百字节(或更多)的空间,这可能很有用。这是一个判断电话。我选择把事情复杂化;很抱歉我这样做了。

【讨论】:

  • 我正在尝试理解代码,但可能我还是太菜鸟。
  • +1 用于双倍分配大小。 (也许是size_t 而不是int?)我喜欢这个有轻微的mod,这个代码也适用于stdin
  • @chux: 是的,使用size_t 比使用int 更好。测试if (i + 8 &lt; n_max) 是这样编写的,因为如果in_maxsize_t(或另一个无符号类型)并且如果n_max 小于8,那么替代if (i &lt; n_max - 8) 永远不会工作。因此,无符号算术和size_t 被考虑在内,但并未实际使用。我通常还会在内存分配失败时打印某种错误消息。在这种情况下,我可能还会在函数内部强制*vet = NULL;,并释放旧数据并将*vet 重置为null,然后再返回错误。
  • @chux:代码也不处理每行多个数字。做到这一点并不难(if (sscanf(buff, "%d", &amp;(*vet)[i]) != 1) 代码变成了类似于int offset = 0; int nbytes; while (sscanf(buff+offset, "%d%n", &amp;(*vet)[i], &amp;nbytes) != 1) offset += nbytes; 来遍历行中的所有数字。
  • @JonathanLeffler 我尝试使用代码,但遇到了一些问题:[5] 如何处理错误?例如,如果if (v_new == 0) 为真(v_new 为 0),则该函数返回 -1 并且 main 执行 perror("numbers.dat");,但不是文件错误。 [6] 写if (sscanf(buff, "%d", &amp;(*vet)[i]) != 1)if (sscanf(buff, "%d", &amp;(*vet)[i]) == EOF)一样吗?但是文件控制的结束不是由while (fgets(buff, sizeof(buff), fin)) 完成的吗? [7] 我还没弄清楚为什么if (i + 8 &lt; n_max)。你已经告诉我了,但我不明白。为什么我不能if (i &lt; n_max)
【解决方案2】:

将(整个)文件读入一个与文件大小相同的缓冲区。没有realloc

关闭您的文件,不用担心稍后在您的程序中泄露句柄。

扫描所述缓冲区以查找换行符。数一数。不用担心数字。

既然您知道有多少,请分配您的数字数组。没有realloc。再次扫描缓冲区时写入该数组。

如果您不再需要缓冲区,请释放它。

除非您谈论的是到其他进程的管道、套接字或 4+ GB 文件,否则在低级语言中缓冲人类语言数据是没有意义的。或者,如果您正在编写将在电梯控制器、微波炉或电动剃须刀或其他东西上运行的代码 - 但是您没有任何文件,是吗?如果您的文件适合您的地址空间(现在大多数文件都适合),那么缓冲是对内存占用的过早优化,这将花费您的时间,包括写入时间和运行时间。

【讨论】:

  • 我不知道如何将整个文件读入一个大小与文件大小相同的缓冲区中!
  • 使用 WinAPI/windows?还是其他操作系统?
  • 使用fseek() 查找文件的结尾,使用ftell() 指定文件大小。然后分配那么多空间,并希望文件不再增长,并且不是管道、FIFO、套接字或终端(它们不支持查找)。如果一行中有多个数字,则概述的算法存在缺陷(fscanf() 不会坚持数字之间的换行符),但 slurp-and-scan 处理基本上是一种有效的操作方式。您可能还猜测您的文件通常包含带有单个分隔符的 N 位数字,并将文件大小除以 N+1 以获得要分配的大小。
  • 实际上,@JonathanLeffler 所说的,但从技术上讲,fseek 实现不允许有意义地支持文件末尾的偏移量。可能会。我一直在使用 KERNEL32FileSize 很长时间,我将不得不查看 C 规范以找到可以保证工作的东西。
  • @JonathanLeffler 如果您的文件不是预期的格式 - 无论是从一开始就是这样,还是在文件扩展时写入/读取之间存在竞争条件 - 程序将失败 否不管你的算法是什么。一次阅读全部内容至少可以排除竞争条件。
猜你喜欢
  • 1970-01-01
  • 2014-01-31
  • 2023-02-01
  • 2018-08-29
  • 1970-01-01
  • 2021-09-09
  • 2015-01-04
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多