目录
前言
文件是可以用来存储数据的空间,它有别于内存,不会随着程序的结束而被销毁其中的数据(因为它存在于磁盘这类外部储存单元中)这就意味着你可以在下次运行程序的时候使用上一次程序运行的“记忆”,比方说你写了一个通讯录这样用于记录数据的程序,如果不使用文件这类可以持续性保存数据的空间来保存通讯录中的数据的话,那么每次打开通讯录时都会像第一次一样,是空的,那么,我们要怎么使用文件来保存数据呢?且看笔者字字道来:
文件的大致分类
文件名的组成
广义上的文件名的组成是由 文件名的主干 + 文件后缀 组成的,如test.c;
而狭义上的文件名的组成是 文件的路径+广义上的文件名 组成的,也即 文件的路径 + 文件名的主干 + 文件后缀,如C:\Users\kkoia\Desktop\test.c
程序文件和数据文件
我们可以把文件分为程序文件和数据文件:
程序文件中有程序的源文件(.c/.c++...)、目标文件(.obj)以及可执行文件(.exe)......
如下图:
文件后缀在不同的环境下可能会用不一样的字符串表示,笔者这里是以windows环境举的例。
数据文件是泛指除程序文件外的其它所有文件,如下图:
文件操作
文件指针
要想通过程序来操作位于磁盘上的文件,得先让程序知道文件的具体位置是哪,通过狭义上的文件名程序就能获取文件的具体位置了,但是程序不是通过文件名来对文件进行操作的,而是在程序选择打开一个文件后,系统会在内存中申请一块自定义结构体大小的空间,并自动将文件的各种信息填入进去,在不同的编译器上,这个自定义结构体的成员会有所不同,前面也提到了,系统会自动填写里面的信息,所以没必要太在乎其中的细节,知道它是程序和所打开文件交互的桥梁即可,在对文件操作时,不是通过修改这个结构体来实现的,而是通过使用相关函数来实现的。
在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;
那么既然在内存中申请了一块空间,就需要变量来维护啊,而系统只负责申请和初始化空间,那么就需要一个指针变量来帮忙了,这个维护文件的指针就被称为文件指针(FILE*),通过它,就可以操作对应的文件了。
文件的打开与关闭
//打开文件 FILE * fopen ( const char * filename, const char * mode )//关闭文件 int fclose ( FILE * stream )
其实这里所说的打开和关闭文件,更像是获取和取消文件的访问权,你看,在使用fopen()后,系统在内存中申请并初始化一块文件结构体大小的空间,返回一个指向该结构体的文件指针,不就相当于给你进入文件空间的钥匙,那么你就获得文件空间的使用权了,而使用fclose()后,文件结构体会被释放,就相当于把钥匙还回去,你就不能再访问文件空间了;申请空间和释放空间,欸!这是不是很像动态空间的使用啊!是的,fopen()和fclose()的使用逻辑与malloc()和free()的使用是相差无几的,所以记得在打开文件后也及时关闭文件并将对应的文件指针置为空指针。
fopen()中的参数1:const char* filename,既可以使用狭义的文件名,也称为使用绝对路径;也能使用广义的文件名,也称为使用相对路径;这里需要注意的是,在使用广义的文件名时,需要确保要打开的文件与程序文件中的源文件,即保存这个代码的文件,位于同一文件夹内。
参数2:const char* mode:这里指的是你想怎样使用文件,是只读不写?还是只写不读?还是既读又写呢?对应使用方式的字符表述如下:
文件使用方式 含义 如果指定文件不存在
“r”(只读) 为了输入数据,打开一个已经存在的文本文件 出错
“w”(只写) 为了输出数据,打开一个文本文件 建立一个新的文件
“a”(追加) 向文本文件尾添加数据 建立一个新的文件
“rb”(只读) 为了输入数据,打开一个二进制文件 出错
“wb”(只写) 为了输出数据,打开一个二进制文件 建立一个新的文件
“ab”(追加) 向一个二进制文件尾添加数据 出错
“r+”(读写) 为了读和写,打开一个文本文件 出错
“w+”(读写) 为了读和写,建议一个新的文件 建立一个新的文件
“a+”(读写) 打开一个文件,在文件尾进行读写 建立一个新的文件
“rb+”(读写) 为了读和写打开一个二进制文件 出错
“wb+”(读写) 为了读和写,新建一个新的二进制文件 建立一个新的文件
“ab+”(读写) 打开一个二进制文件,在文件尾进行读和写 建立一个新的文件
这里所说的二进制文件和文本文件后文会进行讲解,这里要关注的是文件的使用方式,我们发现,在打开文件时,如果指定文件不存在,不同的使用方式,会有不同的结果,文件不存在时“报错”好理解,但是有些使用方式则是直接创建一个新的文件,这里可能会有点反常识,所以在使用的时候得注意,看下面这段代码就明白了:
#include <stdio.h>
#include <errno.h>
int main()
{
//打开文件
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
perror(fopen);
return 1;
}
//对文件进行的操作
//......
//使用完后关闭文件
fclose(pf);
pf = NULL;
return 0;
}
这里补充说明一下,通过“写”使用方式打开文件时,是会清空文件原有的内容的!!!使用时要注意。
什么是读、写、追加?
这里先引入一个词:流。
除了文件,其实还有很多的数据是不在内存中存储的,而存储这些数据的“外存”的种类五花八门,但是,它们都是数据的集合,于是就抽象出 流 这一名词来指代这些“外存”,如,规定键盘是标准输入流,终端是标准输出流,其他的流还有磁盘、U盘、网络、外部设备......
流 是一种抽象结构,可以理解为字符的集合,但有别于字符串,它没有统一的结束标志,因为它是各类集合的抽象集合。
了解完 流 的基本概念,那么我们就能理解什么是读、写和追加,以上三个词是当两个有别的储存空间有交流时使用的词,这两个有别的储存空间可以是内存和“外存”,也可以是两个不同的“外存”,其实,读和写与我们熟悉的输入和输出是一样的逻辑,只不过是换了另一种角度的说法罢了,见下图:
追加就是特殊的写:在不改变打开前的 流 中的原有数据的前提下,向 流 中写入数据的操作。
文件的基本操作
文件的顺序访问
这里说的顺序访问是指,再不使用其他函数函数前提,下面介绍的函数会从文件起始位置开始,顺序进行文件的读写操作。
int fgetc ( FILE * stream ) // 从流中读取一个字符 | 适用于所有输入流int fputc ( int character, FILE * stream ) // 将一个字符写入流 | 适用于所有输出流
steam指向一个流,fgetc()从这个流中读取一个字符,读取成功返回读取字符对应的ASCII码值,读取失败返回EOF(值为-1);
fputc():将字符character写入steam指向的流,写入成功则返回字符character对应的ASCII码值,写入失败返回EOF。
char * fgets ( char * str, int num, FILE * stream ) // 不是读取一个字符串哦!而是从流中读取num个字符 | 适用于所有输入流int fputs ( const char * str, FILE * stream ) //将一个字符串写入文件,结尾的'\0'不写入,这里就是字符串了 | 适用于所有输出流
fgets():从steam指向的流中读取num个字符到str指向的字符数组中,读取成功则返回数据输入的地址,即参数str,读取失败则返回NULL(值为0);
fputs():将字符串str写入steam指向的流,写入成功则返回一个非负值,不一定是字符串的长度,写入失败则返回EOF。
int fscanf ( FILE * stream, const char * format, ... ) //格式化读取数据 | 适用于所有输入流int fprintf ( FILE * stream, const char * format, ... ) //格式化写入数据 | 适用于所有输出流
别被这个省略号吓到了,其实就是在我们经常用的scanf()和printf()的逻辑是基本一样的,就是多了个流作参数罢了,如下:
//其实,scanf()函数和printf()函数只是默认了流,不然在使用时也要传 流 作参数
scanf("%d", &num) // 从标准输入流中以 %d 读取数据,写入num变量中
fscanf(pf, "%d", 12) //从pf指向的文件流中以 %d 读取数据,写入num变量中(这里假设pf是一个文件指针)
printf("%d", 12) // 将 12 以 %d 的形式写入标准输出流--终端
fprintf(pf, "%d", 12) //将 12 以 %d 的形式写入pf指向的文件(这里假设pf是一个文件指针)
fscanf():格式化读取成功则返回成功读取的元素个数,失败则返回EOF;
fprintf():格式化写入成功则返回成功写入的字符个数,注意这里是字符个数,不是元素个数,如将12以%d的形式成功写入后,函数返回的是2而不是1,写入失败则返回一个负数。
细心的同学可能发现了,上述函数都有说适用于所有输出流、所有输入流,也就是说,使用上述函数可以在所有“外存”中读写数据,就拿格式化读写数据函数( fscanf()和fprintf() )举个例子吧,其实,用stdin作fscanf()的FILE* steam参数就等效于scanf()函数,同理用stdout作fprintf()的参数也能等效于printf()函数,这里的stdin和stdout指的是标准输入流(键盘)和标准输出流(终端,就那个每次运行程序后会出现的弹窗)
size_t fread ( void * ptr, size_t size, size_t count, FILE * stream ) // 二进制读取数据 | 仅适用于文件流size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream ) //二进制写入数据 | 仅适用于文件流
注意了这两个函数仅适用于文件流!!!
fread():从steam指向的二进制文件中读取count*size字节的二进制数据,然后写入ptr指向的内存空间中,读取成功则返回成功读取的元素个数,即count,读取失败时可用feof()是否是由于文件已经读到末尾导致,或用ferror()检查是否是出现别的错误导致;
fwrite():将从ptr指向地址开始,往后count*size字节的数据写入steam指向的二进制文件中,成功与否的返回结果同fread()。
二进制文件和文本文件
二者都属于数据文件,由于对数据的组织形式不同而区分开来。
内存中的数据是以二进制的形式存储的,如果不加处理地写入文件,那么这样的文件就称为二进制文件;
相反,如果对数据加以处理,即按你给的指令来读取数据,比方你指定按照 %d 识别数据,那系统会读取接下来的4个字节大小的空间,也即32位的二进制序列,将其写入文件,之后打开文本文件查看时,此处就会是一个int型的数,这样的文件就称为文本文件(当然不只有%d这种格式化输出的指令,望读者依据上文提到的函数,用发散型思维思考)
通俗点说,用文本文件存数据,打开时看的懂,而用二进制文件存数据,打开时看不大懂,如下:
#include <stdio.h>
#include <errno.h>
int main()
{
FILE* pf = fopen("test.txt", "w"); // 打开文本文件
//FILE* pf = fopen("test.txt", "wb"); // 打开二进制文件
if (pf == NULL)
{
perror(fopen);
return 1;
}
int num = 10000;
fprintf(pf, "%d", num);
//fwrite(&num, sizeof(int), 1, pf);
fclose(pf);
pf = NULL;
return 0;
}
随机访问
这里的随机访问指的是能调整指向文件中字符的箭头的位置(就像我们打字一样,可以调整输入和删除的位置)然后就可以自主读写特定位置上的数据了,注意!这里介绍的函数只起调整的作用,读写还是得用上文中介绍的函数。
int fseek ( FILE * stream, long int offset, int origin )
用于调整箭头的位置,参数origin只能使用以下三个在stdio.h中声明的常量来传递:
SEEK_SET //代表文件开头位置
SEEK_CUR //代表当前位置
SEEK_END //代表文件结束位置
参数offset指相对于参数origin指代的位置需要偏移多少字节的空间,offset为正数则向右偏移,为负数则向左偏移;所以fseek()的功能就是将steam指向的流中的箭头指向origin位置偏移offset字节空间的位置上,调整成功返回0,失败返回非0值。
long int ftell ( FILE * stream )
获取当前箭头在steam指向的流中的位置,获取成功则返回其位置,失败则返回-1L,用于辅助fseek()的使用。
void rewind ( FILE * stream )
将箭头位置重置到steam指向的流的开头,无返回值。
文件读取结束的判断
int feof ( FILE * stream )
这里对话一下知道feof()函数的读者,你是否使用它来判断文件是否读取结束呢?
其实,feof()它并不能直接用于判断文件是否结束,而是在读写失败时,检查是否是因为文件已经读取结束导致的,如果是返回非0值,不是则返回0。
在读取过程中不出现错误的情况下:
文本文件读取是否结束,看顺序读取函数的返回值是不是EOF(fgetc())或NULL(fgets())
二进制文件的去是否结束,看fread()返回值是不是小于参数count
如果要检查是否是出现了错误而导致读取失败的话,可用ferror():
int ferror ( FILE * stream )
如果出现错误则返回非0值,如果没出现错误则返回0。
文件缓冲区
ANSIC 标准采用“缓冲文件系统”处理的数据文件的,所谓缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块“文件缓冲区”。从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上。如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓 冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓冲区的大小根据C编译系统决定的。
可用以下代码验证其的存在:
#include <stdio.h>
#include <windows.h>
//VS2013 WIN10环境测试
int main()
{
FILE*pf = fopen("test.txt", "w");
fputs("abcdef", pf);//先将代码放在输出缓冲区
printf("睡眠10秒-已经写数据了,打开test.txt文件,发现文件没有内容\n");
Sleep(10000);
printf("刷新缓冲区\n");
fflush(pf);//刷新缓冲区时,才将输出缓冲区的数据写到文件(磁盘)
//注:fflush 在高版本的VS上不能使用了
printf("再睡眠10秒-此时,再次打开test.txt文件,文件有内容了\n");
Sleep(10000);
fclose(pf);
pf = NULL;
return 0;
}
注:fclose在关闭文件的时候,也会刷新缓冲区,所以为了避免有些数据遗留在文件缓冲区,造成数据流失,我们要在打开文件后及时进行关闭操作。
设置缓冲区的作用其实是为了提高操作系统的效率,因为在不同存储空间之间传递数据时,是需要借助操作系统提供的接口才能实现的,假设不设置缓冲区,操作系统估计得“烦死”,假设这台机器上有很多个程序在运行,每个程序都在进行读写操作,就像老师面对一群一有问题就提问的学生一样,而且每个学生都说他的问题只有一个,在老师回答完这个学生的问题准备回答下一个时,这个学生又提了一个新问题,老师只好继续回答,回答完后老师以为该解决下一位同学的问题了吧,可是这个学生又有新问题......这样每次回答问题时,老师都要重新调整心理状态,回答问题的效率不就低了嘛;相反,有了文件缓冲区,就像每个学生在攒够相应数量的问题后再找老师解决一样,老师每解决完一个同学的所有问题后,这个同学暂时不用冒出新问题,也就不用频繁地调整心理状态,这样效率不就高起来了。
总结
本文介绍了文件的大致分类和文件的基本使用,希望能给需要的人带来帮助。