c = a + b和隐式转换

18

使用我的编译器,c 是 54464(只保留了16位),d 是 10176。但是使用 gccc 变成了 120000,d 变成了 600000。

真正的行为是什么?这种行为是否未定义?还是我的编译器有误?

unsigned short a = 60000;
unsigned short b = 60000;
unsigned long c = a + b;
unsigned long d = a * 10;

是否有选项可以在这些情况下发出警告?

Wconversion会对以下情况发出警告:

void foo(unsigned long a);
foo(a+b);

但是不会警告:

unsigned long c = a + b

2
你的编译器是什么?它看起来像是一个嵌入式C编译器。最好使用专门的嵌入式工具链来查看。 - Jason Hu
如果你没有使用嵌入式编译器,那么你可能正在使用Turbo C,它不是一个“真正”的C编译器。尽快抛弃它。 - phuclv
我正在使用Microchip设备的C30编译器。 - VTiTux
3个回答

16
首先,您应该知道在C语言中,标准类型没有特定的精度(可表示值的数量)用于标准整数类型。它只需要每种类型的最小精度。这些导致以下典型的位大小,标准允许更复杂的表示:
  • char: 8位
  • short: 16位
  • int: 16 (!)位
  • long: 32位
  • long long(自C99以来):64位
注意:一个实现的实际限制(意味着一定的精度)在 limits.h 中给出。
其次,操作的类型由操作数的类型决定,而不是赋值左侧的类型(因为赋值也只是一个表达式)。为此,上述给出的类型按照转换等级进行排序。比int等级低的操作数首先会被转换为int。对于其他操作数,等级较低的操作数将被转换为另一个操作数的类型。这些是通常的算术转换
您的实现似乎使用了与unsigned short相同大小的16位unsigned int,因此ab都被转换为unsigned int,并进行16位操作。对于unsigned,操作执行模65536(2的16次方)取模运算,这被称为环绕运算(对于有符号类型,这不是必需的!)。然后将结果转换为unsigned long并分配给变量。
对于gcc编译器,我假设它是为PC或32位CPU编译的。因此,(unsigned)int通常具有32位,而(unsigned)long至少具有32位(必需)。因此,操作没有包装。请注意:对于PC,操作数会转换为int,而不是unsigned int。这是因为int可以表示所有unsigned short值;不需要unsigned int。如果操作结果溢出了signed int,这可能导致意外(实际上是实现定义)的行为!如果您需要指定大小的类型,请参见stdint.h(自C99起)中的uint16_tuint32_t。这些是用于您的实现的适当大小的类型的typedef。您也可以将其中一个操作数(不是整个表达式!)强制转换为结果类型:
unsigned long c = (unsigned long)a + b;

或者,使用已知大小的类型:
#include <stdint.h>
...
uint16_t a = 60000, b = 60000;
uint32_t c = (uint32_t)a + b;

请注意,由于转换规则,只需将一个操作数强制转换即可。 更新(感谢 @chux):
上面显示的强制转换可以正常工作。但是,如果 a 的转换等级比类型转换更高,则可能会将其值截断为较小的类型。虽然这可以很容易地避免,因为所有类型都在编译时已知(静态类型),但另一种方法是用所需类型的1进行乘法运算:
unsigned long c = ((unsigned long)1U * a) + b

这样将使用转换中给定类型的更大等级或a(或b)。任何合理的编译器都会消除乘法。

另一种方法是避免甚至知道目标类型名称,可以使用typeof() gcc扩展:

unsigned long c;

... many lines of code

c = ((typeof(c))1U * a) + b

1
从“limits.h”到范围表的转换有点突然。具体来说,标准列出了可表示的范围。你给出的大小表与标准兼容。而“limits.h”描述的是编译器实现的限制,理所当然地期望它符合或超过你的表中的值。 - Eric Towers
1
细节:(uint32_t)a + b; 可能会导致 a 的 _缩小_(尽管在这个简单的例子中不会)。_一般而言_,为了扩大一个整数,建议使用类型的乘法因子 1,例如 1UL*a + b; 而不是进行强制转换。这种方法永远不会导致 a 的缩小。 - chux - Reinstate Monica
@EricTowers:谢谢,作为非母语人士,有时我需要多次编辑我的文本才能得到一个好的措辞。希望现在的编辑更好了。 - too honest for this site
现在看起来好多了。即使作为母语人士,我也不得不多次编辑我的文本,以修复在打字时(考虑到多个上下文线程)很好但在阅读时(仅限于书面上下文)不太好的措辞。 - Eric Towers
@supercat: x *= x <=> x = x * x(语义上相同)。这里的 x 首先被提升为 int,因此乘法的结果是_实现定义_的(这就是我强调使用无符号类型的原因)。在这里阅读链接。如果您不想要这种行为,可以使用 x *= (unsigned)x;(或使用 chux 使用的乘法转换)。对于不知情的人来说,C 可能非常恶心。但是 Modula(或 Pascal)显然输掉了更好的语言的竞赛。 - too honest for this site
显示剩余4条评论

6
a + b将被计算为一个unsigned int(它被分配给unsigned long是不相关的)。C标准规定,这个和将对"最大可能的无符号整数加1"取模并且"循环回溯"。在您的系统上,看起来unsigned int是16位,因此结果被计算模65536。
另一方面,另一个系统中intunsigned int更大,因此能够容纳更大的数字。现在发生的事情相当微妙(感谢@PascalCuoq):因为所有unsigned short的值都可以表示为int,所以a + b会被计算为一个int。(只有当shortint具有相同的宽度或者以其他方式,部分unsigned short的值不能被表示为int时,该和将被计算为unsigned int。)
虽然C标准未指定unsigned shortunsigned int的固定大小,但您的程序行为是明确定义的。请注意,这对于有符号类型来说是适用的。
最后需要说明的是,您可以使用大小为uint16_tuint32_t等的定长类型,如果您的编译器支持,这些类型的大小是保证的。

请注意,如果程序将a*b相乘而不是相加,那么结果在int大于unsigned short但不超过两倍的平台上将无法定义。 - supercat

3
在C语言中,类型为charshort(以及它们的无符号版本)和float应被视为“存储”类型,因为它们旨在优化存储,但不是CPU所偏爱的“本地”大小,而且从不用于计算
例如,当你有两个char值并将它们放入表达式中时,它们首先被转换为int,然后进行操作。这样做的原因是CPU更适合使用int。对于float也是同样的情况,它总是隐式地转换为用于计算的double
在你的代码中,计算a+b是两个无符号整数的和;在C中没有办法计算两个无符号短整型的和...你可以将最终结果存储在一个无符号短整型变量中,由于模运算的属性,结果将会相同。

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