练习24:输入输出和文件

    你已经学会了使用来打印变量,这非常不错,但是还需要学习更多。这个练习中你会用到fscanffgets在结构体中构建关于一个人的信息。在这个关于读取输入的简介之后,你会得到C语言IO函数的完整列表。其中一些你已经见过并且使用过了,所以这个练习也是一个记忆练习。

    这个程序非常简单,并且引入了叫做fscanf的函数,意思是“文件的格式化输入”。scanf家族的函数是printf的反转版本。printf用于以某种格式打印数据,然而scanf以某种格式读取(或者扫描)输入。

    文件开头没有什么新的东西,所以下面只列出main所做的事情:

    ex24.c:24-28

    创建所需的变量。

    ex24.c:30-32

    使用fgets函数获取名字,它从输入读取字符串(这个例子中是stdin),但是确保它不会造成缓冲区溢出。

    ex24.c:34-36

    ex24.c:38-39

    使用fscanf来从stdin读取整数,并且将其放到you.age中。你可以看到,其中使用了和printf相同格式的格式化字符串。你也应该看到传入了you.age的地址,便于fscnaf获得它的指针来修改它。这是一个很好的例子,解释了使用指向数据的指针作为“输出参数”。

    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

    当你运行这个程序时,你应该看到你的输入被适当地转换。你应该尝试给它非预期的输入,看看程序是怎么预防它的。

    1. $ make ex24
    2. cc -Wall -g -DNDEBUG ex24.c -o ex24
    3. $ ./ex24
    4. What's your First Name? Zed
    5. What's your Last Name? Shaw
    6. How old are you? 37
    7. 1) Blue
    8. 2) Green
    9. 4) Black
    10. 5) Other
    11. > 1
    12. How much do you make an hour? 1.2345
    13. ----- RESULTS -----
    14. First Name: Zed
    15. Last Name: Shaw
    16. Age: 37
    17. Eyes: Blue
    18. Income: 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。你应该永远避免使用这个函数。

    最后,找到输入的地方,并移除对其是否在正确范围内的检查。然后,为它输入一个错误的数值,比如-1或者1000。在Valgrind执行这些操作,来观察会发生什么。

    译者注:根据最新的C11标准,对于输入函数,你应该总是使用_s后缀的安全版本。对于向字符串的输出函数,应该总是使用C99中新增的带n的版本,例如snprintf。如果你的编译器支持新版本,就不应该使用旧版本的不安全函数。

    这是一个各种IO函数的简单列表。你应该查询每个函数并为其创建速记卡,包含函数名称,功能和它的任何变体。

    • fscanf
    • fgets
    • fopen
    • fdopen
    • fclose
    • fcloseall
    • fgetpos
    • fseek
    • ftell
    • rewind
    • fprintf
    • fwrite
    • fread

    过一遍这些函数,并且记住它们的不同变体和它们的功能。例如,对于fscanf的卡片,上面应该有scanfsscanfvscanf,以及其它。并且在背面写下每个函数所做的事情。

    • 将这个程序重写为不需要fscanf的版本。你需要使用类似于atoi的函数来将输入的字符串转换为数值。
    • 修改这个程序,使用scanf来代替fscanf,并观察有什么不同。
    • 修改程序,是输入的名字不包含任何换行符和空白字符。