正文
文件操作涉及到的内容还是相当的多的,当然相比看完这篇文件操作也会对文件有着更深层次的理解,大致可分为三个问题:是什么? 怎么用?要注意什么会围绕着这个三个问题的衍生出很多问题,其中怎么用是内容最丰富的版块,要学习多个函数的用法相当也比较简单,让我们直接进入正题吧。
关于文件
什么是文件?
也便是存储在磁盘上的电子文件。就是在我们电脑中存储那个 数字、照片、文件等都属于电子文件,只要硬盘没坏,那么这些数据随时随地都能找到,而且方便探索。在程序设计中,我们还将文件分为俩种:程序文件与数据文件(从文件功能的角度分类),本篇还是主要介绍的数据文件。
程序文件
程序文件包括源程序文件,比如我们的.c 文件;目标文件,经过预编译、编译、汇编后生成的目标文件,后缀为.obj,对其进行链接后,就能生成可执行程序;当然最后一种就是可执行程序文件,后缀名为.exe
数据文件
数据文件主要存储的是各种数据信息,但内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件,这些数据是能够持久化存储的。
文件有什么用?
电脑C盘中存储的各种信息
文件可以保存数,使数据能做到持久化存储。文件可以使用我们的操作更加合理,比如现在我们所写的博客。本质就是一个文件,不过是只是存储在服务器上的文件(数据)。电子文件的族弟啊特点就是易于探索了,着也正是电脑与别的电子设备优点之一。至于c语言中的文件可以用于保存程序运行所产生的数据,比如通讯录系统,可以将联系人信息保存在一个文件夹中,现在的程序设计数据一般都是存储在数据库中,毕竟本地文件夹安全系数还是比较低的。
文件的格式是什么?
所有文件都有唯一的标识符,标识符可以分为三个部分:文件路径 + 文件名主干+文件后缀,比如存储在我电脑中的VS文件标识为:
为了方便称呼,我们一般将其称为文件名,比如deven.exe 就是一个典型的程序文件
文本文件与二进制文件
文本文件
文本文件指以ASCll码(文本方式)存储的数据,原始数据机器能直接看懂,将内存中的数据对应ASCll码解码存储后,我们人类也能看懂,换句话来说,就是在记事本中写的文件吗,看懂就是文本文件
二进制文件
二进制文件是将数据编译后转成二进制形式,然后直接存储的文件,这种文件机器能懂,读取效果很高(因为不需要转译),但二进制一般人是看不懂的,部分二进制数据也无法通过ASCll码解码为正确的数据,因此强行输出二进制文件,既有可能会得到乱码。比如将上面的那段话通过二进制形式写入文件中,可以看到出字符类型数外,其他类型的数据变成了乱码。(看不懂代码没关系哈,待会都会更加详细介绍,现在只要注意记事本的状态就好了)
可以看到数据成乱码,可这时我们又该怎么查看呢?接下来教大家一个方法:将这个文件拉到VS二进制来打开
注意
- 如果待读取的文件中存储的时二进制数据,就需要使用 二进制读取 "rb" 的形式读取数据;反之如果想写入二进制数据,就需要用 二进制写入"wb" ,无论是二进制还是普通文本,计算机都能读懂,只是我们看不得罢了。小技巧:可以使用二进制存储重要数据,这样外行人一时班会也理解不了,好比跟看摩斯密码一样。(后续会继续介绍更多的函数)
文件的打开和关闭
流和标准流
1.流
我们程序的数据需要输出待各种外部设备,也需要从外部获取数据,不同的外部设备的输入输出操作各不相同,为了方便程序员对各种设备进行方便的操作,我们抽象出了流的概念,我们可以把流想象成流淌着各种各样的字符的河流,伸手便能拿到你想要的字符
C程序针对文件、画面、键盘等的数据输入输出操作都是通过流操作的。
一般情况下,我们要想向流里写数据,或者从流中读取数据,都是打开流,然后操作。
2.标准流
那为什么我们从键盘输入数据,向屏幕上输出数据,并没有打开流呢?
那是因为我们C语言在启动程序时,已经提前打开了,意思就是给你铺好路了,只要会走路就好啦。可想而知,C语言程序有多贴心。
- stdin - 标准输入流,在大多数环境中从键盘输入,scanf函数就是从标准输入流中读取数据。
- stdout - 标准输出流,大多数的环境中输出至显示器界面,printf函数就是将信息输出到标准输出流中。
- stderr - 标准错误流,大多数环境中输出到显示器界面。
这是默认打开了这三个流,我们使用scanf、printf等函数就可以直接进行输入输出操作的。
stdin、stdout、stderr 三个流的类型是:FILE* 通常称为 文件指针(马上就会讲到)。
c语言中,就是通过FILE* 文件指针来维护流的各种操作的。
使用文件
文件指针
文件是一个庞大的集合体,类似于结构体,不过更为复杂,因此在C语言中有一个专门的指针 文件类型指针,简称为 文件指针 用来指向文件首地址。系统会将为文件规范化,当使用我呢见时,系统会在内存中开辟一个对应的文件信息区,这个信息区中包括了文件的各种信息(文件名、文件状态、文件位置等),如果对应信息缺失,系统会自动补齐。前面说过,文件类似于结构体,因此整个文件信息是保存在一个庞大的结构体中的,为了与传统结构体分开,专门创建了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类型包含的内容不完全相同,但是大同小异,但上面这个在VS2013编译器下是解释的最清晰的不过,大家就看看长长眼,看看就行,使用者不必关心细节。但知道 FILE 这个东西本质是个结构体就行了。
下面我们可以创建一个FILE*的指针变量:
FILE* pf; //文件指针变量
定义pf是一个指向FILE类型数据的指针变量。可以使pf指向某个文件信息区(是一个结构体变量)。通过该文件信息区中的信息就能够访问该文件。也就是说,通过文件指针变量能够间接找到与他关联的文件。
文件的打开和关闭
文件的先打开,才能关闭,最好跟我们之前学习动态内存管理一样,有申请就要有释放,成对出现更为安全。
文件打开
文件打开用的是 fopen 这个函数,fopen 的作用是从一个文件中以某种方式打开文件,返回类型是
FILE* 即打开文件的起始地址,因此我们需要用一个 FILE* 类型的指针来接收。
文件在读写之前应该先 打开文件,然后便时 写入文件,紧接着就是在使用结束之后应该 关闭文件
在编写程序的时候,在打开文件的同时,都会返回一个FILE* 的指针变量指向该文件,也相当于建立了指针和文件的关系。
注意:文件打开后,要对文件指针进行判断,如果指针为空,说明文件打开失败,此时要报错,并中止后续操作
//列如:
int main()
{
//打开文件
FILE* pf = fopen("daat.txt", "w"); // "w"表示只写
if (pf == NULL)
{
perror("fopen"); //假如为空指针就会报错相应的错误
return 1; //结束程序
}
// 写文件 (很快就会解释到) ...//
//关闭文件(很快就会解释到) ...//
return 0;
}
目标文件
有两种形式,一种时绝对地址,另一种是相对地址
即唯一路径,使用绝对地址访问绝对地址文件时,文件可以在电脑种的任何位置,前提是地址要合法。绝对位置的文件标识符必须全,即文件路径+文件明主干+文件后缀名。
列如:可以看到刚刚我们讲述的IDE的绝对地址:
F:\Program Files\Microsoft Visual Studio\2022\Community\Common7\IDE
注意:
使用绝对路径时,需要在每个\前面再额外加一个\ 原因也很简单,单个\与其他字符结合,可能会变成一个转义字符,因此需要俩个\\来标识一个\
如果时Mac就不需要担心这个问题的,因为它用的是/
// 绝对,指此地址是唯一,能通过这个地址找到唯一的文件
FILE *pf = fopen("F:\Program Files\Microsoft Visual Studio\2022\Community\Common7\IDE");
相对地址
此时的路径是固定的,一般和当前源文件处于同一个位置,相对吗,就是相对于当前程序文件。相对位置只需要文件名主干+文件后缀就行了。
比如 devenv.exe,此时存储的位置相对于上面的绝对地址,位于同以目录下
//相对,指在当前工程文件内的文件
FILE* fp = fopen("data.txt", "w");
打开方式
文件打开方式有很多种,比如只读、只写、读+、二进制写等...
值得注意的是当我们通过 读 的方式打开文件时,如果目标文件不存在,那么打开就会失败;当如果是通过 写 的方式打开文件时,如果文件不存在,会自动创建一个目标文件。
接下来就是演示写的方式打开文件,然后文件不存在,自动创建文件的情况:
注意:这种文件的标准使用方式,既先打开,然后判断是否打开失败,如果失败就报错,否则就可以使用文件,最好再关闭文件
//文件创建,通过程序创建
int main()
{
//再利用只读的特性,没有就会自己创建
// 这里时相对路径
//打开文件
FILE* pf = fopen("data.txt", "w"); // "w"表示只写 没有即会自己创建
if (pf == NULL)
{
perror("fopen"); //假如为空指针就会报错相应的错误
return 1; //结束程序
}
// 进行文件操作
//关闭文件
fclose(pf); //关闭
pf = NULL; // 手动置空
return 0;
}
注意:
- 这个特点很好用,但也很致命,因为每次写文件,都相当于覆盖文件,假如我们想要对原文的进行追加,就需要创建原来的数据,再创建新数据,然后一起写入文件中。其实面对这种场景,C语言还提供了另外一种文件打开方式追加 "a" ,下面就是各种打开指令的集合表。
文件打开指令 | 含义 | 如果目标文件不在 |
" r" 只读 | 打开一个文件,只能对它这个文件进行读取操作 | 打开失败,报错 |
" w" 只写 | 打开一个文件,只能对它这个文件进行写入操作 | 建立目标文件 |
"a" 追加 | 向文件末尾处添加数据 | 建立目标文件 |
" rb" 只读 | 打开二进制文件,只能对其进行二进制读取 | 打开失败,报错 |
" wb" 只写 | 打开二进制文件,只能对其进行二进制写入 | 建立目标文件 |
" ab" 追加 | 向二进制文件末尾处添加数据 | 追加失败,报错 |
" r+" 读写 | 打开一个文件,可以进行读取和写入操作 | 打开失败,报错 |
" w+" 读写 | 打开一个文件,可以进行读取和写入操作 | 建立目标文件 |
" a+" 读写 | 对文件末尾处进行读取和写入操作 | 建立目标文件 |
" rb+" 读写 | 打开二进制文件,可以进行二进制读取和写入 | 打开失败,报错 |
" wb+" 读写 | 打开二进制文件,可以进行二进制读取和写入 | 建立目标文件 |
" ab+" 读写 | 对二进制文件末尾处进行读取和写入操作 | 建立目标文件 |
文件关闭
文件关闭用到的就是刚刚我们用的很陌生函数, fclose 这个函数,当然也相对简单,只需要一个参数-文件指针(FILE*),然后就能关闭这个文件指针所指向的文件,感觉有点像 free , 功能强大,使用方便。同 free 一样,fclose 关闭文件后,也需要将指针(文件指针)手动置空,避免出现野指针。
//列如:
int main()
{
//打开文件
FILE* pf = fopen("daat.txt", "w"); // "w"表示只写
if (pf == NULL)
{
perror("fopen"); //假如为空指针就会报错相应的错误
return 1; //结束程序
}
// 写文件 (很快就会解释到) .....//
//关闭文件
fclose(fp); //关闭
fp = NULL; //手动置空
return 0;
}
顺序读写
接下来对文件进行操作各种输入输出函数集合表:
功能 | 函数明 | 适用于(目标流) |
进行单字符的输入 | fgetc | 所有输入流 |
进行单字符的输入 | fputc | 所有输出流 |
文本行输入函数(读取一行数据) | fgets | 所有输入流 |
文本行输出函数(读取一行数据) | fputs | 所有输出流 |
格式化输入函数 | fscanf | 所有输入流 |
格式化输出函数 | fprintf | 所有输出流 |
二进制输入函数 | fread | 文件输入流 |
二进制输出函数 | fwrite | 文件输出流 |
注意:为了方便函数的介绍,接下来我会介绍写入(也就是输出),再介绍读取(输入)函数
fputc 与 fgetc
fputc 对文件进行单字符的写入,fgetc 读取文件中的单字符
fputc
int main()
{
//文件打开
FILE* pf = fopen("data.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//进行文件操作
char* pc = "abcdef";
//逐字符写入
while (*pc)
{
fputc(*pc, pf); // 逐字符放入
pc++;
}
// 文件关闭
fclose(pf);
pf = NULL; //手动置空
return 0;
}
代码也在这,大家要想的验证也可以直接复制粘贴上去,进到相对地址找到文件打开它,但有的时候需要注意字体预防报错。(反正就是有报错把打印的字体也重新输入再尝试尝试)。
fgetc
//文件读写之逐字符读
int main()
{
//文件打开
FILE* pf = fopen("data.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//逐字符读取
int ch = 0; // 需要用整形,因为EOF是 -1
while ((ch = fgetc(pf)) != EOF)
{
//逐字符读取后,赋给字符变量ch,然后打印
printf("%c ", ch);
}
// 文件关闭
fclose(pf);
pf = NULL; //手动置空
return 0;
}
注意:
- 在读取或者写入字符串时,可以通过特定的条件读写。比如写入:可以通过字符串自带的结束标志 \0 结束写入 ;读取:可以通过fgetc 的返回值进行判读,如果返回-1(EOF)就说明数据已经读取完了。
- 单纯写文本数据时,要使用指令“w” ; 单纯读数据时,要使用指令“r”,指令与操作一定要匹配上,不然就会发生意想不到的错误
fputs 与 fgets
fputs 对文件的进行一行数据的写入,fgets 是读取文件中的一行数据
fputs
//文件读写之行写
int main()
{
//打开
FILE* pf = fopen("data.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//文件进行操作
char* pc = "这是有标准输入输出写入的数据";
fputs(pc, pf); //行写入
//文件关闭
fclose(pf);
pf = NULL;
return 0;
}
fgets
//文件读写之行读
int main()
{
//打开
FILE* pf = fopen("data.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//文件进行操作
char tmp[30] = "0";
fgets(tmp, sizeof(tmp), pf); //行 读取
printf("%s\n", tmp);
//文件关闭
fclose(pf);
pf = NULL;
return 0;
}
注意
- 行写入时,要确保写入的字符串数据,传参数时要传地址;行读取时,需要设定待读取数据的数量,一般时跟待存储空间大小相匹配。如果行读取结束,有俩种情况:1、因无法读取数据而结 2、因读取到文件末尾而结束
- 单纯写文本数据时,要使用指令 “w”;单纯读数据时,要使用指令 “r”
fprintf 与 fscanf
fprintf 是对文本进行格式化数据的写入(可以对各种格式写入),scanf 是将文本中的数据进行格式化读取(可以对各种格式写入)
fprintf
//按找文件流格式化写入
struct S
{
char name[20];
int age;
float score;
}a = {"zhansan",20,78.8f};
int main()
{
//文件打开
FILE* pf = fopen("data.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//文件操作
//适合与所有流格式化输入输出函数
fprintf(pf, "%s %d %.2f", a.name, a.age, a.score);
//文件关闭
fclose(pf);
pf = NULL;
return 0;
}
fscanf
//按找文件流格式化读取
struct S
{
char name[20];
int age;
float score;
}a;
int main()
{
//文件打开
FILE* pf = fopen("data.txt", "r"); //记得改只读
if (pf == NULL)
{
perror("fopen");
return 1;
}
//文件操作
//适合与所有流格式化输入输出函数
fscanf(pf, "%s %d %f", a.name, &(a.age), &(a.score));
printf( "%s %d %.2f", a.name, a.age, a.score);
//文件关闭
fclose(pf);
pf = NULL;
return 0;
}
延申:sprintf 和 scanf
除了 fprintf / fscanf 和 printf / scanf 这俩组格式化输入输出外,还存在另一组格式化输入输出函数数:sprintf / sscanf
简单介绍一下,sprintf 是把格式化的数据按照的一定的格式妆化为字符串,相反的,sscanf 就是从字符串中按照一个格式读取出格式化的数据
sprintf 和 sscanf 可以把结构体中的数据打包成一个字符串,也可以对某个字符串进行拆分。
这个东西在我们生活中有应用,比如当我们登录账号时,会把账号、密码这个结构体打包成一串字符串,交给后端处理,当然有个更高级的名词:序列化与反序列化。
注意:
- printf 输出家族返回的是实际写入(输出)的字符总数(包括转义字符),而scanf 输入家族返回的是实际读取(输入)的元素个数。举例一下,字符串abc,输出返回3,输入返回1,因为此时的字符串视为一个元素。
- 单纯写文本数据时,要使用指令 "w" ;单纯读数据时,要使用指令 "r"
fwrite 与 fread
fwrite 是对文件进行二进制数据的写入,fread是以二进制的形式读取文件中的数据
fwrite
//文件读写指二进制写入
struct S
{
char name[20];
int age;
float score;
}a = {"zhangsan",20,65.7f};
int main()
{
//文件打开
FILE* pf = fopen("data.txt", "wb"); //将用二进制写入
if (pf == NULL)
{
perror("fopen");
return 1;
}
//文件操作
//二进制的写文件
fwrite(&a, sizeof(a), 1, pf);
//文件关闭
fclose(pf);
pf = NULL;
return 0;
}
fread
//文件读写指二进制读取
struct S
{
char name[20];
int age;
float score;
}a ;
int main()
{
//文件打开
FILE* pf = fopen("data.txt", "rb"); //将用二进制读取
if (pf == NULL)
{
perror("fopen");
return 1;
}
//文件操作 把文件中的数据读取到a中
fread(&a, sizeof(a), 1, pf);
printf("%s %d %.1f", a.name, a.age, a.score);
//文件关闭
fclose(pf);
pf = NULL;
return 0;
}
注意:
- 当我们使用二进制写入数据到文件时,如果是以文本的方式打开,只能看懂字符串部分,数字部分是看不懂的,我们可以通过VS中的二进制编码器,来观察其中的数据。
- 单纯写二进制数据时,要使用指令 “wb” ;单纯读二进制数据时,要使用指令 “rb"
随机读写
随机读写函数的,需要配合上面的输入输出函数使用,所谓的随机读写,是指通过改变文件指针的偏移量,来写入或者读取数据。介绍三个和随机读取有关的函数:fseek 改变文件指针偏移量、ftell 查看当前我呢见指针的偏移量、rewind 使文件指针复原至起始位置。
fseek
//fseek ,文件指针偏移量
int main()
{
//打开
FILE *pf = fopen("data.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
// 进行操作
char* pc = "abc";
fseek(pf, 5, SEEK_SET); //起点往后偏移
fputs(pc, pf);
//关闭
fclose(pf);
pf = NULL;
return 0;
}
ftell
//ftell,返回当前文件指针偏移信息
int main()
{
//打开
FILE* pf = fopen("data.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
// 进行操作
printf("当前文件指针偏移量为:%d\n", ftell(pf));
fseek(pf, 5, SEEK_SET); //起点往后偏移 5个
printf("经过fseek设置后的文件指针偏移量为:%d\n", ftell(pf));
//关闭
fclose(pf);
pf = NULL;
return 0;
}
rewind
//rewind,使文件指针恢复至当前位置
int main()
{
//打开
FILE* pf = fopen("data.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
// 进行操作
fseek(pf, 5, SEEK_SET); //先让文件指针向后偏移5
printf("当前文件指针偏移量为:%d\n", ftell(pf));
rewind(pf);
printf("经过fseek设置后的文件指针偏移量为:%d\n", ftell(pf));
//关闭
fclose(pf);
pf = NULL;
return 0;
}
注意:
- 每进行一个我呢见输入输出操作,文件指针都会向后移动一位,比如读到字符'b'后,文件指针向后移动一位,指向字符‘c’ ,此时只需要把文件指针向后偏移一位,就能愉快的读取到字符‘d’了
文件使用注意是事项
被错误使用的feof
很多小伙伴会把 feof 是用来判断文件是否读取结束,这个是错误的用法,因为 feof 的作为u是判断当前文件读取结束原因,如果是因为读取到了末尾而结束,feof(fp) 就为真;除了这个以外,还有另一个文件读取结算原因判断函数,ferror ,当 ferror(fp) 为真时,说明此时发生了读取异常,并非正常结束,我们可以通过这两个报错函数来判断文件读取结束的真正原因。
牢记:在⽂件读取过程中,不能⽤feof函数的返回值直接来判断⽂件的是否结束。feof 的作⽤是:当⽂件读取结束的时候,判断是读取结束的原因是否是:遇到⽂件尾结束。⽂本⽂件读取是否结束,判断返回值是否为 EOF ( fgetc ),或者 NULL ( fgets )例如:
- fgetc 判断是否为 EOF
- fgets 判断返回值是否为 NULL
- ⼆进制⽂件的读取结束判断,判断返回值是否⼩于实际要读的个数
- fread判断返回值是否⼩于实际要读的个数。
⽂件缓冲区
验证文件缓冲区是否存在
我们可以利用睡眠函数 Sleep 来使程序暂停,此时数据还没有被写入文件中,仍然位于缓冲区;之后再手动刷新缓冲区,数据此时会被推送至文件中。
//文件缓冲区 #include<windows.h> int main() { //打开文件 FILE* fp = fopen("test.txt", "w"); if (NULL == fp) { perror("fp"); return 1; } char* ps = "测试文件缓冲区"; fputs(ps, fp);//先将数据写到缓冲区中 printf("数据现在已经在缓冲区里面了,但还没有推送到文件中\n"); printf("程序睡眠10秒,10秒后刷新缓冲区\n"); Sleep(10000);//睡眠函数,单位是毫秒 fflush(fp); printf("现在缓冲区已经刷新,数据已经写入文件中了\n"); Sleep(10000); //关闭文件,当文件关闭时,缓冲区也会被刷新 fclose(fp); fp = NULL; return 0; }
注意:
- fclose 关闭文件后,会自动刷新缓冲区,数据能够推送至文件
- 当程序运行结束后,缓冲区也会被自动刷新
- scanf 遇到 \n 也会触发缓冲区刷新,另外如果其在读取字符型数据时,遇到空白字符(空格、TAB键)也会触发缓冲区的刷新
总结
以上就是C语言文件操作的所有内容了,从文件的打开到文件的关闭,中间可以进行多种操作,构造出巧妙的数据。学到这里相必对文件操作有着一定的理解和收获。
每一篇都在很用新的写,如果觉得不错的话,可以用你发财的小手点点赞!!!