news 2026/9/24 5:01:45

C语言格式化输入输出与通讯录持久化

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C语言格式化输入输出与通讯录持久化

C 语言课堂练习:几组输入输出函数,以及通讯录怎么存档

这次课的作业,一部分是比较scanffscanfsscanf这些函数,另一部分是给之前写的动态通讯录加上文件保存。我刚看题时觉得它们的名字太像了,背函数名很容易背混。后来换了个办法:先看数据从哪儿读、最后写到哪儿,区别就清楚多了。

一、先把几组函数分清楚

操作标准输入或输出指定的文件流内存中的字符串
格式化读取scanf,从stdinfscanf,从传入的FILE *sscanf,从字符串读
格式化写入printf,写到stdoutfprintf,写到传入的FILE *sprintf,写到字符数组

我以前看到FILE *,第一反应就是磁盘文件。其实stdinstdout也是流,也能传给对应的函数。fscanf从流里读,fprintf往流里写;到了sscanfsprintf,处理对象就换成了内存里的字符串。C 标准草案的格式化输入输出章节和 Microsoft 的sscanf说明也把字符串与流分开说明。

Alice 20举个例子。读入整数时要传地址,读完还要检查返回值,不能只看变量里有没有数字。

intage;charname[20];if(scanf("%d",&age)==1)printf("age=%d\n",age);FILE*fp=fopen("people.txt","w");if(fp!=NULL){fprintf(fp,"%s %d\n","Alice",20);fclose(fp);}if(sscanf("Alice 20","%19s %d",name,&age)==2)printf("%s: %d\n",name,age);

这里写%19s是因为name只有 20 个位置,还得给末尾的\0留一个。如果要把格式化结果写进数组,我更倾向于用snprintf(buf, sizeof buf, ...)sprintf不接收数组容量,内容一长就可能写过界。Microsoft 的sprintf说明也建议考虑snprintf

题目 1572 我选D。前面三项分别说标准流、指定流和字符串读取;D 说sprintf把数据写到“输出流”,这里写错了,目标应该是字符数组。

二、feof不能拿来预测下一次读取

feof(fp)这个名字很容易让我理解成“文件现在是不是读完了”。准确地说,它查的是 EOF 标志。文件里的最后一个字节已经读走了,标志也可能还是 0;再读一次,确实读不到,标志才会被设置。Microsoft 的feof文档有一个读完 10 字节、再尝试读第 11 字节的例子。

这样再看while (!feof(fp)),问题就出来了:一次读取已经失败,循环体却可能还在用上一次的数据。我现在会让读取函数的返回值决定要不要继续处理:

intch;// fgetc 要用 int 接收,才能表示 EOF。while((ch=fgetc(fp))!=EOF){putchar(ch);}if(ferror(fp)){/* 这是读取错误,不是正常读到末尾。 */}/* 按行读取:while (fgets(line, sizeof line, fp) != NULL) { ... } *//* 按记录读取:while (fread(&person, sizeof person, 1, fp) == 1) { ... } */

还有一个容易漏掉的地方:fgetc返回EOFfgets返回NULL,或者fread没读够,都不一定是正常到文件末尾,也可能是读取出错。这时再用feofferror分原因。fgetc返回值、fgets返回值、fread返回值的官方说明都提到了这两种情况。

题目 1573 如果按课堂上强调的用法选单项,我会选A。A 说“用feof判断文件是否读取结束”,容易被理解成读之前先问它还有没有数据。但这题的措辞确实有点含糊:若把 A 理解成“读取失败以后检查 EOF 标志”,它又说得通。C、D 给出的返回值也只能说明这一次没有按预期读到数据,原因还得再查。我给自己记的版本是:先看读取函数的返回值,再用feofferror判断为什么停下。

三、给原来的通讯录加上保存和加载

原来的通讯录里,PeoInfo记录姓名、年龄、性别、电话和地址,Contact管理动态数组、当前人数与容量。代码在 GitHub 的 contact 目录。联系人如果只放在内存里,退出后一释放就留不住;这次要让下次启动还能看到他们。

程序启动时,InitContact初始化数组,接着由LoadContact"rb"模式打开contact.dat。第一次运行没有这个文件,就从空通讯录开始。读取时一次fread一个PeoInfo;只有返回 1,才扩容并放进数组,循环结束后还要看ferror

选退出时则反过来:SaveContact"wb"打开文件,fwrite写入当前sz条记录,比较实际写入的个数,再关闭文件。最后才由DestroyContact释放动态数组。

翻源码时我还发现一个没处理好的地方:SaveContact返回void,菜单里调用完就打印“通讯录已经保存”。要是打开文件或写入失败,这句话照样会出现。而且"wb"打开已有文件会截断旧内容,所以“打开成功”和“数据保存成功”不能画等号。后面如果继续改这个项目,我会让保存函数把成功与否返回给主程序。

四、我用来练习的单文件版本

