10.1 C文件的有关基本知识

10.1.1 什么是文件

程序设计中主要用到两种文件:

  1. 程序文件。源文件,目标文件,可执行文件,文件内容为程序代码
  2. 数据文件。文件内容不是不是程序代码,是程序文件运行时读写的数据。

本章主要讨论数据文件

从操作系统角度看,每个与主机相连的输入输出设备看作一个文件。

文件一般指存储在外部介质上数据的集合。

输入输出是数据传送的过程。输入输出也被称为流,也就是数据流。

C语言把文件看做是一个字符(或字节)的序列。

一个输入输出流就是一个字符或字节(内容为进制数据)流。

输入输出数据流的开始和结束仅受程序控制而不受物理符号(如回车换行)的控制,这样的文件称为流式文件。

10.1.2 文件名

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

文件标识包括3各部分:

  1. 文件路径
  2. 文件名主干(包含文件后缀)
  3. 文件后缀:表示文件的性质(图片还是文本)

文件标识常称为文件名,不仅仅是文件名主干

10.1.3 文件的分类

数据的组织形式,数据文件分为ASCII文件二进制文件

映像文件:内存中的二进制数据

ASCII文件又称文本文件:每一个字节放一个字符的ASCII码。

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

推荐使用二进制文件方便内存的读取和输出。

10.1.4 文件缓冲区

ANSI C标准采用”缓冲文件系统”处理数据文件。

文件缓冲区:系统自动在内存区为程序每一个正在使用文件开辟一个文件缓冲区。

从内存向磁盘输出数据,先送到内存中的缓冲区,将装满缓冲区的数据送到磁盘中去,反之亦然。

文件缓冲区.png

10.1.5 文件类型指针

缓冲文件系统中,关键的概念是“文件类型指针”。

为方便起见,通常讲这种指向文件信息去的指针变量简称为指向文件的指针变量。

用结构体存储文件信息。该结构体由系统声明,取名为FILE。

typedef struct
{
    short level;                //缓冲区“满”或“空”的程度
    unsigned flags;                //文件状态标识
    char fd;                    //文件描述符
    unsigned char hold;         //缓冲区无内容不读取字符
    short bsize;                //缓冲区的大小
    unsigned char * buffer;        //数据缓冲区的位置
    unsigned char * curp;        //指针当前指向
    unsigned istemp;            //临时文件指示器
    short token;               //用于有效性检查
}FILE;

/*
一般设置指向FILE类型变量的指针变量,然后通过它来引用这些FILE类型变量,二部定义FILE类型变量,这样使用比较方便。
*/

10.2 打开与关闭文件

打开:为文件建立相应的信息区(存放有关文件的信息)和文件缓冲区(暂时存放输入输出的数据。)

10.2.1 用fopen函数打开数据文件

fopen函数的调用方式

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

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

使用文件方式.png

使用文件方式2.png

注意:

  1. 如果不能实现“打开”任务,返回值NULL
  2. 有些编译系统可能不完全提供所有这些功能(有的只能用”r”,”w”,”a”),有的C版本不能用“r+”,”w+”,”a+”,而用“rw”,“wr”,“ar”时,看系统是否支持。
  3. 对于ASCII文件来说,遇到回车换行符,系统把它转换为一个换行符,在输出时吧吧换行符转换为回车和换行两个字符。二进制文件则不需要这要这种转换。
  4. 程序中可以使用3个标准的流文件——标准输入流、标准输出流、标准出错输出流系统对这3个文件制定了与终端的对应关系。
  5. 程序运行时自动打开3个标准流文件,系统定义了3个头文件指针变量
    • stdin->标准输入流
    • stdout->标准输出流
    • stderr->标准出错输出流

10.2.2 用fclose函数关闭数据文件

使用完一个文件后应该关闭它,防止被误用。

“关闭”就是 撤销文件信息区文件缓冲区

关闭文件用fclose函数。

fclose(文件指针);

fclose执行成功时返回0,否则返回EOF(-1)

不关闭文件将会出现丢失数据,因为所修改的数据都还在缓冲区中,也就是在内存中,不在外部介质里。一旦程序结束,数据就会丢失。用fclose函数关闭文件,先把缓冲区中的数据写到文件,从而避免这个问题。应该养成在程序终止之前关闭所有文件的习惯。

10.3 顺序读写数据文件

