c-programming-chap10

对文件的输入输出

To understand a program you must become both the machine and the program. [1]

[TOC]

C文件的有关基本知识

什么是文件?

我们对文件的概念已经非常熟悉了,比如常见的 Word 文档、txt 文件、源文件等。文件是数据源的一种,最主要的作用是保存数据。

文件(file)一般指存储在外部介质上数据的集合。 操作系统是以文件为单位对数据进行管理的。

输入输出是数据传送的过程,数据如流水一样从一处流向另一处,因此常将输入输出形象地称为流(stream),即数据流。流表示了信息从源到目的端的流动。在输入操作时,数据从文件流向计算机内存,在输出操作时,数据从计算机流向文件(如打印机、磁盘文件)。

文件名

一个文件要有一个唯一的文件标识,以便用户识别和引用。
文件标识包括3部分: (1)文件路径; (2)文件名主干; (3)文件后缀。

D:\CC\temp\file1.dat

  1. 文件路径表示文件在外部存储设备中的位置。
  2. 文件名主干的命名规则遵循标识符的命名规则。
  3. 文件后缀用来表示文件的性质。

文件的分类

ASCII文件

如果要求在外存上以ASCII代码形式存储,则需要在存储前进行转换。
ASCII文件又称文本文件(text file),每一个字节存放一个字符的ASCII代码。

二进制文件

数据在内存中是以二进制形式存储的,如果不加转换地输出到外存,就是二进制文件,
可以认为它就是存储在内存的数据的映像,所以也称之为映像文件(image file)。

字符一律以ASCII形式存储,数值型数据既可以用ASCII形式存储,也可以用二进制形式存储。

文件缓冲区

所谓缓冲文件系统是指系统自动地在内存区为程序中每一个正在使用的文件开辟一个文件缓冲区。从内存向磁盘输出数据必须先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘去。如果从磁盘向计算机读入数据,则一次从磁盘文件将一批数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(给程序变量)。这样做是为了节省存取时间,提高效率,缓冲区的大小由各个具体的C编译系统确定。
缓冲文件系统

每一个文件在内存中只有一个缓冲区,在向文件输出数据时,它就作为输出缓冲区,在从文件输入数据时,它就作为输入缓冲区。

文件类型指针

缓冲文件系统中,关键的概念是 “文件类型指针”,简称 “文件指针”。每个被使用的文件都在内存中开辟一个相应的文件信息区,用来存放文件的有关信息(如文件的名字、文件状态及文件当前位置等)。这些信息是保存在一个结构体变量中的。该结构体类型是由系统声明的,取名为FILE

定义文件类型指针
1
2
FILE *fp;
//定义一个指向FILE类型数据的指针变量

可以使fp指向某一个文件的文件信息区(是一个结构体变量),通过该文件信息区中的信息就能够访问该文件。也就是说,通过文件指针变量能够找到与它关联的文件。 如果有n个文件,应设n个指针变量,分别指向n个FILE类型变量,以实现对n个文件的访问。为方便起见,通常将这种指向文件信息区的指针变量简称为指向文件的指针变量
指向文件的指针变量

注意:指向文件的指针变量并不是指向外部介质上的数据文件的开头,而是指向内存中的文件信息区的开头。

打开与关闭文件

对文件读写之前应该“打开”该文件,在使用结束之后应“关闭”该文件。

所谓“打开”是指为文件建立相应的信息区(用来存放有关文件的信息)和文件缓冲区(用来暂时存放输入输出的数据)。

在编写程序时,在打开文件的同时,一般都指定一个指针变量指向该文件,也就是建立起指针变量与文件之间的联系,这样,就可以通过该指针变量对文件进行读写了。

所谓“关闭”是指撤销文件信息区和文件缓冲区,使文件指针变量不再指向该文件,显然就无法进行对文件的读写了。

用fopen函数打开数据文件

fopen(文件名,使用文件方式);

1
2
3
4
5
FILE*fp;				//定义一个指向文件的指针变量fp
fp=fopen(″a1″,″r″); //将fopen函数的返回值赋给指针变量fp
//表示以“读入”方式打开名字为a1的文件


