C语言文件操作(下)
5. scanf/fscanf/sscanf printf/fprintf/sprintf 两组函数对比
C语言中有这样两组函数:
scanf,fscanf,sscanf
和
printf/fprintf/sprintf
大家看它们是不是长的很像啊,那它们之间有什么区别和不同吗?
相信scanf和printf大家应该都比较熟悉了,那fscanf和fprintf其实在上面的内容中我们也学习了,现在应该就剩下sscanf 和sprintf 大家比较陌生了。
下面我们就一起来学习一下:
5.1 sprintf
我们先来学习一下sprintf
是干什么的?
我们已经知道
fprintf
是将格式化的数据写入文件流。对比一下它们。还是很相似的。
只有一个参数不同,fprintf
是将格式化的数据写入文件流,所以它的第一个参数是文件指针。
而sprintf
的作用其实是将格式化的数据写入字符串,所以第一个参数是一个字符指针,它可以指向一个字符数组,字符数组是可以存放一个字符串的。
其余细节和fprintf
也是比较一样的。
那就写个代码练习练习:
我们尝试把一个结构体数据格式化的写入到一个字符串中,并打印出来看看:
#include <stdio.h>
struct S
{
char name[20];
int age;
float score;
};
int main()
{
char buf[100] = { 0 };
struct S s = { "zhangsan",20,95.5f };
sprintf(buf, "%s %d %f", s.name, s.age, s.score);
printf("%s", buf);
return 0;
}
那就是这样一段代码,将结构体变量s中的数据,格式化的写入到字符数组buf中,我们运行看看:
就打印出来了。
那能不能把字符串里的内容再还原到一个结构体变量中呢?
当然可以。用
sscanf
5.2 sscanf
我们可以再来对比一下sscanf
和fscanf
:
它们还是第一个参数不同:
fscanf
是从流中读取格式化数据,参数是文件指针。
而sscanf
是从字符串中读取格式化数据,所以参数还是字符指针。
好,那我们接着刚才上面写的代码,把写入字符串的数据再还原到一个结构体变量中。
int main()
{
char buf[100] = { 0 };
struct S s = { "zhangsan",20,95.5f };
sprintf(buf, "%s %d %f\n", s.name, s.age, s.score);
printf("%s", buf);
struct S tmp = { 0 };
sscanf(buf,"%s %d %f", tmp.name, &(tmp.age), &(tmp.score));
printf("%s %d %f\n", tmp.name, tmp.age, tmp.score);
return 0;
}
结构体变量tmp
我们初始化为0,然后把从字符串buf
中读取的格式化数据放到tmp
打印出来
两次打印结果应该是一样的:
没有问题。
5.3 总结
那现在我们来对这几个函数:
scanf/fscanf/sscanf printf/fprintf/sprintf
做一个小小的总结.
scanf和printf
scanf:从标准输入流(stdin )读取格式化数据。
stdin是标准输入,一般指键盘输入到缓冲区里的东西
printf:将格式化数据打印到标准输出流(stdout)
标准输出流是应用程序输出的默认目标。在大多数系统中,默认情况下,它通常定向到文本控制台(通常在屏幕上)。
fscanf和fprintf
fscanf:从流(文件/stdin)中读取格式化数据
fprintf:将格式化数据写入流(文件/stdout)
它们两个适用于所有的输入输出流。
sscanf 和sprintf
sscanf :从字符串中读取格式化数据
sprintf :将格式化数据写入字符串
相信现在大家就对这几个函数有所认识了。
6. 文件的随机读写
我们上面刚刚讲过了文件的顺序读写。
为什么叫顺序读写呢?
大家有没有发现,我们上面讲的那几个函数,在读写文件数据的时候,要么是从文件起始位置开始,一次读取一个字符,如果再读的话就从上次的位置继续往后再读一个;要么是一次读取一个字符串,依次往后读…。
那说到底,都是在顺序读取文件。
那么,如果我们在读取文件时,不想按照顺序读呢?
如果我们想对文件进行随机读取,想从哪个位置开始读就从哪个位置读,能不能做到呢?
当然可以。
接下来,我们就来学习一下,如何对文件进行随机读写。
6.1 fseek
首先我们来看一个函数叫做——fseek
。
那它是干什么的,怎么用呢?
它可以重新定位流位置指示器,将与流关联的位置指示器设置为新位置。
什么意思呢?
我们打开一个文件,与该文件关联的位置指示器是默认指向文件开头的,所以我们读取文件也默认是从开头进行的。
那fseek呢,就可以重新定位与文件关联的这个位置指示器,那这样的话,我们想从哪个位置读写文件,就可以通过fseek把位置指示器定位到我们想要的位置,这样,就可以实现对文件的随机读写了。
fseek
3个参数,我们来了解一下:
第一个参数
FILE * stream
还是接收文件对应的文件指针。第二个参数
long int offset
,接收相对于参考位置的偏移量。第三个参数
int origin
,用于指定用作偏移参考的位置。
origin
可以有三个取值:
文件开头,文件末尾和文件位置指示器的当前位置。
第二个参数
offset
就是接收相对这三个位置的偏移量。
看一下返回值:
然后我们就练习一下吧。
现在电脑上有一个文本文件test.txt
,我们先在就对它进行一个随机读写。
首先我们想直接第一次就读取到字符d
,怎么做:
- 以文件开头
a
的位置(SEEK_SET
)为参考位置,读从文件开头向后偏移量为3的位置。
那要这样写:
#include <stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen()");
return 1;
}
fseek(pf, 3, SEEK_SET);
int ch = fgetc(pf);
printf("%c\n", ch);
fclose(pf);
pf = NULL;
return 0;
}
运行代码:
一次就读到d
了。
- 从文件末尾
SEEK_END
向前偏移量为3的位置
fseek(pf, -3, SEEK_END);
int ch = fgetc(pf);
printf("%c\n", ch);
这里注意从后向前偏移要写成负数。
那如果我们上去先顺序读取一个字符,然后我们想跳到d
的位置读取d
,怎么搞呢?
- 先读一个字符,那现在指向第二个字符
b
的位置,从当前位置SEEK_CUR
向后偏移量为2的位置就是d
int ch = fgetc(pf);
fseek(pf, 2, SEEK_CUR);
ch = fgetc(pf);
printf("%c\n", ch);
也可以。
6.2 ftell
那通过上面的学习我们知道有时候想使用fseek
实现对文件的随机读写,是需要知道当前位置指示器的位置的。
那有没有什么方法可以快速获取当前位置指示器的位置,不需要我们自己再去计算呢?
当然有。
函数
ftell
就是专门来干这件事情的。
我们来看一下:
它的作用就是返回流的位置指示器的当前值(或者说当前位置指示器相对于起始位置的偏移量)。
怎么用呢?
它只有一个参数FILE * stream,接收对应的文件指针,就可以返回当前位置指示器相对于起始位置的偏移量。
那我们接着上面的代码,我们刚才读取了d,那现在位置指示器应该指向d的后面,也就是e的位置。
文件中放的是abcdef,那e相对于起始位置a的偏移量应该是4。
那我们现在就用ftell 验证一下,我们打印一下ftell 的返回值,看是不是4:
通过ftell
,我们很容易就能知道当前位置指示器的位置。
6.3 rewind
然后我们再来看一个函数——rewind
。
那它的作用是什么呢?
它的作用就是:不管当前文件的位置指示器指到了哪个位置,使用rewind
就可以让位置指示器直接回到起始位置,指向文件开头。
只有一个参数,还是接收文件对应的文件指针,无返回值。
那我们现在就来练习一下:
还是上面那个文件,通过之前的操作,我们知道它现在是指向
e
的位置了,那我们就调用一次rewind
,然后在读一次,如果读到的是a
,就证明位置指示器在rewind
的作用下回到起始位置了。
rewind(pf);
ch = fgetc(pf);
printf("%c\n", ch);
是a。
7. 文本文件和二进制文件
根据数据的组织形式,数据文件被称为文本文件或者二进制文件。
数据在内存中以二进制的形式存储,如果不加转换的输出到外存,就是二进制文件。
如果要求在外存上以ASCII码的形式存储,则需要在存储前转换。以ASCII字符的形式存储的文件就是文本文件。
一个数据在内存中是怎么存储的呢?
字符一律以ASCII形式存储,数值型数据既可以用ASCII形式存储,也可以使用二进制形式存储。
如有整数10000,如果以ASCII码的形式输出到磁盘,则磁盘中占用5个字节(每个字符一个字节),而二进制形式输出,则在磁盘上只占4个字节(VS2013测试)。
我们可以测试一下:
我们现在就把10000以二进制的形式存到文件中。
看看是什么样的:
int main()
{
int a = 10000;
FILE* pf = fopen("test.txt", "wb");
if (pf == NULL)
{
perror("fopen()");
return 1;
}
fwrite(&a, 4, 1, pf);
fclose(pf);
pf = NULL;
return 0;
}
运行代码,生成test.txt
,我们打开看看:
我们 直接看到的并不是10000,而是一个乱码,因为我们是以二进制的形式存进去的。
有没有什么方法可以查看呢?
有的,我们可以借助vs查看:
将这个文件添加到vs2022中;
右键选择打开方式,以二进制编辑器的方式打开
我们知道vs上是小端存储模式,变成00 00 27 10 是不是跟我们上面分析的一样。
证明就是二进制的存储。
8. 文件读取结束的判定
我们先来看一个函数:
8.1 feof
这个函数是用来干什么的呢?
注意:函数feof 不是用来判断文件是否读取结束的。
而是应用于当文件读取结束的时候,判断是读取失败导致结束,还是遇到文件尾结束。
feof 只有一个参数,接收一个文件指针,判读该文件读取结束时是由于哪种原因导致的结束。
那如何判断是哪种原因导致的结束呢?
我们看到feof 的返回值是int。
如果文件是因为读取到了文件尾而结束的,feof 将返回一个非零值;
否则,将返回0。
我们可以来练习一下:
还是这个文件,我们搞个循环,把它的内容读完,然后用feof
判断一下,看返回值是不是非0值(返回非0就表示读到文件尾的正常结束)。
int main()
{
//打开文件
FILE* pf = fopen("test.txt", "r");
if (NULL == pf)
{
printf("fopen");
return 1;
}
int ch = 0;
while((ch = fgetc(pf))!=EOF)
{
printf("%c\n", ch);
}
printf("%d\n", feof(pf));
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
是1,非0值,说明文件是读取到文件尾结束的。
大家看这样可以吗?
int i = 0;
for (i = 0; i < 6; i++)
{
int ch = fgetc(pf);
printf("%c\n", ch);
}
printf("%d\n", feof(pf));
文件中总共6个字符,循环6次是不是就读到文件尾了啊,那我们看一下结果吧:
诶~,我们看到前面abcdef
都打印出来了,和上面一样,但是feof(pf)
的返回值却是0,为什么呢?
这里注意:
如果用for循环的话需要循环7次,因为第6次我们是读到了
f
,并不是文件的结束标志,所以需要在读一次,读取7次才读到文件结尾。
这次就是非0值1了。
不过我们发现这样好像多打印出来了一个空格,那按照第上面的分析第7次就应该读到文件结束标志了(文件尾指示器),那就是EOF嘛(本质是一个-1)。
而我们把EOF以
%c
的形式打印:
就是一个空格。
所以:
文件结束标志(文件尾指示器)应该是在最后一个字符后面。
8.2 如何判断文件是否读取结束
那说到底函数feof 不是用来判断文件是否读取结束的,那我们应该如何去正确判断一个文件是否读取结束呢?
正确的方法是:我们要根据相关函数的返回值来判断文件是否读取结束。
其实它们的返回值我们在学习这些函数的时候也提过。
8.2.1 文本文件
文本文件读取是否结束,判断其返回值是否为 EOF ( fgetc ),或者 NULL ( fgets )或是否小于指定数据个数(fscanf )
对于fgetc来说:
我们看一下它的返回值是啥:
那我们就可以通过判断fgetc
的返回值是否为EOF
来判断文件是否读取结束。(当然这里我们看到如果发生其它读取错误,也会返回EOF,那这种情况我们是不是可以使用feof 再进行判断)
那fgets
呢?
对于fgets
来说,读取文件结束返回空指针,所以我们可以通过判断其返回值是否为空指针来判断文件是否读取结束。
还有fscanf
:
判断返回值是否小于指定的数据个数来判断是否是读取结束。2.我是文本 蓝色
正确的使用示范:
#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) // 标准C I/O读取文件循环
{
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;
}
大家可以看看这段代码。
这里面用了一个ferror
我们没有说。
ferror
其实是判断是否发生错误的,如果发生读取错误,则ferror
返回非0值。
8.2.2 二进制文件
二进制文件的读取结束判断,判断(fread)返回值是否不同于(可能小于count)实际要读的个数
来看一下fread
的返回值:
所以对于fread
我们可以通过判断其返回值是否小于实际要读的个数(count)来判断文件是否读取结束。
正确示范:
enum { SIZE = 5 };
int main()
{
double a[SIZE] = { 1.,2.,3.,4.,5. };
FILE* fp = fopen("test.bin", "wb"); // 必须用二进制模式
fwrite(a, sizeof * a, SIZE, fp); // 写 double 的数组
fclose(fp);
double b[SIZE];
fp = fopen("test.bin", "rb");
if (!fp) {
perror("File opening failed");
return EXIT_FAILURE;
}
size_t ret_code = fread(b, sizeof * b, SIZE, fp); // 读 double 的数组
if (ret_code == SIZE) {
puts("Array read successfully, contents: ");
for (int n = 0; n < SIZE; ++n) printf("%f ", b[n]);
putchar('\n');
}
else
{ // error handling
if (feof(fp))
printf("Error reading test.bin: unexpected end of file\n");
else if (ferror(fp))
{
perror("Error reading test.bin");
}
}
fclose(fp);
fp = NULL;
}
大家可以仔细读一下这段代码。
9. 文件缓冲区
接下来我们再来了解一个知识叫做“文件缓冲区”。
ANSIC 标准采用“缓冲文件系统”来处理数据文件,所谓缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块“文件缓冲区”。
从内存向磁盘输出的数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上。
如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区至充满缓冲区后,再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓冲区的大小根据C编译系统决定的。
当然:
虽然说的是要把缓冲区装满才会开始传输数据(这样做可以提高效率),但是我们也可以根据自己的需求去刷新缓冲区,比如函数
fflush
就可以强制刷新缓冲区,另外,遇到\n
(行缓冲)和关闭文件时也会自动刷新缓冲区。
下面有一段代码,可以让我们感受一下缓冲区的存在:
#include <stdio.h>
#include <windows.h>
//VS2022 WIN10环境测试
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;
}
我们运行代码,abcdef
不会直接写入文件,而是先放在缓冲区,当我们使用fflush
强制刷新缓冲区后,才将输出缓冲区的数据写到文件(磁盘),然后文件中才有内容。
我们运行代码给大家看一下:
运行之前该文件还未创建:
现在我们运行:
此时打开文件,里面还没有内容,因为还没有刷新缓冲区。
刷新后,我们再打开文件,发现里面就有内容了。
这里可以得出一个结论:
因为有缓冲区的存在,C语言在操作文件的时候,有时需要刷新缓冲区,或者在文件操作结束的时候关闭文件。
如果不做,可能导致读写文件的问题。
好了,那到这里,C语言文件操作的内容就全部讲完了。希望对大家有帮助,也欢迎大家指正!!!
- 点赞
- 收藏
- 关注作者
评论(0)