【文件】C语言文件操作及其使用总结篇【初学者保姆级别福利】
【文件】C语言文件操作及其使用总结篇【初学者保姆级别福利】
一篇博客学好动态内存的管理和使用
这篇博客干货满满,建议收藏再看哦!!
求个赞求个赞求个赞求个赞 谢谢🙏
先赞后看好习惯 打字不容易,这都是很用心做的,希望得到支持你 大家的点赞和支持对于我来说是一种非常重要的动力 看完之后别忘记关注我哦!️️️
本篇基本上涵盖了初学阶段动态内存的所有知识点,喜欢的伙伴一定要看完哦!
强烈建议本篇收藏后食用~ 干货满满!
看完这篇博客,相信伙伴们可以基本上掌握以下内容:
1.为什么使用文件
2.什么是文件
3.文件的打开和关闭
4.文件的顺序读写
5.文件的随机读写
6.文件读取结束的判定
7.文件缓冲区
文章目录
前言
1.为什么要使用文件
在以前的C语言学习当中,我们了解到,我们在运行程序的时候所定义的数据,是保存在内存中的,有可能是栈区上,有可能是堆区上,也有可能是在其它地方上,但是,这些地方,都是属于内存上的。在内存上的数据,是计算机在程序运行的时候所开辟出来的,因此,在程序结束的时候,内存上的空间都会被返还给内存,因此我们是做不到数据的持久化保存的。
这时,我们就需要用到文件。使用文件,我们就可以将数据直接存放在电脑的硬盘上,做到了数据持久化。
什么是文件?
磁盘上的文件是文件。但是在程序设计中,我们一般所指的文件有两种:程序文件、数据文件。
关于这部分知识相对于没有那么的重要,博主在这里就不展开细讲,想了解的小伙伴可以给我私信留言哦。
文件的打开和关闭
文件指针
首先,我们要知道,每一个被使用的文件都在内存中开辟了一个相应的文件信息去,用来存放文件的相关信息(如文件的名字,文件状态及文件当前的位置等)。这些信息是要保存在一个结构体当中的。但是这个结构体是不用我们自己声明的,系统已经帮我们声明好了,已经被取好名了,为
FILE
。
另外,每次我们打开文件的时候,系统都会自己生成一个FILE结构的变量,里面有关信息系统会自己录入,我们作为使用者暂时不必关心细节。
OK。接下来,我们想要找到这个系统给我们准备好的结构体并使用它,我们就要通过文件指针找到这个文件。
FILE*pf;//文件指针变量
文件的打开和关闭
有了pf这个指针,我们在操作文件之前,就要通过pf来打开文件,在使用结束后一定要记得关闭文件。
想要打开和关闭文件,我们要用到两个函数:
fopen()和fclose()
我们先来看这两个函数的原型:
int main()
{
//打开文件
FILE* fopen(const char* filename, const char* mode);
//关闭文件
int fclose(FILE * stream);
return 0;
}
在fopen()函数中,filename指的是文件名,这个很好理解,fclose()函数中stream指的是,流。关于流的概念,我会在后面继续讲解,这里指的就是我们用FILE*
类型定义的函数指针。
而mode,指的是文件的打开方式,为char*
类型。
文件的打开方式有如下几种
文件使用方式 | 含义 | 如果指定文件不存在 |
---|---|---|
“r”(只读) | 为了输入数据,打开一个已经存在的文本文件 | err |
“w”(只写) | 为了输出数据,打开一个文本文件 | 建立一个新的文件 |
“a”(追加) | 往文本文件尾添加数据 | 建立一个新的文件 |
“rb”(只读) | 为了输入数据,打开一个二进制文件 | err |
“wb”(只写) | 为了输出数据,打开一个二进制文件 | 建立一个新的文件 |
“ab”(追加) | 向一个二进制文件尾添加数据 | err |
“r+”(读写) | 为了读和写,打开一个新的文件 | err |
“w+”(读写) | 为了读和写,建立一个新的文件 | 建立一个新的文件 |
“a+”(读写) | 打开一个文件,在文件尾进行读写 | 建立一个新的文件 |
“rb+”(读写) | 为了读和写打开一个二进制文件 | err |
“wb+”(读写) | 为了读和写,创建一个新的二进制文件 | 建立一个新的文件 |
“ab+”(读写) | 打开一个二进制文件,在文件尾进行读和写 | 建立一个新的文件 |
例子:
int main()
{
FILE*pf1=fopen("test.dat", "w");//这种是相对路径,是要放到程序那个指定文件夹里面才能被找到
FILE* pf2 = fopen("D:\\test.dat", "r");//有位置的叫绝对路径,是可以找到的
//注意\如果只有一个,是会被理解成转义字符的
//所以\都要变成两个
if (pf1 == NULL)
{
perror("fopen");
return 1;
}
//写文件
//关闭文件
fclose(pf1);
pf1 = NULL;
return 0;
}
在这里,我想给各位小伙伴讲一下使用使用文件的一些好习惯和细节:问下大家有注意到fopen()后面这一段代码吗
if (pf1 == NULL) { perror("fopen"); return 1; }
这一段代码的作用,就是防止文件打开失败,而fopen()
在文件打开失败的时候,是会返回空指针的,如果我们不加以检验就对pf
进行使用,注意:此时pf
是个空指针,是会出问题的,所以,我们需要给pf
验空。
另外,在我们fclose()
关闭文件之后,必须要将pf置为空指针,防止后面对pf
进行非法的解引用操作。
文件的顺序读写
基本函数的介绍
文件的顺序读写,我们必须知道一些必要的文件操作函数
功能 | 函数名 | 适用于 |
---|---|---|
字符输入函数 | fgetc | 所有输入流 |
字符输出函数 | fputc | 所有输出流 |
文本行输入函数 | fgets | 所有输入流 |
文本行输出函数 | fputs | 所有输出流 |
格式化输入函数 | fscanf | 所有输入流 |
格式化输出函数 | fprintf | 所有输出流 |
二进制输入 | fread | 文件 |
二进制输出 | fwrite | 文件 |
例子
int main()
{
FILE* pf = fopen("test.dat", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//写文件
fputc('b', pf);//往pf所指向的文件里面写入一个b
fputc('i', pf);
fputc('t', pf);
//按顺序写的
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
我们打开后台文件夹,就可以找到test.txt这个文件了,我们可以看到bit是按照顺序录入进了这个文件当中。
流的概念
流是一个高度抽象的概念
程序-屏幕/硬盘/优盘/光盘…
各种硬件读写方式也不同
为了让写程序的人不用那么麻烦
所以在程序和硬件之间加一个流的概念
C程序只要运行起来,就默认打开了三个流
stdin
-标准输入流-键盘
stdout
-标准输出流-屏幕
stderr
-标准错误流-屏幕
以上三个流都是FILE*
类型的
因此我们可以得出,文件是一种流,屏幕,键盘也都是流,而流一般用stream表示。
例子:
//想用fputc给屏幕打印数据
int main()
{
fputc('b', stdout);
fputc('i', stdout);
fputc('t', stdout);
return 0;
//屏幕也是个流,文件也是个流
//我们写数据可以往文件里面写,当然也可以向屏幕上写
}
一些文件操作函数使用的例子
例子1:
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
}
//读文件
int ret = fgetc(pf);
printf("%c", ret);
ret = fgetc(pf);
printf("%c", ret);
ret = fgetc(pf);
printf("%c", ret);
ret = fgetc(pf);
printf("%c", ret);
ret = fgetc(pf);
printf("%c", ret);
ret = fgetc(pf);
printf("%c", ret);
ret = fgetc(pf);
printf("%c", ret);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
分析:
这里的文件是用
"r"
,也就是读取的方式打开的,我们通过fgetc()
函数一次读取一个文件里面的字符,并存进ret
后打印。因此,如果我们一开始要现在指定的文件夹里面先写入一些字符,我们F5运行起来之后,我们就可以看到这些打印的字符了。
> 运行后:
例子2:
//读键盘
//从标准输入流里面读取信息
int main()
{
int ret = fgetc(stdin);//从标准输入流读取信息
printf("%c", ret);
ret = fgetc(stdin);
printf("%c", ret);
ret = fgetc(stdin);
printf("%c", ret);
return 0;
}
分析:
从标准输入流(键盘)
里面输入信息之后,用printf()
打印出来。
例子3:
int main()
{
FILE* pf = fopen("test.dat", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//写文件-按照行来写
fputs("abc\n",pf);
fputs("qwertyuiop", pf);
//这种都叫以文本的形式写进去
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
经过上面的学习,这些代码都比较好理解,这里就不赘述了。
运行之后:
例子4:
int main()
{
FILE* pf = fopen("test.dat", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
char arr[10] = { 0 };
fgets(arr, 4, pf);
printf("%s", arr);
fgets(arr, 4, pf);
printf("%s", arr);
//这里的4,事实上其实只会读三个,因为要留位置给\0
//后面接着读是不会从第二行开始读的,要把内容读完才行
return 0;
}
注意:
arr
表示从文件录入的数据存入arr
中,4表示录入4个字符,pf
指向那个文件
但是,此时的录入的4个字符,只有3个是有效的,因为要留位置给'\0'
。
后面接着读是不会从第二行开始读的,要把内容读完才行
运行:
fprintf、fscanf、sprintf和sscanf函数的使用
fprintf()函数和fscanf()函数
fprintf()原型:
int fprintf ( FILE * stream, const char * format, ... );
fscanf()原型:int fscanf ( FILE * stream, const char * format, ... );
通过对比我们发现,这两个函数分别比printf,scanf只多了一个参数FILE*stream
,这个参数,也就是一个流。
那么我们通过两个例子,就可以很好的理解这两个函数。
例子1:
struct S
{
char arr[10];
int num;
float sc;
};
int main()
{
struct S s = { "abcdef",10,5.5f };
//现在想把它放到文件里面
//对格式化的数据进行写文件
FILE* pf = fopen("test.dat", "w");
if (NULL == pf)
{
perror("fopen");
return 1;
}
//写文件
fprintf(pf, "%s %d %f", s.arr, s.num, s.sc);
fclose(pf);
pf=NULL;
很容易理解,我们想要进行格式化输出的时候,只需要在fprintf()的第一个参数上传我们要操作的流,后面的参数和printf()的使用完全一致。同理,我们也可以很好的理解fscanf()这个函数。
例子2:
现在我们想要把刚才放进test.dat里面的数据还原到一个结构体中来。
struct S
{
char arr[10];
int num;
float sc;
};
//能否读出来,还原成一个结构体?
int main()
{
struct S s = { 0 };
//现在想把它放到文件里面
//对格式化的数据进行写文件
FILE* pf = fopen("test.dat", "r");
if (NULL == pf)
{
perror("fopen");
return 1;
}
//读文件
fscanf(pf,"%s %d %f", s.arr, &(s.num), &(s.sc));
//打印以下看看结果
printf("%s %d %f\n", s.arr, s.num, s.sc);
fclose(pf);
pf = NULL;
return 0;
}
思考:
通过以上两个例子,我们就可以很好的明白fprintf()
,fscanf()
这两个函数的使用。此时,我们要思考:我们前面说了,键盘,屏幕也都是流,那么,我们将fprint()
,fscanf()
的第一个参数改成标准的输出输入流,那么它们的作用,就是相当于printf()
和scanf()
啊。
事实确实是如此:
fprintf(stdout,...,...)
和printf(...,...)
等价。
fscanf(stdin,...,...)
和scanf(...,...)
等价。
sprintf()函数和sscanf()函数
其实这两个函数的运用并不属于文件这一部分的内容,但是我们讲到fprintf(),fscanf(),printf,scanf()
这些函数,那我们也把这最后两个名字相似的函数讲了,给伙伴们区分一下。
scanf()
针对标准输入的格式化的输入语句-stdin
fscanf()
针对所有输入流的格式化的输入语句
sscanf()
从一个字符串中读取一个格式化的数据
printf()
针对标准输出的格式化输出语句-stdout
fprintf()
针对所有输出流的格式化输出语句
sprintf()
把格式化的数据,转成字符串
例子:
//sprintf/sscanf
//把格式化的数据写到一个字符串里面
struct S
{
char arr[10];
int age;
float f;
};
int main()
{
struct S s = { "hello",20,5.5f };
//能否转化为一个字符串呢
char buf[100] = { 0 };
sprintf(buf, "%s %d %f", s.arr, s.age, s.f);
printf("%s\n", buf);
//能否从buf字符串中还原出一个结构体
struct S tmp = { 0 };
sscanf(buf, "%s %d %f", tmp.arr, &(tmp.age), &(tmp.f));
printf("%s %d %f\n", tmp.arr, tmp.age, tmp.f);
//两次printf的结果应该是一样的
return 0;
}
文件的二进制读写
两个函数:fwrite()
函数和fread()
函数
fwrite():
原型:size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream );
第一个参数表示被写数据(可以是个数组)的地址,第二个参数表示每一个数据的大小(如果是数组,就是每个元素的大小),第三个参数表示数据个数(如果不是数组,就是1,如果是数组,就是数组元素个数,第四个参数表示流。
fread():
原型:size_t fread ( void * ptr, size_t size, size_t count, FILE * stream );
参数含义和fwrite()相同。
顾名思义就是用二进制的形式读写,是我们看不懂的,我们通过两个例子就可以很好的理解这两个函数。
例子:
//二进制的读写
struct S
{
char arr[10];
int num;
float sc;
};
//fwrite
//size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream );
int main()
{
struct S s = { "abcde",10,5.5f };
//二进制的形式写
FILE* pf = fopen("test.dat", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//写文件
fwrite(&s,sizeof(s),1,pf);
//后台打开文件,我们是看不懂的,因为是二进制形式搞进去的
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
我们是看不懂后台输入文件的含义的,因为我们是以二进制的形式输入的,要想读懂,我们必须通过fread()
函数,以二进制的形式读。
//我们要用fread才能看懂
//fread
struct S
{
char arr[10];
int num;
float sc;
};
//fread
//size_t fread ( void * ptr, size_t size, size_t count, FILE * stream );
int main()
{
struct S s = {0};
//二进制的形式读
FILE* pf = fopen("test.dat", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读
//参数和fwrite一样
fread(&s, sizeof(struct S), 1, pf);
//都出来之后我们打印出来看看
printf("%s %d %f\n", s.arr, s.num, s.sc);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
通过fread(),我们就成功读出fwrite()刚刚写进去的内容了。
文件的随机读写
想要掌握文件的随机读写,我们必须掌握的其中一个函数就是fseek()
函数
fseek():
这个函数的作用就是用来调整文件指针位置的。
比如说,后台的test.dat里面有一串字符串abcdefg,但是我读的时候不想从头开始读,那么我们就可以通过fseek()
函数来调整文件指针的位置,让它从a的位置挪开。
原型:int fseek ( FILE * stream, long int offset, int origin );
第一个参数表示针对的流,第二个参数表示调整后的文件指针相对于原始位置的偏移量,第三个参数表示文件指针的原始位置。
在此其中,第三个参数中我们常用的三个量为:
SEEK_CUR;
//current position of file pointer指针当前位置
SEEK_END;
//end of file文件末尾
SEEK_SET;
//beginning of file文件开头
ftell():
这个函数返回一个整型,告诉我们当前文件指针相对于起始位置的偏移量。
rewind():
这个函数可以让文件指针回到起始位置
那么上面这些函数怎么用呢,我们看一个例子就明白了
例子:
int main() {
FILE* pf = fopen("test.txt", "r");
if (pf == NULL) {
perror("fopen");
}
char ch = 0;
//先打开后台在里面写入abcdefg
//现在里面已经有abcdefg了
ch=fgetc(pf);
printf("%c\n", ch);//此处打印的是a
//现在a已经读完了,文件指针指向的是b,那么怎么跳过b,c直接读d呢?
fseek(pf, 2, SEEK_CUR);//用fseek()调整一下指针位置,跳过两个字符
ch = fgetc(pf);
printf("%c\n", ch);//此处打印的是d
int ret = ftell(pf);
printf("%d\n", ret);//此处打印的是4
rewind(pf);
ret = ftell(pf);
printf("%d\n", ret);//此处打印的是0,因为pf已经被rewind了
fclose(pf);
return 0;
}
文件读取结束的判定
文件读取结束的判定
被错误使用的feof:在很多文章中和教学中经常说feof可以判断结束,其实是不准确的
牢记:在文件读取过程中,不能用feof函数的返回值直接用来判断文件是否结束
feof的作用是用于文件读取结束的时候,判断是读取失败结束,还是遇到文件尾结束,如果feof返回值为真,表示正常读取结束;如果为假表示文件读取失败而结束读取。
文本文件读取是否结束,是通过fgetc函数的返回值是否是EOF来判定的
fgets函数正常读取结束的时候会返回空指针,正常读取的时候,返回放字符串的空间起始地址
fread函数在读取的时候,返回的是实际读取到的完整元素的个数
如果发现读取到的完整的元素的个数小于指定的元素个数,这就是最后一次读取了。
例子:
写一个函数拷贝一个文件:
//写代码把test.txt文件拷贝一份,生成test2.txt文件
int main()
{
FILE* pfread = fopen("test.txt", "r");
if (pfread == NULL)
{
return 1;
}
FILE* pfwrite = fopen("test2.txt", "w");
if (pfwrite == NULL)
{
fclose(pfread);
pfread = NULL;
return 1;
}
//读写文件
int ch = 0;
while ((ch = fgetc(pfread) != EOF))
{
//写文件
fputc(ch, pfwrite);
}
//判断读取是因为什么结束的
if (feof(pfread))
{
printf("遇到文件结束标志,文件正常结束\n");
}
else if (ferror(pfread))
{
printf("文件读取失败结束\n");
}
//关闭文件
fclose(pfread);
pfread = NULL;
fclose(pfwrite);
pfwrite = NULL;
return 0;
}
文件缓冲区
文件缓冲区
程序数据区(内存)<>硬盘
在这其间存在输出缓冲区和输入缓冲区
例子:
这个例子不是本章重点,只是让伙伴们理解一下文件缓冲区的概念,小伙伴可以复制代码按照指示运行一下就可以理解了,这里不再赘述。
#include<windows.h>
int main()
{
FILE* pf = fopen("test.txt", "w");
fputs("abcdef", pf);//先将代码放在输出缓冲区
printf("睡眠10s-已经在写数据了,打开test.txt文件,发现文件没有内容\n");
Sleep(10000);
fflush(pf);//刷新缓冲区时,才将输出缓冲区的数据写到文件(硬盘)
//注:fflush在高版本vs上不能使用了
printf("再睡眠10秒-此时,再次打开test.txt文件,文件有内容了\n");
Sleep(10000);
fclose(pf);
//注:fclose在关闭文件的时候,也会刷新缓冲区
pf = NULL;
return 0;
}
尾声
能看到这里的小伙伴,如果你已经完全理解了这篇博客的内容,相信你对文件的理解已经提高了一个层次了。如果感觉这篇博客对你有帮助的话,别忘了你的赞,收藏和关注哦