cpp-负整数如何转换成无符号整数?-反码和移码的发现

你有没有想过,负整数如何转换成无符号整数?

在 C++ 中,将一个负的有符号整数转换为无符号整数有一套明确的规则。

核心转换规则

转换遵循一个基本原则:模运算(Modular Arithmetic)

无符号类型的取值范围是一个从 0MAX(最大值)的环。当你试图赋予它一个超出这个范围的值时,结果会是 MAX + 1(这个环的大小)取模后的值 ,即 原数字 % (MAX + 1),负数的位模式被直接解释为无符号数。

负数转换后的结果 = 无符号类型的最大值 (MAX) + 原始负数值 + 1


详细步骤与例子

让我们用最常见的场景来说明:将 -1 转换为 unsigned int

  1. 确定 unsigned int 的最大值 (MAX) 对于一个 32 位的 unsigned int,其最大值是: MAX = 2³² - 1 = 4,294,967,295 这个值也通常写作 0xFFFFFFFF(十六进制)。

  2. 应用模运算规则 转换公式为:Result = (-1) % (MAX + 1) 因为 -1 是负数,我们需要让它变成正数模。等价的做法是: Result = MAX + (-1) + 1

  3. 计算结果 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

  • 32位 int -1 的位模式:11111111 11111111 11111111 11111111
  • 32位 unsigned int 的位模式:11111111 11111111 11111111 11111111

关键点: 转换过程不会改变数据的底层位模式 。编译器只是换了一种方式去解释这串相同的二进制位。

  • 有符号解释 :如果把这串 1111...1111 解释为 int(二进制补码),它的值就是 -1
  • 无符号解释 :如果把这完全相同 的位模式解释为 unsigned int,它的值就是 2³² - 1 = 4,294,967,295

这种"重新解释"是转换的本质。


更多示例(假设 32 位系统)

有符号值 (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 为负数时)


代码验证

你可以写一段简单的代码来验证这个转换:

cpp 复制代码
#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;
}

运行结果将会是:

objectivec 复制代码
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 的根源,尤其是在循环和条件判断中混合使用有符号和无符号类型时。

危险的例子:

cpp 复制代码
#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!

总结

  1. 规则 :负的有符号整数转换为无符号整数时,通过模 MAX + 1 运算得到一个很大的正数。
  2. 本质:转换不改变底层比特位,只是改变了编译器解释这些比特位的方式。
  3. 结果(unsigned int) -1 等于 UINT_MAX(例如 4,294,967,295)。
  4. 警示尽量避免在代码中混合使用有符号和无符号类型 ,这是现代 C++ 强调的最佳实践。使用 static_cast<unsigned int>(some_int) 进行显式转换,以明确你的意图。

原码,反码,和补码概念区分

为什么需要这些码?

计算机底层只能存储 01。如何用它们表示负数?人们设计了多种方案,核心目的有两个:

  1. 表示负数
  2. 让硬件运算(尤其是加法)变得简单

假设我们用 4 位(4 bits) 来表示整数,可以表示 16 个不同的数值。


1. 原码 (Sign-Magnitude)

定义

  • 最高位(最左边的位)为符号位0 代表正数,1 代表负数。
  • 其余位为数值位:表示该数的绝对值。

4位原码示例:

二进制 计算过程 数值
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

