开讲啦:Chap 10 对文件的输入输出

简介: 在程序设计中,主要用到两种文件:程序文件:包括源程序文件.c、目标文件.obj、可执行文件.exe等;数据文件:文件的内容不是程序,而是供程序运行时读写的数据或在程序运行过程中供读入的数据;

10.1 C文件的有关知识


10.1.1 什么是文件


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


  • 程序文件:包括源程序文件.c、目标文件.obj、可执行文件.exe等;
  • 数据文件:文件的内容不是程序,而是供程序运行时读写的数据或在程序运行过程中供读入的数据;


10.1.2 文件名


一个文件有一个唯一的文件标识,以便用户识别和引用,文件识别包括3部分:


  • 文件路径
  • 文件名主干
  • 文件名后缀


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


微信图片_20220611045504.png


表示file1.dat文件存放在D盘中的CC目录下的temp子目录下面,文件标识常被称为文件名,文件名主干的命名规则遵循标识符的命名规则,后缀用来表示文件的性质,如:


  • docword生成的文件;
  • txt:文本文件;
  • dat:数据文件;
  • cC语言源程序文件;
  • cppC++源程序文件;
  • forFORTRAN语言源程序文件;
  • pasPascal语言源程序文件;
  • obj:目标文件;
  • exe:可执行文件;
  • ppt:电子幻灯片;
  • bmp:图形文件;


10.1.3 文件的分类


根据数据的组织形式,数据文件可分为ASCII文件和二进制文件,数据在内存中是以二进制形式存储的,如果不加转换的输出到外存,就是二进制文件,可以认为它就是存储在内存的数据的映像,即映像文件;如果要求在外存上以ASCII代码形式存储,则需要在存储前进行转换,ASCII文件又称文本文件,每一个字节存放一个字符的ASCII代码。


一个数据在磁盘上怎样存储呢?字符一律以ASCII形式存储,数值型数据既可以用ASCII形式存储,也可以用二进制形式存储。


10.1.4 文件缓冲区


所谓缓冲文件系统是指系统自动地在内存区为程序中每一个正在使用的文件开辟一个文件缓冲区,从内存向磁盘输出数据必须先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘去,如果从磁盘向计算机读入数据,则一次从磁盘文件将一批数据输入到内存缓冲区,然后在从缓冲区逐个地将数据送到程序数据区,缓冲区的大小由各个具体的C编译系统确定。


微信图片_20220611045511.png


10.1.5 文件类型指针


每个被使用的文件都在内存中开辟一个相应的文件信息区,用来存放文件的有关信息,如文件的名字、文件状态以及文件当前位置等,如FILE *fp所示即为定义fp是一个指向FILE类型数据的指针变量,可以使fp指向某一个文件的文件信息区,通过该文件信息区中的信息就能够访问该文件,也就是说,通过文件指针变量能够找到与它关联的文件。


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


10.2 打开与关闭文件


所谓打开是指为文件建立相应的信息区(用来存放有关文件的信息 )和文件缓冲区(用来暂时存放输入输出的数据),在打开文件的同时,一般都指定一个指针变量指向该文件,也就是建立起指针变量与文件之间的联系,这样就可以通过该指针变量对文件进行读写了,所谓关闭是指撤销文件信息区和文件缓冲区,使文件指针变量不再指向该文件,显然就无法进行对文件的读写了。


10.2.1 用fopen函数打开数据


例10.1 文件的打开与关闭


#include <stdio.h>
#include<stdlib.h>
#include<time.h>
int main(){
    FILE *input ,*destfile;
    int t;
    input = fopen("1.txt","r");
    destfile = fopen("2.txt" , "w");
    if(input == NULL){
        // 抛出最近一次的系统错误信息
        perror("the file is empity");
        exit(EXIT_FAILURE);
    }
    else{
        perror("the file is not empity");
        // feof()函数用来检测当前流文件上的文件结束表识,判断是否读到了文件结尾。
        while(!feof(input)){
            t = fgetc(input);
            if(t =='{'){
                t ='s';
            }
            printf("%c" , t);
            fputc(t , destfile);
        }
    }
    // 用于清空文件缓冲区
    fflush(stdout);
    fclose(destfile);
    fclose(input);
    return0;
}


fopen函数的调用方式为:fopen(文件名,使用文件方式)


微信图片_20220611045514.png


程序中可以使用3个标准的流文件 — 标准输入流stdin标准输出流stdout标准出错输出流stderr,系统已对这3个文件指定了与终端的对应关系,标准输入流是从终端的输入,标准输出流是向终端的输出,标准出错输出流是当程序出错时将出错信息发送到终端。


