以下面的C代码为例:
int main(int argc, char *argv[])
{
signed char i;
unsigned char count = 0xFF;
for (i=0; i<count;i++)
{
printf("%x\n", i);
}
return 0;
}
此代码在无限循环中运行,即使我按如下方式编译它:
# gcc -Wall -Wpedantic -Wconversion -Wsign-compare -Wtype-limits -Wsign-conversion test.c -o test
是否有人知道应该警告这些问题的编译器标志?
为了清楚起见,我不是要问为什么会得到一个无限循环,而是要知道是否有使用编译器或静态分析防止它的方法?< / p>
答案 0 :(得分:13)
标志-Wconversion不会捕获错误,因为比较中的两个操作数:i<count
,使用整数提升升级为int。
gcc中没有可以捕获此标记的标记。
除此之外,代码的行为是未定义的,因为变量i溢出,当它具有值0x7F
并且递增时:i++
。
如果您要迭代某个值,请确保您使用的类型可以代表该值。
答案 1 :(得分:7)
i
是signed char
,将其递增到SCHAR_MAX
以上,具有实现定义的效果。计算i + 1
是在将i
升级为int
后执行的,并且不会溢出(除非sizeof(int) == 1
和SCHAR_MAX == INT_MAX
)。然而,此值超出了i
的范围,并且i
具有签名类型,结果是实现定义的,或者引发了实现定义的信号。(C11 6.3.1.3p3有符号和无符号整数)。
根据定义,编译器是实现,因此为每个特定系统和x86架构定义了行为,其中存储值导致屏蔽低位,gcc
应该知道循环测试绝对是不变的,使它成为一个无限循环。
请注意,clang
也未检测到常量测试,但clang 3.9.0
将count
声明为const
,如果i < count
确实发出警告与i < 0xff
不同,{}替换为gcc
。
两个编译器都没有抱怨签名/未签名的比较问题,因为在比较之前,两个操作数实际上都被提升为int
。
你在这里发现了一个有意义的问题,特别重要,因为一些编码约定坚持对所有变量使用尽可能小的类型,导致像int8_t
或uint8_t
循环索引变量这样的奇怪。这样的选择确实容易出错,我还没有找到一种方法让编译器警告程序员有关你发布的错误等愚蠢的错误。
答案 2 :(得分:3)
由于i
是signed char
,因此其值通常为-128 to 127
。 count
为unsigned char
时,255 (0xFF)
的值为i
。
在循环内部,当127
值达到128
并再次递增时,它永远不会达到-128
并转到127
,然后再次转到{{1}然后再次转到-128
,依此类推。 i
的值将永远小于循环内count
的值,因此循环永远不会终止!
由于数据类型的溢出而发生这种情况,必须小心谨慎地检查可能发生自动类型强制的表达式,因为它们不会发出任何警告。
编辑: 从GCC文档中,
-Wconversion: 警告可能会改变值的隐式转换。
在这里,由于比较而不是分配,我们变得不一致。
答案 3 :(得分:1)
没有警告要抓住这个,因为从编译器的角度来看,这段代码没有任何问题。
正如其他答案中所指出的,比较线被有效地视为
for (i=0; (int)i<(int)count; i++)
如Rationale for International Standard—Programming Languages—C第6.3.1.1节所述,他们在比较值时为隐式类型转换选择了“保值”方法,因为它可能会产生意外的比较结果。
如果这些是int
s,那么“保留值”语义将无法实现 - 因为int
通常是硬件类型(或至少C是设计的)考虑到这一点,并且很少(如果有的话)架构允许一个操作数被签名而另一个操作数未签名。 如果您将char
替换为此int
,则编译器发出“已签名和未签名的比较”警告的核心原因。
对于增量操作,i++
,
最终,造成这种混乱的原因是你对int
推广语义的遗忘。这导致代码的行为意外的(不要感觉不好,在阅读其他答案之前我也不知道这个!)。然而,事实证明,对于标准而言,它是非常期待的 - 而且,由它决定的 “无知的法律不是借口”,正如他们所说的那样。