【问题标题】:What is the purpose of the h and hh modifiers for printf?printf 的 h 和 hh 修饰符的目的是什么?
【发布时间】:2011-06-02 23:38:34
【问题描述】:

除了%hn%hhnhhh 指定指向 对象的大小)之外,hhh printf 格式说明符的修饰符?

由于标准要求将默认提升应用于可变参数函数,因此无法将charshort(或其任何有符号/无符号变体)类型的参数传递给printf

根据 7.19.6.1(7),h 修饰符:

指定后面的 d、i、o、u、x 或 X 转换说明符适用于 short int 或 unsigned short int 参数(参数将 已按整数提升,但其值应 在打印前转换为 short int 或 unsigned short int); 或者后面的 n 转换说明符适用于指向 short 的指针 int 参数。

如果参数实际上是 shortunsigned short 类型,则提升到 int 然后转换回 shortunsigned short 将产生与升级到int 没有任何转换回来。因此,对于shortunsigned short 类型的参数,%d%u 等应该给出与%hd%hu 等相同的结果(同样对于char 类型和@987654345 @)。

据我所知,hhh 修饰符可能有用的唯一情况是,当参数传递给它的int 超出shortunsigned short 的范围时,例如

printf("%hu", 0x10000);

但我的理解是,像这样传递错误的类型无论如何都会导致未定义的行为,所以你不能指望它打印 0。

我见过的一个真实案例是这样的代码:

char c = 0xf0;
printf("%hhx", c);

作者希望它打印f0,尽管实现具有已签名的普通char 类型(在这种情况下,printf("%x", c) 将打印fffffff0 或类似的)。但这种期望是否合理?

(注意:原来的类型是char,它被提升为int并转换回unsigned char而不是char,从而改变了打印的值。但是标准指定了这种行为,还是损坏的软件可能依赖的实现细节?)

