目录
文件
看下面的程序,我们发现,第二次运行程序的时候n不是我们第一次输入的数字,为什么呢?
#include<stdio.h>
int main()
{
int n = 0;
printf("%d\n", n);
scanf_s("%d", &n);
return 0;
}
第一次运行:
第二次运行:
这是因为我们写的程序数据是存储在电脑的内存中,如果程序退出,内存回收,数据就丢失了。如果想把数据长时间保存下来,就要用到文件。
文件是什么?
广义上来讲,文件是计算机上存储信息的基本单位。可以理解为一些电子数据的集合,这些数据可以是文本,图片,音频,视频等不同形式的内容。文件储存在电脑硬盘或者其他存储设备(如U盘,移动硬盘盘,光盘等)上,并且每个文件都有唯一的文件名,特定的文件格式。
但在程序设计中,我们所说的文件通常是指“程序文件”“数据文件”这两种(从文件功能的角度来说)。
程序文件是用于指导计算机执行特定操作的文件,包括源程序文件(后缀为.c),目标文件(windows环境后缀为.obj),可执行程序(windows环境后缀为.exe)
数据文件 用于储存由用户或程序生成的数据。它们可以包含各种类型的信息,如文本、图片、音乐、视频等。数据文件一般有特定的格式,这取决于创建它们的程序。
本文讨论的是数据文件。
以前我们所说的数据的输入输出都是以终端为对象的,即从终端的键盘输入数据,运行结果显示到显示器上。
其实更多时候,我们需要把这些数据变为文件存入到存储设备中,当需要的时候再从存储设备上把数据读取到内存中使用。
每个文件都有一个唯一的文件标识,以便于用户识别和引用。
文件名包括三部分:文件路径+文件名主干+文件后缀,比如:c: \ code\ test.txt;为了方便起见,文件标识常被称为文件名。
根据数据的组织形式,数据文件被称为文本文件或者二进制文件。
我们都知道,数据在内存中是以二进制的形式存储的,如果把这二进制序列串不经过任何转换,直接输出到硬盘U盘光盘等存储设备而产生的文件就是二进制文件;
如果这二进制串是经过某种转换,比如ASCII码,再存入到存储设备中所产生的文件就是文本文件;
⼀个数据在文件中是怎么存储的呢?
字符⼀律以ASCII形式存储,数值型数据既可以用ASCII形式存储,也可以使用二进制形式存储。
如有整数10000,如果以ASCII码的形式输出到磁盘,则磁盘中占用5个字节(每个字符⼀个字节,分别是’1’‘0’‘0’‘0’‘0’),而二进制形式输出,则在磁盘上占用4个字节(因为10000是个整数,也就是整型,整型四字节)。
下面是代码:
#include<stdio.h>
#include<string.h>
int main()
{
FILE* pf = fopen("test.txt", "w");//把文件test.txt以"w"(只写)的形式打开
if (pf == NULL)
{
perror("Open test.txt:");//如果打开失败,显示错误信息
return;
}
fputs("10000", pf);//把五个字符(严格来说是六个,因为后面还有'\0')写进去
fclose(pf);
pf = NULL;
return 0;
}
执行过上面程序后,项目文件夹就会多了一个叫做"test.txt"的文件,这是因为,如果项目文件夹没有这个文件,fopen就会自己建一个。
点击VS打开文件按钮:
双击一下就可以看到了:
刚刚是以字符串的形式存,接下来是以二进制的形式存:
#include<stdio.h>
#include<string.h>
int main()
{
int n = 10000;
FILE* pf = fopen("test.txt", "wb");//把文件test.txt以"wb"(只写)的形式打开
if (pf == NULL)
{
perror("Open test.txt:");//如果打开失败,显示错误信息
return;
}
fwrite(&n, 4, 1, pf);//以二进制的方式把内存中四字节大小数据一次性写入文件
fclose(pf);
pf = NULL;
return 0;
}
执行完后,照着之前的步骤,我们发现:
弹出了这个东西:
不是这么看的,要换个流程:
首先打开解决方案资源管理器:
右键源文件,添加现有项:
双击test.txt:
右键test.txt,选择打开方式:
找到二进制编辑器,确定:
然后就看到了数值1000了,其它东西不用管。(前面那串零个人估计可能是类似地址的东西)
这里的10 27 00 00就是内存中的二进制序列串(为了方便观察,是以十六进制显示的);
10000的二进制原码为:00000000 00000000 00100111 00010000,转换成补码就是00000000 00000000 00100111 00010000,超过一个字节,VS小端存储,内存实际存入的是00010000 00100111 00000000 00000000,换成十六进制就是10 27 00 00。
流
我不确定你们在高中时是否选择了生物作为选修课,但我可以告诉你们,我选择了。在我们的生物课本中,我们了解了细胞膜是细胞与外界进行物质和信息交换的媒介,可以说,这可能是我们第一次系统地接触到了信息交流的概念。在信息通信方面,细胞膜上的受体蛋白能感知到细胞外部环境中的特定信号分子(例如激素或神经递质),并通过内部的信号路由,将这些信息传递到细胞核,于是,特定的基因片段被激活,对环境做出了动态调整。
类似地,计算机程序也需要一种媒介来进行信息交流,这就是我们所说的流。和细胞膜的作用相似,如果我们需要操作文件或者处理数据,就需要借助于某种特定流。
流的存在,使得程序与外界进行信息交流变得尽可能直观简单。如果没有流,那么程序员可能还需要深入学习信息论,考虑串行并行通信等问题,而这需要具有深厚的计算机科学知识,毕竟,能理解并精通这些理论的,已然不再是普通的程序员,而是值得尊重的计算机科学家。还好,流的存在,让信息的输入和输出变得直接简单,对于程序员来说,我们只需要将信息投入流中,或者从流中读取信息,其他事情则不需要考虑。
C/C++中流的分类
在C/C++中,有四种流:
- 输入/输出(I/O)流
- 字节流和字符流
- 缓冲与非缓冲流
- 数据流
其中,输入/输出(I/O)流和字节流和字符流将是本文讨论的重点。
标准流
回到之前的代码:
#include<stdio.h>
int main()
{
int n = 0;
printf("%d\n", n);
scanf_s("%d", &n);
return 0;
}
我们发现,这每一步我都看得懂呀,我怎么没看到有什么把流打开的操作呢?没把流打开,我怎么把键盘输入的信息读出来,又把这段信息显示在屏幕上呢?
事实上,在这段代码中,确实没有对流打开的操作,这是因为,C语言程序在启动的时候,默认打开了3个流:
- stdin-标准输入流,在大多数环境中从键盘输入,scanf函数就是从标准输入流中读取数据。
- stdout-标准输出流,大多数环境中输入至显示器界面,printf函数就是把信息输出到标准输出流中。
- stderr-标准错误流,大多数环境中输出到显示器界面。
这三种流都从属于输入/输出(I/O)流;就是因为默认打开了这三种流,我们才可以直接使用scanf,printf这种函数进行信息交流;
正如我们使用指针来管理和操作变量一样,我们也可以利用FILE*,即流的指针,来管理和操作数据流。
FILE* 在中文中常被翻译为’文件指针’,然而我个人认为这种翻译有些偏颇,可能会导致误解。因为实际上,FILE* 是作为流——即文件与程序进行信息交流的媒介,而非文件本身。然而,由于FILE* 最常被用于文件的读写操作,所以’文件指针’这种说法逐渐成为了通用词汇。
备注:这里讲的输入指外界向程序输入信息,或者说是程序从外界读取信息;输出指程序向外界输出信息,或者说是程序向外界写入信息;后面的也是这样,不要弄错主语了。
文件指针
文件指针,全称为文件类型指针,是用于文件操作的重要工具。当我们打开一个文件时,系统会根据文件的具体情况,在内存中建立一个用以描述该文件的结构体变量。这个结构体包含了如文件名、文件状态、文件当前位置等文件相关信息。此种结构体类型在stdio.h头文件中声明,类型名为FILE。
在版本较低的VS中,我们在文件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类型并不完全一致,但大同小异。
文件的打开和关闭
我们对文件的操作是有固定流程的:想要使用某个文件,先打开它,然后进行各种操作,用完了,就把文件关上。
C标准规定用fopen函数来打开文件,fclose来关闭文件
fopen
头文件:stdio.h
函数声明:
FILE * fopen ( const char * filename, const char * mode );
其中,filename指的是文件名,一般情况下不包括文件路径,若系统支持,也可包含路径;
mode是个字符串指针,用来描述哪些对文件的操作是被允许的,这些操作又是如何执行的,具体如下:
模式 | 含义 |
---|---|
“r”(只读) | 为了输入数据,打开一个已经存在的文本文件;如果文本文件不存在,则打开失败 |
“w”(只写) | 为了输出数据,打开一个文本文件;如果文本文件存在,则清除原有内容,使之变为空白文件;如果文本文件不存在,则新建一个同名文件 |
“a” (追加) | 为了输出数据,打开一个文本文件;如果文本文件存在,则不改变原有内容;如果文本文件不存在,则新建一个同名文件 |
“r+”(读写) | 为了输入和输出数据,打开一个已经存在的文本文件;如果文本文件不存在,则打开失败 |
“w+”(读写) | 为了输出和输入数据,打开一个文本文件;如果文本文件存在,则清除原有内容,使之变为空白文件;如果文本文件不存在,则新建一个同名文件 |
“a+”(追加) | 为了输出和输入数据,打开一个文本文件;如果文本文件存在,则不改变原有内容;如果文本文件不存在,则新建一个同名文件 |
下面讲一下各模式的光标起始位置
模式 | 光标起始位置 |
---|---|
“r”(只读) | 文本开始 |
“w”(只写) | 文本开始 |
“a” (追加) | 文本末尾 |
“r+”(读写) | 文本开始 |
“w+”(读写) | 文本开始 |
“a+”(追加) | 文本末尾 |
注:
加上 “b” :以二进制方式打开文件,例如 “rb”、“wb”、“ab”、“r+b”、“w+b”、“a+b”;后三种模式也可写作"rb+“、“wb+”、“ab+”。
加上 “x”:如果文件不存在,新建并打开用于读写,否则操作失败,比如"wx”、“w+x”。
mode是个字符串指针,输的时候一定要用双引号,而不是单引号。
文件打开失败会返回空指针,并产生特定的错误代码,可以用strerror或者perror接收。
fclose
头文件:stdio.h
函数声明:
int fclose ( FILE * stream );
没什么好说的,参数就是文件指针,成功关闭返回0,不成功,返回EOF。
示例
#include<stdio.h>
int main()
{
//打开文件
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
perror("Open test.txt(w)");
return;
}
//文件操作
fputs("hello world.", pf);
//关闭文件
fclose(pf);
pf = NULL;
//打开文件
pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("Open test.txt(r)");
return;
}
//文件操作
char str[13] = { 0 };
fgets(str, 13, pf);
printf("%s\n", str);
//文件关闭
fclose(pf);
pf = NULL;
return 0;
}
文件的顺序读写
fputc
头文件:stdio.h
函数主要功能:把字符(c)放置(put)到文件(f)中
函数声明:
int fputc ( int character, FILE * stream );
stream是文件指针,character是字符,你说这字符怎么是int类型的?因为这是ASCII码形式的字符,如果成功,返回这个字符;如果不成功,返回EOF,错误信息可以用strerror接收。
比如我们输出一个字符‘h’:
#include<stdio.h>
int main()
{
//打开文件
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
perror("Open test.txt(w)");
return;
}
//文件操作
fputc('h', pf);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
我们打开test.txt文件看一下:
现在我把26个英文字母都写进去:
#include<stdio.h>
int main()
{
//打开文件
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
perror("Open test.txt(w)");
return;
}
//文件操作
char ch = 0;
for (ch = 'a'; ch <= 'z'; ch++)
{
fputc(ch, pf);
}
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
你看,写入成功了;有人讲,这光标位置不对呀,怎么在前面呀?前面讲错了?前面没讲错,我们前面说的光标是对读写文本来说的,这张图片里的光标和我们前面讲的光标不是同一个光标,图片里的光标默认在起始位置
刚刚我们说的是fputs的一般用法,但实际上fputs适用于所有输入流,而上文中对文件进行输入的流只是输入流中的一种,为了区分,我们下面把进行文件输入\输出的流称为文件输入\输出流,文件输入\输出流只是输入\输出流的一部分,比如下面我们把文件指针换成标准输出流指针
#include<stdio.h>
int main()
{
char ch = 0;
for (ch = 'a'; ch <= 'z'; ch++)
{
fputc(ch, stdout);
}
return 0;
}
fgetc
头文件:stdio.h
函数主要功能:从文件(f)中得到(get)字符(c)
函数声明:
int fgetc ( FILE * stream );
stream 指的是文件指针,读取成功返回读取得到的字符(ASCII码形式,所以返回值是int);读取失败返回EOF(EOF是-1,还是int),int是专门为那个EOF定的
#include<stdio.h>
int main()
{
//打开文件
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("Open test.txt(r)");
return;
}
//文件操作
int ch = 0;
while ((ch = fgetc(pf)) != EOF)
{
printf("%c", ch);
}
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
同样的,文件输入流只是输入流中的一部分,而fgetc适用于所有输入流,比如标准输入流
#include<stdio.h>
int main()
{
char ch = 0;
//键盘输入字符'h'
ch = fgetc(stdin);
printf("ch=%c", ch);
return 0;
}
fputs
头文件:stdio.h
函数主要功能:把字符串(s)放置(put)到文件(f)中
函数声明:
int fputs(const char * str,FILE * stream);
将str指向的字符串 写入文件。 该函数从指定的地址 ( str ) 开始复制,直到到达终止空字符 ( ‘\0’ )。此终止空字符不会复制到文件中;如果成功,则返回一个非负值;如果出错,该函数将返回EOF,错误信息可以用strerror接收
如果这样写的话,字符串不会换行:
#include<stdio.h>
int main()
{
//打开文件
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
perror("Open test.txt(w)");
return;
}
//文件操作
fputs("hello world", pf);
fputs("Genshin Impact, Start Up!", pf);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
换行要这样写:
#include<stdio.h>
int main()
{
//打开文件
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
perror("Open test.txt(w)");
return;
}
//文件操作
fputs("hello world\n", pf);
fputs("Genshin Impact, Start Up!\n", pf);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
同样的,文件输出流只是输出流的一部分,而fputs适用于所有输出流,下面我们把输出流改成标准输出流
#include<stdio.h>
int main()
{
fputs("hello word", stdout);
return 0;
}
fgets
头文件:stdio.h
函数主要功能:从文件(f)中得到(get)字符串(s)
函数声明:
char * fgets ( char * str, int num, FILE * stream );
从文件中读取字符并将它们作为 字符串存储到str中,直到读取到 ( num -1) 个字符或者到达换行符或文件末尾(以先发生者为准)。换行符会使fgets停止读取,但函数会将其视为有效字符,并将其包含在复制到str 的字符串中。在读取并拷贝完后,函数会自动在字符串后追加一个终止空字符’\0’;成功时,该函数返回str,失败时返回空指针
#include<stdio.h>
int main()
{
//打开文件
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("Open test.txt(r)");
return;
}
//文件操作
char str[30] = "xxxxxxxxxxxxxxxxxxxxxxxxxx";
fgets(str, 10, pf);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
通过调试可以明显看到,只读取了9个字符,第十个字符是’\0’
现在把num改成20再试试
#include<stdio.h>
int main()
{
//打开文件
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("Open test.txt(r)");
return;
}
//文件操作
char str[30] = "xxxxxxxxxxxxxxxxxxxxxxxxxx";
fgets(str, 20, pf);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
把’\n’读取后就不再读取了
现在我们把全部内容都读出来:
#include<stdio.h>
int main()
{
//打开文件
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("Open test.txt(r)");
return;
}
//文件操作
char str[30] = "xxxxxxxxxxxxxxxxxxxxxxxxxx";
char* i = NULL;
while ((i = fgets(str, 20, pf)) != NULL)
{
printf("%s", str);
}
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
要是把num改成2也能全部读出来吗?(改成1可就真读不出来了,因为实际读取个数是num-1)
#include<stdio.h>
int main()
{
//打开文件
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("Open test.txt(r)");
return;
}
//文件操作
char str[30] = "xxxxxxxxxxxxxxxxxxxxxxxxxx";
char* i = NULL;
while ((i = fgets(str, 2, pf)) != NULL)
{
printf("%s", str);
}
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
为什么呢?
当num等于2的时候,实际读取字符是一个;
第一次读取,读到’h’,str就是"h\0"(后面内容打印不出来,我们就不考虑了)打印了一个’h’
第二次读取,读到’e’,str就是"e\0",打印了一个’e’
读了很多次,读到’\n’,str就是"\n\0",打印换行
随笔——文件操作——使用fgets读取文本
同样,文件输入流只是输入流的一部分,而fgets是适用于所有输入流的,比如标准输入流
#include<stdio.h>
int main()
{
char str[20] = { 0 };
//输入"hello world\n"
fgets(str, 20, stdin);
printf("str = %s", str);
return 0;
}
如果最后不按回车,则必须输满19个字符才能进行下一步的打印操作
fprintf
头文件:stdio.h
函数主要功能:将格式化的数据输出到文件上
函数声明:
int fprintf (FILE * stream,const char * format,...);
一看这名字就知道这函数肯定和printf有关系,实际上,printf就是fprintf的简化版,printf的函数声明是
int printf ( const char * format, ... );
就比fprintf少一个参数,其它都是一样的,返回值都是成功输出的数据个数,如果出错,就返回EOF,错误信息可以用strerror来接收
#include<stdio.h>
//这里为了多整几个格式,声明一个结构体
struct t
{
char name[20];
int age;
float score;
};
int main()
{
struct t e = { "张三",20,65.6f };
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
perror("Open test.txt(w)");
return;
}
fprintf(pf, "%s %d %f", e.name, e.age, e.score);
fclose(pf);
pf = NULL;
return 0;
}
同样的,文件输出流仅是输出流中的一部分,而fprintf适用于所有的输入流,实际上,printf对fprintf简化就是默认那个输出流是标准输出流;
#include<stdio.h>
//这里为了多整几个格式,声明一个结构体
struct t
{
char name[20];
int age;
float score;
};
int main()
{
struct t e = { "张三",20,65.6f };
fprintf(stdout, "%s %d %f", e.name, e.age, e.score);
return 0;
}
fscanf
头文件:stdio.h
函数主要功能:把文件中格式化的数据输入到程序中
函数声明:
int fscanf (FILE * stream,const char * format,...);
一看这名字就知道这函数肯定和scanf有关系,实际上,scanf就是fscanf的简化版,scanf的函数声明是
int scanf ( const char * format, ... );
就比fscanf少一个参数,其它都是一样的,返回值都是成功输入的数据个数,如果出错,就返回EOF,错误信息可以用strerror来接收
#include<stdio.h>
//这里为了多整几个格式,声明一个结构体
struct t
{
char name[20];
int age;
float score;
};
int main()
{
struct t e = { 0 };
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("Open test.txt(r)");
return;
}
fscanf(pf, "%s %d %f", e.name, &(e.age), &(e.score));
fprintf(stdout, "%s %d %f", e.name, e.age, e.score);
fclose(pf);
pf = NULL;
return 0;
}
同样的,文件输入流只是输入流中的一种,而fscanf适用于所有输入流,比如,标准输入流
#include<stdio.h>
//这里为了多整几个格式,声明一个结构体
struct t
{
char name[20];
int age;
float score;
};
int main()
{
struct t e = { 0 };
//输入张三 20 65.6f
fscanf(stdin, "%s %d %f", e.name, &(e.age), &(e.score));
fprintf(stdout, "%s %d %f", e.name, e.age, e.score);
return 0;
}
题外话
sprintf
头文件:stdio.h
函数功能:将格式化的数据输出到字符流,或者通俗的说,是把格式化的数据转化成字符串
函数声明:
int sprintf ( char * str, const char * format, ... ) ;
如果成功,返回输出的数据个数,失败,返回负数
#include<stdio.h>
//这里为了多整几个格式,声明一个结构体
struct t
{
char name[20];
int age;
float score;
};
int main()
{
struct t e = { "张三",20,65.6f };
char str[100] = { 0 };
sprintf(str, "%s %d %f", e.name, e.age, e.score);
fprintf(stdout, "%s", str);
return 0;
}
sscanf
头文件:stdio.h
函数功能:把字符流中的格式化数据输入到程序中,或者说把字符串转换成格式化的数据
函数声明:
int sscanf (const char * s,const char * format,...);
返回值是成功输入的数据个数,如果出错,就返回EOF
#include<stdio.h>
//这里为了多整几个格式,声明一个结构体
struct t
{
char name[20];
int age;
float score;
};
int main()
{
struct t e = { "张三",20,65.6f };
struct t s = { 0 };
char str[100] = { 0 };
sprintf(str, "%s %d %f", e.name, e.age, e.score);
sscanf(str, "%s %d %f", s.name, &(s.age), &(s.score));
fprintf(stdout, "%s %d %f", s.name, s.age, s.score);
return 0;
}
fwrite
头文件:stdio.h
函数功能:把数据以二进制的形式输出到文件中,本函数只适用于文件输出流
函数声明:
size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream );
从ptr指向的地址开始,把count个元素(元素大小为size字节)的数据以二进制的形式输出到stream 指向的文件输出流中
#include<stdio.h>
int main()
{
int arr[5] = { 1,2,3,4,5 };
FILE* pf = fopen("test.txt", "wb");
if (pf == NULL)
{
perror("Open test.txt(wb)");
return;
}
size_t sz = sizeof(arr) / sizeof(arr[0]);
fwrite(arr, sizeof(arr[0]), sz, pf);
fclose(pf);
pf = NULL;
return 0;
}
右键源文件,选择现有项,添加test.txt,右键test.txt,点击打开方式,选择二进制编辑器
fread
头文件:stdio.h
函数功能:把二进制数据输入到程序中,本函数只适用于文件输入流
函数声明:
size_t fread ( void * ptr, size_t size, size_t count, FILE * stream );
从stream指向的文件输入流中读取二进制序列,将它们视为const个大小为size的数据存入ptr指向的内存块中
返回成功读取的元素总数
#include<stdio.h>
int main()
{
int arr[5] = { 1,2,3,4,5 };
FILE* pf = fopen("test.txt", "wb");
if (pf == NULL)
{
perror("Open test.txt(wb)");
return;
}
size_t sz = sizeof(arr) / sizeof(arr[0]);
fwrite(arr, sizeof(arr[0]), sz, pf);
fclose(pf);
pf = NULL;
int a[5] = { 0 };
pf = fopen("test.txt", "rb");
if (pf == NULL)
{
perror("Open test.txt(rb)");
return;
}
size_t t = sizeof(a) / sizeof(a[0]);
fread(a, sizeof(int), t, pf);
int i = 0;
for (; i < 5; i++)
{
printf("%d ", a[i]);
}
fclose(pf);
pf = NULL;
return 0;
}
有时候,可能不知道文件里到底有多少数据,那怎么一次性全读出来呢?这时候就要看返回值了,如果返回值小于count不就说明读到末尾读完了吗?(当然前提是数组足够大)
#include<stdio.h>
int main()
{
int arr[5] = { 1,2,3,4,5 };
FILE* pf = fopen("test.txt", "wb");
if (pf == NULL)
{
perror("Open test.txt(wb)");
return;
}
size_t sz = sizeof(arr) / sizeof(arr[0]);
fwrite(arr, sizeof(arr[0]), sz, pf);
fclose(pf);
pf = NULL;
pf = fopen("test.txt", "rb");
if (pf == NULL)
{
perror("Open test.txt(rb)");
return;
}
int a[5] = { 0 };
int i = 0;
while (fread(a + i, sizeof(int), 1, pf))
{
i++;
}
for (i = 0; i < 5; i++)
{
printf("%d ", a[i]);
}
fclose(pf);
pf = NULL;
return 0;
}
文件的随机读写
前面我们说的都是顺序读写,这顺序读写的顺序体现在哪里呢?体现在光标的移动方向是固定的,光标只能被动地跟随数据的读写而改变。比如说,现在test.txt文件内容是这样的:
我们以"r"的形式打开这个文件流,初始的光标位置就在文本开始位置,也就是"h"前面,那现在我们用来fgetc读一个字符,就会读出字符"h",光标也会移动,跑到"e"前面,以此类推,我们每读取一定长度的文本,光标也会向后移动一定长度,其他模式也是如此。
如果要对文章进行随机读写,或者说,主动地改变光标位置,则需要借助于一些函数
fseek
头文件:stdio.h
函数功能:更改文件流的光标位置
函数声明:
int fseek ( FILE * stream, long int offset, int origin );
其中的stream是要更改光标的文件流,offset是偏移量,origin 是偏移量的参考基准,origin 有三种选择,分别是SEEK_SET(文件开头),SEEK_CUR(光标当前位置),SEEK_END(文件末尾)。
#include<stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
perror("Open test.txt(w)");
return;
}
fputs("abcdefghi\n", pf);
fclose(pf);
pf = NULL;
pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("Open text.txt(r)");
return;
}
int ch = 0;
ch = fgetc(pf);
printf("%c", ch);
fseek(pf, 0, SEEK_SET);
ch = fgetc(pf);
printf("%c", ch);
fclose(pf);
pf = NULL;
return 0;
}
#include<stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
perror("Open test.txt(w)");
return;
}
fputs("abcdefghi\n", pf);
fclose(pf);
pf = NULL;
pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("Open text.txt(r)");
return;
}
int ch = 0;
ch = fgetc(pf);
printf("%c", ch);
fseek(pf, 1, SEEK_SET);
ch = fgetc(pf);
printf("%c", ch);
fclose(pf);
pf = NULL;
return 0;
}
#include<stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
perror("Open test.txt(w)");
return;
}
fputs("abcdefghi\n", pf);
fclose(pf);
pf = NULL;
pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("Open text.txt(r)");
return;
}
int ch = 0;
ch = fgetc(pf);
printf("%c", ch);
fseek(pf, 2, SEEK_SET);
ch = fgetc(pf);
printf("%c", ch);
fclose(pf);
pf = NULL;
return 0;
}
#include<stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
perror("Open test.txt(w)");
return;
}
fputs("abcdefghi\n", pf);
fclose(pf);
pf = NULL;
pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("Open text.txt(r)");
return;
}
int ch = 0;
ch = fgetc(pf);
printf("%c", ch);
fseek(pf, 2, SEEK_CUR);
ch = fgetc(pf);
printf("%c", ch);
fclose(pf);
pf = NULL;
return 0;
}
#include<stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
perror("Open test.txt(w)");
return;
}
fputs("abcdefghi\n", pf);
fclose(pf);
pf = NULL;
pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("Open text.txt(r)");
return;
}
int ch = 0;
ch = fgetc(pf);
printf("%c", ch);
fseek(pf, 3, SEEK_CUR);
ch = fgetc(pf);
printf("%c", ch);
fclose(pf);
pf = NULL;
return 0;
}
#include<stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
perror("Open test.txt(w)");
return;
}
fputs("abcdefghi\n", pf);
fclose(pf);
pf = NULL;
pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("Open text.txt(r)");
return;
}
int ch = 0;
ch = fgetc(pf);
printf("%c", ch);
fseek(pf, 0, SEEK_END);
ch = fgetc(pf);
printf("%d", ch);
fclose(pf);
pf = NULL;
return 0;
}
#include<stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
perror("Open test.txt(w)");
return;
}
fputs("abcdefghi\n", pf);
fclose(pf);
pf = NULL;
pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("Open text.txt(r)");
return;
}
int ch = 0;
ch = fgetc(pf);
printf("%c", ch);
fseek(pf, -1, SEEK_END);
ch = fgetc(pf);
printf("%c", ch);
fclose(pf);
pf = NULL;
return 0;
}
#include<stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
perror("Open test.txt(w)");
return;
}
fputs("abcdefghi\n", pf);
fclose(pf);
pf = NULL;
pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("Open text.txt(r)");
return;
}
int ch = 0;
ch = fgetc(pf);
printf("%c", ch);
fseek(pf, -2, SEEK_END);
ch = fgetc(pf);
printf("%c", ch);
fclose(pf);
pf = NULL;
return 0;
}
#include<stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
perror("Open test.txt(w)");
return;
}
fputs("abcdefghi\n", pf);
fclose(pf);
pf = NULL;
pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("Open text.txt(r)");
return;
}
int ch = 0;
ch = fgetc(pf);
printf("%c", ch);
fseek(pf, -3, SEEK_END);
ch = fgetc(pf);
printf("%c", ch);
fclose(pf);
pf = NULL;
return 0;
}
ftell
头文件:stdio.h
函数功能:返回基于起始位置的偏移量
函数声明:
long int ftell ( FILE * stream );
失败返回EOF
#include<stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
perror("Open test.txt(w)");
return;
}
fputs("abcdefghi\n", pf);
fclose(pf);
pf = NULL;
pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("Open text.txt(r)");
return;
}
int ch = 0;
ch = fgetc(pf);
printf("%c\n", ch);
fseek(pf, -3, SEEK_END);
printf("%d", ftell(pf));
fclose(pf);
pf = NULL;
return 0;
}
这个函数的妙用是计算文本的字节数
#include<stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
perror("Open test.txt(w)");
return;
}
fputs("abcdefghi\n", pf);
fclose(pf);
pf = NULL;
pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("Open text.txt(r)");
return;
}
fseek(pf, 0, SEEK_END);
printf("%d", ftell(pf));
fclose(pf);
pf = NULL;
return 0;
}
当然,这里"\n"被拆分成"\r\n"了,要注意一下。
rewind
头文件:stdio.h
函数功能:将光标移动到文本开头
函数声明:
void rewind ( FILE * stream );
#include<stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
perror("Open test.txt(w)");
return;
}
fputs("abcdefghi\n", pf);
fclose(pf);
pf = NULL;
pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("Open text.txt(r)");
return;
}
int ch = 0;
ch = fgetc(pf);
printf("%c", ch);
fseek(pf, -3, SEEK_END);
ch = fgetc(pf);
printf("%c", ch);
rewind(pf);
ch = fgetc(pf);
printf("%c", ch);
fclose(pf);
pf = NULL;
return 0;
}
文件读取的判定
经过上面的学习,我们知道有两种情况会导致函数读取失败(或者说,返回EOF或NULL):
- 读到了文本末尾
- 读取错误
如何辨别这两种情况呢?
这就需要函数feof和ferror了,之前我们说过,结构体FILE中存储着流的相关信息,对于文件流来说,有些信息就是用来描述读取失败的原因,当读取失败后,就可以通过feof和ferror来调用这些信息。如果是因为读到文本末尾而导致的读取失败,feof会返回非零,ferror会返回零;如果是因为读取错误而导致的读取失败,feof会返回零,ferror会返回非零。它们都只有一个参数,就是文件指针。
#include<stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
perror("Open test.txt(w)");
return;
}
fputs("hello world\n", pf);
fputs("abcdefghi\n", pf);
fclose(pf);
pf = NULL;
pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("Open test.txt(r)");
return;
}
int ch = 0;
while ((ch = fgetc(pf)) != EOF)
{
printf("%c", ch);
}
printf("\n");
if (feof(pf))
{
printf("成功读取至文本末尾");
}
else
{
perror("fgetc(pf)读取错误");
}
fclose(pf);
pf = NULL;
return 0;
}
这里我们故意写个错的程序(错误原因:光标到末尾后没有用rewind移到开头):
#include<stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "w+");
if (pf == NULL)
{
perror("Open test.txt(w+)");
return;
}
fputs("hello world\n", pf);
fputs("abcdefghi\n", pf);
/*fclose(pf);
pf = NULL;
pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("Open test.txt(r)");
return;
}*/
int ch = 0;
while ((ch = fgetc(pf)) != EOF)
{
printf("%c", ch);
}
printf("\n");
if (feof(pf))
{
printf("成功读取至文本末尾");
}
else
{
perror("fgetc(pf)读取错误");
}
fclose(pf);
pf = NULL;
return 0;
}
文件缓冲区
内存块中的数据并不是直接与硬盘上的文件进行信息交流的,而是借助于内存中的一个特殊区域“文件缓冲区”进行交流的,如果是输出数据,就先把数据存在文件缓冲区,等文件缓冲区满了之后,或者刷新文件缓冲区再把数据输出到文件中;同样的,如果是输入数据,也会暂存在文件缓冲区,等刷新或者满了再传过去。
试想一下,你现在就是操作系统,如果没有缓冲区,每来一个字节我就要传过去,这是不是特别麻烦?而且这样的话,我几乎不能做其他事了?有了文件缓冲区,就可以先把一定大小的数据暂存下来,等够了一次性传过去,或者用fclose的形式刷新缓冲区(使用fclose关闭文件流就会刷新缓冲区,缓冲区被刷新之后,里面的内容也会被传输过去)以下代码可以检测缓冲区的存在:
随笔——文件操作——实验文件缓冲区的存在
由此可以得出⼀个结论:
因为有缓冲区的存在,C语言在操作文件的时候,需要做刷新缓冲区或者在文件操作结束的时候关闭文件。如果不做,可能导致读写文件的问题。