C For 循环跳过循环 scanf 中的第一次迭代和伪造数字

C For loop skips first iteration and bogus number from loop scanf

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

#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] == '\n')
        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] != '\n')
       while (getchar() != '\n');

   /* 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] = '[=10=]';


} /* 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.\n\n");

    /* 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.\n");

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

    /* Begin loop for individual information */

    for(i = 0; i < x; i++)
    {
        printf("\n\nEnter name: ");
        safer_gets(person[i].full_name, 35); /* This is the step being skipped */

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

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

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

        /* Begin loop to verify correct zipcode */

        do
        {
            printf("\nEnter 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("\nInvalid 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("\n\nBelow are your mailing labels:\n\n");
    }

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

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

        /* Output state in all uppercase */

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

        printf("%.5ld\n\n", person[i].zip_code);
    } /* end of loop */

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

}/*end of main */

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

输出:

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:

我已经查看了这里的几个问题,试图了解我哪里出错了,但如果我觉得可能有几个问题影响了我的程序。此外,我们的教授为我的 class 提供了 safergets 函数,以确保用户输入的字符不会超过数组可以容纳的字符数。 感谢您的帮助和包容,让我明白了自己的错误!

让我们一一来看问题:

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

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

它被跳过,因为你的 safer_gets() 只读到第一个 '\n'newline 字符 -- 不是 carriage-return,即'\r')。但是,saver_gets() 在输入流中看到的第一个字符是 '\n' 字符,在您调用 scanf in:

之后,该字符仍保留在 stdin 中未读
    printf("How many people do you want to generate labels for (0-10)? ");
    scanf("%i", &x);

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

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

通过使用具有足够缓冲区的 面向行的 输入函数,用户输入的完整行被消耗(包括来自用户按下的 '\n' 输入)。这可确保 stdin 已为下一次输入做好准备,并且不会有上一次输入遗留下来的未读字符等着你。

正确使用所有输入功能

如果您从这个答案中没有得到任何其他信息,请了解这一点——您无法正确使用任何输入功能,除非您检查 return。对于 scanf 系列函数尤其如此。为什么?如果您尝试使用 scanf 读取整数,而用户输入 "four",则会发生 匹配失败 并且从您的输入流中提取字符会随着第一个无效字符将所有违规字符留在输入流中 unread。 (等着再咬你一口)

正确使用scanf

scanf可以用,只要使用正确。这意味着负责检查scanfreturn每次.您必须处理 三个条件

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

您还必须考虑使用 scanf 成功读取后输入流中剩余的内容。如上所述,scanf 将使所有转换说明符的输入流中的 '\n' 保持未读状态,除非您在 格式字符串 中特别说明它(如果占,通常会导致输入格式字符串脆弱,很容易被所需输入之后但 '\n' 之前的额外无关字符所挫败)当使用 scanf 进行输入时,您必须戴上会计师的帽子和帐户对于保留在输入流中的每个字符,并在需要时清空输入流中的所有违规字符。

您可以编写一个简单的 empty_stdin() 函数来处理删除用户输入后剩余的所有无关字符,方法是简单地向前扫描并丢弃剩余的所有字符,直到找到 '\n'EOF遇到。您在 safer_gets() 函数中在不同程度上做到了这一点。您可以编写一个简单的函数:

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

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

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

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

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

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

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

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

等等?为什么我不必对数组执行此操作? 在访问时,数组被转换为指向第一个元素的指针。所以对于字符串输入,如果正在使用数组来保存字符串,它会自动转换为指针 C11 Standard - 6.3.2.1 Other Operands - Lvalues, arrays, and function designators(p3).

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

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

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

            /* 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]);

safer_gets()

中的基本问题

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

你可以用 safer_gets() 做什么?为什么不 return 一个简单的 int 值提供成功读取的字符数,或者 -1EOF 的正常值)失败。您得到的双重好处是现在能够验证输入是否成功——并且您还可以获得字符串中的字符数(限制为 2147483647 个字符)。您现在还可以通过在 Linux 或 Ctrl+z[=245 上使用 Ctrl+d 生成手动 EOF 来处理用户取消输入=] (windows).

EOF 外,您还应清空 stdin 所有情况下输入的所有字符。这样可以确保在调用 safer_gets() 后没有未读字符,如果您稍后调用另一个输入函数,这些字符会咬住您。进行这些更改后,您可以将 safer_gets() 写为:

/* 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 '\n' or EOF */
    while (nchar + 1 < max_chars && (c = getchar()) != '\n' && c != EOF)
        array[nchar++] = c;         /* assing to array, increment index */
    array[nchar] = 0;               /* nul-terminate array on loop exit */

    while (c != EOF && c != '\n')   /* 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;
}

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

的更安全的重新排列

输入验证的一般方法

现在,您有一个可以使用的输入函数,它指示 success/failure 的输入,允许您在调用函数(此处为 main())中验证输入。以使用 safer_gets() 读取 .full_name 成员为例。您不能只是盲目地调用 safer_gets() 而不知道输入是否被取消或遇到过早的 EOF 并使用然后继续使用它填充的字符串,您对您的代码充满信心。 *验证,验证,验证 每个表达式。回到 main(),您可以通过如下调用 safer_gets() 来读取 .full_name(以及所有其他字符串变量):

#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 ("\nEnter 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.\n", stderr);
                continue;           /* try again */
            }
            else                    /* good input */
                break;
        }

(注: safer_gets() 的 return 被捕获在变量 rtn 中,然后计算为 -1 ( EOF), 0 空字符串, 或大于 0, 好的输入)

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

#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 '\n' or EOF */
    while (nchar + 1 < max_chars && (c = getchar()) != '\n' && c != EOF)
        array[nchar++] = c;         /* assing to array, increment index */
    array[nchar] = 0;               /* nul-terminate array on loop exit */

    while (c != EOF && c != '\n')   /* 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 ("\nWelcome to the mailing label generator program.\n");   /* 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 != '\n' && 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.\n",
                            x, PERSONS);
            }
            else    /* matching failure */
                fputs ("  error: invalid integer input.\n", stderr);
        }
    }
    if (!x) {   /* since zero is a valid input, check here, exit if zero requested */
        fputs ("\nzero persons requested - nothing further to do.\n", 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 ("\nEnter 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.\n", 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.\n", 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.\n", 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.\n", 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 != '\n' && 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.\n",
                                person[i].zip_code);
                }
                else    /* matching failure */
                    fputs ("  error: invalid long integer input.\n", stderr);
            }
        }
    }

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

    fputs ("\nThank you for using the program.\n", stdout);
}

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

示例Use/Output

当您完成任何输入例程的编写时 -- 去尝试打破它!找到失败的极端情况并修复它们。继续尝试通过故意输入 incorrect/invalid 输入来打破它,直到它不再除了用户需要输入的内容之外的任何内容。练习你的输入例程,例如

$ ./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.

尊重用户希望在使用 Ctrl+d 在 Linux 或 Ctrl+z 上生成手动 EOF 时随时取消输入 (windows),您应该能够从代码中的任何位置处理它。

在第一次提示时:

$ ./bin/nameaddrstruct

Welcome to the mailing label generator program.

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

或之后的任何提示:

$ ./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)

处理零人请求:

$ ./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.

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

输入无效:

$ ./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)

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

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