【问题讨论】:

    标签: c printf variadic-functions promotions format-specifiers


    【解决方案1】:

    一个可能的原因:在格式化输入函数中使用这些修饰符是为了对称?我知道这不是绝对必要的,但也许看到了它的价值?

    虽然他们在the C99 Rationale document 中没有提到“h”和“hh”修饰符的对称性的重要性,但委员会确实提到了它作为考虑为什么 @987654322 支持“%p”转换说明符@(尽管这对于 C99 来说并不新鲜——“%p”支持在 C90 中):

    在 C89 中添加了使用 %p 的输入指针转换,尽管为了与 fprintf 对称,这显然是有风险的。

    fprintf() 部分中,C99 基本原理文档确实讨论了添加了“hh”,但只是将读者引至fscanf() 部分:

    在 C99 中添加了 %hh 和 %ll 长度修饰符(参见 §7.19.6.2)。

    我知道这是一个脆弱的线索,但我还是在猜测,所以我想我会给出任何可能的论点。

    另外,为了完整起见,“h”修饰符在最初的 C89 标准中 - 即使由于广泛的现有使用,它不是绝对必要的,即使可能没有技术要求使用修饰符。

    【讨论】:

    • 您是否同意我的初步评估,即一致的实现可以忽略 hhh 修饰符?
    • 我不确定 - 我不确定这会导致未定义的行为:printf("%hu", (unsigned int) 0x10000);。我可以想象两种方式的参数 - 我更喜欢它是明确定义的,但可以看到措辞“指定后面的 d、i、o、u、x 或 X 转换说明符适用于短整数或无符号short int 参数”将其抛出到未定义的区域,尽管紧随其后的“(参数将根据整数提升进行提升,但其值应在打印前转换为 short int 或 unsigned short int)”将其抛出。
    • 基于该文本,我认为使用优化代码“转换为短整数或无符号短整数”的实现是合理的,该优化代码假定它正在转换的值确实是提升的结果标准说是。可以想象,所述优化代码可以用超出范围的值做一些无意义的事情,因此实现至少有一个合理的主张,即它应该是未定义的行为,并且代码具有违反了标准的要求。
    • @R..:我认为没有什么可以禁止实现忽略它们。然而,即使它们什么都不做,将它们包含在规范中也意味着执行printf("%hx",1u); 的程序将具有定义的行为;相比之下,如果没有指定“h”是合法修饰符的文本,这样的程序将是 UB,不是吗?
    【解决方案2】:

    %...x 模式下,所有值都被解释为无符号。因此,负数被打印为它们的无符号转换。在大多数处理器使用的 2 的补码算术中,有符号负数与其正无符号等价物之间的位模式没有区别,后者由模算术定义(将字段的最大值加一到负数,根据符合 C99 标准)。许多软件(尤其是最有可能使用%x 的调试代码)都默默地假设有符号负值的位表示及其无符号强制转换是相同的,这仅在 2 的补码机器上是正确的。

    这种转换的机制使得值的十六进制表示总是暗示(可能不准确)一个数字已经以 2 的补码呈现,只要它没有达到不同整数表示具有不同的边缘条件范围。这甚至适用于值 0 不是用全 0 的二进制模式表示的算术表示。

    因此,在任何机器上,以十六进制显示为 unsigned long 的负数 short 将用 f 填充,这是由于促销中的隐含符号扩展,printf 将打印出来。 是相同的,但它确实在视觉上误导了字段的大小,暗示着大量根本不存在的范围。

    %hx 截断显示的表示以避免这种填充,正如您从实际用例中得出的结论。

    printf 的行为在传递一个超出short 范围的int 时是未定义的,应该打印为short,但迄今为止最简单的实现只是通过原始向下转换丢弃高位,因此,虽然规范不要求任何特定行为,但几乎任何理智的实现都只会执行截断。不过,通常有更好的方法来做到这一点。

    如果 printf 不填充值或显示带符号值的无符号表示,%h 不是很有用。

    【讨论】:

    • 您从哪里得到关于以位形式打印负数的信息?据我所知,为任何无符号格式说明符(%x%u%o)传递负值会导致未定义的行为。此外,据我所知,符合规范的实现可以简单地忽略任何hhh 修饰符的存在,%n 除外。
    • (unsigned)(signed) 之间的转换,在相同的宽度内保证不会对数据的位模式进行实际更改,只是对该位模式的解释。 (改变宽度的强制转换是零扩展或符号扩展。)%x 被定义为处理无符号值,因此它们首先从有符号强制转换为无符号,这不会更改任何数据,但会更改解释-效果,使用带有负数的%x 会显示它的位模式。而%x是整数类型,而h修饰符作用于整数类型,所以我认为是支持的。
    • 您的信息明显不正确。 C 根据而不是位模式定义转换(隐式或强制转换)。标准以与模运算等效的方式定义了到无符号类型的转换。转换为有符号类型是实现定义的,除非值适合目标类型而无需修改。
    • 我的问题是关于 C 语言,而不是关于任何实现。而“通过添加UINT_MAX”是错误的。您忘记了 +1 以及其他详细信息。一旦你修复它,它就等同于模运算。
    • 你说得对,我放弃了 +1;当我将更新汇总到答案时,我会修复它。无论如何,我认为我们已经非常清楚地发现%hx实际 使用仅限于非法使用printf 时(将有符号参数表示为无符号,这通常被认为是是安全的,但只在 2 的补码机器上是安全的;结果是非 2 机器上的公共库中有很多损坏的代码),这使得它本质上是特定于实现的。合理地说,当转换已经发生在非常狭窄的合法范围内时,它没有特别的用途。
    【解决方案3】:

    我能想到的唯一用途是传递unsigned shortunsigned char 并使用%x 转换说明符。您不能简单地使用裸 %x - 该值可能会提升为 int 而不是 unsigned int,然后您就会有未定义的行为。

    您的选择是将参数显式转换为unsigned;或将%hx / %hhx 与裸参数一起使用。

    【讨论】:

    • 如果unsigned shortunsigned char 被提升为int,它仍然是正数,所以C 需要表示匹配unsigned 的表示。据我所知,符号不匹配在可变参数函数参数和没有原型的函数的参数中是有效的,只要该值作为有符号值是正数。当然,%x 旨在与 int 参数一起使用,只要它们是肯定的......
    • @R.:对于一般的可变参数函数,你是对的 - 但对于 printf 系列的特定情况,标准将 unsigned int 作为参数的类型提供给 %x ,然后说 “如果任何参数不是相应转换规范的正确类型,则行为未定义。” - 我不相信这允许您传递int。跨度>
    • 有趣。我怀疑这是无意的。也许我应该看一下标准,看看是否有像printf("%x", 1); 这样的例子(根据你的推理,它需要是1U 而不是1)。
    • 不是UB,只要值在intsigned int两者的范围内,因为这个值可以互换使用。他们特别提到了函数调用。请参阅 C99 标准中的脚注 31 或 C11 中的脚注 41,在 6.2.5 类型中。
    • @12431234123412341234123:这就是之前的cmets讨论的内容。这对于一般的可变参数函数调用是正确的,但对于 printf 函数的特定情况,有特定的覆盖语言(在 C11 7.21.6.1 p9 中)。可以肯定的是,这是一个相当迂腐的观点,正如 R. 所说,上面可能不是故意的。
    【解决方案4】:

    printf() 等的可变参数使用默认转换自动提升,因此任何 shortchar 值在传递给函数时都会提升为 int

    在没有 hhh 修饰符的情况下,您必须屏蔽传递的值才能可靠地获得正确的行为。使用修饰符,您不再需要屏蔽值; printf() 实现正确地完成了这项工作。

    具体来说,对于%hx的格式,printf()里面的代码可以做这样的事情:

    va_list args;
    va_start(args, format);
    
    ...
    
    int i = va_arg(args, int);
    unsigned short s = (unsigned short)i;
    ...print s correctly, as 4 hex digits maximum
    ...even on a machine with 64-bit `int`!
    

    我很高兴地假设 short 是一个 16 位的数量;当然,该标准实际上并不能保证这一点。

    【讨论】:

    • 我的问题的重点是,除非您以似乎会导致未定义行为的方式传递错误的类型,否则屏蔽/转换将是无操作的(就价值而言) .
    【解决方案5】:

    我发现在将无符号字符格式化为十六进制时避免强制转换很有用:

            sprintf_s(tmpBuf, 3, "%2.2hhx", *(CEKey + i));
    

    这是一个小的编码方便,看起来比多重转换 (IMO) 更干净。

    【讨论】:

    • 这个答案中CEkey 的类型是什么?如果不是 unsigned char * ,则行为未定义;或者如果是,hh 是多余的。
    【解决方案6】:

    另一个方便的地方是 snprintf 大小检查。 gcc7 在使用 snprintf 时添加了大小检查 所以这会失败

    char arr[4];
    char x='r';
    snprintf(arr,sizeof(arr),"%d",r);
    

    所以在格式化字符时使用 %d 会强制你使用更大的字符

    这是一个提交,显示了这些修复,而不是增加他们将 %d 更改为 %h 的 char 数组大小。这也给出了更准确的描述

    https://github.com/Mellanox/libvma/commit/b5cb1e34a04b40427d195b14763e462a0a705d23#diff-6258d0a11a435aa372068037fe161d24

    【讨论】:

    • 有趣。这看起来像是 gcc 错误思想的解决方法。对于-Wformat-overflow 警告的级别 1,gcc 记录了它认为“已知绑定到其类型的子范围的数字参数”,这对于提升的字符总是如此。但是第 2 级并没有描述这种行为......? gcc.gnu.org/onlinedocs/gcc/Warning-Options.html
    • 我使用 libvma,我们将这个提交推送到使用 gcc7 编译。我不确定我们使用的溢出级别(我猜是默认级别)
    【解决方案7】:

    我同意你的观点,这不是绝对必要的,因此仅凭这个原因在 C 库函数中是没有好处的 :)

    不同标志的对称性可能“很好”,但它大多适得其反,因为它隐藏了“转换为int”规则。

    【讨论】:

      猜你喜欢
      • 2021-09-13
      • 1970-01-01
      • 2014-09-14
      • 2014-03-13
      • 1970-01-01
      • 2012-05-05
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多