文章目录
- 一、什么是文件
- 1.程序文件
- 2.数据文件
- 二、数据文件
- 1.文件名
- 2.数据文件的分类
- 文本文件
- 二进制文件
- 三、文件的打开和关闭
- 1.流和标准流
- 流
- 标准流
- 2.文件指针
- 3.文件的打开和关闭
- 文件的打开
- 文件的关闭
- 四、文件的顺序读写
- 1.fgetc函数
- 2.fputc函数
- 3.fgets函数
- 4.fputs函数
- 5.fscanf函数
- 6.fprintf函数
- 7.fwrite函数
- 8.fread函数
一、什么是文件
我们写的程序的数据是存储在电脑的内存中,如果程序退出,内存回收,数据就丢失了,等再次运⾏程序,是看不到上次程序的数据的,如果要将数据进⾏持久化的保存,我们可以使⽤⽂件
文件是计算机系统中的一个基本概念,它是存储在计算机上的信息集合,可以是文本文档、图片、程序等,但是在程序设计中,我们⼀般谈的⽂件有两种:程序⽂件、数据⽂件(从⽂件功能的⻆度来分类的)
1.程序文件
程序⽂件包括源程序⽂件(后缀为.c),⽬标⽂件(windows环境后缀为.obj),可执⾏程序(windows环境后缀为.exe)
2.数据文件
文件的内容不⼀定是程序,而是程序运行时读写的数据,比如程序运⾏需要从中读取数据的⽂件,或者输出内容的⽂件
二、数据文件
本文着重讨论的是数据文件,在以前我们学的知识中,所处理的数据的输⼊都是以键盘输⼊数据,用显示器输出
但是我们之前的程序结果输出到显示器后,结束程序,这个结果不会被保存,那是因为我们运行程序时,把数据放在了内存,程序结束后,内存回收了,数据也就没了
那我们很多情况下想把数据永久保存起来,就要使用磁盘上的数据文件存储起来,存储到磁盘的数据就会一直保存,当需要使用数据时,就从数据文件中将数据读入到我们的内存进行操作,本文将会讲解的就是操作数据文件
1.文件名
⼀个⽂件要有⼀个唯⼀的⽂件标识,以便⽤⼾识别和引⽤,这个文件标识就是我们常说的文件名,⽂件名包含3部分:⽂件路径+⽂件名主⼲+⽂件后缀
例如: c:\code\test.txt
在这个例子中,文件的路径就是c:\code\,文件名的主干就是test,文件后缀是.txt,说明这个文件是一个文本文件,属于数据文件之一
而路径又可以分为相对路径和绝对路径,上面演示的就是绝对路径,如果对这个有兴趣的话,可以自行搜索文章学习,这里我们还是继续进行我们的文件操作的学习
2.数据文件的分类
根据数据的组织形式,数据⽂件被称为⽂本⽂件或者⼆进制⽂件
文本文件
如果要求在外存上以ASCII码的形式存储,则需要在存储前转换,以ASCII字符的形式存储的⽂件就是⽂本⽂件
简单的理解就是,如果打开这个文件你可以看懂上面的信息,那么就是文本文件,例如汉字,英文字母等等信息,文本文件常见的后缀为:.txt 和 .docx 以及 .rtf
二进制文件
数据在内存中以⼆进制的形式存储,如果不加转换的输出到外存的⽂件中,就是⼆进制⽂件,由于里面是0和1序列组成的二进制,然后转换出来的字符,所以根本看不懂里面的内容
我们可以在VS运行下面的代码,如果不懂也没有关系,后面会讲到,这里只是举一个例子:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main()
{
int a = 10000;
//以二进制写的方式打开
FILE* pf = fopen("test.txt", "wb");
if (pf == NULL)
{
perror(fopen);
return 1;
}
//⼆进制的形式写到⽂件中
fwrite(&a, 4, 1, pf);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
我们运行它之后会发现当前代码路径下会多出一个test.txt的文件,打开当前代码路径的方法是单击左上角文件夹图标,如图:
然后我们双击打开这个文件:
可以看到我们明明是将10000写入到了这个文件中,但是最后我们打开文件后发现是一个我们看不懂的字符,原因就是我们写入时,是以二进制的写入方式打开文件的,里面存储的是二进制的信息
三、文件的打开和关闭
1.流和标准流
流
我们程序的数据需要输出到各种外部设备,也需要从外部设备获取数据,不同的外部设备的输⼊输出操作各不相同,为了⽅便程序员对各种设备进行方便的操作,我们抽象出了流的概念,我们可以把流想象成流淌着字符的河
比如向文件里输入信息和向屏幕输入信息的方式不同,但是程序员不必了解它们如何输入的,程序员只需要去往对应的流写入或读出操作,不需要担心各种设备的输入输出操作
⼀般情况下,我们要想向流⾥写数据,或者从流中读取数据,都是要打开流,然后操作,打开流的方法我们后面会讲到
标准流
刚刚提到了,如果要输入或者读取信息,都要打开流,然后进行操作,那么每次我们在键盘输入信息,在屏幕上打印信息为什么没有专门打开流呢?那是因为C语言程序在启动时,默认打开了3个标准流:
- stdin - 标准输⼊流,在⼤多数的环境中从键盘输⼊,scanf函数就是从标准输⼊流中读取数据
- stdout - 标准输出流,⼤多数的环境中输出⾄显⽰器界⾯,printf函数就是将信息输出到标准输出流中
- stderr - 标准错误流,⼤多数环境中输出到显⽰器界⾯
这是默认打开的三个标准流,我们使⽤scanf、printf等函数就可以直接进⾏输⼊输出操作的,它们的类型是FILE*的指针,通常称为文件指针,对文件的操作就是使用文件指针进行操作
2.文件指针
缓冲⽂件系统中,关键的概念是“⽂件类型指针”,简称“⽂件指针”,每个被使⽤的⽂件都在内存中开辟了⼀个相应的⽂件信息区,⽤来存放⽂件的相关信息(如⽂件的名字,⽂件状态及⽂件当前的位置等)
这些信息是保存在⼀个结构体变量中的,该结构体类型是由系统声明的,这个结构体就叫FILE,我们可以在VS2013编译环境提供的 stdio.h 头⽂件中有以下的⽂件类型申明:
struct _iobuf {
char* _ptr;
int _cnt;
char* _base;
int _flag;
int _file;
int _charbuf;
int _bufsiz;
char* _tmpfname;
};
typedef struct _iobuf FILE;
不同的C编译器的FILE类型包含的内容不完全相同,但是⼤同⼩异,每当打开⼀个⽂件的时候,系统会根据⽂件的情况⾃动创建⼀个FILE结构的变量,并填充其中的信
息,使⽤者不必关⼼细节
C语言⼀般都是通过⼀个FILE的指针来维护这个FILE结构的变量,这样使⽤起来更加⽅便,下面我们可以创建一个文件指针变量:
FILE* pf;//⽂件指针变量
这里定义的pf是⼀个指向FILE类型数据的指针变量,可以使pf指向某个⽂件的⽂件信息区(⼀个结构体),通过该⽂件信息区中的信息就能够访问该⽂件。也就是说,通过⽂件指针变量能够间接找到与它关联的⽂件,如图:
3.文件的打开和关闭
⽂件在读写之前应该先打开⽂件,在使⽤结束之后应该关闭⽂件,现在我们就来学习如何打开和关闭文件
ANSI C 规定使⽤ fopen 函数来打开⽂件, fclose 来关闭⽂件,在打开⽂件的同时,它们都会返回⼀个FILE*的指针变量指向该⽂件,也相当于建⽴了指针和⽂件的关系
文件的打开
我们来看看打开文件的函数fclose的原型:
FILE * fopen ( const char * filename, const char * mode );
如果文件打开成功了,那么就会返回一个FILE*的指针,我们可以用一个文件指针变量接收,然后我们后续就可以通过这个文件指针变量对这个文件进行操作
如果文件打开失败了,那么就会返回一个空指针NULL,所以我们在使用fopen后,最好再判断一下它的返回值是否是空指针,如果是空指针说明文件打开失败,直接返回
它的参数有两个,第一个是我们要打开的文件的名字,第二个参数是我们打开文件的方式,是以读的方式,还是写的方式,还是读写等等方式,如下图:
在上图中展示了文件打开的方式,以及如果文件不存在,会做出什么操作,现在我们还没有讲解怎么对文件进行读写,所以有点懵也没有关系,在后面的读写部分都会讲解,这里只了解一下
接下来我们来试着写一个代码,以只读的方式打开一个文本文件test.txt,如下:
FILE* pf = fopen("test.txt", "r");//打开文件
//判断是否打开成功,打开失败就返回错误信息并返回:
if (pf == NULL)
{
perror("fopen");
return 1;
}
这样我们就打开了文件了,至于读写操作我们后面讲,现在先来看看如何关闭文件
文件的关闭
我们来看看关闭文件的函数fclose的原型:
int fclose ( FILE * stream );
它的返回值是int类型,如果文件关闭成功就返回0,如果文件关闭失败就返回EOF
它的参数是我们要关闭的流,在这里我们要关闭文件,就把文件的流,也就是对应的文件指针变量传过来
我们要注意的是,关闭文件后,pf这个指针变量就指向野指针了,所以最好关闭文件后将其置为空指针NULL,我们来看看关闭文件关闭的过程:
//关闭⽂件
int fclose (pf);
//为了防止pf成为野指针,可以把它置为空指针
pf = NULL;
这就是我们关闭文件的过程,接下来我们就学习最关键的文件读写操作
四、文件的顺序读写
文件的顺序读写就是按照文件数据从头到尾进行读写,读写操作也是由我们的函数来完成的,如下表:
我们接下来就就一一讲解这些函数:
1.fgetc函数
我们要学习的第一个函数是fgetc,它的作用就是从流中获取一个字符,不是应该属于输出吗?那么为什么在表中它叫字符输入函数呢?
这是我们要注意的一点,我们说的输入输出是站在内存的角度思考的,我们从流里面获取了一个字符,对流来说,也就是对文件来说是输出,但是如果站在内存角度思考就会发现,获取的字符存储到内存中了,应该是属于输入,所以我们说的输入输出都是基于内存角度的
实在不理解也没什么,我们掌握它的用法就行,我们来看看它的原型:
int fgetc ( FILE * stream );
它的参数就是我们要从哪个流里面获取一个字符,在这里我们要操作文件,很明显就是从文件流里面获取字符,所以需要填一个文件指针变量进去
它的返回值是整型,如果成功从文件中读取了一个字符,那么就返回这个字符的Ascll码值,如果读取失败或者读取到了文件末尾,那么就返回EOF,现在我们就来使用一下它
我们首先明确一下条件,在当前代码路径下有一个test.txt的文件,里面的内容是hello world!,然后我们开始写代码,由于这是第一遍,所以我来带大家实现一下全过程,后面文件的打开和关闭就不会再讲解了
首先我们要以读的形式打开文件,然后用文件指针变量接收,判断返回值是否为空,如下:
#include <stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
return 0;
}
随后我们就开始使用fgetc函数来实现读取操作,用一个字符变量ch来接收它的返回值,然后打印ch,如下:
char ch = fgetc(pf);
printf("%c\n", ch);
随后就是最后一步:关闭文件,注意关闭文件后要把pf置为空指针,如下:
fclose(pf);
pf = NULL;
完整代码:
#include <stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
char ch = fgetc(pf);
printf("%c\n", ch);
fclose(pf);
pf = NULL;
return 0;
}
最后我们来运行一下代码:
可以看到我们成功读取了一个字符h,那么问题来了,如果我想将文件中的字符全部读出来怎么办呢?我们也不是每一次都知道文件中有多少个字符
这个时候我们可以利用fgetc的返回值,创建一个while循环,只要fgetc的返回值不是EOF就一直循环,每次循环把读取到的字符打印出来,直到将所有字符读取完毕返回EOF结束循环,如下:
char ch = 0;
while ((ch = fgetc(pf)) != EOF)
{
printf("%c", ch);
}
接着我们再次运行程序试试:
可以看到这里就把文件中的所有字符都读出来了
2.fputc函数
fgetc函数和fputc函数很相似,只是fgetc是将一个字符从流中读出,而fputc的作用是将一个字符写入到文件中,我们来看看它的原型:
int fputc ( int character, FILE * stream );
它的第一个参数就是我们要写入的字符,第二个参数就是我们的流,我们操作文件,所以要写文件流
如果写入成功,那么它的返回值就是这个字符的Ascll码值,如果失败就返回EOF,当然,它的返回值我们很少用到
接着我们就使用一下这个函数,这里要强调的一点是,以写的方式打开文件,第一步会清空文件中的内容,然后再进行写的操作,如果不想文件中的内容被清楚,可以使用追加的方式打开
我们这里就可以使用写的方式打开test.txt文件,让它将里面的hello world!删除了,然后我们再使用fputc来写入一个字符,如下:
#include <stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
char ch = 0;
fputc('x', pf);
fclose(pf);
pf = NULL;
return 0;
}
运行代码后屏幕上没有出现任何信息,接着我们就来看看test.txt文件有没有按预期被修改,如图:
可以看到,hello world!已经被清除了,并且字符x已经被我们写入到文件了
3.fgets函数
fgets函数的作用是从文件中读出一行的信息,我们来看看它的原型:
char * fgets ( char * str, int num, FILE * stream );
它的第一个参数就是我们要把读出的一行数据放入哪个字符串,第二个参数就是我们要读出几个字符,最后一个参数就是要从哪个流中读取数据
如果读取成功,那么它的返回值就是从文件中读取出的第一行的字符串的首地址,可以使用%s的形式打印出来,如果读取失败,则会返回空指针NULL
接着就让我们使用一下这个函数,首先明确前提,当前目录下有一个test.txt的文件,里面的内容有两行,第一行是hello,第二行是world!,我们来读取它的第一行,然后把它的第一行内容打印出来:
char arr[20] = { 0 };
fgets(arr, 20, pf);
printf("%s\n", arr);
完整代码:
#include <stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
char arr[20] = { 0 };
fgets(arr, 20, pf);
printf("%s\n", arr);
fclose(pf);
pf = NULL;
return 0;
}
我们来看看代码运行截图:
还是同样的问题,如果我想将文件的所有行都读出来呢?虽然我们现在知道有两行数据,可以只调用两次fgets函数,但是万一下次遇到很多行数据呢?
所以这里我们还是要利用它的返回值,创建一个while循环,如果fgets没有返回空指针,说明读取到了一行信息,那么我们就把它打印出来,如果返回空指针就结束循环,如下:
char arr[20] = { 0 };
while (fgets(arr, 20, pf))
{
printf("%s", arr);
}
我们来看看代码运行结果:
可以看到代码自动把所有行打印出来了
4.fputs函数
fgets函数和fputc函数很相似,只是fgets是将一行字符从流中读出,而fputs的作用是将一行字符写入到文件中,我们来看看它的原型:
int fputs ( const char * str, FILE * stream );
这个函数的第一个参数就是我们要写入的字符串,第二个参数就是要写入的流
如果文件写入成功,那么就返回一个非零的值,如果写入失败就返回EOF
接着我们就来使用fputs向文件test.txt写入一行字符hello world!,如下:
char arr[20] = "hello world!";
fputs(arr, pf);
完整代码:
#include <stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
char arr[20] = "hello world!";
fputs(arr, pf);
fclose(pf);
pf = NULL;
return 0;
}
然后我们运行代码后来看看test.txt有没有被写入这一行信息:
可以看到文件里已经成功写入了一行信息
5.fscanf函数
fscanf是以格式化的方式对文件进行读取操作,它与scanf函数的使用方法相似,它们的区别就是fscanf的第一个参数是流,后面和scanf的参数一样,我们来对比一下scanf和fscanf的原型:
//scanf的原型:
int scanf ( const char * format, ... );
//fscanf的原型:
int fscanf ( FILE * stream, const char * format, ... );
可以看到它们的区别就是fscanf多一个流的选择,它们的返回值也是一样的,都是返回成功读取的项目的个数,如果读取失败返回EOF,如果还不熟悉scanf可以参考文章:
【C语言】printf和scanf函数详解
我们这里也可以顺便说一下它们之间的关系,scanf是从标准输入流读取数据,而fscanf可以从任何流中读取数据,那么fscanf也必然可以从标准输入流读取数据,此时它们的作用就是一致,我们在上面也说过标准输入流是stdin,我们将fscanf的第一个参数写成标准输入流stdin就可以了,如下:
fscanf (stdin , const char * format, ... );
//等价于scanf
说明了它们的关系,我们就来示例使用一下fscanf,我们的前提条件是:当前文件夹下有一个test.txt文件,里面包含的数据有:123 hello,现在我们要以格式话的方式将它们读取出来,也就是将123读取为整型,hello读取为字符串
首先我们要创建一个整型变量和一个字符数组,用来存储我们读取到的信息,然后将它们打印出来,代码如下:
int i = 0;
char arr[20] = { 0 };
fscanf(pf, "%d %s", &i, arr);
printf("%d %s", i, arr);
可以看到fscanf和scanf确实只有第一个参数的不同,接着我们来看看完整的代码:
#include <stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
int i = 0;
char arr[20] = { 0 };
fscanf(pf, "%d %s", &i, arr);
printf("%d %s", i, arr);
fclose(pf);
pf = NULL;
return 0;
}
我们来看看代码运行结果:
6.fprintf函数
fprintf函数和printf函数又非常相似,也是第一个参数不同,其它的使用方法一致,fprintf的原型如下:
int fprintf ( FILE * stream, const char * format, ... );
它的返回值就不说了,与printf一致,不知道的可以看上面的链接,有printf的详细使用教程,它的参数也只是比printf多一个
它们只是作用不同,fprintff的作用是向所有流中写入数据,而printf是向标准输出流写入数据,fprintf要全面一些,当fprintf的第一个参数是标准输出流stdout的时候,它的作用就和printf相同了,如下:
fprintf(stdout, const char * format, ... )
//等价于printf
说完它们的关系我们就回到正题,来使用一下fprintf函数对文件test.txt写入一些格式化的数据,比如写入字符串world和浮点型的3.14,如下:
float f = 3.14f;
char arr[20] = "world";
fprintf(pf, "%s %f", arr, f);
完整代码如下:
#include <stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
float f = 3.14f;
char arr[20] = "world";
fprintf(pf, "%s %f", arr, f);
fclose(pf);
pf = NULL;
return 0;
}
接着我们运行代码来看看文件test.txt有没有被修改,如下:
可以看到文件被成功写入了格式化的数据
7.fwrite函数
我们要讲的最后两个函数fread和fwrite与上面讲的函数不同,上面的函数都是对文件写入或读取我们看得懂的文本信息,而这两个函数是对文件写入或读取二进制信息
也就是对二进制文件进行操作,所以打开文件时要使用rb或者wb的方式,我们首先来看看fwrite函数,它是向文件写入二进制的信息,它的原型如下:
size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream );
它的第一个参数是我们要写入的信息的首地址,第二个参数size就是我们要写入的信息的一个元素的大小,第三个参数count是我们要写入的元素的个数,最后一个参数就是我们要往哪个流写入信息
它的返回值就是被成功写入文件的元素个数
接着我们就赶紧去使用一下fwrite为test.txt文件写入一些二进制信息,我们要写的就是整型1到5,我们可以使用数组的方式,如下:
int arr[] = { 1,2,3,4,5 };
fwrite(arr, sizeof(arr[0]), 5, pf);
然后我们来看看完整代码:
#include <stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "wb");//二进制写的方式打开
if (pf == NULL)
{
perror("fopen");
return 1;
}
int arr[] = { 1,2,3,4,5 };
fwrite(arr, sizeof(arr[0]), 5, pf);
fclose(pf);
pf = NULL;
return 0;
}
然后我们来运行一下代码,看看test.txt文件有没有发生改变:
可以看到test.txt文件被写入了一些二进制信息,但是我们看不出来是什么,也就不知道里面是不是装的我们写入的整型1到5,所以我们接下来学习对二进制文件信息进行读取的函数fread
8.fread函数
fread函数的作用就是从文件中读取二进制的信息,刚好和fwrite搭配使用,我们来看看它的原型:
size_t fread ( void * ptr, size_t size, size_t count, FILE * stream );
可以看到它的参数和返回值都和fwrite差不多,没错,它们的原型的含义基本一致,这里就不多讲了
在刚刚使用了fwrite向文件写入了整型1到5后,我们看不出来文件中的内容是否正确,现在我们就使用fread将里面的信息读取出来,看看是否是整型1到5,如下:
int arr[5] = { 0 };
fread(arr, sizeof(int), 5, pf);
for (int i = 0; i < 5; i++)
{
printf("%d ", arr[i]);
}
完整代码:
#include <stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "rb");
if (pf == NULL)
{
perror("fopen");
return 1;
}
int arr[5] = { 0 };
fread(arr, sizeof(int), 5, pf);
for (int i = 0; i < 5; i++)
{
printf("%d ", arr[i]);
}
fclose(pf);
pf = NULL;
return 0;
}
我们来运行代码看看fread帮我们从test.txt读出的信息是否是整型1到5:
可以看到成功打印出来了整型1到5,从这里也验证了之前我们的fwrite使用正确了
今天的分享就到这里结束啦,虽然只是文件操作的一部分,但是还是有一万字,基本上讲完了我们在读写时使用的函数,但是还是没有把文件操作部分要掌握的内容完全讲完
所以文件操作还有下一篇文章,uu们敬请期待~
那么今天说到这里,bye~