3.字符I/O
文件处理的最简便的方法是逐字符地遍历该文件。
stdio.c
接口定义了getc(infile)
函数,该函数从某一文件中读取下一个字符,并将其返回给调用函数。
同时还定义了getchar()
函数,该函数从标准输入文件中进行读取,因此与getc(stdin)
作用相当。
getc的的函数原型如下所示:
int getc(FILE *infile);
初看之下,函数返回值的类型是很奇怪的。函数原型指定getc
返回一个整型值,虽然该函数在概念上返回的是一个字符。
这样设计的原因是因为返回一个字符会使得程序无法识别文件结束标记。
字符编码一共只有256个,且一个数据文件中可能包含其中的任意值。因此没有一个值(至少没有char
类型的值)可以用作文件结束标记。
扩展定义,使得getc
返回一个整型值,这样的实现可以返回一个合法字符数据以外的值作为文件结束标记。
通常在stdio.h
中这个值被称为EOF
,EOF
有值-1。
常见错误:
getc
返回一个整型值,而不是一个字符型的值。
如果用字符型的变量存储getc
的结果,程序就检测不到文件结束标记。
如果要写入一个单独的字符,可以用函数putc(ch, outfile)
,它将第一个参数写入指定的输出文件中。
stdio.c
还包括函数putchar(ch)
,其定义与putc(ch, stdout)
相同。
作为getc
和putc
用法的一个例子,可以用下文所示的copy file.c
程序将一个文件拷贝到另一个文件中:
#include <stdio.h>
#include <stdbool.h>
#include <ctype.h>
#include "strlib.h"
#include "string.h"
#include "simpio.h"
/* Function Prototype */
static FILE *OpenUserFile(string prompt, string mode);
static void CopyFile(FILE *infile, FILE *outfile);
/* Main Program */
main() {
FILE *infile, *outfile;
infile = OpenUserFile("Old file: ", "r");
outfile = OpenUserFile("New file: ", "w");
CopyFile(infile, outfile);
fclose(infile);
fclose(outfile);
}
/* Function */
static FILE *OpenUserFile(string prompt, string mode) {
char filename[20];
FILE *result;
while (true) {
printf("%s", prompt);
fgets(filename, 20, stdin);
if (filename[strlen(filename)-1] == '\n') filename[strlen(filename)-1] = '\0';
result = fopen(filename, mode);
if (result != NULL) break;
printf("Can't open the file %s \n", filename);
}
return result;
}
static void CopyFile(FILE *infile, FILE *outfile) {
int ch;
while ((ch=getc(infile))!=EOF) {
putc(ch, outfile);
}
}
copyFile
中的while
循环非常常用。
while
循环的判断表达式通过嵌入式赋值语句将读入字符和检测文件结束标记的操作结合起来。
3.1 文件更新
上文的copyfile.c
程序精确地用另一个文件名创建了一个文件的副本。
如果不是完全复制一个文件,也可以用同样的基本结构写一个程序,在读的同时对字符进行转换。
例如,以下循环将数据从infile
复制到outfile
,并将所有字符转化为大写形式:
while (true) {
if ((ch=getc(infile))!=EOF) {
putc(toupper(ch), outfile);
}
}
但很多时候我们并不需要一个新的文件,我们只需要修改现有文件。
对现有文件进行修改的过程称为更新(update)该文件,但这个过程并不简单。
对大多数系统而言, 如果一个文件已经为进行输入而打开,就不允许再为输出打开。
根据不同系统上的不同文件执行方式,调用fopen
会导致调用失败或毁坏原文件的内容。
更新一个文件最常用的办法是将新数据写入一个临时文件,在写好更新文件的所有内容后用这个临时文件替换原文件。
因此,如果要写一个程序将一个文件的字符全部转换为大写,该程序需要执行如下步骤:
(1) 打开原文件,以便输入。
(2) 打开一个临时文件,以便输出,临时文件名不能与原文件相同。
(3) 将输入文件复制到临时文件,并将所有小写字符用大写替换。
(4) 关闭这两个文件。
(5) 删除原文件。
(6) 用原文件的名字重新命名临时文件。
在编写实现这一策略的代码时,需要使用三个定义在stdio.h
头文件中的函数——tmpnam
、remove
和rename
。
stdio.h
接口中包括的函数tmpnam
可以为临时文件自动生成文件名。文件命名的习惯因不同机器而异。
调用函数tmpnam(NULL)
会返回一个字符串,它的值适合作为该台机器上临时文件的文件名。
因此,创建并打开一个新的临时文件,代码实现如下:
temp = tmpnam(NULL);
infile = fopen(temp, "w");
删除一个文件只需调用函数remove(name)
即可,此处的name
是一文件名。
重命名一个文件,可以通过调用函数rename(old name, new name)
完成。
和ANSI库中的很多其他函数一样,remove
和rename
在调用成功后返回0,调用失败后返回一个非零值。
这三个函数提供了编写程序ucfile.c
所需的内容,该程序可将一个文件中的字符转换为大写字符。代码示例如下:
#include <stdio.h>
#include <stdbool.h>
#include <ctype.h>
#include "strlib.h"
#include "string.h"
#include "simpio.h"
/* Function Prototype */
static void UpperCaseCopy(FILE *infile, FILE *outfile);
/* Main Program */
main() {
string filename, tmpname;
FILE *infile, *outfile;
printf("This program convers file to upper case.\n");
while (true) {
printf("File name: ");
fgets(filename, 20, stdin);
if (filename[strlen(filename)-1] == '\n') filename[strlen(filename)-1] = '\0';
infile = fopen(filename, "r");
if (infile != NULL) break;
printf("File %s not found, try again.\n", filename);
}
tmpname = tmpnam(NULL);
outfile = fopen(tmpname, "w");
if (outfile == NULL) Error("Can't open temporary file.");
UpperCaseCopy(infile, outfile);
fclose(infile);
fclose(outfile);
if ((remove(filename)!=0) || (rename(tmpname, filename)!=0)) Error("Can't rename temporary file.");
}
/* Function */
static void UpperCaseCopy(FILE *infile, FILE *outfile) {
int ch;
while ((ch=getc(infile))!=EOF) {
putc(toupper(ch), outfile);
}
}
3.2 在输入文件中重新读取字符
从一个输入文件中读取数据时,我们经常会遇到这样的问题,即直到读取了多余的字符后才发现早就应该停止读取了。
例如,假设从一个文件读取字符,以找出一个由十进制数组成的数字。
因此,不断读取字符直到非数字字符出现的循环如下所示(使用ctype.h
中的isdigit
函数):
while (isdigit(ch=getc(infile)))...
当读到第一个非数字字符时我们才发现已经找到了一个十进制数。
这时该非数字字符就成为循环结束的标志,但它也很可能是下一次读文件时所需的值。
通过调用getc
,已经将该字符读到变量ch
中,并已经将它从输入流中取出了。
C语言提供了函数ungetc(ch, infile)
,该函数的作用是将字符ch
“推回”到原来的输入流中,作为下一次调用getc
的返回值。
为了理解函数ungetc
是如何使用的,假设需要编写一个程序:
将程序从一个文件复制到另一个文件,在此过程中删除所有的注释语句。
在C语言中,一条注释语句以字符序列“/*”开始,以序列"*/”结束的。
删除注释语句的程序必须能在检测到开始标记“/*”前复制字符,在检测到后逐一读字符但不进行复制,直至检测到结束标记“*/”为止。
这个问题的难点在于注释标记是由两个字符组成的。
每次从文件中复制一个字符,当遇到“/”时,只有读入下一个字符后我们才能进行判断。
如果下一个字符是“*”,那么将两个字符都忽略掉,并标识出已经进入注释语句。
如果不是“*”,则需将其推回到输入流中,留待下一个循环周期中再复制它。
函数CopyRemovingComments
的代码实现如下:
#include <stdio.h>
#include <stdbool.h>
#include <ctype.h>
#include "strlib.h"
#include "string.h"
#include "simpio.h"
/* Function */
static void CopyRemovingComments(FILE *infile, FILE *outfile) {
int ch, nch;
bool CommentFlag;
CommentFlag = FALSE;
while ((ch=getc(infile))!=EOF) {
if (CommentFlag) {
if (ch=='*') {
nch = getc(infile);
if (nch=='/') {
CommentFlag = FALSE;
} else {
ungetc(nch, infile);
}
}
} else {
if (ch=='/') {
nch = getc(infile);
if (nch == '*') {
CommentFlag = TRUE;
} else {
ungetc(nch, infile);
}
}
}
if (!CommentFlag) putc(ch, outfile);
}
}
参考
《C语言的科学和艺术》 —— 15 文件