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
可将字符从一个字符串复制到输出文件,直至达到该字符串的末尾为止。
可以利用fgets
和fputs
来实现 上一节 中介绍的函数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
调用使用同一个临时缓冲区,则需要为之分配新的内存空间。
仅有赋值是不够的。例如,下面的代码从一个输入文件中读入两行数据,并将它们存储在字符串变量line1
和line2
中:
void ReadTwoLines(FILE *infile) {
char buffer[MaxLine];
string line1, line2;
fgets(buffer, MaxLine, infile);
line1 = buffer;
fgets(buffer, MaxLine, infile);
line2 = buffer;
}
上述代码使得line1
和line2
的内容相同,出现这样的问题是因为,变量buffer
虽然在概念上是一个字符串,在内存中占有特定的空间,但赋值语句:
line1=buffer;
仅仅将buffer
的地址赋给了变量line1
。
当程序继续执行,要重新利用这块空间读入第二行文件时,同一块空间就用来存放第二个字符串了。
这样变量line1
和line2
最终指向了相同的内存,因此两者的值是相同的字符串。
常见错误:
如果使用同一个字符缓冲区从同一个文件中读入多行,要记住在读入下一行数据之前,需要将数据从缓冲区中复制到其他存储空间。
如果不进行这样的操作,前一行的内容将被覆盖。
解决这个问题的办法之一是利用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 文件