程序开始运行时系统自动打开这3个标准流文件,因此,程序编写者不需要在程序中用fopen函数打开它们。所以我们以前用到的从终端输入或输出到终端都不需要打开终端文件,系统定义了3个文件指针变量stdinstdoutstderr,分别指向标准输入流、标准输出流和标准出错输出流,可以通过这3个指针变量对以上3种流进行操作,它们都以终端作为输入输出对象,如果程序中指定要从stdin所指的文件输入数据,就是指从终端键盘输入数据。


10.2.2 用fclose函数关闭数据文件


fclose函数调用的一般形式为:fclose(文件指针)


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


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


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


10.3 顺序读写数据文件


10.3.1 怎样向文件读写字符


微信图片_20220611045518.png


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


#include <stdio.h>
#include<stdlib.h>
int main(){
    FILE *fp;
    char ch,filename[10];
    printf("请输入所用的文件名:");
    scanf("%s",filename);
    if ((fp = fopen(filename,"w")) == NULL) {
        printf("无法打开此文件!\n");
        exit(0);
    }
    ch = getchar();
    printf("请输入一个字符(以#结束):");
    ch = getchar();
    while (ch!='#')
    {
        fputc(ch,fp);
        putchar(ch);
        ch = getchar();
    }
    fclose(fp);
    putchar(10);
    return0;
}


效果如下所示:


微信图片_20220611045522.png


fopen函数打开成功之后会返回该文件所建立的信息区的起始地址,并将其赋值给指针变量fpexit是标准的C库函数,其作用是使程序终止,用此函数时在程序的开头应包含stdlib.h头文件。


例10.3 将file1.dat的内容复制到file2.data中。


#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);
    }
    while (!feof(in)) {
        ch = fgetc(in);
        fputc(ch, out);
        putchar(ch);
    }
    fclose(in);
    fclose(out);
    putchar(10);
    return0;
}


效果如下所示:


微信图片_20220611045526.png


feof函数可以检查到文件读写位置标记是否移到文件的末尾,即磁盘文件是否结束,若结束则返回1,否则返回0。


10.3.2 怎样向文件读写一个字符串



微信图片_20220611045529.png


「注」:


  • fgets函数执行成功,则返回值为str数组首元素的地址,如果一开始就遇到文件尾或读数据出错,则返回NULL;
  • fputs函数的原型为int fputs(char *str,FILE *fp),其作用是将str所指向的字符串输出到fp所指向的文件中,该函数第一个参数可以是字符串常量、字符数组名或字符型指针,字符串末尾的\0不输出,若输出成功,函数值为0,失败时,函数值为EOF


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


#include <stdio.h>
#include<stdlib.h>
#include<string.h>
int main(){
    FILE *fp;
    char str[3][10],temp[10];
    int i,j,k,n=3;
    printf("请输入字符串:");
    for (i = 0; i < 3; 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;
            }
        }
        if(k!=i){
            strcpy(temp, str[i]);
            strcpy(str[i], str[k]);
            strcpy(str[k], temp);
        }
    }
    if((fp = fopen("file1.dat", "w"))==NULL){
        printf("不能打开文件!\n");
        exit(0);
    }
    printf("排序之后的字符如下:\n");
    for (i = 0; i < n; i++) {
        fputs(str[i], fp);
        fputs("\n", fp);
        printf("%s\n",str[i]);
    }
    return0;
}


效果如下所示:


微信图片_20220611045533.png


接下来,我们通过程序读file1.dat中的内容,请看代码示例:


#include <stdio.h>
#include<stdlib.h>
int main(){
    FILE *fp;
    char str[3][10];
    int i = 0;
    if((fp = fopen("file1.dat", "r"))==NULL){
        printf("不能打开文件!\n");
        exit(0);
    }
    while (fgets(str[i], 10, fp)!=NULL) {
        printf("%s",str[i]);
        i++;
    }
    fclose(fp);
    return0;
}


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


fprintffscanf函数的读写对象不是终端而是文件,它们的一般调用方式为:


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


用以上两个函数对磁盘文件读写较为方便,但由于在输入时要将文件中的ASCII码转换为二进制形式再保存在内存变量中,再输出时又要将内存中的二进制形式转换成字符,即内存与磁盘的交换数据次数过于频繁。


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


fread函数和fwrite函数的一般调用形式为:fread(buffer,size,count,fp)fwrite(buffer,size,count,fp),其中:


  • buffer:是一个地址,对fread来说,它是用来存放从文件读入的数据的存储区的地址,对fwrite来说,是要把此地址开始的存储区中的数据向文件输出;
  • size:要读写的字节数;
  • count:要读写多少个数据项;
  • fpFILE类型指针;


以下是菜鸟教程中关于fread函数和fwrite函数的介绍:


  • fread函数


