之前我们在程序当中使用的数据都是保存在内存当中
计算机的内存的特点就是读写的速度相对比较快,但是在内存当中的数据不能永久保存,如果程序执行之后或者在执行过程当中突然断电,那么内存当中的数据就会被清空
所以我们通常会把重要的数据保存在计算机的另外一个设备上,那就是硬盘
硬盘的特点就是读写的速度没有内存快,但是在硬盘上的数据可以永久的保存(前提是硬盘没有损坏),即便是在断电状态下,硬盘的数据也不会被清空
需要把数据写入到硬盘,我首先就应该了解一下文件的概念
文件我们可以理解为在硬盘上保存的数据块,我们在硬盘上标记一个数据块的位置,然后在这个数据块当中去保存我们的数据
一般这个过程是不需要我们手动的来干预的,操作系统会自动的帮助我们来完成,我们所需要做的就是利用c语言去调用操作系统的接口完成文件的读写操作
在c语言的标准库当中,对于文件的操作都被定义在了 stdio.h 标准库当中(我们之前使用过的格式化输入 scanf 和格式化输出 printf 都出现在这个库里面)
文件输入/输出 - cppreference.comzh.cppreference.com我们通常会把文件分为几种类型
- 文本文件:可以用字符编码解释成文本内容的文件
- 二进制文件:一般用来保存图片,视频或者可执行的程序,通常情况下如果用文本编辑器打开二进制文件会看到乱码,必须通过特定的应用程序才能打开二进制文件
文本文件和二进制文件在硬盘当中都是以二进制数据进行保存的,区别就在于,文本文件保存的数据可以通过字符编码进行解析,而二进制文件需要特定的程序才可以
我们编写的c程序就是文本文件,但是编译之后生成的可执行程序就是二进制文件



在 windows 操作系统当中文件都会有扩展名,通过扩展名系统会自动根据文件的扩展名选择合适的程序打开对应的文件
在 linux 操作系统当中我们打开一个文件就需要指定文件的打开方式
在c语言当中操作文件非常的简单,只需要找到硬盘上文件的指针位置,然后就可以写入数据,读取数据
c语言在 stdio.h 头文件当中定义了一个新的类型 FILE ,这是一个结构体,代表的就是硬盘上的文件,我们对于 FILE 结构体数据的操作,都会自动的映射到硬盘上的文件当中(这个过程由操作系统完成),我们的工作就是找到文件对应的 FILE 结构体,然后对他进行操作就可以了
FILE 结构体通常会被称为文件流,表示程序当中对于一个文件的映射
#include
这里通过 fopen 函数找到一个文件的指针 fp,可以认为是打开了一个文件,fp就指向了硬盘上 "d:/ttt.txt" 位置的文件
然后通过 fprintf 向文件当中写入内容"hello world"
最后使用 fclose 释放掉指针,相当于关闭文件
程序执行结束,就会在计算机的 d盘下面自动建立一个文件,文件名是 ttt.txt,里面的内容是 hello world

操作文件最重要的就是 fopen 函数,功能就是打开特定的文件,并返回指向关联到该文件的FILE指针
FILE * 文件指针 = fopen(文件名,打开模式);
fopen 的第二个参数指的是可以对文件进行的操作模式,通常会由以下几种

同时还可以使用 t 或者 b 加在上述模式的后面表示打开的是文本文件(t)还是二进制文件(b),如果不加默认的是以文本方式打开
比如说,我们需要新建一个文件,并且向文件当中去写入内容,那么打开文件的模式就应该是 "w","w+",或者"wt",如果新建一个二进制文件,那么就应该是"wb","wb+"
我们可以使用 fprintf 函数向文件当中写入内容
fprintf(文件指针,格式化字符串,数据);
具体的用法和 printf 一样,只不过多了第一个参数写入文件的指针
同样的读取一个文件的内容,打开文件的方式就是 "r","r+"或者"rt",读取二进制文件模式就是"rb","rb+"
读取刚才我们创建的文件的内容,并且打印到控制台上,我们就可以这样写
#include
这里会出现一个问题

只读取了hello,后面的world没有读取出来
这是因为 scanf 和 fscanf在读取内容的时候读取到空白字符(空格,换行n,制表t等等)的时候就会自动的结束读取,如果想要读取完整的信息,我们就需要读取两次
#include

在文件读取的时候使用 fscanf 不是特别的方便,特别是在不知道文件具体的内容和保存格式的时候,所以我们在写入和读取文件的时候通常还会使用到以下的几个函数
- fgetc:从文件中读取一个字符
- fputc:向文件中写入字符
- fgets:从文件中读取字符串,读取到换行符为止
- fputs:向文件写入一个字符串
我们可以使用 fputc 和 fgetc ,逐个字符的把数据写入到文件当中
fputc(字符,文件指针);
字符 = fgetc(文件)
#include<stdio.h>
int main(){
FILE * fp = fopen("d:/ttt.txt","w");//打开文件
char * s = "hello world"; //指针指向字符串
while(*s!='0'){ //字符串没有结束就一直向文件当中写入内容
fputc(*s,fp);
s++;
}
fclose(fp);
return 0;
}
因为字符串都是以 '0' 空字符结尾,所以可以通过指针来遍历字符串,只要内容不是 '0',就把读取到的字符写入文件当中

