C语言之文件操作(详解版)
C语言之文件操作(详解版)
不知不觉我们已经学到C语言的文件操作部分了,这部分内容其实很有意思,因为它可以直接把我们代码中的数据写入硬盘,而不是我们关掉这个程序,代码就没有了,让我们开始学习吧!
目录
1.为什么使用文件
1.使用文件我们可以将数据直接存放在电脑的硬盘上,做到数据的持久化
数据存放在内存中:
前面实现的通讯录,当程序运行起来的时候,可以给通讯录增加,删除数据,但是当程序退出的时候,通讯录中的数据自然就不存在了,当下次运行程序的时候,数据又得重新录入,这样的通讯录就很局限
数据存放在硬盘中:
可以把数据记录下来,只有我们自己选择删除数据的时候,数据才会被销毁,否则,我们下次打开这个程序的时候,上一次存入的数据都存在,不会消失。(下篇博客为大家实现)
保持存入通讯录的数据一直存在,这就涉及到我们数据持久化问题
2.我们一般把数据持久化的方法有:
把数据存放在磁盘文件
把数据存放到数据库等方式
3.我们要学的是把数据放到磁盘文件中
2.什么是文件
简单来说,磁盘上的文件就是文件,就比如我们计算机上C盘中的文件夹下的文件
以上都是存放在硬盘中的文件
在程序设计中,我们一般把文件分为两类(从文件功能的角度分):
程序文件
数据文件
我们主要学习数据文件
2.1程序文件
源程序文件(后缀为.c)
目标文件(windows环境后缀为.obj)
可执行程序(Windows环境下后缀为.exe)
2.2数据文件
文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件或者输出内容的文件
数据在显示器上:
处理数据的输入输出都是以终端为对象的,就是从终端的键盘上输入数据,运行结果显示到显示器上
数据在磁盘文件中:
我们可以把信息输出到磁盘上,当需要的时候再从磁盘上把数据读取到内存中使用
2.3文件名
我们都一个文件要有唯一的文件标识,以方便用户的识别和引用
文件名包含三个部分:文件路径+文件名主干+文件后缀
为了方便起见,文件标识就被认为是文件名
3.文件的打开和关闭
3.1文件指针
1.什么是文件指针:
在缓冲文件系统中,关键的概念是“文件类型指针”,简称“文件指针”,每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(例如:文件名,文件状态,文件当前位置),这些信息是保存在一个结构体变量中的。该结构体类型是由系统声明的,取名FILE。
2.每当我们打开一个文件的时候,系统会根据文件的情况自动创建一个FILE结构的变量,并填充其中的信息,使用者不必关心其中的细节,一般都是通过FILE指针来维护这个FILE结构的变量,这样使用起来更加方便
3.我们创建一个FTLE*的指针变量,例如FILE* pf,它就是一个文件指针变量
相信很多伙伴们又要问为什么要创建这个指针变量:
指针pf是一个指向FILE类型数据的指针变量,它就可以使pf指向某个文件的文件信息区,通过该文件信息区中的信息就能够访问该文件,也就是说通过文件指针变量,能够找到与它相关联的文件
3.2文件的打开和关闭
我们都知道,文件在读写之前,应该先打开文件,在使用结束之后关闭文件
在编写程序时,再打开文件的同时,都会返回一个FILE* 的指针变量来指向该文件,这就相当于建立了指针和文件的关系
接下来让我们学习一下,文件的打开(fopen)和关闭(fclose)这两个函数
3.2.1fopen:文件打开函数
1.函数功能:打开一个文件
2.头文件:#include<stdio.h>
3.函数格式:FILE *fopen( const char *filename, const char *mode );
filename:文件名
mode:文件打开方式(下面会为大家总结一个表格)
4.函数返回值:
打开文件成功:返回一个指向打开文件的指针
打开文件失败:返回一个空指针
所以我们每次在打开文件的代码下,要加一段代码来检测文件是否打开成功
3.2.2fclose:关闭文件函数
1.函数功能:关闭一个文件
2.头文件:#include<stdio.h>
3.函数格式:int fclose( FILE *stream );
stream:流,就是文件流
这里要为大家拓展关于“流”的知识
在我们C程序运行的时候,默认打开三个流:
stdin:标准输入流(就是我们在键盘上可以输入的数据)
stdout:标准输出流(我们在屏幕上可以看到的数据)
stderr:标准错误流(我们在屏幕上可以看到的数据)
它们的类型都是FILE*
所以我们在写C程序的时候才不需要在打开流,因为它是默认打开这三个流的
在进行文件操作的时候,我们是没有默认打开流的,这时需要我们手动给它添加一个流
然后进行读取写入等操作
我们这里的 FILE *stream 就是一个文件流
例如:FILE* pf=(“test.txt”,"w");这个语句里,pf这个文件指针就是一个文件流
那么我们为什么要有流呢?我们的数据是不能直接传输到输出设备上的,它是通过流进行传输的,目前我们知道这些,进行文件操作的时候就不会觉得难以理解了。
4.函数返回值:
文件关闭失败:函数返回值是0,同时需要我们把文件指针置为NULL
文件关闭失败:函数返回EOF来指示错误
3.2.3代码示例
现在介绍完fopen和fclose函数了,让我们看个代码来进一步理解这两个函数的使用
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> #include<stdlib.h> #include<errno.h> int main() { //打开一个文件,以只读的模式打开 FILE* pf = fopen("test.txt", "r");//这里的文件名,如果我们想要使用 //已经存在,也就是创建好的文件,我们需要把文件路径给完整的加上 //检测文件是否打开成功,成功则进入文件,失败返回空指针 if (pf == NULL) { perror("fopen"); return 1; } //进入文件,进行读文件 //... //关闭文件 fclose(pf); pf = NULL; return 0; }
我们运行代码,发现显示了错误信息
提醒我们没有这个文件,这就引出了我们下一个知识点,文件打开模式有哪些,以只读模式打开文件为什么会打开失败。
3.2.4文件打开(fopen)模式
这里给大家总结了一个表格,我们可以对照表格,选择我们需要的文件打开模式
这里我们要注意,以写的形式打开一个文件时,上次书写在文件中的数据将会被销毁,这次打开文件输入的内容将覆盖上一次的内容
4.文件读写顺序
文件读写分为顺序读写和随机读写两类
我们在学习这部分内容之前,还需要明确一个知识点
在我们平时写C语言时,是面向程序的:数据显示到屏幕上
今天学习的面向文件操作:数据显示到文件中
知道输入和输出的面向关系,我们才能更好的学习这部分知识
4.1文件的顺序读写
文件的顺序读写就是,按顺序读写,一个诶一个
这里我们学习几个函数,来帮助我们更好的认识文件顺序读写
字符输入输出函数:fgetc/fputc
文本行输入输出函数:fgets/fputs
格式化输入输出函数:fscanf/fprintf
二进制输入输出函数:fread/fwrite
前三对函数适用于所有流(既适用于标准输出流,也适用于文件流)
最后一对仅适用于文件流
我们一对一对来介绍
4.1.1fputc函数
1.函数功能:将字符写入文件流或者标准输出流
2.头文件:#include<stdio.h>
3.函数格式:int fputc( int c, FILE *stream );
这里的c:要输出(写入)的字符
代码示例我们与fgetc函数放在一起实现
4.1.2fgetc函数
1.函数功能:从文件流或者标准输入流中读取字符
2.头文件:#include<stdio.h>
3.函数格式:int fgetc( FILE *stream );
4.函数返回值:
读取成功:返回作为int读取的字符
读取失败:返回EOF以指示错误或文件结束
下面我们来看个代码,理解这两个函数
代码示例
int main() { //打开一个文件 FILE* pf = fopen("test1.txt", "w");//以写的形式打开 //判断文件是否打开成功 if (pf == NULL) { perror("fopen"); return 1; } //文件打开成功,进行写文件fputc char ch = 0; for (ch = 'a'; ch <= 'z'; ch++) { fputc(ch, pf); } //书写完毕,关闭文件 fclose(pf); pf = NULL; //打开这个文件,我们进行读取操作 FILE* ptf = fopen("test1.txt", "r");//以读的形式打开 if (ptf == NULL) { perror("fopen"); return 1; } //开始读取,我们读取四次 int tmp = fgetc(ptf); printf("%c\n", tmp); tmp = fgetc(ptf); printf("%c\n", tmp); tmp = fgetc(ptf); printf("%c\n", tmp); tmp = fgetc(ptf); printf("%c\n", tmp); //读取完毕,关闭文件 fclose(ptf); ptf = NULL; return 0; }
打开文件所在文件夹,我们可以看到文件中已经存入数据,这是fputc函数的功能,在文件中写入了数据
运行代码,我们可以看到屏幕上的结果,读取到了a b c d ,这是fgetc函数的功能
4.1.3fputs函数
1.函数功能:将字符串写入文件流或者标准输出流
2.头文件:#include<stdio.h>
3.函数格式:int fputs( const char *string, FILE *stream );
string:我们需要写入的字符串
4.函数一次性只能写入一条字符串
代码示例我们与fgets函数放在一起实现
4.1.4fgets函数
1.函数功能:从文件流或者标准输入流中读取字符串
2.头文件:#include<stdio.h>
3.函数格式:char *fgets( char *string, int n, FILE *stream );
string:数据存储的位置,通俗来说,就是从stream中要读的字符串放到string中
n:要读取的最大字符数
4.函数返回值:
读取成功:返回该条字符串
读取失败:返回一个空指针NULL
5.函数一次性只能读取一条字符串
我们来看段代码,加深理解
代码示例
int main() { //打开一个文件 FILE* pf = fopen("test1.txt", "w");//以写的形式打开 //判断文件是否打开成功 if (pf == NULL) { perror("fopen"); return 1; } //文件打开成功,进行写文件fputc fputs("hello world!\n", pf); fputs("亚里士多德!\n", pf); fputs("hehe!\n", pf); //书写完毕,关闭文件 fclose(pf); pf = NULL; //打开这个文件,我们进行读取操作 FILE* ptf = fopen("test1.txt", "r");//以读的形式打开 if (ptf == NULL) { perror("fopen"); return 1; } //开始读取 char buf[30] = { 0 }; fgets(buf, 5, ptf); //读取完毕,关闭文件 fclose(ptf); ptf = NULL; return 0; }
我们打开文件可以看到,文件中已经写入(fputs)三行数据
我们运行代码读取(fgets)第一行的数据
我们预想中的hello并没有出现,而是hell,这是因为要留一个字符位置来存放‘\0’,如果想要打印hello,要让n=6
4.1.5fprintf函数
1.函数功能:格式化把数据写入文件流或者标准输出流
2.头文件:#include<stdio.h>
3.函数格式:int fprintf( FILE *stream, const char *format [, argument ]...);
char *format:格式化控制字符串,就是说它以什么形式输出,例如%d %s %f
[argument ]...:可选参数,就是对应形式的打印参数是什么
4.函数返回值:
写入成功:返回写入的字节数
写入失败:函数中的每一个都会返回一个负值
代码示例我们与fscanf函数放在一起实现
4.1.6fscanf函数
1.函数功能:从文件流或者标准输入流中读取格式化数据
2.头文件:#include<stdio.h>
3.函数格式:int fscanf( FILE *stream, const char *format [, argument ]... );
char *format: 格式化控制字符串,就是说它以什么形式输入,例如%d %s %f
[argument ]...:可选参数,就是对应形式的打印参数是什么
4.函数返回值:
读取成功:返回成功转换和分配的字段数
读取失败:发生错误,或者在第一次转换之前到达文件流的末尾的情况下,则返回值为fscanf的EOF
返回值0表示未分配任何字段
我们来看段代码,进一步了解
代码示例
struct S { char name[12]; int age; float score; }; int main() { //打开一个文件 struct S s1 = { "张翰",34,94.3 }; FILE* pf = fopen("test1.txt", "w");//以写的形式打开 //判断文件是否打开成功 if (pf == NULL) { perror("fopen"); return 1; } //文件打开成功,进行写文件fprintf fprintf(pf, "%s %d %f", s1.name, s1.age, s1.score); //书写完毕,关闭文件 fclose(pf); pf = NULL; //打开这个文件,我们进行读取操作 FILE* ptf = fopen("test1.txt", "r");//以读的形式打开 if (ptf == NULL) { perror("fopen"); return 1; } //开始读取 struct S s2 = { 0 }; fscanf(ptf, "%s %d %f", s2.name,& (s2.age), &(s2.score)); printf("%s %d %f\n", s2.name, s2.age, s2.score); //读取完毕,关闭文件 fclose(ptf); pf = NULL; return 0; }
我们这里的结构体中存放的就是格式化的数据
打开文件,发现文件中已经写入(fprintf)数据
运行代码读取(fscanf),屏幕上显示文件中的数据
4.1.7fwrite函数
1.函数功能:以二进制输出数据,将数据写入文件流
2.头文件:#include<stdio.h>
3.函数格式:size_t fwrite( const void *buffer, size_t size, size_t count, FILE *stream );
void *buffer:指向要写入的数据的指针
size:要写的元素的大小,即每个元素所占字节的大小
count:最多写入几个元素
FILE *stream:文件流
4.函数返回值:
写入成功:写入的完整项目数,即count
写入失败:该数字可能小于count,此外,如果发生错误,则无法确定文件位置指示器
代码示例我们与fread函数放在一起实现
4.1.8fread函数
1.函数功能:以二进制输入数据,从文件中读取数据
2.头文件:#include<stdio.h>
3.函数格式:size_t fread( void *buffer, size_t size, size_t count, FILE *stream );
buffer:数据存储位置
size:要读的元素的大小,即每个元素所占字节的大小
count:最多读取几个元素
4.函数返回值:
读取成功:返回实际读取的完整项目数,即count,如果发生错误或在达到count之前遇到文件末尾,则该数字可能小于count
读取失败:使用feof或ferror函数来区分读取错误和文件结束条件。如果大小或计数为0,则fread返回0,并且缓冲区内容不变。
接下来我们看段代码来加深了解
代码示例
//fwrite fread struct S { char name[12]; int age; float score; }; int main() { //打开一个文件 struct S s1 = { "张翰",34,94.3 }; struct S s3 = { "黄晓明",55,88.9 }; FILE* pf = fopen("test1.txt", "wb");//以二进制写的形式打开 //判断文件是否打开成功 if (pf == NULL) { perror("fopen"); return 1; } //文件打开成功,进行写文件 fwrite(&s1, sizeof(s1), 1, pf); fwrite(&s3, sizeof(s3), 1, pf); //书写完毕,关闭文件 fclose(pf); pf = NULL; //打开这个文件,我们进行读取操作 FILE* ptf = fopen("test1.txt", "rb");//以二进制形式打开要读的文件 if (ptf == NULL) { perror("fopen"); return 1; } //开始读取 struct S s2 = { 0 }; fread(&s2, sizeof(s2),1,ptf); printf("%s %d %f\n", s2.name, s2.age, s2.score); fread(&s2, sizeof(s2), 1, ptf); printf("%s %d %f\n", s2.name, s2.age, s2.score); //读取完毕,关闭文件 fclose(ptf); pf = NULL; return 0; }
打开文件,我们发现文件中已经被写入(fwrite)二进制数据,我们是看不懂的
我们发现只有汉字我们可以看懂,这是因为汉字的二进制形式与汉字本身形式相同
运行代码,我们读取(fread)数据到屏幕上,屏幕上显示数据,读取成功
4.2文件的随机读写
文件顺序读写有对应的函数,同样,文件的随机读写也有对应的函数,我们在这里介绍3个对应函数,fseek ftell rewind
我们继续往下了解
4.2.1fseek函数
1.函数功能:将文件指针移动到指定位置
2.头文件:#include<stdio.h>
3.函数格式:int fseek( FILE *stream, long offset, int origin );
offect:偏移量,也就是当前指针位置到目标位置的字节数
指针从左—>右,偏移量为正数
指针从右—>左,偏移量为负数
origin:起始位置,它还被定义了三个量
SEEK_SET:文件其实位置
SEEK_CUR:文件当前指针位置
SEEK_END:文件末尾
4.函数返回值:
读取成功:fseek将返回0
读取失败:它将返回一个非零值,在无法查找的设备上,返回值未定义
我们将代码例子与ftell函数和rewind函数放在一起
4.2.2ftell函数
1.函数功能:获取文件指针的当前位置,通俗来说,这个函数就是让我们来求偏移量的,也就是文件指针相较于起始位置的偏移量
2.头文件:#include<stdio.h>
3.函数格式:long int ftell( FILE *stream );
4.函数返回值:
读取成功:返回文件指针相较于起始位置的偏移量
读取失败:ftell返回–1L(偏移量),errno设置为errno.H中定义的两个常量之一
我们和rewind函数一起来 看代码例子
4.2.3rewind函数
1.函数功能:将文件指针重新定位到文件起始位置
2.头文件:#include<stdio.h>
3.函数格式:void rewind( FILE *stream );
我们来看个代码进一步加深理解这两个函数
代码示例
int main() { FILE* pf = fopen("test2.txt", "w"); if (pf == NULL) { perror("fopen"); return 1; } //在文件中写入abcdefg char ch = 0; for (ch = 'a'; ch <= 'z'; ch++) { fputc(ch, pf); } //写入完成,关闭文件 fclose(pf); pf = NULL; //再次打开文件,进行随机读取文件 FILE* ptf = fopen("test2.txt", "r"); if (ptf == NULL) { perror("fopen"); return 1; } //先按顺序往下读3个字符 char tmp = fgetc(ptf);//a printf("%c\n", tmp); tmp = fgetc(ptf);//b printf("%c\n", tmp); tmp = fgetc(ptf);//c printf("%c\n", tmp); //读取完三次后,我们想再次读取b,而不是按顺序读取到d,此时 //我们需要使用fseek函数 fseek(ptf, -2, SEEK_CUR);//改变指针位置 tmp = fgetc(ptf); printf("%c\n", tmp);//此时读取到b //假设在此时,我们不知道文件指针的当前位置,这时,我们就需要用到 // ftell函数来求一下当前文件指针的偏移量 printf("文件指针相较于起始位置的偏移量是%d\n", ftell(ptf)); //得到这个文件指针偏移量后,我们想把文件指针归到文件起始位置 // 这时需要用到我们的rewind函数 rewind(ptf); //我们打印一个字符验证一下,查看是否文件指针回到了文件起始位置,如果 // 回到了,则打印出来的是a //进行读取字符 tmp = fgetc(ptf); printf("是起始字符;>%c \n", tmp); //读取完毕,关闭文件 fclose(ptf); ptf = NULL; return 0; }int main() { FILE* pf = fopen("test2.txt", "w"); if (pf == NULL) { perror("fopen"); return 1; } //在文件中写入abcdefg char ch = 0; for (ch = 'a'; ch <= 'z'; ch++) { fputc(ch, pf); } //写入完成,关闭文件 fclose(pf); pf = NULL; //再次打开文件,进行随机读取文件 FILE* ptf = fopen("test2.txt", "r"); if (ptf == NULL) { perror("fopen"); return 1; } //先按顺序往下读3个字符 char tmp = fgetc(ptf);//a printf("%c\n", tmp); tmp = fgetc(ptf);//b printf("%c\n", tmp); tmp = fgetc(ptf);//c printf("%c\n", tmp); //读取完三次后,我们想再次读取b,而不是按顺序读取到d,此时 //我们需要使用fseek函数 fseek(ptf, -2, SEEK_CUR);//改变指针位置 tmp = fgetc(ptf); printf("%c\n", tmp);//此时读取到b //假设在此时,我们不知道文件指针的当前位置,这时,我们就需要用到 // ftell函数来求一下当前文件指针的偏移量 printf("文件指针相较于起始位置的偏移量是%d\n", ftell(ptf)); //得到这个文件指针偏移量后,我们想把文件指针归到文件起始位置 // 这时需要用到我们的rewind函数 rewind(ptf); //我们打印一个字符验证一下,查看是否文件指针回到了文件起始位置,如果 // 回到了,则打印出来的是a //进行读取字符 tmp = fgetc(ptf); printf("是起始字符;>%c \n", tmp); //读取完毕,关闭文件 fclose(ptf); ptf = NULL; return 0; }
运行结果
在这里,我们要特别说明一下那个文件光标位置
打开我们写入的文件,我们发现光标在文件起始位置,这是因为文件指针默认指向第一个字符,当我们一次往后读取(fgetc)字符时,文件指针会自增1,会直接往下走,而不是一直打印那一个字符,这个不需要我们程序员来操作。
5.文本文件和二进制文件
什么是文本文件,什么又是二进制文件?
1.根据数据的组织形式,数据文件被称为文本文件或者二进制文件
2.数据在内存中以二进制的形式存储,如果不加转换的输出到外存,就是二进制文件
(ps:这种二进制文件,我们一般是看不懂的,看不懂思密达!!!)
3.如果要求在外存上以ASCII码的形式存储,则需要在存储前转换,以ASCII码字符的形式存储的文件就是文本文件
(ps:看得懂思密达!!!)
我们举个简单的例子来看什么是二进制存储,什么是ASCII存储
例如我们要把整数10000存入内存中
它在内存中原来就是以二进制的补码形式存储的,也就是
00000000 00000000 00100111 00010000
(不清楚源码反码补码关系的伙伴可以去我这篇博客中了解源码反码补码的关系)
以文本文件(ASCII形式)存储:
这是整数10000,它的五个位数一次转换为ASCII对应值,也就是
以二进制的形式存储:
就是不加转换的二进制码:00000000 00000000 00100111 00010000
它在文件中显示出来的数据我们是看不懂的
可以举个代码例子
int main() { //以二进制的形式进行写入 FILE* pf = fopen("test3.txt", "wb"); if (pf == NULL) { perror("fopen"); return 1; } int a = 10000; fwrite(&a, 4, 1, pf); fclose(pf); pf = NULL; //以二进制的形式进行读取 FILE* ptf = fopen("test3.txt", "rb"); if (ptf == NULL) { perror("ptf::fopen"); return 1; } int b = 0; fread(&b, 4, 1, ptf); printf("%d \n", b); fclose(ptf); ptf = NULL; return 0; }
我们打开对应的文件,可以看到里面存储的数据
由于这篇博客字数已经到极限了,我们还剩下一点内容,文件读取结束的判定和文件缓冲区,我们放到下一篇博客,好了,大家下期再见!!!
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)