微信图片_20220611045537.png


  • fwrite函数


微信图片_20220611045540.png


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


#include <stdio.h>
#include<stdlib.h>
#define SIZE 10
struct Student_type{
    char name[10];
    int num;
    int age;
    char addr[15];
}stud[SIZE];
void save(){
    FILE *fp;
    if((fp = fopen("stu.dat", "wb"))==NULL){
        printf("不能打开文件!\n");
        exit(0);
    }
    for (int i = 0; i < SIZE; i++) {
        if (fwrite(&stud[i], sizeof(struct Student_type), 1, fp)!=1) {
            printf("写入文件错误!\n");
        }
    }
    fclose(fp);
}
void read(){
    FILE *fp;
    if((fp = fopen("stu.dat", "rb"))==NULL){
        printf("不能打开文件!\n");
        exit(0);
    }
    for (int i = 0; i < SIZE; i++) {
        fread(&stud[i], sizeof(struct Student_type), 1, fp);
        printf("姓名:%s\t学号:%d\t年龄:%d\t地址:%s\n",stud[i].name,stud[i].num,stud[i].age,stud[i].addr);
    }
    fclose(fp);
}
int main(){
    printf("请输入学生数据:\n");
    for (int i = 0; i < SIZE; i++) {
        scanf("%s %d %d %s",stud[i].name,&stud[i].num,&stud[i].age,stud[i].addr);
    }
    save();
    read();
    return0;
}


效果如下所示:


微信图片_20220611045544.png


接着,我们从磁盘中读取信息并将其打印出来,效果如下所示:


微信图片_20220611045549.png


10.4 随机读写数据文件


假设需要查询几百万人中最后一个人的资料,按照顺序读写的方式,则需要将前面所有人查询完成之后才可以读取;随机访问不是按数据在文件中的物理位置次序进行读写,而是可以对任何位置上的数据进行访问,显然这种方法比顺序访问效率高得多。


10.4.1 文件位置标记及其定位


  • 文件位置标记


微信图片_20220611045553.png


对流式文件既可以进行顺序读写,也可以进行随机读写,关键在于控制文件的位置标记。如果文件位置标记是按字节位置顺序移动的,就是顺序读写。如果能将文件位置标记按需移动到任意位置,就可以实现随机读写。


所谓随机读写,是指读写完上一个字符/字节后,并不一定要读写其后续的字符/字节,而可以读写文件中任意位置上所需要的字符,即对文件读写数据的顺序和数据在文件中的物理顺序一般是不一致的,可以在任何位置写入数据,在任何位置读取数据。


  • 文件位置标定的定位


我们可以通过rewind函数使文件位置标记指向文件开头,该函数没有返回值;还可以通过fseek函数改变文件位置标记,其调用形式为fseek(文件类型指针,位移量,起始点),起始点用0、1、2代替,0代表文件开始位置,1代表当前位置,2代表文件末尾位置。


微信图片_20220611045557.png

位移量指以起始点为基点向前移动的字节数,应是long型数据,即在数字的末尾加一个字母L


  • fseek(fp,100L,0):将文件位置标记向前移到距离文件开头100个字节处;
  • fseek(fp,50L,1):将文件位置标记向前移到距离当前位置50个字节处;
  • fseek(fp,-10L,2):将文件位置标记从文件末尾处向后退10个字节;


我们还可以通过ftell函数测定文件位置标记的当前位置,由于文件中的文件位置标记经常移动,人们往往不容易知道其当前位置,所以常用ftell函数的到当前位置,用相对于文件开头的位移量来表示,如果调用函数时出错,ftell函数返回值为-1L


例10.6 将磁盘文件的信息第1次显示在屏幕上,第2次把它复制到另一文件上。


#include <stdio.h>
int main(){
    FILE *fp1,*fp2;
    fp1 = fopen("stu.dat", "r");
    fp2 = fopen("stu_cpy.dat", "w");
    while (!feof(fp1)) {
        putchar(getc(fp1));
    }
    putchar(10);
    rewind(fp1);
    while (!feof(fp1)) {
        putc(getc(fp1), fp2);
    }
    fclose(fp1);
    fclose(fp2);
    return0;
}


请看效果展示:


微信图片_20220611045601.png


10.4.2 随机读写


例10.7 在磁盘文件上存有10个学生的数据,要求将第1、3、5、7、9个学生数据输入计算机,并在屏幕上显示出来。


