zl程序教程

您现在的位置是:首页 >  其他

当前栏目

C语言进阶——文件操作(上)

2023-03-14 23:00:24 时间

目录


🌳前言


🌳正文


🌲关于文件


🌱什么是文件?


🌱文件有什么用?


🌱文件的格式是什么?


🌲使用文件


🌱文件指针


🌱文件的打开和关闭


🌲顺序读写


🌱输入、输出流


🌱fputc 与 fgetc


🌱fputs 与 fgets


🌱fprintf 与 fscanf


🌱fwrite 与 fread


🌲随机读写


🌱fseek


🌱ftell


🌱rewind


🌱fseek、ftell、rewind 三合一


🌲文本文件与二进制文件


🌱文本文件


🌱二进制文件


🌱注意


🌲文件使用注意事项


🌱被错误使用的feof


🌱文件读取结束原因判断


🌱文件缓冲区


🌳总结


🌳前言

 文件——是我们生活中必不可缺的一部分,优秀的文件管理能使我们工作效率更高,比如上学时的点名册、平时记账的手账本、电脑中存储数据的各种文件夹等。数据构成文件,文件成就数据,因此我们需要学习C语言中的各种文件操作,使数据能够做到持久化存储。

01c9e71c5ede45959ef39ee6e0949d18.gif

图片来源:百度图片


🌳正文

 文件操作涉及到的内容还是比较多的,大致可分为三个问题:是什么? 怎么用? 要注意什么? 从这三个问题可以衍生出很多问题,其中怎么用是内容最丰富的版块,让我们直接进入正题吧。


🌲关于文件

🌱什么是文件?4ef1513039834c8a813242c388c32a9b.jpg


 如上图所示,这就是文件,不过这是传统的纸质文件。在我们电脑中的都是电子文件夹,存储的都是电子文件,比如数字、图片、文档等,这些数据都是存储在我们电脑的硬盘上的,只要硬盘没坏,那么这些数据随时随地都能找到,而且方便检索。在程序设计中,我们一般将文件分为两种:程序文件与数据文件(从文件功能的角度分类),本文主要介绍的是数据文件。


程序文件


包括源程序文件,比如我们的 .c 文件;目标文件,经过预编译、编译、汇编后生成的目标文件,后缀为 .o ,对其进行链接后,就能生成可执行程序;当然最后一种就是可执行程序文件,后缀为.exe


数据文件


就像上图一样,主要存储的是各种数据信息,数据文件的职能是能让程序读取到数据,以及能够对其写入数据,这些数据是能够持久化存储的。


🌱文件有什么用?f0676f5b419142eab37fa4677af42c83.png


电脑C盘中存储的各种信息

 文件可以保存数据,使数据能做到持久化存储。文件可以使我们的操作更为合理,比如现在写的这篇博客,本质就是一个文件,不过是存储在服务器上的文件(数据)。电子文件的最大特点就是易于检索了,这也正是电脑的优点之一。至于C语言中的文件可以用于保存程序运行所产生的数据,比如通讯录系统,可以将联系人信息保存到文件中,现在的程序设计数据一般都是存储在数据库中,毕竟本地文件夹安全性还是比较低。


🌱文件的格式是什么?

 所有文件都有唯一的标识符,标识符可以分为三部分:文件路径+文件名主干+文件后缀,比如存储在我电脑中的VS文件标识符为:


C:Program Files (x86)Microsoft Visual Studio2019CommunityCommon7IDE

b05f7a87496b456eae9fb3c68c3b14ad.png

 为了方便称呼,我们一般将其称为文件名,比如 devenv.exe 就是一个典型的程序文件


🌲使用文件

🌱文件指针

 文件是一个庞大的集合体,类似于结构体,不过更为复杂,因此在C语言中有一个专门的指针 文件类型指针,简称为 文件指针 用来指向文件首地址。系统会将文件规范化,当使用文件时,系统会在内存中开辟一个对应的文件信息区,这个信息区中包括了文件的各种信息(文件名、文件状态、文件位置等),如果对应信息缺失,系统会自动补齐。前面说过,文件类似于结构体,因此整个文件信息是保存在一个庞大的结构体中的,为了与传统结构体区分开,专门创建了 FILE* 这种特殊的指针,即文件指针。


