C语言文件操作的入门详解教程
一.一些需要掌握的知识点
文件有千千万万,但是在我们的程序设计当中,我们谈的文件一般有两种:
1.程序文件
包括源程序文件(后缀为.c),目标文件(windows环境后缀为.obj),可执行程序(windows环境后缀为.exe)。
2.数据文件
文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件。
而在本节中,我们主要提到的是数据文件。
1.文件名
我们知道,名字都是用来标识和区别事物的,那么文件名也是这样,是区别各个文件的标识。
一个文件名要包含 3 个部分:文件路径+文件名主干+文件后缀
如:C:\Windows\System32\drivers\etc.txt
其中 :C:\Windows\System32\drivers\ 是文件路径,etc 是文件名主干,txt 是文件名后缀。
当然了,各个平台的文件路径并不相同,以及为了方便起见文件标识通常别称为文件名
2.文件类型
根据数据的组织形式,数据文件被称为文本文件或者二进制文件。
二进制文件:数据在内存中以二进制的形式存储,并不加转换的输出到外存。
文本文件:要求在外存上以ASCII码的形式存储,需要在存储前转换,以ASCII字符的形式存储的文件。
那么一个数据在内存中是怎样存储的呢?
字符一律以ASCII形式存储,数值型数据既可以用ASCII形式存储,也可以使用二进制形式存储。
如有整数10000,如果以ASCII码的形式输出到磁盘,则磁盘中占用5个字节(每个字符一个字节)
而二进制形式输出,则在磁盘上只占4个字节(VS2019测试)。
如:
我们可以测试一番:
#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; }
我们打开的时候要注意以二进制编辑器来打开,你会发现出现了如下图显示的一串数字,其中它们是以十六进制现实的,转换一下,刚好是上图显示的那串二进制数字(注意VS采用的是小端储存模式)
3.数据流
数据流:
指程序于数据的交互是以流的形式进行的,包括输入流与输出流;
输入流:
程序从输入流读取数据源。数据源包括键盘,文件,网络等,即:将数据源读入到程序的外界通道。
输出流:
程序向输出流写入数据。将程序中的数据输出到外界(显示器,打印机,文件,网络,等)的通信通道。
采用数据流的目的:使得输入输出独立于设备,不关心数据源来自何方,也不管输出的目的地是何种设备。
4.文件缓冲区
缓冲区:
指在程序运行时,所提供的额外内存,可用来暂时存放做准备执行的数据。它可在创建、访问、删除静态数据上,大大提高运行速度(速度的提高程度有时甚至可高达几十倍),
为我们提供了极大的便捷,节省了大量的时间与精力
文件缓冲区:
ANSIC 标准采用“缓冲文件系统”处理的数据文件的,所谓缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块“文件缓冲区”。
从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上。如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),
然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓冲区的大小根据C编译系统决定的。
如:
无论是输入输出,都先在缓冲区里存着,然后在进行输入输出。
5.文件指针
缓冲文件系统中,关键的概念是“文件类型指针”,简称“文件指针”。
每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(如文件的名字,文件状态及文件当前的位置等)。
这些信息是保存在一个结构体变量中的。该结构体类型是有系统声明的,取名FILE。
我们可以来看在VS 2019中FILE的声明:
//-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // // Stream I/O Declarations Required by this Header // //-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ #ifndef _FILE_DEFINED #define _FILE_DEFINED typedef struct _iobuf { void* _Placeholder; } FILE; #endif
不同的C编译器的FILE类型包含的内容不完全相同,但是大同小异。
每当打开一个文件的时候,系统会根据文件的情况自动创建一个FILE结构的变量,并填充其中的信息,使用者不必关心细节。
一般都是通过一个FILE的指针来维护这个FILE结构的变量,这样使用起来更加方便。
如下:我们便创建了一个文件指针
FILE* pf;//文件指针变量
定义pf是一个指向FILE类型数据的指针变量。可以使pf指向某个文件的文件信息区(是一个结构体变量)。
通过该文件信息区中的信息就能够访问该文件。也就是说,通过文件指针变量能够找到与它关联的文件。
比如:
到此,我们的基本概念就结束了,下面进入到函数部分:
二.与文件操作相关的一些函数
1.文件的打开及关闭
文件在使用前,我们肯定要打开文件;在使用结束后,我们需要关闭文件。
1.fopen() --- 文件打开函数
声明:文档
FILE* fopen(const char* filename, const char* mode);
参数:
const char* filename ---- 文件名
const char* mode ---- 文件打开方式
文件打开方式:
那,接下来看一个实例:
/* fopen example */ #include <stdio.h> int main() { FILE* pFile; pFile = fopen("myfile.txt", "w");//打开一个文件(没有就创建),以写的方式打开 if (pFile != NULL)//如果打开成功 { fputs("fopen example", pFile);//就往文件里写入 fopen example fclose(pFile);//关闭文件 } return 0; }
2.fclose() --- 文件关闭函数
声明:文档
int fclose(FILE* stream);
参数为要关闭文件的文件指针
示例:
int main() { //相对路径 //.. 表示上一级目录 //. 当前目录 //FILE* pf = fopen("../data.txt", "r");//在上一级文件中打开data.txt,如果没有就报错 //绝对路径 C:\Windows\System32\drivers\etc.txt //./hehe/test.txt //../../ FILE* pf = fopen("../../data.txt", "r"); if (pf == NULL) { printf("打开文件失败\n"); printf("%s\n", strerror(errno));//注意头文件的包含 stding.h errno.h return 1;//失败返回 } //打开文件成功 printf("打开文件成功\n"); //读写文件 //... //关闭文件 fclose(pf); pf = NULL;//及时置NULL return 0; }
注:
其他的文件打开模式,将在函数讲解的时候一并讲解:
2.文件的顺序读写
1. 字符输入输出函数
fput--- 向指定输出流输出一个字符声明
int fputc ( int character, FILE * stream );
参数:
int character --- 所输入的字符
FILE * stream --- 指定输出流
fgetc--- 向指定输入流输入一个字符声明
int fgetc ( FILE * stream );
参数:
FILE * stream --- 指定输入流
示例 1:
//在文件里写入a-z26个字母 int main() { //fopen函数如果是以写的形式打开 //如果文件不错在,会创建这个文件 //如果文件存在,会清空文件的内容 //fopen函数如果是以读的形式打开 //文件不存在打开失败 FILE* pf = fopen("data.txt", "w"); if (pf == NULL) { printf("%s\n", strerror(errno)); return 1;//失败返回 } //写文件 int i = 0; for (i = 'a'; i <= 'z'; i++) { fputc(i, pf);//在文件里写 --- pf 我们自己定义的文件指针 fputc(i, stdout);//显示在屏幕上 --- stdout --- 标准输出流 } // 从键盘输入 --- stdin --- 标准输入流 //关闭文件 fclose(pf); pf = NULL; return 0; }
这里需要注意的就是所指定的输出流:
在C语言所写的程序运行起来时,会默认打开三个流:
1.stdin - 标准输入流 (键盘)
2.stdout - 标准输出流 (屏幕)
3.stderr - 标准错误流(屏幕)
示例 2:
//从刚才写的文件,再把内容读出来 int main() { FILE* pf = fopen("data.txt", "r");// r 是以读的形式打开文件,如果没有该文件就报错 if (pf == NULL) { printf("%s\n", strerror(errno)); 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 //ch = fgetc(pf); //printf("%c\n", ch);//d int ch = 0; while ((ch = fgetc(pf)) != EOF)// pf --- 所指定的输入流 { printf("%c ", ch); } //关闭文件 fclose(pf); pf = NULL; return 0; }
这里需要注意一点:
fgetc() 的返回值:我们在这就只说出现错误的信息:
If the position indicator was at theend-of-file, the function returnsEOFand sets theeof indicator(feof) ofstream.
If some other reading error happens, the function also returnsEOF, but sets itserror indicator(ferror) instead.
翻译过来就是:
如果文件指针处于文件末端,则函数返回EOF,并设置流的eof指示器(feof)
如果发生其他读数错误,函数也会返回EOF,但会设置错误指示器(ferror)
这里的feof和ferror我们专门会放到最后讲
2.文本行输入输出函数
fputs--- 文本行输出行数声明
int fputs ( const char * str, FILE * stream );
参数:
const char * str --- 将被写入输出流的字符指针
FILE * stream --- 输出流
fgets--- 文本行输入函数声明
char * fgets ( char * str, int num, FILE * stream );
参数:
char * str --- 所输入信息的存放位置
int num --- 要读内容的大小
FILE * stream --- 输入流
示例 1:
int main() { FILE* pf = fopen("data.txt", "a");// a --- 如果没有就该文件就创建,有就在该文件后方继续追加内容 if (pf == NULL) // 而 w 如果存在该文件会覆盖重写 { printf("%s\n", strerror(errno)); return 1; } //写一行数据 fputs("hello\n", pf);//输出到文件中 fputs("hello\n", stdout);//在屏幕上显示 fputs("hello world\n", pf);//输出到文件中 fputs("hello world\n",stdout);//在屏幕上显示 fclose(pf); pf = NULL; return 0; }
示例 2:
int main() { char arr[100] = {0};//存放写入的信息 FILE* pf = fopen("data.txt", "r"); if (pf == NULL) { printf("%s\n", strerror(errno)); return 1; } //读一行数据 //fgets(arr, 100, pf); //printf("%s\n", arr); while (fgets(arr, 10, pf) != NULL)//输出---一次读十个 { printf("%s", arr); } //fgets从标准输入流中读取 fgets(arr, 100, stdin); printf("%s\n", arr); fclose(pf); pf = NULL; return 0; }
注意:
1.在fgets中num的大小数包含 \0 ,在内的,如果所输入的内容大小大于指定的大小,fgets会强行截断加 \0.
2.返回值的处理:
If theend-of-fileis encountered while attempting to read a character, theeof indicatoris set (feof). If this happens before any characters could be read, the pointer returned is a null pointer
(andthe contentsofstrremain unchanged).
If a read error occurs, theerror indicator(ferror) is set and a null pointer is also returned (but the contents pointed bystrmay have changed).
简单点说:遇到错误和文件结尾都会返回NULL,但会设置不同的指示器(feof,ferror)(后面会说)
3.格式化输入输出函数
fprintf--- 格式化输出函数 声明
int fprintf ( FILE * stream, const char * format, ... );
fscanf--- 格式化输入函数声明
int fscanf ( FILE * stream, const char * format, ... );
别看他们俩长得花里胡哨的,但是使用却和我们的printf,scanf大致相同,只是多出了一个流的填写
我们来看看 printf 和 scanf的声明:
int scanf ( const char * format, ... );int printf ( const char * format, ... );
所以我们来看一个示例:
示例 1:
struct Stu { char name[20]; int age; float score; }; int main() { struct Stu s = {"zhangsan", 20, 66.5f}; FILE* pf = fopen("data.txt", "w"); if (pf == NULL) { printf("%s\n", strerror(errno)); return 1; } //格式化的写入 fprintf(pf,"%s %d %f", s.name, s.age, s.score); //printf("%s %d %f", s.name, s.age, s.score);//对比一下只是差了一个 流 的指定 //关闭文件 fclose(pf); pf = NULL; return 0; }
注意:
读取错误或结束时,fscanf 的返回值也是EOF,判断同 fgetc
这里其实还有一组与它俩相似的函数 ---sprintf以及sscanf (点击函数名看文档)
它俩是干什么的呢,一个是把结构化的数据转换为字符串,一个是把字符串转化为结构化的数据
同样,我们来看看声明:
[code]int sprintf ( char * str, const char * format, ... );
char * str --- 我们要把格式化生成的字符串所存放的地址
int sscanf ( const char * s, const char * format, ...);
const char * s --- 我们所要读取的字符串
示例:
struct Stu { char name[20]; int age; float score; }; int main() { struct Stu s = {"zhangsan", 20, 66.5f}; char buf[200] = { 0 }; //sprintf可以把结构化的数据转换为一个字符串 sprintf(buf, "%s %d %f", s.name, s.age, s.score); printf("按照字符串的形式:%s\n", buf); struct Stu tmp = { 0 }; //sscanf可以把一个字符串转换为一个结构化的数据 sscanf(buf, "%s %d %f", tmp.name, &(tmp.age), &(tmp.score)); printf("按照格式化的形式:%s %d %f\n", tmp.name, tmp.age, tmp.score); return 0; }
4.二进制输入输出函数
fwrite--- 二进制写文档
size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream );
这个函数解释一下就是:将来自 ptr 指向的数据,一次写size个字节,共写 count 次,输出到 stream 指定的流中
fread--- 二进制读文档
size_t fread ( void * ptr, size_t size, size_t count, FILE * stream );
这个函数解释一下就是:将 stream 指定的流中 读 count 次,一次读 size 个字节,存到 ptr 所指向的内容中
示例:
// data.txt 内容 zhangsan 20 66.500000 int main() { struct Stu s = {0}; FILE* pf = fopen("data.txt", "rb");//binary --- 二进制 if (pf == NULL) // rb --- 二进制读 { printf("%s\n", strerror(errno)); return 1; } //读文件-二进制 fread(&s, sizeof(struct Stu), 1, pf); printf("%s %d %f\n", s.name, s.age, s.score); fclose(pf); pf = NULL; return 0; }
注:
若读取时发现读取的内容的个数比指定的最大个数小时,就结束,然后判断是读到文件末尾,还是读取失败
示例 2:
int main() { int a = 10000; FILE*pf = fopen("bin.dat", "wb");//二进制写 if (pf == NULL) { return 1; } fwrite(&a, sizeof(int), 1, pf); fclose(pf); pf = NULL; return 0; }
3.文件的随机读写
1.seek ---根据文件指针的位置和偏移量来定位文件指针。
int fseek ( FILE * stream, long int offset, int origin );
FILE * stream --- 流
long int offset --- 偏移量
int origin --- 从哪里开始,有三个选择
1.SEEK_SET --- 从文件头开始
2.SEEK_CUR --- 从当前位置开始
3.SEEk_END --- 从文件末尾开始
示例:
//此时文件内容为:123456789 int main() { FILE* fp = fopen("data.txt", "r"); if (fp == NULL) { perror("\n"); exit(1); } char a = fgetc(fp); printf("%c ", a);//此时结果为 1,现在指针指向 2 a = fgetc(fp);//此时读取2,指针指向 3 printf("%c ", a); fseek(fp, -1, SEEK_END);//将文件指针置于文章末尾 a = fgetc(fp);//此时读取9,指针再次指向末尾 printf("%c ", a); fseek(fp, 1, SEEK_SET);//将文件指针置于文章头 a = fgetc(fp);//此时读取1,指针再次指向2 printf("%c ", a); fclose(fp); fp = NULL; return 0; }
2.ftell ---返回文件指针相对于起始位置的偏移量
long int ftell ( FILE * stream );
3.rewind ---让文件指针的位置回到文件的起始位置
void rewind ( FILE * stream );
示例:
int main() { FILE*pf = fopen("data.txt", "r"); if (pf == NULL) { return 1; } //读取 int ch = fgetc(pf); printf("%c\n", ch); ch = fgetc(pf); printf("%c\n", ch); //定位文件指针到文件的起始位置 //fseek(pf, -2,SEEK_CUR); //fseek(pf, 0, SEEK_SET); //printf("%d\n", ftell(pf)); rewind(pf); ch = fgetc(pf);//要在这里读取'a' printf("%c\n", ch); fclose(pf); pf = NULL; return 0; }
4.文件缓存区的刷新
我们曾在上文提到文件缓冲区的概念:
缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块“文件缓冲区”。
从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上。
如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。
缓冲区的大小根据C编译系统决定的。
所以,当我们在程序中命令计算机往文件中写一些东西的时候,如果我们想在输出语句已结束,文件就有内容(即需要写的内容从文件缓冲区写到了文件中),
此时,我们不妨fflush一下
fflush --- 刷新文件缓存区
int fflush ( FILE * stream );
参数就是我们所定义的文件指针。
示例:
#include <stdio.h> #include <windows.h> //VS2019 WIN10环境测试 int main() { FILE* pf = fopen("test.txt", "w"); fputs("abcdef", pf);//先将代码放在输出缓冲区 printf("睡眠10秒-已经写数据了,打开test.txt文件,发现文件没有内容\n"); Sleep(10000); printf("刷新缓冲区\n"); fflush(pf);//刷新缓冲区时,才将输出缓冲区的数据写到文件(磁盘) //注:fflush 在一些编译器上并不能使用 printf("再睡眠10秒-此时,再次打开test.txt文件,文件有内容了\n"); Sleep(10000); fclose(pf); //注:fclose在关闭文件的时候,也会刷新缓冲区 pf = NULL; return 0; }
三.一个易被误用的点
feof的错误使用
在刚才的讲解中,我们已经都说过了各输入函数遇到错误时的返回值,所以我们在写文件循环读取的循环条件时,一定要注意各函数的返回值的区别 !
在不满足循环条件后 判断是发生错误跳出,还是读到文本末尾结束(这就是前面所说到的指示器 feof --- 文本结束, ferror --- 遇到错误)
如图:
示例:
int main() { FILE*pf = fopen("data.txt", "r"); if (pf == NULL) { return 1; } //读取 int ch = 0; while ((ch = fgetc(pf)) != EOF) { printf("%c ", ch); } //找结束的原因 if (ferror(pf)) { printf("读取是发生错误,失败,而结束\n"); } else if (feof(pf)) { printf("遇到文件末尾,而结束的\n"); } fclose(pf); pf = NULL; return 0; }
所以,我们在日常使用的时候要注意这个点!