读取文件的时候就需要判断读取的文件结尾位置,否则循环就没有办法结束
在 stdio.h 里面有一个宏定义 EOF (end of file) 表示文件的结尾,当读取到的字符和EOF相同时就可以认为文件结束了,不同操作系统EOF的值也不相同,但是我们不需要知道EOF具体的值,只要保证当前读取的内容是不是和EOF宏的值相同就可以了
#include
除了使用字符去读取文件之外,还可以按行去读取
我们可以把文件的每一行当作一个字符串,当文件出现多行数据的时候就可以把文件想象成一个保存多个字符串的数组,然后使用 fgets 和 fputs来按行读写数据
fgets(字符串,读取个数,文件指针);
fputs(字符串,文件指针)
写入数据
#include

读取数据
#include

使用 fputs fputc fgets fgetc 函数在操作文件的时候都会自动的移动文件指针,指针移动的位置就是文件读写的位置
如果我们需要手动的移动文件指针,那么我们可以使用 fseek,ftell,rewind 三个函数
- ftell 返回当前文件指针的位置
- fseek 手动移动文件指针
- rewind 把文件指针移到文件开头
指针位置 = ftell(文件指针);//返回值是long类型单位是字节,表示当前指针在文件当中的第几个字节处
fseek(文件指针,偏移量,指定位置);//把指针移到指定位置+偏移量的位置
//起始位置有 SEEK_SET(起始位置) 、 SEEK_CUR(当前位置) 、 SEEK_END(偏移量)
rewind(文件指针)//把指针移到起始位置
比如我们向文件当中写入内容,写入之后,我们查看一下文件指针的位置
#include

起始位置是0,结束位置是11,说明我们写入了11个字节的内容
如果我们把原有的内容hello world 改成 hello c language,那么就应该在写内容之前把指针移动到hello的后面然后再去写内容
因为w模式写内容的时候都会从新创建文件,所以我们使用 r 模式打开一个已经存在的文件,同时还需要写入内容,所以我们使用 r+ 模式来打开
#include


会从第6个字符的位置开始写内容,将之前的内容替换掉
我们之前使用的 fscanf fprintf fputc fgetc fputs fgets 都是针对于文本文件来进行操作的
如果我们不考录文件的字符编码,而使用二进制数据直接进行文件的读写,我们还可以使用
- fread:直接读文件内容
- fwrite: 直接向文件写内容
fread(读取数据,读取数据的大小,读取数据的数量,文件指针)
fwrite(写入的数据,写入数据的大小,写入数据的数量,文件指针)
比如说,我们想把一个数组写入文件,因为数组不需要字符编码解释,所以我们可以使用二进制来保存,打开模式是 wb
#include
fwrite的参数表示写入数据的大小是 int 的大小,一共写入了5个数据,刚好是数组a的大小
写入之后我们的文件打开是这样的

文件时以二进制保存的,文本编辑器没办法解析,显示的是乱码
我们只能自己去写程序来解析这个数据
#include
把读取的数据放到数组 a 里面,再输出a

假如我们不知道保存数据的长度,只知道保存的数据类型,那么还可以使用while循环一直读取到文件结尾,这个时候还会用到几个函数
- feof:判断文件指针是否在文件末尾
- ferror:判断文件指针是否出错
我们可以逐个数据来进行获取
#include
fread每次读取1个int数据,同时判断是否到达文件结尾,如果是结束循环,否则输出读到的数据

这个技巧对于读取未知长度的文件非常有用
对于结构体同样也可以使用文件来进行保存
#include
我们创建一个链表保存了4个学生的数据,然后依次的把每个学生的成绩写入到文件student.data当中,文件的扩展名我们可以随便写
由于是二进制写入所以使用记事本打开会看到乱码

接下来我们就需要写一个程序来读取文件的内容
#include
先开辟一块内存空间作为缓存,然后每次使用 fread 读取一个结构体长度的数据,同时判断是否读取到文件的结尾,读到结尾结束循环,否则打印读取的内容

其实我们之前在控制台输出内容和获取键盘的输入本质上也是文件操作
在 stdio.h 文件当中定义了三个宏 stdin, stdout, stderr 分别表示标准输入流,标准输出流,和标准错误流
其中 stdin 标准输入流就是我们的键盘输入对应的文件,stdout 标准输出流就是我们的显示器对应的文件
比如我们需要输出一段内容,可以使用文件输出的方式
#include

就相当于向 stdout 文件当中写入内容一样
#include

当然使用 fputs 也是相同的效果
#include

同样道理获取键盘的输入就相当于从stdin文件当中读取数据
#include
运行程序,在控制台输入hello

也可以使用fscanf和fgets
#include
#include
结果都是一样的
唯一需要注意的就是fgets需要指定获取字符串的长度