在打开一个文件时,通知编译系统以下3个信息:
① 需要打开文件的名字,也就是准备访问的文件的名字
② 使用文件的方式(“读”还是“写”等)
③ 让哪一个指针变量指向被打开的文件


使用文件方式

(1) 用“r”方式打开的文件只能用于向计算机输入而不能用作向该文件输出数据,而且该文件应该已经存在,并存有数据,这样程序才能从文件中读数据。不能用“r”方式打开一个并不存在的文件,否则出错。

(2) 用“w”方式打开的文件只能用于向该文件写数据(即输出文件),而不能用来向计算机输入。如果原来不存在该文件,则在打开文件前新建立一个以指定的名字命名的文件。如果原来已存在一个以该文件名命名的文件,则在打开文件前先将该文件删去,然后重新建立一个新文件。

(3) 如果希望向文件末尾添加新的数据(不希望删除原有数据),则应该用“a”方式打开。但此时应保证该文件已存在;否则将得到出错信息。在每个数据文件中自动设置了一个隐式的“文件读写位置标记”,它指向的位置就是当前进行读写的位置。如果“文件读写位置标记”在文件开头,则下一次的读写就是文件开头的数据。然后“文件读写位置标记”自动移到下一个读写位置,以便读写下一个数据。以添加方式打开文件时,文件读写位置标记移到文件末尾。

(4) 用“r+”“w+”“a+”方式打开的文件既可用来输入数据,也可用来输出数据。

(5) 如果不能实现“打开”的任务,fopen函数将会带回一个空指针值NULL。

1
2
3
4
if ((fp=fopen(″file1″,″r″))==NULL)
{ printf(″cannot open this file\n″);
exit(0);
}

(6) C标准建议用表10.1列出的文件使用方式打开文本文件或二进制文件,但目前使用的有些C编译系统可能不完全提供所有这些功能,需要注意所用系统的规定。

(7) 有12种文件使用方式,其中有6种是在第一个字母后面加了字母b的(如rb,wb,ab,rb+,wb+,ab+),b表示二进制方式。其实,带b和不带b只有一个区别,即对换行的处理。由于在C语言用一个′\n′即可实现换行,而在Windows系统中为实现换行必须要用 “回车”和“换行”两个字符,即′\r′和′\n′。因此,如果使用的是文本文件并且用“w”方式打开,在向文件输出时,遇到换行符′\n′时,系统就把它转换为′\r′和′\n′两个字符,否则在Windows系统中查看文件时,各行连成一片,无法阅读。同样,如果有文本文件且用“r”方式打开,从文件读入时,遇到′\r′和′\n′两个连续的字符,就把它们转换为′\n′一个字符。如果使用的是二进制文件,在向文件读写时,不需要这种转换。加b表示使用的是二进制文件,系统就不进行转换。

(8) 如果用“wb”的文件使用方式,并不意味着在文件输出时把内存中按ASCII形式保存的数据自动转换成二进制形式存储。输出的数据形式是由程序中采用什么读写语句决定的。例如,用fscanf和fprintf函数是按ASCII方式进行输入输出,而fread和fwrite函数是按二进制进行输入输出。

(9) 程序中可以使用3个标准的流文件——标准输入流、标准输出流和标准出错输出流。系统已对这3个文件指定了与终端的对应关系。标准输入流是从终端的输入,标准输出流是向终端的输出,标准出错输出流是当程序出错时将出错信息发送到终端。程序开始运行时系统自动打开这3个标准流文件。

用fclose函数关闭数据文件

1
2
fclose(文件指针);
fclose(fp);

在使用完一个文件后应该关闭它,以防止它再被误用。“关闭”就是撤销文件信息区和文件缓冲区,使文件指针变量不再指向该文件,也就是文件指针变量与文件“脱钩”, 此后不能再通过该指针对原来与其相联系的文件进行读写操作,除非再次打开,使该指针变量重新指向该文件。

