关于百元赌注的报价是准确的。将scanf 与getchar 混合几乎总是一个坏主意。它几乎总是会导致麻烦。但这并不是说它们不能一起使用。可以一起使用它们——但通常,这太难了。有太多繁琐的小细节和“陷阱!”需要跟踪。麻烦多于其价值。
一开始你说过
scanf() with ... %d ... 对我来说,这似乎是从键盘输入十六进制数字的唯一方法
那里有些混乱,因为%d当然是用于十进制输入。但是,由于在您纠正该问题时我已经写了这个答案,所以我们现在继续使用小数。
(目前我也忽略了错误检查——也就是说,如果用户没有输入请求的数字,这些代码片段不会检查或做任何优雅的事情。)无论如何,这里有几种阅读方法整数:
scanf("%d", &integer_variable);
没错,这是(表面上)最简单的方法。
char buf[100];
fgets(buf, sizeof(buf), stdin);
integer_variable = atoi(buf);
我认为这是不使用scanf 的最简单方法。但是现在大多数人都不喜欢使用atoi,因为它没有做太多有用的错误检查。
char buf[100];
fgets(buf, sizeof(buf), stdin);
integer_variable = strtol(buf, NULL, 10);
这与以前几乎相同,但避免使用atoi,而使用首选的strtol。
char buf[100];
fgets(buf, sizeof(buf), stdin);
sscanf(buf, "%d", &integer_variable);
这会读取一行,然后使用 sscanf 对其进行解析,这是另一种流行且通用的技术。
所有这些都会起作用;所有这些都将处理负数。不过,考虑错误条件很重要——我稍后会对此进行更多说明。
如果要输入十六进制数,技巧类似:
scanf("%x", &integer_variable);
char buf[100];
fgets(buf, sizeof(buf), stdin);
integer_variable = strtol(buf, NULL, 16);
char buf[100];
fgets(buf, sizeof(buf), stdin);
sscanf(buf, "%x", &integer_variable);
这些也都应该有效。不过,我不一定期望他们处理“负十六进制”,因为这是一个不寻常的要求。大多数时候,无符号整数使用十六进制表示法。 (实际上,严格来说,%x 与scanf 和sscanf 必须与已声明为unsigned int 的integer_variable 一起使用,而不是普通的int。)
有时“手工”做这类事情是有用或必要的。这是一个读取两个十六进制数字的代码片段。我将从使用getchar 的版本开始:
int c1 = getchar();
if(c1 != EOF && isascii(c1) && isxdigit(c1)) {
int c2 = getchar();
if(c2 != EOF && isascii(c2) && isxdigit(c2)) {
if(isdigit(c1)) integer_variable = c1 - '0';
else if(isupper(c1)) integer_variable = 10 + c1 - 'A';
else if(islower(c1)) integer_variable = 10 + c1 - 'a';
integer_variable = integer_variable * 16;
if(isdigit(c2)) integer_variable += c2 - '0';
else if(isupper(c2)) integer_variable += 10 + c2 - 'A';
else if(islower(c2)) integer_variable += 10 + c1 - 'a';
}
}
如您所见,这有点令人震惊。我,虽然我几乎从不使用scanf 家族的成员,但这是我有时会做的一个地方,正是因为“手工”做的工作量很大。您可以通过使用辅助函数或宏进行数字转换来大大简化它:
int c1 = getchar();
if(c1 != EOF && isascii(c1) && isxdigit(c1)) {
int c2 = getchar();
if(c2 != EOF && isascii(c2) && isxdigit(c2)) {
integer_variable = Xctod(c1);
integer_variable = integer_variable * 16;
integer_variable += Xctod(c2);
}
}
或者您可以将这些内部表达式折叠成只是
integer_variable = 16 * Xctod(c1) + Xctod(c2);
这些工作在辅助功能方面:
int Xctod(int c)
{
if(!isascii(c)) return 0;
else if(isdigit(c)) return c - '0';
else if(isupper(c)) return 10 + c - 'A';
else if(islower(c)) return 10 + c - 'a';
else return 0;
}
或者也许是一个宏(尽管这绝对是一种老派的东西):
#define Xctod(c) (isdigit(c) ? (c) - '0' : (c) - (isupper(c) ? 'A' : 'a') + 10)
我通常不是使用getchar() 解析来自stdin 的十六进制数字,而是来自一个字符串。我经常使用字符指针 (char *p) 来遍历字符串,这意味着我最终得到的代码更像这样:
char c1 = *p++;
if(isascii(c1) && isxdigit(c1)) {
char c2 = *p++;
if(isascii(c2) && isxdigit(c2))
integer_variable = 16 * Xctod(c1) + Xctod(c2);
}
忽略临时变量和错误检查并将其进一步简化是很诱人的:
integer_variable = 16 * Xctod(*p++) + Xctod(*p++);
但不要这样做!除了缺少错误检查之外,这个表达式可能是undefined,而且它肯定不会总是按照你的意愿行事,因为除了你读取字符的顺序之外,不再有任何保证。如果你 知道 p 指向两个十六进制数字中的第一个,你不想再折叠它了
integer_variable = Xctod(*p++);
integer_variable = 16 * integer_variable + Xctod(*p++);
即使那样,这也仅适用于 Xctod 的函数版本,而不是宏,因为宏会多次评估其参数。
最后,我们来谈谈错误处理。有很多可能性需要担心:
- 用户在没有输入任何内容的情况下点击 Return。
- 用户在数字之前或之后键入空格。
- 用户在数字后面输入了额外的垃圾。
- 用户键入非数字输入而不是数字。
- 代码到达文件尾;根本没有字符可供阅读。
然后您如何处理这些取决于您使用的输入技术。以下是基本规则:
A.如果您正在调用scanf、fscanf 或sscanf,总是检查返回值。如果它不是 1(或者,在您有多个 % 说明符的情况下,它不是您希望读取的值的数量),这意味着出现了问题。这通常会捕获问题 4 和 5,并且会优雅地处理案例 2。但它通常会悄悄地忽略问题 1 和 3。(特别是,scanf 和 fscanf 将额外的 \n 视为前导空格。)
B.如果您再次调用fgets,请始终检查返回值。您将在 EOF 上获得 NULL(问题 5)。处理其他问题取决于您对所阅读的行做了什么。
C.如果你调用atoi,它会优雅地处理问题2,但它会忽略问题3,它会悄悄地将问题4变成数字0(这就是为什么atoi通常不再推荐的原因)。
D.如果您正在调用strtol 或任何其他“strto”函数,它们将优雅地处理问题 2,如果您让它们返回“结束指针”,您可以检查并处理问题 3 和4.(请注意,我在上面的两个strtol 示例中省略了结束指针处理。)
E.最后,如果你正在做一些像我的“hardway”两位数十六进制转换器这样的肮脏的事情,你通常必须明确地自己解决所有这些问题。如果你想跳过前导空格,你必须这样做(<ctype.h> 中的isspace 函数可以提供帮助),如果可能有意外的非数字字符,你也必须检查这些字符。 (这就是对isascii 和isxdigit 的调用在我的“hardway”两位数十六进制转换器中所做的事情。)