【问题标题】:Macro directives in C, my code example doesn't workC中的宏指令,我的代码示例不起作用
【发布时间】:2011-12-31 01:23:26
【问题描述】:

我想获得以下代码片段:

#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"'"是完全错误的。有没有办法让它运行?我认为宏是不是最好的解决方案?

【问题讨论】:

  • 您能否添加一个示例,说明如果您不尝试在宏中概括它,您将如何做您想做的事情?我不太明白你想要做什么。
  • 请不要这样做:使用函数。
  • 第1步:编译器给你什么错误信息?

标签: c macros c-preprocessor


【解决方案1】:

您应该只对宏的参数进行字符串化,并且它们必须在宏替换文本中的字符串或字符常量之外。因此,您可能应该使用:

#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;因此,正确的格式说明符是 ud 用于签名的 int)。

而且,最重要的是,宏中的参数 a 被正确字符串化(并且相邻字符串文字的字符串连接 - C89 的一个非常有用的功能! - 为您处理其余的事情。参数 b在宏中也没有嵌入到字符串中。

错误报告是向stderr(报告错误的标准流)完成的,消息以换行符结尾,因此它会实际出现。我没有用exit(EXIT_FAILURE); 替换return EXIT_FAILURE;,但如果宏将在main() 之外使用,这可能是一个明智的选择。这首先假设“出错时终止”是适当的行为。它通常不适用于交互式程序,但修复它有点困难。

我也完全无视我对使用scanf() 的保留意见;我通常避免这样做,因为我发现错误恢复太难了。我只用 C 语言编程了大约 28 年,我仍然觉得 scanf() 太难控制,所以我基本上从不使用它。我通常使用fgets()sscanf() 代替。除其他优点外,我可以报告导致问题的字符串;当scanf() 可能已经吞噬了其中的一部分时,这很难做到。


我对@9​​87654346@ 的想法是,只读取正数而不读取字母。我的整体代码确实创建了一个堆栈,用户输入该堆栈并且类型应该只是正数,否则会出错。 [...] 我只想知道是否有更好的解决方案来禁止用户输入正数以外的内容?

我刚刚尝试了上面的代码(添加了#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 strtolstrtollstrtoulstrtoull 函数返回转换后的 价值,如果有的话。如果无法执行转换,则返回零。如果正确的值 超出可表示值的范围,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 &lt;ctype.h&gt;):

        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 &amp;&amp; errno != 0 子句是否会捕捉到任何错误......也许不是因为eos == buffer(或eos == bos)条件捕捉到了没有什么的情况完全转换。)

【讨论】:

  • 特别感谢最后一段。我知道出于某种原因我不喜欢scanf(3),但无法说出一个简洁的原因。
  • 好的,非常感谢,这解决了我的很多问题。我可以问一个问题。我对 scanf 的想法是,只读取正数而不读取字母。我的整体代码确实创建了一个堆栈,用户输入该堆栈并且类型应该只是正数,否则会出错。然后用户输入随机十进制,也只有正数,然后使用堆栈将其转换为二进制。我只想知道是否有更好的解决方案来禁止用户输入正数以外的内容?
【解决方案2】:

您错误地封装了宏参数,它应该如下所示:

#define READIN(a, b) if(scanf("%"#a, &b) != 1) { printf("ERROR"); return EXIT_FAILURE; }

你使用 stringify 操作符也不正确,它必须直接作为参数名称的前缀。

简而言之,使用"%"#a,而不是'"#%d"',以及&amp;b,而不是'"&amp;a"'

附带说明一下,对于像那些较长的宏,使用\ 将它们设为多行会有所帮助,这样可以保持它们的可读性:

#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;

【讨论】:

  • 在示例代码中,参数a 在调用中以d 给出,因此它应该出现在您拥有#b 的位置,而参数bstack_size 给出,所以它应该出现在您拥有 &amp;a 的位置。
  • 一般来说,使用函数比使用宏更好——我同意这一点。在这种情况下这样做的缺点是您会丢失像 GCC 这样的编译器可以对scanf() 的参数进行的错误检查。特别是,对于您的 readIn() 函数,编译器无法发现以下问题:int i; char s[20]; if (readIn("%s", &amp;i) &amp;&amp; readIn("%d", s)) { ... } 而......好吧,您不能在 if 这样的语句中使用宏,但 GCC 会与变量类型相比,诊断不正确的格式。请参阅__attribute__((format(printf,n,m))) 等。
【解决方案3】:

scanf(3)const char * 作为第一个参数。您正在传递'"..."',它不是 C“字符串”。 C 字符串用" 双引号编写。 ' 单引号用于单个字符'a''\n' 等。

在 C 预处理器宏中放置 return 语句通常被认为是非常糟糕的形式。在将格式化数据存储到文件或内核接口并从文件或内核接口读取数据时,我曾在预处理器宏中看到 goto error; 编码,但这些绝对是例外情况。你会讨厌在六个月内调试它。相信我。不要在 C 预处理器宏中隐藏 gotoreturnbreakcontinueif 可以,只要它完全包含在宏中即可。

另外,请养成这样写printf(3) 声明的习惯:

printf("%s", "ERROR");

Format string vulnerabilities 非常容易编写。您的代码现在不包含任何此类漏洞,但相信我,在未来的某个时间点,这些字符串不可避免地会被修改为包含一些用户提供的内容,并且现在放入一个明确的格式字符串 在未来帮助防止这些。如果你看到这个,至少你以后会考虑。

considered polite to wrap your multi-line macros in do { } while (0) blocks

最后,stringification 没有完全正确地完成;试试这个:

#define READIN(A, B) do { if (scanf("%" #A, B) != 1) { \
        /* error handling */ \
    } else { \
        /* success case */ \
    } } while(0)

编辑:我觉得我应该重新迭代akappa's advice:改用函数。当出现问题时,您可以获得更好的类型检查,更好的回溯,并且更容易使用。功能不错。

【讨论】:

  • 还有比printf("constant string\n"); 更严重的错误,比如在字符串中省略换行符。当用户控制格式字符串时,格式字符串漏洞就会出现。所以printf(user_input); 可能是致命的;那应该是printf("%s\n", user_input);
  • @Jonathan:我考虑过为格式字符串建议"%s\n",但这会打破提示的常见情况。当然,您仍然必须在提示中包含fflush(3) 调用,但这很容易在带有提示的函数中完成。不过,根据我的经验,"constant string\n" 经常 被一个变量替换,当它的一部分被计算出来时......只是有一个格式字符串 present 给出了格式的提示字符串需要小心处理。 :)
猜你喜欢
  • 1970-01-01
  • 2019-06-29
  • 2012-12-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多