顺序读写:对文件读写数据的顺序和数据在文件中的物理顺序是一致的。

10.3.1 怎么向文件读写字符

函数名 调用形式 功能 返回值
fgetc fgetc(fp);//fp是文件指针 从fp指向的文件读入一个字符 读入成功,则返回所读字符;失败返回结束标志EOF(-1)
fputc fputc(ch,fp); 把字符ch写到文件指针变量fp所指向的文件中 输出成功,返回输出的字符;输出失败,返回EOF(-1)
# include<stdio.h>
# include<stdlib.h>
/*练习顺序输出字符*/
int main()
{
    FILE *fp;
    char ch,*filename;
    printf("请输入所用的文件名:\n");
    scanf("%s",filename);
    if((fp=fopen(filename,"w"))==NULL)
    {
        printf("无法打开此文件\n");
        exit(0);
    }
    ch=getchar();//用来接收最后输入的回车符 回车符此时在字符缓冲区内
    printf("请输入一个准备存储到磁盘的字符串(以#结束):\n");
    ch=getchar();//从字符缓冲区里读取字符
    while(ch!='#')
    {
        fputc(ch,fp);//向磁盘文件输出字符
        putchar(ch);//将字符输出终端上
        ch=getchar();//接着从字符缓冲区里取字符
    }
    fclose(fp);//关闭文件
    putchar(10);//向屏幕输出一个换行符
    return 0;
}
# include "stdio.h"
# include "stdlib.h"
/*
练习读入文件
*/
int main()
{
    FILE *fp;
    char ch,*filename;
    printf("请输入文件名:\n");
    scanf("%s",filename);
    if((fp=fopen(filename,"r"))==NULL)
    {
        printf("文件打开错误,请重新尝试。\n");
        exit(0);
    }
    ch=getchar();//将字符缓冲区中的回车符处理掉,避免对读取文件内容时造成干扰。
    while(!feof(fp))//检查fp指向的文件是否结束,如果结束函数值为1(真)
    {
     ch=fgetc(fp);    //从文件中读取字符
     putchar(ch);
    }
    fclose(fp);//关闭文件
    putchar(10);//打印回车
    return 0;
}

/*
以上文件是按文本文件方式处理的,处理二进制文件是将读入/写出方式改为rb/wb
C系统已把fputc和fgetc函数定义宏名为put和get
# define putc(ch,fp) fputc(ch,fp)
# define getc(ch,fp) fputc(ch,fp)
这是在stdio.h中定义的,因此在程序中用putc和fputc作用是一样的。
*/

10.3.2 怎样向文件读写一个字符串(避免了读入字符的麻烦)

C语言允许通过函数一次读写一个字符串。

函数名 调用形式 功能 返回值
fgets fgets(str,n,fp); 读入一个长度为n-1的字符串,存放字符串到数组str中。 读成功,返回地址str,失败返回NULL
fputs fputs(str,fp); 把str所指向的字符串写到文件指针变量fp所指向的文件中 输出成功,返回0;否则返回非0值

说明:

fgets函数原型:

char *fgets(char *str,int n,FILE *fp);//作用是读入字符串

n:代表要得到的字符个数,实际从文件中获得只有n-1个字符,因为最后一个字符作为字符串结束标志’\0’,这样把n个字符放到数组str中。

在读入n-1个字符前碰到’\n’或是文件结束符EOF,读入即结束,但是将所遇到的换行符’\n’也作为一个字符读入。

fputs函数原型为

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

