目录
1.文件简介
无论是否是计算机相关从业者,文件是人们日常工作生活中常挂于嘴边的词语,那么到底什么是文件呢?
为了提高计算机运行程序的速度,我们在程序中创建的变量都储存在内存中,不论是栈区还是堆区,如果程序退出,程序所申请的内存被回收给操作系统,数据就丢失了,当我们再次运行程序,数据就已经不在了。如果要将数据持续化保存,我们就要使用文件。
磁盘上的文件是文件。
使用文件 可以理解为这是一种拿时间换空间的做法(磁盘的调用速度会远小于内存,只不过在目前的小体量代码与程序中不会体现)。
2.文件分类
从⽂件存储数据的⻆度来分类 :程序⽂件、数据⽂件,程序文件一般用来操作数据文件。
文件名中不能包含这些字符:\/:*?"<>|
2.1程序文件
程序⽂件包括源程序⽂件(后缀为.c),⽬标⽂件(windows环境后缀为.obj),可执⾏程序(windows 环境后缀为.exe)。
2.2数据文件
需要从中读取数据的⽂件,或者输出内容的⽂件。如.txt和.bat
也是本文接下来将要讨论的重点。
数据文件也分为二进制文件和文本文件
2.2.1 二进制文件
数据在内存中本来就以二进制的形式储存,如果我们不加转化的输出到外存,就是二进制文件(将在接下来的fwrite和fprintf中会有明显的感知)。
先运行一次程序,接着我们打开相对路径test.txt找到该文本文件。
后缀.txt 是文本文件,不能识别不加转换的二进制文件,所以我们只能查看到一个“?”符号。
2.2.2文本文件
还是上面的用例,a就是1 0 0 0五个字符,将这五个字符转到对应的ASCII码进行存储。‘0’是48(00110000),‘1’是49(00110001)。
3.流
我们程序的数据需要输出到各种外部设备,也需要从外部设备获取数据,不同的外部设备的输⼊输出 操作各不相同,为了⽅便程序员对各种设备进⾏⽅便的操作,我们抽象出了流的概念,我们可以把流 想象成流淌着字符的河。
3.1 标准流
由此可见,流相当于我们之前学习的各种输入输出函数是更宏观的概念,在我们之前所学习的程序“更高层” 。
3.2 一个栗子:
先字面意思理解fputc和fgetc两个函数的使用。
fputc('a',stdout);
fputc将a放入标准输出流,只要被放进标准输出流,都会被屏幕输出。
接下来,fgetc函数希望从标准输入流中获得一个字符,回顾上文我们给出的stdin的定义,再观察屏幕上打印出来的a之后的闪烁的光标可知,刺出程序在等待我们输入,键盘上的输入会进入标准输入流(stdin),fgetc就能从标准输入流中拿到我们输入的字符。
3.3 流的格式
在c语言中,我们规定以上三种流的格式是FILE* ,即文件指针,我们是通过文件指针来维护流的各种操作。
有了流的概念,我们才能更好的理解sscanf sprintf 与fprintf fscanf。
4.文件指针
其本质是由系统申明的叫FILE的结构体来保存,但是在现在的编译器封装的较好,已经不便于观察。
每次文件被调用时都会创建一个文件信息区来进行缓存,文件指针就能用来维护、找到、调用文件。
5.调用文件
5.1文件的打开和关闭
相似于动态内存管理,文件的调用需要使用函数fopen,使用结束之后使用函数fclose关闭。
打开和关闭都是必须存在的
打开文件:
FILE* fopen(const char* filename,const char* mode);
也就是:
第一个参数是文件名,第一排的文件路径是相当路径,也就是相当于程序所在的文件夹,自动在该文件夹里寻找。第二三排是绝对路径,注意使用双斜杠来避免转义字符。
第二个mode表示打开方式,c语言标准下的打开方式是固定的:
(带b的都表示是与二进制(binary)相关的)
注意:如果用w打开一个已经写好有内容的文件,他会清空这个文件来使你再次“write”,如果只是想打开并阅读一个写好的文件,直接使用“r”即可。
关闭文件:
int fclose ( FILE * stream );
直接将对应的文件指针传入fclose函数即可。如:fclose(pf);
在实际项目或者公司写代码中,建议一下两种形式检查是否返回空指针并且将回收的文件指针置NULL
assert不会报出错误,但是perror能报出错误,进一步了解问题出在哪。
return 1;与return EXIT_FAILURE;等价。
5.2文件的读写
文件的读写分为顺序读写和随机读写。当我们打开一个文档时,每写一个字符光标就向后跳一个,这就是顺序读写的体现。
5.2.1顺序读写
1.第一组fgetc和fputc函数
返回值都是int,fgetc的参数是一个文件指针(流),fputc的第一个参数是你想要放的字符,第二个参数是你想放入的流(就像河流一样,不管是输入流还是输出流,你放到哪个流就会向哪个方向走,放入标准输出流就直接在屏幕上输出,放入标准输入流就可以建立一个变量来从键盘等标准输入方式接受一个字符)。
提示:如上文所说,流的类型就是文件指针。
加强了对流的理解,我们通过实例观察如何使用这一类函数
如下,我们实现了:打开两个文件,其中一个在相对路径data.txt 里,从a到z输入字符串并且每四个一排。最后记得关闭文件:fclose(pr和p)并置NULL
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
int main()
{
//打开文件
FILE* p = fopen("data.txt", "w");
//FILE* pr = fopen("C:\\Users\\asus\\Desktop\\data2.txt", "w");
FILE* pr = fopen("C:\\Users\\asus\\Desktop\\data2.txt", "r");
assert(pr != NULL);
if (p == NULL) {
perror("fopen");
return 1;
}
//操作文件
//fputc操作
for (int i = 1; i <= 26; i++) {
fputc('a' + i-1, p);
fputc(' ', p);
if (!(i % 4)) {
fputc('\n', p);
}
}
int c = fgetc(pr);
printf("%c\n", c);
//fputc('a', stdout);//只要被放进标准输出流,都会被屏幕输出,
//fgetc(stdin);此语句执行后,会等待我们输入一个字符到标准输入流,然后fgetc函数会从标准输入流里面取得我们
//输入标准输入流的东西
//关闭文件
fclose(p);
fclose(pr);
p = NULL;
pr = NULL;
return 0;
}
我们再进行举例,先在data2.txt中输入一个x(输入很多字符也可以,但是fgetc只会拿取第一个)。
**标准错误,经典零分:
在第二个例子中我们是稍加修改:
我们将fgetc后面的参数改成了p,此时为什么屏幕打印不出任何东西呢?我们不是明明已经用循坏的方式在p指向的文件data.txt输入了每四个一排的英文字母吗?
这是因为 :p这个文件指针现在是可写(w)打开的,不能读。
我们需要先fclose关掉此文件,再以r或其他方式打开,才能读里面的内容。
2.fgets和fputs
fgets(char* str,int num,FILE* stream);
fputs(char* str,FILE* stream);
注意:fgets函数中的num表示读取的字符个数,但是实际只能读取(n-1)个字符,目的是给\0留位置。
帮助记忆:文件相对于我们学习的数组、动态内存等空间都要大得多(硬盘),所以往里面放东西的时候(fputs)不需要担心能不能放得下,不需要标识需要放几个(参数只有两个,把谁放到哪去),而fgets是从文件这样一个比数组等内存空间“大得多”的空间里取内容,就需要标注拿几个,避免空间非法访问,故参数是三个。
3.fprintf和fscanf
这是cpp官网上对四种函数的定义。相对于我们熟悉的scanf和 printf,只是多了第一个参数流。
如果fscanf的流写stdin,fprintf的流写stdout1,那么就与scanf和printf无异了。
4.fread和fwrite
第一个参数是要被写入文件的地址,第二个是要被写入的大小,第三个是元素个数,最后一个是流。
同理。ptr就是你希望放东西进去的地址:在流中读取count个大小为size的内容到ptr所指向的空间里去,返回真实读取到的空间大小
提醒:fwrite与fread的参数中,元素大小与元素个数的顺序与qsort相反
举例理解:
struct Stu {
char name[6];
int age;
float score;
};
我们先声明一个结构体变量。
敲黑板 :这就是前文提到的,二进制的输入在不被转换的情况下并不能被.txt理解。
但是二进制的内容在 fread函数下就能被理解了。
如果将s2.score按照六位数打印,我们还能发现存进去的99.99出来之后就是99.989998了,更能说明这一段时间该变量一直是以浮点数的形式存储的,感兴趣的朋友可以点这里:整数与浮点数在内存中的存储(包含char类型的加减和打印等经典试题)-优快云博客
5.2.2文件的随机读写
随机读写并不是随机数,并不是让你真正的随机去访问文件内容,而是能够控制整个访问过程,我们将简单介绍几种相关的函数。
1.fseek
int fseek(FILE* stream,long int,int origin);
依然利用一个例子来学习:
我在seek.txt里先输入了qwerdf
由此可见,文件指针pt(光标)每次都会往后移动一个,所以每次都能读取下一个。
那么当程序算法复杂,文件体量较大时,我们可能需要这个光标按照我们的意志来移动,那么我们就使用fseek函数
我们在文件pt里,从SEEK_CUR的位置让指针偏移-2个位置,再度找到r并打印出来。
origin一共有这三种取值,分别表示从开头,当前位置,末尾来启动偏移。
负数向左偏,正数右偏。
2.ftell和rewind
ftell参数为文件指针,返回值为文件指针距离起始位置的偏移量(long int类型)。
rewind没有返回值,参数为文件指针,目的就是让文件指针回到起始位置,重新进行操作。
fseek(pt,0,SEEK_END);
int n = ftell(pt);
//这样就能简易计算文件空间大小
//最后回到起始位置
rewind(pt);
6.文件读取的判定
简单提及两个函数:ferror与feof。
内容较简单,读者百度搜索搜索即可。
此处主要是想提醒feof、ferror是经常被拿来错用的函数:
不是用来判断文件读取是否结束,而是判断是否是读取过程是否遇到错误结束(ferror)或是否遇到文件尾(feof)结束。
if (ferror(fp))
puts("I/O error when reading");
else if (feof(fp))
puts("End of file reached successfully");
至此,初学者需要了解的文件相关知识已经全部介绍结束,本文也只是笔者在初学过程中自我记录与笔记的方式,只是粗浅讲解相关内容,但是是能覆盖大部分学校的基础考试与一般期末,介绍内容的错误望各位看官能指正!!