字符输入函数 fgetc:
int fgetc ( FILE * stream );
它的功能是:从流中读取一个字符。注意返回类型是int。
示例:
#include<stdio.h> int main() { FILE* pf = fopen("data.txt", "r"); if (pf == NULL) { perror("fopen"); return 1; } //读文件 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; }
运行结果:
可以看到,上述fputc和fgetc函数是都是按照顺序一个一个的,往文件里面写(从文件里面读)
当然,如果我们想从键盘读也可以用stdin函数:
#include<stdio.h> int main() { FILE* pf = fopen("data.txt", "r"); if (pf == NULL) { perror("fopen"); return 1; } //读文件 int ch = fgetc(stdin); printf("%c\n", ch); ch = fgetc(stdin); printf("%c\n", ch); ch = fgetc(stdin); printf("%c\n", ch); //关闭文件 fclose(pf); pf = NULL; return 0; }
运行结果:
到这里,大家可能对输入输出的概念有点模糊了,下面来看一个图:
文本行输出函数 fputs:
int fputs ( const char * str, FILE * stream );
它的功能是:把字符串(string)写入到流(stream)中去。
#include<stdio.h> int main() { FILE* pf = fopen("data.txt", "w"); if (pf == NULL) { perror("fopen"); return 1; } //写文件 fputs("hello world", pf); fputs("hello xupt", pf); //关闭文件 fclose(pf); pf = NULL; return 0; }
大家猜这个写进去是一行还是两行?
可以看到是一行,如果要写进去是两行,自己在每一行后面加上“\n”:
当然,fputs函数也是适用于所有数据输出流的,用stdout也可以打印在屏幕上:
文本行输入函数 fgets:
char * fgets ( char * str, int num, FILE * stream );
它的功能是:从流(stream)中读取num-1个字符到str里。
示例:
#include<stdio.h> int main() { FILE* pf = fopen("data.txt", "r"); if (pf == NULL) { perror("fopen"); return 1; } char arr[10] = { 0 }; fgets(arr, 10, pf); printf("%s\n", arr); fclose(pf); pf = NULL; return 0; }
此时文件"data.txt"里面存放的是:
运行结果:
我们刚刚文件里面存放的是hello world hello xupt,传给num的值是10,但是只读出来了hello wor这9个字符。
由此可见,fgets确实只读num-1个字符。
那如果我们再读一行呢?
可以看到第二行只读了7个字符,这是因为读到第7个之后遇到了换行。所以,
fgets函数停止读数据有两种可能:
1. 读够了num-1个字符。
2. 遭遇了换行符。
以上几种函数都是针对字符数据来用的,下面我们讲两种适用于所有数据类型的函数。
格式化输出函数 fprintf:
int fprintf ( FILE * stream, const char * format, ... );
它的功能是:将格式化数据(format)写入流(stream)。
我们搜索一下fprintf和printf函数会发现:
它们的参数只差一个流,那在使用fprintf函数时,只需要在printf函数原有的基础上加上流就行:
#include<stdio.h> struct S { int a; float s; }; int main() { FILE* pf = fopen("data.txt", "w"); if (pf == NULL) { perror("fopen"); return 1; } struct S s = { 100,3.14f }; //写文件 fprintf(pf, "%d %f", s.a, s.s); //关闭文件 fclose(pf); pf = NULL; return 0; }
可以看到,确实将结构体数据写进去了:
格式化输入函数 fscanf:
int fscanf ( FILE * stream, const char * format, ... );
它的功能是:把格式化数据从流中读取出来。
同样,我们可以对比一下fscanf和scanf函数:
和刚才一样,fscanf函数是在scanf函数的基础上加上流:
#include<stdio.h> struct S { int a; float s; }; int main() { FILE* pf = fopen("data.txt", "r"); if (pf == NULL) { perror("fopen"); return 1; } struct S s = { 0 }; //读文件 fscanf(pf, "%d %f", &(s.a), &(s.s)); printf("%d %f\n", s.a, s.s); //关闭文件 fclose(pf); pf = NULL; return 0; }
而fscanf函数也确实将结构体数据读取到了结构体变量s中:
下面我们再来补充两个函数:
sprintf:
int sprintf ( char * str, const char * format, ... );
它的功能是:把格式化的数据转化为字符串存进str里。
示例:
#include<stdio.h> struct S { int a; float s; char str[10]; }; int main() { struct S s = { 100,3.14f,"hehe"}; char arr[30] = { 0 }; sprintf(arr, "%d %f %s\n", s.a, s.s, s.str); printf("%s\n", arr); return 0; }
运行结果:
为了验证sprintf函数是否真的将格式化数据转化成字符串,我们可以打开监视界面:
可以看到,确实转换为字符串形式了,上面我们将数据存放到arr中,那能不能将数据从arr中取出来呢?
这就要用到sscanf函数了。
sscanf:
int sscanf ( const char * s, const char * format, ...);
它的功能是:从字符串中读取格式化数据。
示例:
#include<stdio.h> struct S { int a; float s; char str[10]; }; int main() { struct S s = { 100,3.14f,"hehe"}; char arr[30] = { 0 }; sprintf(arr, "%d %f %s\n", s.a, s.s, s.str); struct S tmp = { 0 }; sscanf(arr, "%d %f %s\n",&(tmp.a),&(tmp.s),tmp.str); printf("%d %f %s\n", tmp.a, tmp.s, tmp.str); return 0; }
上述代码,我们从字符串中提取格式化数据到tmp中:
以上所说的所有函数,不论读写都是以文本的形式,下面我们来看两个以二进制读写的函数:
二进制输入 fwrite:
size_t fwrite( const void * ptr, size_t size, size_t count, FILE*stream )
它的功能是:从ptr指向的地方开始,把count个大小为size的数据写入到流中去。
示例:
#include<stdio.h> struct S { int a; float s; char str[10]; }; int main() { struct S s = { 100,3.14f,"hehe"}; //打开文件 FILE* pf = fopen("data.txt", "wb"); if (pf == NULL) { perror("fopen"); return 1; } //写文件 fwrite(&s, sizeof(struct S), 1, pf); //关闭文件 fclose(pf); pf = NULL; return 0; }
注意二进制的写要用“wb”。
上述代码把结构体变量s中的内容以二进制的形式写入到文件“data.txt”中:
我们发现二进制的文件根本读不懂。
二进制输出 fread:
size_t fread( const void * ptr, size_t size, size_t count, FILE*stream )
fread函数和fwrite函数的参数完全相同。
它的功能是:从流中把count个大小为size的数据从ptr指向的地方开始写入。
fread的返回值是:实际读到的数据的个数。
示例:
#include<stdio.h> struct S { int a; float s; char str[10]; }; int main() { struct S s = { 0 }; //打开文件 FILE* pf = fopen("data.txt", "rb"); if (pf == NULL) { perror("fopen"); return 1; } //读文件 fread(&s, sizeof(struct S), 1, pf); printf("%d %f %s\n", s.a, s.s, s.str); //关闭文件 fclose(pf); pf = NULL; return 0; }
注意二进制的读要用“rb”。
运行结果:
5.文件的随机读写
前面学的函数只能按照顺序依次读写,下面我们来学两个可以随机读写的函数。
5.1 fseek
int fseek ( FILE * stream, long int offset, int origin );
它的功能是:根据文件指针的位置和偏移量来定位文件指针。
参数中offset是偏移量,origin是起始位置
注意参数origin有三种选择:
SEEK_SET | 文件开头 |
SEEK_CUR | 文件指针的当前位置 |
SEEK_END | 文件结尾 * |
示例:
假设此时我们在文件"data.txt"中存放的是:
我们期待从f的位置开始读,用fseek函数实现如下:
#include<stdio.h> int main() { //打开文件 FILE* pf = fopen("data.txt", "r"); if (pf == NULL) { perror("fopen"); return 1; } //读文件 fseek(pf, 5, SEEK_SET); int ch = fgetc(pf); printf("%c\n", ch); //关闭文件 fclose(pf); pf = NULL; return 0; }
运行结果:
上述代码的逻辑如下:
以上是从起始位置开始,如果我们要从末尾开始,要想读到f,那偏移量就是-6:
如果我们在fseek函数之前已经读了3个字符,要想从当前位置往后读2个字符到f就要使用SEEK_CUR:
5.2 ftell
long int ftell ( FILE * stream );
它的功能是:获取流中的当前位置。
如果我们在写代码的过程中也不知道文件指针在哪的时候,就可以用ftell函数,它能告诉你。
示例:
#include<stdio.h> int main() { //打开文件 FILE* pf = fopen("data.txt", "r"); if (pf == NULL) { perror("fopen"); return 1; } //读文件 int ch = fgetc(pf); printf("%c\n", ch);//a ch = fgetc(pf); printf("%c\n", ch);//b ch = fgetc(pf); printf("%c\n", ch);//c int pos = ftell(pf); printf("%d\n", pos); //关闭文件 fclose(pf); pf = NULL; return 0; }
运行结果:
我们在ftell之前已经读了3个字符,此时用ftell函数告诉我们偏移量,就可以找到文件指针的位置。
那如果我们就是想要回到偏移量为0的位置读怎么办呢?
用rewind函数就行。
5.3 rewind
void rewind ( FILE * stream );
它的功能是:让文件指针回到文件的起始位置。
示例:
#include<stdio.h> int main() { //打开文件 FILE* pf = fopen("data.txt", "r"); if (pf == NULL) { perror("fopen"); return 1; } //读文件 int ch = fgetc(pf); printf("%c\n", ch);//a ch = fgetc(pf); printf("%c\n", ch);//b ch = fgetc(pf); printf("%c\n", ch);//c rewind(pf); ch = fgetc(pf); printf("%c\n", ch);//a //关闭文件 fclose(pf); pf = NULL; return 0; }
运行结果:
6.文本文件和二进制文件
根据数据的组织形式,数据文件被称为文本文件或者二进制文件。
数据在内存中以二进制的形式存储,如果不加转换的输出到外存,就是二进制文件。
如果要求在外存上以ASCII码的形式存储,则需要在存储前转换。以ASCII字符的形式存储的文件就是文本文件。
一个数据在内存中是怎么存储的呢?
字符一律以ASCII形式存储,数值型数据既可以用ASCII形式存储,也可以使用二进制形式存储。
如有整数10000,如果以ASCII码的形式输出到磁盘,则磁盘中占用5个字节(每个字符一个字节),而二进制形式输出,则在磁盘上只占4个字节。
讲到这里大家可能还不太懂,先看下图:
当我们把10000转化为二进制在内存中存储的时候就是二进制文件,二进制文件一般看不懂。
当我们把10000的每一位当做一个字符,把该字符的ASCLL值用二进制的形式存放在内存中,就叫做文本文件,文本文件我们能看懂。
下面看一段代码:
#include <stdio.h> int main() { int a = 10000; FILE* pf = fopen("test.txt", "wb"); fwrite(&a, 4, 1, pf);//二进制的形式写到文件中 fclose(pf); pf = NULL; return 0; }
以二进制的形式写到文件中,我们打开文件发现根本看不懂:
那我们就永远看不懂它的意思了吗?
其实也是可以的:
此时我们就可以看到数字10000在内存中的存储了:
10 27 00 00是16进制数,而10000的二进制数化为16进制是:00 00 27 10,因为是小端存储,恢复为原来的数刚好是 10 27 00 00
7.文件读取结束的判定
7.1被错误使用的feof
在文件读取过程中,不能用feof函数的返回值直接来判断文件的是否结束。
feof的作用是:判断读取结束的原因是否是:遇到文件尾结束。
就是说,feof是在文件已经结束后,判断读取结束的原因,而不是用它来判断是否结束。
那么我们应该怎样判断文件读取是否结束呢?
1. 文本文件读取是否结束,判断返回值是否为 EOF ( fgetc ),或者 NULL ( fgets )
例如:
fgetc 判断是否为 EOF .
fgets 判断返回值是否为 NULL .
2. 二进制文件的读取结束判断,判断返回值是否小于实际要读的个数。
例如:
fread判断返回值是否小于实际要读的个数。
下面给两个例子,自行解读一下:
文本文件的例子:
#include <stdio.h> #include <stdlib.h> int main(void) { 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); }
二进制文件的例子:
#include <stdio.h> enum { SIZE = 5 }; int main(void) { 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"); 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); }
8.文件缓冲区
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; }
上述代码,当fclose时也会刷新缓冲区,而刷新缓冲区时,才会将输出缓冲区的数据写到文件,所以为了观察到现象,我们对程序休眠10秒,然后再刷新。
这样就会观察到,刷新前,文件中没有内容,刷新后,文件中就有内容了:
刷新前:
刷新后:
这就证明了,确实存在输入输出缓冲区。
那么今天就学到这里了,未完待续。。。