我们来一一分析一下问题:
读取号码或人员后,换行符保留在标准输入中
printf("\n\nEnter name: ");
safer_gets(person[i].full_name, 35); /* This is the step being skipped */
它被跳过,因为你的safer_gets()
只读到第一个'\n'
(newline字符——不是一个回车, 那是'\r'
)。然而第一个角色saver_gets()
在输入流中看到的是'\n'
保留在的字符stdin
致电后未读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 '\n'
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.
为什么面向行的输入函数更适合用户输入
By using a line-oriented input function with sufficient buffer, the complete line of user-input is consumed (including the '\n'
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.
正确使用所有输入功能
如果您从这个答案中没有得到任何其他内容,请了解这一点 - 您无法正确使用任何输入功能,除非您检查退货。对于scanf
函数族。为什么?如果您尝试使用以下命令读取整数scanf
用户输入"four"
相反,然后匹配失败发生并且从输入流中提取字符停止,第一个无效字符将所有违规字符留在输入流中unread。 (只是等着再次咬你)。
正确使用scanf
scanf
如果使用正确的话,是可以使用的。这意味着you负责检查return of scanf
每次。你必须处理三个条件
-
(return == EOF)
the user canceled input by generating a manual EOF
by pressing Ctrl+d (or on windows Ctrl+z);
-
(return < expected No. of conversions)
a matching or input发生故障。为一个matching如果失败,您必须考虑输入缓冲区中剩余的每个字符。 (在输入缓冲区中向前扫描,读取并丢弃字符,直到'\n'
or EOF
被发现);最后
-
(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
期望相应转换的参数是pointer到适当的类型。在:
printf("\nEnter zipcode: ");
scanf("%ld", person[i].zip_code); /* I get a bogus number here */
你未能提供一个指针,提供一个long int
值代替。自从person[i].zip_code
是类型long int
为了提供pointer for scanf
要填写,您必须使用地址运算符,例如&person[i].zip_code
告诉scanf
用它提供转换的值填充哪个地址。
等待?为什么我不必用数组来做到这一点?访问时,数组将转换为指向第一个元素的指针。因此,对于字符串输入,如果使用数组来保存字符串,它会自动转换为指针C11 标准 - 6.3.2.1 其他操作数 - 左值、数组和函数指示符(p3) http://port70.net/~nsz/c/c11/n1570.html#6.3.2.1p3.
toupper 对字符而不是字符串进行操作
printf("%s\n", toupper(person[i].state)); /* This is where the error code is occurring */
正如我的评论中所讨论的,toupper
采取类型int
作为参数,而不是类型char*
。要将字符串转换为大写/小写,您需要循环遍历每个字符并单独转换每个字符。但是,就您而言.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]);
safe_gets() 中的基本问题
这解决了大多数明显的问题,但是safer_gets()
函数本身有几个基本问题。具体来说,它无法处理EOF
当返回时getchar()
并且它无法向用户提供任何指示,因为请求的用户输入是否成功或失败,因为没有返回任何类型void
。在您编写的任何函数中,如果函数内存在任何失败的可能性,您必须提供有意义的回报向调用函数指示函数请求的操作是成功还是失败。
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).
你也应该清空stdin
除以下情况外的所有情况下输入的所有字符EOF
。这可以确保在您拨打电话后没有未读的字符safer_gets()
如果您稍后调用另一个输入函数,这可能会给您带来麻烦。进行这些更改,您可以编写您的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 '\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;
}
(note:上面的测试nchar + 1 < max_chars
确保字符保留终止字符,并且只是更安全的重新排列nchar < max_chars - 1
)
输入验证的一般方法
现在,您有一个可以使用的输入函数,该函数指示输入的成功/失败,允许您在调用函数中验证输入(main()
这里)。以阅读.full_name
会员使用safer_gets()
。你不能只是盲目地打电话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;
}
(note:的回归safer_gets()
被捕获在变量中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);
}
(note:通过使用#define
要创建所需的常量,如果您需要调整数字,您可以在一个位置进行更改,并且您不必选择每个变量声明和循环限制来尝试进行更改)
使用/输出示例
当你写完任何输入例程后——去尝试打破它吧!找到失败的极端情况并修复它们。继续尝试通过故意输入不正确/无效的输入来打破它,直到它不再排除除用户需要输入的内容之外的任何内容。练习您的输入例程,例如
$ ./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.
在第一次提示时:
$ ./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)
您明白了...最重要的是,在程序中使用输入之前,您必须验证每个用户输入并知道它是有效的。您无法验证任何函数的任何输入,除非您检查退货。如果除此之外你什么也没带走,那么学习就是值得的。
检查一下,如果您还有其他问题,请告诉我。 (并询问你的教授如何safer_gets()
把手EOF
以及您应该如何验证该功能是成功还是失败)