一、什么是文件
文件其实是指一组相关数据的有序集合。这个数据集有一个名称,叫做文件名。文件通常是驻留在外部介质(如磁盘等)上的,在使用时才调入内存中来。
文件一般讲两种:程序文件和数据文件:
- 程序文件:包括源程序文件(后缀为.c),目标文件(windows环境后缀为.obj),可执行程序(windows环境后缀为.exe)。
- 数据文件:包括程序运行时所读写的数据。本篇所涉及的就是数据文件。
二、文件的使用
文件的操作一般分三步:1.打开文件;2.读/写;3.关闭文件;
三、文件的打开与关闭
3.1 流与标准流
在C语言中,“流”(stream)是一种用于输入和输出数据的抽象概念,我们可以把流想象成流淌着字符的河。它是一种数据的传输方式,可以将数据从一个地方传送到另一个地方。在C语言中,输入流和输出流是通过一组标准库函数来实现的,这些函数允许程序从键盘或文件中读取数据,或者将数据写入到屏幕或文件中。
C程序针对文件、画面、键盘等数据输⼊输出操作都是通过流操作的。⼀般情况下,我们要想向流⾥写数据,或者从流中读取数据,都是要打开流,然后操作。
C语言中的流可以分为标准流(standard streams)和文件流(file streams)
注:C语言中操作流的主要函数是标准I/O库中的stdio.h头文件中定义的函数。
3.1.1 标准流
我们需要清楚,C语言程序,只要运行起来,就会默认打开3个流(标准流)
标准输入流(stdin):用于读取输入数据,默认情况下是键盘输入。
标准输出流(stdout):用于向终端或命令行窗口输出数据。
标准错误流(stderr):用于输出错误信息。
3.1.2 文件流
C语言中的文件流是一种用于在程序中读取和写入文件的流。通过文件流,可以在C程序中打开文件,从文件中读取数据或将数据写入文件中。这样可以有效地处理大量数据、持久性存储以及与文件系统的交互。
本次,我们重点讨论文件流
3.2 文件指针
每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(如文件的名字,文件状态及文件当前的位置等)。这些信息是保存在一个结构体变量中的。该结构体类型是由系统声明的,取名FILE。
文件指针的使用方式:
FILE* pf;
定义一个文件指针变量pf,它可以指向某个文件的文件信息区,通过其即可访问到该文件。
3.3 文件的打开与关闭
⽂件在读写之前应该先打开⽂件,在使⽤结束之后应该关闭⽂件,在编写程序的时候,在打开⽂件的同时,都会返回⼀个FILE*的指针变量指向该⽂件,也相当于建⽴了指针和⽂件的关系。
ANSIC规定使⽤ fopen 函数来打开⽂件, fclose 来关闭⽂件。
FILE * fopen ( const char * filename, const char * mode ); //打开文件 int fclose ( FILE * stream ); //关闭文件
3.3.1 文件的访问方式
文件使用方式 | 含义 | 如果指定文件不存在 |
“r”(只读) | 为了输入数据,打开一个已经存在的文本文件 | 出错 |
“w”(追加) | 为了输出数据,打开一个文本文件(清空原有数据) | 建立一个新的文件 |
“a”(追加) | 向文本文件尾添加数据 | 建立一个新的文件 |
“rb”(只读) | 为了输入数据,打开一个二进制文件 | 出错 |
“wb”(只写) | 为了输出数据,打开一个二进制文件(清空原有数据) | 建立一个新的文件 |
“ab”(追加) | 向一个二进制文件尾添加数据 | 建立一个新的文件 |
“r+”(读写) | 为了读和写,打开一个文本文件 | 出错 |
“w+”(读写) | 为了读和写,创建一个新的文件(清空原有数据) | 建立一个新的文件 |
“a+”(读写) | 打开一个文件,在文件尾进行读写 | 建立一个新的文件 |
“rb+”(读写) | 为了读和写打开一个二进制文件 | 出错 |
“wb+”(读写) | 为了读和写新建一个二进制文件(清空原有数据) | 建立一个新的文件 |
“ab+”(读写) | 打开一个二进制文件,在文件尾进行读和写 | 建立一个新的文件 |
注1:当文件打开失败出错时,会返回一个空指针,因此我们一定要在打开文件之后,对文件指针进行有效性检查
注2:对于打开进行更新的文件(包含“+”号的文件),允许输入和输出操作,在写入操作之后的读取操作之前,应刷新(fflush)或重新定位流(fseek,fsetpos,rewind)。流应在读取操作之后的写入操作之前重新定位(fseek、fsetpos、rewind)(只要该操作未到达文件末尾)
3.4 文件的使用方式
代码示例如下:
#define _CRT_SECURE_NO_WARNINGS #include<stdio.h> int main() { //此时该路径下没有名为data.txt的文件,因此会打开失败 FILE* fp = fopen("data.txt", "r"); if (NULL == fp) { perror("fopen"); return 1; } fclose(fp); fp = NULL; return; }
#include<stdio.h> int main() { //用写的方式打开文件,如果文件不存在,会在该路径底下创建一个新的名为data.txt的文件 FILE* fp = fopen("data.txt", "w"); if (NULL == fp) { perror("fopen"); return 1; } fclose(fp); fp = NULL; return; }
四、文件的顺序读写
顺序读写的函数:
功能 | 函数名 | 适用于 |
字符输入函数 | fgetc | 所有输入流 |
4.1 单字符输入输出
(一)fputc函数
#include<stdio.h> int main() { //打开文件 FILE* pf = fopen("test.txt", "w"); if (pf == NULL) { perror(" fopen fail"); return 1; } //将abc放进文件 fputc('a', pf); fputc('b', pf); fputc('c', pf); //关闭文件 fclose(pf); pf = NULL;//防止野指针 return 0; }
(二)fgetc函数
#include<stdio.h> int main() { //打开文件 FILE* pf = fopen("test.txt", "r");//只读 if (pf == NULL) { perror(" fopen fail"); return 1; } fputc('a', pf); fputc('b', pf); fputc('c', pf); int ch = fgetc(pf); printf("读出来的字符为:%c\n", ch); ch = fgetc(pf); printf("读出来的字符为:%c\n", ch); ch = fgetc(pf); printf("读出来的字符为:%c\n", ch); //关闭文件 fclose(pf); pf = NULL;//防止野指针 return 0; }
4.2 文本行输入输出
(一)fputs函数
#include<stdio.h> int main() { //打开文件 FILE* pf = fopen("test.txt", "w"); if (pf == NULL) { perror(" fopen fail"); return 1; } fputs("hello betty", pf); //关闭文件 fclose(pf); pf = NULL;//防止野指针 return 0; }
(二)fgets函数
1.声明:char *fgets(char *str, int n, FILE *stream)
- str – 这是指向一个字符数组的指针,该数组存储了要读取的字符串。
- n – 这是要读取的最大字符数(包括最后的空字符)。通常是使用以 str 传递的数组长度。
- stream – 这是指向 FILE 对象的指针,该 FILE 对象标识了要从中读取字符的流。
2.作用:从指定的流 stream 读取一行,并把它存储在 str 所指向的字符串内。当读取 (n-1) 个字符时,或者读取到换行符时,或者到达文件末尾时,它会停止,具体视情况而定。
3.返回值:如果成功,该函数返回相同的 str 参数。如果到达文件末尾或者没有读取到任何字符,str 的内容保持不变,并返回一个空指针。如果发生错误,返回一个空指针。
#include<stdio.h> int main() { //打开文件 FILE* pf = fopen("test.txt", "r"); if (pf == NULL) { perror(" fopen fail"); return 1; } fputs("hello betty", pf); char arr[] = "##########"; fgets(arr, 5, pf); puts(arr); //关闭文件 fclose(pf); pf = NULL;//防止野指针 return 0; }
为什么只有四个字符呢 ,我们打开调试,会发现/0也要算一个字符
4.3 格式化输入输出
(一)fscanf函数
typedef struct student { char name[20]; int height; float score; }stu; int main() { stu s = { "beidi", 170, 95.0 }; //写文件 FILE* pf = fopen("test.txt", "w"); if (pf == NULL) { perror(" fopen fail"); return 1; } fprintf(pf, "%s %d %f", s.name, s.height, s.score); //关闭文件 fclose(pf); pf = NULL;//防止野指针 return 0; }
(二)fprintf函数
typedef struct student { char name[20]; int height; float score; }stu; int main() { stu s = { "beidi", 170, 95.0 }; //写文件 FILE* pf = fopen("test.txt", "r"); if (pf == NULL) { perror(" fopen fail"); return 1; } fscanf(pf, "%s %d %f", s.name, &(s.height), &(s.score)); printf("%s %d %f", s.name, s.height, s.score); //关闭文件 fclose(pf); pf = NULL;//防止野指针 return 0; }
4.4 二进制输入输出
(一)fread函数
typedef struct student { char name[20]; int height; float score; }stu; int main() { stu s = { "beidi", 170, 95.0 }; //写文件 FILE* pf = fopen("test.txt", "wb");//二进制写入 if (pf == NULL) { perror(" fopen fail"); return 1; } fwrite(&s, sizeof(s), 1, pf); //关闭文件 fclose(pf); pf = NULL;//防止野指针 return 0; }
二进制语言人类无法识别,但电脑能准确读取
(二)fwrite函数
typedef struct student { char name[20]; int height; float score; }stu; int main() { stu s = {0}; //写文件 FILE* pf = fopen("test.txt", "rb");//二进制写出 if (pf == NULL) { perror(" fopen fail"); return 1; } fread(&s, sizeof(s), 1, pf); printf("%s %d %f", s.name, s. height, s.score); //关闭文件 fclose(pf); pf = NULL;//防止野指针 return 0; }
4.5 补充
sscanf和sprintf函数
typedef struct student { char name[20]; int height; float score; }stu; int main() { char buf[100] = { 0 }; stu s = { "betty", 170, 95.0f }; stu tmp = { 0 }; //将这个结构体的成员转化为字符串 sprintf(buf, "%s %d %f", s.name, s.height, s.score); printf("%s\n", buf); //将这个字符串中内容还原为一个结构体数据呢 sscanf(buf, "%s %d %f", tmp.name, &(tmp.height), &(tmp.score)); printf("%s %d %f", tmp.name, tmp.height, tmp.score); return 0; }
五、文件的随机读写
所谓的随机读写,其实就是指定我们想要读写的位置
5.1 fseer()函数
- 该函数可以从定位位置的偏移量处开始读写;
- int fseek( FILE *stream, long offset, int origin );
文件流 偏移量 起始位置
- 返回值:
- 如果成功,fseek返回0;
- 否则,它返回一个非零值;
- 在无法查找的设备上,返回值未定义;
使用实例:
int main() { //打开文件 FILE* pf = fopen("test.txt", "r"); if (pf == NULL) { perror(" fopen fail"); return 1; } fseek(pf, 4, SEEK_SET); //从起始位置偏移四个字节 int ch1 = fgetc(pf); printf("%c ", ch1); fseek(pf, -3, SEEK_END); //从结束位置偏移七个个字节 int ch2 = fgetc(pf); printf("%c ", ch2); fseek(pf, 1, SEEK_CUR); //从当前位置偏移一个字节 int ch3 = fgetc(pf); printf("%c ", ch3); //关闭文件 fclose(pf); pf = NULL;//防止野指针 return 0; }
5.2 ftell()函数
- 头文件:#include<stdio.h>
- 声明:long int ftell(FILE *stream)
- stream – 这是指向 FILE 对象的指针,该 FILE 对象标识了流。
- 作用:返回⽂件指针相对于起始位置的偏移量
- 返回值:该函数返回位置标识符的当前值。如果发生错误,则返回 -1L,全局变量 errno 被设置为一个正值。
我们可以利用fseek和ftell来计算文件的长度(不包含’\0’),下列是代码示例
int main() { FILE* pFile; long size; pFile = fopen("test.txt", "rb"); if (pFile == NULL) perror("Error opening file"); else { fseek(pFile, 0, SEEK_END); //non-portable size = ftell(pFile); fclose(pFile); printf("文件长度为: %ld bytes.\n", size); } return 0; }
5.3 rewind()函数
- 头文件:#include<stdio.h>
- 声明:void rewind(FILE *stream)
- stream – 这是指向 FILE 对象的指针,该 FILE 对象标识了流
- 作用:让⽂件指针的位置回到⽂件的起始位置
- 返回值:该函数不返回任何值。
rewind常常在文件读与写同时使用时,以方便文件读取。下面是rewind的具体使用实例:
#include <stdio.h> int main() { int n; FILE* pFile; char buffer[27]; pFile = fopen("myfile.txt", "w+"); for (n = 'A'; n <= 'Z'; n++) fputc(n, pFile);//放入26个字母 rewind(pFile);//回到起始位置,方便读取 fread(buffer, 1, 26, pFile);//读取· fclose(pFile); buffer[26] = '\0';//字符串的结束标识 printf(buffer); return 0; }
六、二进制文件与文本文件
我们知道数据在内存中是以二进制形式存储的,对于文件而言:如果不加转换直接输出到外存就是二进制文件;如果要在外存上以ASCII码形式存储,就需要提前转换最后以ASCII码值形式存储的文件就是文本文件。
对于字符,一律使用ASCII码形式存储,但对于数值型数据,即可以使用ASCII码存储也可以使用二进制形式存储。
举例:
数字10000的两种存储形式:
二进制文件:
文本文件:
首先将10000分成'1','0','0','0','0', 这五个字符,用每个字符对应的ASCII码值进行转换:
显而易见,二进制文件存储和文本文件存储对不同范围的数字可以做到节省空间。
七、文件读取结束判定
feof()函数:该函数被许多人错误用来判断文件是否读取结束,其实它的作用是判断文件读取结束的原因;
文件读取结束有两种情况:1.读取过程中出现异常; 2.读取到文件末尾;
要找出文件读取是哪个原因,就分为以下情况:
文本文件:
- 如果用 fgetc() 读取,要判断 feof() 的返回值是否为EOF;
- 如果用 fgets() 读取,要判断 feof() 的返回值是否为NULL(0);
二进制文件:
都是使用 fread() 读取,要判断其返回值与指定读取个数的大小,如果小于实际要读的个数,就说明发生读取异常,如果等于实际要读的个数,就说明是因读取成功而结束;
对于读取异常的判断,我们考虑判断 ferror() 函数的返回值:
- 若ferrror()为真——异常读取而结束;
- 若feof()为真——正常读取到尾而结束;
对文本文件的判断:
#include<stdio.h> #include<string.h> #include<errno.h> int main() { FILE* pf = fopen("test.txt", "r"); if (pf == NULL) { perror("fopen is failed !"); return; } int c = 0; //由于要检查EOF——EOF本质是0——所以是int while (c = fgetc(pf) != EOF) { putchar(c); } //直到while不执行了—读取结束了—判断是什么原因结束的 if (ferror(pf)) { printf("读取中出现错误\n"); } else if (feof(pf)) { printf("读取到文件尾\n"); } fclose(pf); pf = NULL; return 0; }
对二进制文件的判断:
#include<stdio.h> #include<string.h> #include<errno.h> int main() { FILE* pf = fopen("test.txt", "rb"); int arr[5] = { 0 }; if (pf == NULL) { return; } size_t num = fread(arr, sizeof(int), 5, pf); if (num == 5) { //说明全部读取成功 printf("Array read successfully\n"); } else { //说明读取不够指定长度—判断是什么原因 if (ferror(pf)) { printf("读取中出现错误\n"); } else if (feof(pf)) { printf("读取到文件尾\n"); } } fclose(pf); pf = NULL; return 0; }
八、文件缓冲区
ANSIC 标准采用缓冲文件系统处理的数据文件的,所谓缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块“文件缓冲区”。
- 从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才⼀起送到磁盘上。
- 如果从磁盘向计算机读⼊数据,则从磁盘⽂件中读取数据输⼊到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)
- 缓冲区的⼤⼩根据C编译系统决定的。
include <stdio.h> #include <windows.h> //VS2019 WIN11环境测试 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语⾔在操作⽂件的时候,需要做刷新缓冲区或者在⽂件操作结束的时候关闭⽂件。如果不做,可能导致读写⽂件的问题。