C - 无法从其中没有尾随换行符的文件中读取

C - Unable to read from a file that has no trailing new line in it

我正在用 C 编写一个程序,该程序必须从文件描述符中读取一行。 如果出现错误,它必须 return 1、0 或 -1,但该行是使用指针地址从 main 读取的。 当我想从我创建的文件中读取时,程序无法输出任何内容: echo -n "test test" > filetest1(-n 不输出尾随换行符)

我使用一个数组,在其中存储新行直到找到 \n,然后我为指针分配内存并将该行存储在其中。 该程序在指针中复制了 "test test" 但它在我编译时不输出任何内容。我不明白为什么。

我不允许:

无需使用中间缓冲区。假设return的值一定是-10+1,函数原型

int get_next_line(int fd, char **lineptr);

是有缺陷的,因为这意味着您无法读取嵌入 nul 字节 ('[=25=]') 的内容,并且重用相同的动态分配的行并不是真正可行的。 (当然,您可以使用 strlen() 猜测 上一行可能有多长,但这只是猜测。)

更好的原型是

int get_next_line(int fd, char **lineptr, size_t *sizeptr, size_t *lenptr);

其中为 *lineptr 分配的内存量将在 *sizeptr 中可用,以及实际行的长度(不包括 end-of-string [=29=] 字节)在 *lenptr。调用时,如果 *lineptr != NULL*sizeptr 会告诉 *lineptr.

已经动态分配了多少内存

无论如何,实现的关键是一个变量跟踪存储在新行中的字节数,另一个变量跟踪分配给该行的字节数,并在必要时重新分配。

假设您需要使用所示的第一个原型,并且调用者负责 free()ing 它,即忽略 *lineptr 的内容,并换行总是分配。

使用必要的变量声明和检查启动函数:

