1、为什么使用文件?
如果没有文件,我们写的程序的数据是存储在电脑的内存中,如果程序退出,内存回收,数据就丢失了,等再次运行程序,是看不到上次程序的数据的,如果要将数据进行持久化的保存,我们可以使用文件。
2、什么是文件?
磁盘(硬盘)上的文件就是文件。
但是程序设计中,我们一般谈两个文件,分别是程序文件、数据文件(从文件的角度来分类的)。
2.1 程序文件
程序文件包括源程序文件(后缀为.c)、目标文件(windows环境后缀为.obj),可执行文件(windows环境后缀为.exe)。
2.2 数据文件
文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件。
本章讨论的是数据文件。
在以前各篇笔记所处理数据的输入输出都是以终端为对象的,即从终端的键盘输入数据,运行结果显示到显示器上,其实有时候我们会把信息输出到磁盘上,当需要的时候再从磁盘上把数据读取到内存中使用,这里处理的就是磁盘上的文件。
2.3 文件名
一个文件要有唯一的文件表示,以便用户识别和引用。
文件名包含3部分:文件路径+文件主干+文件后缀
例如:c:\code\test.txt
为了方便起见,文件标识常被称为文件名。
3、二进制文件和文本文件
根据数据的组织形式,数据文件被称为文本文件或者二进制文件。
数据在内存中以二进制的形式存储,如果不加转换的输出到外存的文件中,就是二进制文件。
如果要求在外出上以ASCII的形式存储,则需要再存储前转换,以ASCII字符的形式存储的文件就是文本文件。
一个数据在文件中是怎么存储的呢?
字符一律以ASCII形式存储,数值型数据既可以用ASCII形式存储,也可以用二进制形式存储。
比如有整数10000,如果以ASCII码的形式输出到磁盘,则磁盘中占用5个字节(每个字符一个字节),而二进制形式输出,则在磁盘上只占4个字节。
代码栗子:
#include <stdio.h>
int main()
{
int a = 10000;
FILE* pf = fopen("test.txt", "wb");//打开文件
fwrite(&a, 4, 1, pf);//二进制的形式写到文件中
fclose(pf);//关闭文件
pf = NULL;
return 0;
}
4、文件的打开和关闭
4.1 流和标准流
4.1.1 流
程序的数据是要输出到各种外部设备,也需要从外部设备获取数据,不同的外部设备的输入输出操作各不相同,为了方便程序员对各种设备进行方便的操作,我们抽象出了流的概念,我们可以把流想象成流淌着字符的河。C程序真的文件、画面、键盘灯的数据输入输出操作都是通过流操作的。
一般情况下,我们要想向流里写数据,或者从流里读数据,都是要打开流,然后操作。
4.1.2 标准流
文件操作时我们需要自己打开文件(流),当操作完后需要自己关闭文件(流),那为什么我们从键盘输入数据,向屏幕上输出数据,并没有打开流呢?
那是因为C语言程序在启动的时候,默认打开了3个流:
- stdin - 标准输入流,大多数的环境中从键盘输入,scanf函数就是从标准输入流中读取数据。
- stdout - 标准输出流,大多数环境中输出值显示器界面,printf函数就是将信息输出到标准输出流中。
- stderr - 标准错误流,大多数环境中输出到显示器界面。
这是默认打开了这三个流,我们使用scanf、printf等函数就可以直接进行输入输出操作的。
stdin、stdout、stderr 三个流的类型是:FILE*,通常称为文件指针。
C语言中,就是通过FILE*的文件指针来维护流的各种操作的。
4.2 文件指针
缓冲文件系统中,关键的概念是 "文件类型指针" ,简称为 "文件指针"。
每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(如文件的名字,文件状态及文件当前的位置等)。这些信息是保存在一个结构体变量中的,该结构体类型是由系统声明的,取名FILE。
例如:VS2013编译环境提供的stdio.h头文件中有以下的文件类型声明:
struct _iobuf{
char *_ptr;
int _cnt;
char* _base;
int _flag;
int _file;
int _charbuf;
int _bufsiz;
char* tmpfname;
};
typedef struct _ioduf FILE;
不同的c编译器的FILE类型包含的内容不完全相同,但是大同小异。
每当打开一个文件的时候,系统会根据文件的情况自动创建一个FILE类型的变量,并填充其中信息,该结构体类型的变量里存放着我们需要打开的文件的信息,因此被称为文件信息区。使用时不必关心细节。开辟好文件信息区后便会返回该信息区的地址,我们需要FILE*类型的指针来接收这个地址,这个FILE*类型指针就是流,属于文件的流。
一般都是通过FILE指针来维护这个FILE结构变量,这样使用更加方便。
FILE* PF;//文件指针变量
定义pf是一个指向FILE类型的指针变量,可以使pf指向某个文件的文件信息区(是一个结构体变量),通过该文件信息区中的信息就能够访问该文件,也就是说,通过文件指针变量能够间接找到与它关联的文件。
比如:
4.3 文件的打开和关闭
文件在读写之前应该先打开文件,在使用结束之后应该关闭文件。
在编写程序的时候,在打开文件的同时,都会返回一个FILE*的指针变量指向该文件,也相当于建立了指针和文件的关系。
ANSI C规定使用fopen来打开文件,fclose来关闭文件。
//打开文件
FILE* fopen(const char* filename, const char* mode);
//关闭文件
int fclose(FILE* ftream);
fopen的函数声明:参数1:filename是所需的文件名,参数2:mode是打开流的形式,是输入还是输出。返回类型:FILE*是一个文件信息区的地址,通过该地址找到文件信息区访问文件。
fclose的函数声明:参数:ftream是我们打开文件时用来接收fopen返回值是创建的变量,将这个变量所存储的地址传参过去就可以回收文件信息区所占用的空间,就是关闭文件
fopen函数的参数2mode的打开形式是什么意思呢?怎么表示打开形式呢?
mode表示文件的打开模式,下面都是文件的打开模式:
文件使用方式 | 含义 | 如果指定文件不存在 |
"r"(只读) | 为了输入数据,打开一个已经存在的文本文件 | 出错 |
"w"(只写) | 为了输出数据,打开一个文本文件 | 建立一个新的文件 |
"a"(追加) | 向文本文件尾部添加数据 | 建立一个新的文件 |
"rb"(只读) | 为了输入数据,打开一个二进制文件 | 出错 |
"wb"(只写) | 为了输入文件,打开一个二进制文件 | 建立一个新文件 |
"ab"(追加) | 向一个二进制文件尾部添加数据 | 建立一个新的文件 |
"r+"(读写) | 为了读和写,打开一个文本文件 | 出错 |
"w+"(读写) | 为了读和写,建立一个新的文本文件 | 建立一个新的文件 |
"a+"(追加) | 打开一个文本文件,在文件尾部进行读写 | 建立一个新的文件 |
"rb+"(读写) | 为了读和写,打开一个二进制文件 | 出错 |
"wb+"(读写) "ab+"(追加) | 为了读和写,建立一个新的二进制文件 打开一个二进制文件,在文件尾部进行读和写 | 建立一个新的文件 建立一个新的文件 |
注:fopen也是会打开失败的,如果打开失败,则返回空指针NULL。打开成功,则返回开辟好后的文件信息区的地址,所以使用前一定要判断一下。
然后就是fclose函数,它是用来关闭文件的,当我们指向文件信息区的FILE*的指针变量pf传进去,关闭好文件后一定要记得将pf置为NULL,因为我们虽然使用fclose函数释放了文件信息区,将文件信息区所占的内存还给操作系统了。但是指针变量pf始终是指向这块内存的,如果解引用访问使用这块内存就是非法访问了,所以当我们关闭文件后就把pf置为NULL。
注:如果以只读" w " 或" wb " 的形式打开文件,如果这个文件本身有数据,则会被清空,因为需要从头写入文件,所以要谨慎的使用只读的形式。
文件的打开方式:
文件打开有两种路径,一种是相对路径,一种是绝对路径
相对路径:
' . '表示当前路径,".."表示上一级路径
如果我们要打开的文件和程序所在的文件在一个路径下的话可以直接使用文件名打开,例如:
FILE* pf = fopen("test.txt","r");
因为没有路径表示编译器便会自动在程序文件相同路径的位置找该文件。
如果该程序文件在许多级文件内存储,如果我们要打开的文件也在这个多级文件中,但是在程序文件所在文件的上一级的上一级的位置,我们可以这样访问,例如:
FILE* pf = fopen(".\\..\\..\\test.txt","r");
一个‘ . ‘表示当前路径,两个 ".." 表示上一级路径。
还是将test.txt存放在当前数据文件所在的文件的上一级的上一级的位置,只不过我在这个位置又新建了一个文件夹叫hehe,然后我将test.txt放入这个hehe文件夹中,我们有什么方法可以访问呢:
FILE* pf = fopen(".\\..\\..\\hehe\\test.txt","r");
".\\..\\..\\hehe\\test.txt"意思就是在当前路径 ' . ' 的上一级 " .. " 的上一级" .. " 路径下的文件夹"hehe"里的文件"test.txt"。
绝对路径:
必须填写文件对应的路径,通过这个路径来找到对应的文件
但当我们想要打开其他路径的文件比如桌面上的文件时,我们就需要额外的输入路径,让编译器通过该路径找到对应的文件,例如:
FILE* pf = fopen("C:\\Users\\zpeng\\Desktop\\test.txt","w");
//绝对路径
在文件名前面添加一条路径,就可以根据这个路径找到对应文件。
场景1:当需要打开的文件和当前程序文件都是一个路径时,比如程序文件的项目是需要创建在一个文件夹中的,如果存在同一个文件夹,则不用填写路径。
场景2:当需要打开的文件和程序文件不在同一个文件夹,则需要在文件名前面添加上路径。
总结:文件路径也分为两个,分别是绝对路径和相对路径。
绝对路径:是在文件和程序文件位置不同时需要填写完整的路径来访问。
相对路径:是和程序文件在同一个文件里的,可能不一级文件,但是位置是有关联的,被称为相对路径
4.4 文件指针的概念
这里要说一下文件是有文件指针的,文件指针决定读取或写入的操作时从哪个位置开始的,如果程序开始运行并且使用过一次函数来访问当前文件信息区的文件了,文件指针就会发生改变,因为文件指针需要访问下一个位置的数据。
假设文件信息区的地址由变量pf来接收,那它的文件指针始终都不会重新开始,方便下一次调用文件访问函数可以从当前位置继续向后访问,所以没访问一次,文件指针会自动向后指向。除非是程序结束、使用rewind函数 或者是 又创建了一个文件信息区,否则当前pf关联的文件的文件指针始终都不会重新指向起始位置。
5、文件的顺序读写
5.1 顺序读写函数介绍
函数名 | 功能 | 适用于 |
fgetc | 字符输入函数 | 所有输入流 |
fputc | 字符输出函数 | 所有输出流 |
fgets | 文本行输入函数 | 所有输入流 |
fputs | 文本行输出函数 | 所有输出流 |
fscanf | 格式化输入函数 | 所有输入流 |
fprintf | 格式化输出函数 | 所有输出流 |
fread | 二进制输入 | 文件 |
fwrite | 二进制输出 | 文件 |
以上第三列表格适用于:所有输入流、所有输出流、文件,意思是每个对应函数的参数里有一个FILE*类型的指针变量参数,也就是流,所以都要有对应的流。所有输入流包括:标准输入流、文件流,所有输出流包括:标准输出流、文件流,二进制文件读写函数只能传文件流。我们也可以使用以上适用于标准输出流的函数数据通过标准输出流输出到屏幕上去,也可以使用以上适用于标准输入流的函数将我们从键盘输入的数据通过标准输入流读取出来,所以要记住,这些函数不仅仅是作用于文件的读取和写入。
以上所有函数的声明:
int fputc(int character, FILE* stream);
int fgetc(FILE* stream);
int fputs(const char* str, FILE* stream);
char* fgets(char* str, int num, FILE* stream);
5.1.1 fputc的使用
fputc的声明:
int fputc(int character, FILE* stream);
fputc函数:参数1:character是需要输出的字符。参数2:stream是FILE*类型的指针,可以是标准输出流或者是对应文件的流。
fputc函数的功能:通过参数2的指向的文件信息区里的信息访问文件,并将参数1的字符输出到当前文件,一次只能写一个字符。
fputc函数的使用:
#include <stdio.h>
#include <string.h>
int main()
{
FILE* pf = fopen("test.txt", "w");//打开文件
if (pf == NULL)
{
perror("fopen");
return;
}
char str[] = "hello world";
ine len = strlen(str);
int i = 0;
for(i = 0; i < len; i++)
{
fputc(str[i], pf);//将"hello world"一个一个输出到文件
}
fclose(pf);//关闭文件
pf = NULL;
return 0;
}
那我们也可以通过该函数将字符输出到屏幕上,就像printf一样:
#include <stdio.h>
int main()
{
fputc('a',stdout);经过标准输出流直接将字符'a'输出到屏幕上
return 0;
}
所以这里也就证明了FILE*类型的指针变量接收的文件信息区的地址是文件的流,顺序读写函数的参数FILE* stream是流,至于什么的流就看自己想怎么操作。
5.1.2 fgetc的使用
fgetc的声明:
int fgetc(FILE* stream);
fgect函数:参数:stream不用说就是流,但仅限于所有输入流,或文件的流,因为fgetc需要从输入流中获取数据。
fgetc函数的功能:将对应的输入流传参过去,getc会读取输入流中的字符,标准输入流是需要我们来输入字符,文件流是fgetc自己读取文件中的字符。
fgetc函数的使用:
#include <stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "r");//打开文件
if (pf == NULL)
{
perror("fopen");
return;
}
char c = 0;
while(c = fgetc(pf) != EOF)//会不断地向文件后读取数据
{
printf("%c",c);
}
fclose(pf);//关闭文件
pf = NULL;
return 0;
}
那我们也可以通过该函数读取我们键盘输入的字符,就像scanf一样:
#include <stdio.h>
int main()
{
char c = fgetc(stdin);
printf("%c\n", c);
return 0;
}
int c = fgetc(stdin);
等价于
int c = getchar();
到这里相信大家也都知道了这些函数可以通过标准输入流来获取我们键盘输入的数据或标准输出流将数据输出到屏幕上,那么下面的函数就不用在举这个例子了。
5.1.3 fputs的使用
fputs的声明:
int fputs(const char* str, FILE* stream);
fputs函数:参数1:str是需要输出的字符串,参数2:stream是FILE*类型的指针,可以是标准输出流或者是对应文件的流。
fputs函数的功能:将字符串根据输出流输出到对应的位置
fputs函数的使用:
#include <stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
perror("fopen");
return;
}
char str[] = "hello world";
fputs(str, pf);
fclose(pf);
pf = NULL;
return 0;
}
5.1.4 fgets的使用
fgets的声明:
char* fgets(char* str, int num, FILE* stream);
fgets函数:参数1:str是存储fgets从输入流读取的数据空间的地址,参数2:num是需要拷贝从输入流读取的字符的个数,参数3:stream是FILE*类型的指针,可以是标准输入流或者是对应文件的流。
fgets函数的功能:从参数3的输入流中读取num个字符拷贝到str。
如果fgets读取失败会返回一个空指针NULL,所以我们使用该函数时也可以判断一下有没有读取成功。
fgets的使用:
#include <stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return;
}
char* str = (char*)malloc(10 * sizeof(char));
fgets(str, 10, pf);
printf("%s\n", str);
fclose(pf);
pf = NULL;
return 0;
}
fgets不管读取多少个字符,最后一定会额外拷贝一个结束字符' \0 ' 放入str中。
5.1.5 fprintf的使用
fprintf是格式化函数,printf也是格式化函数
fprintf函数的声明:
int fprintf(FILE* stream,const char* format,...);
fprintf和printf有什么区别,我们再看一下printf函数声明:
int printf(const char* format,...);
我们可以发现printf和fprintf之间就差一个参数stream,stream就是流,我们可以将stream的参数修改为文件流,后面的参数就和printf一样,printf本身的输出流是标准输出流stdout,输出到屏幕上的,所以我们就将文件想象成正常使用printf将数据输出到屏幕,其他参数就和printf一样。
如果这样的话,那fprintf可以做到和printf等价:
int main()
{
char c = 'a';
int a = 10;
char str[] = "hello world";
printf("%c %d %s",c,a,str);
等价于
fprintf(stdout,"%c %d %s",c,a,str);
return 0;
}
fprintf的使用:
#include <stdio.h>
struct S
{
int n;
float f;
char arr[20];
};
int main()
{
struct S s = { 100, 3.14f, "zhangsan" };
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
perror("fopen");
return;
}
fprintf(pf, "%d %f %s", s.n, s.f, s.arr);
fclose(pf);
pf = NULL;
return 0;
}
5.1.6 fscanf的使用
fscanf和scanf的参数也是相似的,就像fprintf和printf一样:
int fscanf(FILE* stream, const char* format,...);
int scanf(const char* format,...);
fscanf的使用:
#include <stdio.h>
struct S
{
int n;
float f;
char arr[20];
};
int main()
{
struct S c = { 0 };
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return;
}
fscanf(pf, "%d %f %s", &(c.n), &(c.f), c.arr);//输出到变量c中
printf("%d %f %s", c.n, c.f, c.arr);
fclose(pf);
pf = NULL;
return 0;
}
5.1.7 fwrite的使用
fwrite函数声明:
size_t fwrite(const void* ptr, size_t size, size_t count, FILE* stream);
fwrite函数:参数1:ptr是一个const void* 的指针,是可以处理任意类型的数据的地址,不管是整型、浮点型还是结构体类型的地址都可以接收。参数2:size是类型大小,单位是字节。参数3:count是类型变量的个数。参数4:stream必须是文件的流,不能是其他流。
fwrite函数功能:通过参数1的指针将指针指向的count个数量的size类型大小的二进制数据输出到stream流。简单来说就是将数据在内存中的二进制数据传输进流。它的流只能是文件,不能是其他流,例如标准输出流。
#include <stdio.h>
struct S
{
int n;
float f;
char arr[20];
};
int main()
{
struct S s = { 200, 3.14f, "zhangsan" };
FILE* pf = fopen("C:\\Users\\linlu\\Desktop\\test.txt", "wb");//以二进制写的形式打开文件
if (pf == NULL)
{
perror("fopen");
return;
}
//
//使用
fwrite(&s, sizeof(struct S), 1, pf);//以二进制的形式写入文件
//
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
5.1.8 fread的使用
fread函数声明:
size_t fread(void* ptr, size_t size, size_t count, FILE* stream);
可以看到fread的函数声明和fwrite的函数声明是极其相似的。
fread函数和fwrite函数的区别:不同的就是前面那个void*的指针,fwrite是const修饰的,因为只是想读取它指向的空间里的数据并不想更改,所以使用了const。而fread是需要一个指针,通过这个指针指向的空间来接收读取的值,所以不能是const修饰。
fread函数:参数1:ptr是一个void* 的指针,是可以处理任意类型的数据的地址,不管是整型、浮点型还是结构体类型的地址都可以接收。参数2:size是类型大小,单位是字节。参数3:count是类型变量的个数。参数4:stream必须是文件的流,不能是其他流。
fread函数功能:通过seteam文件流将文件中的count个数量的size类型大小的二进制数据输入到ptr中。简单来说就是将文件中的二进制数据输入到ptr空间。它的流只能是文件,不能是其他流,例如标准输出流。
#include <stdio.h>
struct S
{
int n;
float f;
char arr[20];
};
int main()
{
struct S s = { 200, 3.14f, "zhangsan" };
FILE* pf = fopen("C:\\Users\\linlu\\Desktop\\test.txt", "rb");//打开文件
if (pf == NULL)
{
perror("fopen");
return;
}
//
//使用
struct S c = { 0 };
fread(&c, sizeof(struct S), 1, pf);//将文件中二进制的数据读取出来
printf("%d %f %s", c.n, c.f, c.arr);
//
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
5.2 对比一组函数:
scanf / fscanf / sscanf
printf / fprintf / sprintf
- scanf - 针对标准输入流(stdin)的格式化输入函数
- printf - 针对标准输出流(stdout)的格式化输出函数
- fscanf - 针对所有输入流的格式化输入函数
- fprintf - 针对所有输出流的格式化输出函数
那sscanf和sprintf两个函数是干什么的呢?
sprintf的函数声明:
int sprintf(char* str, const char* format,...)
可以从参数上发现sprintf就比printf多了一个char*类型的参数,那具体功能是什么?
sprintf函数功能:将格式化数据输出到字符串中
sprintf和printf的区别:printf是将格式化数据输出到标准输出流也就是屏幕上,sprintf则是将格式化数据输出到一个字符串里
#include <stdio.h>
struct S
{
int n;
float f;
char arr[20];
};
int main()
{
struct S s = { 200, 3.14f, "zhangsan" };
char arr[30] = { 0 };
sprintf(arr, "%d %f %s", s.n, s.f, s.arr);//将格式化数据输出到字符串arr
printf("%s\n", arr);//打印arr接收到的格式化数据
return 0;
}
既然可以使用sprintf函数将格式化数据输出到字符串中,那我们是否可以使用sscanf函数将字符串中的格式化数据提取出来呢?答案是可以的。
sscanf函数声明:
int sscanf(char* str, const char* format,...);
sscanf函数功能:将字符串中的格式化数据读取出来
sscanf和scanf的区别:scanf是将格式化数据输入到标准输入流也就是屏幕上,sscanf则是将格式化数据从字符串里读取出来。
#inlcude <stdio.h>
struct S
{
int n;
float f;
char arr[20];
};
int main()
{
//将格式化的数据输出到字符串数组arr中
struct S s = { 200, 3.14f, "zhangsan" };
char arr[30] = { 0 };
sprintf(arr, "%d %.2f %s", s.n, s.f, s.arr);//将格式化数据输出到字符串arr
printf("%s\n", arr);
//从arr这个字符串中读取出格式化的数据
struct S c = { 0 };
sscanf(arr, "%d %f %s", &c.n, &c.f, c.arr);
printf("%d %f %s", c.n, c.f, c.arr);
return 0;
}
6、文件的随机读写
什么是文件的随机读写?文件的随机读写就是定位到我们想要的位置开始向后读写,从开头向后读写就是顺序读写。定位位置向后读写就是随机读写。
6.1 fseek
int fseek(FILE* stream, long int offset, int origin);
fseek函数:参数1就是stream文件的流。参数2offset就是偏移量,是某个位置开始的向后的偏移量处的位置开始向后读写。而参数三origin就是决定这某个位置。
参数3:origin有三种位置:
Contstant | Reference position |
SEEK_SET | Beginning of file (文件的起始位置) |
SEEK_CUR | Current position of the file pointer(文件指针的当前位置) |
SEEK_END | End of file(从文件的末尾位置向前偏移) |
是从这些位置开始向后计算偏移量的位置,从计算好偏移量的位置开始向后读取。
例子:
#include <stdio.h>
int main()
{
FILE* pf = fopen("C:\\Users\\linlu\\Desktop\\test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return;
}
//
//使用
fseek(pf, 6, SEEK_SET);//文件指针位置:起始位置向后偏移6个偏移量位置
int ch = fgetc(pf);//读取当前文件指针位置的字符
printf("%c", ch);
fseek(pf, -3, SEEK_END);//文件指针位置:文件末尾向前偏移3个偏移量位置
int ch = fgetc(pf);//读取当前文件指针位置的字符
printf("%c", ch);
fseek(pf, 5, SEEK_CUR);//文件指针位置:当前文件指针位置向后偏移5个偏移量位置
int ch = fgetc(pf);//读取当前文件指针位置的字符
printf("%c", ch);
//
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
文件里是存在文件指针的,正常情况下调用一次后该文件指针会向后指向,下一次调用是从后面继续向后访问。顺序读写函数是这样的。而随机读写函数是可以随机改变文件指针的指向,让文件指针改变位置从而进行读取或写入。
注:
1. 文件指针并不是我们熟知的C语言指针,而是一个表示文件位置的指针。
2. 偏移量为负数是向前偏移,偏移量为整数是向后偏移。
3. 不管文件指针的位置如何改变,文件都是自动的从前向后访问
6.2 ftell
ftell的函数声明:
long int ftell(FILE* stream);
如果我们不知道当前的文件初始位置与文件指针之间的偏移量是多少时我们就可以使用ftell库函数,这个函数会计算好文件指针的偏移量并返回。
例子:
#include <stdio.h>
int main()
{
FILE* pf = fopen("C:\\Users\\linlu\\Desktop\\test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return;
}
//
fseek(pf, -3, SEEK_END);//文件指针位置:文件末尾向前偏移3个偏移量位置
int ch = fgetc(pf);//读取当前文件指针位置的字符
printf("%c\n", ch);
int ret = stell(pf);//计算当前偏移量
printf("%d\n",ret);
//
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
6.3 rewind
让文件指针的位置回到文件的起始位置
比如我随意用fseek来设置文件指针的位置导致乱了套,这时我们就可以使用rewind来让文件指针回到起始位置,功能比较简单,容易理解。
void rewind(FILE* stream);
例子:
#include <stdio.h>
int main()
{
FILE* pf = fopen("C:\\Users\\linlu\\Desktop\\test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return;
}
//
fseek(pf, -3, SEEK_END);//文件指针位置:文件末尾向前偏移3个偏移量位置
int ch = fgetc(pf);//读取当前文件指针位置的字符
printf("%c\n", ch);
//不知道当前文件指针的位置就重置
rewind(pf);//重置文件指针位置
int ch = fgetc(pf);//读取起始位置字符
printf("%c\n",ch);
//
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
7、文件读取结束的判定
7.1 被错误使用的feof
牢记:在文件读取过程中,不能用 feof 函数的返回值直接来判断文件是否结束。
feof 的作用是:当文件读取结束的时候,判断是读取结束的原因是否是:遇到文件尾结束。
文件读取结束有两种原因:
1. 文件遇到末尾了
2. 文件读取错误了
1. 文本文件读取是否结束,判断返回值是否为EOF(fgetc的错误),或者是NULL(gets的错误)
例如:
- fgetc判断是否为EOF
- fgets判断是否问NULL
2. 二进制文件的读取结束判断,判断返回值是否小于实际要读的个数。
例如:
- fread判断返回值是否小于实际要读的个数
注:fread的返回值是读取到的元素的个数。
7.2 ferror
feof是判断文件是否是因为读取到文件末尾而结束的,而ferror则是判断是否是因为读取失败而结束的,如果读取失败结束就返回1.
int ferror(FILE* stream);
文本文件读取结束判断:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int ch = 0;
FILE* pf = fopen("C:\\Users\\linlu\\Desktop\\test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return;
}
//
while (ch = fgetc(pf) != EOF)
{
printf("%c ", ch);
}
printf("\n");
//判断是什么原因结束的
if (ferror(pf))//判断是否是读取失败导致结束的
{
puts("1/0 error when reading");
}
else if (feof(pf))//判断是否是读取到文件末尾结束的
{
printf("End of file reached successfully");
}
//
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
二进制文件的例子:
#include <stdio.h>
int main()
{
double a[5] = { 1.0, 2.0, 3.0, 4.0, 5.0 };
FILE* pf = fopen("test.bin", "wb");//以输出二进制的形式打开
fwrite(a, sizeof *a, 5, pf);
fclose(pf);
//
double b[5];
pf = fopen("test.bin", "rb");//以读取二进制的形式打开
size_t ret_code = fread(b, sizeof *b, 5, pf);
if (ret_code == 5){
puts("Array read successfully,contents: ");
for (int n = 0; n < 5; n++)
{
printf("%f ", b[n]);
}
putchar('\n');
}
else
{
//判断是什么原因结束的
if (ferror(pf))//判断是否是读取失败导致结束的
{
puts("1/0 error when reading");
}
else if (feof(pf))//判断是否是读取到文件末尾结束的
{
printf("End of file reached successfully");
}
}
//
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
8、文件缓冲区
ANSIC 标准规定采用 "缓冲文件系统" 处理数据文件的,所谓缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块 "文件缓冲区" ,从内存中向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上,如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓冲区的大小根据C编译器系统决定的。
#include <stdio.h>
#include <windows.h>
int main()
{
FILE* pf = fopen("test.txt", "w");
fputs("abcdef", pf);
printf("睡眠10秒-已经写数据了,打开test.txt文件,发现文件没有内容\n");
Sleep(10000);
printf("刷新缓冲区\n");
fflush(pf);//刷新缓冲区的函数,才将输出缓冲区的数据写到文件(磁盘)
//注: fflush 函数在高版本的VS不能使用了
printf("再睡眠10秒-此时再打开test.txt文件,发现文件有内容了\n");
Sleep(10000);
fclose(pf);
//注:fclose关闭文件时,也会刷新缓冲区
pf = NULL;
return 0;
}
这里可以得出一个结论:
因为有缓冲区的存在,C语言在操作文件的时候,需要做刷新缓冲区或者在文件操作结束时关闭文件,如果不做,可能导致读写文件问题。
C语言第15篇:文件操作到这里也就结束了,我们下一篇笔记再见,拜拜-