如何在比较char和unsigned char时启用编译器警告?

24

以以下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

是否有编译器标志可以警告此类问题?

只是为了明确,我不是在问'为什么会产生无限循环',而是想知道是否有一种方法可以使用编译器或静态分析来防止它?


-W选项不只是警告吗?你的代码会一直运行,因为有符号字符在超过127之前就会绕回(或者执行某些未定义的行为),因此它无法超过循环终止条件。 - nicomp
2
有符号字符将始终小于255。因此它不会退出循环。 - Taha Paksu
如果 CHAR_MAX < INT_MAX,则将其提升为 int 并进行加法运算将产生一个定义良好的结果,该结果小于 INT_MAX;需要将 CHAR_MAX+1INT_MAX 之间的值转换为 signed char,以便产生一个实现定义的值或引发一个实现定义的信号。 - supercat
@supercat 你说得对,谢谢! - Adriano Repetti
显示剩余6条评论
4个回答

12

由于使用整数提升(integer promotions)方式将比较运算符i<count中的两个操作数同时提升为int类型,因此标志-Wconversion无法捕获错误。

在gcc中没有任何标志可以捕获这种错误。

另外,您的代码行为是未定义的(undefined),因为变量i在值为0x7F且进行i++自增操作时会发生溢出(overflow)。

如果您想迭代到某个值,请确保所使用的类型能够表示该值。


13
我知道变量i会溢出,这正是我想要实现的。我不是在问为什么它会进入无限循环,以及如何修复它。我想知道是否有一种方法使编译器能够捕获这个问题? - sagi
1
谢谢@2501,我查看了gcc man页面上的所有警告标志,但没有找到一个。 - sagi
1
@sagi 你可以尝试使用第三方静态分析器。 - 2501
1
虽然本身是正确的,但编译器警告确实指出了这里和那里的未定义行为,在这种情况下,一个警告会非常有帮助。 - Matthieu M.
1
@supercat postfit增量不执行整数提升。操作数未提升为int,因此会溢出。 - 2501
显示剩余11条评论

7

i是一个有符号字符型变量,将其增加到SCHAR_MAX之上会产生一个实现定义的效果。计算i + 1在将i提升为int后执行,不会溢出(除非sizeof(int) == 1SCHAR_MAX == INT_MAX)。但是这个值超出了i的范围,由于i是有符号类型,结果要么是实现定义的,要么会引发一个实现定义的信号。 (C11 6.3.1.3p3 Signed and unsigned integers)。

根据定义,编译器就是实现,因此对于每个特定系统行为都是定义好的,在存储该值会掩盖低位的x86架构上,gcc应该知道循环测试肯定是常量,使其成为无限循环。

请注意,clang也没有检测到常量测试,但如果将count声明为const,则clang 3.9.0将检测到它,并且如果用i < 0xff替换i < count,则会发出警告,而gcc不会。

两个编译器都没有抱怨有符号/无符号比较问题,因为在比较之前,这两个操作数都被提升为int

你发现了一个很有意义的问题,尤其是一些编码惯例坚持使用尽可能小的类型来定义所有变量,导致出现如int8_tuint8_t的奇怪情况。这样的选择确实容易出错,我还没有找到一种方法来让编译器警告程序员关于这种愚蠢错误。


1
你似乎在谈论加法i+1,但是OP正在使用后缀++运算符,它不会执行整数提升。这将导致溢出和未定义行为。 - 2501
2
@2501:我查阅了 C11 标准中相关的语言,但并没有找到明确支持此观点的内容。你能详细说明一下为什么认为 i++ 的行为应该与 i += 1 和/或 i = i + 1 不同吗? - chqrlie
你假设后缀 ++ 的行为与 += 相同。但实际上并不是这样的。因为只有前缀 ++ 被明确提到,所以它才会这样表现。 - 2501
1
@2501:我为此提出了一个单独的问题。答案应该从标准中得到精确的引用。 - chqrlie

3

i 是一个 signed char 类型,通常它的值范围是从 -128127。而 count 是一个 unsigned char 类型,并被赋值为 255 (0xFF)

在循环内部,当 i 的值达到 127 并再次递增时,它永远不会达到 128,而是进入 -128,然后再次到达 127,然后再次回滚到 -128,如此往复。在循环内,i 的值永远小于 count 的值,因此该循环永远不能终止!

这是由于数据类型溢出所导致的,必须注意对可能发生自动类型转换的表达式进行严格审查,因为它们不会发出任何警告。

编辑: 来自 GCC 文档的说明:

-Wconversion: 警告可能改变值的隐含转换。

在这里,由于比较而不是赋值,我们得到了不一致的结果。


7
我知道变量i会溢出,这正是我想要实现的。我不是在问为什么它会进入无限循环,以及如何修复它。我想知道是否有一种方法让编译器捕捉到这个问题? - sagi
值得一提的是:由于有符号字符的所有值都无法表示为无符号字符(反之亦然),因此比较运算符的两个操作数都会被转换为下一个可以容纳它们的 int 类别。该转换的符号保留是有保证的,因此最终会将 -128..+127 与 255 进行比较,这将始终为真。 - Christophe
2
@sagi:由于for循环条件部分的类型转换将较窄类型(signed char)提升为较宽类型(unsigned char),因此编译器不会报告此转换。 - skrtbhtngr
1
实际上,在比较中它们都会被转换为 int,但在这种情况下这是微不足道的。 - skrtbhtngr
如果这个答案对任何人来说似乎不正确或不合适,请在点赞的同时发表评论! - skrtbhtngr
即使i没有溢出,表达式i<count始终为真,因此循环无法停止并将无限运行。 - phuclv

1

没有警告可以捕捉到这个问题,因为从编译器的角度来看,这段代码没有任何可疑之处。

  1. As pointed out in other answers, the comparison line is effectively treated as

    for (i=0; (int)i<(int)count; i++)
    
    • As described in Rationale for International Standard—Programming Languages—C, section 6.3.1.1, they chose the "value preserving" approach for implicit type conversions when comparing values because it has less potential for unexpected comparison results.

      • Note that your program runs "incorrectly" specifically because the result of arithmetic comparison is the expected one.
    • If these were ints, that "value preserving" semantic would be impossible to achieve - because an int is as a rule a hardware type (or at least, C is designed with this in mind), and there are few (if any) architectures that allow one operand to be signed while the other unsigned. That's the core reason for a compiler issuing a "comparison between signed and unsigned" warning if you replace chars with ints here.

  2. For the increment operation, i++,

    • the same above-linked rationale mentions in a few places that "silent overflow" is by far the most common and expected semantic. It even stated to have caused "a lack of sensitivity in the C community to the differences between signed and unsigned arithmetic" in the first place! So, nothing suspicious here, either.

最终,导致混淆的原因是您对 int 提升语义的无知。这导致代码行为对您来说是 意料之外的(不要难过,在阅读其他答案之前我也不知道这一点!)。然而,事实证明,这是 标准所预期的 - 而且还被它所规定。正如人们所说,“不知法不免责。”


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