int get_next_line(int fd, char **lineptr)
{
    char   *line = NULL;
    size_t  size = 0;    /* Allocated for line, remember '[=12=]' at end */
    size_t  used = 0;    /* Number of chars stored in line */

    ssize_t n;           /* Result from read() */

    /* Just to make error handling easier, we clear *lineptr to NULL initially. */
    if (lineptr)
        *lineptr = NULL;

    /* fd must be a valid descriptor, and lineptr a valid pointer. */
    if (fd == -1 || !lineptr)
        return -1;        

在读取循环中,您可以使用n = read(fd, line + used, 1);将下一个字符读入缓冲区,但您显然必须首先确保缓冲区有足够的空间容纳它。因此,在循环 body 开始时,在读取下一个字符之前,您确保该行至少有一个字符的空间,并且还要确保 [=29=] 终止字符串:

        if (used + 2 > size) {
            char *new_line;

            /* Better allocation policy! */
            size = used + 2;

            new_line = realloc(line, size);
            if (!new_line) {
                free(line);
                return -1;
            }
            line = new_line;
        }

关于分配策略的评论意味着你真的不想每隔一个字符读取就增加行缓冲区:你会想要以更大的块来增加它,所以不需要经常重新分配。

那么什么才是好的重新分配政策呢?这是一个讨论的问题。这实际上取决于典型数据是什么样的。如果策略以 1 兆字节左右的块为单位进行分配,那么每一行都可能会浪费大量内存,即使稍后将其调整为正确的大小也是如此。 (这是因为许多 C 库使用一种称为 内存映射 的技术来进行大型分配——比如 256kiB 或更多,但它因实现而异——并且它们有自己的粒度,通常 页面大小 (通常是 2 的幂,介于 4kiB (4096) 和 2MiB (2097152) 之间),因此平均而言,每行至少有一半这样的页面浪费我们对此无能为力。)

在实践中,我推荐一种分配典型长度的行的策略,然后将更长的行的分配加倍,达到某个限制(如兆字节左右),并以该限制的倍数进行分配。例如:

            /* Suggested allocation policy, much better */
            if (used < 126)
                size = 128;
            else
            if (used < 1048574)
                size = 2 * used;
            else
                size = used + 1048576;

上面的确切数字并不重要,只要您牢记 size 必须至少增长到 used + 2。它可以变大,但如果它没有变大到至少 used + 2,我们就有麻烦了(我们的程序将无法正常运行)。

如果read() returns 0,表示遇到文件末尾。在这种情况下,line 中没有存储任何内容,您不应递增 used.

如果read() return为负数,则发生错误。 (通常,read() 应该只能 return -1,在这种情况下,设置 errno 来指示错误。但是,内核或 C 库中的错误总是可能——实际上,我确实知道一个 Linux 内核错误可能导致 read() 到 return 负面,但它只在尝试写入大于 2GiB 的块时发生。)

换句话说,我会推荐类似

的东西
            n = read(fd, line + used, 1);
            if (n == 0)
                break; /* Break out of the loop; no error */
            else
            if (n < 0) {
                /* Read error of some sort;
                   errno set if n == -1 */
                free(line);
                return -1;
            }

            if (line[used] == '\n') {
                /* Keep newline character */
                used++;
                break;
            }

            /* Skip/omit embedded NUL bytes */
            if (line[used] != '[=15=]')
                used++;

循环后,used == 0如果没有读取任何内容。请注意,如果您不保留换行符(将 \n 添加到缓冲区中),您将无法判断您是在读取空行(仅 \n)还是在该行的结尾。你可以通过添加一个标志变量来避免这种情况,比如 newline_seen,它最初是零,但你设置为一。然后,只有当 usednewline_seen 在循环后都为零时,您是否在输入结束时没有任何内容可读。

循环后,您需要附加 '[=25=]' 以正确终止该行。此外,您可能希望将分配给该行的内存优化为精确长度:

    char *new_line;

    new_line = realloc(line, used + 1);
    if (!new_line) {
        free(line);
        return -1;
    }
    line = new_line;

    line[used] = '[=16=]';

最后,将动态分配的行存储到调用者提供的指针中,return。 (请注意,根据需要,您可能需要检查 used 是否使用 01 return。)

    *lineptr = line;

以上绝对不是解决这个问题的唯一方法。例如,当您希望省略行尾的换行符时,您可以在循环内的 break; 语句之前添加终止符 [=29=]。然后,在 n == 0 的情况下,你可以检查是否 used == 0: 如果它是零,这意味着你在输入的末尾,没有任何东西可读,你可以释放 line和return(值表示文件结束,没有可读的内容)。

如果与OP的实现相比,最重要的一点是避免二级缓冲区,并使用realloc()根据需要增加行缓冲区。

注意当size为正数(非零)时,malloc(size)realloc(NULL, size)是等价的。 (无论如何,零的情况有点模棱两可,因为一些实现 return NULL,而其他实现 return 一个你不能用来存储其他数据的指针。)

另外,free(NULL) 是安全的,什么都不做。因此,如果你将你的指针初始化为 NULL,在必要时使用 realloc() 来增长它,你总是可以调用 free(pointer) 来丢弃它,不管它是否仍然是 NULL 或者有一些内存分配给它。在我自己的程序中,我很少使用 malloc(),因为 realloc()free() 就足够了。

最后,我不是建议你复制上面的代码。对于你的学习过程,你的学习曲线,按照你自己的方式,按照你自己的节奏做事很重要;否则,您可能会一无所获。始终通过充分了解您自己的程序来确保您的立足点,因为其他一切都将建立在它之上。知识是建立起来的,而不是堆积起来的。死记硬背——在没有理解的情况下复制——在这个水平上毫无价值。我们站在巨人的肩膀上,等等。


只使用malloc()的要求是完全愚蠢的,因为realloc()是通常使用的功能,而且在很多方面也很容易理解。

无论如何,让我们看一下 OP 的实现,以及 get_next_line() 调用中实际发生的情况:

  1. ft_read_line(fd, buf, &j) 被调用。 fd 是正确的文件描述符,buf 是一个包含 10,000,000 个字符的本地数组,j 是一个清零的整数。

  2. ft_read_line()str 指针初始化为 NULL。

  3. 一个 while 循环试图从文件描述符 fd 中读入一个名为 cchar 变量,只要 read() 不报告输入结束。 (也就是说,即使发生错误。)如果读取到换行符\n,则代码跳出循环,否则将c附加到作为参数指定的缓冲区。

  4. 如果str不是NULL,则释放。这个 if 子句是双重无用的:首先,因为 str 在这里必须是 NULL。第二,因为不需要检查str == NULL是否调用free(str)free(NULL) 是绝对安全的,什么也不做。 (换句话说,free() 本身会检查其参数是否为 NULL,如果是则什么也不做。)

  5. i 是作为参数传递到缓冲区中的字符数。 str 分配了足够的内存用于 i+1 指向 char 的指针。如果分配失败,函数returns NULL.

  6. 此时,缓冲区内容被视为一个字符串。但是,没有 end-of-string NUL 字节 ([=29=]) 被附加到缓冲区,所以它还不是一个字符串。换句话说,strcpy(str, buffer) 可能会尝试从 buffer 复制多于 i 个字符到 str,因为它会在 buffer 中查找 NUL 字节来标记结束。这是一个致命的错误,可能会导致程序崩溃。

  7. 一个过时的函数,bzero(),用于将buffer中的10,000,000字节清零。

  8. 指针 str 被 return 编辑为 get_next_line()。请注意,在 get_next_line() 中,j 将反映来自 read() 的最后一个 return 值(即,如果已读取字符但为 '\0',则为 1,如果没有更多的输入。(所有其他值都是不可能的,因为它们是代码可以跳出 while 循环的仅有的两个值。)

  9. returned指针保存到第二个参数指向get_next_line()的指针。

  10. get_next_line()检查是否j == -1。这是一个无用的检查,因为这里 j 不能是 -1

  11. get_next_line() returns 如果发生输入结束则为 0,否则为 1。

如您所见,中间缓冲区没有用。然而,还有更严重的问题。首先是将输入数据视为字符串,而不是通过附加 string-terminating NUL 字节 [=29=] 使 char 数组成为字符串。第二个是读取错误被忽略,使 while 循环重复,直到它尝试覆盖它不能覆盖的内存,此时程序崩溃。


如何实现int get_next_line(int const fd, char **line)功能,那么如果只允许使用open()read()malloc()memmove()呢?

您需要实现的逻辑实际上非常简单:

  1. 开始(无限)循环:

  2. 确保动态分配的线路至少有两个 char 的空间。

  3. 尝试 read() 从文件描述符到动态分配行中下一个未使用索引的一个字符。

    如果read() returns

      [=342=

      > 0,你有一个新角色。如果它是换行符 (\n),则通过附加 NUL 字节 [=29=] 来终止字符串。如果您希望 get_next_line() 删除每行末尾的换行符,请将读取的换行符替换为 NUL 字节。跳出循环。

      请注意,在某些情况下,文件或输入的数据本身可能包含 "embedded NUL bytes"[=29=]。我个人会像检查换行符一样检查这些,除了不是跳出循环,我只是不将它们保存在动态分配的缓冲区中。

    • 0,没有读取数据,也没有更多的输入。如果缓冲区索引仍然为零,则输入结束发生在读取任何内容之前;因此,释放动态分配的缓冲区,并 return 一个指示 end-of-input 的值(并确保行指针为 NULL)。

      如果缓冲区索引不为零,则表示文件中的最后一行没有以换行符结尾。此类文件偶尔会发生。将'\0'附加到动态分配的缓冲区,并跳出循环。

    • < 0,发生错误。如果read() returned -1,那么确切原因在errno;否则,这是一个读取错误(与 errno == EIO 相同)。我建议您释放动态分配的行(并将行指针设置为 NULL)和 return 指示错误的值。

      如果需要,您可以将终止 NUL 字节 [=29=] 添加到当前读取的行,将行指针设置为指向它,并 return 一个错误值;这样即使发生错误,调用者也会得到部分行。

      但是,在我看来,在发生错误时 return 使用部分行是不明智的:没有什么 safe/reliable 可以用该行完成 - 除了可能显示给用户,但即便如此,它也可能是垃圾。最有可能的是,用户只对知道发生了错误感兴趣,并且会丢弃所有部分数据,并尝试其他的东西。这就是我所做的(并且已经完成)。

  4. 可选地,将行重新分配为其中保存的字符数,再加上一个用于(已附加)string-terminating NUL 字节 [=29=].

  5. 保存指向动态分配缓冲区的指针(指向*line),并且return的值表示一行已成功读取。


现在,如何确保动态分配的缓冲区有足够的空间?

通常,我们使用一个指向缓冲区的指针,缓冲区中的数据字符数(它的长度),以及为缓冲区分配的大小(即我们可以在缓冲区中存储的字符数) :

char  *buffer = NULL;
size_t length = 0;
size_t allocated = 0;

当我们检测到 length >= allocatedlength + 2 > allocated 时,因为我们希望能够在上面添加至少两个字符(一个数据和一个 [=29=])- -,我们需要重新分配一个更大的buffer。使用realloc(),变量如上初始化,就和

一样简单
char *temp;

allocated = length + 2; /* Or, say, 2*length + 2 */
temp = realloc(buffer, allocated);
if (!temp) {
    /* Out of memory; exit program! */
}
buffer = temp;

realloc() 的想法很简单。它有两个参数:一个指针和所需的大小。

如果指针是NULL,则realloc()分配内存,足以存储字符的大小,return存储它。

如果指针已经指向动态分配的内存,realloc() 会将内存量增加或减少到所需的大小。如果更大,则保留所有旧数据。如果更小,则只保留新大小的数据。在这两种情况下,realloc() 可能 return 是不同的指针;但即便如此,还是会有相同的数据。

如果指针指向其他任何地方,如局部变量或数组,程序可能会崩溃。即使它没有崩溃,realloc() 在这种情况下也根本不起作用。

如果realloc()不能重新分配,它将returnNULLerrno == ENOMEM。如果它试图增加或缩小已经动态分配的内存,则该分配和内存仍然有效。 (这就是为什么你不应该使用 buffer = realloc(buffer, new_size) 的原因:如果它失败了,你就失去了之前的 still-valid buffer。正如你在上面看到的,我使用了一个临时变量结果,如果它是 non-NULL,则只分配回 buffer。)

不过,我们可以使用 malloc()memmove() 编写我们自己的 realloc() 模拟。我们只需要知道旧尺寸和新尺寸:

void *our_realloc(void *old_data, const size_t old_size, const size_t new_size)
{
    void *new_data;

    /* Reallocation to zero size is special. We always free and return NULL. */
    if (new_size < 1) {
        free(old_data);
        return NULL;
    }

    /* Allocate memory for the new buffer. */
    new_data = malloc(new_size);
    if (!new_data)
        return NULL; /* Failed! */

    /* Copy old data, if any. */
    if (old_size > 0) {
        if (old_size < new_size)
            memmove(new_data, old_data, old_size);
        else
            memmove(new_data, old_data, new_size);
    }

    free(old_data);

    return new_data;
}

真正的 realloc() 更优越,因为它不需要知道旧的大小(C 库内部会记住它!),而且它通常可以 grow/shrink 就地分配,无需像我们上面那样暂时需要额外的内存。


我担心讲师或课程设计者要么是天才要么是功能白痴,而是认为您应该按照以下几行编写 get_next_line()

  1. 分配您认为足够大的动态缓冲区。

  2. 在一个循环中:

  3. read()一个字符。如果read() returns:

    • > 0,你多读了一个字符。

      如果该字符是 \n,将其追加到缓冲区(如果您希望保留换行符),并通过追加 string-terminating NUL 字节 [= 29=],也是。保存缓冲区指针和 return(表示读取了新行的值)。

      如果该字符不是 [=29=],则将其附加到动态缓冲区。 (由于 [=29=] 终止一个字符串,如果我们从文件中读取它们,我们将跳过它们。)

    • == 0,没啥可看的了。如果动态分配的缓冲区还没有任何数据,我们在任何数据之前得到end-of-input,可以丢弃缓冲区,而return"nothing; end of input".

    • < 0,发生错误。丢弃动态分配的buffer,return读取错误

请注意,在上述情况下,您可以使用无限循环。例如,while (1) { ... }for (;;) { ... }。你可以 break ,但在这里,你也可以只 return 从上述情况下的整个函数。

如果他们使用此练习来展示 malloc() 单独如何导致任意魔法常量和缓冲区大小,realloc() 的重要性,以及如何相对容易地修复它以用于 any-length 行(使用 realloc()`,他们是天才。因为 real-world 代码必须处理这些东西,它实际上可能是教授动态内存管理的有效方法。

如果他们认为这种代码在任何其他情况下都是可以接受的,那么他们就是功能白痴。由于 badly-written 软件(例如,具有秘密限制的那种,如果您碰巧超过这些限制,它会导致它崩溃而没有任何解释)造成的时间和资源损失是天文数字,再加上 body 就像教学生如何将剪纸和家用化学品混合在一起以获得最大的痛苦,并用它来博取同情。功能齐全,但很愚蠢。