【问题标题】:output of negative integer to %u format specifier将负整数输出到 %u 格式说明符
【发布时间】:2017-12-10 14:36:49
【问题描述】:

考虑下面的代码

char c=125;
c+=10;
printf("%d",c); //outputs  -121 which is understood.
printf("%u",c); // outputs 4294967175.
printf("%u",-121); // outputs 4294967175

%d 接受负数,因此在第一种情况下输出为 -121。 案例 2 和案例 3 的输出是 4294967175。我不明白为什么?

【问题讨论】:

  • 机器确实知道有一个负值,只是一个二进制值。 %u 告诉代码将该二进制值解释为 unsigned
  • 前两种情况,c被提升为int
  • 答案取决于您的编译器中char 是签名还是未签名。

标签: c format-specifiers


【解决方案1】:

232 - 121 = 4294967175

printf 解释您提供的数据,这要归功于%

  • %d 有符号整数,值从 -231 到 231-1
  • %u无符号整数,取值范围为0到232-1

在二进制中,两个整数值(-121 和 4294967175)(当然)是相同的:

`0xFFFFFF87`

Two's complement

【讨论】:

  • 应该提到的是,这种情况下的行为不是由 C 标准定义的,观察到的结果只是因为这个特定的实现在这种特定情况下的行为。不可靠。
【解决方案2】:

printf 是一个带有可变参数的函数。在这种情况下,在调用函数之前对参数应用“默认参数提升”。在您的情况下,c 首先从char 转换为int,然后发送到printf。转换不依赖于格式的相应 '%' 说明符。这个 int 参数的值可以解释为 4294967175 或 -121,具体取决于符号。 C标准中对应的部分是:

6.5.2.2 函数调用

6 - ... 如果表示被调用函数的表达式的类型不包含 原型,整数提升对每个参数执行,参数 具有 float 类型的提升为 double。这些被称为默认参数 促销活动。

7- 如果表示被调用函数的表达式具有包含原型的类型, 参数被隐式转换,就像通过赋值一样,转换为 对应的参数,取每个参数的类型为不合格版本 其声明的类型。函数原型声明器中的省略号表示 参数类型转换在最后一个声明的参数之后停止。默认参数 提升是在尾随参数上执行的。

【讨论】:

  • 应该提到的是,这种情况下的行为不是由 C 标准定义的,观察到的结果只是因为这个特定的实现在这种特定情况下的行为。不可靠。
【解决方案3】:

如果 char 在您的编译器中签名(这是最可能的情况)并且是 8 位宽(极有可能),那么 c+=10 将溢出它。有符号整数的溢出会导致未定义的行为。这意味着您无法对所获得的结果进行推理。

如果 char 未签名(在大多数 PC 平台上不太可能),请查看其他答案。

【讨论】:

  • C 标准没有定义整数溢出这一事实意味着您无法单独使用 C 标准来推断它。这并不意味着你根本无法推理。即使你达到了 C 标准的极限,世界上的其他一切仍然按照自己的规则运行——物理规则仍然适用,逻辑规则仍然适用,计算机仍然按照构建的方式运行,软件仍然按照机器上。我们可以推断为什么 -121 会产生“4294967175”,即使它不是 C 标准所保证的。
  • 到目前为止的其他答案也存在缺陷,因为它们未能解释 C 标准未定义此行为。但一个好的答案既解释了这种行为不是由 C 标准定义的,也解释了为什么我们看到了我们所做的结果。
  • @EricPostpischil 我不确定物理定律的来源。如果存在未定义的行为,编译器可以假设不正确的事情。未定义行为允许编译器生成代码或删除代码,从而阻止您对代码进行推理。
  • 是的,编译器允许按照 C 标准执行任何操作。但它在这里不只是“做任何事”。 “4294967175”不是随机处理的结果。它被印刷是有原因的,我们知道这些原因。我们无法对结果进行推理的说法是错误的。
【解决方案4】:

printf 使用称为可变参数的东西。如果您对它们进行简要研究,您会发现使用它们的函数不知道您传递给它的输入的 type。因此,必须有一种方法来告诉函数它必须如何解释输入,并且您正在使用格式说明符来执行此操作。

在您的特定情况下,c 是一个 8 位有符号整数。因此,如果将其设置为其中的文字-121,它将记住:10000111。然后,通过 integer Promotion 机制将其转换为 int:11111111111111111111111110000111

使用 "%d" 您告诉 printf 将 11111111111111111111111110000111 解释为有符号整数,因此您有 -121 作为输出。但是,使用 "%u" 您是在告诉 printf 11111111111111111111111110000111 是一个 无符号 整数,因此它将输出 4294967175

编辑:正如 cmets 中所述,实际上行为在 C 中是未定义的。那是因为您有不止一种方法来编码负数(符号和模,一个补码,...)和其他方面(例如如果我没记错的话,字节序会影响这个结果)。所以结果被称为实现定义。因此,您可能会得到不同的输出,而不是 4294967175。但是我解释的对同一串位的不同解释以及可变参数中数据类型的损失的主要概念仍然成立。

尝试将数字转换为以 10 为底的数字,首先是纯二进制数,然后知道它是以 32 位二进制补码形式存储的……你会得到两个不同的结果。但是,如果我不告诉您需要使用 哪个 解释,那么该二进制字符串可以代表所有内容(一个 4 字符的 ASCII 字符串、一个数字、一个小的 8 位 2x2 图像、您的安全组合, ...)。

编辑:您可以将“%”视为该位字符串的一种“扩展”。你知道,当你创建一个文件时,你通常会给它一个扩展名,这实际上是文件名的一部分,以记住该文件以哪种格式/编码存储。假设您将最喜欢的歌曲保存为 PC 上的song.ogg 文件。如果您将文件重命名为song.txtsong.odtsong.pdfsongsong.akwardextension,则不会更改文件的内容。但是,如果您尝试使用通常与 .txt 或 .whatever 关联的程序打开它,它会读取文件中的字节,但是当它尝试解释字节序列时可能会失败(这就是为什么如果您使用 Emacs 打开song.ogg或 VIm 或任何文本编辑器,你会得到看起来像垃圾信息的东西,如果你用 GIMP 打开它,GIMP 无法读取它,如果你用 VLC 打开它,你会听到你最喜欢的歌曲)。扩展只是对您的提醒:它提醒您如何解释该位序列。由于 printf 对这种解释一无所知,因此您需要提供一个,如果您告诉 printf 一个有符号整数实际上是无符号的,那么这就像用 Emacs 打开 song.ogg...

【讨论】:

  • 应该提到的是,这种情况下的行为不是由 C 标准定义的,观察到的结果只是因为这个特定的实现在这种特定情况下的行为。不可靠。
  • @EricPostpischil,感谢您让我注意到我的答案不完整。我更新了它。无论如何,其他论点(可变参数中的类型丢失和对同一条数据的模棱两可的解释)仍然成立。尽管我可以争辩说“在您的特定情况下”意味着我提到了他的特定 C 实现......
猜你喜欢
  • 1970-01-01
  • 2014-05-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-07-01
相关资源
最近更新 更多