第十四夜 · 幻方与奇妙的二进制
上一夜的信寄出去之后,我等到了回信。天真的我只回了一句话,你说幻方住在机器的肚子里,那机器的肚子,究竟长什么样。
这个问题问到我心里了。要回答它,得先办一件事,买一张王国的国籍。王国的名字叫比特。
比特王国的国民只有一种,名叫比特。它们低调到近乎简陋,一生只有两种表情,亮,或者灭。亮,人们记作 1;灭,记作 0。一个比特独自生活时几乎一无是处,它连十都数不到,只能回答是与否。
但王国自开国的第一天起就立下铁律,任何单个比特都不许独自承担重任。八位比特结为一组,同进同出,同亮同灭,这一组,王国称之为一个字节。八盏灯,二百五十六种亮灭的组合,从全灭的 0,到全亮的 255。一个字节恰好能表达 0 到 255 之间的任何一个整数。
王国的地理更简单,也更惊人。全部的字节排成一条直线,一条一眼望不到头的长街。街上每一个字节都有自己的门牌号,门牌号从 0 开始。你要找第 100 个字节,沿着长街走 100 步就到,一步不多,一步不少。一台机器有多少内存,王国就有多长的街。内存从来不是一个抽象名词,它真的就是这样一条街,街边站满了字节。
天真的我,读到这里你大概已经猜到了。我们的幻方,就住在这条街上。
先别急着去看它。数字客人进城,得先办入住。
数字 100 来了。它摊开自己,二进制 01100100,不多不少,八位。八位住一个字节,刚刚好。前台服务员点亮八盏灯,灭亮亮灭灭亮灭灭,再把房卡递过去。房卡上写着两张速记符号,6 和 4。你先记住这张房卡,王国的速记法,下一夜细说。

数字 33554432 也来了。它摊开自己,三十二位。八盏灯一间房,它得开四连间套房。
麻烦就出在套房上。
四个房间并排,门牌相连。33554432 的三十二盏灯,要分放四个房间。可是谁住第一间?
王国为这个问题打过一场内战。史书把交战双方记作大头党与小头党,这场战争的学名,今天的人称为字节序,俗称大小端。
要讲这场战争,得先讲一则更老的童话。三百年前,斯威夫特写《格列佛游记》。小人国有一条祖训,吃鸡蛋要从大头敲开。直到一位皇帝小时候照祖训敲蛋,割伤了手指,他的父亲,也就是当时的皇帝,颁下敕令,全国臣民从此一律从小端敲蛋,违者重罚。大头党不肯就范,为此爆发六次叛乱,一位皇帝丧命,一位皇帝失位,敌国不来夫斯库在暗处煽风点火,战败的大头党流亡海外,被不来夫斯库收留。斯威夫特写的哪里是鸡蛋,他写的是那个时代打着教义旗号的党争。

1980 年 4 月 1 日,愚人节,计算机科学家丹尼·科恩提笔写下一篇论文,标题就叫《圣战与和平呼吁》。他把小人国的两个党派名字郑重其事借了过来。一个多位数住进连间套房,高位字节住头一间房的,称为大端;低位字节住头一间房的,称为小端。论文的开篇写着,我写这篇文章,是为了阻止一场战争。
因为这不是对错之争,是党争。两党各自效忠一种真实的传统。
大端是写的传统。人在纸上写数,从高位写起,千位、百位、十位、个位。大端的内存布局与写法一致,把内存原样打印出来,人眼读到的顺序和纸面一模一样,查起来舒服。
小端是算的传统。你在竖式里算加法,永远个位对齐,从低位算起,进位往高位走。小端把低位字节放在低门牌,进位恰好沿着地址向上传播;多字节的数字要变大,就往高地址生长。算惯了大数的人,觉得这种对位亲如乡音。
这一夜我不背史书,我带你亲手敲一次蛋。Java 有一件现成的行李,叫 ByteBuffer,能替我们把数字装进四连间,装法由我们指定。下面这段程序,把幻方的第一行 8、1、6 依次装进小端套房,再把 100 和 33554432 也请进去,然后把每一间的灯谱原样打印出来。
java
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
public class Night14BitKingdom {
public static void main(String[] args) {
int[] row = {8, 1, 6}; // 第十三夜幻方第一行
for (int v : row) {
System.out.println(v + " -> " + lights(v));
}
System.out.println(100 + " -> " + lights(100)); // 100 的单间
System.out.println(33554432 + " -> " + lights(33554432)); // 0x02000000 的四连间
}
static String lights(int v) {
ByteBuffer buf = ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN);
buf.putInt(v); // 按小端装入四连间套房
StringBuilder sb = new StringBuilder();
for (byte b : buf.array()) {
sb.append(String.format("%02X ", b)); // 每间房一张两位速记房卡
}
return sb.toString().trim();
}
}
实跑输出如下。
8 -> 08 00 00 00
1 -> 01 00 00 00
6 -> 06 00 00 00
100 -> 64 00 00 00
33554432 -> 00 00 00 02
8、1、6 都是小数字,住小端套房时,头一间房自己扛着全部家当,08、01、06,后面三间全空。100 的房卡是 64,单拿首间房当字符读,是一个字母 d,ASCII 第 100 号恰好就是它。可 33554432 一进来,前三个房间全空,家当全压在最后一间,房卡上只有一个 02。写派的人路过,从左往右念,00 00 00 02,念出一个 2 来。真身是三千三百多万,念出来是 2。对位错一头,数值谬以千里。你以为的顺序,只是你写字的手习惯了的方向;在机器的长街上,方向是产权问题,不是审美问题。

