哇塞塞~又和大家见面啦!今天给你们带来的内容是:C语言文件操作。
一、为什么使用文件
当我们在写程序时,我们输入的数据在程序运行结束后将不会被记录保存,这时我们可以使用C语言中的文件操作将我们所需要的数据永久的保存在硬盘上,做到数据持久化,以便于下次我们不必再重复输入数据,可以直接在文件中读取数据。
什么是文件
通俗来讲,存放在磁盘上的文件便可以定义为文件。
但在程序设计中,我们一般所说的文件有两种:程序文件和数据文件(从文件功能的角度来进行分类)。
1.1 程序文件
程序文件包括源程序文件(后缀为.c),目标文件(Windows环境后缀为.obj),可执行文件(Windows环境后缀为.exe)。
1.2 数据文件
文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件。
在没有文件操作的C语言终端程序中,数据的输入和输出都是以终端为对象的,即从终端的键盘输入数据(scanf),运行结果在屏幕上显示(printf)。但是在有文件操作的程序中,我们可以将终端的键盘输入的数据保存在磁盘中,或者将磁盘中的文件数据都取到程序内存中使用。
二、 文件名
一个文件要有一个唯一的文件标识,以便用户识别和引用。为了方便起见,文件标识常被称为文件名。
文件名包括三个部分:文件路径+文件名主干+文件后缀
例如:c:\code\project.txt
c:\code\ 是文件路径;project 是文件名主干;.txt 为文件后缀
三、文件的打开和关闭
当我们在读写文件时需要先建立一个文件信息区来记录将要使用文件的各种数据(如文件的名字、文件状态及文件所在的位置等等),在C语言中记录这些数据的类型可以使用文件指针,下面我们来深入研究文件指针。
3.1 文件指针
缓冲文件系统中,关键的概念是“文件类型指针“,简称“文件指针”。
每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(如文件的名字,文件状态及文件当前的位置等),这些信息是保存在一个结构体变量中的。该结构体类型是有系统声明的,取名FILE。
现在我们以画图的形式能更直观的理解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结构的变量,并填充其中的信息,使用者不必关心细节。
- 一般都是通过一个FILE的指针(找到文件信息区的起始地址)来维护这个FILE结构的变量,这样使用起来更加方便。
我们可以创建一个FILE*的指针变量:
FILE* pf;//文件指针变量
这里定义pf是一个指向FILE类型数据的指针变量。可以使pf指向某个文件的文件信息区(是一个结构体变量),通过该文件信息区中的信息就能够访问该文件。也就是说,通过文件指针变量能够找到与它关联的文件。
例如以下图解:
3.2 文件的打开与关闭
我们现在清楚了文件操作的关联原理,下面来进行文件打开和关闭的实操:
文件在读写之前应该先打开文件,在使用结束之后应该关闭文件。
ANSIC规定使用fopen函数来打开文件,fclose来关闭文件。
在编写程序的时候,在打开文件的同时,fopen函数都会返回一个FILE*的指针变量指向该文件,也相当于建立了指针和文件的关系。
//打开文件
FILE* fopen(const char* filename,const char*mode);
//关闭文件
int fclose(FILE* stream);
filename:传入将要打开的文件名;
mode:传入文件的打开方式;
stream:传入将要关闭的文件。
打开方式如下表:
文件使用方式 | 含义 | 如果指定文件不存在 |
"r"(只读) | 为了输入数据,打开一个已经存在的文本文件 | 出错 |
"w"(只写) | 为了输出数据,打开一个文本文件 | 建立一个新的文件 |
"a"(追加) | 向文本文件尾添加数据 | 建立一个新的文件 |
"rb"(只读) | 为了输入数据,打开一个二进制文件 | 出错 |
"wb"(只写) | 为了输出数据,打开一个二进制文件 | 建立一个新的文件 |
"ab"(追加) | 向一个二进制文件尾添加数据 | 出错 |
"r+"(读写) | 为了读和写,打开一个文本文件 | 出错 |
"w+"(读写) | 为了读和写,建一个新的文件 | 建立一个新的文件 |
"a+"(读写) | 打开一个文件,在文件尾进行读写 | 建立一个新的文件 |
"rb"(读写) | 为了读和写,打开一个二进制文件 | 出错 |
"wb+"(读写) | 为了读和写,新建一个新的二进制文件 | 建立一个新的文件 |
"ab+"(读写) | 打开一个二进制文件,在文件尾进行读和写 | 建立一个新的文件 |
注:当使用“w”,“wb”,“w+”,“wb+”打开文件时会清除文件原本存储的数据。
该函数的返回值是一个FILE类型的指针,在文件成功打开后返回此文件信息区的起始地址,打开失败则返回一个NULL(空指针),所以使用fopen时需要一个FILE类型的指针来接收其返回值。
例如 :
//打开
FILE* pf=fopen("text.txt","w");
//判断文件是否打开成功
if(pf==NULL)
{
perror("fopen");
return 1;
}
四、文件的顺序读写
文件打开之后会进行一系列读写操作的,下面我们来详细讲解文件的读写操作:
读写文件时我们有两种读取方式:顺序读取和随机读取。
4.1 顺序读取
功能 | 函数名 | 适用于 |
字符输入函数 | fgetc | 所有输入流 |
字符输出函数 | fputc | 所有输出流 |
文本行输入函数 | fgets | 所有输入流 |
文本行输出函数 | fputs | 所有输出流 |
格式化输入函数 | fscanf | 所有输入流 |
格式化输出函数 | fprintf | 所有输出流 |
二进制输入 | fread | 文件 |
二进制输出 | fwrite | 文件 |
1 对比一组函数:
- scanf:针对标准输入流的格式化输入函数;
- printf:针对标准输出流的格式化输出函数 ;
- fscanf:针对所有输入流的格式化输入函数;
- fprintf :针对所有输出流的格式化输出函数;
- sscanf:将字符串中转化出一个格式化的数据;
- sprintf:将格式化的数据转化成字符串 。
2 延伸:输入流与输出流
在我们编写的程序中我们可以将程序运行时的内存数据通过printf函数输入到屏幕,也可以将键盘上的数据通过scanf函数输入到内存中,这两种从屏幕输出数据和从键盘输入数据的方式分别被称为标准输出流和标准输入流:
如下是站在内存的角度进行思考:
这种方式的输入(输出)数据被称为输入 (输出)流。
那我们这些输入和输出文件数据的函数是否能输入或输出外部设备数据(如键盘、屏幕)呢?
对任意一个c程序,只要运行起来,便默认打开3个流:
- stdin——标准输入流——键盘
- stdout——标准输出流——屏幕
- stderr——标准错误流——屏幕
因此在C程序运行时,系统自动打开三个FILE*类型的流:stdin(接收键盘数据),stdout(接收所要向屏幕输出的数据),stderr(接收所要向屏幕输出的数据)。
4.2 随机读取
在介绍随机读写函数之前我们要了解一下文件指针的偏移 。
每读完一次数据时下一次再重新读取时不会从文件开始的位置去读,而是从上一次读取结束的地方再次读取,这就是文件指针的偏移。
下面通过画图的方式更便于大家理解:
下面我们来深入分析随机读写文件的函数:
1 fseek
根据文件指针的位置和偏移量来定位文件指针:
int fseek(FILE* stream,long int offset,int origin);
该函数有FILE*类型stream,long int类型offset,int类型origin三个参数:
- stream:传入将要改变的文件指针(文件流);
- offsrt:传入指针所要偏移的偏移量;
- origin:设置传入的stream指针的起始位置(可传入3种参数:SEEK_SET(设置指针指向文件开头)、SEEK_CUR(不改变指针位置)、SEEK_END(设置指针指向文件结束位置))。
该函数运行成功,返回零。否则回非零值;若发生读取或写入错误,则设置错误指示器(ferror)。
2 ftell
返回文件指针相对于起始位置的偏移量。
long int ftell(FILE* stream);
该函数只有一个FILE*类型的参数stream:
- stream:传入所需计算偏移量的文件指针。
运行成功后,返回位置指示器的当前值。运行失败时,返回 -1,并将 errno 设置为系统特定的正值 。
3 rewind
让文件指针的位置回到文件的起始位置。
void rewind(FILE* stream);
该函数只有一个FILE*类型的参数stream:
- stream:传入将要改变的文件指针。
五、文本文件和二进制文件
根据数据的组织形式,数据文件被称为文本文件或者二进制文件。
- 数据在内存中以二进制的形式存储,如果不加转换的输出到外存,就是二进制文件。
- 如果要求在外存上以ASCII码的形式存储,则需要在存储前转换,以ASCII字符的形式存储的文件就是文本文件。
- 数组在内存中字符一律以ASCII形式存储,数值型数据既可以用ASCII形式存储,也可以使用二进制形式存储。
我们以10000这个数字来举例,用ASCll码来存储需要5个字节(因为10000是五位数,每一位数都要用一个字节的ASCll码来表示),而用二进制存储只需要四个字节(一个整型大小为4字节)。
下面我们使用二进制存储的方式存入10000这个数据:
int main()
{
//打开
FILE* pf = fopen("text.txt", "wb");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//存入
int a = 10000;
fwrite(&a, sizeof(int), 1, pf);
//关闭
fclose(pf);
pf = NULL;
return 0;
}
存入后使用记事本打开此文件:
此时显示的是乱码(记事本看不懂二进制文件)
那我们想看懂二进制文件该怎么办呢?
VS就可以帮助我们读取二进制文件,具体操作如下图:
打开后的结果为:
这里可以看到10000这个数据在二进制中存储为10 27 00 00(前面00000000是地址),0 27 00 00是将二进制数据转换为十六进制并以小端存储方式显示的。
六、文件读取结束的判断定
在文件读取过程中,不能用feof函数的返回值直接用来判断文件的是否结束。而是应用于当文件读取结束的时候,判断是读取失败结束,还是遇到文件尾结束
1. 文本文件读取是否结束,判断返回值是否为EOF(fgetc),或者NULL(fgets)
例如:
fgetc判断是否为EOF。
fgets判断返回值是否为NULL。
2. 二进制文件的读取结束判断,判断返回值是否小于实际要读的个数
例如:
fread判断返回值是否小于实际要读的个数。
七、文件缓冲区
ANSIC 标准采用“缓冲文件系统”处理的数据文件的,所谓缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块“文件缓冲区”。从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上。如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓冲区的大小根据C编译系统决定的。
以下是程序向硬盘输入数据和硬盘向程序输出数据的流程:
结论:
因为有缓冲区的存在,C语言在操作文件的时候,需要做刷新缓冲区或者在文件操作结束的时候关闭文件;
如果不做,可能导致读写文件的问题 。
看到这里本期分享已经接近尾声,文章对C语言中文件操作做了全方位的讲解,如果这篇博客对大家有帮助的话记得给博主一个三连哈~你们的支持是我创作的最大动力!望君加油,不负自己!那我们下期再会啦~