10.1 C文件的有关概念

文件是什么?

文件(file)有不同的类型,在进行C语言程序设计中,主要用到两种文件:

(1)程序文件。包括源程序文件(后级为.c)、目标文件(后级为.obj)、可执行文件(后缀为.exe)等。这种文件是用来存放程序的,以便实现程序的功能。

(2)数据文件。文件的内容不是程序,而是供程序运行时读写的数据,如在程序运行过程中输出到磁盘(或其他外部设备)的数据,或供程序运行时读人内存的数据。如一批学生的成绩数据,或货物交易的数据等,本章讨论的是数据文件。

文件是程序设计中一个重要的概念。所谓“文件”一般指存储在外部介质上数据的集合。一批数据是以文件的形式存放在外部介质(如磁盘)上的。

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

C语言把文件看作是一个字符(字节)的序列,即由一个一个字符(字节)的数据顺序组成。一个输入输出流就是一个字节流或二进制流。在C文件中,数据由一连串的字符(字节)组成,中间没有分隔符,对文件的存取是以字符(字节)为单位的,可以从文件读取一个字符或向文件输出一个字符。输人输出数据流的开始和结束仅受程序控制而不受物理符号(如回车换行符)控制,这就增加了处理的灵活性。这种文件称为流式文件。

文件名是什么?

一个文件要有一个唯一的文件标识,以便用户识别和引用。

文件标识包括三部分:(1)文件路径: (2)文件名主干; (3)文件后缀。

文件路径表示文件在外部存储设备中的位置。如:

后缀用来表示文件的性质,一般不超过3个字母,如:

.doc (Word生成的文件),.txt (文本文件),.dat (数据文件),.c (C语言源程序文件),.cpp (C++源程序文件),.for (FORTRAN 语言源程序文件),.pas (Pascal语言源程序文件),.obj(目标文件),.exe (可执行文件),.ppt(电子幻灯文件),.bmp (图形文件),.jpg(图像文件)等。 

数据文件可分为ASCII文件二进制文件

ASCII文件又称文本(text)文件,它的每一字节放一个字符的ASCII代码。

二进制文件是把内存中的数据按其在内存中的存储形式原样输出到磁盘上存放。

字符型数据只能以ASCII形式存储,数值型数据既可以用ASCII形式存储在磁盘上,也可以用二进制形式存储。

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

下面定义一个指向文件型数据的指针变量

FILE   * fp;

定义fp为一个指向FILE类型变量的指针变量。可以使fp指向某一个文件的文件信息区(是一个结构体变量),通过该文件信息区中的信息能够访问该文件。也就是说,通过文件指针变量能够找到与它相关的文件。如果有n个文件,一般应设n个指针变量,分别指向n个FILE类型变量,以实现对n个文件的访问,见图10.3。  

 为方便起见,通常将这种指向文件信息区的指针变量称为指向文件的指针变量

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

10.2 文件的打开与关闭

实际上,所谓“打开”是指为文件建立相应的信息区(用来存放有关文件的信息)和文件缓冲区(用来暂时存放输入输出的数据)。所谓“关闭”是指撤销文件信息区和文件缓冲区,使文件指针变量不再指向该文件,显然就无法进行对文件的读写了。

用标准输入输出函数fopen来实现打开文件。fopen函数的调用方式通常为 

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

FILE *fp;                                                             //定义一个指向文件的指针变量fp

fp=fopen("a1","r");                                              //将fopen函数的返回值赋给指针变量fp

 

 exit函数的作用是关闭所有文件,终止正在执行的程序,待用户检查出错误,修改后再运行。

 关闭文件用fclose函数。fclose函数调用的一般形式为:

fclose(文件指针);

fclose(fp);

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

10.3 文件的顺序读写

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

程序代码如下:

#include <stdio.h>
#include <stdlib.h>
int main()
{
    system("chcp 65001");//加上这句,中文就不会变成乱码
    FILE *fp;
    char ch,filename[10];
    printf("请输入所用文件名:");
    scanf("%s",filename);
    if((fp=fopen(filename,"w"))==NULL)//打开输出文件
    {
        printf("无法打开此文件\n");//如果打开时出错,就输出"打不开"的信息
        exit(0);//终止程序
    }
    ch=getchar();//ch用来接收在执行scanf语句时最后输人的回车符
    printf("请输入一个准备存储到磁盘的字符串(以#结束):");
    ch=getchar();//接收从键盘输入的第一个字符
    while(ch!='#')//当输人"#"时结束循环
    {
        fputc(ch,fp);//向磁盘文件输出一个字符
        putchar(ch);//将输出的字符显示在屏幕上
        ch=getchar();//再接收从键盘输人的一个字符
    }
    fclose(fp);//关闭文件
    putchar(10);//向屏幕输出一个换行符,换行符的ASCII代码为10
    return 0;
}