字符串末尾’\0’不会输出,输出成功函数值为0;失败时函数的值为EOF(-1)。

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

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/*练习输出字符串*/
# define LEN 3
int main()
{
    FILE *fp;//需要读写的文件
    char temp[20];//输入和输出的临时字符串
    char str[3][20];//是用来存放字符串的二维数组
    char filename[20];//输入文件名   字符指针指向字符串常量,不是字符串变量 
    int i,j;
    printf("请输入想要输入的字符串:\n");
    for (i = 0; i <LEN; i++)//输入字符串
    {
        gets(str[i]);
    }
    for ( i = 0; i < LEN-1;i++)//进行字符串大小
    {
        /* code */
        for(j=i+1;j<LEN;j++)
            if (strcmp(str[i],str[j])>0)
            {
                /* code */
                strcpy(temp,str[i]);
                strcpy(str[i],str[j]);
                strcpy(str[j],temp);
            }
    }

    printf("输入要打开的文件名\n");
//    gets(filename);
    scanf("%s",filename);
    if ((fp=fopen(filename,"w"))==NULL)
    {
        printf("文件打开错误\n");
        exit(0);
    }
    /* 进行字符串输出到文件中 */
    for ( i = 0; i <3; i++)
    {
        /* code */
        fputs(str[i],fp);
        fputs("\n",fp);
        printf("%s\n",str[i]);
    }
    fclose(fp);
    return 0;
}
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
/*练习读入字符串*/
int main()
{
    FILE *fp;//定义文件指针变量
    char str[3][20],filename[20];
    int i;
    printf("请输入打开文件名:\n");
    scanf("%s",filename); 
    if((fp=fopen(filename,"r"))==NULL)
    {
        printf("文件打开错误!\n");
        exit(0);
    }
    while(fgets(str[i],10,fp)!=NULL)
    {
        printf("%s",str[i]);
        i++;
    }
    fclose(fp);//关闭文件
    return 0;
}
/*
1.打开文件时,文件名与路径不要输错
2.fgets(str[i],10,fp) 读取字符串时,指定一次读入10个字符,当遇到'\n',就结束字符串的输入。
3.由于读入字符数组中的每个字符串后都一个'\n',因此向屏幕输入时,不用加'\n'
*/

10.3.3 用格式化的方式读写文件

如同printf和scanf函数一样,进行格式字符的输出和输入,printf和scanf是针对终端来说的。

对于文件来说,我们用fprintf和fscanf函数。一般调用形式为:

fprintf(文件指针,格式字符串,输出表列);

fscanf(文件指针,格式字符串,输入表列);

例如:

fprintf(fp,”%d,%f”,i,f);//作用是将int型变量i和float型变量f的值按%d和%f格式输出到fp指向的文件中。

fscanf(fp,”%d,%f”,&i,&f);//从磁盘文件中读入整数送给整形变量i,读取实数送给实型变量f

用fprintf和fscanf函数对磁盘文件读写,使用方便,容易理解。

输入时要将文件中的ASCII码转换为二进制形式在保存到内存变量中。

输出时又要将内存中的二进制形式转换成字符,需要花费许久时间。

在内存与磁盘频繁交换数据时,不推荐使用。

10.3.4 用二进制方式项文件读写一组数据

C语言允许使用fread函数从文件中读一个数据块,用fwrite函数向文件中写一个数据块。在读写时是以二进制进行的。

一般调用形式:

fread(buffer,size,count,fp);//从文件中读
fwrite(buffer,size,count,fp);//向文件中写
1. buffer:是个地址。
    对于fread来说用于存放从文件读入的数据存储区的地址
    对于fwrite来说是把此地址开始的存储区中的数据向文件输出。
2. size:读写的字节数
3. count:要读写多少个数据项(每个数据长度为size)
4. fp:FILE类型指针

如:

fread(f,4,10,fp);//f是一个float数组,这个函数从fp所指向的文件读入10个4字节的数据存储到数组f中。

fread和fwrite函数的类型为int型,如果fread或fwrite函数执行成功,函数返回值为形参count的值。否则返回与count不相等的值

# include<stdio.h>
# define SIZE 5
/*
练习二进制文件的与输出 
*/
struct Student_type{
    char name[10];
    int num;
    int age;
    char addr[15];
};

void save(struct Student_type * stu)
{
    FILE *fp;
    char filename[20];
    struct Student_type *st;
    st=stu;
    int i;
    printf("请输入文件名\n");
    scanf("%s",filename);
    if((fp=fopen(filename,"wb"))==NULL)
    {
        printf("打开文件失败:\n");
     } 
    for(i=0;i<SIZE;i++,st++)
    {
        /*
        结构体变量长度为它的成员长度之和,33但实际上是36字节,是4的倍数,系统以4个字节为一字
        */
        if(fwrite(st,sizeof(struct Student_type),1,fp)!=1)
        printf("文件写入失败\n");
     } 
     fclose(fp);
}

