文件打开之后,就可以对它进行读写了。这一节将介绍常用的读写函数。
1.正确选择打开方式
【例22.9】下面的程序是将128个字符写入文件,程序是否正确?
#include <stdio.h> #include <string.h> int main (void ) { unsigned char ch ; FILE *fp=NULL ; fp=fopen ("ttt.txt" ,"w" ); // 创建一个文件 for (ch=0 ; ch<128 ; ++ch ) fputc (ch ,fp ); fclose (fp ); return 0 ; }
【解答】程序输出写入的最后一个字符的16进制编码是7f,好像验证了确实写入128个字符。其实,这里是按文本文件方式创建了一个只写文件,所以实际上会多写一个回车符。要写入128个,则不应写入回车符,这就要创建二进制文件。
// 修改后使用读入验证的程序 #include <stdio.h> #include <string.h> int main (void ) { unsigned char ch ; int i=0 ; FILE *fp=NULL ; fp=fopen ("ttt.txt" ,"wb" ); // 创建一个二进制只写文件 for (ch=0 ,i=1 ; ch<128 ; ++ch ,++i ) fputc (ch ,fp ); printf ("i = %d , ch = %x\n" ,i-1 , ch-1 ); fclose (fp ); i=0 ; fp=fopen ("ttt.txt" ,"rb" ); // 打开一个二进制只读文件 while ( !feof (fp ) ) { ch = fgetc ( fp ); i++ ; } printf ("i = %d , ch = %x \n" , i , ch ); fclose (fp ); return 0 ; }
程序输出结果如下:
i = 128 , ch = 7f i = 129 , ch = ff
上面程序加入验证输出信息,证明读出128个字符,第129次读入的是文件结束符ff,也就是EOF(-1),从而说明写入的是128个二进制码。
【例22.10】下面程序错在哪里?
#include <stdio.h> #include <stdlib.h> const char name[ ]="f :\ct3\new\ttt" ; int main (void ) { FILE *fp ; fp = fopen (name ,"w" ); if (fp == NULL ) { printf (" 创建文件%s 出错!\n" , name ); exit (1 ); } fclose (fp ); return 0 ; }
【解答】字串"f:\\ct3\\new\\ttt"如果用在#include包含语句中,是正确表示文件路径和文件名的方法。但这里是用在程序语句中,反斜杠被用作转义字符,\n表示换行。\new变成换行后输出ew。\t是Tab键,它把\ttt分解为按Tab规定,输出tt,即程序输出结果为:
创建文件f :ct3 ew tt 出错!
正确的DOS路径和文件的表示应该为:
const char name="f :\\ct3\\new\\ttt" ;
改正后,程序在f:\ct3\new下面创建文件ttt。注意,一定要给出文件名,如果只给出文件夹,如"f:\\ct3\\new\\ttt",则将给出如下出错信息:
创建文件f :\ct3\new\ 出错!
2.文本文件的操作
【例22.11】下面程序存入文件的内容正确吗?
#include<stdio.h> #include <stdlib.h> void main ( ) { FILE *fp ; char ch ,filename[10] ; printf ( "Enter a file name :" ); scanf ( " %s" , filename ); if ( ( fp = fopen ( filename ,"w" ) ) == NULL ) { printf ( " cannot open file %s\n" ,filename ); exit (1 ); } printf ( "Input :" ); // 输入以字符# 作为结束符 while (1 ){ scanf ( " %c" , &ch ); if ( ch == '#' ) { printf ( "\nBye !\n" ); break ; } fputc (ch ,fp ); putchar (ch ); // 在屏幕上显示出来 } fclose (fp ); }
【解答】不正确。scanf语句是以空格作为分界符的,即它不接受空格,所以存入文件的内容将是去除空格以后的内容,因此不合要求。用“ch=getchar();”语句代替它既可。
putchar(ch)用来在屏幕上显示写入文件的字符。这里作为验证的信息,实际程序中可以删除。putchar函数就是从fputc函数派生出来的。putchar(c)是如下定义的宏。
#difine putchar (c ) fputc ( c ,stdout )
这里的stdout是系统定义的文件指针变量,它与终端输出相关联。fputc(c,stdout)的作用是将c的值输出到终端。用宏putchar(c)比书写fputc(c,stdout)简单一些。从用户的角度看,可以把putchar(c)看做函数而不必严格地称它为宏。
程序运行示范如下:
Enter a file name : t.txt Input : We are here ! We are here ! Go home !# Go home ! Bye !
fputc(ch,fp)函数把一个字符写入磁盘文件中。其中,ch是要输出的字符,它可以是一个字符常量,也可以是一个字符变量。fp是文件指针变量,它从fopen函数得到返回值。fputc函数的作用是将字符(ch的值)输出到fp所指向的文件上去。fputc函数也带回一个值,如果输出成功,则返回值就是输出的字符;如果输出失败,则返回EOF。EOF是在stdio.h文件中定义的符号常量,其值为-1。
可以将t.txt文件中的内容打印出来,以便证明在t.txt文件中已存入了输入的信息。
【例22.12】将程序中的两处调用fgetc简化为一个。
#include <stdio.h> #include <stdlib.h> void main ( ) { FILE *fp ; char ch ; if ( ( fp = fopen ("t.txt" ,"r" ) ) == NULL ) { printf ( "cannot open infile\n" ); exit (1 ); } ch = fgetc ( fp ); while ( ch != EOF ) { putchar ( ch ); ch = fgetc ( fp ); } putchar ('\n' ); fclose (fp ); }
【解答】使用do~while循环即可,程序运行结果就是写入t.txt文件的内容。
#include <stdio.h> #include <stdlib.h> void main ( ) { FILE *fp ; char ch ; if ( ( fp = fopen ("t.txt" ,"r" ) ) == NULL ) { printf ( "cannot open infile\n" ); exit (1 ); } do{ ch = fgetc ( fp ); putchar ( ch ); }while ( ch != EOF ); putchar ('\n' ); fclose (fp ); }
fgetc(fp)从指定文件读入一个字符,该文件必须是以读或读写方式打开。其中,fp为文件指针变量,ch为字符变量。fgetc函数带回一个字符,赋给ch。如果在执行fgetc读字符时遇到文件结束符,函数返回一个文件结束符标志EOF。
注意:EOF不是可输出字符,因此不能在屏幕上显示。由于字符的ASCII码不可能出现-1,因此EOF定义为-1是合适的。当读入的字符值等于-1(EOF)时,表示读入的已不是正常的字符而是文件结束符。
【例22.13】将一个磁盘文件中的信息复制到另一个磁盘文件中。
#include <stdio.h> #include <stdlib.h> void main ( ) { FILE *in , *out ; if ( ( in = fopen ("t.txt" ,"r" ) ) == NULL ) { printf ( "cannot open infile\n" ); exit (1 ); } if ( ( out = fopen ("out.txt" ,"w" ) ) == NULL ) { printf ( "cannot open outfile\n" ); exit (1 ); } while ( !feof (in )) fputc ( fgetc (in ),out ); fclose (in ); fclose (out ); }
【例22.14】用命令行的方式实现将一个磁盘文件中的信息复制到另一个磁盘文件中。
【解答】这时要用到main函数的参数,将它编写在一个文件中,产生可执行文件之后,要在DOS环境下用命令行参数方式运行。
#include <stdio.h> #include <stdlib.h> void main (int argc , char *argv[ ] ) { FILE *in , *out ; if (argc !=3 ){ printf ( "You forgot to enter a filename\n" ); exit (1 ); } if ( ( in=fopen (argv[1] ,"r" )) == NULL ){ printf ( "cannot open infile\n" ); exit (1 ); } if ( (out=fopen (argv[2] ,"w" ) ) == NULL ){ printf ( "cannot open outfile\n" ); exit (1 ); } while ( !feof (in ) ) fputc ( fgetc (in ),out ); fclose (in ); fclose (out ); }
假若本程序的文件名为exam.c,经编译连接后得到的可执行文件名为exam.exe,则可在DOS命令方式下,输入以下的命令行。
C> exam file1.c file2.c
执行文件名后面的file1.c和file2.c两个参数被分别放到指针数组argv[1]和argv[2]中,argv[0]的内容为exam,argc的值等于3(因为此命令行共有3个参数)。如果输入的参数少于3个,则程序会输出:“You forgot to enter a filename”。程序执行结果是将file1.c中的信息复制到file2.c中。如前所述,可以用type file1.c和type file2.c命令验证。
最后说明一点:为了书写方便,把fputc和fgetc定义为宏名putc和getc。
#define putc (ch ,fp ) fputc (ch ,fp ) #define getc (fp ) fget (fp )
这在stdio.h中已经定义。用putc和getc,跟用fputc和fget是一样的。一般可以把它们作为相同的函数来对待。
3.二进制文件的操作
现在ANSI C已允许用缓冲文件系统处理二进制文件,而读入某一个字节中的二进制数据的值有可能是-1,而这又恰好是EOF的值。这就出现了需要读入有用数据却被处理为文件结束的情况。为了解决这个问题,ANSI C提供一个feof函数来判断文件是否真的结束。Feof(fp)用来测试fp所指向文件的当前状态是否文件结束。如果是文件结束,函数Feof(fp)的值为1(真),否则为0(假)。
打开一个文件后,如果顺序读入一个二进制文件中的数据,可以用
while ( ! feof ( fp ) ) { c = fgetc ( fp ); …… }
判断读入文件是否结束。当没有遇到文件结束时,feof(fp)的值为0,而!feof(fp)为1,则将读入一个字节的数据赋给整型变量c(当然可以接着对这些数据进行所需处理)。直到遇到文件结束,feof(fp)的值为1,!feof(fp)的值为0,不再执行while循环。这种方法也适用于文本文件。
getc和putc函数可以用来读写文件中的一个字符。
【例22.15】找出下面程序中的错误。
#include <stdio.h> int main (void ) { char ch ,cs[16] , *str="How are you" ; int i=0 ; FILE *fp ; fp=fopen ("ttt.bin" ,"wb" ); // 创建一个二进制只写文件 while (str[i] !='\0' ) { ch=str[i] ; putc (ch ,fp ); i++ ; } fp=fopen ("ttt.bin" ,"rb" ); // 打开一个二进制只读文件 i=0 ; while ( feof (fp ) ) { cs[i] = getc ( fp ); // 读入字符串到字符数组cs i++ ; } cs[i]='\0' ; fclose (fp ); printf ( "%s\n" , cs ); // 输出How are you ? return 0 ; }
【解答】getc和putc函数用来读写文件中的一个字符,是正确的。但while(feof(fp))的用法不对,应改为while(!feof(fp))。修改后读入字符数组的内容为空,这是因为没有关闭写入的文件造成的。要先关闭写入的文件,再重新以只读方式打开即可。另外,在文件最后写入一个字符串结束标志,读文件时就不需要处理了,这样更方便些。
// 改正的程序1 #include <stdio.h> int main (void ) { char ch ,cs[16] , *str="How are you ?" ; int i=0 ; FILE *fp ; fp=fopen ("ttt.bin" ,"wb" ); // 创建一个二进制只写文件 while (str[i] !='\0' ) { ch=str[i] ; putc (ch ,fp ); i++ ; } putc ('\0' ,fp ); // 写入字符串结束标志 fclose (fp ); fp=fopen ("ttt.bin" ,"rb" ); // 打开一个二进制只读文件 i=0 ; while ( !feof (fp ) ) { cs[i] = getc ( fp ); i++ ; } fclose (fp ); printf ( "%s\n" , cs ); return 0 ; }
另一种方法是第1次将文件以"rb+"方式建立一个可以读写的二进制文件。当写完文件后,用rewind(fp)语句将文件指针回到文件开始处,即可读文件。
// 改正的程序2 #include <stdio.h> int main (void ) { char ch ,cs[16] , *str="How are you ?" ; int i=0 ; FILE *fp ; fp=fopen ("ttt.bin" ,"rb+" ); // 创建一个二进制读写文件 while (str[i] !='\0' ) { ch=str[i] ; putc (ch ,fp ); i++ ; } putc ('\0' ,fp ); // 写入字符串结束标志 rewind (fp ); // 恢复到文件起点 i=0 ; while ( !feof (fp ) ) { cs[i] = getc ( fp ); i++ ; } fclose (fp ); printf ( "%s\n" , cs ); return 0 ; }
fread和fwrite函数是按数据块的长度来处理输入输出的,一般用于二进制文件的输入输出,下面是改为使用它们的程序。
// 改正的程序3 #include <stdio.h> #include <string.h> int main (void ) { char cs[16] , *str="How are you ?" ; FILE *fp ; fp=fopen ("ttt.bin" ,"rb+" ); // 创建一个二进制读写文件 fwrite (str ,sizeof (str ),1 ,fp ); rewind (fp ); fread (cs ,sizeof (cs ),1 ,fp ); fclose (fp ); printf ( "%s\n" , cs ); return 0 ; }
如果读文件时使用
fread (cs ,4 ,3 ,fp );
语句,则是分三次依次读入4个字符,没有读入字符结束符,输出时,“?”后面就是乱码。如果读4次,则读入结束符。语句
fread (cs ,8 ,2 ,fp );
也可以保证读入结束符。这跟语句
fread (cs ,sizeof (cs ),1 ,fp );
是等效的。它按cs的长度读到文件结束。cs的长度必须大于str。使用语句
fread (cs ,sizeof (str ),1 ,fp );
是保证读入原来的长度,当cs<str时,会把数据写到紧邻cs存储区后面的空间,如果没有破坏有用数据,程序会正常运行,否则会出现运行时错误。
【例22.16】找出下面程序中的错误。
#include <stdio.h> #include <stdlib.h> #define SIZE 2 struct student_type{ char name[10] ; int num ; int age ; char addr[15] ; }stud[SIZE] ,st[SIZE] ; int main (void ) { FILE *fp ; int i ; for ( i=0 ; i<SIZE ; i++ ) scanf ( "%s %d %d %s" ,stud[i].name ,stud[i].num , stud[i].age ,stud[i].addr ); if ( ( fp=fopen ("stu_list" ,"wb" ) ) == NULL ) { printf ("cannot open file.\n" ); return 1 ; } for ( i=0 ; i<SIZE ; i++ ) fwrite (stud[i] ,sizeof (struct student_type ),1 ,stdin ); fclose (fp ); fp = fopen ( "stu_list" ,"rb" ); for ( i=0 ; i<SIZE ; i++ ) { fread (st[i] ,sizeof ( struct student_type ),1 ,fp ); printf ( "%-10s %4d %4d%15s\n" ,st[i].name ,st[i].num , st[i].age ,st[i].addr ); } fclose (fp ); return 0 ; }
【解答】fread和fwrite函数一般用于二进制文件的输入输出。ANSI C标准提出设置fread和fwrite两个函数,用来读写一个数据块。它们的一般调用形式为:
fread (buffer , size ,count , fp ); fwrite (buffer ,size ,count ,fp );
其中:
buffer:是一个指针。对fread来说,它是读入数据的存放地址。对fwrite来说,是输出数据的地址(以上指的均是起始地址)。
size:表示要读写的数据一个数据项占多少字节。
count:表示要读写多少个数据项。
fp:文件指针。
如果文件以二进制形式打开,用fread和fwrite函数就可以读写任何类型的信息。例如:
fread ( f ,4 ,2 ,fp );
其中f是一个实型数组名。一个实型变量占4个字节。这个函数从fp所指向的文件读入两次(每次4个字节)数据,存储到数组f中。
如果有一个如下的结构类型:
struct student_type{ char name [10 ]; int num ; int age ; char addr [30 ]; }stud [40 ];
结构数组stud有40个元素,每一个元素用来存放一个学生的数据(包括姓名、学号、年龄、地址)。因为stud是结构数组,所以每次是读取stud的1个数组元素。stud是整个结构数组的存储首地址,这里要求的是指针,对数组元素来讲,必须使用“&”,即&stud[i]表示结构数组的每个元素的存储地址。假设学生的数据已存放在磁盘文件中,可以用下面的for语句和fread函数读入40个学生的数据。
for ( i=0 ; i<40 ; i++ ) fread ( &stud [i ],sizeof (struct student_type ),1 ,fp );
下面的for语句和fwrite函数可以将内存中的学生数据输出到磁盘文件中去,同样道理,这里也必须使用&号。
for ( i=0 ; i<40 ; i++ ) fwrite ( &stud [i ],sizeof (struct student_type ),1 ,fp );
如果fread或fwrite调用成功,则函数返回值为count的值,即输入或输出数据项的完整个数。
程序中除了这两句漏掉“&”号之外,scanf语句也有错误。num和age是整数,也必须使用“&”号才行。下面是一个完整的程序,其中增加必要的错误处理,并将打印放在关闭文件之后。
// 完整程序 #include <stdio.h> #include <stdlib.h> #define SIZE 40 struct student_type{ char name[10] ; int num ; int age ; char addr[15] ; }stud[SIZE] ,st[SIZE] ; int main (void ) { FILE *fp ; int i ; for ( i=0 ; i<SIZE ; i++ ) scanf ( "%s %d %d %s" ,stud[i].name ,&stud[i].num , &stud[i].age ,stud[i].addr ); if ( ( fp=fopen ("stu_list" ,"wb" ) ) == NULL ) { printf ("cannot open file.\n" ); return 1 ; } for ( i=0 ; i<SIZE ; i++ ) if ( fwrite (&stud[i] ,sizeof (struct student_type ),1 ,fp )!=1 ) printf ( " file write error.\n" ); fclose (fp ); fp = fopen ( "stu_list" ,"rb" ); for ( i=0 ; i<SIZE ; i++ ) { if (fread (&st[i] ,sizeof (struct student_type ),1 ,fp )!=1 ) { if (feof (fp ) ) return 0 ; printf ( " file read error\n" ); } } fclose (fp ); for ( i=0 ; i<SIZE ; i++ ) printf ( "%-10s %4d %4d%15s\n" ,st[i].name ,st[i].num , st[i].age ,st[i].addr ); return 0 ; }
下面是将SIZE定义为2进行验证的运行示范。
李萍 1001 19 8-201 张明 1003 25 8-305 李萍 1001 19 8-201 张明 1003 25 8-305
由于ANSI C标准决定不采用非缓冲输出系统,所以在缓冲系统中增加了fread和fwrite两个函数,用来读写一个数据块。有些目前使用的C编译不具备这两个函数,请读者注意。
4.fprintf和fscanf函数
【例22.17】下面程序给出一个奇怪的输出,找出并改正错误。
#include <stdio.h> int main (void ) { char cs[16] , *str="How are you ?" ; char cs1[5] ,cs2[5] ; int i=3 ,j=0 ; float f1=4.56f ,f2=0 ; FILE *fp ; fp=fopen ("ttt.bin" ,"wb" ); // 创建一个二进制只写文件 fprintf ( fp ,"%s %d%f" ,str ,i ,f1 ); fclose (fp ); fp=fopen ("ttt.bin" ,"rb" ); // 打开一个二进制只读文件 fscanf ( fp ,"%s%s%s%d%f" ,cs ,cs1 ,cs2 ,&j ,&f2 ); fclose (fp ); printf ( "%d %f\n" , j ,f2 ); printf ( "%s %s %s\n" , cs ,cs1 ,cs2 ); return 0 ; }
【解答】fprintf函数、fscanf函数与printf函数、scanf函数的作用相仿,都是格式化读写函数。只有一点不同:fprint函数和fscanf函数的读写对象不是终端而是磁盘文件。它们的一般调用方式为:
fprintf (文件指针,格式字符串,输出列表); fscanf (文件指针,格式字符串,输入列表);
语句
fprintf ( fp ,"%s %d%f" ,str ,i ,f1 );
把字符串str写入用的“%s”格式,它与变量i用空格隔开,但i和f1是“%d%f”格式,所以写入的内容为“How are you?34.560000”。因为在读文件时,是以空格为分隔符的,所以要读三次才能把原来的字符串内容读完。读整数则读入34,实数则为0.560000。
最简单的解决办法就是使用空格符分割,即
fprintf ( fp ,"%s %d %f" ,str ,i ,f1 );
合理使用宽度修饰符也能解决这个问题,即选用的宽度要保证最左边有一空格,这样即可保证写入数据之间留有分隔符。例如在本例中,语句
fprintf ( fp ,"%s %d%6.3f" ,str ,i ,f1 );
可以得到正确结果,如选%5.3f,则不能区分整数3和实数4.56。
同样,用以下语句
fscanf ( fp ,"%s%s%s%d%f" ,cs ,cs1 ,cs2 ,&j ,&f2 );
从磁盘文件上读入ASCII字符。格式符之间无需使用空格,“&”的使用方法与scanf一样,不再赘述。另外,文件既可以是文本文件,也可以是二进制文件。
【例22.18】下面程序输出2014-5-122014-5-132014-5-14,请将三个日期分开以便分辨。
#include <stdio.h> void print_msg_one ( FILE * , const char msg ); void print_msg (FILE * , char str ); int main (void ) { char str[32] ; FILE *fp ; fp=fopen ("log.txt" ,"w" ); print_msg_one (fp ,"2014-5-12" ); print_msg_one (fp ,"2014-5-13" ); print_msg_one (fp ,"2014-5-14" ); fclose (fp ); fp=fopen ("log.txt" ,"r" ); print_msg (fp , str ); fclose (fp ); return 0 ; } void print_msg_one ( FILE *fp , const char msg ) { fprintf ( fp ,"%s" ,msg ); } void print_msg (FILE *fp , char str ) { fscanf ( fp ,"%s" , str ); printf ( "%s\n" , str ); }
【解答】写入文件的三个数据之间没有分隔符。如果用空格做分隔符,读取文件时必须知道有几个字符串。设计一个全局变量num来计算字符串数目,输出时每次读取一个字串。
// 改正的程序 #include <stdio.h> void print_msg_one ( FILE * , const char msg ); void print_msg (FILE * , char str ); int num=0 ; // 字符串计数 int main (void ) { char str[16] ; FILE *fp ; fp=fopen ("log.txt" ,"w" ); print_msg_one (fp ,"2014-5-12 " ); // 字符串后面增加空格 print_msg_one (fp ,"2014-5-13 " ); print_msg_one (fp ,"2014-5-14 " ); fclose (fp ); fp=fopen ("log.txt" ,"r" ); print_msg (fp , str ); fclose (fp ); return 0 ; } void print_msg_one (FILE *fp , const char msg ) { fprintf ( fp ,"%s" ,msg ); num++ ; } void print_msg (FILE *fp , char str ) { int i=0 ; for (i=0 ;i<num ;i++ ) { fscanf ( fp ,"%s" , str ); printf ( "%s " , st r ); } putchar ('\n' ); }
用fprintf和fscanf函数对磁盘文件进行读写,使用方便,容易理解,但由于在输入时要将ASCII码转换为二进制形式,在输出时又要将二进制形式转换成字符,花费时间比较多。因此,在内存与磁盘频繁交换数据的情况下,建议最好不使用fprint和fscanf函数,而应该使用fread和fwrite函数。