2800a4a09460444487c9fa5fe647d508.png


 因为VS2019将其分的太细了,这里不好演示,但知道 FILE 这个东西本质是个结构体就行了


🌱文件的打开和关闭

 文件得先打开,才能关闭,最好跟动态内存管理一样,有申请就要有释放,成对出现更为安全。先来说说打开吧!


🪴文件打开


 文件打开用的是 fopen 这个函数,fopen 的作用是从一个文件中以某种方式打开文件,返回类型是 FILE* 即打开文件的起始地址,因此我们需要用一个 FILE* 类型的指针来接收。


582b204a1cf14958bee0a7e2a89aef55.png


注意:文件打开后,要对文件指针进行判断,如果指针为空,说明文件打开失败,此时要报错,并终止后续操作

if (NULL == fp)
{
    //报错函数,说明此文件打开失败
    perror("fopen::test.txt!");
    return 1;//错误结束
}

🪴目标文件


 有两种形式,一种是绝对地址,另一种是相对地址


绝对地址


即唯一路径,使用绝对地址访问文件时,文件可以在电脑中的任意位置,前提是地址要合法。绝对位置的文件标识符必须全,即文件路径+文件名主干+文件名后缀。


//绝对,指此地址是唯一的,能通过这个地址找到唯一的文件
FILE* fp = fopen(" C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\Common7\IDE", "w");

注意:


使用绝对路径时,需要在每个 前额外加一个  原因很简单,单个 与其他字符结合,可能会变成一个转义字符,因此需要两个 \ 来表示一个  


如果是 Mac 就不需要担心这个问题,因为它用的是 /


//绝对,指此地址是唯一的,能通过这个地址找到唯一的文件
FILE* fp = fopen(" C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\Common7\IDE", "w");

相对位置


此时的路径是固定的,一般和当前源文件处于同一位置,相对嘛,就是相对于当前程序文件。相对位置只需要文件名主干+文件后缀就行了 。


比如 devenv.exe ,此时存储位置相对于上面的绝对地址,位于同一目录下


//相对,指在当前工程文件内的文件
FILE* fp = fopen("test.txt", "w");

🪴打开方式


 文件打开方式有很多种,比如只读、只写、读+、二进制写等……


 值得注意的是当我们通过读的方式打开文件时,如果目标文件不存在,那么打开就会失败;但如果是通过写的方式打开文件时,如果文件不存在,会自动创建一个目标文件。


 下面来演示下用写的方式打开文件,然后文件不存在,自动创建文件的情况:


注意:这种是文件的标准使用方式,即先打开,然后判断是否打开失败,如果失败就报错,否则就可以使用文件,最后再关闭文件

//文件创建,通过程序创建
int main()
{
    //可以利用只读的特性:没有就会自己创建
    //这里是相对路径
    FILE* fp = fopen("test.txt", "w");//打开
    if (NULL == fp)
    {
        //报错函数,说明此文件打开失败
        perror("fopen::test.txt!");
        return 1;//错误结束
    }
 
    //进行文件操作……//
 
    fclose(fp);//关闭
    fp = NULL;//置空
    return 0;
}

8fb38b85080445af99bac9c4df397f5e.png

e169aab386084bf6afcd40a858de452d.png


注意:


这个特点很好用,但也很致命,因为每次写文件,都相当于在覆盖文件,假如我们想对原文进行追加,就需要创建原来的数据,再创建新数据,然后一起写入文件中。其实面对这种场景,C语言还提供了另一种文件打开方式 追加 "a" ,下面就是各种打开指令的集合表。


文件打开指令 含义 如果目标文件不存在

"r"        只读 打开一个文件,只能对这个文件进行读取操作 打开失败,报错