#include <stdio.h>
#include<stdlib.h>
#define SIZE 10
struct Student_type{
    char name[10];
    int num;
    int age;
    char addr[15];
}stud[SIZE];
int main(){
    FILE *fp;
    if((fp = fopen("stu.dat","rb"))==NULL){
        printf("不能打开文件!\n");
        exit(0);
    }
    for (int i = 0; i < SIZE; i+=2) {
        fseek(fp, i * sizeof(struct Student_type), 0);
        fread(&stud[i], sizeof(struct Student_type), 1, fp);
        printf("名字:%5s\t学号:%d\t年龄:%d\t地址:%s\n",stud[i].name,stud[i].num,stud[i].age,stud[i].addr);
    }
    fclose(fp);
    return0;
}


效果如下所示:


微信图片_20220611045605.png


10.5 文件读写的出错检测


  • ferror函数


我们可以在调用各种输入输出函数(如putcgetcfreadfwrite等)时,通过ferror函数进行检查,其一般调用形式为ferror(fp);,若其返回值为0,则表示未出错;若其返回值为非零值,则表示出错。


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


  • clearerr函数

clearerr的作用是使文件错误标志和文件结束标志置为0,假设在调用一个输入输出函数时出现错误,ferror函数值为一个非零值,应该立即调用clearerr(fp),使ferror(fp)的值变为0,以便再进行下一次的检测。

相关文章
|
消息中间件 缓存 安全
抱歉,Xposed真的可以为所欲为——终 · 庖丁解码(下)
Xposed的使用不难,API也就那些,难点是: 逆向弄清楚Hook APP的方法调用流程,怎么调,参数都是干嘛的等。 经过反复练习,逆向Hook一个普通的APP(非企业级加固)写出可用的Xposed插件早已驾轻就熟(主要是磨时间),但有一个顾虑一直萦绕心间:不知道Xposed底层的具体实现原理。Tips:Xposed通常只能 Hook java层 及 应用资源的替换,有两个实现版本:4.4前的Dalvik虚拟机实现 和 5.0后ART虚拟机实现,本文针对后者进行分析,同时搭配 Android 5.1.1_r6 源码食用。
1590 0
|
3月前
|
运维 安全 数据安全/隐私保护
下次遇到PPP接口协议Down,这样处理,敢不敢试试?
下次遇到PPP接口协议Down,这样处理,敢不敢试试?
|
网络架构
|
调度
METSO IOP111 比特流用于在部署时对设备进行编程
METSO IOP111 比特流用于在部署时对设备进行编程
107 0
METSO  IOP111 比特流用于在部署时对设备进行编程
|
存储 编译器 C++
开心档之C++ 基本的输入输出
C++ 标准库提供了一组丰富的输入/输出功能,我们将在后续的章节进行介绍。本章将讨论 C++ 编程中最基本和最常见的 I/O 操作。 C++ 的 I/O 发生在流中,流是字节序列。如果字节流是从设备(如键盘、磁盘驱动器、网络连接等)流向内存,这叫做输入操作 。如果字节流是从内存流向设备(如显示屏、打印机、磁盘驱动器、网络连接等),这叫做输出操作。
|
安全 Perl
RFSoC应用笔记 - RF数据转换器 -01- 概述和IP接口介绍(二·)
RFSoC应用笔记 - RF数据转换器 -01- 概述和IP接口介绍
647 0
RFSoC应用笔记 - RF数据转换器 -01- 概述和IP接口介绍(二·)
|
运维 安全 Shell
金鱼哥戏说RHCSA认证:十六、文件查找、传输和打包压缩
第十六章 文件查找、传输和打包压缩
136 0
金鱼哥戏说RHCSA认证:十六、文件查找、传输和打包压缩
|
自然语言处理 算法 搜索推荐
开讲啦:Chap 02 算法 - 程序的灵魂
一个程序设计人员应具备算法、数据结构、程序设计方法以及语言工具四个方面的知识,其中算法是灵魂,数据结构是加工对象,语言是工具,编程需要采用合适的方法。
开讲啦:Chap 02 算法 - 程序的灵魂
|
存储 算法 C语言
开讲啦:Chap 03 顺序程序设计
为了能编写出C语言程序,必须具备以下的知识和能力: 1.要有正确的解题思路,即学会设计算法,否则无从下手; 2.掌握C语言的语法,知道怎样使用C语言所提供的功能编写出一个完整的、正确的程序; 3.在写算法和编写程序时,要采用结构化程序设计方法,编写出结构化的程序
开讲啦:Chap 03 顺序程序设计
|
存储 C语言
开讲啦:Chap 06 利用数组处理批量数据
数组:同一类性质的数据 数组是一组有序数据的集合,数组中各数据的排列是有一定规律的,下标代表数据在数组中的序号。;用一个数组名和下标来唯一地确定数组中的元素;数组中的每一个元素都属于同一个数据类型,不能把不同类型的数据(如学生的成绩和学生的性别)放在同一个数组中。
开讲啦:Chap 06 利用数组处理批量数据