下面是本文的目录啊,嫌太长直接挑想看的看就行了
一些碎碎念
啊!鄙人好像因为各种烦(lan)事(ai)缠(wan)身(qi)有很久没有写博客了,连CSDN的Markdown编辑器语法都快忘光了,现在竟然沦落到要边码字边看帮助文档的地步TAT。
不扯了,最近开始复习考研的专业课了(看这篇文章所属的分类),以前主要是用C语言给单片机写驱动程序,对C语言的文件操作接触比较少,正好借考研给自己查漏补缺,把文件操作这一块的知识补齐。
文件指针变量
在ANSI C中采用了“缓冲文件系统”来处理数据文件。当需要读写文件时,C语言并不是直接读写磁盘,而是在内存中开辟一块文件缓冲区,在里面存储相关文件的信息和内容。当要读取文件时,通过指向这一缓冲区的指针变量可以读取缓冲区中的文件内容和一些缓冲区信息。而当要写入文件时,也是先写入文件缓冲区,然后再保存到磁盘。指向文件缓冲区的指针变量我们称之为文件指针变量,在C语言中,我们可以通过如下语句声明一个文件指针变量:
FILE *fp;//声明一个名为 fp 的文件指针
很明显,文件指针的类型是 FILE * ,这是FILE结构体变量的指针类型,在FILE这个结构体中除了包含用来指向文件数据缓冲区地址的成员变量外,还有很多变量用来指示缓冲区本身的状态信息。FILE结构体的原型声明见下面代码(此处使用的是《C语言程序设计(第四版)》给出的声明,不同编译系统提供的声明可能有所不同,不过用起来都大同小异):
typedef struct
{
short levl; //缓冲区“满”或“空”的程度指示
unsigned flags; //文件状态标志
char fd; //文件描述符
unsigned char hold; //如缓冲区无内容则不读取
short bsize; //缓冲区大小
unsigned char *buffer; //数据缓冲区地址
unsigned char *curp; //指针当前指向
unsigned istenp; //临时文件指示器
short token; //有效性检查
} FILE;
其中很多变量完全不知道干嘛的啊有木有!!!幸好我们只要声明使用就行了。
文件操作相关的函数
C语言标准库中常用的基础文件操作相关函数共有12个,其中功能函数6个(fopen、fclose、rewind、fseek、ferror、clearerror),字符读写操作函数2个(fputc、fgetc),格式化字符读写操作函数2个(fprintf、fscanf),二进制读写操作函数2个(fread、fwrite),它们的函数接口都被包含在 stdlib.h 这一头文件中,在使用时要记得包含头文件。
文件的打开与关闭
要对文件进行读写操作,首先需要打开文件。这里的打开文件是指讲文件从磁盘等外部存储介质中读出,放入内存中开辟的文件缓冲区中,之后对文件的读写操作都在文件缓冲区中完成,避免直接读写外部存储介质导致的意外文件损坏。只要用fopen这个函数就可以轻松的打开文件啦,打开完后会返回一个文件指针,使用文件指针变量接收它就行,如果文件打开出错,返回的指针为NULL,可以使用if语句判断文件是否打开成功,并在出错时给出提示,之后的文件操作就全部通过这个文件指针变量来进行了,代码如下:
FILE *fp;//声明一个文件指针变量
fp = fopen("C:\\test.dat","r");//以读文本方式打开C盘根目录下的test.dat文件
有时为了谨慎起见,需要对文件是否打开成功进行判断,于是可以这么写:
FILE *fp;//声明一个文件指针变量
//以读文本方式打开C盘根目录下的test.dat文件,并判断打开是否成功
if((fp = fopen("C:\\test.dat","r")) == NULL){
printf("file open error!\n");
return;//终止函数运行,当然也可以采取其他处理措施
}
上述代码使用fopen函数打开文件,VS2017中的fopen函数接口如下:
_ACRTIMP FILE* __cdecl fopen(_In_z_ char const* _FileName, _In_z_ char const* _Mode );
由函数接口可知,使用fopen函数打开文件需要指定打开的文件名(这里的文件名需要完整文件名,即文件所在路径加文件的名字)与打开模式。fopen支持以下几种模式打开文件:
文件打开模式参数 | 含义 | 如果指定文件不存在 |
---|---|---|
r | 以只读模式打开一个文本文件 | 文件打开失败 |
w | 以写模式打开一个文本文件 | 在指定位置创建新文件 |
a | 以追加模式(文件位置标记移动至文件末尾)打开一个文本文件 | 文件打开失败 |
r+ | 以读写方式打开一个文本文件 | 文件打开失败 |
w+ | 以读写方式建立一个文本文件(文件已存在就直接打开) | 创建新文件 |
a+ | 以读写方式打开一个文本文件(文件指示标识移动至文件末尾) | 文件打开失败 |
rb | 以只读模式打开一个二进制文件 | 文件打开失败 |
wb | 以写模式打开一个二进制文件 | 在指定位置创建新文件 |
ab | 以追加模式(在文件末尾添加内容)打开一个二进制文件 | 文件打开失败 |
rb+ | 以读写方式打开一个二进制文件 | 文件打开失败 |
wb+ | 以读写方式建立一个二进制文件(文件已存在就直接打开) | 在指定位置创建新文件 |
ab+ | 以读写方式打开一个二进制文件(文件位置标记移动至文件末尾) | 文件打开失败 |
注1:文件位置标记,可以理解为记事本编辑时的光标,通过文件位置标记,可以指示当前的读取位置或者写入位置,默认情况下打开文件后会置于文件开头。
注2:关于文本文件与二进制文件的区别,可以参考这篇博文:C语言中文本文件与二进制文件的区别。
在对文件操作完成后,需要使用fclose函数关闭文件,这样文件缓冲区中的更改才会写入外部存储介质,不关闭文件直接退出程序可能导致文件更改丢失。如下就可关闭文件了:
fclose(fp);//关闭文件指针变量fp所指向的文件
上述代码使用fclose函数关闭文件,VS2017中的fclose函数接口如下:
_ACRTIMP int __cdecl fclose(_Inout_ FILE* _Stream);
由函数接口可知,使用fclose函数关闭文件需要给出对应的文件指针执行fclose函数后,文件指针变量所指向的文件缓冲区中的更改会被写入外部存储介质,然后文件缓冲区会被关闭以释放内存空间。如果一切正常的话,函数会返回0表示文件关闭成功,否则返回EOF(-1)。
文本文件的读写操作
常用的文本文件读写操作函数共有4个,个人觉得可以分为两组:普通字符读写函数和格式化字符串读写函数。
普通字符读写函数
普通字符串读写函数为 fputc 和 fgetc,分别负责写和读文件,由vs2017的标准库头文件可以得知这两个函数其实是两个宏定义(其他编译器可能情况不同?),如下:
#define fgetc(_Stream) _fgetc_nolock(_Stream)
#define fputc(_Ch, _Stream) _fputc_nolock(_Ch, _Stream)
fgetc宏定义对应的函数是_fgetc_nolock,fputc宏定义对应的函数是_fputc_nolock,在标准库中像这样通过宏定义调用函数的用法还有很多。
每使用一次fgetc或fputc函数,就会从文件中读取或者写入一个字节,同时文件指示标识向后移动一位为下次读写做好准备。下面是一个使用这两个函数进行文件读写的小栗子:
//从文件读取一个字符
FILE *fp; //声明一个文件指针
fp = fopen("test.txt","r"); //读方式打开运行目录下的test.txt文件
printf("%c",fgetc(fp)); //从fp指向的文件中读取一个字符并打印出来
fclose(fp); //关闭文件
fgetc函数使用时只需要指定读取的文件指针,读取成功时fgetc函数会返回当前读取的字符,出错时,返回EOF(-1)。
//写入一个字符到文件
FILE *fp; //声明一个文件指针
fp = fopen("test.txt","w"); //写方式打开运行目录下的test.txt文件
fputc('a',fp); //将字符a写入fp所指向的文件
fclose(fp); //关闭文件
fputc函数使用时需要指定写入的字符和文件指针,写入成功时返回写入文件的字符的ASCII码值,出错时,返回EOF(-1)。
格式化字符串读写函数
相信很多人在初学C语言时都接触过printf和scanf这两个格式化输出输入函数,printf用来向控制台输出格式化信息,scanf用来接收键盘输入的信息,并按指定格式赋值给各个接收变量。在对文件进行读写操作时,也有两个功能类似的函数,它们是fprintf和fscanf,用于向文件输入格式化数据或者从文件中读取格式化数据。
fprintf用于向文件写入格式化字符串,在VS2017中的函数接口如下:
_CRT_STDIO_INLINE int __CRTDECL fprintf(
_Inout_ FILE* const _Stream, //文件指针
_In_z_ _Printf_format_string_ char const* const _Format, //格式化字符串
...) //最后是可变参数表,用来指定格式化字符串中格式说明对应的内容
除了要指定文件指针外,用法其实和printf一样,一个使用的小栗子:
FILE *fp = fopen("test.txt","w"); //写方式打开文本文件
fprintf(fp, "%s+%s+%s\n%d", "I", "LOVE", "CHINA",20180821); //格式化写入字符串
然后在运行目录下打开文件,里面应该如下:
I+LOVE+CHINA
20180821
fscanf用于从文件读取格式化字符串,在VS2017中的函数接口如下:
_CRT_STDIO_INLINE int __CRTDECL fscanf(
_Inout_ FILE* const _Stream, //文件指针
_In_z_ _Scanf_format_string_ char const* const _Format, //格式化字符串
...) //最后是可变参数表,用来指定接收格式说明对应的内容
除了要指定文件指针外,用法其实和scanf一样,一个使用的小栗子:
首先前面的栗子里我们已经创建了text.txt文件,里面有如下内容
I+LOVE+CHINA
20180821
现在我想把 I+LOVE+CHINA 这个字符串赋值给一个字符数组,把 20180821 赋值给一个整型变量,于是我可以这么干:
char String[100]; //用来存储字符串
int Num; //存储整数
FILE *fp = fopen("test.txt", "r"); //读方式打开文件
fscanf(fp, "%s\n%d",String,&Num); //从文件中输入内容,按格式赋值给String和Num
printf("%s\n", String); //打印输出String
printf("%d\n", Num); //打印输出Num
运行效果就跟下面一样:
二进制文件的读写操作
使用字符方式读写文件,好处是文件方便用户查看,但是这是建立在牺牲效率的基础上的,为了将二进制代码转换为人们可读的字符,在写入文件时需要进行转换,会花费很多时间,而在将文件读入计算机内存时,又要将字符转换为计算机可识别的二进制代码,同样要花费很多时间。在需要频繁读写文件的情况下,可以考虑直接通过二进制方式读写文件以提高读写效率(不过就不能直接用记事本查看文件内容了喵~,有得必有失)。C语言标准库提供了专门的函数用于二进制读写,它们是fread和fwrite
fwrite用于以二进制格式向文件内写入内容,在VS2017中的接口声明如下:
_ACRTIMP size_t __cdecl fwrite(
_In_reads_bytes_(_ElementSize * _ElementCount) void const* _Buffer, //指向要写入数据开头的指针
_In_ size_t _ElementSize, //每个数据元素的大小
_In_ size_t _ElementCount,//写入的数据元素总数
_Inout_ FILE* _Stream //文件指针
);
要注意的是,因为是直接写入二进制,不像字符方式有明确的字符作为结尾(比如字符串都以’\0’结尾)所以要通过_ElementSize和_ElementCount这两个参数指定要写入的数据长度!
总写入数据长度=_ElementSize x _ElementCount
比如,若_ElementSize=sizeof(char),_ElementCount=20,则总写入长度为8x20=160个二进制位(一个字符占8个二进制位),相当于向文件里写入了20个字符,如果写入的数据长度不足20个字符,则写到数据尾会自动停止写入。
举个使用的小栗子:
char String[20] = "I LOVE CHINA !";//要写入的字符串
FILE *fp = fopen("test.dat", "wb");//二进制方式创立test.dat文件
fwrite(String, sizeof(char), 20, fp);//从String数组开头,每次写char类型那么长的一串二进制,共写20个
这样我们就在运行目录下得到了一个 test.dat 文件了,里面是表示”I LOVE CHINA !”这个字符串的二进制代码,当然,因为不是字符格式,直接用记事本打开只会看到一堆乱码。不过使用freadf函数就可以把内容再读出来啦。
fread用于从二进制文件内读取内容,在VS2017中的接口声明如下:
_ACRTIMP size_t __cdecl fread(
_Out_writes_bytes_(_ElementSize * _ElementCount) void* _Buffer, //指向用来存储读取数据位置的指针
_In_ size_t _ElementSize, //每个数据元素的大小
_In_ size_t _ElementCount,//读取的数据元素总数
_Inout_ FILE* _Stream //文件指针
);
要注意的是,因为是直接读取二进制,不像字符方式有明确的字符作为结尾(比如字符串都以’\0’结尾)所以要通过_ElementSize和_ElementCount这两个参数指定要读取的数据长度!
总读取数据长度=_ElementSize x _ElementCount
比如,若_ElementSize=sizeof(char),_ElementCount=20,则总读取长度为8x20=160个二进制位(一个字符占8个二进制位),相当于从文件里读取了20个字符,如果文件内数据长度不足20个字符,则读到文件尾会自动停止读取。
同样举个使用的小栗子,这个栗子用于将刚才写入test.dat文件的”I LOVE CHINA !”字符串读出:
char String[20]; //用来存储读出的内容
FILE *fp = fopen("test.dat", "rb"); //打开文件
fread(String, sizeof(char), 20, fp); //从文件中读取20个char类型长度的二进制位,存入String中
printf("%s\n", String); //打印String中内容
运行结果如下:
文件位置标记的控制
前面已经提过了,C语言的文件操作函数通过文件位置标记(想象一下打字时的光标)来确定当前的文件读写位置,每次使用fopen打开文件都会将文件位置标记初始化于文件开头(追加模式打开文件是初始化到文件末尾),每次对文件进行读写操作都会自动将文件位置标记向后移动到读写后的位置。那么问题来了,有时我不想从文件头开始读取文件,或者我想将刚才读过的文件位置再读一遍,该怎么办呢?C语言的标准库提供了文件位置标记控制函数来解决这个问题。它们分别是rewind函数和fseek函数。
rewind函数用于将光标重置到文件的开头,在VS2017中接口声明如下:
_ACRTIMP void __cdecl rewind(
_Inout_ FILE* _Stream //要重置文件位置标记的文件指针
);
feek函数用于从指定位置开始,将文件位置标记后移指定位数(这里指二进制位)在VS2017中接口声明如下:
_ACRTIMP int __cdecl fseek(
_Inout_ FILE* _Stream, //要移动文件位置标记的文件指针
_In_ long _Offset, //指定移动多少个二进制位
_In_ int _Origin //指定从文件的哪个位置开始移动
);
其中_Origin的参数可以从以下三个宏定义(定义在stdio.h文件中)中选一个,如下:
#define SEEK_CUR 1 //文件位置标记当前所在位置
#define SEEK_END 2 //文件结尾位置
#define SEEK_SET 0 //文件开头位置
其实直接填1、2、3也可以的 = =(小声bb)
下面当然又是一个使用它们的小栗子啦:
//三个存字符串的数组
char String1[20];
char String2[20];
char String3[20];
FILE *fp = fopen("test.dat", "rb"); //打开上面创建的test.dat文件
fread(String1, sizeof(char), 20, fp); //读一下
printf("String1 = %s\n", String1); //打印出来
rewind(fp); //重置文件位置标记到文件开头
fread(String2, sizeof(char), 20, fp); //再读一下
printf("String2 = %s\n", String2); //打印出来
fseek(fp, sizeof(char) * 2, SEEK_SET); //文件位置标记从开头向后移动两个字符那么长的二进制位
fread(String3, sizeof(char), 20, fp); //又读一下
printf("String3 = %s\n", String3); //还是打印出来
运行结果如下(注意String2和String3的不同):
文件读写出错的检测
有时因为某种意想不到(其实就是不知道怎么回事)的原因,可能会在文件读写时发生错误,C语言标准库提供了ferror函数和clearerr函数来检测错误是否发生(小声bb:其实自己检测函数读写函数的返回值也行)。
ferror函数会在每次使用文件读写函数对文件进行读写操作时更新返回数值,如果本次操作成功则返回0,否则返回一个非零值表示出错。其在VS2017中的函数接口声明如下:
_ACRTIMP int __cdecl ferror(
_In_ FILE* _Stream //要检测错误的文件读写操作对应文件指针
);
clearerr函数用来对ferror函数的返回值置0,应该在每次使用ferror函数后立刻使用,防止本次的检测返回值遗留影响到下次检测准确度,clearerr函数在VS2017中的函数接口声明如下:
_ACRTIMP void __cdecl clearerr(
_Inout_ FILE* _Stream //要重置的文件指针
);
最后是一个使用它们的栗子:
char String1[20]; //放读的字符串·
int flag; //存放错误标识
FILE *fp = fopen("test.dat", "rb"); //读方式打开文件
printf("\n开始读文件\n"); //开始读文件
fread(String1, sizeof(char), 20, fp); //读文件(这里应该成功)
flag = ferror(fp); //错误检测
clearerr(fp); //错误检测置0
printf("ferror返回值 = %d ", flag); //输出错误检测值
if (flag == 0) { //根据错误检测值判读操作是否失败
printf("读文件成功了!\n");
}
else {
printf("读文件失败了!\n");
}
printf("\n开始写文件\n"); //开始写文件
fwrite(String1, sizeof(char), 20, fp); //写文件(这里应该失败,因为是读方式打开的文件)
flag = ferror(fp); //错误检测
clearerr(fp); //错误检测置0
printf("ferror返回值 = %d ", flag); //输出错误检测值
if (flag == 0) { //根据错误检测值判写操作是否失败
printf("写文件成功了!\n");
}
else {
printf("写文件失败了!\n");
}
运行结果如下:
结尾
就这些了,花了两天零零碎碎总算写完了,好累,我去睡觉ZZZZ