目录
一般的说,我们磁盘(硬盘)上的文件就是我们常说的文件。
但是在程序设计中,我们所说的文件有更加精细的划分,根据文件功能来划分,可以分成程序文件和数据文件两种。
像我们人一样,为了区分文件,程序一般也有它们的文件名,并且在同一路径下,是不允许存在一模一样的文件名(路径不一样另说)。
文件名的组成是 文件路径+文件名主干+文件后缀
例如:c: \ code \ today \main.txt
像 c: \ code \ today 这样的就叫做文件路径,文件路径一般是不限制长度的,所以你要把文件路径写很长也是可以的,c: 表示的就是它在c盘的路径下,剩下的都是这个路径下的文件夹名称。
像 main 就是这个文件的文件名主干,而 txt 就是这个文件的文件后缀,与main之间用 .隔开,表示这个文件的性质,txt就表示这个main文件是文本文件。
一般来说,每个文件都有它的文件后缀,没有文件后缀的一般只有文件夹了,文件夹不被称为我们这里提到的“文件”,它算是一种存放文件的集合
一般我们在写c语言代码时候,会创建一个源文件(后缀一般为 .c),编译我们的源文件之后会产生目标文件(windows环境下为.obj),最后产生可执行程序(windows环境下后缀为.exe)。上面所说的全部文件都称为我们的程序文件
我们所写的程序在运行时读写的数据都可以称作是数据文件,比如程序运行时需要从这个文件里读取数据或者输出内容,那么这个文件就是我们所说的数据文件。这里我们主要就对这个数据文件进行讨论。
根据数据的组织形式,数据文件又细分为文本文件和二进制文件。
数据在内存中以二进制的形式存储,它就叫做二进制文件;如果以ASCII码的形式存储,那么这个文件就叫做文本文件。
这里用一个整数10000来举例,用ASCII码的形式存储,就是把10000分成1 0 0 0 0 五个数字分别用ASCII 码形式存储,一个字符占一个字节,用二进制形式存储,就占4个字节(一个 int 的大小)
这里用一个测试代码可以写出一个把整数10000以二进制存储到文件中:
- #include
- int main()
- {
- int a = 10000;
- FILE* pf = fopen("text.txt", "wb");
- fwrite(&a, 4, 1, pf);
- fclose(pf);
- pf = NULL;
- return 0;
- }
产生的文件会在main.c源文件的同一文件夹中
然后查看一下我们写的文件:右击源文件,点击添加->现有项
添加之后就右击test.txt->打开方式->二进制编辑器 就可以看到我们的文件内容了
这里10 27 00 0
0就是存储的10000的二进制值,顺序是反过来的,因为采用的是小端存储的方式。
我们的程序需要外部设备输入信息,同时也会向外部设备发送信息,这个其实是一个很复杂的过程,为了方便我们写代码,我们抽象出一个流的概念,类似于你在影视作品中看到的场景,一个黑客,戴着一个面具,面前是一个流淌的数据之河。我们可以把流看成一个个数据在流淌的河,C语言对于文件、画面、键盘的数据输入都是用流操作的。
一般来说,我们要在流里面写数据,或者从流中读取数据,都要打开流,然后再操作。
我们一般使用scanf和printf 两个函数在屏幕上进行数据的输入和输出的时候,好像也没有打开什么流啊?那是因为c语言在启动的时候就默认帮我们打开了3个流:
这三个流就是我们所说的标准输入流,这个时候我们才可以使用scanf 和printf 两个函数直接使用输入和输出操作。其中,stdin 、stdout 、stderr 三个流的类型都是FILE* ,通常称为文件指针
C语言中我们就是使用FILE*指针来维护流的各种操作的。
在头文件封装的还没有那么复杂的VS2013中,在stdio.h的头文件中对 FILE 有下面的定义:
- struct _iobuf{
- char* _ptr;
- int _cnt;
- char* _base;
- int _falg;
- int _file;
- int _charbuf;
- int _bufsiz;
- char* _tmpfname;
- };
- typedef struct _iobuf FILE;
上面这段代码其实就是对FILE的一个定义,它先是定义了一个结构体_iobuf,然后再把它重定义成FILE类型,所以,我们其实可以知道,FILE其实是一个结构体类型,它的内部储存的是一串数据。在我们创建一个文件date.txt的时候,我们就可以创建一个 FILE 类型的变量用来把date.txt文件的信息存在这个FILE类型结构体中,然后我们再定义一个指向这个FILE类型的指针,就可以管理这个结构体,同时也就管理了整个date.txt文件。
而我们一般在使用时不用关心这些细节,我们创建文件的时候就会产生FILE类型的结构体,这个结构体会把这个文件的所有信息都存储起来,我们就可以使用FILE* 类型的文件指针对这个文件进行管理。
上面我们使用一个图示可以对这个过程进行一个大致的观察,我们一般就只需要使用文件指针直接管理这个文件,中间的过程我们省略掉,也同时被抽象成一个“流”的概念,也就是说,我们只这样通过一个文件指针pf就可以直接找到和它相关联的文件,然后再进行维护。
我们读写一个文件的时候,就类似于我们喝一瓶饮料的过程,我们要喝饮料,第一步,打开瓶盖,第二步,进行喝饮料的操作,第三步,盖上瓶盖。同样的,我们读写文件也可以分成这样的操作,第一步,打开文件,第二步,进行读写操作,第三步,关闭文件。
然后再具体到每个操作。标准规定,我们需要使用fopen函数打开文件,使用fclose函数关闭文件。
- //打开文件
- FILE * fopen ( const char * filename, const char * mode );
- //关闭文件
- int fclose ( FILE * stream );
其中,mode表示打开文件的模式,有下面的几种打开文件的模式:
我们可以总结一下,方便记忆:有三个位可以考虑:
第一个位:r \ w \ a
r 表示读,w 表示写 , a表示在文件尾追加(r和w都是针对整个文件,a偏向于对文件尾部进行操作)
首位为 r :看文件是否存在,文件不存在就报错,否则就可以正常进行读取操作
首位为 w \ a :看文件是否存在,文件不存在就新建文件,否则就进行正常写 \ 追加操作
第二个位:无 \ b
第二个位如果没有就表示默认为文本文件,如果有就应该是b,表示文件是二进制文件
第三个位:无 \ +
第三个位用来表示是否通讯进行读写,如果第三位没有就只能进行第一位的操作,如果第三位有+就表示可以对文件一次性进行 读、写 两种操作
就大概是上面的几种操作了,下面我们可以使用实例进行演示:
- #include
- int main()
- {
- //创建一个文件指针
- FILE* pf;
- //打开文件,并使用文件指针pf接收fopen函数返回值
- pf = fopen("data.txt", "w");//这里是进行对文本文件只写的操作,由于没有创建过,这里会创建一个新的文本文件
- if (pf != NULL)//再次判断是不是成功创建了这个文件
- {
- fputs("Hello World!", pf);//写入数据
- fcolse(pf);//关闭文件
- pf = NULL;//把指针置为空,防止它成为野指针
- }
- return 0;
- }
程序运行成功之后我们就可以在和main.c源文件所在的同一文件夹中找到data.txt文件了。
刚刚我们介绍了怎么打开文件和关闭文件,但是没有讲打开文件和关闭文件中间我们需要的操作:写入和读取文件,刚刚的举例中,我们也使用了一个函数:fputs,用来放入“Hello World”这个字符串,类似于我们平时在控制台上使用的函数,我们对文件的函数其实就是大同小异的,只是我们在控制台上使用的函数一般只能用来使用控制台上的操作(标准输入流)。这里我们使用的对文件操作函数大多是适用范围更广的函数:
所有输入输出流是包含标准输入输出流的,所以我们也可以在标准输入流里使用这些函数
1) fgetc函数的原型如下:
int fgetc(FILE *stream);
函数返回一个整数,如果成功读取,这个函数会返回所读到的字符的ASCII码值,如果读取失败(来到文件末尾),就返回值EOF。
2) fputc函数的原型如下:
int fputc(int c, FILE *stream);
fputc函数的输入除了流,还有一个输入的字符(用ASCII码表示),返回值就是这个输入成功的字符的ASCII码值,如果发生错误,比如文件已关闭,就返回-1。
1)fgets函数的原型如下:
char *fgets(char *str, int n, FILE *stream);
其中这个str是是指向存储读取的字符串的指针,n是最大读取的字符数量(这里包含‘\0’终止符,所以其实最大获取字符数量应该是n-1)
函数停止的条件有两个,达到最大字符数或遇到换行符
函数成功读取就会返回字符串的首元素地址,否则返回空指针NULL。
2)fputs函数的原型如下:
int fputs(const char *str, FILE *stream);
fputs函数停止的条件是遇到终止符'\0' ,或者遇到文件末尾或换行符‘\n’,所以使用fputs的时候要确认字符串是不是有‘\0’终止符。
fputs有很多需要注意的点,比如在“w”只写模式下使用fputs会覆盖之前的内容,所以需要使用追加模式“a”防止把之前的内容覆盖。
fputs函数的返回值有下面4种:
1) fscanf函数的原型如下:
int fscanf(FILE *stream, const char *format, ...);
这个函数的和scanf函数很类似,只是在前面加上流的类型这个变量,比如,你可以类比函数scanf,基本上是一样的。
其中format是一个格式化字符串,用于指定要读取的数据的类型和格式,其实就是我们所说的占位符,比如整形%d,字符串%s...
(两个相邻的占位符之间一般用空字符隔开,否则需要输入内容也存在该字符\字符串)
返回值一般是一个正整数,表示成功读取的元素数量,或者是输入结束(也可能是发生错误)返回EOF
2)fprintf函数的原型如下:
int fprintf(FILE *stream, const char *format, ...);
同样的,这个函数比printf函数只是多了一个流,format依然是格式化字符串,只是还可以有其他的字符串(也就是说可以是字符串+占位符)。返回值依旧是正整数和EOF两个情况。
不同于上面的函数,fread和fwrite函数只能对文件有作用,并且一般用在后缀为bin的二进制文件,不然可能会出现编码错误的问题;然后就是打开文件的时候要使用带b的二进制模式。
1)fread函数原型如下:
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
参数:
函数返回值:
fread
返回实际读取的数据项的数量。注意如果算读取二进制数据,就要把文件名后缀改成.bin,并且在打开文件时就要使用“rb”等模式。
2)fwrite函数原型如下:
size_t fwrite(const void *ptr, size_t size, size_t count, FILE *stream);
这里的参数和fread函数的基本一致,返回值也是参考fread函数
准确的说,这个应该不算是随机读写,更像是指定读写
这里介绍下面几个函数:fseek ,ftell ,rewind
fseek 函数的原型如下:
int fseek(FILE *stream, long offset, int whence);
ftell 函数原型如下:
long ftell(FILE *stream);
ftell函数返回当前文件指针在文件中的位置(以字节数为单位)。通常情况下,ftell函数可以帮助确定文件指针的当前位置,以便进一步进行文件操作或记录文件的读写位置。
其次,ftell 函数的返回值类型是long (等价于 long int),这里在定义变量接受函数返回值的时候需要注意。
rewind函数原型如下:
void rewind(FILE *stream);
rewind函数会将文件指针移动到文件的起始位置,即相当于调用了fseek(stream, 0, SEEK_SET);。它的作用是将文件指针重置为文件开头,以便重新开始对文件的读取或写入。