C语言文件操作(上)
文章目录
1. 为什么要使用文件
2. 什么是文件
2.1 程序文件
2.2 数据文件
2.3 文件名
3. 文件的打开和关闭
3.1 文件指针
3.2 如何打开和关闭文件
3.2.1. 打开文件:fopen
3.2.2 关闭文件:fclose
3.2.3 补充
4. 文件的顺序读写
4.1 fputc
4.2 fgetc
4.3 fputs
4.4 fgets
4.5 fprintf
4.6 fscanf
4.7 补充
4.8 fwrite
4.8 fread
5. scanf/fscanf/sscanf printf/fprintf/sprintf 两组函数对比
5.1 sprintf
5.2 sscanf
5.3 总结
6. 文件的随机读写
6.1 fseek
6.2 ftell
6.3 rewind
7. 文本文件和二进制文件
8. 文件读取结束的判定
8.1 feof
8.2 如何判断文件是否读取结束
8.2.1 文本文件
8.2.2 二进制文件
9. 文件缓冲区
这篇文章,我们再来一起学习一个新知识——C语言中的文件操作,一起来学习吧!!!
1. 为什么要使用文件
相信大家对于“文件”这个词应该都不陌生,肯定都会有一些自己的理解,而且大家之前肯定都使用过文件,比如在我们的电脑上就有很多文件。
那现在我们来思考一个问题:
为什么要使用文件?
比如我们用C语言写了一个通讯录的程序,当通讯录运行起来的时候,可以给通讯录中增加、删除数据,此时数据是存放在内存中,当程序退出的时候,通讯录中的数据自然就不存在了,等下次运行通讯录程序的时候,数据又得重新录入,如果使用这样的通讯录就很难受。
我们想既然是通讯录就应该把信息记录下来,只有我们自己选择删除数据的时候,数据才不复存在。
那这就涉及到了数据持久化的问题。
那大家想一下:我们平时数据持久化的方法一般有哪些呢?
比如:把数据存放在磁盘文件、存放到数据库等方式。
大家想一下,我们自己电脑磁盘上存放的文件,不就是持久化的文件嘛,只要我们不删除,就算隔很长时间再次打开,里面的数据是不是还在啊。
那现在,我们就应该知道为什么要使用文件了:
使用文件我们可以将数据直接存放在电脑的硬盘上,做到了数据的持久化。
2. 什么是文件
我们电脑磁盘里面存放的就是文件:
但是在程序设计中,我们一般谈的文件有两种:
程序文件、数据文件(从文件功能的角度来分类的)。
2.1 程序文件
包括源程序文件(后缀为.c),目标文件(windows环境后缀为.obj),可执行程序(windows环境后缀为.exe)。
比如我们平时写的代码:
2.2 数据文件
文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件。
我们本篇文章讨论的是数据文件,即如何用C语言去操作数据文件。
在以前我们所处理数据的输入输出都是以终端为对象的,即从键盘输入数据,运行结果显示到显示器(屏幕)上。
其实有时候我们会把信息输出到磁盘上,当需要的时候再从磁盘上把数据读取到内存中使用,这里处理的就是磁盘上文件。
2.3 文件名
一个文件要有一个唯一的文件标识,以便用户识别和引用。
为了方便起见,文件标识常被称为文件名。
文件名包含3部分:文件路径+文件名主干+文件后缀
比如:
c:\code\test.txt
3. 文件的打开和关闭
那知道了什么是文件,接下来我们就来学习对文件的操作,首先,我们先来学习文件的打开和关闭。
那为什么要有文件的打开和关闭呢?
就好比有一瓶水,我们想取出里面的水,或者往里面倒水,是不是都要先打开瓶子,然后才能对里面的水进行操作啊,当然最后我们最好把盖子盖上。
水瓶如此,文件亦然。
对于文件来说,我们想对它进行操作,也需要先打开它,然后再进行相应的操作,最后,我们也要关闭文件。
3.1 文件指针
首先我们来了解一个概念——文件指针。
那什么是文件指针呢?
缓冲文件系统中,关键的概念是“文件类型指针”,简称“文件指针”。
每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(如文件的名字,文件状态及文件当前的位置等)。这些信息是保存在一个结构体变量中的。该结构体类型是由系统声明的,取名FILE(是一个结构体类型)。
例如,VS2013编译环境提供的 stdio.h
头文件中有对文件类型FILE
的申明:
struct _iobuf {
char *_ptr;
int _cnt;
char *_base;
int _flag;
int _file;
int _charbuf;
int _bufsiz;
char *_tmpfname;
};
typedef struct _iobuf FILE;
不同的C编译器的FILE类型包含的内容不完全相同,但是大同小异。
每当打开一个文件的时候,系统会根据文件的情况自动创建一个FILE结构的变量,并填充其中的信息,使用者不必关心细节。
那既然我们不知道文件信息区相关细节,我们怎么去操作对应的文件呢?
一般情况下,我们都是通过一个FILE*的指针来维护这个FILE类型结构体的变量,这样使用起来更加方便。
而这个FILE*的指针其实就是文件指针。
比如:
FILE* pf;——文件指针变量
这里定义的pf就是一个指向FILE类型数据的指针变量。可以使pf指向某个文件的文件信息区(是一个结构体变量)。通过该文件信息区中的信息就能够访问该文件。
也就是说,通过文件指针变量能够找到与它关联的文件。
比如:
3.2 如何打开和关闭文件
文件在读写之前应该先打开文件,在使用结束之后应该关闭文件。
编写程序时,在打开文件的同时,都会返回一个FILE*的指针变量指向该文件,也相当于建立了指针和文件的关系。
ANSIC (美国国家标准协会(ANSI)及国际标准化组织(ISO)推出的关于C语言的标准)规定使用fopen函数来打开文件,fclose来关闭文件。
3.2.1. 打开文件:fopen
既然要使用fopen
来打开文件,那我们就先来学习一下fopen
这个函数吧。
有两个参数,分别是干什么的呢?
先来看第一个参数const char * filename
其实就是用来接收我们要打开的文件的文件名。
那第二个呢?
const char * mode
是用来接收我们打开文件的模式。都有哪些模式呢?
大家先了解一下,我们后面用到了再详细说。
那它的返回值呢?
是FILE *,这是什么,是不是就是我们前面提到的文件指针类型啊,它创建的指针变量就指向当前打开文件的文件信息区(是一个结构体变量)。通过该文件信息区中的信息就能够访问该文件。
好了,那了解了打开文件的函数fopen,我们就尝试写一个打开文件的代码:
#include <stdio.h>
int main()
{
//打开文件
FILE* pf = fopen("test.txt", "w");
return 0;
}
那这段代码的意思就是以只写的方式w打开一个名为test.txt文件(对于w方式来说,如果该文件不存在,会创建一个新文件)。然后将其返回值赋给文件指针FILE* pf。
既然提到返回值了,那我们思考一下,函数fopen打开文件有没有可能打开失败啊,失败的话返回什么?
当然是有可能的。
如果打开失败,将会返回一个空指针。
那既然有可能返回空指针,我们是不是最后对它的返回值判断一下,或断言一下,不是空指针,我们再使用。
那我们继续往下写代码:
int main()
{
//打开文件
FILE* pf = fopen("test.txt", "w");
if (NULL == pf)
{
printf("fopen");
return 1;
}
//写文件
//关闭文件
return 0;
}
如果成功打开,那我们就可以继续下面的操作了。
3.2.2 关闭文件:fclose
那最后我们是不是要关闭文件呢?怎么关呢?
使用
fclose
函数,我们来学习一下:
我们看到,它只有一个参数FILE * stream
用来接收什么呢?
其实FILE * stream
就是用来接收我们要关闭的文件对应的文件指针。
再看一下返回值:
那现在我们就可以关闭上面打开的文件了:
int main()
{
//打开文件
FILE* pf = fopen("test.txt", "w");//相对路径
if (NULL == pf)
{
printf("fopen");
return 1;
}
//写文件
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
fclose(pf);
就可以了。
但是要注意
fclose
关闭文件是不会将文件指针置空的,但是文件关闭后它指向的文件信息区就没有了,所以我们最好手动将它置空pf = NULL
,这样pf
就不再是野指针了。
那这就是一个完整的打开和关闭文件的过程。
3.2.3 补充
那接下来再给大家补充一点:
我们上面不是说一个完整的文件名报含3部分嘛:
但是上面我们打开的文件它的文件名是不是只有后两个部分,没有带上文件路径啊。
而且test.txt这个文件其实在我电脑上是不存在的,我们上面说过对于w方式来说,如果该文件不存在,会创建一个新文件。
那这样它会创建到哪里呢?
其实这样也是可以的,如果我们不带路径,它会默认创建到当前工程所在的路径(或者说就跟我们写的代码放在一块了)
我们可以验证一下
运行代码前,代码所在的文件夹是这样的:
好,运行代码结束:
那如果我们带上一个路径呢?像这样:
FILE* pf = fopen("c:\\code\\test.txt", "w");//绝对路径
注意:这里'\'
写了两个,防止其被解析为转义字符。
那此时这个文件就会创建到我们指定的这个路径下,当然前提是你给的路径得是存在的。
运行看一下:
就存在了。
那学会了了打开和关闭文件,接下来我们就来学习一下文件的读写。
4. 文件的顺序读写
首先我们来来学习顺序读写。
开始之前,我们先来回忆一个东西,我们已经熟悉的scanf和printf。
scanf的作用是什么啊?
是不是可以将我们在键盘(外部设备)上敲出来的信息输入(读操作)到内存。
printf呢?
是不是可以将内存里的东西输出(写操作)到屏幕(外部设备)上。
那这是我们已经知道的。
那我们今天要做的是:
把内存中的数据放到文件中,这叫做输出操作(写操作)
把文件中的数据放入内存中,这叫做输入操作(读操作)。
那文件的输入输出函数都有哪些呢?
好,那接下来我们就来学习一下这些函数:
4.1 fputc
fputc
可以把字符一个一个的写入到文件中。(将字符写入文件流)。
来学习一下这个函数:
看一下它的参数和返回值:
第一个参数int character
就是接收我们要写入的字符。
第二个参数
FILE * stream
接收目标文件的文件指针。
练习一下,我们现在就尝试在我们上面打开的文件test.txt
中写入一些数据:
我们先写3个字符
'a','b','c'
。
int main()
{
//打开文件
FILE* pf = fopen("test.txt", "w");
if (NULL == pf)
{
printf("fopen");
return 1;
}
//写文件
fputc('a', pf);
fputc('b', pf);
fputc('c', pf);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
那我们运行代码验证一下:
写进去了。
我们想把26个字母都写进去呢?
那就可以用一个for循环:
int i = 0;
for (i = 0; i < 26; i++)
{
fputc('a' + i, pf);
}
看看效果:
4.2 fgetc
fgetc
就是从文件流中获取字符。
学习一下:
fgetc
只有一个参数FILE * stream
,接收一个文件指针,我们想从哪个文件中获取字符,把该文件对应的文件指针传给它就行了。
返回值为int
,其实就是对应字符的ASCII码值,失败返回EOF。
我们来练习一下,就从刚才上面的文件获取一些数据打印出来:
int main()
{
//打开文件
FILE* pf = fopen("test.txt", "r");
if (NULL == pf)
{
printf("fopen");
return 1;
}
int ch = fgetc(pf);
printf("%c\n", ch);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
那我们就可以这样写。
注意:我们这次是从文件中读取数据,进行的是读操作,要把操作模式从之前的w
改成r
。
看看效果:
之前我们放在文件中的第一个字符a
就打印出来了。
如果我们在继续往后读,就会从b
开始往后接着读,不会再从头开始了:
我们可以再用一个for循环,循环26次,因为我们之前给文件中放进去了26个字符:
int i = 0;
for (i = 0; i < 26; i++)
{
int ch = fgetc(pf);
printf("%c\n", ch);
}
那这样写的话是因为我们知道有26个字符,所以循环26次,如果我们不知道文件里有多少数据,再这样写就不合适了。
那怎么解决呢?
我们再来看一下fgetc
的返回值:
它在读取失败或者读到文件末尾时都会返回EOF,那我们是不是可以利用这一点写一个循环。
int ch = 0;
while ((ch = fgetc(pf)) != EOF)
{
printf("%c", ch);
}
这样就可以了:
那这是一个字符一个字符的操作,如果想一次操作一行呢?
我们接着往下看:
4.3 fputs
fputs
可以一次把一个字符串写入数据流中。
还是先来学习一下:
两个参数,第一个参数const char * str
接收我们想要写入文件的字符串,第二个参数const char * str
还是接收文件指针。
那我们就来练习一下,写两个字符串到文件中。
int main()
{
//打开文件
FILE* pf = fopen("test.txt", "w");
if (NULL == pf)
{
printf("fopen");
return 1;
}
fputs("hello\n", pf);
fputs("world", pf);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
看看结果:
我们就把“hello world”写进去了。
注意这里“hello”后面我们自己加了一个换行符\n
,因为fputs
是不会自己在末尾追加换行符的。
4.4 fgets
fgets
是文件流中获取字符串。
char * str接收一个字符数组,这个字符数组用来存放获取到的字符串;
int num接收要复制到 str 中的最大字符数(包括终止空字符\0)。直到读取 (num-1) 个字符或到达换行符或文件结尾,以先发生者为准。终止空字符会自动追加到复制到 str 的字符之后。
FILE * stream还是接收目标文件的文件指针。
那fgets 的作用其实就是将目标文件中的num个字符作为字符串拷贝到str 指向的数组中。
注意:换行符会使 fgets 停止读取,但它被函数视为有效字符,并包含在复制到 str 的字符串中。
那返回值呢?
来练习一下吧,就把刚才我们写入的字符串读取一下,需要注意的点再给大家提一下:
int main()
{
//打开文件
FILE* pf = fopen("test.txt", "r");
if (NULL == pf)
{
printf("fopen");
return 1;
}
char arr[] = "#########";
fgets(arr, 5, pf);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
大家看:
我们给num
传参是5,但它只读了4个字符,因为最后还要补一个\0
(5个字符并不能将第一行全部读完),这样它才是一个完整的字符串。
那如果我们读10个字符呢(第一行算上\0
是6个字符)?
char arr[] = "#########";
fgets(arr, 10, pf);
我们发现它读到换行就结束了,然后补了一个\0
(当然第一行的字符串结尾本来就有一个\0
)。
当然我们也可以把两行内容全部读取并打印出来看看:
char arr[] = "#########";
fgets(arr, 10, pf);
printf("%s", arr);
fgets(arr, 10, pf);
printf("%s", arr);
4.5 fprintf
fprintf是将格式化的数据写入文件流。
我们刚刚处理的,要么是字符,要么是字符串,那如果我们想要处理其它类型的数据,比如我们想把一个结构体类型的数据写入到文件中,又该怎么办呢?
这时候就需要用到fprintf了。
其实fprintf和我们经常用的printf是很相似的,我们可以对比一下:
我们发现fprintf
只是比printf
多了一个参数FILE * stream
,就是来接收文件指针的嘛。
那就直接用呗。
我们就搞一个结构体变量,将它的成员写入文件中。
struct S
{
char name[20];
int age;
float score;
};
int main()
{
struct S s = { "zhangsan",20,95.5f };
//打开文件
FILE* pf = fopen("test.txt", "w");
if (NULL == pf)
{
printf("fopen");
return 1;
}
fprintf(pf, "%s %d %f", s.name, s.age, s.score);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
看看效果:
写进去了。
4.6 fscanf
我们把一个结构体数据写入文件了,那现在我们想把它取出来打印在屏幕上呢?
这时候需要使用fscanf
,fscanf
是从流中读取格式化数据。
那
fscanf
和scanf
又是非常相似:
fscanf
多了一个参数,用来接收目标文件的文件指针。
上代码吧:
struct S s = { 0 };
//打开文件
FILE* pf = fopen("test.txt", "r");
if (NULL == pf)
{
printf("fopen");
return 1;
}
fscanf(pf, "%s %d %f", s.name, &(s.age), &(s.score));
printf("%s %d %f\n", s.name, s.age, s.score);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
运行代码:
4.7 补充
我们知道使用printf
可以把内容输出到屏幕上,这里的屏幕叫做标准输出流,scanf
可以从**键盘(标准输入流)**读取数据。
而我们刚才学习的,把数据输入到文件中,或从文件中读取数据,文件,也是一种输出输出流。
不知道大家有没有注意到上面的一张图:
也就是说,我们刚才学的6个函数,是适用于所有的输入,输出流的。
那是不是说像fgetc
,fputc
这几个函数不仅可以作用于文件,也可以作用于屏幕和键盘?
是的!!!
另外,我们要知道:
对于任何一个C程序,只要运行起来,就会默认打开3个流:
stdin——标准输入流:键盘
stdout——标准输出流:屏幕
stderr——标准错误流:屏幕
而且这三个流的类型都是:
FILE *
那么:
如果我们想使用
fgetc
从键盘获取一个字符,只需把stdin
作为参数传给fgetc
就行了。同样的,把
stdout
传给fputc
,就可以把数据输出到屏幕上了。
我们试一下:
int main()
{
int ch = fgetc(stdin);
fputc(ch, stdout);
return 0;
}
我们输入一个字符:
就打印到屏幕上了。
那同样的,
fgets fputs fscanf fprintf
,如果也想在键盘屏幕上进行输入输出,只需让参数FILE * stream
接收stdin stdout
就行了。就不再一一举例了。
4.8 fwrite
那此外,还有两个函数,对文件进行二进制输入输出。
首先我们来看fwrite
:
参数还挺多,我们看看分别是什么:
第一个参数const void * ptr接收一个指针,该指针指向我们要写入的数据的内存地址;
第二个参数size_t size,接收每个元素的大小;
第三个参数size_t count接收元素个数;
第四个参数FILE * stream接收要操作文件的文件指针。
那我们就把之前那个结构体数据写入文件,上代码:
struct S
{
char name[20];
int age;
float score;
};
int main()
{
struct S s = { "zhangsan",20,95.5f };
//打开文件
FILE* pf = fopen("test.txt", "wb");
if (NULL == pf)
{
printf("fopen");
return 1;
}
fwrite(&s,sizeof(s),1,pf);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
这里要注意fwrite
是以二进制形式向文件中写入数据,模式要写成wb
。
看看效果:
因为是以二进制形式写入的,所以我们可能看不太懂。
4.8 fread
那我们以二进制的形式存进去了,怎么取出呢?
用
fread
,它是以二进制的形式从文件中取出数据。
我们对比一下发现,它们的参数几乎完全一样。
接收的内容其实也是一样的。
fread
其实就是从文件中取出count个大小为size的元素放到ptr
指向的空间中。
直接上代码:
int main()
{
struct S s = { 0 };
//打开文件
FILE* pf = fopen("test.txt", "rb");
if (NULL == pf)
{
printf("fopen");
return 1;
}
fread(&s, sizeof(s), 1, pf);
printf("%d %d %f", s.name, s.age, s.score);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
注意这次是rb
。传的参数 和fwrite
完全一样
看看结果:
虽然,以二进制的形式存进去我们看不懂,但是以二进制形式取出就还原回来了。
- 点赞
- 收藏
- 关注作者
评论(0)