开始前,会这些就够了
- 知道二进制只使用 0 和 1
- 认识与、或;代码需
Node.js20 或更新版本
十进制里,个位装不下十就向左进一。二进制每一位只能写 0 或 1,所以 1 加 1 要写成 10。这个 10 表示二,不是十;最右边留下 0,左边多出一个进位。电路怎样把这两件事同时报告出来?
从右往左,把进位传过去
每个输入只有两位。先算最右列,再把它的进位交给左边一列;结果额外保留一位。
右列:1 + 1留下 0,向左进 1左列: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 误读成二进制数的高位、低位顺序。
这是一种容易解释的逐列连接,并不代表所有处理器都按同样的电路安排加法。真实设计还会考虑进位传播时间。本实验也没有讲有符号数溢出;若只保存固定的两位,就必须另行规定超过范围时怎样处理,不能把少掉的进位说成没发生。
检查完整连接时,也不能只挑有进位的例子。两个输入各有四种取值,总共十六种配对;没有进位、只从低列进位以及最后再进位,都在里面。把短输出转换回整数比较,能帮助找到接错列的位置。
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 对所有两位无符号输入是否都算对。
- 列出 0 到 3 的所有输入配对。
- 把输出按二进制读回整数。
- 与普通整数相加比较,保留差异。
做到这些,就算完成
- 没有漏掉任一配对。
- 保留最左侧进位。
- 说明函数允许的输入范围。
查原始资料
想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。
- Project 2: Boolean Arithmetic (新标签页)↗
支持半加器、全加器、多位加法器的学习顺序和输入输出检查。
- The Elements of Computing Systems:Chapter 2 Boolean Arithmetic (新标签页)↗
支持半加器与全加器的输入、输出、真值表,两个半加器组合全加器及逐列传递进位。
找一份资料继续练
先选一项,按下面的范围继续学;不必从头读完整门课程。
NandGame:从一个逻辑门开始搭电脑 (新标签页)↗
只练前面的逻辑门组合,逐行核对输入与输出,再观察多个小组件怎样接成一个更大的计算步骤。
查看学习准备与访问说明 →