4、读写二进制文件
C语言还提供了用于整块数据的读写函数。可用来读写一组数据,如一个数组元素,一个结构变量的值等。
读数据块函数调用的一般形式为:
fread(buffer,size,count,fp);
写数据块函数调用的一般形式为:
fwrite(buffer,size,count,fp);
其中:
buffer:是一个指针,在fread函数中,它表示存放输入数据的首地址。在fwrite函数中,它表示存放输出数据的首地址。
size:表示数据块的字节数。
count:表示要读写的数据块块数。
fp:表示文件指针。
例如:
fread(fa,4,5,fp);
其意义是从fp所指的文件中,每次读4个字节(一个实数)送入实数组fa中,连续读5次,即读5个实数到fa中。
从键盘输入两个学生数据,写入一个文件中,再读出这两个学生的数据显示在屏幕上。
struct stu{
char name[10];
int num;
int age;
char addr[15];
}boya[2],boyb[2],*pp,*qq;
int main(void)
{
FILE *fp;
char ch;
int i;
pp=boya;
qq=boyb;
if((fp=fopen("~/stu_list","wb+"))==NULL){
printf("Cannot open file strike any key exit!");
getch();
exit(1);
}
printf("input data\n");
for(i=0;i<2;i++,pp++)
scanf("%s%d%d%s",pp->name,&pp->num,&pp->age,pp->addr);
pp=boya;
fwrite(pp,sizeof(struct stu),2,fp);
rewind(fp);
fread(qq,sizeof(struct stu),2,fp);
printf("\n\nname\tnumber age addr\n");
for(i=0;i<2;i++,qq++)
printf("%s\t%5d%7d %s\n",qq->name,qq->num,qq->age,qq->addr);
fclose(fp);
}
本例程序定义了一个结构stu,说明了两个结构数组boya和boyb以及两个结构指针变量pp和qq。pp指向boya,qq指向boyb。程序第14行以读写方式打开二进制文件“stu_list”,输入二个学生数据之后,写入该文件中,然后把文件内部位置指针移到文件首,读出两块学生数据后,在屏幕上显示。
5、文件读写位置控制
前面介绍的对文件的读写方式都是顺序读写,即读写文件只能从头开始,顺序读写各个数据。但在实际问题中常要求只读写文件中某一指定的部分。为了解决这个问题可移动文件内部的位置指针到需要读写的位置,再进行读写,这种读写称为随机读写。
实现随机读写的关键是要按要求移动位置指针,这称为文件的定位。
移动文件内部位置指针的函数主要有两个,即rewind()和fseek()。
rewind函数的调用形式为:
rewind(文件指针);
它的功能是把文件内部的位置指针移到文件首。
下面主要介绍fseek函数。fseek函数用来移动文件内部位置指针,其调用形式为:
fseek(文件指针,位移量,起始点);
其中:
“文件指针”指向被移动的文件。
“位移量”表示移动的字节数,要求位移量是long型数据,以便在文件长度大于64KB 时不会出错。当用常量表示位移量时,要求加后缀“L”。
“起始点”表示从何处开始计算位移量,规定的起始点有三种:文件首,当前位置和文件尾。
其表示方法如下表:
起始点 表示符号 数字表示
文件首 SEEK_SET 0
当前位置 SEEK_CUR 1
文件末尾 SEEK_END 2
例如:
fseek(fp,100L,0);
其意义是把位置指针移到离文件首100个字节处。
还要说明的是fseek函数一般用于二进制文件。在文本文件中由于要进行转换,故往往计算的位置会出现错误。
文件的随机读写
在移动位置指针之后,即可用前面介绍的任一种读写函数进行读写。由于一般是读写一个数据据块,因此常用fread和fwrite函数。下面用例题来说明文件的随机读写。
struct stu{
char name[10];
int num;
int age;
char addr[15];
}boy,*qq;
main(){
FILE *fp;
char ch;
int i=1;
qq=&boy;
if((fp=fopen("~/stu_list","rb"))==NULL){
printf("Cannot open file strike any key exit!");
getch();
exit(1);
}
rewind(fp);
fseek(fp,i*sizeof(struct stu),0);
fread(qq,sizeof(struct stu),1,fp);
printf("\n\nname\tnumber age addr\n");
printf("%s\t%5d %7d %s\n",qq->name,qq->num,qq->age,qq->addr);
}
文件stu_list已由上个例子的程序建立,本程序用随机读出的方法读出第二个学生的数据。程序中定义boy为stu类型变量,qq为指向boy的指针。以读二进制文件方式打开文件,程序第19行移动文件位置指针。其中的i值为1,表示从文件头开始,移动一个stu类型的长度,然后再读出的数据即为第二个学生的数据。
6、错误处理
1) 文件结束检测函数feof函数
调用格式:
feof(文件指针);
功能:判断文件是否处于文件结束位置,如文件结束,则返回值为1,否则为0。
int main(void)
{
FILE* fp;
fp=fopen("file.bin","r");
fgetc(fp);
if(feof(fp))
printf("Wehave reached end of file\n");
fclose(fp);
return0;
}
feof(fp)有两个返回值:如果遇到文件结束,函数feof(fp)的值为非零值,否则为0。
EOF是文本文件结束的标志。在文本文件中,数据是以字符的ASCⅡ代码值的形式存放,普通字符的ASCⅡ代码的范围是32到127(十进制),EOF的16进制代码为0x1A(十进制为26),因此可以用EOF作为文件结束标志。[1]
当把数据以二进制形式存放到文件中时,就会有-1值的出现,因此不能采用EOF作为二进制文件的结束标志。为解决这一个问题,ASCI C提供一个feof函数,用来判断文件是否结束。feof函数既可用以判断二进制文件又可用以判断文本文件。
C语言的“feof()”函数和数据库中“eof()”函数的运作是完全不同的。数据库中“eof()”函数读取当前指针的位置,“C”语言的“feof()”函数返回的是最后一次“读操作的内容”。多年来把“位置和内容”相混,从而造成了对这一概念的似是而非。
那么,位置和内容到底有何不同呢?举个简单的例子,比如有人说“你走到火车的最后一节车箱”这就是位置。而如果说“请你一直向后走,摸到铁轨结束”这就是内容。也就是说用内容来判断会“多走一节”。这就是完全依赖于“while(!feof(FP)){...}”进行文件复制时,目标文档总会比源文档“多出一些”的原因。
2) 读写文件出错检测函数
ferror函数调用格式:
ferror(文件指针);
在调用各种输入输出函数(如 putc.getc.fread.fwrite等)时,如果出现错误,除了函数返回值有所反映外,还可以用ferror函数检查。它的一般调用形式为 ferror(fp);如果ferror返回值为0(假),表示未出错。如果返回一个非零值,表示出错。应该注意,对同一个文件每一次调用输入输出函数,均产生一个新的ferror函数值,因此,应当在调用一个输入输出函数后立即检查ferror函数的值,否则信息会丢失。在执行fopen函数时,ferror函数的初始值自动置为0。
3) 文件出错标志和文件结束标志置0函数
clearerr函数调用格式:
clearerr(文件指针);
clearerr的作用是使文件错误标志和文件结束标志置为0。假设在调用一个输入输出函数时出现了错误,ferror函数值为一个非零值。在调用clearerr(fp)后,ferror(fp)的值变为0。
只要出现错误标志,就一直保留,直到对同一文件调用clearerr函数或rewind函数,或任何一个输入输出函数。
int main(void)
{
FILE*fp;
charch;
fp=fopen("file.bin","w");
ch=fgetc(fp);
printf("%c\n",ch);
if(ferror(fp))
{
printf("Errorreading from file.bin\n");
clearerr(fp);
}
fclose(fp);
return0;
}
C标准库的I/O缓冲区
用户程序调用C标准I/O库函数读写文件或设备,而这些库函数要通过系统调用把读写请求传给内核,最终由内核驱动磁盘或设备完成I/O操作。C标准库为每个打开的文件分配一个I/O缓冲区以加速读写操作,通过文件的FILE结构体可以找到这个缓冲区,用户调用读写函数大多数时候都在I/O缓冲区中读写,只有少数时候需要把读写请求传给内核。以fgetc/fputc为例,当用户程序第一次调用fgetc读一个字节时,fgetc函数可能通过系统调用进入内核读1K字节到I/O缓冲区中,然后返回I/O缓冲区中的第一个字节给用户,把读写位置指向I/O缓冲区中的第二个字符,以后用户再调fgetc,就直接从I/O缓冲区中读取,而不需要进内核了,当用户把这1K字节都读完之后,再次调用fgetc时,fgetc函数会再次进入内核读1K字节到I/O缓冲区中。
C标准库之所以会从内核预读一些数据放在I/O缓冲区中,是希望用户程序随后要用到这些数据,C标准库的I/O缓冲区也在用户空间,直接从用户空间读取数据比进内核读数据要快得多。另一方面,用户程序调用fputc通常只是写到I/O缓冲区中,这样fputc函数可以很快地返回,如果I/O缓冲区写满了,fputc就通过系统调用把I/O缓冲区中的数据传给内核,内核最终把数据写回磁盘。有时候用户程序希望把I/O缓冲区中的数据立刻传给内核,让内核写回设备,这称为Flush操作,对应的库函数是fflush,fclose函数在关闭文件之前也会做Flush操作。
C标准库的I/O缓冲区有三种类型:全缓冲、行缓冲和无缓冲。当用户程序调用库函数做写操作时,不同类型的缓冲区具有不同的特性。
全缓冲:如果缓冲区写满了就写回内核。常规文件通常是全缓冲的。
行缓冲:如果用户程序写的数据中有换行符就把这一行写回内核,或者如果缓冲区写满了就写回内核。标准输入和标准输出对应终端设备时通常是行缓冲的。
无缓冲:用户程序每次调库函数做写操作都要通过系统调用写回内核。标准错误输出通常是无缓冲的,这样用户程序产生的错误信息可以尽快输出到设备。
下面通过一个简单的例子证明标准输出对应终端设备时是行缓冲的。
int main()
{
printf("helloworld");
while(1);
return0;
}
运行这个程序,会发现hello world并没有打印到屏幕上。用Ctrl-C终止它,去掉程序中的while(1);语句再试一次:
$ ./a.out
hello world$
hello world被打印到屏幕上,后面直接跟Shell提示符,中间没有换行。
我们知道main函数被启动代码这样调用:exit(main(argc, argv));。main函数return时启动代码会调用exit,exit函数首先关闭所有尚未关闭的FILE*指针(关闭之前要做Flush操作),然后通过_exit系统调用进入内核退出当前进程。
在上面的例子中,由于标准输出是行缓冲的,printf("hello world");打印的字符串中没有换行符,所以只把字符串写到标准输出的I/O缓冲区中而没有写回内核(写到终端设备),如果敲Ctrl-C,进程是异常终止的,并没有调用exit,也就没有机会Flush I/O缓冲区,因此字符串最终没有打印到屏幕上。如果把打印语句改成printf("hello world\n");,有换行符,就会立刻写到终端设备,或者如果把while(1);去掉也可以写到终端设备,因为程序退出时会调用exitFlush所有I/O缓冲区。在本书的其它例子中,printf打印的字符串末尾都有换行符,以保证字符串在printf调用结束时就写到终端设备。
我们再做个实验,在程序中直接调用_exit退出。
int main()
{
printf("helloworld");
_exit(0);
}
结果也不会把字符串打印到屏幕上,如果把_exit调用改成exit就可以打印到屏幕上。
除了写满缓冲区、写入换行符之外,行缓冲还有一种情况会自动做Flush操作。如果:用户程序调用库函数从无缓冲的文件中读取或者从行缓冲的文件中读取,并且这次读操作会引发系统调用从内核读取数据。那么在读取之前会自动Flush所有行缓冲。例如:
int main(void)
{
charbuf[20];
printf("Pleaseinput a line: ");
fgets(buf,20, stdin);
return0;
}
虽然调用printf并不会把字符串写到设备,但紧接着调用fgets读一个行缓冲的文件(标准输入),在读取之前会自动Flush所有行缓冲,包括标准输出。
如果用户程序不想完全依赖于自动的Flush操作,可以调fflush函数手动做Flush操作。
int fflush(FILE *stream);
返回值:成功返回0,出错返回EOF并设置errno对前面的例子再稍加改动:
int main()
{
printf("helloworld");
fflush(stdout);
while(1);
}
虽然字符串中没有换行,但用户程序调用fflush强制写回内核,因此也能在屏幕上打印出字符串。fflush函数用于确保数据写回了内核,以免进程异常终止时丢失数据。作为一个特例,调用fflush(NULL)可以对所有打开文件的I/O缓冲区做Flush操作。