(十五)文件 -- 4. 面向行的I/O

4. 面向行的I/O

由于文件通常被划分成行,因此很有必要一次读入整行数据。

stdio.h中执行这一操作的函数为fgets,其函数原型为:

string fgets(char buffer[], int bufsize, FILE *infile);

此函数的作用在于将下一行文件读入字符数组buffer中。

通常,fgets在读入第一个换行字符后停止读取,但如果该行的长度超过了由函数参数bufsize限定的长度,则fgets函数将提前返回。

因此,buffer数组中表示终止的空字符前应为一换行符,除非该行文件过长,超出buffer的限制。


无论fgets读入的是一整行还是此行的一部分,通常情况下都会返回一个指针,指向函数中的第一个参数(即字符数组buffer)。

如果fgets在文件的末尾调用,则返回NULL


相应的输出函数为fputs,其函数原型为

void fputs(string str, FILE *outfile);

调用函数fputs可将字符从一个字符串复制到输出文件,直至达到该字符串的末尾为止。


可以利用fgetsfputs来实现 上一节 中介绍的函数CopyFile,该程序如下所示:

static void CopyFile(FILE *infile, FILE *outfile) {
    char buffer[MaxLine];
    while (fgets(buffer, MaxLine, infile)!=NULL) {
        fputs(buffer, outfile);
    }
}

当使用函数fgets时,必须为输入行提供一个缓冲区。

CopyFile这个例子中,数组的空间是由声明

char buffer[MaxLine];

在当前帧中显式分配的。

需要注意的是,此缓冲区的内存空间将在函数返回时释放。

如果想要将行中的字符更长久地存储起来,需将它们存储在函数调用结束后仍然存在的内存空间中。
通常来说,从堆中动态分配内存是一种更简便的方法。


如果对多次fgets调用使用同一个临时缓冲区,则需要为之分配新的内存空间。

仅有赋值是不够的。例如,下面的代码从一个输入文件中读入两行数据,并将它们存储在字符串变量line1line2中:

void ReadTwoLines(FILE *infile) {
    char buffer[MaxLine];
    string line1, line2;
    fgets(buffer, MaxLine, infile);
    line1 = buffer;
    fgets(buffer, MaxLine, infile);
    line2 = buffer;
}

上述代码使得line1line2的内容相同,出现这样的问题是因为,变量buffer虽然在概念上是一个字符串,在内存中占有特定的空间,但赋值语句:

line1=buffer;

仅仅将buffer的地址赋给了变量line1

当程序继续执行,要重新利用这块空间读入第二行文件时,同一块空间就用来存放第二个字符串了。

这样变量line1line2最终指向了相同的内存,因此两者的值是相同的字符串。


常见错误:

如果使用同一个字符缓冲区从同一个文件中读入多行,要记住在读入下一行数据之前,需要将数据从缓冲区中复制到其他存储空间。

如果不进行这样的操作,前一行的内容将被覆盖。


解决这个问题的办法之一是利用strlib库中的函数CopyString将每一个字符串写入各自的堆内存中,如下所示:

void ReadTwoLines(FILE *infile) {
    char buffer[MaxLine];
    string line1, line2;
    fgets(buffer, MaxLine, infile);
    line1 = CopyString(buffer);
    fgets(buffer, MaxLine, infile);
    line2 = CopyString(buffer);
}

另外一个解决方案就是利用在simpio.h中定义的函数ReadLine,它可以避免与fgets相关的其他问题:

  • 难以确定缓冲区的大小。有些文件中的行比较长,因此很难选择一个适合所有文件的缓冲区的大小。
  • 难以判断是否超出了缓冲区的界限。如果在调用函数fgets的过程中给定一个最大值,则意味着数据不会被写到所分配的缓冲区之外,但同样需要知道fgets是否读入了一个完整的行。使用fgets时的唯一做法是浏览缓冲区内的所有字符,检测是否包括换行符。
  • fgets存储换行符通常也会引起麻烦。在大多数的程序中,换行符只是一行结束的标记,而并不真正是数据的一部分。使用fgets意味着还需采取其他的步骤将缓冲区中的换行符删除。

fgets相比,ReadLine具有以下优点:

  • ReadLine在需要时自动分配堆内存,使得缓冲区不可能溢出。
  • ReadLine删除了标记每一行结束的换行符,所返回的数据只包括这一行中的字符。
  • ReadLine返回的每一个字符串都保存在各自的内存中,因此在存储一个字符串之前不必考虑是否需要进行复制。

函数ReadLine在遇到文件结束符时返回NULL





参考

《C语言的科学和艺术》 —— 15 文件

上一篇:PHP fgets() 函数


下一篇:用 C 语言开发一门编程语言 — 交互式 Shell