"w"        只写 打开一个文件,只能对这个文件进行写入操作 建立目标文件

"a"        追加 向文件末尾处添加数据 建立目标文件

"rb"        只读 打开二进制文件,只能对其进行二进制读取 打开失败,报错

"wb"        只写 打开二进制文件,只能对其进行二进制写入 建立目标文件

"ab"        追加 向二进制文件末尾处添加数据 追加失败,报错

"r+"        读写 打开一个文件,可以进行读取和写入操作 打开失败,报错

"w+"        读写 打开一个文件,可以进行读取和写入操作 建立目标文件

"a+"        读写 对文件末尾处进行读取和写入操作 建立目标文件

"rb+"        读写 打开二进制文件,可以进行二进制读取和写入 打开失败,报错

"wb+"        读写 打开二进制文件,可以进行二进制读取和写入 建立目标文件

"ab+"        读写 对二进制文件末尾处进行读取和写入操作 建立目标文件


🪴文件关闭


 文件关闭用到的是 fclose 这个函数,fclose 就比较简单了,只需要一个参数—文件指针(FILE*),然后就能关闭这个文件指针所指向的文件,感觉有点像 free,功能强大,使用方便。同 free 一样,fclose 关闭文件后,也需要将指针(文件指针)置空,避免出现野指针。


ca1f475ac79e477eb2c0650a9bdec043.png

fclose(fp);//关闭
fp = NULL;//置空

🌲顺序读写

🌱输入、输出流

 在介绍文件读写操作前,需要先说明一下C语言中“流”(format)的概念。假设将数据看作水流,那么它就有两个关键部分:从哪里流出(源头)、流入哪里(终点),其中流出可以看作输出,流入可以看作输入。C语言中有三种流:标准输入输出流、文件输入输出流、二进制输入输出流(实际使用时用前两种流,第三种的目标流一般为文件)。

210a736eab6548828d5cfc958b438356.jpg


🪴标准输入输出流


 标准输入输出流(I/O)包括标准输入流(stdin)—从键盘输入、标准输出流(stdout)—从屏幕输出、标准错误流(strerr)—从屏幕输出,任何一个C程序,只要运行起来都会默认打开以上三个流,比如我们常用的 scanf、printf就是基于标准输入输出流而运行的,这也正是二者需要键盘、屏幕的原因。

061883ed904340ca8381d14fc98fadf6.jpg


🪴文件输入输出流


 顾名思义,文件输入输出流所依赖的载体为文件,无论是输入还是输出数据,都是在文件上进行的,因此它的对象类型为 FILE* ,文件输入输出流可以使用所有输入输出流函数,比如fputc、fprintf、fscanf等,使用时只需要加上目标流类型就行了。


95529d1fba3f4604b2cf0f89ea149954.png


🪴二进制输入输出流


 二进制输入输出流主要适用于文件操作,对文件进行二进制数据的读取和写入,所以二进制输入输出一般用在文件操作中。二进制只有0、1这两个数,因此如果我们使用二进制输出流对某个文件进行写入,文件中存储的信息就变成了一串二进制数(可以使用二进制文件查看器观察),如果用普通文本的形式查看此文件,会得到一串乱码。二进制输入输出流有fwrite、fread这两个函数。

f9a5147e7ed949be92cbeac4b532fb23.png


注意:


printf、scanf、gets等这种不需要指定目标流的函数,设计时就已经规定好了,它们是标准输入输出流函数。而fprintf、fscanf、fgets等这些面向所有输入输出流的函数更为原始,需要用户使用时根据具体情况选择目标流,所以这些函数也能实现标准输入输出流函数的功能,只需要把目标流写成 stdin(输入)、stdout(输出)就行了。下面是各种输入输出


函数的集合表:

功能 函数名 适用于(目标流)

进行单字符的输入 fgetc                  所有输入流                

进行单字符的输出 fputc 所有输出流

文本行输入函数(读取一行数据) fgets 所有输入流

