文件操作的相关知识
一、为什么使用文件
如果没有文件,我们写的程序数据是存储在电脑的内存中,如果程序退出,内存回收,数据就丢失了,再次运行程序,我们无法看到上次程序的数据。想要实现数据持久化保存,我们可以使用文件。
二、什么是文件
硬盘上的是文件。
程序设计中,我们将文件一般分为两种,一种是数据文件一种是程序文件 ,或者输出内容的文件
数据文件:文件的内容不一定是程序,而是程序运行是读写的数据,比如说程序运行需要从中读取数据的文件,或者是输出内容的文件。
程序文件:程序文件包括源程序文件(后缀为.c),目标文件(Windows环境后缀为.obj),可执行程序(Windows环境后缀为.exe)。
文件名:一个文件要有一个唯一的文件标识,以便用户识别和引用。
文件名包括文件路径+文件名主干+文件后缀。
例如:c:\code\text.txt
三、二进制文件和文本文件
数据文件被分为文本文件和二进制文件。
文本文件:文本文件是以 ** 字符编码(如 ASCII、UTF-8 等)** 存储数据的文件,内容由可显示的字符(如字母、数字、符号)组成,可直接用文本编辑器(如记事本、Notepad++)打开阅读。
二进制文件:二进制文件以 ** 二进制数据(0 和 1 的序列)** 存储信息,内容无法直接用文本编辑器正确解读,需通过特定软件解析。
四、文件的打开和关闭
1.流
流是 C 语言中处理输入输出的核心机制,它将不同设备抽象为统一的接口,使程序可以用相同的方式处理文件、终端、网络等数据源。通过流,程序员无需关心底层设备的差异,只需专注于数据的读写操作,大大提高了代码的可移植性和灵活性。程序只需通过流进行读写操作,无需关心底层设备的差异。例如:
标准输入流(stdin):通常对应键盘输入。
标准输出流(stdout):通常对应屏幕输出。
文件流:对应磁盘上的文件。
2.标准流
c语言程序在启动的时候默认打开了3个流
stdin:标准输入流,在大多数的环境中从键盘输入,scanf函数就是从标准流中读取数据
stdout:标准输出流,大多数环境中输出至显示器界面,printf函数就是将信息输出到标准流中。
stderr:标准错误流,大多数环境中输出到显示器界面。
这样我们就可以直接通过scanf,printf等函数直接输入,输出。(stdin,stdout,stderr三个流的类型是FILE *)
3.文件指针
每一个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(如文件的名字,文件的状态,及文件当前的位置)这些信息是存放在一个结构体变量中,该结构体类型是由系统申明,取名为FILE。
比如在VS2013编译环境提供的stdio.h的头文件中有以下文件的类型声明
#define _CRT_SECURE_NO_WARNINGS
struct _iobuf {char* _ptr;int _cnt;char* _base;int _file;int _flag;char* _tmpfname;
};
typedef struct _iobuf FILE;
不同的编译环境FILE类型包含的内筒大同小异。
当我们打开一个文件的时候,使用者不用关心细节。
一般都是通过一个FILE的指针来维护这个FILE结构体变量。
FILE* pf;
定义pf是一个指向FILE类型数据的指针变量,可以通过pf只想某个文件的文件信息区通过该文件信息区中的信息就能访问该文件(也就是通过文件指针变量能够间接的找到与它相关联的文件)
4.文件的打开和关闭
文件在读写之前应该先打开文件,在使用结束后应该关闭文件。
在编写程序的时候,再打开文件的同时,都会返回一个FILE*的指针变量指向该文件,就相当于建立了联系。
ANSI C规定使用fopen函数来打开文件,fclose函数来关闭文件。
fopen:头文件stdio.h
FILE * fopen ( const char * filename, const char * mode );
打开在参数filename中指定其名称的文件,将其与在将来的操作中可以通过返回的FILE指针标识的流相关联。
允许对流执行的操作,以及这些操作的执行方式由mode参数定义。
如果文件没有打开则会返回NULL指针
fclose:头文件stdio.h
int fclose ( FILE * stream );
关闭与流关联的文件并取消关联 。
如果流成功关闭,则返回零值。(这也就是野指针,所以我们需要在最后进行NULL赋值 )
失败时,返回 EOF。
实操:
#include<stdio.h>int main() {FILE* pf = fopen("data.txt", "r");//这个文件能运行的前提是你代码所在的文件夹内含有相应的文件//否则fopen返回的指针为空指针就会符合if的判断条件//我们将r换成w后,则会新建一个文件名为data.txt的文件/*FILE* pf = fopen("C:\\Users\\邪恶全麦面包\\Desktop\\新建 DOC 文档.doc","r");将这里的\\都改成/也是可以的*/
//只要知道对应文件的地址,就可以打开相应的文件
//避免了上面代码中只能打开代码所在的文件夹内含有相应的文件if (pf == NULL) {perror("fopen:");return 1;}fclose(pf);pf = NULL;return 0;
}
5.文件路径
绝对路径:C:\\Users\\邪恶全麦面包\\Desktop\\新建 DOC 文档.doc 这一个就是绝对路径
相对路径:
. 当前路径
.. 上一级路径
以这个为例,如果我们想在文件操作的这个文件的上上个路径创建一个文件则可以这样写
#include<stdio.h>int main() {FILE* pf = fopen("./../../新建 DOC 文档.doc", "w");//这里的/同样可以换成\\(如果还想再上上个 //文件夹中选定一个文件夹(比如说game文件夹)在里面进行创建//则可以继续添加该文件夹的名称并且在后面加/)
//就像这样./../../game/新建 DOC 文档.docif (pf == NULL) {perror("fopen:");return 1;}fclose(pf);pf = NULL;return 0;
}
五、文件的顺序读写
1.顺序读写函数介绍
下面几个函数的头文件都是stdio.h
上面说的适用于所有输入流一般指适用于标准流和其他输入流(如文件输入流);所有输入流一般指适用于标准输入流和其他输入流
2.简单介绍几个函数
fputc:(将字符写入输出流FILE对象的指针)
int fputc ( int character, FILE * stream );
将字符写入流并推进位置指示器。
该字符写入流的内部位置指示器指示位置,然后自动进1。
character:
是要写入的字符
stream:
是指向标识输出流的FILE对象的指针。
如果发生写入错误,则会返回EOF并设置错误标识符(ferror)。
例子:
#include<stdio.h>int main() {FILE* pf = fopen("data.txt","w");if (pf == NULL) {perror("fopen:");return 0;}fputc('a', pf);fputc('x', pf);fputc('s', pf);//如果想换行的话可以在后面加一个‘\n’fclose(pf);pf = NULL;return 0;
}
注意它的输入顺序是axs(注意是顺序输入),文件的内容不会因为你多次运行代码而增加。就算你运行两次代码,你创建的文件内容仍然只有axs,而不是axsaxs。(因为每次打开文件的时候文件指针都会回到初始位置)
因为此时我们是以w来打开文件,我们每次打开文件的时候如果文件中存在内容,则会清空文件内容
fgetc:(将文件指针指向的内容返回)
int fgetc ( FILE * stream );
返回指定流的内部文件位置指示器当前指向的字符。然后,内部文件位置指示器前进到下一个字符。
如果调用时流位于文件末尾,则函数返回 EOF 并设置流 (feof) 的文件末尾指示器。
如果发生读取错误,该函数将返回 EOF 并设置流的错误指示器 (ferror)。
fgetc 和 getc 是等效的,只是 getc 可以在某些库中作为宏实现。
stream:
指向标识输入流的FILE对象的指针。
如果读取失败则会返回一个EOF
例子:
#include<stdio.h>int main() {FILE* pf = fopen("data.txt", "w");//然后将这里的w改成w+if (pf == NULL) {perror("fopen:");return 0;}fputc('a', pf);fputc('x', pf);fputc('s', pf);//此时我们以写文件的形式打开文件,向文本内输入axsint c = fgetc(pf);//当我们运行代码的时候我们会发现,fgetc得到的内容为空//这是因为我们是以w的形式打开文件,只能写入无法读取,此时会造成fgetc为未定义形式//当我们改为w+形式后,fgetc得到的内容仍然为空//这是因为我们此时的文件指针位于最后一个字符之后//此时调用fgetc指针已经在文件末尾直接返回EOF/*如果我们想要成功读到内容则需要添加下面这些内容//rewind(pf);*/if (c != EOF) {//这里用于判断是否读取成功printf("%c", c);}fclose(pf);pf = NULL;return 0;
}
fputs:(将字符串写入输出流FILE对象的指针)
int fputs ( const char * str, FILE * stream );
将 str 指向的 C 字符串写入流。
该函数开始从指定的地址 (str) 复制,直到到达终止的空字符 ('\0')。此终止 null 字符不会复制到流中。
请注意,fputs 与 puts 的不同之处不仅在于可以指定目标流,而且 fputs 不会写入额外的字符,而 puts 会自动在末尾附加一个换行符。
char* str:
是字符串
stream:
是指向标识输出流的FILE对象的指针。
#include<stdio.h>int main() {FILE* pf = fopen("data.txt", "w");//如果以写文件的形式打开文件//如果文件不存在,则创建一个新文件//如果文件存在,则会清空原文件的所有内容,重新写入if (pf == NULL) {perror("fopen");return 1;}fputs("hello world", pf);//如果想要换行的话,正常往后面添加\nfclose(pf);pf = NULL;return 0;
}
fgets: (将文件中的内容存放在str指向的内容空间)
char * fgets ( char * str, int num, FILE * stream );
从流中读取字符并将它们作为 C 字符串存储到 str 中,直到读取 (num-1) 个字符或到达换行符或文件末尾,以先发生者为准。
换行符
使 fgets 停止读取,但它被函数视为有效字符并包含在复制到 str 的字符串中。
在复制到 str 的字符之后自动附加
终止 null 字符。
请注意,fgets 与 gets 有很大不同:fgets 不仅接受流参数,还允许指定 str 的最大大小,并在字符串中包含任何结束换行符。
str:
指向要存放读取到内容的空间
num:
是要复制到str中的最大字符数(包括null字符)
stream:
是指向标识输入流的FILE对象的指针。
#include<stdio.h>int main() {FILE* pf = fopen("data.txt", "r+");//因为我们要读和写所以这里用r+if (pf == NULL) {perror("fopen");return 1;}fputs("hello world", pf);rewind(pf);//这里就是前面说的让指针回到偏移量为0处、//如果缺少rewind我们将会发现无法将文件中的内容按照预想的一样存放在arr中char arr[20] = { 0 };fgets(arr, 5, pf);//这里我们要复制的字符串数目少于文件中保存的//如果文件中有换行符的时候,即使复制的字符串数目多余文件中保存的,再使用fgets当识别到换行符后会自动停止//所以复制到arr中的内容为hell和\0,(\0也算一个字符)for (int i = 0;i < 5;i++) {printf("%c", arr[i]);}fclose(pf);pf = NULL;return 0;
}
fprintf:(按照格式将内容写入输出流FILE对象的指针)
int fprintf ( FILE * stream, const char * format, ... );
将按格式指向的 C 字符串写入流。如果 format 包含格式说明符(以 % 开头的子序列),则格式后面的其他参数将被格式化并插入到结果字符串中,以替换其各自的说明符。
在 format 参数
之后,该函数需要至少与 format 指定的一样多的其他参数。
stream:
是指向标识输出流的 FILE 对象的指针。
format:
C 字符串,其中包含要写入流的文本。它可以选择包含嵌入的格式说明符,这些说明符将替换为后续附加参数中指定的值,并根据请求进行格式设置。
#include<stdio.h>int main() {int age = 100;char name[20] = "xiaomin";double math = 98.6;FILE* pf = fopen("data.txt", "w");if (pf == NULL) {perror("fopen");return 1;}fprintf(pf,"%d %s %lf", age, name, math);fclose(pf);pf = NULL;return 0;
}
fscanf:(将文件中的内容按照格式存入系统中)
int fscanf ( FILE * stream, const char * format, ... );
从流中读取数据,并根据参数格式将它们存储到附加参数指向的位置。
其他参数应指向格式字符串中相应格式说明符指定的类型的已分配对象
stream:
指向标识要从中读取数据的输入流的 FILE 对象的指针
format:
C 字符串,其中包含一系列字符,这些字符控制如何处理从流中提取的字符
#include<stdio.h>int main() {int age = 0;char name[20] = "a";double math = 0;FILE* pf = fopen("data.txt", "r");//假设我们现在文件里面的内容存放的是20 zhangsan 120。if (pf == NULL) {perror("fopen");return 1;}printf("%d %s %lf\n", age, name, math);//所以这里我们打印出来的结果应该是0 a 0fscanf(pf, "%d %s %lf", &age, name, &math);//然后我将文件只中的内容输入到系统中printf("%d %s %lf", age, name, math);//所以我们这里输出的应该是20 zhangsan 120fclose(pf);pf = NULL;return 0;
}
fwrite:(将ptr指针中的内容以二进制的形式存入到stream指向的FILE对象的指针)
size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream );
将 count 元素数组写入,每个元素的大小为 bytes,从 ptr 指向的内存块到流中的当前位置。
流的位置指示器按写入的总字节数前进。
在内部,该函数将指向的块解释为类型元素的数组,并按顺序将它们写入,就像为每个字节调用一样。ptr(size*count)
unsigned char
streamfputc
ptr:指向要写入的元素数组的指针,转换为 const void*。
size:要写入的每个元素的大小(以字节为单位)。
count:元素数,每个元素的大小为 bytes。
steam:指向指定输出流的FILE对象的指针。
#include<stdio.h>int main() {int arr[] = { 1,2,3,4,5,6 };FILE* pf = fopen("data.txt", "wb");if (pf == NULL) {perror("fopen");return 1;}fwrite(arr, sizeof(int), 6, pf);//这里我们将数组arr中的内容写入到了data.txt文件里面fclose(pf);pf = NULL;return 0;
}
fread:(可以看作是fwrite的逆过程)
size_t fread ( void * ptr, size_t size, size_t count, FILE * stream );
从流中读取 count 元素数组,每个元素的大小为 bytes,并将它们存储在 ptr 指定的内存块中。
流的位置指示器按读取的字节总量提前。
如果成功,读取的字节总量为 (size*count)。
ptr:指向大小至少为(size*count)字节的内存块的指针,转换为void*
size:要写入的每个元素的大小(以字节为单位)。
count:元素数,每个元素的大小为 bytes。
steam:指向指定输入流的FILE对象的指针。
#include<stdio.h>int main() {FILE* pf = fopen("data.txt", "rb");//假设我们这里存放的是1,2,3,4,5,6if (pf == NULL) {perror("fopen");return 1;}int arr[6] = { 0 };fread(arr, sizeof(int), 6, pf);for (int i = 0; i < 6; i++){printf("%d ", arr[i]);}fclose(pf);pf = NULL;return 0;
}
3.拓展说明一下scanf/fscanf/sscanf,printf/fprintf/sprintf
scanf:针对stdin的格式化的输入函数
printf:针对stdin的格式化的输出函数
fscanf:针对所有输入流的格式化输入函数
fprintf:针对所有输出流的格式化输出函数
(可以将上面的两个函数看成是下面两个函数的子集)
sprintf:
int sprintf ( char * str, const char * format, ... );
使用与在printf上使用 format 时将打印的文本相同的文本组成一个字符串,但内容不是打印,而是作为 C 字符串存储在 str 指向的缓冲区中。
缓冲区的大小应足够大,以包含整个结果字符串(有关更安全的版本,请参阅 snprintf)。
终止的空字符会自动附加在内容之后。
在 format 参数之后,该函数至少需要与格式所需的附加参数一样多。
str:
指向存储生成的 C 字符串的缓冲区的指针。
缓冲区应足够大以包含生成的字符串。
format:
C 字符串,其中包含遵循与printf中的 format 相同的规范的格式字符串(有关详细信息,请参阅 )。
...(附加参数):
根据格式字符串,函数可能需要一系列附加参数,每个参数都包含一个值,用于替换格式字符串中的格式说明符(或指向存储位置的指针,对于 n)。
这些参数的数量应至少与格式说明符中指定的值数一样多。函数将忽略其他参数。
#include <stdio.h>int main() {int age = 10;char name[10] = { "zhangsan" };char buf[20] = { 0 };sprintf(buf, "%d %s", age, name);//这个函数的作用可以理解为//将其他数据的格式,格式化//然后统一转化为字符串printf("%s", buf);return 0;
}
sscanf:
int sscanf ( const char * s, const char * format, ...);
从 s 读取数据并根据参数格式将它们存储到附加参数给出的位置中,就像使用了scanf 一样,但从 s 而不是标准输入 (stdin) 读取。
其他参数应指向格式字符串中相应格式说明符指定的类型的已分配对象。
s:
函数作为其源处理的 C 字符串,以检索数据。
format:
C 字符串,其中包含遵循与scanf中的格式相同的规范的格式字符串
...(附加参数):
根据格式字符串,函数可能需要一系列附加参数,每个参数都包含指向已分配存储的指针,其中提取的字符的解释以适当的类型存储。
这些参数的数量应至少与格式说明符存储的值数一样多。函数将忽略其他参数。
#include <stdio.h>int main() {int age = 10;char name[10] = { "zhangsan" };char buf[20] = { 0 };sprintf(buf, "%d %s", age, name);//这个函数的作用可以理解为//将其他数据的格式,格式化//然后统一转化为字符串printf("%s\n", buf);int age2 = 0;char name2[10] = { 0 };sscanf(buf, "%d %s", &age2, name2);//这个可以看作是sprintf的逆过程printf("%d\n", age2);printf("%s", name2);return 0;
}
六、文件的随机读写
fseek:(将文件指针指向的位置进行偏移)
int fseek ( FILE * stream, long int offset, int origin );
将与流关联的位置指示器设置为新位置。
对于以二进制模式打开的流,新位置是通过向 origin 指定的参考位置添加偏移量来定义的。
对于以文本模式打开的流,偏移量应为零或先前调用 ftell 返回的值,并且 origin 必须SEEK_SET。
如果使用这些参数的其他值调用函数,则支持取决于特定的系统和库实现(不可移植)。
成功调用此函数后,流的文件结束内部指示器将被清除,并且之前在此流上调用 ungetc 的所有效果都将被删除。
在打开更新(读取+写入)的流上,对 fseek 的调用允许在读取和
stream:
指向标识流的 FILE 对象的指针。
offset:
二进制文件:从源偏移的字节数。
文本文件:零或 ftell 返回的值。
origin:
用作偏移参考的位置。它由 <cstdio> 中定义的以下常量之一指定,专门用作此函数的参数:
不断 | 参考位置 |
---|---|
SEEK_SET | 文件开头 |
SEEK_CUR | 文件指针的当前位置 |
SEEK_END | 文件结束 |
#include<stdio.h>
int main() {FILE* pf = fopen("data.txt", "r");//此时假设我们文件里面存放的内容是abcdefgif (pf == NULL) {perror("fopen");return 1;}int ch = fgetc(pf);//此时文件中指针指向的是文件的起始位置printf("%c\n", ch);//这里打印的是afseek(pf, 3, SEEK_SET); //我们使用fseek函数将文件中指针与起始位置偏移3个单位//所以我们后面printf打印的结果是d//如果我们使用SEEK_CUR且偏移数还是三的话//前面printf打印的结果不变后面变成e//因为SEEK_CUR是让指针在当前位置开始偏移,SEEK_END同理不再进行解释ch = fgetc(pf);printf("%c", ch);//这里打印的是dfclose(pf);pf = NULL;return 0;
}
ftell:(将文件指针的当前位置与文件指针的起始位置返回出来)、
long int ftell ( FILE * stream );
返回流的位置指示器的当前值。
对于二进制流,这是从文件开头开始的字节数。
对于文本流,数值可能没有意义,但仍可用于稍后使用 fseek 将位置恢复到同一位置(如果有使用 ungetc 放回的字符仍在等待读取,则行为未定义)。
stream:
指向标识流的 FILE 对象的指针。
#include<stdio.h>
int main() {FILE* pf = fopen("data.txt", "r");//同样我们这里假设存放的是abcdefif (pf == NULL) {perror("fopen");return 1;}int ch = fgetc(pf);printf("%c\n", ch);fseek(pf, 3, SEEK_SET);ch = fgetc(pf);printf("%c\n", ch);int a = ftell(pf);//我们先与起始位置偏移了3单位长度//然后又使用fgetc,使指针又往后偏移了一个单位长度printf("%d", a);//所以我们这里打印出来的结果是4。fclose(pf);pf = NULL;return 0;
}
rewind:(将文件指针返回到起始位置,我们在前面使用过所以也不过多进行解释)
七、文件读写结束的判定
feof:(当文件读取结束的时候,判断读取结束的原因是否是:遇到文件尾结束)
int feof ( FILE * stream );
检查是否设置了与流关联的文件结束指示器,如果设置了,则返回与零不同的值。
此指示器通常由尝试读取文件末尾或超过文件末尾的流的先前作设置。
请注意,流的内部位置指示器可能指向下一个作的文件末尾,但仍然可能不会设置文件结束指示器,直到作尝试在该点读取。
注意:在文件读取过程中,不能使用feof函数的返回值来判断文件的是否结束。
1.文件读写结束的判定
1.文本文件读取是否结束,判断返回值是否为EOF(fgetc),或者NULL(fgets)
2.二进制文件的读取结束判断,判断返回值是否小于世界要读的个数。
例如:fread判断返回值是否小于实际要读取的个数。
下面这段代码是文本文件的例子
#include <stdio.h>
#include <stdlib.h>int main() {int c;FILE* pf = fopen("data.txt", "r");if (pf == NULL) {perror("FILE opening failed");return 1;}while (c = fgetc(pf) != EOF) {putchar(c);}if (ferror(pf)) puts("I\O error when reading");else if (feof(pf)) puts("end of file reached successful");fclose(pf);pf = NULL;return 0;
}
\\这段代码最后的运行结果就是输出了else if 的结果
下面这段代码是二进制文件的例子
#include<stdio.h>
#include<stdlib.h>
#define n 5
int main() {int arr[n] = { 1,2,3,4,5 };FILE* pf = fopen("data.bin", "wb");fwrite(arr, sizeof(*arr), n , pf);fclose(pf);int arr1[n] = { 0 };pf = fopen("data.bin", "rb");int ret_code =fread(arr1, sizeof(*arr), n , pf);if (ret_code == n) {puts("Array read successfully contents\n");for (int i = 0; i < n; i++) {printf("%d ", arr1[i]);}}else {if (feof(pf)) puts("Error reading data.bin unexpected end of file");else if (ferror(pf)) perror("Error reading data.bin");}fclose(pf);pf = NULL;return 0;
}
八、文件缓冲区
ANSI C标准采用“缓冲文件系统”处理数据文件的,所谓缓冲文件是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块”文件缓冲区“,从文件向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上,如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓冲区的大小根据c编译系统决定的。