练习24:输入输出和文件
原文:
译者:
你已经学会了使用printf
来打印变量,这非常不错,但是还需要学习更多。这个练习中你会用到fscanf
和fgets
在结构体重构建关于一个人的信息。在这个关于读取输入的简介之后,你会得到C语言IO函数的完整列表。其中一些你已经见过并且使用过了,所以这个练习也是一个记忆练习。
#include#include "dbg.h"#define MAX_DATA 100typedef enum EyeColor { BLUE_EYES, GREEN_EYES, BROWN_EYES, BLACK_EYES, OTHER_EYES} EyeColor;const char *EYE_COLOR_NAMES[] = { "Blue", "Green", "Brown", "Black", "Other"};typedef struct Person { int age; char first_name[MAX_DATA]; char last_name[MAX_DATA]; EyeColor eyes; float income;} Person;int main(int argc, char *argv[]){ Person you = {.age = 0}; int i = 0; char *in = NULL; printf("What's your First Name? "); in = fgets(you.first_name, MAX_DATA-1, stdin); check(in != NULL, "Failed to read first name."); printf("What's your Last Name? "); in = fgets(you.last_name, MAX_DATA-1, stdin); check(in != NULL, "Failed to read last name."); printf("How old are you? "); int rc = fscanf(stdin, "%d", &you.age); check(rc > 0, "You have to enter a number."); printf("What color are your eyes:\n"); for(i = 0; i <= OTHER_EYES; i++) { printf("%d) %s\n", i+1, EYE_COLOR_NAMES[i]); } printf("> "); int eyes = -1; rc = fscanf(stdin, "%d", &eyes); check(rc > 0, "You have to enter a number."); you.eyes = eyes - 1; check(you.eyes <= OTHER_EYES && you.eyes >= 0, "Do it right, that's not an option."); printf("How much do you make an hour? "); rc = fscanf(stdin, "%f", &you.income); check(rc > 0, "Enter a floating point number."); printf("----- RESULTS -----\n"); printf("First Name: %s", you.first_name); printf("Last Name: %s", you.last_name); printf("Age: %d\n", you.age); printf("Eyes: %s\n", EYE_COLOR_NAMES[you.eyes]); printf("Income: %f\n", you.income); return 0;error: return -1;}
这个程序非常简单,并且引入了叫做fscanf
的函数,意思是“文件的格式化输入”。scanf
家族的函数是printf
的反转版本。printf
用于以某种格式打印数据,然而scanf
以某种格式读取(或者扫描)输入。
文件开头没有什么新的东西,所以下面只列出main
所做的事情:
ex24.c:24-28
创建所需的变量。
ex24.c:30-32
使用fgets
函数获取名字,它从输入读取字符串(这个例子中是stdin
),但是确保它不会造成缓冲区溢出。
ex24.c:34-36
对 you.last_name
执行相同操作,同样使用了fgets
。
ex24.c:38-39
使用fscanf
来从stdin
读取整数,并且将其放到you.age
中。你可以看到,其中使用了和printf
相同格式的格式化字符串。你也应该看到传入了you.age
的地址,便于fsnaf
获得它的指针来修改它。这是一个很好的例子,解释了使用指向数据的指针作为“输出参数”。
ex24.c:41-45
打印出用于眼睛颜色的所有可选项,并且带有EyeColor
枚举所匹配的数值。
ex24.c:47-50
再次使用了fscanf
,从you.eyes
中获取数值,但是保证了输入是有效的。这非常重要,因为用户可以输入一个超出EYE_COLOR_NAMES
数组范围的值,并且会导致段错误。
ex24.c:52-53
获取you.income
的值。
ex24.c:55-61
将所有数据打印出来,便于你看到它们是否正确。要注意EYE_COLOR_NAMES
用于打印EyeColor
枚举值实际上的名字。
你会看到什么
当你运行这个程序时,你应该看到你的输入被适当地转换。你应该尝试给它非预期的输入,看看程序是怎么预防它的。
$ make ex24cc -Wall -g -DNDEBUG ex24.c -o ex24$ ./ex24What's your First Name? ZedWhat's your Last Name? ShawHow old are you? 37What color are your eyes:1) Blue2) Green3) Brown4) Black5) Other> 1How much do you make an hour? 1.2345----- RESULTS -----First Name: ZedLast Name: ShawAge: 37Eyes: BlueIncome: 1.234500
如何使它崩溃
这个程序非常不错,但是这个练习中真正重要的部分是,scanf
如何发生错误。对于简单的数值转换没有问题,但是对于字符串会出现问题,因为scanf
在你读取之前并不知道缓冲区有多大。类似于gets
的函数(并不是fgets
,不带f
的版本)也有一个我们已经避免的问题。它并不是道输入缓冲区有多大,并且可能会使你的程序崩溃。
要演示fscanf
和字符串的这一问题,需要修改使用fgets
的那一行,使它变成fscanf(stdin, "%50s", you.first_name)
,并且城市再次运行。你会注意到,它读取了过多的内容,并且吃掉了你的回车键。这并不是你期望它所做的,你应该使用fgets
而不是去解决古怪的scanf
问题。
接下来,将fgets
改为gets
,接着使用valgrind
来执行valgrind ./ex24 < /dev/urandom
,往你的程序中输入一些垃圾字符串。这叫做对你的程序进行“模糊测试”,它是一种不错的方法来发现输入错误。这个例子中,你需要从/dev/urandom
文件来输入一些垃圾,并且观察它如何崩溃。在一些平台上你需要执行数次,或者修改MAX_DATA
来使其变小。
gets
函数非常糟糕,以至于一些平台在程序运行时会警告你使用了gets
。你应该永远避免使用这个函数。
最后,找到you.eyes
输入的地方,并移除对其是否在正确范围内的检查。然后,为它输入一个错误的数值,比如-1或者1000。在Valgrind
执行这些操作,来观察会发生什么。
译者注:根据最新的C11标准,对于输入函数,你应该总是使用
_s
后缀的安全版本。对于向字符串的输出函数,应该总是使用C99中新增的带n
的版本,例如snprintf
。如果你的编译器支持新版本,就不应该使用旧版本的不安全函数。
IO函数
这是一个各种IO函数的简单列表。你应该查询每个函数并为其创建速记卡,包含函数名称,功能和它的任何变体。
fscanf
fgets
fopen
freopen
fdopen
fclose
fcloseall
fgetpos
fseek
ftell
rewind
fprintf
fwrite
fread
过一遍这些函数,并且记住它们的不同变体和它们的功能。例如,对于fscanf
的卡片,上面应该有scanf
、sscanf
、vscanf
,以及其它。并且在背面写下每个函数所做的事情。
最后,为了获得这些卡片所需的信息,使用man
来阅读它的帮助。例如,fscanf
帮助页由man fscanf
得到。
附加题
将这个程序重写为不需要
fscanf
的版本。你需要使用类似于atoi
的函数来将输入的字符串转换为数值。修改这个程序,使用
scanf
来代替fscanf
,并观察有什么不同。修改程序,是输入的名字不包含任何换行符和空白字符。
使用
scanf
编写函数,按照文件名读取文件内容,每次读取单个字符,但是不要越过(文件和缓冲区的)末尾。使这个函数接受字符串大小来更加通用,并且确保无论什么情况下字符串都以'\0'
结尾。