如果不关闭文件就结束程序运行将会丢失数据。因为,在向文件写数据时,是先将数据输出到缓冲区,待缓冲区充满后才正式输出给文件。如果当数据未充满缓冲区时程序结束运行,就有可能使缓冲区中的数据丢失。用fclose函数关闭文件时,先把缓冲区中的数据输出到磁盘文件,然后才撤销文件信息区。有的编译系统在程序结朿前会自动先将缓冲区中的数据写到文件,从而避免了这个问题,但还是应当养成在程序终止之前关闭所有文件的习惯。

fclose函数也带回一个值,当成功地执行了关闭操作,则返回值为0;否则返回EOF(-1)。

顺序读写数据文件

读写一个字符的函数

 读写一个字符的函数

fgetc的第1个字母f代表文件(file),中间的get表示“获取”,最后一个字母c表示字符(character),fgetc的含义很清楚: 从文件读取一个字符。fputc也类似。

示例1

从键盘输入一些字符,并逐个把它们送到磁盘上去,直到用户输入一个“#”为止。

用来存储数据的文件名可以在fopen函数中直接写成字符串常量形式 ,也可以在程序运行时由用户临时指定。

用fopen函数打开一个“只写”的文件(“w”表示只能写入不能从中读数据),若成功,函数返回该文件所建立的信息区的起始地址给文件指针变量fp。若失败,则显示“无法打开此文件”,用exit函数终止程序运行,此函数在stdlib.h头文件中。

用getchar函数接收用户从键盘输入的字符。注意每次只能接收一个字符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>
#include <stdlib.h>
int main()
{ FILE *fp; //定义文件指针fp
char ch,filename[10];
printf("请输入所用的文件名: ");
scanf("%s",filename); //输入文件名
getchar(); //用来消化最后输入的回车符
if((fp=fopen(filename,"w"))==NULL) //打开输出文件并使fp指向此文件
{ printf("cannot open file\n"); //如果打开出错就输出“打不开”
exit(0); //终止程序
}
printf("请输入一个准备存储到磁盘的字符串(以#结束): ");
ch=getchar(); //接收从键盘输入的第一个字符
while(ch!='#') //当输入′#′时结束循环
{ fputc(ch,fp); //向磁盘文件输出一个字符
putchar(ch); //将输出的字符显示在屏幕上
ch=getchar(); //再接收从键盘输入的一个字符
}
fclose(fp); //关闭文件
putchar(10); //向屏幕输出一个换行符
return 0;
}

示例2

将一个磁盘文件中的信息复制到另一个磁盘文件中。今要求将上例建立的file1.dat文件中的内容复制到另一个磁盘文件file2.dat中。

在访问磁盘文件时,是逐个字符(字节)进行的,为了知道当前访问到第几个字节,系统用“文件读写位置标记”来表示当前所访问的位置。开始时“文件读写位置标记”指向第1个字节,每访问完一个字节后,当前读写位置就指向下一个字节,即当前读写位置自动后移。

为了知道对文件的读写是否完成,只须看文件读写位置是否移到文件的末尾。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <stdio.h>
#include <stdlib.h>
int main()
{ FILE *in,*out; //定义指向FILE类型文件的指针变量
char ch,infile[10],outfile[10]; //定义两个字符数组,分别存放两个数据文件名
printf("输入读入文件的名字:");
scanf("%s",infile); //输入一个输入文件的名字
printf("输入输出文件的名字:");
scanf("%s",outfile); //输入一个输出文件的名字
if((in=fopen(infile,"r"))==NULL) //打开输入文件
{ printf("无法打开此文件\n"); exit(0); }
if((out=fopen(outfile,"w"))==NULL) //打开输出文件
{ printf("无法打开此文件\n"); exit(0); }
ch=fgetc(in); //从输入文件读入一个字符,赋给变量ch
while(!feof(in)) //如果未遇到输入文件的结束标志
{ fputc(ch,out); //将ch写到输出文件
putchar(ch); //将ch显示到屏幕上
ch=fgetc(in); //再从输入文件读入一个字符,赋给变量ch
}
putchar(10); //显示完全部字符后换行
fclose(in); //关闭输入文件
fclose(out); //关闭输出文件
return 0;
}

向文件读写一个字符串

文件读写一个字符串