优缺点

  • 优点 :对人类来说非常直观。 (对人重要,但对计算机没有意义!!
  • 缺点
    1. 存在 +0-0:浪费了一个编码空间。
    2. 运算复杂 :CPU 需要先判断符号位,然后决定做加法还是减法,电路设计非常麻烦。
      • (+5) + (-3) 需要变成 |5| - |3| 的运算。

2. 反码 (Ones' Complement)

定义

为了解决原码的运算问题,反码出现。

  • 正数:其反码表示与原码相同。
  • 负数 :将其正数的所有位按位取反(包括符号位)。

4位反码示例:

十进制 正数原码 负数反码(按位取反) 数值
+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),结果正确。
  • 缺点
    1. 依然存在 +0-0 (00001111)。
    2. 需要处理回卷进位,电路设计仍然不够优雅。

3. 补码 (Two's Complement) - 现代计算机标准

定义

补码是反码的改进,彻底解决了 0 的问题。

  • 正数:其补码与原码、反码相同。
  • 负数 :将其正数的所有位按位取反,然后再加 1

4位补码示例:

十进制 正数原码 -> 按位取反 -> +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)00000000 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 -> -8
  • 1001 -> -7
  • ...
  • 1111 -> -1
  • 0000 -> 0
  • 0001 -> +1
  • ...
  • 0111 -> +7

为什么补码是完美的?

  1. 解决了 0 的问题0 只有一种表示 (0000)。
  2. 表示范围更广:能多表示一个数(-8)。
  3. 运算最简单加法器无需任何修改,即可直接用于有符号数加法! 无需判断符号,无需回卷进位。
    • 5 - 3 = 5 + (-3) = 0101 + 1101 = (1)0010。直接丢弃溢出的最高位,得到 0010 (+2),结果正确。

这就是几乎所有现代计算机都使用补码表示有符号整数的原因。


4. 移码 (Excess-N / Offset-Binary)

定义

移码的思路完全不同:将所有数字统一加上一个偏移量 N,使得所有值在存储时都是非负的

  • 存储值 = 真实值 + 偏移量 (N)
  • 通常,对于 k 位,取 N = 2^(k-1)2^(k-1)-1

4位移码示例(假设偏移量 N = 8):

真实值 存储值 (真实值 + 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)。
  • 应用
    • 主要用于浮点数的指数部分(IEEE 754 标准)。因为浮点数需要频繁比较指数大小。
    • 极少用于表示通用的整数,因为加减法运算不方便(需要先减去偏移量)。

总结对比

表示法 核心思想 零的表示 优点 缺点 主要应用
原码 符号位 + 绝对值 ±0 对人类直观 运算复杂,有±0 极少,有时用于浮点数的尾数
反码 正数不变,负数按位取反 ±0 减法可变加法 有±0,需处理回卷进位 历史阶段,已被补码取代
补码 正数不变,负数取反+1 唯一 运算简单,无±0,范围大 对人类不直观 所有现代计算机的有符号整数
移码 真实值 + 固定偏移量 唯一 比较大小极其方便 加减运算不方便 浮点数的指数部分

最终的结论是:

  • 如果你想理解计算机如何做整数运算 ,必须彻底掌握补码
  • 如果你想理解浮点数如何表示 ,需要额外了解移码
  • 原码和反码主要是为了理解补码的演进历史。

反码怎么被发现的?

"反码"这个概念并不是凭空冒出来的,它的诞生是逻辑演进的必然结果,是为了解决一个非常具体且棘手的问题:如何让计算机用最简单的加法器来做减法?。这是一个触及计算机科学历史根源的精彩问题。

1. 核心要解决的痛点:简化硬件

早期的计算机硬件非常昂贵和复杂。设计者有一个执念:能否只用加法器这一种电路,同时完成加法和减法运算?

  • 用加法器做加法:天经地义。
  • 用加法器做减法X - Y = X + (-Y)。如果能找到一个表示 -Y 的方法,使得 X + (-Y) 能得到正确结果,那么减法电路就可以被淘汰,极大简化CPU设计。

原码无法做到这一点,因为它的符号位需要特殊处理。

2. 灵感的来源:机械计算的"补数"思想

在计算机出现之前,机械计算器(如手摇计算器)和人们心算中就已经广泛使用"补数"的概念来简化减法。

最经典的例子是时钟(模运算系统)

  • 现在时针指向 10 点 (X = 10),要减去 4 小时 (Y = 4)。
  • 直接减:10 - 4 = 6
  • 用加法"补"10 + (12 - 4) = 10 + 8 = 18
  • 18 点就是下午 6 点,因为时钟是模 12 系统,18 mod 12 = 6
  • 我们发现,在模12的系统里,减去一个数 Y,等价于加上它的"补数" (12 - Y)

这个 (12 - Y) 就是 -Y 在这个系统里的等价物!

3. 从"模"思想到"反码"

计算机的 n 位二进制系统,就是一个2^n 的系统。比如 4 位系统,模是 16 (10000)。

  • 根据时钟的灵感,要计算 X - Y,可以转化为 X + (M - Y),然后对结果取模。
  • 对于 4 位系统: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 = ~YY 的反码)