int main()
{
    int i;
    struct Student_type stu[SIZE];
    void save(struct Student_type * stu);
    for(i=0;i<SIZE;i++)
    {
        printf("Please enter data of students:\n");
        scanf("%s %d %d %s",&stu[i].name,&stu[i].num,&stu[i].age,&stu[i].addr);
    }
    save(stu);
    return 0;
}
# include "stdio.h"
# include "stdlib.h"
# define SIZE 5;

/*练习读取二进制文件*/

struct Student_type{
    char name[10];
    int num;
    int age;
    char addr[15];
};

int main()
{
    int i;
    FILE *fp;
    char filename[20];
    printf("请输入文件名\n");
    scanf("%s",filename);
    if((fp=fopen(filename,"rb"))==NULL)
    {
        printf("打开文件失败:\n");
        exit(0);
     }
    for(i=0;i<SIZE;i++)
    {
         fread(&stu[i],sizeof(struct Student_type),1,fp);
         printf("%s\t%d\t%dt\t%s\n",&stu[i].name,&stu[i].num,&stu[i].age,&stu[i].addr);
     }
    fclose(fp);
    return 0;
}

10.4 随机读写数据文件

10.4.1 文件位置标记及其定位

1.文件位置标记

系统为每个文件设置了一个文件读写位置标记(简称 文件位置标记文件标记):用来指示”接下来读写下一个字符的位置“。

顺序读取时文件标记指向文件头,这时对文件内容依次读入,文件标记就依次向后移动,直到文件末尾。

顺序写文件时与读文件类似,把所有数据写完,文件标记在最后一个数据之后。

根据读写需要,人为地移动文件位置标记的位置。就是随机读写。随机读写对文件读写数据的顺序和数据在文件中的物理顺序是不一致的。(读写位置,读写内容)

流式文件既可以进行顺序读写也可以进行随机读写。

2.文件位置标记的定位

可以强制是文件标记位置指向人们指定的位置。可以用以下函数实现。

1.rewind函数使文件位置标记指向文件开头。没有返回值
# include <stdio.h>
# include <stdlib.h>
# include <string.h>

int main()
{
    FILE *fp1,*fp2;
    //文件打开检查
    if((fp1=fopen("text.txt","r"))==NULL)
    {
        printf("text.txt打开错误\n");
        exit(0);
    }
    if((fp2=fopen("file2.dat","w"))==NULL)
    {
        printf("file2.dat打开错误\n");
        exit(0);
    }
    while(!feof(fp1))
    putchar(getc(fp1));//输出从text中读取到的字符 
    putchar(10);//回车符 
    rewind(fp1);//使文件标记位置回到文件头 
    while(!feof(fp1))
    putc(getc(fp1),fp2);//从文件头中重新逐个读字符,输出file2文件 
    fclose(fp1);
    fclose(fp2);
    return 0;
}
2.用fseek函数改变文件位置标记

fseek函数的调用形式:

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

起始点:

  1. 0代表文件开始位置
  2. 1代表当前位置
  3. 2文件末尾位置

位移量:以起始点为基点,向前移动的字节数。位移量应是long型数据

fseek函数一般用于二进制文件,例如:

fseek(fp,100L,0); //将文件位置标记向前移动离文件开头100个字节处

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

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

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

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

用相对文件开头的位移量表示返回值,如果函数调用出错,ftell函数返回值为-1L

如:

i=ftell(fp);//变量i存放文件当前位置
if(i==-1L)
    printf("error\n");//如果调用函数时出错,输出error

10.4.2 随机读写

# include <stdio.h>
# include "stdlib.h"
# define SIZE 10
/*
练习随机读写 
*/
struct Student_type
{
    char name[10];
    int num;
    int age;
    char addr[20];
 }stud[SIZE];

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 %4d %4d %-15s\n",stud[i].name,stud[i].num,stud[i].age,stud[i].addr);
    }
    fclose(fp);
    return 0;
 } 

10.5 文件读写的出错检测

1.ferror函数

用于检测各种输入输出函数,如:putc,getc,fread,fwrite等等

ferror(fp);

如果返回值为0,表示未出错,返回非0数值,表示出错。

每次调用输入输出函数,都会产生新的ferror函数值。

执行fopen函数时,ferror函数的初始值自动设置为0

2.clearerr函数

用于使文件错误标志和文件结束标志置为0。比如说:当调用一个输入输出函数出现错误,ferror函数值为一个非零的值。应立即调用clearerr(fp),使ferror(fp)的值变成0,以便下一次检测。

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