原项目还有查找、修改、删除等功能。为了单独练习文件读写,我照着它的数据结构写了下面这个小版本,只留添加、显示、保存和加载。示例文件名是contact_demo.dat,不会覆盖原项目的contact.dat。输入时各字段用空格隔开,所以这里还不能录入带空格的姓名或地址。

#include<errno.h>#include<stdio.h>#include<stdlib.h>#defineDATA_FILE"contact_demo.dat"#defineINITIAL_CAPACITY3#defineGROW_STEP2typedefstructPeoInfo{charname[20];intage;charsex[5];chartele[12];charaddr[30];}PeoInfo;typedefstructContact{PeoInfo*data;size_tsize;size_tcapacity;}Contact;staticintEnsureCapacity(Contact*book){if(book->size<book->capacity)return1;size_tnext=book->capacity==0?INITIAL_CAPACITY:book->capacity+GROW_STEP;PeoInfo*p=(PeoInfo*)realloc(book->data,next*sizeof(PeoInfo));if(p==NULL){perror("realloc");return0;}book->data=p;book->capacity=next;return1;}staticintLoadContact(Contact*book){FILE*fp=fopen(DATA_FILE,"rb");if(fp==NULL){if(errno==ENOENT)// 首次运行,还没有数据文件。return1;perror("fopen for reading");return0;}PeoInfo person;intok=1;while(fread(&person,sizeofperson,1,fp)==1){if(!EnsureCapacity(book)){ok=0;break;}book->data[book->size++]=person;}if(ferror(fp)){perror("fread");ok=0;}if(fclose(fp)!=0){perror("fclose after reading");ok=0;}returnok;}staticintSaveContact(constContact*book){FILE*fp=fopen(DATA_FILE,"wb");if(fp==NULL){perror("fopen for writing");return0;}size_twritten=book->size==0?0:fwrite(book->data,sizeof(PeoInfo),book->size,fp);intok=written==book->size;if(!ok)fputs("fwrite: incomplete save\n",stderr);if(fclose(fp)!=0){perror("fclose after writing");ok=0;}returnok;}staticvoidShowContact(constContact*book){printf("Total: %zu\n",book->size);for(size_ti=0;i<book->size;++i){constPeoInfo*p=&book->data[i];printf("%s %d %s %s %s\n",p->name,p->age,p->sex,p->tele,p->addr);}}intmain(void){Contact book={0};charline[160];intchoice;if(!LoadContact(&book)){free(book.data);return1;}for(;;){fputs("1 add, 2 show, 0 exit: ",stdout);if(fgets(line,sizeofline,stdin)==NULL)break;if(sscanf(line,"%d",&choice)!=1)continue;if(choice==0)break;if(choice==2){ShowContact(&book);continue;}if(choice==1){PeoInfo person={0};fputs("name age sex phone address (no spaces in fields): ",stdout);if(fgets(line,sizeofline,stdin)==NULL)break;if(sscanf(line,"%19s %d %4s %11s %29s",person.name,&person.age,person.sex,person.tele,person.addr)!=5){fputs("Invalid record.\n",stderr);continue;}if(!EnsureCapacity(&book)){free(book.data);return1;}book.data[book.size++]=person;}}intsaved=SaveContact(&book);free(book.data);if(!saved)return1;puts("Saved.");return0;}

我实际试了两次运行。第一次输入Alice 20 F 12345678901 Nanjing后退出,第二次启动直接选“显示”,还能看到同一条记录:

Total: 1 Alice 20 F 12345678901 Nanjing Saved.

这份代码在本机用 MSBuild 编译通过,两次运行的退出码都是 0,测试目录里也生成了contact_demo.dat。我还试了空通讯录直接退出,再启动后显示为 0 条。测试只说明同一台机器、同一个程序版本能把这份文件写回、读出;换编译器或改结构体之后,不能直接拿这个二进制文件通用。

五、写完后给自己记的几句话

这几组函数,我现在按“数据的位置”来记:标准流用scanf/printf,传入的流用fscanf/fprintf,字符串用sscanf/sprintf。读文件时先检查这一次读到了什么;feof是读完后判断原因用的,不能放在前面猜。

通讯录这部分,fread/fwrite的返回值是实际读写的元素个数。读少了要分清是 EOF 还是出错,写少了就不能说保存成功。直接把结构体写进二进制文件,做这次本机练习很方便;如果以后想长期保存、换机器使用,还得另想稳定的文件格式,处理文件损坏和版本变化。

参考资料:WG14 C 标准草案、Microsoft C 运行库feof、Microsoft C 运行库fread

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/24 4:59:42

Tinkercad Circuits零基础电路仿真实战:从LED点亮到Arduino控制

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/24 4:45:47

Jetson Orin NX USB3.0接口配置实战:从硬件映射到设备树

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/24 4:38:44

STM32F407ZGT6硬核解析:Cortex-M4+FPU+外设矩阵实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/24 4:31:27

工控现货实战:货源、定价、库存与风险控制全解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/24 4:29:53

2.4GHz Wi-Fi LNA设计:从ADS仿真到实板落地的工程闭环

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华