C++ 为什么减去无符号和有符号后符号不同?

C++ 为什么减去无符号和有符号后符号不同?,c++,language-lawyer,unsigned,negative-number,C++,Language Lawyer,Unsigned,Negative Number,为什么c的值为正而e为负?让我们从分析t-d的结果开始 t是一个unsigned int,而d是一个int,所以要对它们进行算术运算,d的值被转换成unsigned int(C++规则说unsigned在这里获得优先权)。所以我们得到了10u-16u,它(假设32位int)环绕到4294967290u 然后,该值在第一个声明中转换为float,在第二个声明中转换为int 假设float(32位单精度IEEE)的典型实现,其最高表示值大致为1e38,因此4294967290u在该范围内。将出现舍入

为什么
c
的值为正而
e
为负?

让我们从分析
t-d
的结果开始

t
是一个
unsigned int
,而
d
是一个
int
,所以要对它们进行算术运算,
d
的值被转换成
unsigned int
(C++规则说unsigned在这里获得优先权)。所以我们得到了
10u-16u
,它(假设32位
int
)环绕到
4294967290u

然后,该值在第一个声明中转换为
float
,在第二个声明中转换为
int

假设
float
(32位单精度IEEE)的典型实现,其最高表示值大致为
1e38
,因此
4294967290u
在该范围内。将出现舍入错误,但到浮点的转换不会溢出

对于
int
,情况有所不同
4294967290u
太大,无法装入
int
,因此会发生换行,我们返回值
-6
。请注意,标准并不能保证这样的环绕:本例中的结果值是实现定义的(1),这意味着结果值由编译器决定,但必须记录下来


(1) C++17(N4659),[conv.integral]7.8/3:

如果目的地类型是有符号的,如果可以在目的地类型中表示,则该值不变; 否则,该值由实现定义


首先,您必须理解(该链接是针对C的,但规则在C++中是相同的)。在C++中,如果你使用混合类型的算术(你应该避免在可能的情况下,顺便说一下),有一组规则决定计算的类型。

在本例中,您将从无符号整数中减去有符号整数。升级规则规定实际计算是使用
无符号整数
完成的

因此,在无符号整数算术中,您的计算是
10-16
。无符号算术是模算术,这意味着它是环绕的。因此,假设您的典型32位int,这个计算的结果是2^32-6

这两条线都是相同的。注意减法与赋值完全无关;左侧的类型对计算方式完全没有影响。这是一个常见的初学者错误,认为左侧的类型在某种程度上影响了计算;但是
float f=5/6
为零,因为除法仍然使用整数算法

那么,不同之处在于作业过程中发生了什么。减法的结果在一种情况下隐式转换为
float
,在另一种情况下隐式转换为
int

到float的转换尝试查找与该类型可以表示的实际值最接近的值。这将是一个非常大的价值;虽然不是最初减法得出的结果

转换为int表示,如果该值符合int的范围,则该值将保持不变。但是2^32-6远远大于32位int所能容纳的2^31-1,因此可以得到转换规则的另一部分,即结果值是实现定义的。这是标准中的一个术语,意思是“不同的编译器可以做不同的事情,但他们必须记录他们所做的事情”

出于所有实际目的,您可能遇到的所有编译器都会说位模式保持不变,只是被解释为带符号。由于2的补码算法的工作方式(几乎所有计算机都表示负数的方式),因此计算的结果是-6


但所有这些都是重复第一点的一段很长的路,那就是“不要做混合型算术”。首先,显式地将类型转换为您知道会做正确事情的类型。

只是好奇,但在这种情况下,实现定义是否意味着与硬件相关?因为,最常见的硬件是两个补码,所以您得到了这种行为,但即使使用同一个编译器,它也会在一个补码CPU上发生变化?@user1810087这意味着编译器必须记录将要发生的事情,这意味着编译器作者必须考虑将要发生的事情,并一致地实现它(或者至少与实现一致地记录下来)。他们的决定很可能会受到目标硬件的影响,因此这方面的记录将是特定于平台的。这不是由实现定义的行为-只有价值(参见您自己的脚注)。特别是,由于这种转换,实现不能引发异常、发出信号等。我认为您在第7段中的第二个值是
2^31-1
。因为“2^32-6远远大于2^32-1”从表面上看,这似乎不是一件显而易见的事情。大多数人会认为第一个值比第二个值正好小5倍,也不比第二个大多少:-)大多数编译器都有一个选项来警告您,如果您正在隐式地将
signed
值转换为
unsigned
,或者反之亦然。在GCC和Clang上,这是
-Wconversion
。你的问题是一个很好的例子,说明了为什么用它进行编译是一个好主意。如果你展示了你看到的实际值,这会有所帮助。
unsigned int t = 10;
int d = 16;
float c = t - d;
int e = t - d;