文件管理
- 一、为什么使用文件
- 二、什么是文件
- 2.1程序文件
- 2.2数据文件
- 2.3文件名
- 三、文件的打开和关闭
- 3.1文件指针
- 3.2文件的打开和关闭
- 四、文件的顺序读写
- 4.1顺序读写函数介绍
- 4.1.1fgetc
- 4.1.2fputc
- 4.1.3fgets
- 4.1.4fputs
- 4.1.5fscanf
- 4.1.6fprintf
- 4.1.7fread
- 4.1.8fwrite
- 4.2对比一组函数
- 4.2.1sscanf
- 4.2.2sprintf
- 4.2.3函数对比
- 五、文件的随机读写
- 5.1fseek
- 5.2ftell
- 5.3rewind
- 六、文本文件和二进制文件
- 七、文件读取结束的判定
- 7.1feof & perror
- 八、文件缓冲区
一、为什么使用文件
我们前面已经介绍了结构体,也学习了通讯录,存在一个问题就是,每次通讯录程序退出,之前写好的信息都没了,下次进入通讯录,又得重新输入信息,这样的通讯录使用起来就很难受。那么怎样可以使数据保留下来,这里就涉及到了数据持久化的问题,数据持久化的方法有,把数据存放在磁盘文件、存放到数据库等方式。
这里使用文件我们可以将数据直接存放在电脑的硬盘上,文件在我们的硬盘上(C盘,D盘等),将数据存放到文件当中,就是存放到硬盘上,做到了数据的持久化。
二、什么是文件
磁盘上的文件是文件
但是在程序设计中,我们一般谈的文件有两种:程序文件、数据文件(从文件功能的角度来分类的)。
2.1程序文件
包括源程序文件(后缀为.c),目标文件(Windows环境后缀为.obj、Linux环境下为.o),可执行程序(Windows环境后缀为.exe)
2.2数据文件
文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件。
本章讨论的是数据文件。
在以前各章所处理数据的输入输出都是以终端为对象的,即从终端的键盘输入数据,运行结果显示到显示器上。
其实有时候我们会把信息输出到磁盘上,当需要的时候再从磁盘上把数据读取到内存中使用,这里处理的就是磁盘上文件。
2.3文件名
一个文件要有一个唯一的文件标识,以便用户识别和引用。
文件名包含3部分:文件路径+文件名主干+文件后缀
例如:c:\code\test.txt 为了方便起见,文件标识常被称为文件名。
三、文件的打开和关闭
3.1文件指针
缓冲文件系统中,关键的概念是"文件类型指针",简称"文件指针".
每个被使用的文件都在内存中开辟一个相应的文件信息区,用来存放文件的相关信息(如文件的名字,文件状态及文件当前的位置等)。这些信息是保存在一个结构体变量中的。该结构体类型是由系统声明的,取名FILE。
每当打开一个文件的时候,系统会根据文件的情况自动创建一个FILE结构的变量,并填充其中的信息,FILE是结构体类型,那用该结构体类型定义的文件指针就是用来维护着文件信息区(即FILE结构体变量),文件信息区会返回起始地址由文件指针来维护,通过访问文件信息区的内容就能访问该文件的内容。也就是说通过文件指针变量能够找到与它关联的文件。
以下是VS2022编译环境提供的stdio.h头文件中的文件类型申明:
typedef struct _ibouf
{
char* ptr;
int buffer;
}FILE;
FILE* p;//文件指针变量
3.2文件的打开和关闭
文件在读写之前应该先打开文件,在使用结束之后应该关闭文件。
在编写程序的时候,打开文件的同时就会返回一个FILE*的指针变量指向该文件,建立了指针和文件的关系。
ANSIC规定用fopen函数来打开文件,fclose来关闭文件。
//打开文件
FILE* open(const char* filename, const char* mode);//filename文件名,mode文件打开方式
//关闭文件
int fclose(FILE* stream);
文件的打开方式:
这里的输入数据指的是文件/屏幕中的数据输入到程序上,即从文件/屏幕读取数据。输出数据指的是程序中的数据输出到文件/屏幕中,即向文件/屏幕写入数据。带有r的形式去读取文件,而文件本身不存在时,就会出错,不会建立一个新的文件,会返回NULL,而去写或追加数据到文件中,而文件不存在时,都会自动建立一个新的文件。
示例:
#include <stdio.h>
int main()
{
FILE* pFile;
//打开文件
pFile = fopen("myfile.txt", "w");
//文件操作
if (pFile != NULL)
{
fputs("fopen example", pFile);
//关闭文件
fclose(pFile);
}
return 0;
}
以w的形式向文件myfile.txt写入数据,目前我们原本的路径中没有这个文件,而文件名以这种形式写表明的意思执行程序时,在当前程序所在的目录下创建一个myfile.txt的文件,这种文件形式是绝对路径(当前目录下)
还有一种是指定在哪创建文件就在哪的方式叫做相对路径(任意目录下),例如:c:\code\test.txt。
当然fopen只是打开文件,这只是一种打开文件形式,但还不是真的写,fputs在文件打开的基础上才能真正的写入数据。
程序未执行前,当前目录下是没有该文件,程序执行后,就会生成一个改文件。如何找到当前目录?
找到打开后
程序执行后
以记事本的形式打开改文件,就可以看到刚刚写入的内容。
接下来,我们进行讨论类似fputs读写函数的介绍。
四、文件的顺序读写
4.1顺序读写函数介绍
这里涉及到流,什么是流?
流是一种抽象概念,例如:水流,表示水的运动状态,沿着某一路径运动的现象。这里的流用来表示数据流,表示对数据的输入输出进行处理的方式,使得程序可以以连续的方式读取或写入数据。
C语言程序只要运行起来,默认就打开3个流:
1.标准输入流stdin–从键盘输入
2.标准输出流stdout–输出到屏幕上、终端,主要是给用户看
3.标准错误流stderr–用于输出程序运行时的错误和诊断信息的一种特殊流
所有输入流、输出流包含标准输入、输出流,文件输入、输出流等。
4.1.1fgetc
fgetc函数用于从指定的流(stream)中获取一个字符。它返回当前被内部文件位置指示器指向的字符,并将该指示器前移至下一个字符。即读取完一个字符后,光标会移向后一位,等待下一次读取
参数说明:
stream:要读取的文件流。
返回值:
函数返回值为读取的字符的整数表示。如果发生错误或到达文件末尾,返回值为EOF(End of File)。并设置流的文件结束指示器(feof)。如果发生读取错误,函数将返回EOF并设置流的错误指示器(ferror)。
#include <stdio.h>
int main()
{
FILE* pFile;
//打开文件
pFile = fopen("myfile.txt", "r");//从文件中读取数据
//文件操作
if (pFile != NULL)
{
int c = fgetc(pFile);
printf("%c", c);
//关闭文件
fclose(pFile);
}
return 0;
}
运行结果:
#include <stdio.h>
int main()
{
FILE* pFile;
//打开文件
pFile = fopen("myfile.txt", "r");
//文件操作
if (pFile != NULL)
{
char pc[10]="abcdefg";
int num = 3;
while (num--)
{
int c = fgetc(pFile);//将数组中的内容写入文件流中
printf("%c", c);
}
//关闭文件
fclose(pFile);
}
return 0;
}
运行结果:
4.1.2fputc
fputc()是C语言标准库函数之一,用于将一个字符写入文件流中,并在写入后将流的位置指示器自动前进一位。
参数说明:
character:要写入的字符。
stream:文件流指针,指向要写入的文件。
返回值:
如果成功写入字符,则返回写入的字符的无符号值(unsigned char)。
如果发生错误,则返回EOF。
#include <stdio.h>
int main()
{
FILE* pFile;
//打开文件
pFile = fopen("myfile.txt", "w");
//文件操作
if (pFile != NULL)
{
int c = fputc('y', pFile);//将字符y写入到pFile所指向的文件中去
printf("%c", c);
//关闭文件
fclose(pFile);
}
return 0;
}
执行程序前:
执行结果后:
4.1.3fgets
fgets()是C语言标准库函数之一,用于从文件流中读取字符串并存储到字符数组中。
参数说明:
str:指向目标字符数组的指针,用于存储读取的字符串。
num:要读取的最大字符数(包括末尾的空字符),通常是目标字符数组的长度。
stream:文件流指针,指向要读取的文件。
返回值:
如果成功读取字符串,则返回指向目标字符数组的指针(即str参数)。
如果发生错误或已到达文件末尾,则返回NULL。
#include <stdio.h>
int main()
{
FILE* pFile;
//打开文件
pFile = fopen("myfile.txt", "r");
//文件操作
if (pFile != NULL)
{
char pc[10];
char* c = fgets(pc,6, pFile);//从文件流中读取6个字符到数组pc,返回指向pc的指针
printf("%s", pc);
//关闭文件
fclose(pFile);
}
return 0;
}
文件中本身内容:
运行结果:
4.1.4fputs
fputs()是C语言标准库函数之一,用于将字符串写入文件流中。
参数说明:
str:指向要写入的字符串的指针。
stream:文件流指针,指向要写入的文件。
返回值:
如果成功写入字符串,则返回非负值。
如果发生错误,则返回EOF。
#include <stdio.h>
int main()
{
FILE* pFile;
//打开文件
pFile = fopen("myfile.txt", "w");
//文件操作
if (pFile != NULL)
{
char pc[10]="abcdefg";
fputs(pc, pFile);//将数组中的内容写入文件流中
printf("%s", pc);
//关闭文件
fclose(pFile);
}
return 0;
}
运行前:
运行后:
4.1.5fscanf
fscanf()是C语言标准库函数之一,用于从文件流中按格式读取数据并存储到指定的变量中。
参数说明:
stream:文件流指针,指向要读取的文件。
format:格式字符串,指定了要读取的数据的格式。
…:可变参数列表,指定了要读取的数据存储的位置。
返回值:
如果成功读取数据,则返回成功读取的参数个数。
如果发生错误或已到达文件末尾,则返回EOF。
空格会被作为分隔符,跳过输入时的空白字符(包括空格、制表符和换行符),直到找到一个非空白字符或达到文件末尾。
#include <stdio.h>
int main() {
int num1;
char str[100];
FILE* file = fopen("example.txt", "r");
if (file == NULL) {
printf("文件打开失败");
return 1;
}
int result = fscanf(file, "%d %s", &num1, str);//从文件中以%d %s的格式读取数据到num1,str中
if (result != 2) {
printf("读取文件失败");
fclose(file);
return 1;
}
printf("成功读取整数:%d", num1);
printf("成功读取字符串:%s", str);
fclose(file);
return 0;
}
执行结果:
4.1.6fprintf
fprintf()是C语言标准库函数之一,用于将格式化的数据写入文件流中。
参数:
stream: FILE类型的指针,表示要写入的目标流。可以是标准输出流(如stdout)或文件流(通过fopen函数打开的文件流),甚至可以是网络连接的套接字。
format: const char类型的指针,表示格式化的字符串,指定了数据的输出格式和位置。它类似于printf函数中的格式化字符串。
…: 可变参数列表,用于填充格式化字符串中的格式控制符。这些参数的类型和顺序必须与格式化字符串中的格式控制符相匹配。
返回值:
int类型的返回值表示成功写入的字符数,如果出现错误则返回负值。常见的返回值有:
正整数:表示成功写入的字符数,其中正数表示已写入的字符数。
0:表示没有成功写入任何字符。
负数:表示发生了错误,具体的负值代表不同的错误类型。
#include <stdio.h>
int main() {
int num1=2;
char str[6]= "abcde";
FILE* file = fopen("myfile.txt", "w");
if (file != NULL)
{
int a = fprintf(file, "%d %s", num1, str);//以%d %s的形式向文件输入数据
fclose(file);
}
return 0;
}
运行结果:
4.1.7fread
fread函数是C语言标准库中的一个输入函数,用于从文件流中读取数据块。
参数说明:
ptr:指向要存储读取数据的内存块的指针。
size:每个元素的大小(以字节为单位)。
count:要读取的元素个数。
stream:要读取的文件流。
返回值:
函数返回值为实际成功读取的元素个数。如果发生错误或到达文件末尾,返回值可能小于count。
#include <stdio.h>
int main()
{
FILE* file = fopen("myfile.txt", "rb");//以二进制形式读取
if (file != NULL)
{
char buffer[10] = { 0 };
fread(buffer, sizeof(char), 4, file);//从文件流中读取4个大小为sizeof(char)的元素到buffer中
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%c ", buffer[i]);
}
fclose(file);
}
return 0;
}
运行结果:
4.1.8fwrite
fwrite函数用于将数据块写入流中。
参数说明:
ptr:指向要写入数据的内存块的指针。
size:每个元素的大小(以字节为单位)。
count:要写入的元素数量。
stream:表示流的 FILE 对象的指针。
返回值:
返回值是一个 size_t 类型的值,表示成功写入的元素个数。如果返回值等于 count,则表示所有元素都成功写入;如果返回值小于 count,则表示部分或者没有元素被写入。
#include <stdio.h>
int main()
{
FILE* file = fopen("myfile.txt", "wb");//以二进制形式写入
if (file != NULL)
{
char buffer[10] = "abcdef";
fwrite(buffer, sizeof(char), 6, file);//从buffer中取出6个大小为sizeof(char)的元素到文件流中
fclose(file);
}
return 0;
}
4.2对比一组函数
4.2.1sscanf
参数说明:
s:要从中读取数据的字符串。
format:指定数据格式的格式字符串。
其他额外的参数:用来接收解析出的数据
返回值:
函数返回一个 int 类型的值,表示成功解析并存储的参数个数。如果返回值小于期望的参数个数,则可能存在解析错误。
用法跟fscanf差不多,但不适用于标准输入流
#include <stdio.h>
int main() {
char name[20];
int age;
float height;
char str[] = "John 25 1.75";
// 使用 sscanf 解析字符串
int num = sscanf(str, "%s %d %f", name, &age, &height);//从str读取字符串到name,age,height中
if (num == 3)
{
printf("Name: %s", name);
printf("Age: %d", age);
printf("Height: %.2f ", height);
}
else
{
printf("Failed to parse the input string. ");
}
return 0;
}
运行结果:
4.2.2sprintf
sprintf 函数用于将格式化的数据写入字符串中,而不是打印到标准输出。它与 printf 函数非常相似,但是将结果存储在提供的字符串缓冲区中。
参数说明:
str:指向要写入的字符串缓冲区的指针。
format:格式化字符串,指定输出的格式。
…:额外的参数,用于填充格式化字符串中的占位符。
返回值:
函数的返回值是写入字符串的字符数(不包括终止空字符 \0)。如果发生错误,则返回负数。
#include <stdio.h>
int main() {
char buffer[50];
int value = 123;
float floatValue = 3.14;
// 使用 sprintf 将格式化数据写入字符串缓冲区
int numWritten = sprintf(buffer, "Value: %d, Pi: %.2f", value, floatValue);//将格式化的数据写入buffer中
printf("Formatted string: %s\n", buffer);
printf("Number of characters written: %d", numWritten);
return 0;
}
4.2.3函数对比
scanf/fscanf/sscanf
输入源:
scanf函数从标准输入流(stdin)中读取数据。
fscanf函数从指定的文件流(FILE *stream)中读取数据。
sscanf函数从一个字符串(const char *str)中读取数据。
函数原型:
scanf的函数原型是int scanf(const char *format, …)
fscanf的函数原型是int fscanf(FILE *stream, const char *format, …)
sscanf的函数原型是int sscanf(const char *str, const char *format, …)
功能:
scanf、fscanf和sscanf都用于按照指定格式解析输入数据,并将解析结果存储到对应的变量中。
它们都返回成功匹配和赋值的参数个数。
输入错误处理:
scanf、fscanf和sscanf在解析输入数据时,如果遇到与指定格式不匹配的情况,会停止解析,并返回已成功匹配和赋值的参数个数。
可以通过检查返回值来判断是否成功解析了预期的参数个数。
printf/fprintf/sprintf
输出位置:
printf:输出到标准输出设备(通常是控制台)。
fprintf:输出到指定的文件。
sprintf:输出到字符数组作为结果保存。
函数原型:
printf:int printf(const char *format, …)
fprintf:int fprintf(FILE *stream, const char *format, …)
sprintf:int sprintf(char *str, const char *format, …)
功能和用途:
printf:主要用于将格式化的数据输出到控制台供用户查看。
fprintf:主要用于将格式化的数据输出到文件,可以用于日志记录等。
sprintf:主要用于将格式化的数据转换为字符串并保存在字符数组中,可以用于字符串构建、打印到日志等。
参数和返回值:
printf 和 fprintf:接受格式化字符串作为第一个参数,后续参数为要填充到格式化字符串中的值。返回成功输出的字符数,若发生错误则返回负值。
sprintf:接受一个字符数组作为第一个参数,后续参数与 printf 相同。返回成功输出的字符数,不包括结尾的空字符’\0’。若发生错误则返回负值。
输出结果处理:
printf:直接将结果输出到标准输出设备,不保存输出结果。
fprintf:直接将结果输出到指定文件,不保存输出结果。
sprintf:将结果输出到字符数组中并保存,可以通过访问该字符数组获取输出结果。
五、文件的随机读写
5.1fseek
fseek 函数用于设置流的位置指示器到一个新的位置。
参数说明:
stream:指向要定位的流的指针。
offset:偏移量,用于指定新位置相对于起始位置的偏移量。
origin:指定起始位置的参考点。可选值有:
SEEK_SET:从文件开头开始计算偏移量。
SEEK_CUR:从当前位置开始计算偏移量。
SEEK_END:从文件末尾开始计算偏移量。
返回值:
fseek 函数的返回值是一个整数,表示操作的成功与否。
#include <stdio.h>
int main()
{
FILE* pFile;
pFile = fopen("myfile.txt", "wb");
fputs("this is an apple.", pFile);
fseek(pFile, 9, SEEK_SET);//起始位置文件开头,偏移量为9
fputs(" sam", pFile);
fclose(pFile);
return 0;
}
运行结果:
5.2ftell
ftell 函数的作用是获取流的当前位置指示器的值。
参数:
stream:指向 FILE 结构的指针,代表要获取当前位置指示器值的流对象。
返回值:
long int:表示当前位置指示器的值。对于二进制流,该值是从文件开头算起的字节数;对于文本流,该值可能没有实际意义。
#include <stdio.h>
int main()
{
FILE* pFile;
long size;
pFile = fopen("myfile.txt", "rb");
if (pFile == NULL)
{
perror("pFile");
}
else
{
fseek(pFile, 0, SEEK_END);//起始位置为文件末尾,偏移量为0
size = ftell(pFile);
fclose(pFile);
printf("%ld", size);
}
return 0;
}
运行结果:
5.3rewind
rewind 函数的作用是将流的位置指示器设置到文件开头。
stream:要操作的流的指针,通常是使用 fopen 函数返回的文件指针。
没有特定的返回值
int main()
{
char n;
FILE* pFile;
char buffer[27];
pFile = fopen("myfile.txt", "w+");//建立一个可读可写的文件
for (n = 'A'; n <= 'z'; n++)
fputc(n, pFile);//将26个字母输入到文件中去
rewind(pFile);//光标移到了最后一位,调用rewind使光标回到起始位置
fread(buffer, 1, 26, pFile);//读取文件中的字符到buffer数组中
fclose(pFile);
buffer[26] = '\0';//在数组末尾放入'\0'以便打印
puts(buffer);//输出数组中的内容
return 0;
}
运行结果:
六、文本文件和二进制文件
数据文件被称为文本文件和二进制文件。
数据在内存中以二进制形式存储,如果不加以转换输出到外存,就是二进制文件。
如果在外存上是以ASCLL码字符的形式存储那就是文本文件
#include <stdio.h>
int main()
{
int a = 10000;;
FILE* pFile;
pFile = fopen("myfile.txt", "wb");//打开一个文件,用于存储二进制的数据
if (pFile != NULL)
{
fwrite(&a, 4, 1, pFile);
fclose(pFile);
}
return 0;
}
当我们写入的数据是以二进制的形式存储,这个时候打开文件发现它是乱码的,明明写入的a是10000,但却是看不懂的符号,因为我们目前打开的文件是文本文件,不能够解读二进制数据。那么我们可以通过编译器去解读二进制文件。
在源文件中将我们的现有项文件导过来
右击该文件,选择打开方式,选择二进制编辑器
这个时候就会解读二进制数据,会以十六进制显示该数据
七、文件读取结束的判定
7.1feof & perror
feof() 函数用于检查给定文件流的文件结束标志是否已设置。它接受一个指向 FILE 对象的指针作为参数,并在文件结束标志被设置时返回一个非零值,表示文件已完全读取。
perror()函数用于将errno的值解释为错误消息,并将其打印到stderr(标准错误输出流,通常是控制台)
因此不能直接使用feof来判断文件的是否结束,因为文件可能因为遇到错误而导致读取结束,这时就要用perror来判断。
#include <stdio.h>
int main()
{
int c;
FILE* fp = fopen("myfile.txt", "r");
if (!fp)
{
perror("fp");
return -1;
}
//fgetc当读取失败的时候或者遇到文件结束的时候,就会返回EOF
while ((c = fgetc(fp)) != EOF)
{
putchar(c);
}
//判断是什么原因结束的
if (ferror(fp))
puts("\n遇到错误读取结束\n");
else if (feof(fp))
puts("\n遇到文件末尾读取结束\n");
}
运行结果:
八、文件缓冲区
ANSIC标准采用"缓冲文件系统"处理的数据文件的,所谓缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块"文件缓冲区"。从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上。如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓冲区的大小根据c编译系统决定的。
如图:
#include <stdio.h>
int main()
{
FILE* pf = fopen("myfile.txt", "w");
fputs("abcdef", pf);//此时才将数据写到输出缓冲区上
printf("睡眠10秒-已经写数据了,打开myfile.txt文件,发现文件没有内容\n");
Sleep(10000);//等待10秒钟,发现在文件上并没有数据,说明数据还在输出缓冲区上
printf("刷新缓冲区\n");
fflush(pf);//10过后,刷新缓冲区时,才将输出缓冲区的数据写到文件(磁盘)
printf("再睡眠10秒-此时,再次打开myfile.txt文件,文件有内容了\n");
Sleep(10000);//再过10过后关闭文件
fclose(pf);
pf = NULL;
return 0;
}
运行结果:
结论:
因为有缓冲区的存在,C语言在操作文件的时候,需要做刷新缓冲区或者在文件操作结束的时候关闭文件。如果不做,可能导致读写文件的问题。
end~