文章目录
- 引入
- 1. 文件的打开与关闭:先取得通道,再释放资源
- 1.1 `fopen`:用路径和模式打开文件
- 1.2 `fclose`:关闭流并处理最后的写入
- 2. 顺序读写:位置指针自动向前移动
- 2.1 `fgetc`:从流中读取一个字符
- 2.2 `fputc`:向流中写入一个字符
- 2.3 `fgets`:读取一行(最多 `n - 1` 个字符)
- 2.4 `fputs`:写入字符串,不自动补换行
- 2.5 `fprintf`:按格式向文件写入文本
- 2.6 `fscanf`:按格式从文件读取文本
- 2.7 `fread`:按块读取二进制数据
- 2.8 `fwrite`:按块写入二进制数据
- 3. 随机读写:跳到指定位置再处理
- 3.1 `fseek`:相对某个基准移动位置
- 3.2 `ftell`:获取当前位置
- 3.3 `rewind`:回到文件开头
- 4. 文件读取结束的判断:`EOF`、`feof` 和 `ferror`
- 4.1 `feof`:查询 EOF 标志
- 4.2 `ferror` 与 `clearerr`:区分和清除错误状态
- 5. 缓冲区:为什么写入后文件大小可能暂时不变
- 5.1 `fflush`:刷新输出流
- 6. 文本还是二进制?先根据数据形态做选择
- 7. 小练习
- 7.1 统计文件中的行数
- 7.2 文本文件拷贝
- 总结
引入
内存中的int score = 95;只在程序运行时存在。想让下次启动程序还能得到 95,就需要把字节写入磁盘文件。C 标准库用一组stdio.h函数把磁盘细节封装起来:程序拿到一个FILE *,通过它读写文件。FILE是库内部维护的结构体,里面记录文件当前位置、读写模式、缓冲区和错误状态。我们只能通过指针使用它,不能直接访问其成员。stdin、stdout、stderr也是预先打开的FILE *,所以printf本质上是向标准输出流写入。
1. 文件的打开与关闭:先取得通道,再释放资源
1.1fopen:用路径和模式打开文件
函数原型:
FILE*fopen(constchar*filename,constchar*mode);成功时返回一个FILE *;失败时返回NULL。mode决定“是否允许读写”和“文件不存在时怎么办”。最常用的模式如下:
| 模式 | 含义 | 文件不存在 | 原有内容 |
|---|---|---|---|
"r" | 只读 | 失败 | 保留 |
"w" | 只写 | 创建 | 清空 |
"a" | 追加写 | 创建 | 保留,写入总在末尾 |
"r+" | 读写 | 失败 | 保留 |
"w+" | 读写 | 创建 | 清空 |
"a+" | 读写追加 | 创建 | 保留,写入在末尾 |
在这些模式后加"b"(如"rb"、"wb")表示二进制流。
代码示例:
#include<stdio.h>intmain(){FILE*fp=fopen("note.txt","w");if(fp==NULL){perror("fopen");// 输出系统给出的原因return1;}fputs("first line\n",fp);if(fclose(fp)!=0){perror("fclose");return1;}return0;}执行后,当前工作目录出现note.txt,内容为一行first line。fputs的细节稍后介绍。注意"w"会清空已有文件;想保留内容并在尾部增加,应改为"a"。
1.2fclose:关闭流并处理最后的写入
intfclose(FILE*stream);返回0表示成功,非0表示关闭时出错。关闭输出流不仅是“释放指针”,还会把缓冲区中尚未写到操作系统的数据刷新出去。因此即使前面可能出现的fprintf没报错,fclose仍然可能因为磁盘满而失败。
关闭后不能再使用fp:fgetc(fp)、fclose(fp)都属于未定义行为。若变量还要保留,可写fp = NULL,避免误用。
2. 顺序读写:位置指针自动向前移动
顺序读写适合从文件开头一路处理到结尾。每成功读写一个字符、一个文本行或一块数据,流的位置都会更新。
2.1fgetc:从流中读取一个字符
intfgetc(FILE*stream);返回值看起来是int而不是char,因为它既要容纳所有字符值,还要额外表示EOF。正确循环必须先接收返回值,再判断:
#include<stdio.h>intmain(){FILE*fp=fopen("note.txt","r");intch;if(fp==NULL)return1;while((ch=fgetc(fp))!=EOF){putchar(ch);}fclose(fp);return0;}如果note.txt是前面的first line,屏幕会输出first line。不要写成char ch; while ((ch = fgetc(fp)) != EOF):当真实字符值与EOF比较时,可能产生误判。
2.2fputc:向流中写入一个字符
intfputc(intc,FILE*stream);成功返回写入的字符(转换为unsigned char后再返回),失败返回EOF。
代码示例:
#include<stdio.h>intmain(){FILE*fp=fopen("letters.txt","w");if(fp==NULL)return1;for(intch='A';ch<='C';ch++){if(fputc(ch,fp)==EOF){perror("fputc");fclose(fp);return1;}}fclose(fp);return0;}letters.txt内容为ABC。参数类型是int,但写入的是一个字符;不要把它误解为一次写入四字节整数。
2.3fgets:读取一行(最多n - 1个字符)
char*fgets(char*str,intn,FILE*stream);它最多读取n - 1个字符,并自动在末尾补\0。如果在达到上限前遇到换行,换行也会被放入数组。成功返回str,遇到文件结束或错误返回NULL。
代码示例:
#include<stdio.h>intmain(){charline[32];FILE*fp=fopen("note.txt","r");if(fp==NULL)return1;while(fgets(line,sizeof(line),fp)!=NULL){printf("读到:%s",line);// line 可能已经含换行}fclose(fp);return0;}如果一行超过 31 个字符,fgets会分多次返回;这不是丢数据,而是需要你继续读取。
2.4fputs:写入字符串,不自动补换行
intfputs(constchar*str,FILE*stream);成功时返回非负值,失败返回EOF。它会写入字符串中的字符直到\0,但不会自动增加换行:
#include<stdio.h>intmain(){FILE*fp=fopen("message.txt","w");if(fp==NULL)return1;fputs("one line\n",fp);fputs("second line",fp);fclose(fp);return0;}文件有两行,因为第一段字符串自己包含\n。与puts相比,fputs允许你指定目标文件流。
2.5fprintf:按格式向文件写入文本
intfprintf(FILE*stream,constchar*format,...);用法与printf类似,只是第一个参数是目标流。返回成功写入的字符数,编码或 I/O 失败时返回负值。
代码示例:
#include<stdio.h>intmain(){FILE*fp=fopen("student.txt","w");if(fp==NULL)return1;intid=1001;doublescore=92.5;if(fprintf(fp,"id=%d score=%.1f\n",id,score)<0){perror("fprintf");fclose(fp);return1;}fclose(fp);return0;}
文件内容是id=1001 score=92.5。注意,和printf一样,格式说明符必须与参数类型匹配;例如float使用%f,整数不要用%s。
2.6fscanf:按格式从文件读取文本
intfscanf(FILE*stream,constchar*format,...);返回成功赋值的字段数;遇到文件结束返回EOF。因此循环应检查“转换数量”,而不是只检查feof:
#include<stdio.h>intmain(){FILE*fp=fopen("student.txt","r");intid;doublescore;if(fp==NULL)return1;if(fscanf(fp,"id=%d score=%lf",&id,&score)==2){printf("id=%d, score=%.1f\n",id,score);}else{puts("文件格式不符合预期");}fclose(fp);return0;}
这里&id、&score是写入目标地址;double必须使用%lf。格式中的空白可以匹配任意空白,普通字符(如id=)必须与文件一致。对于不可信文本,fgets加sscanf往往更容易控制每行的边界。
2.7fread:按块读取二进制数据
size_tfread(void*ptr,size_tsize,size_tcount,FILE*stream);其中ptr是 指向一块内存缓冲区的指针,该内存至少要有size × count字节大小 ;size是 要读取的单个元素的字节大小;count是要读取的元素个数;每个元素占size字节;返回的是“实际读取的元素个数”,不是字节数。下面一次读取 4 个int:
#include<stdio.h>intmain(){inta[4];FILE*fp=fopen("numbers.bin","rb");if(fp==NULL)return1;size_tgot=fread(a,sizeof(a[0]),4,fp);if(got<4&&ferror(fp)){perror("fread");}for(inti=0;i<got;++i){printf("%d ",a[i]);}printf("\n");fclose(fp);return0;}若文件只剩 2 个整数,got就是 2。fread(a, 1, sizeof(a), fp)则把“元素”定义为字节,返回值才是读取的字节数。直接把结构体写入文件时还会受到字节序、类型大小和内存对齐影响.
2.8fwrite:按块写入二进制数据
size_tfwrite(constvoid*ptr,size_tsize,size_tcount,FILE*stream);其中ptr是 指向一块内存缓冲区的指针,该内存至少要有size × count字节大小;size是 要读取的单个元素的字节大小;count是要读取的元素个数;每个元素占size字节;返回实际写入的元素个数。
代码示例:
//把数组原样写成二进制#include<stdio.h>intmain(){constinta[]={10,20,30,40};FILE*fp=fopen("numbers.bin","wb");if(fp==NULL)return1;size_twritten=fwrite(a,sizeof(a[0]),4,fp);if(written!=4){perror("fwrite");fclose(fp);return1;}fclose(fp);return0;}3. 随机读写:跳到指定位置再处理
顺序读取不适合“只改文件中间一条记录”。随机访问通过一个位置指针直接跳转。
3.1fseek:相对某个基准移动位置
intfseek(FILE*stream,longoffset,intorigin);origin可取SEEK_SET(文件开头)、SEEK_CUR(当前位置)、SEEK_END(文件末尾)。成功返回 0,失败返回非 0,并清除 EOF 标志。
#include<stdio.h>intmain(){FILE*fp=fopen("letters.txt","rb");//和上文同样内容 letter.txt: ABCintch;if(fp==NULL)return1;if(fseek(fp,1L,SEEK_SET)!=0){fclose(fp);return1;}ch=fgetc(fp);// 读取 'B'printf("字符:%c\n",ch);fclose(fp);return0;}对二进制流,偏移量通常就是字节数。文本流为了保持可移植性,除 0 和ftell返回的位置外,不要随意计算偏移。
3.2ftell:获取当前位置
longftell(FILE*stream);成功返回当前位置,失败返回-1L。它常用于记录“读到哪里”,之后配合fseek返回:
longpos=ftell(fp);if(pos==-1L){perror("ftell");}if(fseek(fp,pos,SEEK_SET)!=0){perror("fseek");}3.3rewind:回到文件开头
voidrewind(FILE*stream);它等价于把位置设为开头,并清除流的 EOF 和错误标志(不报告错误)。
代码示例:
FILE*fp=fopen("note.txt","r");intfirst,again;if(fp!=NULL){first=fgetc(fp);rewind(fp);again=fgetc(fp);printf("%c %c\n",first,again);// 同一个首字符fclose(fp);}4. 文件读取结束的判断:EOF、feof和ferror
4.1feof:查询 EOF 标志
intfeof(FILE*stream);返回非零表示 EOF 标志已设置。关键点是:EOF 是一次读操作的结果,不是文件中预先存放的字符。所以while (!feof(fp))会多执行一次循环。
4.2ferror与clearerr:区分和清除错误状态
intferror(FILE*stream);voidclearerr(FILE*stream);读失败后,feof和ferror可能需要区分:前者表示正常读到末尾,后者表示 I/O 错误。clearerr清除这两个标志,使流可以继续尝试后续操作。
FILE*fp=fopen("note.txt","r");if(fp!=NULL){intch;while((ch=fgetc(fp))!=EOF){// 处理字符}if(feof(fp))puts("正常到达文件末尾");if(ferror(fp))perror("读取失败");clearerr(fp);// 清除状态,谨慎使用fclose(fp);}clearerr不能修复磁盘或权限问题,只是重置状态;真正的错误原因仍需处理。
5. 缓冲区:为什么写入后文件大小可能暂时不变
标准 I/O 常先把数据放入用户态缓冲区,再批量交给操作系统。这样可以减少频繁系统调用,提高性能,但也意味着程序异常终止时,未刷新的内容可能丢失。
5.1fflush:刷新输出流
intfflush(FILE*stream);对输出流调用会把缓冲数据推送出去,成功返回 0,失败返回EOF。常见场景是日志程序希望立即看到最新内容:
FILE*fp=fopen("app.log","a");if(fp!=NULL){fprintf(fp,"程序仍在运行\n");if(fflush(fp)==EOF)perror("fflush");fclose(fp);}
fflush(stdin)在标准 C 中没有可移植的“清空输入缓存”含义;不要依赖它处理键盘残留字符。输入流应通过循环读取并丢弃,或重新设计输入流程。
6. 文本还是二进制?先根据数据形态做选择
文本文件(fprintf/fscanf、fgets/fputs)便于人眼查看和调试,适合配置、日志和简单表格;二进制文件(fread/fwrite)体积小、速度快,适合大量定长记录,但可移植性和版本兼容需要额外设计。
7. 小练习
7.1 统计文件中的行数
要求:使用fgets读取note.txt,统计包含内容的行数(每行的字符数小于255)。
思路:每次fgets成功就得到一行;循环结束后用ferror判断是否因错误中断。
参考答案:
intcount_lines(constchar*path){FILE*fp=fopen(path,"r");charbuf[256];intcount=0;if(fp==NULL)return-1;while(fgets(buf,sizeof(buf),fp)!=NULL){count++;}if(ferror(fp))count=-1;fclose(fp);returncount;}7.2 文本文件拷贝
要求:把numbers.txt文本文件复制到copy.txt,使用文本模式r/w,缓冲区方式读写。
intcopy_text(constchar*src,constchar*dst){FILE*in=fopen(src,"r");FILE*out=fopen(dst,"w");//检测路径是否有效与打开操作是否成功if(in==NULL||out==NULL){if(in)fclose(in);if(out)fclose(out);return-1;}intch;//使用int类型便于判断EOFwhile((ch=fgetc(in))!=EOF){if(fputc(ch,out)==EOF){fclose(in);fclose(out);return-1;}}intread_ok=!ferror(in);//判断读是否发生错误intin_ok=(fclose(in)==0);//判断是否成功关闭intout_ok=(fclose(out)==0);return(read_ok&&in_ok&&out_ok)?0:-1;}总结
可靠的文件代码通常遵循同一条线:fopen检查NULL→ 读写函数检查返回值 → 用feof/ferror解释失败原因 → 必要时fflush→ 用fclose检查最终落盘结果。先决定文本还是二进制,再选择顺序还是随机访问;当你能说清“当前位置在哪里、缓冲区里还有什么、失败后如何恢复”,文件操作就不再是只能背函数名的黑盒。