【问题标题】:Printf function formatterPrintf 函数格式化程序
【发布时间】:2019-10-18 20:19:20
【问题描述】:

具有以下简单的 C++ 代码:

#include <stdio.h>

int main() {
    char c1 = 130;
    unsigned char c2 = 130;

    printf("1: %+u\n", c1);
    printf("2: %+u\n", c2);
    printf("3: %+d\n", c1);
    printf("4: %+d\n", c2);
    ...
    return 0;
}

输出是这样的:

1: 4294967170
2: 130
3: -126
4: +130

谁能解释一下第 1 行和第 3 行的结果?

我正在使用具有所有默认设置的 Linux gcc 编译器。

【问题讨论】:

  • 这叫溢出。
  • 你能提供一个minimal reproducible example吗? (即,添加#includes 和main?)
  • @L.F.不是按照标准......其中“溢出”是指算术运算产生超出其类型范围的结果
  • 这里解释溢出的概念; C++ integer overflowchar 用 8 位表示
  • 在我看来更像 C 代码。

标签: c++ integer printf overflow implicit-conversion


【解决方案1】:

char 是 8 位。这意味着它可以表示 2^8=256 个唯一值。 uchar 代表 0 到 255,带符号的 char 代表 -128 到 127(绝对可以代表任何值,但这是典型的平台实现)。因此,将 130 分配给 char 超出范围 2,当它被解释为带符号的 char 时,值会溢出并将值包装为 -126。编译器将 130 视为整数并进行从 intchar 的隐式转换。在大多数平台上,int 是 32 位,符号位是 MSB,值 130 很容易适合前 8 位,但是编译器希望将 24 位切碎以将其压缩成 char。当这种情况发生时,并且你已经告诉编译器你想要一个有符号字符,前 8 位的 MSB 实际上代表 -128。哦哦!你现在在内存中有这个1000 0010,当解释为有符号字符时,它是-128+2。我平台上的 linter 对此大喊大叫。 .

我之所以强调解释这一点很重要,因为在记忆中,这两个值是相同的。您可以通过在printf 语句中转换值来确认这一点,即printf("3: %+d\n", (unsigned char)c1);,您将再次看到130。

您在第一个 printf 语句中看到较大值的原因是您将已签名的 char 转换为未签名的 int,其中 char 已经溢出。机器首先将char解释为-126,然后转换为无符号int,它不能代表那个负值,所以你得到有符号int的最大值并减去126 .

2^32-126 = 4294967170 。 .宾果游戏

printf语句2中,机器所要做的就是添加24个零以达到32位,然后将值解释为int。在语句一中,您告诉它您有一个有符号值,因此它首先将其转换为 32 位 -126 值,然后将该 -ve 整数解释为无符号整数。同样,它翻转了它解释最高有效位的方式。有两个步骤:

  1. Signed char 被提升为signed int,因为您想使用int。 char(可能被复制并且)添加了 24 位。因为我们查看的是有符号值,一些机器指令会发生二进制补码,所以这里的内存看起来很不一样。
  2. 新的有符号 int 内存被解释为无符号,因此机器会查看 MSB 并将其解释为 2^32,而不是促销中发生的 -2^31。

一个有趣的琐事是,如果您执行char c1 = 130u;,您可以抑制clang-tidy linter 警告,但您仍然会根据上述逻辑得到相同的垃圾(即隐式转换会丢弃前24位,并且符号位无论如何都是零)。我已经提交了一份基于探索这个问题的 LLVM clang-tidy 缺失功能报告(如果你真的想关注它,请发布42137)?。

【讨论】:

  • 如果您提到这是未定义的行为,那就太好了。
  • @L.F.它不是 UB(除非你的意思是不正确的格式说明符)
  • @learnvst 好的,你能解释一下包装是如何制作的吗?那么第 1 行:4294967170 值呢?
  • @Daros - 抱歉,进一步清理了这个答案
  • @M.M 经过一番研究,我发现char(130) 是实现定义的行为而不是UB。把我的评论理解为不正确的格式说明符;-)
【解决方案2】:

(此答案假定,在您的机器上,char 的范围是 -128 到 127,unsigned char 的范围是 0 到 255,unsigned int 的范围是 0 到 4294967295,恰好是这种情况.)

char c1 = 130;

这里,130 超出了char 可表示的数字范围。 c1 的值是实现定义的。在您的情况下,数字恰好“环绕”,将 c1 初始化为 static_cast&lt;char&gt;(-126)

printf("1: %+u\n", c1);

c1 被提升为int,结果为-126。然后,%u 说明符将其解释为unsigned int。这是未定义的行为。这次得到的数字恰好是 unsigned int 表示的唯一数字,它与 -126 模 4294967296 一致,即 4294967170。

printf("3: %+d\n", c1);

int-126%d 说明符直接解释为 int,并按预期输出 -126 (?)。

【讨论】:

  • @eerorika 我才意识到我错了。我已经更新了答案。
【解决方案3】:

在情况 1、2 中,格式说明符与参数的类型不匹配,因此程序的行为是未定义的(在大多数系统上)。在大多数系统上,charunsigned char 小于 int,因此它们在作为可变参数传递时提升为 int。 int 与需要 unsigned int 的格式说明符 %u 不匹配。

unsigned charint 一样大的奇异系统(您的目标不是)上,它将被提升为unsigned int,在这种情况下,4 将具有UB,因为它需要int


对 3 的解释很大程度上取决于实现指定的细节。结果取决于char是否有符号,取决于可表示的范围。

如果 130 是 char 的可表示值,例如当它是无符号类型时,那么 130 将是正确的输出。情况似乎并非如此,因此我们可以假设 char 是目标系统上的签名类型。

使用无法表示的值(例如 char 在本例中为 130)初始化有符号整数会导致实现定义的值。

在有符号数的 2 的补码表示的系统上 - 这是当今普遍存在的表示 - 实现定义的值通常是可表示的值,它与不可表示的值以可表示的值的数量为模一致。 -126 与 130 模 256 一致,是char 的可表示值。

【讨论】:

    猜你喜欢
    • 2013-08-19
    • 2021-05-17
    • 2010-10-06
    • 2019-07-01
    • 2012-03-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-08-05
    相关资源
    最近更新 更多