运行结果如下:

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

程序代码如下:

#include <stdio.h>
#include <stdlib.h>
int main()
{
    FILE *in,*out;
    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;
}

运行结果如下:

文件尾标志:用标识符EOF(End Of File)表示,在stdio.h头文件中EOF被定义为-1。用feof函数检测文件尾标志EOF是否已被读过。如果文件尾标志被读过,表示文件有效字符已全部读完了,即文件已结束,此时feof函数值为真(以1表示),否则feof函数值为假(以0表示)。

 

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

程序代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main()
{
    FILE *fp;
    char str[3][10],temp[10];//str是用来存放字符串的二维数组,temp是临时数组
    int i,j,k,n=3;
    printf("Enter Strings:\n");
    for(i=0;i<n;i++)
        gets(str[i]);//从键盘输入字符串
    for(i=0;i<n-1;i++)//用选择法对字符串排序
    {
        k=i;
        for(j=i+1;j<n;j++)
            if(strcmp(str[k],str[j])>0) k=j;//用strcmp函数对字符串比较大小 
        if(k!=i)
        {
            strcpy(temp,str[i]);//复合语句的作用是将str[i]与str[k]的值对换
            strcpy(str[i],str[k]);
            strcpy(str[k],temp);
        }
    }
    if((fp=fopen("D:\\cc\\temp\\string.dat","w"))==NULL)//打开磁盘文件
    {
        printf("can't open file!\n");
        exit(0);
    }
    printf("\nThe new sequence:\n");
    for(i=0;i<n;i++)
    {
        fputs(str[i],fp);fputs("\n",fp);//向磁盘文件写数据
        printf("%s\n",str[i]);//在屏幕上显示
    }
    return 0;
}

运行结果如下:

 注意:磁盘文件路径是必须存在,才会自动创建string.dat文件。

为了验证输出到磁盘文件中的内容,可以编写出以下的程序,从该文件中读回字符串,并在屏幕上显示。

程序代码如下:

#include <stdio.h>
#include <stdlib.h>
int main()
{
    FILE *fp;
    char str[3][10];
    int i=0;
    if((fp=fopen("D:\\cc\\temp\\string.dat","r"))==NULL)
    {
        printf("can't open file\n");
        exit(0);
    }
    while(fgets(str[i],10,fp)!=NULL)
    {
        printf("%s",str[i]);
        i++;
    }
    fclose(fp);
    return 0;
}

运行结果如下:

用二进制方式读写文件

fread(buffer,size,count,fp);

fwrite(buffer,size,count,fp);

其中:
buffer:是一个地址。对fread来说,它是读入数据的存放地址。对fwrite来说,是要输出数据的地址(以上指的是起始地址)。
size:要读写的字节数。
count:要进行读写多少个size字节的数据项。
fp:文件型指针。文件以二进制形式打开。
用fread和fwrite函数就可以读写任何类型的信息,例如:
fread (f, 4,10, fp);
其中f是一个实型数组名。一个实型变量占4字节。这个函数从fp所指向的文件读入10个4字节的数据,存储到数组f中。

例10.4 从键盘输入10个学生的有关数据,然后把它们转存到磁盘文件上。

程序代码如下:

#include <stdio.h>
#define SIZE 10
struct student_type
    {
        char name[10];
        int num;
        int age;
        char addr[15];
    }stud[SIZE];//定义全局结构体数组stud,包含4个学生数据

void save()//定义函数 save,向文件输出SIZE个学生的数据
    {
        FILE *fp;
        int i;
        if((fp=fopen("D:\\cc\\temp\\stu_dat","wb"))==NULL)//打开输出文件 stu_dat
        {
            printf("cannot open file\n");
            return;
        }
        for(i=0;i<SIZE;i++)
        {
            if(fwrite(&stud[i],sizeof(struct student_type),1,fp)!=1)
            {
                printf("file write error\n");
            }
        }
        fclose(fp); 
    }

