C++中的无符号和有符号转换

6

我之前看到过这种问题的提问,但是给出的答案并没有完全解决我的问题。通常在发布这个问题时,会附带下面的示例:

#include <iostream>

int main()
{
    unsigned int u = 10;
             int i = -42;

    std::cout << i + i << std::endl;
    std::cout << i + u << std::endl;

    return 0;
}

输出:

-84
4294967264

int 转换为无符号数时,一切都按预期进行。但是,如果 i 的绝对值小于 u,似乎不会发生这样的转换。

#include <iostream>

int main()
{
    unsigned int u = 10;
             int i = -3;

    std::cout << i + i << std::endl;
    std::cout << i + u << std::endl;

    return 0;
}

输出:

-6
7

我没看到任何提到它的答案,也找不到任何解释。虽然它似乎是一件符合逻辑的事情,但我还没有找到任何解释。

2
你如何区分有符号的 7 和无符号的 7 - 463035818_is_not_a_number
您可以使用 decltypestd::is_same_v 来验证表达式的类型。(或者使用一些技巧在文章中打印类型。) - chris
通常对于这种问题,@463035818_is_not_a_number会回答说,有符号变量(int i)会像第一个示例中一样转换为无符号变量。如果发生在第二个示例中,结果将不等于7。 - Aisec Nory
哦,我想现在我明白了 ;) - 463035818_is_not_a_number
@463035818_is_not_a_number:无符号7需要加上U后缀,否则它是一个整数,也就是有符号整型。 - Thomas Matthews
@ThomasMatthews 输出的是通过std::cout在屏幕上打印的7,而不是整数字面量7。不用理会我,我一开始完全误解了问题,这就是问一些愚蠢问题的原因。 - 463035818_is_not_a_number
3个回答

8

之后:

unsigned int u = 10;
         int i = -3;

i + u的求值首先将i转换为unsigned int。对于一个32位的unsigned int,这个转换会取模232,即4294967296。这个取模的结果是-3 + 4294967296 = 4294967293。

在转换后,我们将4294967293(转换后的i)和10(u)相加。这将得到4294967303。由于这超出了32位unsigned int的范围,它将被取模成4294967296。这样得到的结果是4294967303 - 4294967296 = 7。

因此输出“7”。


5
但如果绝对值小于 u,似乎没有进行任何转换。
你的假设是错误的:确实进行了这样的转换。我所说的“这样的转换”是指当 -3 转换为无符号类型时,结果为 4'294'967'293。
没有任何答案提到它或找不到任何解释。
无符号算术是模运算。这只是模运算的工作原理。
要理解模运算,以 12 小时制钟表的工作方式为例。它也是模运算。你会注意到时钟面板上没有负数:
- 时间是 10 点 (上午,今天)。3 小时前是几点?7 点 (上午,今天)。 - 时间是 10 点 (上午,今天)。42 小时前是几点?4 点 (下午,前天)
无符号算术的工作方式完全相同。除了 12 个值之外,在 32 位类型的情况下有 4'294'967'296 个值。
为了将一个不可表示的值转换为可表示范围内的值,只需加上或减去模数 (时钟为 12,32 位无符号整数为 4'294'967'296),直到该值在可表示范围内。
以下是钟表示例的计算:
R10 + (-3)          (mod 12)
// -3 = 9  + (12 * -1)
R10 + 9             (mod 12) 
R19                 (mod 12)
// 19 = 7  + (12 *  1)
R7                  (mod 12)

R10 + (-42)         (mod 12)
// -42 = 6 + (12 * -4)
R10 + 6             (mod 12)
R16                 (mod 12)
// 16 = 4 +  (12 *  1)
R4                  (mod 12)

以下是有关您示例的数学计算:

R ≡ 10 + (-42)         (mod 4'294'967'296)
// -42           = 4'294'967'254 + (4'294'967'296 * -1)
R ≡ 10 + 4'294'967'254 (mod 4'294'967'296)
R ≡ 4'294'967'264      (mod 4'294'967'296)

R ≡ 10 + (-3)          (mod 4'294'967'296)
// -3            = 4'294'967'293 + (4'294'967'296 * -1)
R ≡ 10 + 4'294'967'293 (mod 4'294'967'296)
R ≡ 4'294'967'303      (mod 4'294'967'296)
// 4'294'967'303 = 7             + (4'294'967'296 * -1)
R ≡ 7                  (mod 4'294'967'296)

0

理解负数和无符号数之间关系的两个非常重要的概念是补码。这是一个非常古老的标准,现在已经不再使用,但很容易理解。负数只是每个位的非。-1 = ~1 = 1111...1110。 补码的问题是有两个零(+0/-0)。这很快变得复杂。考虑

X = 2-2  
Y = -2+2  
X != Y

这就是为什么现代计算机和C/C++采用二进制补码的原因。二进制补码去掉了-0(不存在),因此

-1 = 1111...1111  
-2 = 1111...1110  
so -v = ~1+1

你会发现没有“操作”可以在有符号和无符号之间进行转换。在硬件中,每个都只是一组位向量。值是有符号还是无符号,仅仅是如何解释这些位的问题。为了证明这一点,你可以尝试

printf("%x", -1); // print a signed value as if it was unsigned

所以回到你的问题,任何小的(+v)数字都是一样的,无论它是否带符号。(signed) 1与unsigned (1)是相同的。只有当最高有效位为1时,解释才会改变。

顺带说一句:在C/C++中,“unsigned int”可以缩写为“unsigned”,这是一条规则的残余,即任何未指定类型的类型都被认为是int。


一补数(不是“赞美”)和二补数在这个问题中没有影响。C标准规定了转换为无符号整数类型以及使用算术模M+1来包装无符号算术运算,其中M是可表示的最大值unsigned type。符合C实现使用一补数、二补数或者补码都不会影响在这种情况下观察到的结果。 - Eric Postpischil
对于经验较少的程序员来说,理解C/C++标准不是一堆武断的规则,而是选择与其运行的硬件相匹配。当硬件采用标准时,C/C++通常会遵循。虽然你的答案更受欢迎,所以我会让步,但我仍坚持主要教学观点,即整数转换不涉及值的任何操作(因此没有开销),它只是改变原始值的解释。这与int-float形式形成了鲜明对比,后者具有显着的开销。 - Tiger4Hire

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