qinghuan
← 回到知识探索

计算机趣闻 / 动手实验

1 加 1 写成 10,进位怎样接着走?

先分开本位结果与进位,再看半加器为什么还不够处理每一列。

展开本文目录
  1. 先动手看看
  2. 一次相加,要留两个出口
  3. 到了下一列,还会多来一个数
  4. 把两列接起来,算 11 加 01
  5. 先检查三输入,再看整条连接
  6. 记住这三点
  7. 别把例子用过头
  8. 自己试一小步
  9. 查原始资料
  10. 找一份资料继续练
开始前,会这些就够了
  • 知道二进制只使用 0 和 1
  • 认识与、或;代码需 Node.js 20 或更新版本

十进制里,个位装不下十就向左进一。二进制每一位只能写 0 或 1,所以 1 加 1 要写成 10。这个 10 表示二,不是十;最右边留下 0,左边多出一个进位。电路怎样把这两件事同时报告出来?

从右往左,把进位传过去

每个输入只有两位。先算最右列,再把它的进位交给左边一列;结果额外保留一位。

  1. 右列:1 + 1留下 0,向左进 1
  2. 左列:1 + 0 + 1收到右列进位 1;留下 0,再向左进 1

完整二进制结果:11 + 01 = 100

用十进制核对:3 + 1 = 4。三个结果位从左往右是:最后进位 1、左列结果 0、右列结果 0。

只演示两位无符号数的逐列相加;最前面的 0 也保留,以便看清每个输出的位置。这里不判断有符号数溢出,也不模拟电路传播时间。

一次相加,要留两个出口

先只加两个各占一位的数 a、b。00 的结果是 00,01 和 10 的结果都是 01,11 的结果是 10。把右边一位叫作 sum,也就是本位和;把左边一位叫作 carry,也就是向更高位置送出的进位。

负责这件事的部件叫半加器。它并非只算了一半的答案,而是只接收两个输入,没有接收前一列进位的入口。检查刚才四种情况会发现:sum 在两个输入不同时为 1,这叫异或;carry 只在二者都为 1 时出现,正好是与。

两个输出合起来表达的数量是“进位乘二,再加本位和”。这是核对结果的另一把尺子:不管函数内部怎样连接,这个数量都应等于两个输入之和。进位虽然也只有一位,位置更高,代表的份量却不同。

到了下一列,还会多来一个数

计算多位数时,高一列除了自己的 a、b,还可能收到低一列送来的 carryIn。例如三个输入都是 1,总量就是三,写成二进制是 11,所以本位和与输出进位都为 1。只计算 a 和 b 会漏掉那个传来的量。

全加器因此有三个一位输入和两个一位输出。可以先用一个半加器算 a、b,再用另一个半加器把所得本位和与 carryIn 相加。两次产生的进位用“或”合在一起;在这些合法输入下,两处不会同时产生进位。

把两列接起来,算 11 加 01

从最右边开始,1 加 1 得到本位 0、进位 1。再看左边,本来是 1 加 0,还要接收刚才的进位,得到本位 0、进位 1。最后从左往右读三个输出,才得到完整结果 100。进位不是重复相加,而是把装不下的量移到更高一位。

下面的 add2 专门接收 0 到 3 的两个整数,把它们当作两位无符号数。最低列没有外来进位,所以用半加器;高一列用全加器。返回值刻意保留三个二进制字符,避免把最左边的进位悄悄丢掉。

先检查三输入,再看整条连接

代码中的 ^ 是按位异或,& 是按位与,| 是按位或;这里参与门运算的值都限定为 0 或 1。第一段输出把全加器的八种输入全部列出,列名 in 表示输入进位。不要把打印顺序 sum、carry 误读成二进制数的高位、低位顺序。

这是一种容易解释的逐列连接,并不代表所有处理器都按同样的电路安排加法。真实设计还会考虑进位传播时间。本实验也没有讲有符号数溢出;若只保存固定的两位,就必须另行规定超过范围时怎样处理,不能把少掉的进位说成没发生。

检查完整连接时,也不能只挑有进位的例子。两个输入各有四种取值,总共十六种配对;没有进位、只从低列进位以及最后再进位,都在里面。把短输出转换回整数比较,能帮助找到接错列的位置。

小实验 · javascript
function half(a, b) {
  return { sum: a ^ b, carry: a & b };
}
function full(a, b, carryIn) {
  const first = half(a, b);
  const second = half(first.sum, carryIn);
  return { sum: second.sum, carry: first.carry | second.carry };
}
function add2(a, b) {
  const low = half(a & 1, b & 1);
  const high = full((a >> 1) & 1, (b >> 1) & 1, low.carry);
  return `${high.carry}${high.sum}${low.sum}`;
}

console.log("a b in | sum carry");
for (const a of [0, 1]) {
  for (const b of [0, 1]) {
    for (const c of [0, 1]) {
      const result = full(a, b, c);
      console.log(a, b, c, "|", result.sum, result.carry);
    }
  }
}
console.log("11 + 01 =", add2(3, 1));
console.log("11 + 11 =", add2(3, 3));

怎样运行:保存为 add-bits.js,运行 node add-bits.js。使用 Node.js 20 或更新版本;half/full 的输入为单个位,add2 的输入为 0 到 3。

运行后对照结果
a b in | sum carry
0 0 0 | 0 0
0 0 1 | 1 0
0 1 0 | 1 0
0 1 1 | 0 1
1 0 0 | 1 0
1 0 1 | 0 1
1 1 0 | 0 1
1 1 1 | 1 1
11 + 01 = 100
11 + 11 = 110

记住这三点

  • 半加器加两个位;全加器还接收输入进位。
  • 本位和与输出进位必须分开保存。
  • 多位相加需要把低位进位接到高位。

别把例子用过头

  • 半加器也有进位输出,只是没有进位输入。
  • 无符号进位不等于有符号数溢出判定。

自己试一小步

检查 add2 对所有两位无符号输入是否都算对。

  1. 列出 0 到 3 的所有输入配对。
  2. 把输出按二进制读回整数。
  3. 与普通整数相加比较,保留差异。

做到这些,就算完成

  • 没有漏掉任一配对。
  • 保留最左侧进位。
  • 说明函数允许的输入范围。

查原始资料

想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。

找一份资料继续练

先选一项,按下面的范围继续学;不必从头读完整门课程。