文本行输出函数(写入一行数据) fputs 所有输出流

格式化输入函数 fscanf 所有输入流

格式化输出函数 fprintf 所有输出流

二进制输入函数 fread 文件输入流

二进制输出函数 fwrite 文件输出流


注意:为了方便函数的介绍,接下来会先介绍写入(输出),再介绍读取(输入)函数


🌱fputc 与 fgetc

 fputc 对文件进行单字符的写入,fgetc 读取文件中的单字符


🪴fputc

76326f86fcd94d91a52fbd924b812432.png


//文件读写之逐字符写
//因为没有数据,所以我们先写再读
int main()
{
    //打开
    FILE* fp = fopen("test.txt", "w");
    if (NULL == fp)
    {
        perror("fopen::test.txt!");
        return 1;
    }
 
    //进行操作
    char* pc = "abcdef123";
    //逐字符写入
    while (*pc)
    {
        fputc(*pc, fp);//逐字符放
        pc++;
    }
 
    //关闭
    fclose(fp);
    fp = NULL;
    return 0;
}

e41fa8b7eea84c4ab014a623a234ade9.png

🪴fgetc

3543815a547948efb5c84bb425002dfa.png

//文件读写之逐字符读
int main()
{
    FILE* fp = fopen("test.txt", "r");
    if (NULL == fp)
    {
        perror("fopen::test.txt!");
        return 1;
    }
    //逐字符读取
    int ch = 0;//需要用整型,因为EOF是-1
    while ((ch = fgetc(fp)) != EOF)
    {
        //逐字符读取后,赋给字符变量ch,然后打印
        printf("%c", ch);
    }
 
    //关闭
    fclose(fp);
    fp = NULL;
    return 0;
}


fd63ee3d49cb4028a29828932590cdd6.png

注意:


在读取或写入字符串时,可以通过特定的条件结束读写。比如写入:可以通过字符串自带的结束标志 结束写入;读取:可以通过fgetc的返回值进行判读,如果返回 -1(EOF) 就说明数据已经读取完了。

单纯写文本数据时,要使用指令 "w" ;单纯读数据时,要使用指令 "r" ,指令与操作一定要匹配上,不然就会发生意想不到的错误

🌱fputs 与 fgets

 fputs 对文件进行一行数据的写入,fgets 是读取文件中的一行数据


🪴fputs

3b1d77300af44a80bcb46871221e5d05.png

//文件读写之行写
int main()
{
    FILE* fp = fopen("test.txt", "w");
    if (NULL == fp)
    {
        perror("fopen::test.txt");
        return 1;
    }
 
    char* pc = "这是由标准输入输出流写入的数据";
    fputs(pc, fp);//行写入
    fclose(fp);
    fp = NULL;
    return 0;
}


🪴fgets

7db5936b7f4b4f37b811d00f9531314d.png

