ASCII 的优雅:为什么小写字母不紧随大写字母之后
当第一次看到 ASCII 表时,会注意到一个奇怪的空隙。大写字母 Z(十进制 90)之后,并不是直接出现小写字母 a(十进制 97),而是出现了六个字符:[, \\, ], ^, _ 和 `。对随意观察者来说,这似乎是一堆随机符号打断了字母表。但这并非偶然,而是有意的设计选择,旨在简化计算机处理文本的方式。
二的幂的力量
计算机处理二进制数据,ASCII(American Standard Code for Information Interchange)的设计者针对当时的硬件对编码进行了优化。理解 Z 与 a 之间空隙的关键在于数字 32——一个二的幂($2^5$)。
英文字母共有 26 个字母。通过在大写字母集合和小写字母集合之间加入 6 个额外字符,设计者使得任意大写字母与其对应的小写字母之间恰好相差 32 个码位。
比较二进制表示
如果我们查看 'A' 与 'a' 的二进制表示,模式就很清晰了:
| 十进制 | 二进制 | 符号 |
|---|---|---|
| 65 | 01000001 |
A |
| 97 | 01100001 |
a |
| 66 | 01000010 |
B |
| 98 | 01100010 |
b |
| 67 | 01000011 |
C |
| 99 | 01100011 |
c |
在每一对对应的字符中,唯一的差别是 第 5 位(代表 $2^5$,即 32)。第 5 位为 0 时为大写字母,为 1 时为小写字母。
大小写转换的位运算技巧
由于大小写差异仅体现在单个位上,开发者可以使用极其快速的位运算来完成大小写转换,而不必依赖复杂的条件判断或查找表。
转换为大写
要强制字符变为大写,可以对字符进行位与(AND)操作,使用 32 的位非(NOT)作为掩码。这实际上会清除第 5 位,而保持其他位不变。
步骤 1:创建掩码
~ 00100000 (32) → 11011111
步骤 2:对 'a' 应用掩码
01100001 ('a') & 11011111 → 01000001 ('A')
转换为小写
转换为小写更为简单。对字符进行位或(OR)操作并加上 32,即可确保第 5 位被设为 1,无论它原本是否已经是 1。
01000001 ('A') | 00100000 (32) → 01100001 ('a')
切换大小写
如果需要翻转字符的大小写(大写转小写或小写转大写),只需对字符进行位异或(XOR)并加上 32,即可切换第 5 位。
01100001 ('a') ^ 00100000 (32) → 01000001 ('A')
01000001 ('A') ^ 00100000 (32) → 01100001 ('a')
确定字母在字母表中的位置
除了大小写转换,这种结构还提供了一种快速获取字母在字母表中位置(A 为 1,B 为 2,依此类推)的方法。对字符进行位与(AND)操作并使用 31(二进制 00011111),即可清除高位,只保留低五位。
这在数学上等价于 character % 32,因为 32 是二的幂。
'A'(65) → $65 \pmod{32} = 1$'Z'(90) → $90 \pmod{32} = 26$'a'(97) → $97 \pmod{32} = 1$'z'(122) → $122 \pmod{32} = 26$
这种设计反映了当时每个 CPU 周期都极其宝贵的时代。通过让字母表与二进制边界对齐,ASCII 的设计者确保了常见的文本操作能够以最高效率完成。