您应该只对宏的参数进行字符串化,并且它们必须在宏替换文本中的字符串或字符常量之外。因此,您可能应该使用:
#define READIN(a, b) do { if (scanf("%" #a, &b) != 1) \
{ fprintf(stderr, "ERROR\n"); return EXIT_FAILURE; } \
} while (0)
int main(void)
{
unsigned int stack_size;
printf("Type in size: ");
READIN(u, stack_size);
printf("You entered %u\n", stack_size);
return(0);
}
有很多变化。 do { ... } while (0) 习惯用法可防止您在以下情况下出现编译错误:
if (i > 10)
READIN(u, j);
else
READIN(u, k);
使用您的宏,您会收到 unexpected keyword 'else' 类型的消息,因为第一个 READIN() 后的分号将是嵌入 if 后的空语句,因此 else 不属于可见的if 或宏内的if。
stack_size 的类型是unsigned int;因此,正确的格式说明符是 u(d 用于签名的 int)。
而且,最重要的是,宏中的参数 a 被正确字符串化(并且相邻字符串文字的字符串连接 - C89 的一个非常有用的功能! - 为您处理其余的事情。参数 b在宏中也没有嵌入到字符串中。
错误报告是向stderr(报告错误的标准流)完成的,消息以换行符结尾,因此它会实际出现。我没有用exit(EXIT_FAILURE); 替换return EXIT_FAILURE;,但如果宏将在main() 之外使用,这可能是一个明智的选择。这首先假设“出错时终止”是适当的行为。它通常不适用于交互式程序,但修复它有点困难。
我也完全无视我对使用scanf() 的保留意见;我通常避免这样做,因为我发现错误恢复太难了。我只用 C 语言编程了大约 28 年,我仍然觉得 scanf() 太难控制,所以我基本上从不使用它。我通常使用fgets() 和sscanf() 代替。除其他优点外,我可以报告导致问题的字符串;当scanf() 可能已经吞噬了其中的一部分时,这很难做到。
我对@987654346@ 的想法是,只读取正数而不读取字母。我的整体代码确实创建了一个堆栈,用户输入该堆栈并且类型应该只是正数,否则会出错。 [...] 我只想知道是否有更好的解决方案来禁止用户输入正数以外的内容?
我刚刚尝试了上面的代码(添加了#include <stdlib.h> 和#include <stdio.h>)并输入了-2 并被告知4294967294,这不是我想要的(%u 格式不会拒绝-2,至少在 MacOS X 10.7.2 上)。所以,我很可能会选择fgets() 和strtoul()。但是,准确检测 strtoul() 的所有可能问题是一项微妙的练习。
这是我想出的替代代码:
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <limits.h>
#include <string.h>
int main(void)
{
unsigned int stack_size = 0;
char buffer[4096];
printf("Type in size: ");
if (fgets(buffer, sizeof(buffer), stdin) == 0)
printf("EOF or error detected\n");
else
{
char *eos;
unsigned long u;
size_t len = strlen(buffer);
if (len > 0)
buffer[len - 1] = '\0'; // Zap newline (assuming there is one)
errno = 0;
u = strtoul(buffer, &eos, 10);
if (eos == buffer ||
(u == 0 && errno != 0) ||
(u == ULONG_MAX && errno != 0) ||
(u > UINT_MAX))
{
printf("Oops: one of many problems occurred converting <<%s>> to unsigned integer\n", buffer);
}
else
stack_size = u;
printf("You entered %u\n", stack_size);
}
return(0);
}
strtoul() 的规范在 ISO/IEC 9899:1999 §7.20.1.4 中给出:
¶1 [...]
unsigned long int strtoul(const char * restrict nptr,
char ** restrict endptr, int base);
[...]
¶2 [...] 首先,
他们将输入字符串分解为三个部分:一个初始的,可能是空的,序列
空白字符(由isspace 函数指定),主题序列
类似于一个整数,以由 base 的值确定的某个基数表示,以及
一个或多个无法识别的字符的最终字符串,包括终止的 null
输入字符串的字符。然后,他们尝试将主题序列转换为
整数,并返回结果。
¶3 [...]
¶4 主题序列定义为输入字符串的最长初始子序列,
从第一个非空白字符开始,即预期的形式。主题
如果输入字符串为空或完全由白色组成,则序列不包含字符
空格,或者如果第一个非空白字符不是符号或允许的字母
或数字。
¶5 如果主题序列具有预期形式并且 base 的值为零,则该序列
以第一个数字开头的字符被解释为一个整数常量,根据
6.4.4.1 的规则。如果主题序列具有预期的形式和碱基的值
介于 2 到 36 之间,用作转换的基础,为每个字母赋予其值
如上所述。如果主题序列以减号开始,则产生的值来自
转换被否定(在返回类型中)。指向最终字符串的指针存储在
endptr 指向的对象,前提是 endptr 不是空指针。
¶6 [...]
¶7 如果主题序列为空或不具有预期形式,则不进行转换
执行; nptr 的值存储在endptr 指向的对象中,前提是
endptr 不是空指针。
返回
¶8 strtol、strtoll、strtoul 和 strtoull 函数返回转换后的
价值,如果有的话。如果无法执行转换,则返回零。如果正确的值
超出可表示值的范围,LONG_MIN、LONG_MAX、LLONG_MIN,
返回 LLONG_MAX、ULONG_MAX 或 ULLONG_MAX(根据返回类型
和值的符号,如果有的话),宏 ERANGE 的值存储在 errno 中。
我得到的错误来自 64 位编译,其中 -2 被转换为 64 位无符号长整数,并且超出了 32 位 unsigned int 可接受的范围(失败条件是 @987654368 @)。当我在 32 位模式下重新编译时(所以 sizeof(unsigned int) == sizeof(unsigned long)),然后值 -2 再次被接受,再次解释为 4294967294。所以,即使这还不够精细......您可能必须手动跳过前导空白并拒绝负号(也可能是正号;您还需要#include <ctype.h>):
char *bos = buffer;
while (isspace(*bos))
bos++;
if (!isdigit(*bos))
...error - not a digit...
char *eos;
unsigned long u;
size_t len = strlen(bos);
if (len > 0)
bos[len - 1] = '\0'; // Zap newline (assuming there is one)
errno = 0;
u = strtoul(bos, &eos, 10);
if (eos == bos ||
(u == 0 && errno != 0) ||
(u == ULONG_MAX && errno != 0) ||
(u > UINT_MAX))
{
printf("Oops: one of many problems occurred converting <<%s>> to unsigned integer\n", buffer);
}
正如我所说,整个过程相当重要。
(再看一遍,我不确定u == 0 && errno != 0 子句是否会捕捉到任何错误......也许不是因为eos == buffer(或eos == bos)条件捕捉到了没有什么的情况完全转换。)