内战的结局,也值得每一个进城的人知道。和平条约分三层。第一问,统一到哪边?两党都举着常识的旗帜,圣战因此打得起来,这一问本身就没有答案。第二层,接口处其实早就统一了,互联网上机器交换数据,统一按大端报数,两位翻译官 htons 与 htonl 常年驻在端口。第三层,硬件处换不起,机器可以换代,可已经写进磁盘、要流传几十年的数据,读法不敢动。所以科恩的和平呼吁没有判胜负,他判的是划界而治。
至于你的机器为什么永远输出小端,那不是微型机的天性,是 x86 打赢了个人电脑的江山。个人电脑的历史上大端也曾称王,经典 Macintosh 用的是摩托罗拉的芯片,全族大端。今天的手机芯片两派方言都会讲,只是和平年代,大家默认用小端开口。你看见的永远,是战争结束之后的版图。
天真的我,现在可以回答你的问题了。我们的幻方住在那条长街上,8 住进 08 00 00 00 的套房,6 住在它隔壁的隔壁,每一盏灯都还亮着我们摆放那天的样子。你当年在黑泽没看懂的九宫格,如今连它睡着的样子,我们都数得一清二楚。
下一夜,讲讲那张房卡。速记符号 6 和 4 是怎么来的,王国为什么偏要用一门别扭的外语做门牌。速记官的故事,比你想的更长。
灯不会说谎,门牌从不撒谎。所谓内存,就是一条替你记住每盏灯的长街。
① 真伪核对 逐条照实登记。
Ⅰ 格列佛敕令 《格列佛游记》第一卷第四章,小人国皇帝幼时按祖训从大端破蛋割伤手指,其父颁敕令令全国从小端破蛋,违者重罚;由此爆发六次叛乱,一位皇帝丧命、一位失位;不来夫斯库国王煽动叛乱,大头党流亡该国受庇(真,据原著)。
Ⅱ 讽喻 斯威夫特以鸡蛋之争讽教义与党派圣战(真)。
Ⅲ 科恩论文 丹尼·科恩《On Holy Wars and a Plea for Peace》落款 1980 年 4 月 1 日愚人节,时属 USC 信息科学研究所,编号 IEN 137;修改版刊 IEEE Computer 1981 年 10 月;大小端两词借自《格列佛游记》;开篇自陈为阻止战争而写(真,已联网核查)。
Ⅳ 字节与阵营 一字节=8 比特;Java 的 int 占 4 字节;x86 全系小端;网络字节序统一大端,htons 与 htonl 为其翻译函数;摩托罗拉 68k 芯片的经典 Macintosh 为大端,PowerPC 在 Mac 上传统大端;ARM 两派方言兼有、现实默认小端(真,已联网核查)。
Ⅴ ASCII 第 100 号字符为 d,十六进制速记 64(真)。
Ⅵ 演示件 ByteBuffer 指定小端装入,实跑输出 08、01、06、64、00 00 00 02 五行,与文中一致(真,实跑验证)。
Ⅶ 幻方第一行 8、1、6(真,第十三夜实跑在案)。
② 今夜习题 三道,评论区见。
Ⅰ 灯谱。写出 2026 的二进制灯谱,数一数一共亮了几盏灯。
Ⅱ 幻方第二行。幻方第二行是 3、5、7,写出这三个数字各自套房的房卡,再画出它们的灯谱。
Ⅲ 念错的代价。把 00 00 00 02 按大端的习惯念一遍,得到哪个数?再想一件事,什么场景下,这种念错会从一个玩笑变成一场事故。
本书目录导航 上一篇:第十三夜 幻方,武侠世界里的数学迷宫 下一篇:第十五夜 一门别扭的外语 起点:卷首语·算法童话
版权声明:本文为作者原创,受著作权法保护。未经授权禁止转载、搬运、摘编、改编及任何形式的二次创作;个人学习引用请注明作者与原文出处。转载授权请联系作者(CSDN 私信)。侵权必究。