hololive滚滚山免安装绿色中文版
995M · 2025-10-31
你有没有想过,负整数如何转换成无符号整数?
在 C++ 中,将一个负的有符号整数转换为无符号整数有一套明确的规则。
转换遵循一个基本原则:模运算(Modular Arithmetic)。
无符号类型的取值范围是一个从 0 到 MAX(最大值)的环。当你试图赋予它一个超出这个范围的值时,结果会是 MAX + 1(这个环的大小)取模后的值,即 原数字 % (MAX + 1),负数的位模式被直接解释为无符号数。
负数转换后的结果 = 无符号类型的最大值 (MAX) + 原始负数值 + 1
让我们用最常见的场景来说明:将 -1 转换为 unsigned int。
确定 unsigned int 的最大值 (MAX)
对于一个 32 位的 unsigned int,其最大值是:
MAX = 2³² - 1 = 4,294,967,295
这个值也通常写作 0xFFFFFFFF(十六进制)。
应用模运算规则
转换公式为:Result = (-1) % (MAX + 1)
因为 -1 是负数,我们需要让它变成正数模。等价的做法是:
Result = MAX + (-1) + 1
计算结果
Result = 4,294,967,295 + (-1) + 1 = 4,294,967,295
所以,(unsigned int) -1 的结果就是 4,294,967,295。
怎么理解呢?你可以认为无符号的-1就是2^32 - 1。
有一本书叫做CSAPP,(《深入理解计算机系统》——简称CSAPP,被称为计算机领域的圣经,豆瓣评分9.8),比较详细的解释了数据在计算机中的表达。参见这个网络地址 CMU 15-213: CSAPP - CS自学指南。
计算机中整数用二进制补码表示。-1 的二进制补码表示是所有位都是 1。
int -1 的位模式:11111111 11111111 11111111 11111111unsigned int 的位模式:11111111 11111111 11111111 11111111关键点: 转换过程不会改变数据的底层位模式。编译器只是换了一种方式去解释这串相同的二进制位。
1111...1111 解释为 int(二进制补码),它的值就是 -1。unsigned int,它的值就是 2³² - 1 = 4,294,967,295。这种“重新解释”是转换的本质。
| 有符号值 (int) | 转换后的无符号值 (unsigned int) | 计算过程 ( MAX = 4,294,967,295) | 
|---|---|---|
| -1 | 4,294,967,295 | MAX - 1 + 1 = MAX | 
| -5 | 4,294,967,291 | MAX - 5 + 1 | 
| -100 | 4,294,967,196 | MAX - 100 + 1 | 
| INT_MIN(-2,147,483,648) | 2,147,483,648 | MAX + INT_MIN + 1 | 
你可以看到一个规律:转换后的值 U 和原始值 S 满足:
U = S + (MAX + 1) (当 S 为负数时)
你可以写一段简单的代码来验证这个转换:
#include <iostream>
#include <climits> // 用于 INT_MAX, UINT_MAX
int main() {
    int negative_num = -1;
    unsigned int positive_num = negative_num; // 隐式转换发生在这里
    std::cout << "Original signed value: " << negative_num << std::endl;
    std::cout << "After conversion to unsigned: " << positive_num << std::endl;
    std::cout << "The maximum value of unsigned int (UINT_MAX): " << UINT_MAX << std::endl;
    // 验证其他值
    std::cout << "n-5 as unsigned: " << (unsigned int)(-5) << std::endl;
    std::cout << "-100 as unsigned: " << (unsigned int)(-100) << std::endl;
    std::cout << "INT_MIN as unsigned: " << (unsigned int)(INT_MIN) << std::endl;
    return 0;
}
运行结果将会是:
Original signed value: -1
After conversion to unsigned: 4294967295
The maximum value of unsigned int (UINT_MAX): 4294967295
-5 as unsigned: 4294967291
-100 as unsigned: 4294967196
INT_MIN as unsigned: 2147483648
这种转换是很多 bug 的根源,尤其是在循环和条件判断中混合使用有符号和无符号类型时。
危险的例子:
#include <iostream>
#include <vector>
int main() {
    std::vector<int> vec = {1, 2, 3}; // size() 返回 size_t (无符号)
    int index = -1; // 有符号整数
    // 灾难!index 被隐式转换为无符号数,变成了一个巨大的正数
    if (index < vec.size()) { // 比较:巨大的正数 < 3? false!
        std::cout << "This is safe.n";
    } else {
        std::cout << "This will be printed! Unexpected behavior!n";
    }
    return 0;
}
输出: This will be printed! Unexpected behavior!
MAX + 1 运算得到一个很大的正数。(unsigned int) -1 等于 UINT_MAX(例如 4,294,967,295)。static_cast<unsigned int>(some_int) 进行显式转换,以明确你的意图。为什么需要这些码?
计算机底层只能存储 0 和 1。如何用它们表示负数?人们设计了多种方案,核心目的有两个:
假设我们用 4 位(4 bits) 来表示整数,可以表示 16 个不同的数值。
0 代表正数,1 代表负数。| 二进制 | 计算过程 | 数值 | 
|---|---|---|
| 0 111 | + (1+2+4) | +7 | 
| 0 110 | + (2+4) | +6 | 
| ... | ... | ... | 
| 0 001 | + (1) | +1 | 
| 0 000 | + (0) | +0 | 
| 1 000 | - (0) | -0 | 
| 1 001 | - (1) | -1 | 
| ... | ... | ... | 
| 1 110 | - (2+4) | -6 | 
| 1 111 | - (1+2+4) | -7 | 
+0 和 -0:浪费了一个编码空间。(+5) + (-3) 需要变成 |5| - |3| 的运算。为了解决原码的运算问题,反码出现。
| 十进制 | 正数原码 | 负数反码(按位取反) | 数值 | 
|---|---|---|---|
| +7 | 0 111 | 1 000 | -7 | 
| +6 | 0 110 | 1 001 | -6 | 
| +5 | 0 101 | 1 010 | -5 | 
| +4 | 0 100 | 1 011 | -4 | 
| +3 | 0 011 | 1 100 | -3 | 
| +2 | 0 010 | 1 101 | -2 | 
| +1 | 0 001 | 1 110 | -1 | 
| +0 | 0 000 | 1 111 | -0 | 
X - Y = X + (-Y)。
5 - 3 = 5 + (-3) = 0101 + 1100 = 1 0001。由于是4位系统,最高位溢出,需要回卷(End-around carry):0001 + 1 = 0010 (+2),结果正确。+0 和 -0 (0000 和 1111)。补码是反码的改进,彻底解决了 0 的问题。
1。| 十进制 | 正数原码 | -> 按位取反 | -> +1 (负数补码) | 数值 | 
|---|---|---|---|---|
| +7 | 0 111 | 1 000 | 1 001 | -7? | 
| +6 | 0 110 | 1 001 | 1 010 | -6? | 
| +5 | 0 101 | 1 010 | 1 011 | -5? | 
| +4 | 0 100 | 1 011 | 1 100 | -4 | 
| +3 | 0 011 | 1 100 | 1 101 | -3 | 
| +2 | 0 010 | 1 101 | 1 110 | -2 | 
| +1 | 0 001 | 1 110 | 1 111 | -1 | 
| 0 | 0 000 | 1 111 | (1)0000→0000 | 0 (唯一) | 
| -1 | 1 111 | -1 | ||
| -2 | 1 110 | -2 | ||
| -3 | 1 101 | -3 | ||
| -4 | 1 100 | -4 | ||
| -5 | 1 011 | -5 | ||
| -6 | 1 010 | -6 | ||
| -7 | 1 001 | -7 | ||
| -8 | 1 000 | -8 (没有原码和反码!) | 
(注意:+7 的补码计算 ~0111 -> 1000 + 1 -> 1001,但 1001 按照上表应该是 -7。这里是为了展示计算过程,实际上 -7 的补码就是 1001)
4位补码最终表示范围:-8 到 +7
1000 -> -81001 -> -71111 -> -10000 -> 00001 -> +10111 -> +70 的问题:0 只有一种表示 (0000)。5 - 3 = 5 + (-3) = 0101 + 1101 = (1)0010。直接丢弃溢出的最高位,得到 0010 (+2),结果正确。这就是几乎所有现代计算机都使用补码表示有符号整数的原因。
移码的思路完全不同:将所有数字统一加上一个偏移量 N,使得所有值在存储时都是非负的。
k 位,取 N = 2^(k-1) 或 2^(k-1)-1。| 真实值 | 存储值 (真实值 + 8) | 4位二进制 | 
|---|---|---|
| -8 | 0 | 0000 | 
| -7 | 1 | 0001 | 
| -6 | 2 | 0010 | 
| -5 | 3 | 0011 | 
| -4 | 4 | 0100 | 
| -3 | 5 | 0101 | 
| -2 | 6 | 0110 | 
| -1 | 7 | 0111 | 
| 0 | 8 | 1000 | 
| +1 | 9 | 1001 | 
| +2 | 10 | 1010 | 
| +3 | 11 | 1011 | 
| +4 | 12 | 1100 | 
| +5 | 13 | 1101 | 
| +6 | 14 | 1110 | 
| +7 | 15 | 1111 | 
(注意:这里为了和补码范围对比,用了 N=8。更常见的可能是 N=7,让范围在 -7 到 +8)
0000 (真实-8) < 1111 (真实+7)。| 表示法 | 核心思想 | 零的表示 | 优点 | 缺点 | 主要应用 | 
|---|---|---|---|---|---|
| 原码 | 符号位 + 绝对值 | ±0 | 对人类直观 | 运算复杂,有±0 | 极少,有时用于浮点数的尾数 | 
| 反码 | 正数不变,负数按位取反 | ±0 | 减法可变加法 | 有±0,需处理回卷进位 | 历史阶段,已被补码取代 | 
| 补码 | 正数不变,负数取反+1 | 唯一 | 运算简单,无±0,范围大 | 对人类不直观 | 所有现代计算机的有符号整数 | 
| 移码 | 真实值 + 固定偏移量 | 唯一 | 比较大小极其方便 | 加减运算不方便 | 浮点数的指数部分 | 
最终的结论是:
“反码”这个概念并不是凭空冒出来的,它的诞生是逻辑演进的必然结果,是为了解决一个非常具体且棘手的问题:如何让计算机用最简单的加法器来做减法?。这是一个触及计算机科学历史根源的精彩问题。
早期的计算机硬件非常昂贵和复杂。设计者有一个执念:能否只用加法器这一种电路,同时完成加法和减法运算?
X - Y = X + (-Y)。如果能找到一个表示 -Y 的方法,使得 X + (-Y) 能得到正确结果,那么减法电路就可以被淘汰,极大简化CPU设计。原码无法做到这一点,因为它的符号位需要特殊处理。
在计算机出现之前,机械计算器(如手摇计算器)和人们心算中就已经广泛使用“补数”的概念来简化减法。
最经典的例子是时钟(模运算系统):
X = 10),要减去 4 小时 (Y = 4)。10 - 4 = 6。10 + (12 - 4) = 10 + 8 = 18。18 mod 12 = 6。Y,等价于加上它的“补数” (12 - Y)。这个 (12 - Y) 就是 -Y 在这个系统里的等价物!
计算机的 n 位二进制系统,就是一个模 2^n 的系统。比如 4 位系统,模是 16 (10000)。
X - Y,可以转化为 X + (M - Y),然后对结果取模。X - Y = X + (16 - Y),然后舍弃最高位的进位(相当于 mod 16)。但是,(16 - Y) 的计算本身似乎又是一个减法? 这看起来并没有简化问题。
这时,一个绝妙的观察出现了:
对于二进制数来说,(2^n - 1) - Y 这个计算极其简单!
(2^n - 1) 是一个所有位都是 1 的数。例如,4 位系统中,2^4 -1 = 15,即 1111。(1111 - Y) 的操作,就是将 Y 的每一位二进制位取反!(因为 1-0=1, 1-1=0)。
Y = 3 (0011),1111 - 0011 = 1100。而 1100 正好是 0011 的按位取反。所以:
(2^n - 1) - Y = ~Y (Y 的反码)
现在我们有了:
X - Y = X + (16 - Y) = X + [(16 - 1) - Y] + 1 = X + (~Y) + 1
这个 X + (~Y) + 1 就是现代补码的运算方式。但早期的设计者先看到了 X + (~Y) 这一步。
他们发现,如果先计算 X + (~Y),结果已经非常接近正确答案了:
X + (~Y) = X + (15 - Y) = (X - Y) + 15(X-Y) 多了一个 15。于是,天才的想法诞生了:既然结果多了一个 15(即 2^n -1),那只要再把多出来的这个 15 减掉就行了!
减去15就等于加上1,因为在模为16的计算中,效果如此,你再想想时钟的运算。
但是如何减?通过进位回卷(End-around carry):
X + (~Y)。15 已经被“包”在这个进位里了。Result = (X + ~Y) + carry。carry = 1
这个过程就是反码的运算规则。
所以,反码的发明思路可以概括为以下几步:
X - Y = X + (-Y))。-Y 可以用 (M - Y) 表示。(2^n - 1 - Y) 的计算就是简单的按位取反(~Y)。X + (~Y),发现结果与正确答案差一个固定值 (2^n -1)。X + (~Y) 产生进位,说明和已经“溢出”了一圈,把这个溢出的进位再加到最低位,就相当于减去了 (2^n -1),从而得到正确结果。6 - 3 (4位系统)+3 的原码/反码都是 0011。-3 的反码是 1100(按位取反)。6 + (-3的反码):0110 + 1100 = 1 0010。(产生了进位 1,结果是 0010)1 加回到结果的最低字节:0010 + 1 = 0011 (+3)。6 - 3 = 3。这个能通过简单“取反”操作得到负数,并能用加法器进行减法运算的表示法,就被命名为“反码(Ones' Complement)”。
反码是一个伟大的过渡性思想。它几乎成功了,它实现了用加法做减法的目标。但它最大的遗产是引导人们发现了最终的完美方案:既然 X + (~Y) 已经很接近,只需要再加一个 1 就能得到绝对正确的结果,为什么不直接把 -Y 定义为 ~Y + 1 呢?
这个 ~Y + 1 就是补码(Two's Complement)。补码彻底抛弃了繁琐的“回卷进位”,使得运算更加简单直接,并且解决了 ±0 的问题。
所以,反码是想出来的一个非常聪明但略显繁琐的解决方案,而补码则是在反码基础上想出来的一个更优雅、更完美的终极方案。没有反码的探索,很可能就没有补码的最终确立。
 
                     
                            995M · 2025-10-31
 
                            90.9M · 2025-10-31
 
                            478M · 2025-10-31
