C For 循环跳过循环 scanf 中的第一次迭代和虚假数字 [英] C For loop skips first iteration and bogus number from loop scanf

查看:32
本文介绍了C For 循环跳过循环 scanf 中的第一次迭代和虚假数字的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在为学校创建邮件标签生成器,但遇到了一些问题.我的程序是从 0 到 10 获取个人的全名、地址、城市、州和邮政编码.在运行我的程序时,我遇到了两个主要问题.for 循环跳过全名safergets()"并移至地址safergets.我继续查看其他一切是否正常,但我对邮政编码的验证无法正常工作.我添加了一个 printf 来查看输入是否是相同的数字,并发现它是假的.此外,我在尝试将状态输出大写的行中收到错误代码.我确定我错误地使用了 toupper.下面附上我的代码、错误代码和输出.

#include #include /* 定义结构 */结构信息{字符全名[35],地址[50],城市[25],州[3];长整数邮政编码;};/* 函数更安全 *//* ------------------- */void safe_gets (char array[], int max_chars){/* 声明变量.*//* ------------------ */国际我;/* 从输入缓冲区读取信息,一个字符一个字符,*//* 直到可能的最大字符数.*//* ------------------------------------------------------ */for (i = 0; i 10)printf("无效数字.请重新输入数字.必须是从 0 到 10.
");}while(x<0 || x>10);/* 开始循环获取个人信息 */for(i = 0; i 99999){printf("
无效的邮政编码.必须是从 00001 到 99999.");}}while(person[i].zip_code<00001 || person[i].zip_code>99999);/* 结束循环 */}/* 循环结束 *//* 以邮件格式输出个人信息,条件为0个人*/if(x>0 && x<10){printf("

以下是您的邮寄标签:

");}/* 开始循环以输出个人邮寄标签 */for(i = 0; i 

