C中的宏指令,我的代码示例不起作用
我想获得以下代码片段:
#define READIN(a, b) if(scanf('"#%d"', '"&a"') != 1) { printf("ERROR"); return EXIT_FAILURE; } int main(void) { unsigned int stack_size; printf("Type in size: "); READIN(d, stack_size); }
我不明白,如何使用#
运算符指令。 我想多次使用带有打印错误等的scanf
,但是"'"#%d"'
和'"&a"'"
我认为是完全错误的。 有没有办法让它运行? 我认为宏是最好的解决方案吗?
您应该只将参数字符串化为宏,并且它们必须在宏的替换文本中的字符串或字符常量之外。 因此你可能应该使用:
#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
,宏中的参数a
被正确地字符串化(并且相邻字符串文字的字符串连接 – C89的一个非常有用的特性! – 为您处理其余部分。并且宏中的参数b
未嵌入到也是一个字符串。
错误报告是针对stderr
(用于报告错误的标准流)完成的,并且消息以换行符结束,因此它将实际显示。 我没有替换return EXIT_FAILURE;
exit(EXIT_FAILURE);
,但如果宏将在main()
之外使用,那么这可能是一个明智的选择。 这假设’终止错误’是首先适当的行为。 它通常不适用于交互式程序,但修复它有点困难。
我也忽略了我对使用scanf()
保留; 我通常会避免这样做,因为我发现错误恢复太难了。 我只用C编程了大约28年,我仍然发现scanf()
太难控制了,所以我基本上从不使用它。 我通常使用fgets()
和sscanf()
代替。 在其他优点中,我可以报告造成麻烦的字符串; 当scanf()
可能已经吞噬了一些时,这很难做到。
我对
scanf()
想法是,只读正数而不是字母。 我的整体代码确实创建了一个用户输入的堆栈,类型应该只是正数,否则就是错误。 […]我只是想知道是否有更好的解决方案禁止用户输入正数以外的其他内容?
我刚刚尝试了上面的代码(添加了#include
和#include
)并输入-2
并被告知4294967294,这不是我想要的( %u
格式不拒绝-2
,至少在MacOS X 10.7.2上)。 所以,我最有可能选择fgets()
和strtoul()
。 然而,准确地检测strtoul()
所有可能的问题是一些精致的运动。
这是我提出的替代代码:
#include #include #include #include #include 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
函数指定),一个类似于某个基数所表示的整数的主题序列by base的值,以及一个或多个无法识别的字符的最终字符串,包括输入字符串的终止空字符。 然后,他们尝试将主题序列转换为整数,并返回结果。¶3[…]
¶4主题序列被定义为输入字符串的最长初始子序列,从第一个非空白字符开始,即预期forms。 如果输入字符串为空或完全由空格组成,或者第一个非空白字符不是符号或允许的字母或数字,则主题序列不包含任何字符。
¶5如果主题序列具有预期forms且base的值为零,则根据6.4.4.1的规则将以第一个数字开头的字符序列解释为整数常量。 如果主题序列具有预期forms并且base的值在2和36之间,则将其用作转换的基础,将其值归于每个字母,如上所述。 如果主题序列以减号开头,则转换产生的值将被否定(在返回类型中)。 如果endptr不是空指针,则指向最终字符串的指针存储在endptr指向的对象中。
¶6[…]
¶7如果主题序列为空或者没有预期的forms,则不进行转换; 如果
endptr
不是空指针,则nptr
的值存储在endptr
指向的对象中。返回
¶8strtol,
strtoll
,strtoul
和strtoull
函数返回转换后的值(如果有的话)。 如果无法执行转换,则返回零。 如果正确的值超出可表示值的范围,则返回LONG_MIN,LONG_MAX,LLONG_MIN,LLONG_MAX,ULONG_MAX或ULLONG_MAX(根据值的返回类型和符号,如果有),并且宏ERANGE的值为存储在errno中。
我得到的错误来自64位编译,其中-2
被转换为64位无符号长unsigned int
,并且超出了32位unsigned int
可接受的范围(失败条件是u > UINT_MAX
)。 当我以32位模式重新编译时(所以sizeof(unsigned int) == sizeof(unsigned long)
),再次接受值-2
,再次解释为4294967294。 所以,即使这还不够精细……你可能不得不手动跳过前导空格并拒绝负号(也许是一个正号;你也需要#include
) :
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
)条件捕获了这个案例没有什么可以转换的。 )
您错误地包含了您的宏参数,它应该如下所示:
#define READIN(a, b) if(scanf("%"#a, &b) != 1) { printf("ERROR"); return EXIT_FAILURE; }
你使用stringify运算符也是不正确的,它必须直接在参数名前加前缀。
简而言之,使用"%"#a
,而不是'"#%d"'
和&b
,而不是'"&a"'
。
作为旁注,对于像这样的longish宏,它有助于使它们多行使用\
,这使它们可读:
#define READIN(a, b) \ if(scanf("%"#a, &b) != 1) \ { \ printf("ERROR"); \ return EXIT_FAILURE; \ }
当做这样的事情时,最好使用一个函数,这应该是有效的:
inline int readIn(char* szFormat, void* pDst) { if(scanf(szFormat,pDst) != 1) { puts("Error"); return 0; } return 1; }
调用它就像这样:
if(!readIn("%d",&stack_size)) return EXIT_FAILURE;
scanf(3)
将const char *
作为第一个参数。 你正在传递'"..."'
,这不是C“字符串”。 C字符串用"
双引号"
编写。 '
单引号用于单个字符 : 'a'
或'\n'
等。
将return
语句放在C预处理器宏中通常被认为是非常差的forms。 我见过goto error;
在将格式化数据存储到文件或内核接口并从中读取数据时,在重复error handling代码之前编码预处理器宏内部,但这些都是特殊情况。 你会厌恶六个月的调试。 相信我。 不要在C预处理器宏中隐藏goto
, return
, break
, continue
。 只要它完全包含在宏中,如果没有问题。
另外,请养成写这样的printf(3)
语句的习惯:
printf("%s", "ERROR");
格式化字符串漏洞非常容易编写。 您的代码现在不包含任何此类漏洞,但相信我,在将来的某些时候,这些字符串不可避免地会被修改为包含一些用户提供的内容,现在添加一个显式格式字符串将有助于防止这些。 如果你看到这个,至少你将来会考虑它。
将多行宏包装在do { } while (0)
块中被认为是礼貌的 。
最后, 字符串化没有完全正确完成; 试试这个:
#define READIN(A, B) do { if (scanf("%" #A, B) != 1) { \ /* error handling */ \ } else { \ /* success case */ \ } } while(0)
编辑 :我觉得我应该重新考虑一下阿尔法的建议 :改用一个函数。 您可以获得更好的类型检查,在出现问题时更好地回溯,并且更容易使用。 function很好。