ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

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

C语言格式化输入输出与通讯录持久化 C 语言课堂练习几组输入输出函数以及通讯录怎么存档这次课的作业一部分是比较scanf、fscanf、sscanf这些函数另一部分是给之前写的动态通讯录加上文件保存。我刚看题时觉得它们的名字太像了背函数名很容易背混。后来换了个办法先看数据从哪儿读、最后写到哪儿区别就清楚多了。一、先把几组函数分清楚操作标准输入或输出指定的文件流内存中的字符串格式化读取scanf从stdin读fscanf从传入的FILE *读sscanf从字符串读格式化写入printf写到stdoutfprintf写到传入的FILE *sprintf写到字符数组我以前看到FILE *第一反应就是磁盘文件。其实stdin、stdout也是流也能传给对应的函数。fscanf从流里读fprintf往流里写到了sscanf和sprintf处理对象就换成了内存里的字符串。C 标准草案的格式化输入输出章节和 Microsoft 的sscanf说明也把字符串与流分开说明。拿Alice 20举个例子。读入整数时要传地址读完还要检查返回值不能只看变量里有没有数字。intage;charname[20];if(scanf(%d,age)1)printf(age%d\n,age);FILE*fpfopen(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((chfgetc(fp))!EOF){putchar(ch);}if(ferror(fp)){/* 这是读取错误不是正常读到末尾。 */}/* 按行读取while (fgets(line, sizeof line, fp) ! NULL) { ... } *//* 按记录读取while (fread(person, sizeof person, 1, fp) 1) { ... } */还有一个容易漏掉的地方fgetc返回EOF、fgets返回NULL或者fread没读够都不一定是正常到文件末尾也可能是读取出错。这时再用feof和ferror分原因。fgetc返回值、fgets返回值、fread返回值的官方说明都提到了这两种情况。题目 1573 如果按课堂上强调的用法选单项我会选A。A 说“用feof判断文件是否读取结束”容易被理解成读之前先问它还有没有数据。但这题的措辞确实有点含糊若把 A 理解成“读取失败以后检查 EOF 标志”它又说得通。C、D 给出的返回值也只能说明这一次没有按预期读到数据原因还得再查。我给自己记的版本是先看读取函数的返回值再用feof、ferror判断为什么停下。三、给原来的通讯录加上保存和加载原来的通讯录里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。输入时各字段用空格隔开所以这里还不能录入带空格的姓名或地址。#includeerrno.h#includestdio.h#includestdlib.h#defineDATA_FILEcontact_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-sizebook-capacity)return1;size_tnextbook-capacity0?INITIAL_CAPACITY:book-capacityGROW_STEP;PeoInfo*p(PeoInfo*)realloc(book-data,next*sizeof(PeoInfo));if(pNULL){perror(realloc);return0;}book-datap;book-capacitynext;return1;}staticintLoadContact(Contact*book){FILE*fpfopen(DATA_FILE,rb);if(fpNULL){if(errnoENOENT)// 首次运行还没有数据文件。return1;perror(fopen for reading);return0;}PeoInfo person;intok1;while(fread(person,sizeofperson,1,fp)1){if(!EnsureCapacity(book)){ok0;break;}book-data[book-size]person;}if(ferror(fp)){perror(fread);ok0;}if(fclose(fp)!0){perror(fclose after reading);ok0;}returnok;}staticintSaveContact(constContact*book){FILE*fpfopen(DATA_FILE,wb);if(fpNULL){perror(fopen for writing);return0;}size_twrittenbook-size0?0:fwrite(book-data,sizeof(PeoInfo),book-size,fp);intokwrittenbook-size;if(!ok)fputs(fwrite: incomplete save\n,stderr);if(fclose(fp)!0){perror(fclose after writing);ok0;}returnok;}staticvoidShowContact(constContact*book){printf(Total: %zu\n,book-size);for(size_ti0;ibook-size;i){constPeoInfo*pbook-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(choice0)break;if(choice2){ShowContact(book);continue;}if(choice1){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;}}intsavedSaveContact(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。
返回列表