错误代码:142:警告:传递 `toupper' 的 arg 1 会从指针生成整数而不进行强制转换.

输出:

欢迎使用邮件标签生成器程序.您想为 (0-10) 生成多少人的标签?1输入名字:输入街道地址:100 Needhelp Ave.输入城市:加德纳输入状态:NY输入邮政编码:01420无效的邮政编码.必须是从 00001 到 99999.输入邮政编号:

我在这里查看了几个问题,试图了解我哪里出错了,但如果觉得这可能是影响我的程序的几个问题.此外,我们的教授给我的班级提供了更安全的功能,以确保用户输入的字符不会超过数组可以容纳的字符数.感谢您的帮助和包容,帮助我理解我的错误!

解决方案

我们来一一看问题:

换行符在 No. 或 Persons Read 后仍保留在 stdin 中

<块引用>

 printf("

请输入姓名:");更安全的获取(人 [i].full_name,35);/* 这是被跳过的步骤 */

它被跳过是因为您的 safer_gets() 仅读取第一个 ' '(换行符 字符 -- 不是 回车,即' ').但是,saver_gets() 在输入流中看到的第一个字符是 ' ' 字符,在您调用scanf 在:

<块引用>

 printf("你想为(0-10)生成多少人的标签?");scanf("%i", &x);

所有scanf 格式说明符用于数字转换,仅通过构成数字的最后一位(或小数点)读取,离开' ' 由用户在输入流中按下 Enter 未读生成(此处为 stdin).这是鼓励新 C 程序员使用 面向行 输入函数读取用户输入的主要原因之一,例如 fgets()(或 POSIX getline()) 然后使用 sscanf 从填充的缓冲区中解析值.

为什么用户输入首选面向行的输入函数

通过使用具有足够缓冲区的面向行输入函数,可以消耗完整的用户输入行(包括用户按下 ' 'kbd>输入).这可以确保 stdin 为下一个输入做好准备,并且不会有前一个输入中剩余的未读字符等待咬你.

正确使用所有输入功能

如果您没有从这个答案中得到任何其他信息,请了解这一点 - 除非您检查返回,否则您无法正确使用任何输入功能.对于 scanf 函数系列尤其如此.为什么?如果您尝试使用 scanf 读取一个整数,而用户输入的是 "four",则会发生匹配失败,并从您的输入流在第一个无效字符处停止,在您的输入流中留下所有有问题的字符未读.(就等着再咬你).

正确使用 scanf

scanf 可以使用,如果使用得当.这意味着负责每次检查scanf返回.你必须处理三个条件

  1. (return == EOF) 用户通过按 Ctrl+d(或在 Windows Ctrl+z);
  2. (返回<预期的转换次数)发生匹配输入失败.对于匹配失败,您必须考虑输入缓冲区中剩余的每个字符.(在输入缓冲区中向前扫描读取和丢弃字符,直到找到 ' 'EOF);最后
  3. (return == expected No. of conversions) 表示读取成功——然后由您检查输入是否满足任何其他条件(例如正整数、正浮点数),在需要的范围内,等等.

在使用 scanf 成功读取后,您还必须考虑输入流中剩余的内容.如上所述,scanf 将使所有转换说明符的输入流中的 ' ' 保持未读状态,除非您在格式字符串中专门考虑了它em>(如果考虑到这一点,通常会导致输入格式字符串脆弱,在需要的输入之后但在 ' ' 之前很容易被额外的无关字符挫败)当使用 scanf 对于输入,您必须戴上您的会计师的帽子,并考虑输入流中剩余的每个字符,并在需要时清空输入流中的任何有问题的字符.

您可以编写一个简单的 empty_stdin() 函数,通过简单地向前扫描丢弃所有在 ' '<找到/code> 或遇到 EOF.您可以在 safer_gets() 函数中不同程度地做到这一点.你可以写一个简单的函数:

void empty_stdin(void){int c = getchar();/* 读取字符 */while (c != '
' && c != EOF)/* 如果不是 '
' 并且不是 EOF */c = getchar();/* 重复 */}

你可以用一个简单的 for 内联循环来做同样的事情,例如

for (int c = getchar(); c != '
' && c != EOF; c = getchar()) {}

下一个问题 -- 尝试写入无效地址

当使用 scanf 时,scanf 期望相应转换的参数是一个指向适当类型的指针.在:

<块引用>

 printf("
请输入邮编:");scanf("%ld", person[i].zip_code);/* 我在这里得到一个假数字 */

您未能提供一个指针,而是提供了一个 long int 值.由于 person[i].zip_codelong int 类型,以便为 scanf 提供一个指针来填充你必须使用 address-of 运算符,例如&person[i].zip_code 告诉 scanf 用它提供转换的值填充哪个地址.

等等?为什么我不必对数组这样做? 在访问时,数组被转换为指向第一个元素的指针.所以对于字符串输入,如果使用数组来保存字符串,它会自动转换为指针 C11 标准 - 6.3.2.1 其他操作数 - 左值、数组和函数指示符 (p3).

toupper 对字符而非字符串进行操作

<块引用>

 printf("%s
", toupper(person[i].state));/* 这是发生错误代码的地方 */

正如我在评论中所讨论的,toupperint 类型作为参数,而不是 char* 类型.要将字符串转换为大写/小写,您需要遍历每个字符,分别转换每个字符.但是,在您的结构的 .state 成员的情况下,只需担心 2 个字符,因此只需在读取它们时将它们都转换,例如

/* 只需 2 个字符即可转换为大写 - 在此处执行 */person[i].state[0] = toupper (person[i].state[0]);person[i].state[1] = toupper (person[i].state[1]);

safety_gets() 中的基本问题

这解决了大部分明显的问题,但是 safer_gets() 函数本身有几个基本问​​题.具体来说,当由 getchar() 返回时,它无法处理 EOF 并且无法向用户提供任何指示,即请求的用户输入是成功还是失败,因为没有返回任何内容类型为 void.在您编写的任何函数中,如果函数内有任何可能失败的可能性,您必须向调用函数提供一个有意义的返回,以指示该函数请求的操作是成功还是失败.

你可以用 safer_gets() 做什么?为什么不返回一个简单的 int 值,在成功时提供读取的字符数,或者在失败时返回 -1(EOF 的正常值).您现在可以获得双重奖励,即能够验证输入是否成功——并且您还可以获得字符串中的字符数(仅限于 2147483647 字符).您现在还可以通过在 Linux 上使用 Ctrl+dCtrl+z (Windows) 生成手动 EOF 来处理用户取消输入.

您还应该清空 stdin 在除 EOF 之外的所有情况下输入的所有字符.这可以确保在您调用 safer_gets() 之后没有任何未读的字符会在您稍后调用另一个输入函数时咬到您.进行这些更改,您可以将 safer_gets() 编写为:

/* 总是提供有意义的返回来指示成功/失败 */int safe_gets (char *array, int max_chars){int c = 0, nchar = 0;/* 循环,而数组中的空间和字符读取不是 '
' 或 EOF */while (nchar + 1 < max_chars && (c = getchar()) != '
' && c != EOF)数组[nchar++] = c;/* 赋值给数组,增加索引 */数组[nchar] = 0;/* 循环退出时空终止数组 */while (c != EOF && c != '
')/* 读/丢弃直到换行或 EOF */c = getchar();/* 如果 c == EOF 并且没有读取字符,返回 -1,否则没有.字符数 */返回 c == EOF &&!nchar ?-1:nchar;}

(注意: 上面对 nchar + 1 < max_chars 的测试确保 nul-termination 字符保留一个字符,并且只是 nchar < max_chars - 1)

的更安全的重新排列

输入验证的一般方法

现在,您有一个可以用来指示输入成功/失败的输入函数,允许您在调用函数中验证输入(此处为main()).以使用 safer_gets() 读取 .full_name 成员为例.您不能只是盲目地调用 safer_gets() 并且不知道是输入被取消还是遇到了过早的 EOF 并使用然后继续使用它充满信心的字符串你的代码.*验证、验证、验证每个表达式.回到 main(),您可以通过调用 safer_gets() 来读取 .full_name(以及所有其他字符串变量)来执行此操作:

#define NAMELEN 35/* 如果你需要一个常量,#define 一个(或多个)*/#define ADDRLEN 50/*(不要吝啬缓冲区大小)*/...for (;;) {/* 不断循环直到输入有效名称 */fputs ("
输入名称:", stdout);/* 迅速的 */int rtn = safe_gets(person[i].full_name, NAMELEN);/* 读取名称 */if (rtn == -1) {/* 用户取消输入 */puts ("(用户取消输入)");返回 1;/* 处理优雅退出 */}else if (rtn == 0) {/* 如果名称为空 - 处理错误 */fputs (" error: full_name empty.
", stderr);继续;/* 再试一次 */}else/* 好的输入 */休息;}

(注意:safer_gets() 的返回值被捕获在变量 rtn 中,然后为 -1 求值code> (EOF), 0 空字符串,或大于0,好输入)

您可以对需要使用的每个字符串变量执行此操作,然后使用上面讨论的相同原理来读取和验证 .zip_code.把它放在一个简短的例子中,你可以这样做:

#include #include #define NAMELEN 35/* 如果你需要一个常量,#define 一个(或多个)*/#define ADDRLEN 50/*(不要吝啬缓冲区大小)*/#define 城市 25#define STATELEN 3#define 人 10结构信息{字符全名[NAMELEN],地址[地址],城市[CITYLEN],状态[状态];长整数邮政编码;};/* 总是提供有意义的返回来指示成功/失败 */int safe_gets (char *array, int max_chars){int c = 0, nchar = 0;/* 循环,而数组中的空间和字符读取不是 '
' 或 EOF */while (nchar + 1 < max_chars && (c = getchar()) != '
' && c != EOF)数组[nchar++] = c;/* 赋值给数组,增加索引 */数组[nchar] = 0;/* 循环退出时空终止数组 */while (c != EOF && c != '
')/* 读/丢弃直到换行或 EOF */c = getchar();/* 如果 c == EOF 并且没有读取字符,返回 -1,否则没有.字符数 */返回 c == EOF &&!nchar ?-1:nchar;}int main (void) {/* 声明变量,初始化为零 */结构信息 person[PERSONS] = {{ .full_name = "" }};int i = 0, x = 0;puts ("
欢迎使用邮件标签生成器程序.
");/* 问候 */for (;;) {/* 不断循环直到一个有效的编号.进入的人数 */int rtn = 0;/* 保存从 scanf 返回的变量 */fputs("生成标签的人数?(0-10):", stdout);rtn = scanf ("%d", &x);if (rtn == EOF) {/* 用户生成的手动 EOF (ctrl+d [ctrl+z windows]) */puts ("(用户取消输入)");返回0;}else {/* 好的输入或(匹配失败或超出范围)*//* 所有需要通过换行符清除 - 在这里做 */for (int c = getchar(); c != '
' && c != EOF; c = getchar()) {}if (rtn == 1) {/* 返回等于请求的转换 - 好的输入 */if (0 <= x && x <= PERSONS)/* 验证范围内的输入 */休息;/* 所有检查通过,中断读取循环 */else/* 否则,输入超出范围 */fprintf (stderr, " 错误: %d, 不在范围 0 - %d 内.
",x,人);}else/* 匹配失败 */fputs(" 错误:整数输入无效.
", stderr);}}if (!x) {/* 因为零是一个有效的输入,在这里检查,如果零请求退出 */fputs ("
请求的人数为零 - 无事可做.
", stdout);返回0;}/* 开始循环获取个人信息 */for (i = 0; i < x; i++) {/* 循环直到所有人都填满 *//* 读取姓名、地址、城市、州 */for (;;) {/* 不断循环直到输入有效名称 */fputs ("
输入名称:", stdout);/* 迅速的 */int rtn = safe_gets(person[i].full_name, NAMELEN);/* 读取名称 */if (rtn == -1) {/* 用户取消输入 */puts ("(用户取消输入)");返回 1;/* 处理优雅退出 */}else if (rtn == 0) {/* 如果名称为空 - 处理错误 */fputs (" error: full_name empty.
", stderr);继续;/* 再试一次 */}else/* 好的输入 */休息;}for (;;) {/* 不断循环直到有效的街道输入 */fputs("输入街道地址:", stdout);/* 迅速的 */int rtn =safety_gets(person[i].address, ADDRLEN);/* 读取地址 */if (rtn == -1) {/* 用户取消输入 */puts ("(用户取消输入)");返回 1;/* 处理优雅退出 */}else if (rtn == 0) {/* 如果地址为空 - 处理错误 */fputs("错误:街道地址为空.
", stderr);继续;/* 再试一次 */}else/* 好的输入 */休息;}for (;;) {/* 不断循环直到有效的城市输入 */fputs("输入城市:", stdout);/* 迅速的 */int rtn =safety_gets(person[i].city, CITYLEN);/* 读取城市 */if (rtn == -1) {/* 用户取消输入 */puts ("(用户取消输入)");返回 1;/* 处理优雅退出 */}else if (rtn == 0) {/* 如果城市为空 - 处理错误 */fputs ("错误: 城市为空.
", stderr);继续;/* 再试一次 */}else/* 好的输入 */休息;}for (;;) {/* 不断循环直到有效的状态输入 */fputs("进入状态:", stdout);/* 迅速的 */int rtn =safety_gets(person[i].state, STATELEN);/* 读取状态 */if (rtn == -1) {/* 用户取消输入 */puts ("(用户取消输入)");返回 1;/* 处理优雅退出 */}else if (rtn == 0) {/* 如果状态为空 - 处理错误 */fputs("error: state empty.
", stderr);继续;/* 再试一次 */}else {/* 好的输入 *//* 只需 2 个字符即可转换为大写 - 在此处执行 */person[i].state[0] = toupper (person[i].state[0]);person[i].state[1] = toupper (person[i].state[1]);休息;}}/* 读取/验证邮政编码 */for (;;) {/* 不断循环直到输入有效的邮政编码 */fputs("输入邮政编码:", stdout);/* 迅速的 */int rtn = scanf ("%ld", &person[i].zip_code);/* 读取压缩包 */if (rtn == EOF) {/* 用户按下 ctrl+d [ctrl+z windows] */puts ("(用户取消输入)");返回 1;}else {/* 处理所有其他情况 *//* 通过换行符或 EOF 删除所有字符 */for (int c = getchar(); c != '
' && c != EOF; c = getchar()) {}if (rtn == 1) {/* long int read *//* 在范围内验证 */if (1 <= person[i].zip_code && person[i].zip_code <= 99999)休息;别的fprintf (stderr, " 错误: %ld 不在 1 - 99999 的范围内.
",人[i].zip_code);}else/* 匹配失败 */fputs(" 错误:无效的长整数输入.
", stderr);}}}/* 以邮件格式输出个人信息,条件为0个人*/for(i = 0; i 

(注意:通过使用 #define 创建所需的常量,如果您需要调整一个数字,您可以在一个地方进行更改,并且您是尽管每个变量声明和循环限制都没有留下来尝试进行更改)

示例使用/输出

当你写完任何输入例程时——去尝试打破它!找到失败的极端情况并修复它们.不断尝试通过故意输入不正确/无效的输入来破坏它,直到它不再除了用户需要输入的内容之外的任何内容.练习你的输入例程,例如

$ ./bin/nameaddrstruct欢迎使用邮件标签生成器程序.要为其生成标签的人数?(0-10): 3输入名称:米老鼠输入街道地址:111 Disney Ln.输入城市:奥兰多进入状态:fL输入邮政编码:44441输入名称:米妮老鼠输入街道地址:112 Disney Ln.输入城市:奥兰多进入状态:Fl输入邮政编码:44441输入名称:冥王星(狗)输入街道地址:111-b.yard Disney Ln.输入城市:奥兰多输入状态:fl输入邮政编码:44441米老鼠111迪斯尼Ln.佛罗里达州奥兰多市 44441米妮老鼠112迪斯尼Ln.佛罗里达州奥兰多市 44441冥王星(狗)111-b.yard Disney Ln.佛罗里达州奥兰多市 44441感谢您使用该程序.

尊重用户希望在 Linux 上使用 Ctrl+dCtrl+z (Windows) 生成手动 EOF 时随时取消输入,您应该能够从代码中的任何一点处理它.

在第一个提示处:

$ ./bin/nameaddrstruct欢迎使用邮件标签生成器程序.要为其生成标签的人数?(0-10): (用户取消输入)

或在此后的任何提示处:

$ ./bin/nameaddrstruct欢迎使用邮件标签生成器程序.要为其生成标签的人数?(0-10): 3输入名称:米老鼠输入街道地址:111 Disney Ln.输入城市:(用户取消输入)

处理零人请求:

$ ./bin/nameaddrstruct欢迎使用邮件标签生成器程序.要为其生成标签的人数?(0-10): 0要求零人 - 无事可做.

(**个人而言,我只想更改输入测试并让他们输入 1-10 中的值)

无效输入:

$ ./bin/nameaddrstruct欢迎使用邮件标签生成器程序.要为其生成标签的人数?(0-10):-1错误:-1,不在 0 - 10 范围内.要为其生成标签的人数?(0-10): 11错误:11,不在 0 - 10 范围内.要为其生成标签的人数?(0-10): 香蕉错误:无效的整数输入.要为其生成标签的人数?(0-10): 10输入姓名:(用户取消输入)

你明白了... 最重要的是,在你的程序中使用输入之前,你必须验证每个用户输入并知道它是有效的.除非您检查返回,否则您无法验证来自任何函数的任何输入.如果你除此之外什么都不带走,那么学习是值得的.

检查一下,如果您还有其他问题,请告诉我.(并询问您的教授.safer_gets() 如何处理 EOF 以及您应该如何验证函数是成功还是失败)

I am creating a mailing label generator for school and am having an issue with a few problems. My program is to take the full name, address, city, state, and zip code for individuals from 0 to 10. When running my program I am having two major problems. The for loop skips the full name "safergets()" and moves on to the address safergets. I moved on to see if everything else works, but my verification for the zip code would not work correctly. I added a printf to see if the input was the same number and found it to be bogus. Also, I am getting an error code for my line attempting to capitalize the state output. I'm sure I am using toupper incorrectly. Attached below is my code, error code, and output.

#include <stdio.h>
#include <ctype.h>

/* Define structure */

struct information
{
    char full_name[35], address[50], city[25], state[3];
    long int zip_code;
};

/* Function safer_gets */
/* ------------------- */

void safer_gets (char array[], int max_chars)
{
  /* Declare variables. */
  /* ------------------ */

  int i;

  /* Read info from input buffer, character by character,   */
  /* up until the maximum number of possible characters.    */
  /* ------------------------------------------------------ */

  for (i = 0; i < max_chars; i++)
  {
     array[i] = getchar();


     /* If "this" character is the carriage return, exit loop */
     /* ----------------------------------------------------- */

     if (array[i] == '
')
        break;

   } /* end for */

   /* If we have pulled out the most we can based on the size of array, */
   /* and, if there are more chars in the input buffer,                 */
   /* clear out the remaining chars in the buffer.                      */
   /* ----------------------------------------------------------------  */

   if (i == max_chars )

     if (array[i] != '
')
       while (getchar() != '
');

   /* At this point, i is pointing to the element after the last character */
   /* in the string. Terminate the string with the null terminator.        */
   /* -------------------------------------------------------------------- */

   array[i] = '';


} /* end safer_gets */

/* Begin main */

int main()
{
    /* Declare variables */

    struct information person[10];
    int x, i;

    /* Issue greeting */

    printf("Welcome to the mailing label generator program.

");

    /* Prompt user for number of individuals between 0 - 10. If invalid, re-prompt */

    do
    {
        printf("How many people do you want to generate labels for (0-10)? ");
        scanf("%i", &x);

        if(x<0 || x>10)
        printf("Invalid number. Please re-enter number. Must be from 0 to 10.
");

    }while(x<0 || x>10);

    /* Begin loop for individual information */

    for(i = 0; i < x; i++)
    {
        printf("

Enter name: ");
        safer_gets(person[i].full_name, 35); /* This is the step being skipped */

        printf("
Enter street address: ");
        safer_gets(person[i].address, 50);

        printf("
Enter city: ");
        safer_gets(person[i].city, 25);

        printf("
Enter state: ");
        gets(person[i].state);

        /* Begin loop to verify correct zipcode */

        do
        {
            printf("
Enter zipcode: ");
            scanf("%ld", person[i].zip_code); /* I get a bogus number here */

            if(person[i].zip_code<00001 || person[i].zip_code>99999)
            {
                printf("
Invalid zipcode. Must be from 00001 to 99999.");
            }
        }while(person[i].zip_code<00001 || person[i].zip_code>99999);
        /* end loop */

    }/* end of loop */

    /* Output individual information in mailing format, condition for 0 individuals */
    if(x>0 && x<10)
    {
    printf("

Below are your mailing labels:

");
    }

    /* Begin loop for outputting individual(s) mailing labels */

    for(i = 0; i < x; i++)
    {
        printf("%s
",person[i].full_name);
        printf("%s
",person[i].address);
        printf("%s
",person[i].city);

        /* Output state in all uppercase */

        printf("%s
", toupper(person[i].state)); /* This is where the error code is occurring */

        printf("%.5ld

", person[i].zip_code);
    } /* end of loop */

    printf("Thank you for using the program.
");

}/*end of main */

Error code:142: warning: passing arg 1 of `toupper' makes integer from pointer without a cast.