//文件读写之行读
int main()
{
    FILE* fp = fopen("test.txt", "r");
    if (NULL == fp)
    {
        perror("fopen::test.txt");
        return 1;
    }
 
    char tmp[30] = "0";
    fgets(tmp, sizeof(tmp), fp);//行读取
    printf("%s
", tmp);
    fclose(fp);
    fp = NULL;
    return 0;
}

1e6773ab02a443e3952e8b9c440ca5e1.png


注意:


行写入时,要确保写入的是字符串数据,传参数时要传地址;行读取时,需要设定待读取数据的数量,一般是跟待存储空间大小相匹配。如果行读取结束,有两种情况:1、因无法读取数据而结束   2、因读取到文件末尾而结束

单纯写文本数据时,要使用指令 "w" ;单纯读数据时,要使用指令 "r"

🌱fprintf 与 fscanf

 fprintf 是对文本进行格式化数据的写入,fscanf 是将文本中的数据进行格式化读取


🪴fprintf

2d5441c980734c55bc35e41237597496.png

//按照文件流格式化写入
struct S
{
    char name[20];
    int age;
    float score;
}a = { "张三",20,88.8f };
int main()
{
    FILE* fp = fopen("test.txt", "w");
    if (NULL == fp)
    {
        perror("fopen::test.txt");
        return 1;
    }
    fprintf(fp, "%s %d %.2f", a.name, a.age, a.score);//适用于所有流的格式化输入输出函数
 
    fclose(fp);
    fp = NULL;
    return 0;
}


🪴fscanf

98a2321b3bb74041af51b48500ddd1f6.png

//按照文件流格式化读取
struct S
{
    char name[20];
    int age;
    float score;
}tmp;
int main()
{
    FILE* fp = fopen("test.txt", "r");
    if (NULL == fp)
    {
        perror("fopen::test.txt");
        return 1;
    }
    fscanf(fp, "%s %d %f", tmp.name, &(tmp.age), &(tmp.score));//适用于所有流的格式化输入输出函数
    printf("%s %d %.2f
", tmp.name, tmp.age, tmp.score);
    fclose(fp);
    fp = NULL;
    return 0;
}


03db17ee88f8491b8588ce759f221081.png


🪴延申:sprintf 和 scanf


 除了 fprintf / fscanf 和 printf / scanf 这两组格式化输入输出外,还存在另一组格式化输入输出函数:sprintf / sscanf


简单介绍一下,sprintf 是把格式化的数据按照一定的格式转换为字符串,相反的,sscanf 就是从字符串中按照一定格式读取出格式化的数据

c5d332da8e0f4a7bac761abfd3e6d037.png

6a65d8e476bb4062bf77e08701ec91a6.png

sprintf 和 sscanf 可以把结构体中的数据打包成一个字符串,也可以对某个字符串进行拆分。这个东西在我们生活中有应用,比如当我们登录账号时,会把账号、密码这个结构体打包成一串字符串,交给后端处理,当然有个更高级的名词:序列化与反序列化。


注意:


printf 输出家族返回的是实际写入(输出)的字符总数(包括转义字符),而 scanf 输入家族返回的是实际读取(输入)的元素个数。举个栗子,字符串 abc ,输出返回 3,输入返回 1,因为此时的字符串视为一个元素。

单纯写文本数据时,要使用指令 "w" ;单纯读数据时,要使用指令 "r"

🌱fwrite 与 fread

 fwrite 是对文件进行二进制数据的写入,fread 是以二进制的形式读取文件中的数据


🪴fwrite

909729bc688d49849ae15c3e1628b117.png

//文件读写之二进制写入
struct S
{
    char name[20];
    int age;
    float score;
}a = { "张三",20,88.8f };
 
int main()
{
    //把a中的数据写到文件中
    FILE* fp = fopen("test.txt", "wb");
    if (NULL == fp)
    {
        perror("fopen::test.txt");
        return 1;
    }
    //二进制的写文件
    fwrite(&a, sizeof(a), 1, fp);
 
    fclose(fp);
    fp = NULL;
    return 0;
}

2f8c1a6a639e4e9eb62c7166386f6ad5.png

🪴fread

f89c2c3493144eae9e07594d5b480603.png

//文件读写之二进制读取
struct S
{
    char name[20];
    int age;
    float score;
}tmp;
 
int main()
{
    //把文件中的数据读取到tmp中
    FILE* fp = fopen("test.txt", "rb");
    if (NULL == fp)
    {
        perror("fopen::test.txt");
        return 1;
    }
    fread(&tmp, sizeof(tmp), 1, fp);
    printf("%s %d %.2f
", tmp.name, tmp.age, tmp.score);
 
    fclose(fp);
    fp = NULL;
    return 0;
}

3fad9c589a844b12a6a1bd240510c83e.png

注意:


当我们使用二进制写入数据到文件时,如果是以文本的方式打开,只能看懂字符串部分,数字部分是看不懂的,我们可以通过VS中的二进制编辑器,来观察其中的数据。

单纯写二进制数据时,要使用指令 "wb" ;单纯读二进制数据时,要使用指令 "rb"