现在我们有了: 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)

  1. 先计算 X + (~Y)
  2. 如果计算产生了进位(即结果 >= 16),说明这个多出来的 15 已经被"包"在这个进位里了。
  3. 把这个进位再从最低位加回去Result = (X + ~Y) + carry

carry = 1

这个过程就是反码的运算规则。

4. 反码的构想总结

所以,反码的发明思路可以概括为以下几步:

  1. 目标 :用加法器做减法 (X - Y = X + (-Y))。
  2. 借鉴 :从模运算(如时钟)中获得灵感,-Y 可以用 (M - Y) 表示。
  3. 发现 :在二进制中,(2^n - 1 - Y) 的计算就是简单的按位取反~Y)。
  4. 妥协 :先计算 X + (~Y),发现结果与正确答案差一个固定值 (2^n -1)
  5. 解决方案 :通过回卷进位 的技巧来修正这个误差。如果 X + (~Y) 产生进位,说明和已经"溢出"了一圈,把这个溢出的进位再加到最低位,就相当于减去了 (2^n -1),从而得到正确结果。

一个简单的例子:6 - 3 (4位系统)

  1. +3 的原码/反码都是 0011
  2. -3反码1100(按位取反)。
  3. 计算 6 + (-3的反码)0110 + 1100 = 1 0010。(产生了进位 1,结果是 0010
  4. 回卷进位 :将进位 1 加回到结果的最低字节:0010 + 1 = 0011 (+3)。
  5. 结果正确:6 - 3 = 3

这个能通过简单"取反"操作得到负数,并能用加法器进行减法运算的表示法,就被命名为"反码(Ones' Complement)"。

历史地位

反码是一个伟大的过渡性思想 。它几乎成功了,它实现了用加法做减法的目标。但它最大的遗产是引导人们发现了最终的完美方案:既然 X + (~Y) 已经很接近,只需要再加一个 1 就能得到绝对正确的结果,为什么不直接把 -Y 定义为 ~Y + 1 呢?

这个 ~Y + 1 就是补码(Two's Complement) 。补码彻底抛弃了繁琐的"回卷进位",使得运算更加简单直接,并且解决了 ±0 的问题。

所以,反码是想出来的一个非常聪明但略显繁琐的解决方案 ,而补码则是在反码基础上想出来的一个更优雅、更完美的终极方案。没有反码的探索,很可能就没有补码的最终确立。

相关推荐
自由生长20244 小时前
cpp-string::size_type的作用
c++
Jooolin5 小时前
大名鼎鼎的哈希表,真的好用吗?
数据结构·c++·ai编程
爱和冰阔落5 小时前
C++ 模板初阶:从函数重载到泛型编程的优雅过渡
开发语言·c++
杰 .6 小时前
c++二叉搜索树
数据结构·c++
咔咔咔的6 小时前
3000. 对角线最长的矩形的面积
c++
ShineWinsu8 小时前
对于牛客网—语言学习篇—编程初学者入门训练—复合类型:BC136 KiKi判断上三角矩阵及BC139 矩阵交换题目的解析
c语言·c++·学习·算法·矩阵·数组·牛客网
可可睡着辽12 小时前
C++链表双杰:list与forward_list
c++·链表·list
Jayden_Ruan14 小时前
C++计算正方形矩阵对角线和
数据结构·c++·算法
李白同学14 小时前
C++:list容器--模拟实现(下篇)
开发语言·数据结构·c++·windows·算法·list