目录
1. 为什么使用文件?
如果没有⽂件,我们写的程序的数据是存储在电脑的内存中,如果程序退出,内存回收,数据就丢失 了,等再次运⾏程序,是看不到上次程序的数据的,如果要将数据进⾏持久化的保存,我们可以使⽤ ⽂件。
2. 什么是文件?
磁盘(硬盘)上的⽂件是⽂件。
在程序设计中,我们一般谈的文件有两种:程序文件、数据文件。
1.程序文件
程序文件包括源程序⽂件(后缀为.c),目标文件(windows环境后缀为.obj),可执行程序(windows 环境后缀为.exe)。
2.数据文件
文件的内容不⼀定是程序,而程序运⾏时读写的数据,比如程序运行需要从中读取数据的文件,或 者输出内容的文件。
3.文件名
一个⽂件要有一个唯一的文件标识,以便用户识别和引用。 文件名包含3部分:文件路径+文件名主干+文件后缀
例如: c:\code\test.txt 为了方便起见,文件标识常被称为文件名。
3. 二进制文件和文本文件
根据数据的组织形式,数据文件被称为文本文件或者二进制文件。
数据在内存中以二进制的形式存储,如果不加转换的输出到外存的⽂件中,就是二进制⽂件。 如果要求在外存上以ASCII码的形式存储,则需要在存储前转换。
以ASCII字符的形式存储的文件就是文本文件。
4. 文件的打开和关闭
1.流和标准流
a.流
我们程序的数据需要输出到各种外部设备,也需要从外部设备获取数据,不同的外部设备的输⼊输出操作各不相同,为了方便程序员对各种设备进行方便的操作,我们抽象出了流的概念,我们可以把流 想象成流淌着字符的河。
C程序针对文件、画面、键盘等的数据输入输出操作都是通过流操作的。
在C语言中,程序员只需要关心流是怎样操作的不用去关心外部设备的数据怎样进入流中的,或者流中标的数据值怎样输出到显示器或外部设备中的,程序员只需要关心流是怎样操作的,就可以了,这就大大的降低了程序员学习编程的难度。
就像百度搜索引擎一样,我们在搜索一个想要的资料时,我们只需要去将问题输入到搜索栏里面,而不需要关心操作系统是怎样将相关数据输入输出来的一样,方便了我们的使用。
b.标准流
那为什么我们从键盘输⼊数据,向屏幕上输出数据,并没有打开流呢? 那是因为C语⾔程序在启动的时候,默认打开了3个流:
1.stdin - 标准输⼊流,在⼤多数的环境中从键盘输⼊,scanf函数就是从标准输⼊流中读取数据。
2.stdout - 标准输出流,⼤多数的环境中输出⾄显⽰器界⾯,printf函数就是将信息输出到标准输出 流中。
3.stderr - 标准错误流,⼤多数环境中输出到显⽰器界⾯。
这是默认打开了这三个流,我们使⽤scanf、printf等函数就可以直接进⾏输⼊输出操作的。 stdin、stdout、stderr 三个流的类型是: FILE* ,通常称为⽂件指针。 C语⾔中,就是通过 FILE* 的⽂件指针来维护流的各种操作的。
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 _iobuf FILE;
每当打开⼀个⽂件的时候,系统会根据⽂件的情况⾃动创建⼀个FILE结构的变量,并填充其中的信 息,使⽤者不必关⼼细节。 ⼀般都是通过⼀个FILE的指针来维护这个FILE结构的变量,这样使⽤起来更加⽅便。 下⾯我们可以创建⼀个FILE*的指针变量:
FILE* pf;
定义pf是⼀个指向FILE类型数据的指针变量。可以使pf指向某个⽂件的⽂件信息区(是⼀个结构体变 量)。通过该⽂件信息区中的信息就能够访问该⽂件。也就是说,通过⽂件指针变量能够间接找到与 它关联的⽂件。
3.文件的打开好关闭
⽂件在读写之前应该先打开⽂件,在使⽤结束之后应该关闭⽂件。
在编写程序的时候,在打开⽂件的同时,都会返回⼀个FILE*的指针变量指向该⽂件,也相当于建⽴了 指针和⽂件的关系。
ANSIC 规定使⽤ fopen 函数来打开⽂件, fclose 来关闭⽂件。
//打开⽂件
FILE* fopen(const char* filename, const char* mode);
//关闭⽂件
int fclose(FILE* stream);
fopen中的第一个参数是文件名,我们要打开哪个文件就将它的文件名打到上面,第二个参数是文件的打开方式。
接下来,我们来看几个文件的打开方式:
我们用“r”(只读)的打开方式感受一下文件的打开好关闭:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读文件
//关闭文件
fclose(pf);
pf = NULL;;
return 0;
}
观察运行结果,我们发现输出结果提示我们 No such file or directory,原因是我们指定打开的文件不在,所以出错了。所以,我们在使用“r”(只读)的打开方式时,一定要创建出指定的文本文件。
我们再用“w”(只写)的打开方式感受一下文件的打开好关闭:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读文件
//关闭文件
fclose(pf);
pf = NULL;;
return 0;
}
观察运行结果发现没有如何问题,这时候会在项目文件中新建出一个test.txt文件。
本篇文章主要使用的是这两种打开方式,其他就不再做过多的介绍。
5. 文件的顺序读写
顺序读写函数:
1.fgetc函数和fputc函数
fputc函数第一个参数是将将要写入流中的内容,第二个参数是指向标识输出流的FILE对象的指针。每次字符写入流的内部位置指示器指示的位置,然后自动前进1(文件中的光标向后移动一位)。返回成功后,将返回一个非负值,出错时,该函数返回EOF(-1)。
fgetc函数的参数是指向标识输入流的FILE对象的指针。返回指定流的内部文件位置指示器当前指向的字符。然后,内部文件位置指示器将前进到下一个字符。成功后,将返回读取的字符,如果位置指示器位于文件末尾或者发生其他读取错误,该函数也会返回EOF(-1)。
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "w");//写文件
if (pf == NULL)
{
perror("fopen");
return 1;
}
int ch = 0;
for (ch = 'a'; ch <= 'z'; ch++)
{
fputc(ch, pf);
}
fclose(pf);
pf = NULL;
FILE* pf1 = fopen("test.txt", "r");//读文件
if (pf1 == NULL)
{
perror("fopen");
return 1;
}
int i = 0;
while ((i = fgetc(pf1)) != EOF)
{
printf("%c", i);
}
fclose(pf1);
pf1 = NULL;
return 0;
}
读文件后会生成一个名为test.txt的文件,内容如下:
写文件后会将test.txt中的内容在输入到运行结果中。
2.fgets函数和fputs函数
fputs函数第一个参数是字符串指针,其中包含要写入流的内容。第二个参数是指向标识输出流的FILE对象的指针。返回成功后,将返回一个非负值,出错时,该函数返回EOF。作用是将字符串写入流。
fgets函数的第一个参数是指向复制读取的字符串的 char数组的指针,第二个参数要复制到str中的最大字符数,第三个参数是指向标识输入流的FILE对象的指针。stdin( 标准输⼊流)可以用作从标准输入读取的参数。返回成功后,该函数返回 str。如果在尝试读取字符时遇到文件末尾,则设置 eof 指示符 。如果在读取任何字符之前发生这种情况,则返回的指针为空指针(并且 str 的内容保持不变)。如果发生读取错误,则设置错误指示符 (ferror),并返回 null 指针(但 str 指向的内容可能已更改)。
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "w");//写文件
if (pf == NULL)
{
perror("fopen");
return 1;
}
char* ch = "hello world";
fputs(ch, pf);
fclose(pf);
pf = NULL;
FILE* pf1 = fopen("test.txt", "r");//读文件
if (pf1 == NULL)
{
perror("fopen");
return 1;
}
char ch1[20] = "********************";
fgets(ch1, 10, pf1);
int i = 0;
for (i = 0; ch1[i] != NULL; i++)
{
printf("%c", ch1[i]);
}
fclose(pf1);
pf1 = NULL;
return 0;
}
写文件后,会将字符串输出到test.txt文件中:
读文件后,会将num个字符输入(因为会将'\0'也加上去,所以真正读取的字符的个数只有9个),我们结合调试窗口观察一下:
3.fscanf函数和fprintf函数
对于fscanf函数,我们可以参照scanf函数来进行使用。fscanf函数的作用是从流中读取格式化数据,其实它仅仅比scanf函数多了一个参数,就是第一个参数,是指向标识输出流的FILE对象的指针。后面的用法与scanf函数的用法完全一致。
fprintf函数的作用是将格式化数据写入流式处理,用法同样和printf函数相似。fprintf函数就比printf函数多了一个参数,参数是是指向标识输出流的FILE对象的指针。
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
struct S
{
char name[20];
int age;
float score;
};
int main()
{
struct S s = { "张三",20,65.5f };
FILE* pf = fopen("test.txt", "w");//写文件
if (pf == NULL)
{
perror("fopen");
return 1;
}
fprintf(pf, "%s %d %f", s.name, s.age, s.score);
fclose(pf);
pf = NULL;
struct S s1 = { 0 };
FILE* pf1 = fopen("test.txt", "r");//读文件
if (pf1 == NULL)
{
perror("fopen");
return 1;
}
fscanf(pf1, "%s %d %f", s1.name, &(s1.age), &(s1.score));
printf("%s %d %f", s1.name, s1.age, s1.score);
fclose(pf1);
pf1 = NULL;
return 0;
}
同样,写文件将结构体s写到test.txt文件中:
读文件时,将文件中的内容格式读出并打印出来:
到这里,再来拓展一点内容:sscanf函数和sfprint函数sptintf的作用是将格式化的数据写入字符串,内容不是被打印,而是作为字符串存储在 str 指向的缓冲区中。str参数是指向存储生成的 C 字符串的缓冲区的指针。(缓冲区应足够大,以包含生成的字符串)。
sscanf函数的作用是将字符串中读取格式化的数据,从 s 读取数据,并根据参数格式将它们存储到附加参数给定的位置,就像使用scanf一样,但从 s 读取而不是标准输入。参数s是函数处理的字符串作为其源来检索数据。
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
struct S
{
char name[20];
int age;
float score;
};
int main()
{
struct S s = { "张三", 20, 65.5f };
char arr[200] = { 0 };
sprintf(arr, "%s %d %f", s.name, s.age, s.score);
printf("1.以字符串的形式: %s\n", arr);
struct S s1 = { 0 };
sscanf(arr, "%s %d %f", s1.name, &(s1.age), &(s1.score));
printf("2.以格式化数据的形式:%s %d %f\n", s.name, s.age, s.score);
return 0;
}
4.fread函数和fwrite函数
fwrite函数的作用是将数据以二进制的形式输出到文件中,ptr指向要写入的元素数组的指针,size要写入的每个元素的大小(以字节为单位),count是元素数,每个元素的大小为字节,steam指向指定输出流的FILE对象的指针。返回成功读取的元素总数。
fread函数是从流中读取二进制的数据块,ptr指向大小至少为 (size*count) 字节的内存块的指针,转换为 void*。size是要读取的每个元素的大小(以字节为单位)。count是元素数,每个元素的大小为字节。stream是指向指定输入流的FILE对象的指针。返回成功读取的元素总数。
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "wb");//写二进制文件
//wb:为了输出数据,打开⼀个⼆进制⽂件
if (pf == NULL)
{
perror("fopen");
return 1;
}
int arr[5] = { 1,2,3,4,5 };
fwrite(arr, sizeof(arr[0]), 5, pf);
fclose(pf);
pf = NULL;
FILE* pf1 = fopen("test.txt", "rb");//读二进制文件
//wb:为了输出数据,打开⼀个⼆进制⽂件
if (pf1 == NULL)
{
perror("fopen");
return 1;
}
int arr1[5] = { 0 };
int i = 0;
while (fread(&arr1[i], sizeof(int), 1, pf1))
{
printf("%d ", arr1[i]);
}
fclose(pf1);
pf1 = NULL;
return 0;
}
在这里要注意因为fread函数的返回值是成功读取的元素总数,所以可以直接使用while循环来打印文件中的内容。
6. 文件的随机读写
1.fseek
fseek函数可以重新定位流位置指示器,参数stream指向标识流的FILE对象的指针。参数offset是偏移量。参数origin用作偏移量参考的位置。
例:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
char arr[200] = { 0 };
char ch=fgetc(pf);
printf("%c", ch);
int i = 0;
fseek(pf, 4, SEEK_CUR);
ch=fgetc(pf);
printf("%c", ch);
fclose(pf);
pf = NULL;
return 0;
}
2.ftell
ftell函数可以获取流中的当前位置,stream指向标识流的FILE对象的指针。
例:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
char arr[200] = { 0 };
char ch=fgetc(pf);
printf("%c\n", ch);
int i = 0;
fseek(pf, 4, SEEK_CUR);
ch=fgetc(pf);
printf("%c\n", ch);
printf("%d\n", ftell(pf));
fclose(pf);
pf = NULL;
return 0;
}
3.rewind
rewind函数可以将流的位置设置为开头,也就是将光标重置到开头的位置。
例:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
char arr[200] = { 0 };
char ch=fgetc(pf);
printf("%c\n", ch);
int i = 0;
fseek(pf, 4, SEEK_CUR);
ch=fgetc(pf);
printf("%c\n", ch);
rewind(pf);
ch = fgetc(pf);
printf("%c\n", ch);
fclose(pf);
pf = NULL;
return 0;
}
7. 文件读取结束的判定
在读取文件结束的时候,想要知道读取结束的原因的话,可以使用feof函数和ferror函数进行判断。
当打开一个六的时候,这个六上会有两个标记值:
1.是否遇到文件末尾,我们使用foef函数进行判断
2.是否发生错误,我们使用ferror进行判断
例:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
int main()
{
int c; // 注意:int,⾮char,要求处理EOF
FILE* fp = fopen("test.txt", "r");
if (!fp) {
perror("File opening failed");
return EXIT_FAILURE;
}
//fgetc 当读取失败的时候或者遇到⽂件结束的时候,都会返回EOF
while ((c = fgetc(fp)) != EOF)
{
putchar(c);
}
//判断是什么原因结束的
if (ferror(fp))
puts("I/O error when reading");
else if (feof(fp))
puts("End of file reached successfully");
fclose(fp);
fp = NULL;
}
8. 文件缓冲区
ANSIC 标准采⽤“缓冲⽂件系统” 处理的数据⽂件的,所谓缓冲⽂件系统是指系统⾃动地在内存中为 程序中每⼀个正在使⽤的⽂件开辟⼀块“⽂件缓冲区”。从内存向磁盘输出数据会先送到内存中的缓 冲区,装满缓冲区后才⼀起送到磁盘上。如果从磁盘向计算机读⼊数据,则从磁盘⽂件中读取数据输 ⼊到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓 冲区的⼤⼩根据C编译系统决定的。
#define _CRT_SECURE_NO_WARNINGS 1
#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;
}