int main()
{
    int i;
    printf("Please enter data of student:\n");
    for(i=0;i<SIZE;i++)//输入 SIZE 个学生的数据,存放在数组 stud中
        scanf("%s %d %d %s",stud[i].name,&stud[i].num,&stud[i].age,stud[i].addr);
    
    save();
    return 0;
}

验证程序

程序代码如下:

#include <stdio.h>
#define SIZE 10
struct student_type
{
    char name[10];
    int num;
    int age;
    char addr[15];
}stud[SIZE];

int main()
{
    int i;
    FILE *fp;
    if((fp=fopen("./stu_dat","rb"))==NULL)
    {
        printf("cannot open file\n");
        return;
    }
    for(i=0;i<SIZE;i++)
    {
        fread(&stud[i],sizeof(struct student_type),1,fp);
        printf("%-10s %4d %4d %-15s\n",stud[i].name,stud[i].num,stud[i].age,stud[i].addr);
    }
    fclose(fp);
    return 0;
}

运行结果如下:

10.4 文件的随机读写

流式文件既可以进行顺序读写,也可以进行随机读写。关键在于控制文件位置标记。如果文件位置标记是按字节位置顺序移动的,就是顺序读写。如果能将文件位置标记按需移动到任意位置,就可以实现随机读写。所谓随机读写,是指读写完上一个字符(字节),并不一定要读写其后续的字符(字节),而可以读写文件中任意位置上所需要的字符(字节),即对文件读写数据的顺序和数据在文件中的物理顺序一般是不一致的。可以在任何位写入数据,在任何位置读取数据。

可以强制使文件位置标记指向需要的位置,用以下函数实现。

(1)用rewind函数使文件位置标记指向文件头。

rewind 函数的作用是使文件位置标记重新返回文件的开头,此函数没有返回值。

例10.5 有一个磁盘文件,第一次将它的内容显示在屏幕上,第二次把它复制到另一文件上。

程序代码如下:

#include <stdio.h>
int main()
{
    FILE *fp1,*fp2;
    fp1=fopen("file1.dat","r");//打开输入文件
    fp2=fopen("file3.dat","w");//打开输出文件
    while(!feof(fp1)) putchar(getc(fp1));//逐个读入字符并输出到屏幕
    putchar(10);
    rewind(fp1);//使文件位置标记返回文件头
    while(!feof(fp1)) putc(getc(fp1),fp2);//从文件头重新逐个读字符,输出到file3文件
    fclose(fp1);fclose(fp2);
    return 0;
}

(2)用fseek函数移动文件位置标记。

fseek函数可以改变文件位置标记的位置。

fseek函数的调用形式为:

fseek(文件类型指针,位移量,起始点);

fseek(fp,100L,0);//将文件位置标记移到离文件头100字节处

fseek(fp,50L,1);//将文件位置标记移到离当前位置后面50字节处

fseek(fp,-10L,2);//将文件位置标记从文件末尾处向后退10字节

“位移量”指以“起始点”为基点,向前移动的字节数。C标准要求位移量是long型数据(在数字的末尾加一个字母L,就表示是long型)。 

(3)用ftell 函数测定文件位置标记的当前位置。

ftell 函数的作用是得到流式文件位置标记的当前位置。

i=ftell(fp);

if(i==-1L) printf("error\n");//如果ftell函数返回值为-1L,表示出错,则输出“error”

例10.6 在磁盘文件stu_dat上已存有10个学生的数据(stu_dat是执行例10.4程序时建立的数据文件)。现要求将该文件中的第1、3、5、7、9个学生数据输入计算机,并在屏幕上显示出来。 

程序代码如下:

#include <stdio.h>
#include <stdlib.h>
struct student_type
{
    char name[10];
    int num;
    int age;
    char addr[15];
}stud[10];

int main()
{
    int i;
    FILE *fp;
    if((fp=fopen("stu_dat","rb"))==NULL)
    {
        printf("can not open file\n");
        exit(0);
    }
    for(i=0;i<10;i+=2)
    {
        fseek(fp,i*sizeof(struct student_type),0);//移动文件位置标记
        fread(&stud[i],sizeof(struct student_type),1,fp);//读一个数据块结构体变量
        printf("%-10s %d %d %-15s\n",stud[i].name,stud[i].num,stud[i].age,
            stud[i].addr);//屏幕输出
    }
    fclose(fp);
    return 0;
}

