前言
上节我们学习了文件操作,因为文件操作的内容比较多,我把文件操作的博客拆分成两节来进行讲解,那么事不宜迟,我们正式的开始今天的学习
文件的顺序读写(2)
fprintf、fscanf函数的使用
fprintf是格式化的输出函数,具体使用方法如下:
通过比较,我们发现 fprintf 函数和 printf 函数的参数及其相像,仅仅在第一个参数上面有区别,fprintf 函数相较于 printf 函数多出了一个文件流的参数,下面我们来试着使用一下 fprintf 函数:
#include <stdio.h>
struct S
{
char name[20];
int age;
float score;
};
int main(void)
{
struct S s = { "张三",20,65.5f };
//把s里面的数据存在文件中
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;
return 0;
}
通过分析 test.txt 文件,我们可以知道:
写文件是以文本的形式写入的,而非二进制文件形式
我们可以通过 fprintf 函数将数据写入文件,那么我们应该用什么方式把文件读出来呢?
答案是使用 fscanf 函数,我们来看看 fscanf 函数的具体使用方式:
我们通过对比也可以知道:fscanf 函数和 scanf 函数参数相似度也很高,也只缺少一个流参数,下面我们来尝试使用一下:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
struct S
{
char name[20];
int age;
float score;
};
int main(void)
{
struct S s = { 0 };
//在文件中提取数据放入s
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读文件
fscanf(pf, "%s %d %f", s.name, &s.age, &s.score);
//打印在屏幕上
printf("%s %d %f\n", s.name, s.age, s.score);
fclose(pf);
pf = NULL;
return 0;
}
我们知道,fprintf 和 fscanf 函数适用于所有输出流和所有输入流,那么我们该怎么印证这一点呢?
我们可以通过尝试使用 fprintf 将数据输出到屏幕上打印,我们来尝试一下:
struct S
{
char name[20];
int age;
float score;
};
int main(void)
{
struct S s = { 0 };
//在文件中提取数据放入s
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读文件
fscanf(pf, "%s %d %f", s.name, &s.age, &s.score);
//打印在屏幕上
fprintf(stdout, "%s %d %f\n", s.name, s.age, s.score);
fclose(pf);
pf = NULL;
return 0;
}
scanf / fscanf / sscanf && printf / fprintf /sprintf
下面我们来进行一组函数的对比:
scanf / fscanf / sscanf
printf / fprintf /sprintf
sscanf 函数和 sprintf 函数
我们来了解一下 sscanf 函数和 sprintf 函数:
sprintf 函数:将格式化的数据写入一个字符串,也就是说,把格式化的数据转化成字符串
struct S
{
char name[20];
int age;
float score;
};
int main(void)
{
char buf[200] = { 0 };
struct S s = { "张三",20,65.5f };
sprintf(buf, "%s %d %f", s.name, s.age, s.score);
printf("%s\n", buf);
return 0;
}
sscanf 函数:在字符串中读取格式化的数据
struct S
{
char name[20];
int age;
float score;
};
int main(void)
{
char buf[200] = { 0 };
struct S s = { "张三",20,65.5f };
sprintf(buf, "%s %d %f", s.name, s.age, s.score);
printf("1:以字符串的形式打印 %s\n", buf);
struct S t = { 0 };
sscanf(buf, "%s %d %f", t.name, &t.age, &t.score);
printf("2:按照格式打印 %s %d %f\n", t.name, t.age, t.score);
return 0;
}
(因为两个函数的使用与 printf 函数和 scanf 函数差不多,所有没有做过多的讲解,仅仅呈现代码来带大家了解其使用过程,若不能够理解,可以自行去 http://cplusplus.com 进行更细致入微的了解)
函数对比
通过之前的学习,我们可以知道:
scanf 函数:从标准输入流(键盘)上读取格式化的数据
fscanf 函数:从所有输入流上读取格式化的数据
sscanf 函数:在字符串中读取格式化的数据
printf 函数:把数据以格式化的形式打印在标准输出流上
fprintf 函数:把数据以格式化的形式打印在指定的输出流上
sprintf 函数:将格式化的数据写入一个字符串,也就是说,把格式化的数据转化成字符串
fread 函数和 fwrite 函数
我们可以知道:
ptr 是指向需要写入进数据的数组
size 是写入数据类型所占用的字节,例如:整型数组的 size 就为 4
count 是写入元素的个数
stream 是流
具体使用方法如下:
int main(void)
{
int arr[] = { 1,2,3,4,5 };
FILE* pf = fopen("test.txt", "wb");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//写数据
int sz = sizeof(arr) / sizeof(arr[0]);
fwrite(arr, sizeof(arr[0]), sz, pf);
fclose(pf);
pf = NULL;
return 0;
}
我们此时打开 test.txt 文件,看看里面是否有我们写入的内容:
打开了 test.txt 文件,虽然里面存有了文件,但是我们发现里面存储的是乱码,这是为什么呢?
因为 fwrite 函数是针对二进制数据输出的函数,当我们用文本的形式打开时,就会显示乱码,当我们以二进制的形式读取文件,就能出现我们写入的数据,此时我们需要使用 fread 函数:
通过比较我们可以发现,fread 函数和 fwrite 函数的参数是如出一辙的,那么接下来我们使用 fread 函数将二进制文件里面的数据读出来:
int main(void)
{
int arr[5] = { 0 };
FILE* pf = fopen("test.txt", "rb");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读取二进制数据
int sz = sizeof(arr) / sizeof(arr[0]);
fread(arr, sizeof(arr[0]), sz, pf);
for (int i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
fclose(pf);
pf = NULL;
return 0;
}
因为 fread 的返回值类型是size_t 那么 fread 函数一次会读取 count 个数据并且返回,如果数组里面的元素大于 count 个,那么 fread 函数将会多次读取,直到数组里面的元素全部被读取完,这样我们就可以更好的读取未知数组元数个数的数组里面的数据,因此我们可以这样改写代码:
int main(void)
{
int arr[5] = { 0 };
FILE* pf = fopen("test.txt", "rb");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读取二进制数据
int sz = sizeof(arr) / sizeof(arr[0]);
int i = 0;
while (fread(arr + i, sizeof(arr[0]), 1, pf))
{
printf("%d ", arr[i]);
}
fclose(pf);
pf = NULL;
return 0;
}
文件的随机读写
fseek
fseek 函数可以根据文件指针的偏移量和位置来定位文件指针,也就是定位文件的内容里面的光标
变量 offset 就是偏移量
变量 origin 就是文件的起始位置,其中文件的起始位置有三种:
SEEK_SET:是文件的起始位置
SEEK_CUR:是文件指针当前的位置
SEEK_END:是文件的末尾
下面我们来使用一下:
int main(void)
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读文件
int ch = fgetc(pf);
printf("%c\n", ch);
return 0;
}
首先是没有使用 fseek 函数的时候
每次顺序读取以后,光标都会自动向后移动一位
当我们使用 fseek 函数的时候,此时我们使用SEEK_CUR,并且偏移量为4
int main(void)
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读文件
int ch = fgetc(pf);
printf("%c\n", ch);
fseek(pf, 4, SEEK_CUR);
ch = fgetc(pf);
printf("%c\n", ch);
return 0;
}
此时我们读到了f,我们还可以用其他方法读到f:
int main(void)
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读文件
int ch = fgetc(pf);
printf("%c\n", ch);
fseek(pf, 5, SEEK_SET);
ch = fgetc(pf);
printf("%c\n", ch);
return 0;
}
int main(void)
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读文件
int ch = fgetc(pf);
printf("%c\n", ch);
fseek(pf, -4, SEEK_END);
ch = fgetc(pf);
printf("%c\n", ch);
return 0;
}
这三种方法我们都可以得到f
ftell函数
ftell函数可以返回文件指针相对于起始位置的偏移量
int main(void)
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读文件
int ch = fgetc(pf);
printf("%c\n", ch);
fseek(pf, -4, SEEK_END);
ch = fgetc(pf);
printf("%c\n", ch);
printf("%d\n", ftell(pf));
return 0;
}
rewind函数
rewind 函数可以让文件指针回到文件的起始位置
int main(void)
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读文件
int ch = fgetc(pf);
printf("%c\n", ch);
fseek(pf, -4, SEEK_END);
ch = fgetc(pf);
printf("%c\n", ch);
rewind(pf);
ch = fgetc(pf);
printf("%c\n", ch);
return 0;
}
文件读取结束的判断
被错误使用的feof
我们在读取文件的时候,不能用eof函数的返回值来直接判断文件是否读取结束
feof 函数的作用是当文件结束读取的时候,判断读取结束的原因是否为遇到文件末尾
文件读取结束有以下几种原因:
1.遇到了文件末尾-----feof
2.读取的时候发生错误-----ferror
当我们打开一个流的时候,每一个流上都有两个标记值:
1.是否遇到文件末尾---------feof
2.是否发生错误-----------ferror
文本文件在读取的时候,我们可以通过返回值来判断读取是否结束:
1.fgetc 判断返回值是否为EOF(如果读取正常,返回读取字符的ASCII码值,如果遇到文件末尾或者发生错误,则返回EOF)
2.fgets 判断返回值是否为 NULL(如果读取正常,返回存储字符串的字符数组的地址,如果遇到文件末尾或者读取错误,返回NULL)
3.二进制文件读取结束判断返回值是否小于实际要读的个数(fread)
下面我们来运用所学的知识完成文件与文件间的内容拷贝:
int main(void)
{
FILE* pfin = fopen("test1.txt", "r");
if (pfin == NULL)
{
perror("fopen1");
return 1;
}
FILE* pfout = fopen("test2.txt", "w");
if (pfout == NULL)
{
fclose(pfin);
perror("fopen2");
return 1;
}
//读文件和写文件
int ch = 0;
while ((ch = fgetc(pfin)) != EOF)
{
fputc(ch, pfout);
}
fclose(pfin);
pfin = NULL;
fclose(pfout);
pfout = NULL;
return 0;
}
代码比较简单,仅仅只作演示,不作讲解
文件缓冲区
在数据从内存中写入硬盘的时候,需要经过文件缓冲区,下面是内存缓冲区的官方解释:
ANSIC标准采⽤“缓冲⽂件系统”处理的数据⽂件的,所谓缓冲⽂件系统是指系统⾃动地在内存中为 程序中每⼀个正在使⽤的⽂件开辟⼀块“⽂件缓冲区”。从内存向磁盘输出数据会先送到内存中的缓 冲区,装满缓冲区后才⼀起送到磁盘上。如果从磁盘向计算机读⼊数据,则从磁盘⽂件中读取数据输 ⼊到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓 冲区的⼤⼩根据C编译系统决定的。
我们在把程序数据区的内容写到硬盘里面去的时候,我们需要先填满缓冲区,在缓冲区满了以后才会写入到硬盘里面去,若是没有内存缓冲区,系统效率就会很低,会频繁的被打断。fflash 函数可以刷新缓冲区,直接将缓冲区里面现有的代码写入硬盘中,下面我们来写一个代码来验证这一点:
#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;
}
(因为我是VS2022,没有办法演示,大家知道怎么使用以及文件缓冲区的概念就可以了)
所以我们可以得出以下结论:
因为有缓冲区的存在,C语⾔在操作⽂件的时候,需要做刷新缓冲区或者在文件操作结束的时候关闭文件。 如果不做,可能导致读写⽂件的问题。
结尾
那么文件操作的所有内容就到此结束了,谢谢您的浏览!!!