在今天的文章中,我将要讲解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 文件输入输出函数
- 4.2 fputc函数
- fgetc函数
- fputs函数
- fgets函数
- fprintf函数
- fscanf函数
- 标准输入输出
- fwrite函数
- fread函数
- fprintf函数的多次写入和fscanf函数的多次读取
- 5.对比一组函数
- 6.文件的随机读写
- 6.1 fseek函数
- 6.2 ftell函数
- 6.3 rewind函数
- 7.文本文件和二进制文件
- 8.文件读取结束的判定
- 8.1 被错误使用的feof函数
- 8.2 文本文件判断读取是否结束
- 8.3 二进制的文件判断是否读取结束
- 8.4 函数返回值判断总结
- 8.5 读取文件的正确方法
- 8.5.1 读取文本文件的正确方法
- 8.5.2 读取二进制文件的正确方法
- 9.文件缓冲区
1.为什么使用文件
将数据放在磁盘文件里可以让数据持久化,而将数据放在内存里,程序结束运行后数据将会丢失,如实现通讯录时,存放的数据在内存里,那么当程序结束运行后,数据就会丢失。
2.什么是文件
我们打开C盘、D盘或者E盘就可以看见文件
而在文件设计的中,我们一般谈的文件有两种:程序文件、数据文件(从文件的功能角度来分类)。
2.1程序文件
程序文件包括源文件(点缀为.c)、头文件(点缀为.h)、目标文件(windows环境下点缀.obj)、可执行程序(windows环境下点缀为.exe)。
2.2数据文件
文件的内容不一定是程序,有可能是程序运行过程时读写的数据,比如程序运行时需要从中读取数据的文件,或者输出内容的文件。如:通讯录存储联系人信息的文件。(前面文章中的文件版本的通讯录主要讨论数据文件)
在以前使用的数据的输入和输出都是以终端为对象的,即从终端的键盘输入数据,运行结果显示到显示器上。而现在我可以将信息输出到磁盘上的文件,当需要的时候再从磁盘上的文件把数据读取到内存上使用。
2.3文件名
一个文件要有唯一的文件标识,以便用户识别和引用,文件标识包括三部分:文件路径+文件名主干+文件后缀。例如:c:\code\test.txt,为了方便起见,文件标识常被称为文件名。
3.文件的打开和关闭
3.1文件指针
缓冲文件系统中,关键的概念是“文件类型指针”,简称“文件指针”。
每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(如文件的名字、文件的状态以及文件当前的位置等)。这些信息是保存在一个结构体变量中的,该结构体类型是有系统声明的,取名为FILE。
一般是提供文件信息区的起始位置,再通过文件信息区找到文件(如:test.txt)。
例如:vs2010编译环境提供的stdio.h头文件中有以下的文件类型说明。
不同的C编辑器的FILE类型包含的内容不完全相同,但是大同小异。
每当打开一个文件的时候,系统会根据文件的情况自动创建一个FILE结构的变量,并填充其中的信息,使用者不必关心细节。
一般是通过一个FILE类型的指针来维护文件这个FILE结构的变量,这样使用起来更方便。
FILE* pf;
如图,FILE*的文件指针指向了文件信息区的起始位置,而文件信息区又存储着文件的相关信息,方便找到该文件,所以,文件指针可以找到与它相关联的文件。
3.2文件的打开和关闭
在进行文件的读写时,应该先打开文件,在使用结束之后应该关闭文件。
在打开文件的同时,都会返回一个FILE*的指针变量。
ANSI C规定分别使用fopen函数和fclose来打开和关闭文件。
3.2.1 fopen函数
由msdn查询可以得知,fopen的返回类型是FILE*,其中第一个参数filename是要传参文件名,第二个参数mode是要传参文件的打开方式。
3.2.2 fclose函数
由msdn查询可以得知,fclose函数的返回类型是int类型,参数stream需要传参“文件指针。
当fclose函数正常关闭文件时,返回0。
当fclose函数关闭文件失败时,返回EOF(-1)。
3.2.3 文件的打开方式
文件的使用方式 | 含义 | 如果指定的文件不存在 |
---|---|---|
“r”(只读) | 为了输出数据,打开一个已经存在的文本文件 | 出错 |
“w”(只写) | 为了输出数据,打开一个文本文件,若文件存在则清除文件内容 | 建立一个新的文件 |
“a”(追加) | 向文件的文件尾添加数据 | 建立一个新的文件 |
“rb”(只读) | 为了输入数据,打开一个二进制文件 | 出错 |
“wb”(只写) | 为了输出数据,打开一个二进制文件,若文件存在则清除文件内容 | 建立一个新的文件 |
“ab”(追加) | 向一个二进制文件尾添加数据 | 出错 |
“r+”(读写) | 为了读和写,打开一个文本文件 | 出错 |
“w+”(读写) | 为了读和写,建立一个新的文件,若文件存在则清除文件内容 | 建立一个新的文件 |
“a+”(读写) | 打开一个文件,在文件尾进行读写 | 建立一个新的文件 |
“rb+”(读写) | 为了读和写打开一个二进制文件 | 出错 |
“wb+”(读写) | 为了读和写,新建一个二进制文件,若文件存在则清除文件内容 | 建立一个新的文件 |
“ab+”(读写) | 打开一个二进制文件,在文化尾进行读和写 | 建立一个新的文件 |
使用文件的例子:
#include<stdio.h>
int main()
{
//打开文件
FILE* pf = fopen("test.txt","w");
//检查文件是否正常打开
if(pf == NULL)
{
perror("fopen");
return 1;
}
//使用文件
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
如同上面的例子,如果要在文件进行读写,那么就要先使用fopen函数打开文件,再检查文件是否打开正常,然后就可以对文件进行读和写,最后使用fclose函数关闭文件和将文件指针置为空指针。
注意的是上面的fopen函数的写法
FILE* pf = fopen("test.txt","w");
该写法并没有指明test.txt文件的路径,那么程序运行时,默认在项目文档中寻找test.txt文件,如果找不到,就会在项目中创建一个test.txt文件。
如图,项目文档中没有test.txt文件
运行程序时,程序会在项目文档中创建test.txt文件
那么我现在提出问题?程序运行时,如果要搜索需要被打开的文件时,只能在项目底下寻找吗?如果要搜索不到,需要创建文件时,是在项目底下创建吗?
答案是否定的,我们可以利用另外一种方法来让程序去对应的地方搜索或创建文件。
FILE* pf = fopen("E://test.c//test.txt","w");
我们可以写想要查询地方的绝对路径,这样就可以在相应的地方搜索或创建文件。(注意要写两个反斜杠,参考转义字符的知识)
如上面的绝对路径中,我填写的是在E盘里的test.c文档寻找test.txt文件。
因为打开方式是"w",所以当找不到该文件时,程序将会创建一个test.txt文件。
综上,如果我在使用fopen函数打开文件时,那么程序只会在项目底下寻找文件或者创建一个新的文件(特殊的文件打开方式下)
如果想要在其他地方寻找文件或者创建一个新的文件(特殊的文件打开方式下),那么就要写对应绝对路径。
4.文件的顺序读写
4.1 文件输入输出函数
功能 | 函数名 | 适用于 |
---|---|---|
字符输入函数 | fgetc | 所有输入流 |
字符输出函数 | fputc | 所有输出流 |
文本行输入函数 | fgets | 所有输入流 |
文本行输出函数 | fputs | 所有输出流 |
格式化输入函数 | fscanf | 所有输入流 |
格式化输出函数 | fprintf | 所有输出流 |
二进制输入 | fread | 文件 |
二进制输出 | fwrite | 文件 |
输入和输出、读和写的关系如下:
在C语言的输入和输出函数中,还有scanf函数和printf函数。
scanf函数是从键盘读取数据,输入到内存。
printf函数是从内存读取数据,输出到屏幕。
键盘和屏幕都是外部设备。
4.2 fputc函数
由msdn查询可以得知,fputc函数的返回类型是int类型,参数c要传参想要输出的字符的阿斯玛值,参数stream要传参文件指针。
fputc函数要是成功输出字符到文件,那么返回该字符的阿斯玛值,如果输入失败,返回EOF(-1)。
下面,我来举一个fputc函数的使用例子
#include<stdio.h>
int main()
{
int i = 0;
int n = 10;
//打开文件
FILE* pf = fopen("test.txt","w");
if(pf == NULL)
{
perror("fopen");
return 1;
}
//使用文件
for(i = 0;i<n;i++)
{
fputc('a'+ i,pf);
}
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
运行后,项目下的test.txt文件存10个字母。
fgetc函数
在msdn查询可以得知,返回类型是int,参数stream要传参文件指针。
如果fgetc函数读取到字符,返回字符的阿斯玛值,如果读取失败,返回EOF(-1)。
下面我来举一个fgetc函数的使用例子
#include<stdio.h>
int main()
{
int ch = 0;
//打开文件
FILE* pf = fopen("test.txt","r");
if(pf == NULL)
{
perror("fopen");
return 1;
}
//使用文件
while((ch = fgetc(pf)) != EOF)
{
printf("%c ",ch);
}
printf("\n");
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
注意,在介绍fputc函数时,我已经向文件中输入了字符。
运行结果如下:
经过上面的例子,对于fgetc函数的使用就有所了解。
接下来,我来介绍一下,在读取文件时,文件指针的指向是如何变化的。
如图,刚开始打开文件时,文件指针pf指向第一个字符
再读取一次,文件指针pf向后走一步
再读取一次,文件指针再向后走一步
文件指针就按照这样,每读取一次,向后走一步。
fputs函数
fputs函数的返回类型是int类型,第一个参数是string,传参字符串,第二个参数是stream,传参文件指针。
fputs函数输出成功时,返回非负值,即正数或者0,取决于编辑器。
fputs函数输出失败时,返回EOF(-1)。
接下来,我举一个fputs函数的使用例子
#include<stdio.h>
int main()
{
//打开文件
FILE* pf = fopen("test.txt","w");
if(pf == NULL)
{
perror("fopen");
return 1;
}
//使用文件
fputs("hello ",pf);
fputs("world",pf);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
运行结果如下:
fputs函数是直接以一行数据进行输出到文件,而fputc只是以一个一个字符进行输出到文件。
当然,我们也可以让字符在不同行写入。
#include<stdio.h>
int main()
{
//打开文件
FILE* pf = fopen("test.txt","w");
if(pf == NULL)
{
perror("fopen");
return 1;
}
//使用文件
fputs("hello\n",pf); //与前面的代码不同的,这里多加了个'\n'。
fputs("world",pf);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
如图,test.txt文件里的单词就换行写了。
fgets函数
由msdn查询可以得知,fgets函数的返回类型是char*,第一个参数是string,传参字符数组,读到的内容将拷贝进去。第二个参数是n,即传参要拷贝到string字符数组的字符数(包括‘\0’)。第三个参数是stream,传参文件指针。
fgets函数的的返回值,如果读取成功,返回指向字符数组的指针。
如果读取错误或者遇到文章末尾,返回NULL。
接下来,我来举一个fgets函数的使用例子。
在上一个代码里,test.txt文件已经存储着字符串了,第一行存储着hello,第二行存储着world。接下来,我来读取该文件。
#include<stdio.h>
int main()
{
char arr[] = "##########";
//打开文件
FILE* pf= fopen("test.txt","r");
if(pf == NULL)
{
perror("fopen");
return 1;
}
//使用文件
fgets(arr,5,pf);
printf("%s\n",arr);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
运行结果如下:
观察运行结果,是否有一些疑惑呢?我明明在fgets函数要读取的是五个字符,但是这里只有4个字符,这是为什么呢?我来调试一下。
观察arr里的内容可以发现,存储着hell和’\0’,其他的就是#号。
而一开始arr里的内容全部是#号。
所以,fgets函数在读取文件的时候,如果我们要读取n个字符,那么真正读取的有(n-1)个字符,然后补’\0’。
那么fgets函数能不能读取下一行的字符呢?我们使用代码来验证一下。
#include<stdio.h>
int main()
{
char arr[] = "##########";
//打开文件
FILE* pf= fopen("test.txt","r");
if(pf == NULL)
{
perror("fopen");
return 1;
}
//使用文件
fgets(arr,10,pf);
printf("%s",arr);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
在该串代码中,我将读取字符个数改为了10个字符,文件的第一行有hello’\n’,总共有6个字符,而我要读取10个字符,那么按道理应该可以读取到下一行的字符。
运行结果如下:
由运行结果可以得知,程序读取了hello’\n’。这里的’\n’已经发挥了作用,因为如果没有’\n’时,运行结果里面的第二行的提示语句是跟在hello字符后面的。
我来调试一下,观察arr的内容。
这里的10是’\n’的阿斯玛值,观察可以发现,arr被替换的内容有hello’\n’和一个’\0’。
综上,fgets函数只会读取一行的字符,不会读取下一行的字符。
那么,我们应该如何让fgets函数读取多行的字符呢?多次使用fgets函数嘛,如下面代码。
#include<stdio.h>
int main()
{
char arr[] = "##########";
//打开文件
FILE* pf= fopen("test.txt","r");
if(pf == NULL)
{
perror("fopen");
return 1;
}
//使用文件
fgets(arr,10,pf); //读取第一行
printf("%s",arr);
fgets(arr,10,pf); //读取第二行
printf("%s",arr);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
运行结果如下:
在hello后面有’\n’,所以会换行打印,而world后面没有’\n’,所以提示语句跟在world后面。
fprintf函数
fprintf函数和printf函数名字十分相近,它们的是否有相近点呢?
由msdn查询可得,fprintf函数的返回类型和参数类型。
由msdn查询可得,printf函数的返回类型和参数类型。
观察可以发现,printf函数和fprintf函数的返回类型是相同的,并且printf函数的参数与fprintf的第二个参数是相同的。
printf函数的返回值是打印的所有字符的总数。
fprintf返回写入文件的字节数。
fprintf函数多了一个参数,传参文件指针。
接下来,我来举一个fprintf函数的使用例子。
#include<stdio.h>
struct student
{
char name[20];
int age;
char sex[4];
};
int main()
{
struct student a = {"zhangsan",20,"nan"};
//打开文件
FILE* pf = fopen("test.txt","w");
if(pf == NULL)
{
perror("fopen");
return 1;
}
//使用文件
fprintf(pf,"%s %d %s",a.name,a.age,a.sex);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
运行后,test.txt文件存储的内容如下:
fscanf函数
fscanf函数与scanf函数名字也是比较相似,我们也来对比一下它们吧。
由msdn查询可以得知,fscanf函数的参数类型和返回类型。
由msdn查询可以得知,scanf函数的返回类型和参数类型。
观察可以发现,fscanf函数和scanf函数的返回类型是相同的,并且scanf函数的参数和fscanf函数的第二个参数是相同的,fscanf函数还有一个参数stream,需要传参文件指针。
scanf函数返回成功读取到和分配的字段数量,返回值不包括已读取但未赋值的字段,如果出现错误或者遇到文件结束符或者字节结束符,则返回值为EOF。
fscanf函数返回成功转换和分配的字段数量,返回值不包括已读取但未赋值的字段,返回值为0表示没有分配字段,如果发生错误,或者在第一次转换之前到达文件流的末尾,则返回值为EOF。
接下来,我来举一个fscanf函数的使用例子。
注意在前面的代码中,我已经把test.txt文件存为zhangsan 20 nan,接下来,我来把test.txt文件里的信息读取出来。
#include<stdio.h>
struct student
{
char name[20];
int age;
char sex[4];
};
int main()
{
struct student s = {0};
//打开文件
FILE* pf = fopen("test.txt","r");
if(pf == NULL)
{
perror("fopen");
return 1;
}
//使用文件
fscanf(pf,"%s %d %s",s.name,&s.age,s.sex);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
我来调试一下,观看是否写入成功。
fscanf函数读取文件前,结构体s的内容如下:
fscanf函数读取文件后,结构体s的内容如下:
由结构体s的内容的变化可以得知,fscanf函数成功将文件里的内容写进结构体s。
标准输入输出
对如何一个C语言程序,只要运行起来,就默认打开三个流。
stdin--------标准输入文件--------对应终端的键盘
stdout--------标准输出文件--------对应终端的屏幕
stderr--------标准错误输出文件--------对应终端的屏幕
在上面介绍的函数中,如果是输入函数,比如fgetc函数、fgets函数、fscanf函数适用于所有输入流,上面已经有文件流(即文件向内存输入信息)的操作,而标准输入流(键盘往内存输入信息)应该如何操作呢?这就要利用到上面的三个流了。
同样,如果是输出函数,比如fputc函数、fputs函数、fprintf函数适用于所有的输出流,上面依然也有文件流(即内存向文件输出信息)的操作,而标准输出流(内存往屏幕输出信息)应该如何操作呢?依然也是要利用上面三个流。
下面我以代码为例子。
fgetc函数和fputc函数对于标准输入输出流的操作
#include<stdio.h>
int main()
{
int ch = fgetc(stdin);//stdin是标准输入流,对应终端的键盘,该代码意思是键盘往内存输入
fputc(ch,stdout);//stdout是标准输出流,对应终端的屏幕,该代码意思是内存往屏幕输出
return 0;
}
输入一个字母:
按Enter键(即’\n’),让程序运行
观看运行结果可以得知,再次打印了我输入的字母。
在上面的代码中,fgetc函数接受了键盘的输入的值,输入内存里,fputc成功将内存里的值输出到屏幕上。
所以上面的代码写法就是fgetc函数、fputc函数分别操作于标准输入流和标准输出流的方法。
fgets函数和fputs函数对于标准输入输出流的操作
#include<stdio.h>
int main()
{
char arr[31] = {0};
fgets(arr,31,stdin);//stdin是标准输入流,对应终端的键盘,该代码意思是键盘往内存输入
fputs(arr,stdout);//stdout是标准输出流,对应终端的屏幕,该代码意思是内存往屏幕输出
return 0;
}
我输入我的CSDN账号的ID
按Enter键(即’\n’),让程序运行(但是这里读取的是字符串,不同于fgetc函数读取的是字符,所以这里的fgets函数会读取’\n’,那么当打印字符串的时候,程序也会把最后的’\n’打印出来,所以程序结束运行时自带的“按任意键结束运行”的提示语句,不会跟在打印语句的后面)。
同样,因为fgets函数接受了在键盘输入的一串字符,存储在内存里,fputs函数将内存里的一串字符打印出来。
所以上面的代码写法就是fgets函数、fputs函数分别操作于标准输入流和标准输出流的方法。
fscanf函数和fprintf函数对于标准输入输出流的操作
#include<stdio.h>
struct student
{
char name[20];
int age;
char sex[4];
};
int main()
{
struct student s = {0};
fscanf(stdin,"%s %d %s",s.name,&s.age,s.sex);//stdin是标准输入流,对应终端的键盘,该代码意思是键盘往内存输入
fprintf(stdout,"%s %d %s",s.name,s.age,s.sex);//stdout是标准输出流,对应终端的屏幕,该代码意思是内存往屏幕输出
return 0;
}
我输入zhangsan 20 nan
点击Enter键(‘\n’)
同样,fscanf函数接受了来自键盘输入的内容,并给到内存里,fprintf函数将键盘里的内容打印到屏幕上,所以,fscanf函数和fprintf函数对于标准输入流和标准输出流的操作就是上面的方法。
这里会不会有人有疑惑,在结构体变量的最后一个参数,我依然使用%s进行读取,但是’\n’没有被读取进去,而在前面的fgets函数却读取进去,这是为什么呢?原因:fgets函数会读取换行符,而fscanf函数不会读取换行符
fwrite函数
由msdn查询可以得知,fwrite函数的返回类型是size_t,另外还有四个参数。
第一个参数buffer指向要被写的数据元素,即为起始地址
第二个参数size是每个要写元素的大小
第三个参数count是要被写的元素个数
第四个参数stream是文件指针
fwrite函数的返回值是实际写入的完整项的数量,如果发生错误,该数可能小于count。
下面我来举一个fwrite函数的使用例子。
#include<stdio.h>
struct student
{
char name[20];
int age;
char sex[4];
float score;
};
int main()
{
struct student s = {"zhangsan",20,"nan",95.5f};
//打开文件
FILE* pf = fopen("test.txt","wb");
if(pf == NULL)
{
perror("fopen");
return 1;
}
//使用文件
fwrite(&s,sizeof(struct student),1,pf);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
程序运行以后,观察项目底下的test.txt文档内容
对于该test.txt的文档内容是否有疑惑呢?出现一些乱码,这是因为我们是以二进制的形式往文件输入信息的,而txt后缀的文档无法识别二进制内容,所以出现了乱码。
由test.txt文档的内容可以得知,fprintf函数成功将内存里的信息写入文件里。
fread函数
由msdn查询可以得知,fread函数的返回类型是size_t,另外还有四个参数。
第一个参数buffer指向数据存储的起始位置
第二个参数size是要写的元素大小
第三个参数count是要读取的元素个数
第四个参数stream是文件结构指针
fread函数的返回实际读取的完整项数,如果发生错误或达到count之前遇到文件结束,则该数可能小于count。
#include<stdio.h>
struct student
{
char name[20];
int age;
char sex[4];
float score;
};
int main()
{
struct student s = {0};
//打开文件
FILE* pf = fopen("test.txt","rb");
if(pf == NULL)
{
perror("fopen");
return 1;
}
//使用文件
fread(&s,sizeof(struct student),1,pf);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
调试一下观察结构体s的内容
fread函数运行前
fread函数运行后
由调试可以得知,fread函数成功读取了文件里的内容。
fprintf函数的多次写入和fscanf函数的多次读取
下面我举一个例子
#include<stdio.h>
struct student
{
char name[20];
int age;
char sex[4];
float score;
};
int main()
{
struct student s = {"zhangsan",20,"nan",95.5f};
struct student S = {"lisi",20,"nan",95.f};
//打开文件
FILE* pf = fopen("test.txt","wb");
if(pf == NULL)
{
perror("fopen");
return 1;
}
//使用文件
fwrite(&s,sizeof(struct student),1,pf);
fwrite(&S,sizeof(struct student),1,pf);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
在上面的代码中,我使用了多次的fwrite函数,多次往文件写入内容。
程序运行结束后,我打开test.txt文档观察内容。
观察可以发现,结构体s和结构体S的内容堆在了test.txt文档里的一行,那么利用fread函数是否可以读取出来呢?
#include<stdio.h>
struct student
{
char name[20];
int age;
char sex[4];
float score;
};
int main()
{
struct student n = {0};
//打开文件
FILE* pf = fopen("test.txt","rb");
if(pf == NULL)
{
perror("fopen");
return 1;
}
//使用文件
fread(&n,sizeof(struct student),1,pf);
fread(&n,sizeof(struct student),1,pf);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
第一个fread函数生效前
第一个fread函数生效后
第二个fread函数生效后
由调试可以发现,即使结构体s和结构体S的内容堆在了test.txt文档里的一行,利用fread函数也是可以读取出来的。
5.对比一组函数
scanf/fscanf/sscanf
printf/fprintf/sprintf
其中,fprintf函数和printf函数的返回类型和参数之间的区别,fscanf函数和scanf函数的返回类型和参数之间的区别,都在前面有所提及,现在我来全方面介绍一下两者的区别。
scanf:按照一定的格式从键盘输入数据
printf:按照一定的格式把数据打印(输出)到屏幕上
适用于标准输入/输出流的格式化的输入/输出语句
fscanf:按照一定的格式从输入流(文件/stdin)输入数据
fprintf:按照一定的格式向输出流(文件/stdout)输出数据
适用于所有的输入/输出流的格式化输入/输出语句
sscanf:从字符串中按照一定的格式读取出格式化的数据
sprintf:把格式化的数据按照一定的格式转换为字符串
接下来,我来讲解sprintf函数和sscanf函数。
sprintf函数
由msdn查询可以得知,sprintf函数的返回类型是int类型。第一个参数buffer为输出字符的存储位置,第二个参数format [, argument] … 为格式控制字符串。
sprintf函数返回缓冲区中存储的字节数,不计算结束的空字符。
sscanf函数
由msdn查询可以得知,sscanf函数的返回类型是int类型。第一个参数buffer是为字符串的存储位置,第二个参数format [, argument ] … 为格式控制字符串。
返回值是成功转换和分配的字段的数量,返回值不包括已读取的字段,但返回值为0表示没有分配字段。如果出现错误,或者在第一次转换之前到达字符串末尾,则返回值为EOF。
下面我举一个sprintf函数和sscanf函数的使用例子。
#include<stdio.h>
struct student
{
char name[20];
int age;
char sex[4];
};
int main()
{
char tmp[100] = {0};
struct student s = {"zhangsan",20,"nan"};
struct student S = {0};
sprintf(tmp,"%s %d %s",s.name,s.age,s.sex);//将结构体里的数据转换为字符串
printf("%s\n",tmp);
sscanf(tmp,"%s %d %s",S.name,&(S.age),S.sex);//将字符串转换为结构体信息
printf("%s %d %s\n",S.name,S.age,S.sex);
return 0;
}
在最开始的初始化中,s存储着一个学生的信息,数组tmp和结构体S没有存储信息,我来调试一下观察它们存储信息的变化。
sprintf函数将结构体信息写成字符串前,字符数组tmp存储的内容为全0
sprintf函数将结构体信息写成字符串后,字符数组tmp存储的内容发生变化。
sscanf函数将字符串tmp的内容转为结构体信息,存储到结构体S前,结构体S的内容为全0
sscanf函数将字符串信息转为结构体信息,存储到结构体S后,结构体S的内容如下:
sprintf函数和sscanf函数的区别
在前端的注册页面,我们需要输入昵称、年龄、性别等信息,以字符串的形式进行输入,而在后端中,可能要以结构体的形式进行存储,所以这些函数就发挥了大作用。
实际上,有更好的工具:序列化/反序列化的工具,底层还是与这些函数类似。
6.文件的随机读写
在文件的顺序读写中,文件指针在第一次读写中指向文件的第一个元素,接下来的每一次读写,文件指针都会往后走一步。而在文件的随机读写中,文件指针的位置通过在相应的位置加上偏移量,让文件指针随时随地指向想要读取的位置,实现随机读取。
6.1 fseek函数
根据文件指针的位置和偏移量来定位文件指针。
由msdn查询可以得知,fseek函数的返回类型是int类型,第一个参数stream是文件指针,第二个参数offset是偏移量,第三个参数origin是起始位置。
fseek函数如果成功读写,返回值是0。否则,它返回一个非零值。在无法查找的设备上,返回值未定义。
origin有三个选择
可能取值 | 参考的位置 |
---|---|
SEEK_SET | 文件开头 |
SEEK_CUR | 当前位置 |
SEEK_END | 文件结尾 |
下面我来举三个fseek函数的使用例子。
我们先让项目底下的test.txt文档存储着abcdef字符串。
#include<stdio.h>
int main()
{
//打开文件
FILE* pf = fopen("test.txt","w");
if(pf == NULL)
{
perror("fopen");
return 1;
}
//使用文件
fputs("abcdef",pf);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
假设在下面的三个例子中,我都要找到test.txt文档里面的’d’字符。
从文件开头进行偏移
在这种情况下,文件指针一开始指向第一个字符,即字符a,如果偏移一次,文件指针来到了字符b,再偏移一次,文件指针来到了字符c,再偏移一次,文件指针来到了字符d,所以文件指针一共偏移了三次,才能读取到d字符。
#include<stdio.h>
int main()
{
int ch = 0;
//打开文件
FILE* pf = fopen("test.txt","r");
if(pf == NULL)
{
perror("fopen");
return 1;
}
//使用文件
fseek(pf,3,SEEK_SET); //文件指针向由右偏移3个位置
ch = fgetc(pf); //读取
printf("%c\n",ch); //打印
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
运行结果如下:
由运行结果可以得知,程序成功读取了字符d。
从当前位置进行偏移
假如我使用fgetc函数读取一次文件,那么文件指针从一开始的指向a字符,变成了指向了b字符,那么在当前的位置下,我要进行偏移。第一次偏移,文件指针指向了c字符,第二次偏移,文件指针指向了d字符,所以总共要两次偏移才能读取到d字符。
#include<stdio.h>
int main()
{
int ch = 0;
//打开文件
FILE* pf = fopen("test.txt","r");
if(pf == NULL)
{
perror("fopen");
return 1;
}
//使用文件
fgetc(pf); //fgetc函数读取文件,文件指针从指向字符a,向右走了一步,变成了指向字符b
fseek(pf,2,SEEK_CUR); //文件指针向由右偏移2个位置
ch = fgetc(pf); //读取
printf("%c\n",ch); //打印
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
运行结果如下:
由运行结果可以得知,程序成功读取了字符d。
从文件结尾进行偏移
在这种情况下,文件指针一开始指向了test.txt文件里的文件末尾,即在字符f的后面一个位置,那么文件指针向左偏移一步到达字符f,文件指针再向左偏移一步到达字符e,再偏移一步到达字符d,所以文件指针一共要向左偏移三步。
注意,向右偏移时,偏移量为正数,向左偏移时,偏移量为负数。
#include<stdio.h>
int main()
{
int ch = 0;
//打开文件
FILE* pf = fopen("test.txt","r");
if(pf == NULL)
{
perror("fopen");
return 1;
}
//使用文件
fseek(pf,-3,SEEK_END); //文件指针向由左偏移3个位置
ch = fgetc(pf); //读取
printf("%c\n",ch); //打印
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
运行结果如下:
由运行结果可以得知,程序成功读取了字符d。
6.2 ftell函数
该函数返回文件指针相对起始位置的偏移量。
由msdn查询可以得知,ftell函数的返回类型是long类型。参数stream是文件指针。
下面,我来举一个ftell函数的使用例子。
注意,项目底下的test.txt文档依然存储着abcdef。
#include<stdio.h>
int main()
{
int ch = 0;
int offset = 0;
//打开文件
FILE* pf = fopen("test.txt","r");
if(pf == NULL)
{
perror("fopen");
return 1;
}
//使用文件
ch = fgetc(pf); //读取了字符a,文件指针自动向右偏移一次,指向字符b
printf("%c\n",ch);
fseek(pf,2,SEEK_CUR); //文件指针在当前位置向后偏移两次,指向字符d
ch = fgetc(pf); //读取字符d,文件指针自动向右偏移一次
printf("%c\n",ch);
offset = ftell(pf);
printf("偏移量是:%d\n",offset);
return 0;
}
第一次读取时,文件指针指向字符a,读取了字符a后,文件指针自动向右走了一步,指向字符b。
fseek函数让文件指针向后偏移两步,指向字符d。
第二次读取时,文件指针指向字符d,读取了字符d后,文件指针向右走了一步,指向了字符e。
第一次读取是字符a,第二次读取的是字符d。并且文件指针总共向右走了四步,偏移量为4。
运行结果如下:
观察运行结果可以发现,我们对于结果的猜测是正确的。
6.3 rewind函数
让文件指针的位置回退到文件的起始位置。
由msdn查询可以得知,rewind函数无返回类型,参数stream是文件指针。
接下来,我来举一个rewind函数的使用例子。
#include<stdio.h>
int main()
{
int ch = 0;
int offset = 0;
//打开文件
FILE* pf = fopen("test.txt","r");
if(pf == NULL)
{
perror("fopen");
return 1;
}
//使用文件
ch = fgetc(pf); //读取了字符a,文件指针自动向右偏移一次,指向字符b
printf("%c\n",ch);
fseek(pf,2,SEEK_CUR); //文件指针在当前位置向后偏移两次,指向字符d
ch = fgetc(pf); //读取字符d,文件指针自动向右偏移一次
printf("%c\n",ch);
offset = ftell(pf);
printf("偏移量是:%d\n",offset);
rewind(pf);
offset = ftell(pf);
printf("偏移量是:%d\n",offset); //让文件指针的位置回到文件的起始位置
return 0;
}
运行结果如下:
由运行结果可知,在rewind函数的作用下,偏移量从4变成了0。
7.文本文件和二进制文件
根据数据的组织形式,数据文件被称为文本文件或者二进制文件。
数据在内存中以二进制的形式存储,如果不加转换的输出到外存,就是二进制文件。
如果要求在外存上以ascll码的形式存储,则需要在存储前转换。以ascll字符的形式存储的文件就是文本文件。
数据在内存中是怎样存储的呢?
字符一律以ascll码的形式存储,数值型数据既可以用ascll形式存储,也可使用二进制形式存储。
如有整数10000,如果以ascll码的形式输出到磁盘,则磁盘中占用5个字节(每个字符一个字节),而二进制形式输出,则在键盘上只占用4个字节(vs2010测试)。
即10 27 00 00(16进制的形式显示的)。
读取二进制文本的方法
我先来往项目底下的test.txt文档存储二进制的信息。
#include<stdio.h>
int main()
{
int a = 10000;
//打开文件
FILE* pf = fopen("test.txt","w");
if(pf == NULL)
{
perror("fopen");
return 1;
}
//使用文件
fwrite(&a,sizeof(int),1,pf);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
运行后,test.txt文档存储的内容如下:
观察可以得知,test.txt文档存储着乱码。接下来,我来通过vs编辑器分析test.txt文档里的乱码。
通过VS的二进制编辑器,我可以读取到test.txt文档里的乱码就是10000,上面图片显示的是16进制的10000。
8.文件读取结束的判定
8.1 被错误使用的feof函数
牢记:在文件读取的过程中,不能用feof函数的返回值直接用来判断文件是否读取结束,而是应用于当文件读取结束的时候,判断是读取失败结束还是遇见文件末尾结束。
8.2 文本文件判断读取是否结束
文本文件判断读取是否结束,判断返回值是否为EOF(fgetc)、NULL(fgets)。
8.3 二进制的文件判断是否读取结束
fread判断返回值是否小于实际要读的个数。
8.4 函数返回值判断总结
fgetc函数
如果读取正常,会返回读取到的字符的ascll码值。
如果读取失败,返回EOF
fgets函数
如果读取正常,返回的是存放读取到的数据的地址
如果读取失败,返回NULL
fscanf
读取正常,返回的数字与想要读取的字符个数相同
读取失败,返回的数字与想要读取的字符个数不相等
fread函数
判断返回值是否小于实际要读的个数
8.5 读取文件的正确方法
在前面的读取文本文件时,都是按照打开文件,使用文件,关闭文件的流程,现在,我们应当养成良好的习惯,再加上判断文件读取结束是读取失败结束还是遇见文件末尾读取结束。
所以,在以后的读取文件中,我们将按照打开文件、使用文件、关闭文件、判断文件以何种方式结束的流程来读取文件。
ferror函数
ferror函数是用来判断文件读取结束时,是否是遇见I/O错误而导致文件读取结束。
由msdn查询可以得知,返回类型是int类型。参数stream是文件指针。
在文件读取结束后,将文件指针串传参给ferror函数,如果有出现过I/O错误,返回一个非0的值,如果没有出现错误,返回0,也就是,如果ferror函数返回值为真,证明在读取的过程中遇到I/O错误,如果ferror函数返回值为假,证明在读取的过程中没有遇到I/O错误。
feof函数
feof函数是判断文件读取结束时,是否是遇到文件末尾而导致文件读取结束。
由msdn查询可以得知,feof函数的返回类型是int。参数stream是文件指针。
在文件读取结束后,将文件指针传参给feof函数,如果是遇到文件末尾,那么返回非0的值,如果没有遇到文件末尾,那么返回0,也就是说,如果feof函数的返回值是真,那么就是遇到文件末尾而导致的文件结束,如果feof函数为假,那么就不是遇到文件末尾而导致的文件结束。
8.5.1 读取文本文件的正确方法
我先往test.txt文档写入abcdef的内容。
#include<stdio.h>
int main()
{
//打开文件
FILE* pf = fopen("test.txt","w");
if(pf == NULL)
{
perror("fopen");
return 1;
}
//使用文件
fputs("abcdef",pf);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
下面,我来正确地读取文本文件
#include<stdio.h>
#include<stdlib.h>
int main()
{
int c = 0;
//打开文件
FILE* pf = fopen("test.txt","r");
if(pf == NULL)
{
perror("fopen");
return EXIT_FAILURE; //EXIT_FAILURE可作为exit()或return的参数来使用,表示没有成功的执行一个程序
}
//使用文件
while((c = fgetc(pf)) != EOF) //标准C的I/O读取文件循环
{
putchar(c);
}
printf("\n");
//判断文件读取结束是读取错误结束还是到达文件末尾结束
if(ferror(pf))
puts("I/O error when reading");
else if(feof(pf))
puts("End of file reached successfully");
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
运行结果如下:
由运行结果可以得知,读取文件读取结束是遇到文件末尾结束。
8.5.2 读取二进制文件的正确方法
我先往test.txt文档存入一些内容。
#include<stdio.h>
int main()
{
int arr[5] = {1,2,3,4,5};
//打开文件
FILE* pf = fopen("test.txt","wb");
if(pf == NULL)
{
perror("fopen");
return 1;
}
//使用文件
fwrite(arr,sizeof(*arr),5,pf);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
下面,我来正确地读取二进制文件
#include<stdio.h>
int main()
{
int a[5] = {0};
int count = 5;
int num = 0;
int i = 0;
//打开文件
FILE* pf = fopen("test.txt","rb");
if(pf == NULL)
{
perror("fopen");
return 1;
}
//使用文件
num = fread(a,sizeof(*a),5,pf);
if(num == count) //读取五个元素后正好到达文件结尾
{
for(i = 0; i < count; i++)
{
printf("%d ",a[i]);
}
puts("\nArray read successfully contents");
}
else
{
if(feof(pf)) //文件里没有五个元素,在完成读取五个元素前,已经到达文件结尾
{
printf("Error reading test.txt:unexpected end of file\n");
}
else if(ferror(pf)) //读取遇到I/O错误
{
perror("Error reading test.txt");
}
}
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
运行结果如下:
由运行结果可以得知,此次读取是读取到我们想要的元素个数后,遇到文件末尾结束的。
9.文件缓冲区
ANSIC标准采用"缓冲文件系统"处理数据文件,所谓的缓冲文件系统是指系统自动地在内存中为程序的每一个正在使用的文件开辟一块"文件缓冲区"。从内存向磁盘输出的数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上。如果磁盘向计算机输出数据,则从磁盘文件中读取数据输入到文件缓冲区(充满缓冲区),然后再从缓冲区逐个将数据送到程序数据区(程序变量等)。缓冲区的大小根据编译系统决定。
输出缓冲区在未装满时,遇到’\n’,也会把信息放到磁盘。或者使用fflush(stdout),强制将输出缓冲区数据送到磁盘或者写入文件,关闭文件也会把输出缓冲区内容存入磁盘。
下面,我来举一个证明文件缓冲区存在的代码
#include<stdio.h>
#include<Windows.h> //vs2010测试
int main()
{
//打开文件
FILE* pf = fopen("test.txt","w");
if(pf == NULL)
{
perror("fopen");
return 1;
}
//使用文件
fputs("abcdef",pf);
printf("睡眠15秒,已经写数据了,打开test.txt文档观察,发现没有内容\n");
Sleep(15000);
fflush(pf); //该函数在较新版本的vs不能使用
printf("刷新缓冲区\n");
printf("睡眠15秒,打开test.txt文档发现有内容了\n");
Sleep(15000);
//关闭文件
fclose(pf); //fclose函数在关闭文件时,也会刷新缓冲区
pf = NULL;
return 0;
}
在代码的运行时,打开test.txt文档观察内容变化,证明存在着输出缓冲区,如果没有刷新缓冲区,在缓冲区内容没有满的情况下,内容不会从缓冲区送到磁盘。
结论:因为有缓冲区的存在,C语言在操作文件的时候,需要做刷新缓冲区或者在文件操作结束的时候关闭文件。如果不做,可能导致读写文件的问题。
今天对于文件操作的内容就讲解到这里,关注点一点,下期更精彩。