在C语言中进行位运算后出现类型转换警告

11

你如何解释为什么第7行会收到警告,但第5行或第6行却没有?

int main()
{
    unsigned char a = 0xFF;
    unsigned char b = 0xFF;
    a = a | b;                        // 5: (no warning)
    a = (unsigned char)(b & 0xF);     // 6: (no warning)
    a = a | (unsigned char)(b & 0xF); // 7: (warning)
    return 0;
}

在32位架构(Windows PC)上编译时,GCC 4.6.2的输出:

gcc -c main.c --std=c89 -Wall -Wextra -Wconversion -pedantic
main.c: In function 'main':
main.c:7:11: warning: conversion to 'unsigned char' from 'int' may alter its value [-Wconversion]
如果这可以帮助你理解我的问题,那么我是这样认为的(可能不正确):
我假设在32位机器上操作是基于32位数字的。由于“unsigned char”适合于32位的“int”,因此操作结果是32位的“int”。但是,由于GCC在第5和第6行没有给出警告,我猜测还有其他事情正在发生:
第5行:GCC认为“(uchar) OR (uchar)”永远不会大于“MAX(uchar)”,所以不会出现警告。
第6行:GCC认为“(uchar) AND 0xF”永远不会大于“MAX(uchar)”,所以不会出现警告。显式转换甚至不是必要的。
基于上述假设,根据AND应该不会出现警告(因为第6行),OR也不应该出现警告(因为第5行)。
我猜我的逻辑有些错误。帮我理解编译器的逻辑。

3
这看起来像是编译器的一个错误:基于clang的Mac编译器按照您指定的设置进行编译时没有警告。 - Sergey Kalinichenko
在Linux/x86-64上,GCC 4.4.5没有发出任何警告。 - Fred Foo
有人能确认他们是否收到与我相同的警告吗? - Alex
1
是的,我使用GCC 4.4.3/Linux也出现了同样的警告。 - P.P
如果你在那里写(char)0xf会怎样? - dbrank0
2
我认为答案可能在这里:http://gcc.gnu.org/wiki/NewWconversion。即使在编译时已知值不会改变的变量之间进行隐式转换,为什么Wconversion会发出警告?原因是前端没有流控制(所以我们不知道d的值)。 - Alexey Frunze
4个回答

2
编译器是由人类构建的,他们没有无限的时间去决定哪些情况值得发出警告。所以我认为(注意这是个观点),编译器工程师会采取以下方式:
- 如果代码看起来可能有问题,则通常会发出警告。 - 找到所有明显的情况,其中编译器可以轻松地进行更正。 - 将其余的警告保留为误报,因为该人员要么知道自己在做什么,要么将感到安心,因为编译器在发出警告。
我期望人们编写的代码中,要么将结果转换为(unsigned char),要么最外层的运算符使用常量掩码来屏蔽所有高位字节。
- 那么a = (unsigned char) ( /* 一些模糊的位运算表达式 */ );就可以了。 - a = 0xff & ( /* 一些模糊的位运算表达式 */ );也可以。
如果您知道您的编译器可以正确地转换这两种模式,那么其他情况对您影响不大。
我曾经见过一些编译器会因为a = a | b;而发出警告,所以GCC不发出警告是一个免费的奖励。可能是因为GCC推断出 a | b的常量赋值,因此用0xff | 0xff替换它是没有问题的。然而,如果这样做了,我不知道为什么不能推导出其他语句中a的常量值。

+1 为了好的回答,感谢!目前为止是最好的,如果在一段时间内我找不到更好的答案,我会将其标记为答案。 - Alex
1
我认为Alex的最后一段包含了重要的信息。GCC不会对第5行抛出错误,因为它在编译时执行“OR”操作并优化掉了那行代码。我怀疑如果你交换第7行和第5行,你会看到编译器为不同的代码片段抛出警告。 - bta

0

我使用 Linux x86_64,GCC 4.70。并且出现了相同的错误。 我编译代码,并使用 gdb 来反汇编执行文件。以下是我得到的内容。

(gdb) l
1   int main(){
2     unsigned char a = 0xff;
3     unsigned char b = 0xff;
4     a = a | b;
5     a = (unsigned char)(b & 0xf);
6     a |= (unsigned char)(b & 0xf); 
7     return 0;
8   }
(gdb) b 4
Breakpoint 1 at 0x4004a8: file test.c, line 4.
(gdb) b 5
Breakpoint 2 at 0x4004af: file test.c, line 5.
(gdb) b 6
Breakpoint 3 at 0x4004b9: file test.c, line 6.
(gdb) r
Starting program: /home/spyder/stackoverflow/a.out 

Breakpoint 1, main () at test.c:4
4     a = a | b;
(gdb) disassemble 
Dump of assembler code for function main:
   0x000000000040049c <+0>: push   %rbp
   0x000000000040049d <+1>: mov    %rsp,%rbp
   0x00000000004004a0 <+4>: movb   $0xff,-0x1(%rbp)
   0x00000000004004a4 <+8>: movb   $0xff,-0x2(%rbp)
