让我们一一来看问题:
换行符在编号或人读后保留在标准输入中
printf("\n\nEnter name: ");
safer_gets(person[i].full_name, 35); /* This is the step being skipped */
它被跳过是因为您safer_gets()
只读取第一个'\n'
(换行符- 不是回车符,即'\r'
)。但是,saver_gets()
在输入流中看到的第一个字符是在您调用 in 后'\n'
仍处于未读状态的字符:stdin
scanf
printf("How many people do you want to generate labels for (0-10)? ");
scanf("%i", &x);
用于数字转换的所有scanf
格式说明符仅通过构成数字的最后一位(或小数点)读取,留下'\n'
由用户Enter在输入流中按下未读生成的数字(stdin
此处)。这是鼓励新的 C 程序员使用诸如(或 POSIX )之类的面向行的输入函数读取用户输入,然后用于解析填充缓冲区中的值的主要原因之一。fgets()
getline()
sscanf
为什么用户输入首选面向行的输入函数
通过使用具有足够缓冲区的面向行的输入功能,消耗了用户输入的完整行(包括'\n'
来自用户的按Enter)。这可确保stdin
为下一个输入做好准备,并且不会有前一个输入留下的未读字符等待咬你。
正确使用所有输入功能
如果您从这个答案中没有得到任何其他信息,请学习这一点——除非您检查 return,否则您无法正确使用任何输入函数。scanf
对于函数族尤其如此。为什么?如果您尝试读取一个整数,scanf
而用户输入"four"
了,则会发生匹配失败,并且从您的输入流中提取字符会停止,第一个无效字符会在您的输入流中留下所有违规字符unread。(只是等着再咬你)。
正确使用 scanf
scanf
可以使用,如果使用正确。这意味着您有责任检查每次退货。你必须处理三个条件scanf
(return == EOF)
EOF
用户通过按下Ctrl+d(或在 windows 上)生成手册来取消输入Ctrl+z;
(return < expected No. of conversions)
发生匹配或输入失败。对于匹配失败,您必须考虑输入缓冲区中剩余的每个字符。(在输入缓冲区中向前扫描读取并丢弃字符,直到找到一个'\n'
或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
期望相应转换的参数是指向适当类型的指针。在:
printf("\nEnter zipcode: ");
scanf("%ld", person[i].zip_code); /* I get a bogus number here */
您没有提供指针,long int
而是提供了一个值。因为person[i].zip_code
is 类型long int
为了提供一个要填充的指针,scanf
所以您必须使用address-of运算符,例如&person[i].zip_code
,告诉scanf
使用它提供转换的值来填充哪个地址。
等待?为什么我不必用数组来做呢?在访问时,数组被转换为指向第一个元素的指针。因此对于字符串输入,如果使用数组来保存字符串,它会自动转换为指针C11 标准 - 6.3.2.1 其他操作数 - 左值、数组和函数指示符(p3)。
toupper 作用于字符而不是字符串
printf("%s\n", toupper(person[i].state)); /* This is where the error code is occurring */
正如我在评论中所讨论的,toupper
将 typeint
作为参数,而不是 type 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()
并且由于没有返回 type ,因此无法向用户提供所请求的用户输入是成功还是失败的任何指示void
。在你编写的任何函数中,如果函数内有任何失败的可能性,你必须向调用函数提供一个有意义的返回,以指示函数的请求操作是成功还是失败。
你能做什么safer_gets()
?为什么不返回一个简单的int
值,提供成功时读取的字符数,或失败时-1
(的正常值EOF
)。您现在可以获得双重奖励,即能够验证输入是否成功——您还可以获得字符串中的字符数(仅限于2147483647
字符)。Ctrl+d您现在还可以通过在 Linux 或Ctrl+z(windows)上生成手动 EOF 来处理用户取消输入的问题。
您还应该清空stdin
在除 之外的所有情况下输入的所有字符EOF
。这样可以确保在您调用后没有未读的字符,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 终止字符,并且只是一个更安全的重新排列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;
}
(注意:返回的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);
}
(注意:通过使用#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.
Ctrl+d尊重用户希望在 Linux 或(Windows)上生成手动 EOF 时随时取消输入Ctrl+z,您应该能够从代码中的任何位置处理该问题。
在第一个提示:
$ ./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
以及您应该如何验证功能是成功还是失败)