10.5 提高部分

在标准输入输出库中,系统定义了三个FILE型的指针变量:

(1) stdin(标准输入文件指针)。指向在内存中与键盘相应的文件信息区,因此,用它进输入就蕴涵了从键盘输入。

(2) stdout(标准输出文件指针)。指向在内存中与显示器屏幕相应的文件信息区,因,用它进行输出就蕴涵了输出到显示器屏幕。

(3) stderr(标准出错文件指针),用来输出出错的信息,它也指向在内存中与显示器屏幕应的文件信息区,因此,在程序运行时的出错的信息就输出到显示器屏幕。

这三个FILE型的指针变量称为标准文件(standard file)指针,有时简称标准文件。它是在stdio.h头文件中定义的。因此在使用键盘输入和屏幕输出时用户不需要自己定义应的文件指针。按规定,在程序中所有用到的文件必须先打开才能使用,但是为什么在对终端(显示器,印机等)输入输出时程序中并没有打开相应的文件呢?原因是:为了方便用户,系统在程开始运行时,自动打开3个标准文件:标准输入stdin、标准输出stdout和标准出错输出derr。因此用户就不需要自己打开终端文件了。

1、ferror函数

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

它的一般调用形式为:

ferror (fp);

如果ferror返回值为0(假),表示未出错;如果返回一个非零值,表示出错。应该注意,对同一个文件每一次调用输入输出函数,均产生一个新的ferror函数值,因,应当在调用一个输入输出函数后立即检查ferror函数的值,否则信息会丢失。在执行fopen函数时,ferror函数的初始值自动置为0

2、clearerr函数

clearerr的作用是使文件错误标志和文件结束标志置为0。假设在调用一个输入输出函时出现错误,ferror函数值为一个非零值。应该立即调用clearerr(fp),使ferror(fp)的值成0,以便再进行下一次的检测。只要出现文件读写错误标志,它就一直保留,直到对同一文件调用clearerr函数或rewind函数,或任何其他一个输入输出函数。 

本章小结

(1)文件是在外部介质上数据的集合,操作系统把所有输入输出设备都作为文件来管理。每一个文件需要有一个文件标识,包括文件路径、文件主干名和文件后缀。

(2)数据文件有两类: ASCII文件二进制文件。数据在内存中是以二进制形式存储的,如果不加转换地输出到外存,就是二进制文件,可以认为它就是存储在内存的数据的映像,所以也称为映像文件。如果要求在外存上以ASCII代码形式存储,则需要在存储前进行转换。

(3) C语言采用缓冲文件系统,为每一个使用的文件在内存开辟一个文件缓冲区,在计算机输入时,先从文件把数据读到文件缓冲区,然后从缓冲区分别送到各变量的存储单元。在输出时,先从内存数据区将数据送到文件缓冲区,待放满缓冲区后一次输出,这有利于提高效率。

(4)文件指针是缓冲文件系统中的一个重要的概念。在文件打开时,在内存建立一个文件信息区,存放文件的有关特征和当前状态。这个信息区的数据组织成结构体类型,系统把它命名为FILE类型。文件指针是指向FILE类型数据的,具体来说就是指向某一文件信息区的开头。通过这个指针可以得到文件的有关信息,从而对文件进行操作。这就是指针指向文件的含义 。

(5)文件使用前必须“打开”,用完后应当“关闭”。所谓打开,是建立相应的文件信息区,开辟文件缓冲区。由于建立的文件信息区没有名字,只能通过指针变量来引用,因此一般在打开文件时同时使指针变量指向该文件的信息区,以便程序对文件进行操作。所谓关闭,是撤销文件信息区和文件缓冲区,指针变量不再指向该文件。

(6)有两种对文件的读写方式,顺序读写随机读写。对于顺序读写而言,对文件读写数据的顺序和数据在文件中的物理顺序是一致的。对于随机读写而言,对文件读写数据的顺序和数据在文件中的物理顺序一般是不一致的。

(7)对文件的操作,要通过文件操作函数实现。表10.5归纳了常用的文件操作函数及其功能。