=> 0x00000000004004a8 <+12>:    movzbl -0x2(%rbp),%eax
   0x00000000004004ac <+16>:    or     %al,-0x1(%rbp)
   0x00000000004004af <+19>:    movzbl -0x2(%rbp),%eax
   0x00000000004004b3 <+23>:    and    $0xf,%eax
   0x00000000004004b6 <+26>:    mov    %al,-0x1(%rbp)
   0x00000000004004b9 <+29>:    movzbl -0x2(%rbp),%eax
   0x00000000004004bd <+33>:    mov    %eax,%edx
   0x00000000004004bf <+35>:    and    $0xf,%edx
   0x00000000004004c2 <+38>:    movzbl -0x1(%rbp),%eax
   0x00000000004004c6 <+42>:    or     %edx,%eax
   0x00000000004004c8 <+44>:    mov    %al,-0x1(%rbp)
   0x00000000004004cb <+47>:    mov    $0x0,%eax
   0x00000000004004d0 <+52>:    pop    %rbp
   0x00000000004004d1 <+53>:    retq   
End of assembler dump.

a = a | b被编译为

movzbl -0x2(%rbp),%eax
or     %al,-0x1(%rbp)

a = (unsigned char)(b & 0xf)编译后的结果为

mov    %al,-0x2(%rbp)
and    $0xf,%eax
mov    %al,-0x1(%rbp)

a |= (unsigned char)(b & 0xf); 是编译成的代码。

movzbl -0x2(%rbp),%eax
mov    %eax,%edx
and    $0xf,%edx
movzbl -0x1(%rbp),%eax
or     %edx,%eax
mov    %al,-0x1(%rbp)

在汇编代码中没有出现显式转换。问题出在执行 (b & 0xf) 操作时,操作的输出是 sizeof(int)。 因此,你应该使用以下代码:

a = (unsigned char)(a | (b & 0xF));

PS:显式转换不会产生任何警告,即使您会失去一些东西。


movzbl -offset(%rbp),%eaxmov %al,-0x1(%rbp) 有效地进行强制类型转换。我认为原帖的作者不应该改变他们的代码。这是合法且合理的C代码。 - Alexey Frunze
@Alex 看看这个 or %al,-0x1(%rbp)and $oxf, %eax,它们的区别在于第二个需要显式转换。而第三个显式转换没有任何作用,因为在 and $0xf,%edx 指令之后,结果没有进行任何内存操作。 - spyder
无论如何,我认为这种反汇编并不能证明什么。生成的代码是正确的。 - Alexey Frunze
a = a | b 等同于 a |= b,它们都是两个成员,每个成员都是8位。 - spyder
可以使用8位比特运算,但是a = b & 0xf有三个成员,0xf不清楚,因此需要32位比特运算。显然结果是32位的。 - spyder
2
在执行|(和|=)之前,ab会被提升为int类型,而a|b的值也是int类型。这是C标准规定的。编译器在决定如何生成符合标准的代码时有很大的自由度。它可能足够聪明,将此|编译成仅具有8位操作数的指令。它也可能足够聪明,将其编译成更快的32位操作数或32位和8位操作数混合的指令。只要程序的行为符合标准,编译器可以做任何事情。反汇编证明不了什么。 - Alexey Frunze

0

按位运算符 & 的返回类型是整数。每当您将 int(4 字节)强制转换为 char 或 unsigned char(1 字节)时,它会给出警告。

因此,这与按位运算符无关,而是与从 4 字节变量到 1 字节变量的类型转换有关。


我同意Omer的观点,我不久前也遇到了同样的问题。在C语言中,任何位运算的结果都会被提升到寄存器的大小。如果你使用的是32位机器,那么它的大小就是4个字节。 - Alexander Oh
@Alex,请再仔细阅读我的问题。我不是在问编译器为什么会给出警告,而是在问它为什么会对第5/6/7行进行不同的处理。你的帖子只是在陈述显而易见的事实,根本没有回答我的问题。 - Alex
@Alex 我编辑了Omers的帖子(他目前还没有批准):如果您使用常量进行位运算,编译器可以直接推导出结果大小,并检查结果是否会溢出。因此,第5行不会收到警告。然而,许多编译器在算术优化方面并不擅长。当优化器没有清晰的结果时,它会发出警告。 - Alexander Oh
@Alex 我不确定你的意思。你是说如果有一个常量,那么编译器就能推导出结果大小并进行检查,因此第5行没有警告。首先,第5行中没有任何常量。此外,第7行实际上使用了一个常量并且会产生警告,但是如果你将常量更改为变量(即 a = a | (unsigned char)(b & a);),那么警告就会消失。我想我应该等待你的编辑出现。 - Alex
@Alex 我尝试更详细地回答了自己的问题并发布了它。希望你能欣赏。 - Alexander Oh
1
亚历克斯和亚历克斯交谈?天哪,这太令人困惑了! - Pieter Müller

-1

我认为问题在于您将 int 转换为 unsigned char,并且再转回 int

第6行将 int 转换为 unsigned char,但只是将其存储到 unsigned char 中。
第7行将 int 转换为 unsigned char,然后为了进行算术运算又将其转换回 int。新的整数可能与原始值不同,因此会出现警告。


你考虑过第5行吗?按照你的逻辑,第5行不应该收到警告吗?“第5行使用了unsigned char,然后为了进行算术运算,将其转换为int[...]” - Alex
根据 C 语言的规则,赋值运算符右侧的三个表达式都是 int 类型。在这方面它们并没有不同。 - Alexey Frunze
@Alex,只有第7行将int转换为unsigned char 并且 再转回int - ugoren
好的,我对第二种情况是错误的,但是 sizeof(a | b) == sizeof(a | (unsigned char)(b & 0xF)) == sizeof(int) - Alexey Frunze
1
a|b 中,unsigned char 被转换为 int,仅此而已。在 a|(unsigned char)(b&0xf) 中,b&0x0f(即 int)被转换为 unsigned char,然后再转回 int - ugoren
显示剩余2条评论

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