Output:

Welcome to the mailing label generator program.

How many people do you want to generate labels for (0-10)? 1


Enter name:
Enter street address: 100 Needhelp Ave.

Enter city: Gardner

Enter state: NY

Enter zipcode: 01420

Invalid zipcode. Must be from 00001 to 99999.
Enter zipcode:

I have looked at several questions on here to try and understand where I am going wrong, but if feel it might be a couple of issues effecting my program. Also, the safergets function was given to my class by our professor to ensure that the user does not input more characters than the array can hold. Thank you for your help and tolerance in helping me understand my mistakes!

解决方案

Let's take a look at the problem one by one:

newline Remains in stdin after No. or Persons Read

    printf("

Enter name: ");
    safer_gets(person[i].full_name, 35); /* This is the step being skipped */

It is skipped because your safer_gets() reads only to the first ' ' (newline character -- not a carriage-return, that is ' '). However, the first character saver_gets() sees in the input stream is the ' ' character that remains in stdin unread after your call to scanf in:

    printf("How many people do you want to generate labels for (0-10)? ");
    scanf("%i", &x);

All scanf format specifiers for numeric conversion read only through the last digit (or decimal point) that makes up a number leaving the ' ' generated by the user pressing Enter unread in the input-stream (stdin here). This is one of the primary reasons new C programmers are encouraged to read user input with a line-oriented input function such as fgets() (or POSIX getline()) and then use sscanf to parse values from the filled buffer.

Why Line-Oriented Input Functions are Preferred for User-Input

By using a line-oriented input function with sufficient buffer, the complete line of user-input is consumed (including the ' ' from the user pressing Enter). This ensures that stdin is ready for the next input and doesn't have unread characters left-over from a prior input waiting to bite you.

Using All Input Functions Correctly

If you take nothing else from this answer, learn this -- you cannot use any input function correctly unless you check the return. This is especially true for the scanf family of functions. Why? If you are attempting to read an integer with scanf and the user enters "four" instead, then a matching failure occurs and character extraction from your input stream ceases with the first invalid character leaving all offending characters in your input stream unread. (just waiting to bite you again).

Using scanf Correctly

scanf can be used, if used correctly. This means you are responsible for checking the return of scanf every time. You must handle three conditions

  1. (return == EOF) the user canceled input by generating a manual EOF by pressing Ctrl+d (or on windows Ctrl+z);
  2. (return < expected No. of conversions) a matching or input failure occurred. For a matching failure you must account for every character left in your input buffer. (scan forward in the input buffer reading and discarding characters until a ' ' or EOF is found); and finally
  3. (return == expected No. of conversions) indicating a successful read -- it is then up to you to check whether the input meets any additional criteria (e.g. positive integer, positive floating-point, within a needed range, etc..).

You also must account for what is left in your input stream after a successful read with scanf. As discussed above, scanf will leave the ' ' in the input stream unread for ALL conversion specifiers unless you specifically account for it in your format string (which, if accounted for, usually results in a fragile input format string, easily foiled by additional extraneous characters after the wanted input but before the ' ') When using scanf for input, you must put your accountant's hat on and account for every character that remains in the input stream and empty the input stream of any offending characters when required.

You can write a simple empty_stdin() function to handle removing all extraneous characters that remain after a user input by simply scanning forward discarding all characters that remain until the ' ' is found or EOF encountered. You do that to a varying degree in your safer_gets() function. You can write a simple function as:

void empty_stdin(void)
{
    int c = getchar();                /* read character */

    while (c != '
' && c != EOF)     /* if not '
' and not EOF */
        c = getchar();                /* repeat */
}

You can do the same thing with a simple for loop inline, e.g.

for (int c = getchar(); c != '
' && c != EOF; c = getchar()) {}

Next Problem -- Attempting to Write to Invalid Address

When using scanf, scanf expects the parameter for a corresponding conversion to be a pointer to the appropriate type. In:

         printf("
Enter zipcode: ");
         scanf("%ld", person[i].zip_code); /* I get a bogus number here */

You fail to provide a pointer, providing a long int value instead. Since person[i].zip_code is type long int in order to provide a pointer for scanf to fill you must use the address-of operator, e.g. &person[i].zip_code to tell scanf which address to fill with the value it provides a conversion for.

Wait? Why don't I have to do that with array? On access, an array is converted to a pointer to the first element. So for string input, if an array is being used to hold the string, it is automatically converted to a pointer C11 Standard - 6.3.2.1 Other Operands - Lvalues, arrays, and function designators(p3).

toupper Operates on Characters not Strings

    printf("%s
", toupper(person[i].state)); /* This is where the error code is occurring */

As discussed in my comment, toupper takes type int as the parameter, not type char*. To convert a string to upper/lower case, you need to loop over each character converting each character individually. However, in your case with the .state member of your struct, there are only 2 characters to worry about, so just convert them both when they are read, e.g.

            /* just 2-chars to convert to upper - do it here */
            person[i].state[0] = toupper (person[i].state[0]);
            person[i].state[1] = toupper (person[i].state[1]);

Fundamental Problems in safer_gets()

That takes care of the most of the obvious problems, but the safer_gets() function itself has several fundamental problems. Specifically, it fails to handle EOF when returned by getchar() and it fails to provide any indication to the user whether the requested user-input succeeded or failed due to returning nothing with type void. In any function you write, where there is any possibility of failure within the function, you MUST provide a meaningful return to the calling function to indicate whether the requested operation of the function succeeded or failed.

What can you do with safer_gets()? Why not return a simple int value providing the number of characters read on success, or -1 (the normal value for EOF) on failure. You get the double-bonus of now being able to validate whether the input succeeded -- and you also get the number of character in the string (limited to 2147483647 chars). You also now have the ability to handle a user canceling the input by generating a manual EOF with Ctrl+d on Linux or Ctrl+z (windows).

You should also empty stdin of all characters input in ALL cases other than EOF. This ensures there are no characters that remain unread after your call to safer_gets() that can bite you if you make a call to another input function later. Making those changes, you could write your safer_gets() as:

/* always provide a meaninful return to indicate success/failure */
int safer_gets (char *array, int max_chars)
{
    int c = 0, nchar = 0;

    /* loop while room in array and char read isn't '
' or EOF */
    while (nchar + 1 < max_chars && (c = getchar()) != '
' && c != EOF)
        array[nchar++] = c;         /* assing to array, increment index */
    array[nchar] = 0;               /* nul-terminate array on loop exit */

    while (c != EOF && c != '
')   /* read/discard until newline or EOF */
        c = getchar();

    /* if c == EOF and no chars read, return -1, otherwise no. of chars */
    return c == EOF && !nchar ? -1 : nchar;
}

(note: above the test on nchar + 1 < max_chars ensures that a character remains for the nul-terminating character, and is just a safer rearrangement of nchar < max_chars - 1)

General Approach to Input Validation

Now, you have an input function you can use that indicates success/failure of input allowing you to validate the input back in the calling function (main() here). Take for example reading the .full_name member using safer_gets(). You can't just blindly call safer_gets() and not know whether input was canceled or a premature EOF encountered and use then proceed to use the string it filled with any confidence in your code. *Validate, validate, validate each expression. Back in main(), you can do that by calling safer_gets() as follows to read .full_name (and every other string variable):

#define NAMELEN 35  /* if you need a constant, #define one (or more) */
#define ADDRLEN 50  /*         (don't skimp on buffer size)          */
...
        for (;;) {      /* loop continually until valid name input */
            fputs ("
Enter name           : ", stdout);            /* prompt */
            int rtn = safer_gets(person[i].full_name, NAMELEN);     /* read name */
            if (rtn == -1) {        /* user canceled input */
                puts ("(user canceled input)");
                return 1;           /* handle with graceful exit */
            }
            else if (rtn == 0) {    /* if name empty - handle error */
                fputs ("  error: full_name empty.
", stderr);
                continue;           /* try again */
            }
            else                    /* good input */
                break;
        }

(note: the return of safer_gets() is captured in the variable rtn and then evaluated for -1 (EOF), 0 empty-string, or greater than 0, good input)

You can do that for each string variable you need to use, and then use the same principals discussed above to read and validate .zip_code. Putting it altogether in a short example, you could do:

#include <stdio.h>
#include <ctype.h>

#define NAMELEN 35  /* if you need a constant, #define one (or more) */
#define ADDRLEN 50  /*         (don't skimp on buffer size)          */
#define CITYLEN 25
#define STATELEN 3
#define PERSONS 10

struct information {
    char full_name[NAMELEN],
        address[ADDRLEN],
        city[CITYLEN],
        state[STATELEN];
    long int zip_code;
};

/* always provide a meaninful return to indicate success/failure */
int safer_gets (char *array, int max_chars)
{
    int c = 0, nchar = 0;

    /* loop while room in array and char read isn't '
' or EOF */
    while (nchar + 1 < max_chars && (c = getchar()) != '
' && c != EOF)
        array[nchar++] = c;         /* assing to array, increment index */
    array[nchar] = 0;               /* nul-terminate array on loop exit */

    while (c != EOF && c != '
')   /* read/discard until newline or EOF */
        c = getchar();

    /* if c == EOF and no chars read, return -1, otherwise no. of chars */
    return c == EOF && !nchar ? -1 : nchar;
}

int main (void) {

    /* declare varaibles, initialize to all zero */
    struct information person[PERSONS] = {{ .full_name = "" }};
    int i = 0, x = 0;

    puts ("
Welcome to the mailing label generator program.
");   /* greeting */

    for (;;) {          /* loop continually until a valid no. of people entered */
        int rtn = 0;    /* variable to hold RETURN from scanf */

        fputs ("Number of people to generate labels for? (0-10): ", stdout);
        rtn = scanf ("%d", &x);

        if (rtn == EOF) {   /* user generated manual EOF (ctrl+d [ctrl+z windows]) */
            puts ("(user canceled input)");
            return 0;
        }
        else {  /* either good input or (matching failure or out-of-range) */
            /* all required clearing though newline - do that here */
            for (int c = getchar(); c != '
' && c != EOF; c = getchar()) {}

            if (rtn == 1) { /* return equals requested conversions - good input */
                if (0 <= x && x <= PERSONS) /* validate input in range */
                    break;                  /* all checks passed, break read loop */
                else                        /* otherwise, input out of range */
                    fprintf (stderr, "  error: %d, not in range 0 - %d.
",
                            x, PERSONS);
            }
            else    /* matching failure */
                fputs ("  error: invalid integer input.
", stderr);
        }
    }
    if (!x) {   /* since zero is a valid input, check here, exit if zero requested */
        fputs ("
zero persons requested - nothing further to do.
", stdout);
        return 0;
    }

    /* Begin loop for individual information */

    for (i = 0; i < x; i++) {   /* loop until all person filled */

        /* read name, address, city, state */
        for (;;) {      /* loop continually until valid name input */
            fputs ("
Enter name           : ", stdout);            /* prompt */
            int rtn = safer_gets(person[i].full_name, NAMELEN);     /* read name */
            if (rtn == -1) {        /* user canceled input */
                puts ("(user canceled input)");
                return 1;           /* handle with graceful exit */
            }
            else if (rtn == 0) {    /* if name empty - handle error */
                fputs ("  error: full_name empty.
", stderr);
                continue;           /* try again */
            }
            else                    /* good input */
                break;
        }

        for (;;) {      /* loop continually until valid street input */
            fputs ("Enter street address : ", stdout);              /* prompt */
            int rtn = safer_gets(person[i].address, ADDRLEN);       /* read address */
            if (rtn == -1) {        /* user canceled input */
                puts ("(user canceled input)");
                return 1;           /* handle with graceful exit */
            }
            else if (rtn == 0) {    /* if address empty - handle error */
                fputs ("error: street address empty.
", stderr);
                continue;           /* try again */
            }
            else                    /* good input */
                break;
        }

        for (;;) {      /* loop continually until valid city input */
            fputs ("Enter city           : ", stdout);              /* prompt */
            int rtn = safer_gets(person[i].city, CITYLEN);          /* read city */
            if (rtn == -1) {        /* user canceled input */
                puts ("(user canceled input)");
                return 1;           /* handle with graceful exit */
            }
            else if (rtn == 0) {    /* if city empty - handle error */
                fputs ("error: city empty.
", stderr);
                continue;           /* try again */
            }
            else                    /* good input */
                break;
        }

        for (;;) {      /* loop continually until valid state input */
            fputs ("Enter state          : ", stdout);              /* prompt */
            int rtn = safer_gets(person[i].state, STATELEN);        /* read state */
            if (rtn == -1) {        /* user canceled input */
                puts ("(user canceled input)");
                return 1;           /* handle with graceful exit */
            }
            else if (rtn == 0) {    /* if state empty - handle error */
                fputs ("error: state empty.
", stderr);
                continue;           /* try again */
            }
            else {                  /* good input */
                /* just 2-chars to convert to upper - do it here */
                person[i].state[0] = toupper (person[i].state[0]);
                person[i].state[1] = toupper (person[i].state[1]);
                break;
            }
        }

        /* read/validate zipcode */
        for (;;) {      /* loop continually until valid zipcode input */
            fputs ("Enter zipcode        : ", stdout);              /* prompt */
            int rtn = scanf ("%ld", &person[i].zip_code);           /* read zip */

            if (rtn == EOF) {   /* user pressed ctrl+d [ctrl+z windows] */
                puts ("(user canceled input)");
                return 1;
            }
            else {      /* handle all other cases */
                /* remove all chars through newline or EOF */
                for (int c = getchar(); c != '
' && c != EOF; c = getchar()) {}

                if (rtn == 1) {    /* long int read */
                    /* validate in range */
                    if (1 <= person[i].zip_code && person[i].zip_code <= 99999)
                        break;
                    else
                        fprintf (stderr, "  error: %ld not in range of 1 - 99999.
",
                                person[i].zip_code);
                }
                else    /* matching failure */
                    fputs ("  error: invalid long integer input.
", stderr);
            }
        }
    }

    /* Output individual information in mailing format, condition for 0 individuals */
    for(i = 0; i < x; i++)
        /* you only need a single printf */
        printf ("
%s
%s
%s, %s %ld
", person[i].full_name, person[i].address,
                person[i].city, person[i].state, person[i].zip_code);

    fputs ("
Thank you for using the program.
", stdout);
}

(note: by using #define to create the needed constants, if you need to adjust a number, you have a single place to make the change and you are not left picking though each variable declaration and loop limit to try and make the change)

Example Use/Output

When you have finished writing any input routine -- go try and break it! Find the corner-cases that fail and fix them. Keep trying to break it by intentionally entering incorrect/invalid input until it no longer excepts anything but what the user is required to input. Exercise your input routines, e.g.

$ ./bin/nameaddrstruct

Welcome to the mailing label generator program.

Number of people to generate labels for? (0-10): 3

Enter name           : Mickey Mouse
Enter street address : 111 Disney Ln.
Enter city           : Orlando
Enter state          : fL
Enter zipcode        : 44441

Enter name           : Minnie Mouse
Enter street address : 112 Disney Ln.
Enter city           : Orlando
Enter state          : Fl
Enter zipcode        : 44441

Enter name           : Pluto (the dog)
Enter street address : 111-b.yard Disney Ln.
Enter city           : Orlando
Enter state          : fl
Enter zipcode        : 44441

Mickey Mouse
111 Disney Ln.
Orlando, FL 44441

Minnie Mouse
112 Disney Ln.
Orlando, FL 44441

Pluto (the dog)
111-b.yard Disney Ln.
Orlando, FL 44441

Thank you for using the program.

Respecting the users wish to cancel input at any point when they generates a manual EOF with Ctrl+d on Linux or Ctrl+z (windows), you should be able to handle that from any point in your code.

At the first prompt:

$ ./bin/nameaddrstruct

Welcome to the mailing label generator program.

Number of people to generate labels for? (0-10): (user canceled input)

Or at any prompt thereafter:

$ ./bin/nameaddrstruct

Welcome to the mailing label generator program.

Number of people to generate labels for? (0-10): 3

Enter name           : Mickey Mouse
Enter street address : 111 Disney Ln.
Enter city           : (user canceled input)

Handle a request for zero person:

$ ./bin/nameaddrstruct

Welcome to the mailing label generator program.

Number of people to generate labels for? (0-10): 0

zero persons requested - nothing further to do.

(**personally, I would just change the input test and have them enter a value from 1-10 instead)

Invalid input:

$ ./bin/nameaddrstruct

Welcome to the mailing label generator program.

Number of people to generate labels for? (0-10): -1
  error: -1, not in range 0 - 10.
Number of people to generate labels for? (0-10): 11
  error: 11, not in range 0 - 10.
Number of people to generate labels for? (0-10): banannas
  error: invalid integer input.
Number of people to generate labels for? (0-10): 10

Enter name           : (user canceled input)

You get the point... Bottom line, you must validate every user input and know it is valid before making use of the input in your program. You cannot validate any input from any function unless you check the return. If you take away nothing except that, the learning has been worthwhile.

Look things over and let me know if you have further questions. (and ask your prof. how safer_gets() handles EOF and how you are supposed to validate whether the funciton succeeded or failed)

这篇关于C For 循环跳过循环 scanf 中的第一次迭代和虚假数字的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