fgets中最后一个字母s表示字符串(string)。见名知义,fgets的含义是: 从文件读取一个字符串。

fgets()函数

fgets函数的函数原型为:

1
char *fgets(char*str, int n, FILE*fp);

从文件读入一个字符串,调用时可以写成下面的形式:

1
fgets(str,n,fp);

其中,n是要求得到的字符个数,但实际上只从fp所指向的文件中读入n-1个字符,然后在最后加一个′\0′字符,这样得到的字符串共有n个字符,把它们放到字符数组str中。如果在读完n-1个字符之前遇到换行符“\n”或文件结束符EOF,读入即结束,但将所遇到的换行符“\n”也作为一个字符读入。若执行fgets函数成功,则返回值为str数组首元素的地址,如果一开始就遇到文件尾或读数据出错,则返回NULL。

fputs()函数

fputs函数的函数原型为:

1
int fputs (char *str, FILE *fp);

其作用是将str所指向的字符串输出到fp所指向的文件中。调用时可以写成:

1
2
fputs("China",fp); 

把字符串″China″输出到fp指向的文件中。fputs函数中第一个参数可以是字符串常量、字符数组名或字符型指针。字符串末尾的′\0′不输出。若输出成功,函数值为0;失败时,函数值为EOF(即-1)。


fgets和fgets这两个函数的功能类似于gets和puts函数,只是gets和puts以终端为读写对象,而fgets和fputs函数以指定的文件作为读写对象。

实例1

【教材例10.3】从键盘读入若干个字符串,对它们按字母大小的顺序排序,然后把排好序的字符串送到磁盘文件中保存。

文件读写小结

  1. 数据的存储方式
  • 文本方式: 数据以字符方式(ASCII代码)存储到文件中。如整数12,送到文件时占2个字节,而不是4个字节。以文本方式保存的数据便于阅读。
  • 二进制方式: 数据按在内存的存储状态原封不动地复制到文件。如整数12,送到文件时和在内存中一样占4个字节。
  1. 文件的分类
  • 文本文件(ASCII文件): 文件中全部为ASCII字符。
  • 二进制文件: 按二进制方式把在内存中的数据复制到文件的,称为二进制文件,即映像文件。
  1. 文件的打开方式
  • 文本方式: 不带b的方式,读写文件时对换行符进行转换。
  • 二进制方式: 带b的方式,读写文件时对换行符不进行转换。
  1. 文件读写函数
  • 文本读写函数: 用来向文本文件读写字符数据的函数(如fgetc,fgets,fputc,fputs,fscanf,fprintf等)。
  • 二进制读写函数: 用来向二进制文件读写二进制数据的函数(如getw,putw,fread,fwrite等)。

随机读写数据文件

对文件进行顺序读写比较容易理解,也容易操作,但有时效率不高。随机访问不是按数据在文件中的物理位置次序进行读写,而是可以对任何位置上的数据进行访问,显然这种方法比顺序访问效率高得多。

文件位置标记及其定位

  1. 用rewind函数使文件位置标记指向文件开头
  2. 用fseek函数改变文件位置标记
  3. 用ftell函数测定文件位置标记的当前位置

实例

教材10.5、10.6

文件读写的出错检测

  1. ferror函数
  • 在调用各种输入输出函数(如:putc,getc,fread,fwrite等)时,如果出现错误,除了函数返回值有所反映外,还可以用ferror函数检查。

  • 如果ferror返回值为0(假),表示未出错;
    如果返回一个非零值,表示出错。

  1. clearerr函数
  • clearerr的作用是使文件出错标志和文件结束标志置为0。假设在调用一个输入输出函数时出现错误,ferror函数值为一个非零值。

  • 应该立即调用clearerr(fp),使ferror(fp)的值变成0,以便再进行下一次的检测。

  • 只要出现文件读写出错标志,它就一直保留,直到对同一文件调用clearerr函数或rewind函数,或任何其他一个输入输出函数。

参考资料:cppreference.com《C语言程序设计:现代方法》

  1. 要理解一段程序,你得同时成为机器和这段程序。 Epigrams on Programming 编程警句

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!