为什么模运算会返回意外的值

17

为什么以下代码会打印出255

#include <stdint.h>
#include <stdio.h>

int main(void) {
  uint8_t i = 0;
  i = (i - 1) % 16;
  printf("i: %d\n", i);
  return 0;
}

我假设了15,虽然i - 1的值为整数。


1
uint - 无符号整数,0-1 变为 MAX_INT。 - Marc B
2
@MarcB: 0 - 1-1MAX_INT>= 32767 - too honest for this site
1
@Olaf:它是一个无符号整数。 - Marc B
3
由于 整数提升 ,第一个操作数(类型为 uint8_t)被转换为 int。它可能会被转换为 unsigned int,但仅当 int 类型不能覆盖 uint8_t 的所有值时才会发生,这是可能的,但非常不太可能发生。 - Grzegorz Szpetkowski
3
int 至少是16位,而 uint8_t 正好是8位,因此实际上是不可能的。 - David Eisenstat
显示剩余2条评论
3个回答

16
由于C标准中存在整数提升,简单来说:在使用之前,任何“小于”int的类型都会转换为int。通常情况下无法避免这种情况。
所以发生了什么:将i提升为int。表达式被评估为int(您使用的常量也是int)。模数是-1。然后通过赋值将其转换为uint8_t255
对于printfi再次被提升为int(int)255。但是,这不会造成任何影响。
请注意,在C89中,对于a < 0a % b不一定是负数。它是实现定义的,可以是15。但是,自C99以来,-1 % 16保证为-1,因为除法必须产生代数商
如果你想保证模数给出正结果,你必须通过将i进行强制类型转换来评估整个表达式unsigned
i = ((unsigned)i - 1) % 16;

建议:启用编译器警告。至少,赋值转换应该给出截断警告。


4
在C89/C90中,这仅是实现定义。链接的C11标准段落甚至在第二句话中定义了它,并且UB相关内容在这里并不相关。 - cremno
2
然后阅读第一句话,包括脚注。您的第二个示例(特别是a / 16 == -1)不符合要求。C99解释(第67页上的6.5.5,第164和165页上的7.20.6)也是一个有用的资源。 - cremno
关于截断警告,((unsigned)i - 1) % 16保证在0到15的范围内,因此将其分配给uint8_t不应该有警告,因为该值不会超出范围。 - M.M
@cremno:第二点已经注意到了。我没有正确地关注“代数商”的部分。不知何故,旧的C90定义掩盖了这一点。同意,这是一个非常重要的区别。 - too honest for this site
@KeithThompson:好的,我明白了(抱歉,那是一段时间之前的事了)。我认为我可以限制在问题中的常量上,它们明确是“int”类型。 - too honest for this site
显示剩余3条评论

13
这是因为-1%n将返回-1而不是n-11。由于在这种情况下i是无符号8位整数,它变成了255。 1请参阅此问题以获取有关C / C ++中负整数取模的更多详细信息

1
没错,但是OP显然希望在评估过程中uint8_t一直表现为8位无符号类型,并在计算模数之前产生255而不是-1uint8_t之所以不会这样行事,是因为它首先被提升为int - AnT stands with Russia
因为整数提升规则(请参见Olaf的更完整的答案),所以这并没有解释为什么是-1%n而不是255%n,所以我要给它一个踩。 - Peter Cordes

2

使用Microsoft C编译器(没有stdint.h,所以我使用了typedef),这段代码可以正常工作(显示15):

#include <stdio.h>
typedef unsigned char uint8_t;

int main(void) {
    uint8_t i = 0;
    i = (uint8_t)(i - 1) % 16;
    printf("i: %d\n", i);
    return 0;
}

255是因为(i-1)被提升为整数,在C中用于%的整数除法会向零舍入,而不是向负无穷大舍入(向负无穷大舍入是数学、科学和其他编程语言采用的方式)。因此,在C中,%为零或具有与被除数相同的符号(在本例中,-1%16 == -1),而在数学中,模运算为零或具有与除数相同的符号。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接