目录
一.基础概念
1.硬盘 / 磁盘
硬盘 / 磁盘:存储数据的硬件(持久性 -- 断电后数据依然存在),作为数据的持久化存储。
内存条:吞吐量更大的存储硬件(易逝性 -- 断电后数据消失),作为数据处理的中间缓冲带。
硬盘是一个整体,就是一个存储数据的介质(并不管存储的是什么数据)。
硬盘硬件层面上数据的读写是以扇区作为单位的 -- 通常是512B。
2.IO
IO:input / output -- 输入 / 输出。
- input:输入,表示从硬盘获取数据。
- output:输出,将数据写入外设(硬盘)。
3.文件
(1).为什么要引入文件?
在我们用户层面,对数据的读写要有上层的区分:
比如我有两段代码在硬盘中存储,怎么区分从哪里到哪里(数据),是第一段代码呢?
因此在上层就提到了一个概念:文件 -- 以文件进行应用层面的管理。
比如有个a.c文件,使用了哪几个磁盘块存储数据,有多大,有什么属性。
要对磁盘以文件为单位进行操作,我们必须明确这个文件要操作的是磁盘的哪一块数据,能否操作,以及读写到哪里了。因此在程序中我们要操作一个文件,必须在程序中先把这个文件给描述出来,通过这些描述完成对指定磁盘指定位置的数据的操作。
大佬们除了在操作系统内核中对打开的文件的描述外,在上层还进行了一层上层的封装(为了让我们程序员对于文件的操作更加简洁方便)。因此我们在程序中操作文件就是通过这些封装的信息进行的。
(2).概念
文件:磁盘上的文件是文件。
在程序设计中,我们一般谈的文件有两种:程序文件、数据文件。
- 程序文件:包括源程序文件(后缀为.c),目标文件(windows环境后缀为.obj),可执行程序(windows环境后缀为.exe)。
- 数据文件:文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件。
在文件IO中一般是指数据文件。
文件名:一个文件要有一个唯一的文件标识,以便用户识别和引用。
文件名包含3部分:文件路径+文件名主干+文件后缀。
eg: c:\code\test.txt
为了方便起见,文件标识常被称为文件名。
文件类型:根据数据的组织形式,数据文件被称为文本文件或者二进制文件。
数据在内存中以二进制的形式存储,如果不加转换的输出到外存,就是二进制文件。文件的操作:以文件为单位对磁盘进行操作。
4.文件缓冲区
文件缓冲区:ANSIC 标准采用“缓冲文件系统”处理的数据文件的,所谓缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块“文件缓冲区”。从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上(特性先进先出)。如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓冲区的大小根据C编译系统决定的。
5.文件指针
文件指针:缓冲文件系统中,关键的概念是“文件类型指针”,简称“文件指针”。
每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(如文件的名字,文件状态及文件当前的位置等)。这些信息是保存在一个结构体变量中的。该结构体类型是有系统声明的,取名FILE.
eg:VS2008编译环境提供的 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指向某个文件的文件信息区(是一个结构体变量)。通过该文件信息区中的信息就能够访问该文件。也就是说,通过文件指针变量能够找到与它关联的文件。
三.相关操作函数
1.打开文件——fopen
FILE *fopen(char *filename,char *mode);
- filename: 文件名 -- 用户对文件标识的区分。
- mode: 文件打开方式 -- 决定了我们唔够对文件进行什么样的操作。
返回值:成功返回文件流指针 -- 文件的操作句柄(因为接下来文件所有操作都是通过这个文件流指针进行的);失败则返回NULL。
PS:句柄:一个广泛的概念,就像遥控器一样。
打开方式如下:
文件使用方式 | 含义 | 如果指定文件不存在 |
“r”(只读) | 为了输入数据,打开一个已经存在的文本文件 | 出错 |
“w”(只写) | 为了输出数据,打开一个文本文件 | 建立一个新的文件 |
“a”(追加) | 向文本文件尾添加数据 | 出错 |
“rb”(只读) | 为了输入数据,打开一个二进制文件 | 出错 |
“wb”(只写) | 为了输出数据,打开一个二进制文件 | 建立一个新的文件 |
“ab”(追加) | 向一个二进制文件尾添加数据 | 出错 |
“r+”(读写) | 为了读和写,打开一个文本文件 | 出错 |
“w+”(读写) | 为了读和写,建立一个新的文件 | 建立一个新的文件 |
“a+”(读写) | 打开一个文件,在文件尾进行读写 | 建立一个新的文件 |
“rb+”(读写) | 为了读和写打开一个二进制文件 | 出错 |
“wb+”(读写) | 为了读和写,新建一个新的二进制文件 | 建立一个新的文件 |
“ab+”(读写) | 打开一个二进制文件,在文件尾进行读和写 | 建立一个新的文件 |
注意:
1. b:以二进制形式打开文件,默认是以文本形式打开(在文本模式下操作\n换行就会被解释成为\r\n写入文件)。
2. 不管是哪种方式,文件打开后,默认的读写位置都在起始位置(r+,w,w+写入数据时默认都是从起始位置覆盖写入;a-会将读写位置先移动到文件末尾,然后再写入数据)。
2.写文件——fwrite
size_t fwrite(char *buf,size_t bsize,size_t nmem,FILE *fp);
- buf:一块内存空间的地址,表示要把哪块空间中的数据写入到文件中。
- bsize:块大小; nmem:块个数。
bsize*nmem就是实际要将buf中多少字节的数据写入到文件中。
- fp:fopen返回的文件流指针(操作句柄),表示了我们要操作哪个文件(因为一个程序中可能会打开很多文件)。
返回值:成功返回实际写入文件的完整块个数;失败则返回小于nmem大小的数字。
eg:要给文件写入10个int大小的数据,bsize = sizeof ( int ) , nmem=10;
但是磁盘剩余空间只有10个字节,因此只能写入2.5个 int 数据,因此返回值就是2(因为只成功写入了2个完整块数据)。
3.读文件——fread
size_t fread(char *buf,size_t bsize,size_t nmem,FILE *fp);
- buf:一块内存空间的首地址,表示要把从文件读取到的数据放到内存中buf这块空间中。
- bsize:块大小; nmem:块个数。
bsize*nmem就是实际要从文件中读取多少数据。
- fp:fopen返回的文件流指针--操作句柄--表示了我们要操作哪个文件。
返回值:成功返回nmem;失败则返回实际读取到的完整块个数。
eg:假设要从文件中读10个int大小的数据,则 bsize = sizeof ( int ) , nmem=10; 表示总共要读取40个字节数据。
但是文件中假设只有10字节,相当于实际只读取了2.5块数据,则返回值是2;
假设文件中只有2字节,读取了0.5块数据,则返回值是0;
但是,读取的时候因为某些原因出现了错误,返回的也是0。
所以我们可以发现返回值为0时是有歧义的,会出现以下两种情况:
1.出错了,没有读到数据。
2.有可能是读取到文件末尾了,读取到了数据但是不足一块。
因此,我们尽量将bsize设置为1,将nmem设置为要读取的数据长度,这样的话只要读取到了数据返回值总是大于0的。当然也可以通过ferror 和feof 这两种函数进行判断。
4.文件是上一步是否成功判定——ferror
int ferror(FILE *fp);
功能:用于判断上一步文件操作是否成功的,通常是fread之后用于判断读取数据是否成功。
返回值:没有错误则返回非0值,上一次操作则返回false。
5.文件结束判定——feof
int feof(FILE *fp);
功能:用于判断当前是否读取文件内容(或者说文件读写位置)到达了文件末尾。
返回值:到达了文件末尾返回非0值 -- 真;没有到达文件末尾则返回0 -- 假。
注意: feof函数是在文件读取结束后,判断文件读取结束的原因的,是读取失败结束,还是遇到文件尾结束。
6.文件的随机读写——fseek
int fseek(FILE *fp,size_t offset,size_t whence);
- fp:文件流指针 - 操作句柄。
- offset:偏移量。
- whence:相对偏移的起始位置:SEEK_SET--文件起始位置;SEEK_CUR--当前读写位置;SEEK_END--文件末尾。
功能:将文件读写位置跳转到从whence位置偏移 offset 个字节处(跳转到哪里,读写数据就是从哪里开始)。
返回值:成功返回0;失败返回非0。
就像有一种功能在文件下载时体现 -- 断点续传(从上次失败的位置开始下载 -- 提高下载效率)。
eg:文件100字节,已经下载了99字节,剩余1字节,从文件哪里开始读。
SEEK_SET(offset = 99) SEEK_END(offset = -1)
7.文件的顺序读写
功能 | 函数名 | 适用于 |
字符输入函数 | fgetc | 所有输入流 |
字符输出函数 | fputc | 所有输出流 |
文本行输入函数 | fgets | 所有输入流 |
文本行输出函数 | fputs | 所有输出流 |
格式化输入函数 | fscanf | 所有输入流 |
格式化输出函数 | fprintf | 所有输出流 |
二进制输入 | fread | 文件 |
二进制输出 | fwrite | 文件 |
9.关闭文件——fclose
int fclose(FILE *fp);
- fp:fopen返回的操作句柄。
功能:关闭文件,释放资源。
返回值:成功返回0。
9.ftell 和 rewind
long int ftell ( FILE * stream );
ftell:返回文件指针相对于起始位置的偏移量。
void rewind ( FILE * stream );
rewind:让文件指针的位置回到文件的起始位置。
如有建议或想法,欢迎一起讨论学习~