为什么 ASCII 中小写字母没有紧跟在大写字母后面?
摘要
本文解释了 ASCII 表的设计,特别是为什么大写和小写字母之间存在间隔,并说明了这种结构如何通过位运算实现高效的字母大小写转换。
<p><a href="https://lobste.rs/s/lecxsm/why_don_t_lowercase_letters_come">评论</a></p>
查看缓存全文
缓存时间: 2026/05/08 09:28
# 为什么 ASCII 中小写字母没有紧跟在大写字母后面?—— Tyler Hillery
来源:https://tylerhillery.com/blog/why-dont-lowercase-chars-come-after-upper/
我终于想明白了。查看 ASCII 表时,你会发现大写 `Z` 之后、小写 `a` 之前还有几个字符:
| 十进制 | 二进制 | 符号 |
|--------|--------|------|
| 88 | 01011000 | X |
| 89 | 01011001 | Y |
| 90 | 01011010 | Z |
| 91 | 01011011 | [ |
| 92 | 01011100 | \ |
| 93 | 01011101 | ] |
| 94 | 01011110 | ^ |
| 95 | 01011111 | _ |
| 96 | 01100000 | ` |
| 97 | 01100001 | a |
字符用数字表示,这很合理,因为计算机只会存储和处理数字。所以需要某种编码将数字映射到字符。ASCII 是最早的字符编码方案之一,但它只用了 7 位,只能表示 128 个码点:\(2^7\)。这对于人类使用的所有字符来说远远不够,尤其是考虑到中文这样拥有数万个字符的语言。
如今我们使用 Unicode 作为标准字符集,它有 UTF-8、UTF-16 等多种编码。Unicode 的好处在于它的前 128 个码点与 ASCII 相同。
有了这些背景,我一直觉得奇怪:ASCII 的设计者为什么在大写 `Z` 之后放了 6 个字符才开始小写字母?然后我突然想到:英文字母有 26 个,加上小写开始前额外的 6 个字符:26 + 6 = 32。如果你了解计算机,2 的幂次往往会很显眼。我们来看看一些字符与其小写形式的二进制表示对比。
| 十进制 | 二进制 | 符号 |
|--------|--------|------|
| 65 | 01000001 | A |
| 97 | 01100001 | a |
| 66 | 01000010 | B |
| 98 | 01100010 | b |
| 67 | 01000011 | C |
| 99 | 01100011 | c |
看出来了吗?比较大写字母和对应的小写字母时,第 5 位总是翻转的。换算成十进制就明白了:
\[
\begin{flalign*}
& \begin{array}{ccccccccccccccccc}
& 0 & & 0 & & 1 & & 0 & & 0 & & 0 & & 0 & & 0 & \\
\times & 2^7 & & 2^6 & & 2^5 & & 2^4 & & 2^3 & & 2^2 & & 2^1 & & 2^0 & \\
\hline
& 0 & + & 0 & + & 32 & + & 0 & + & 0 & + & 0 & + & 0 & + & 0 & = 32 \\
\end{array} &
\end{flalign*}
\]
数字 32!正因如此,我们可以做一些有趣的位运算。例如,要将字符转为大写,可以用 32 的按位取反做按位与:
**步骤 1:对 32 按位取反,生成掩码**
```
~ 0 0 1 0 0 0 0 0 (32)
-------------------
1 1 0 1 1 1 1 1 (掩码)
```
**步骤 2:将 'a' 与掩码做按位与**
```
0 1 1 0 0 0 0 1 (97 = 'a')
& 1 1 0 1 1 1 1 1 (掩码)
-------------------
0 1 0 0 0 0 0 1 (65 = 'A')
```
如果原本就是大写字母,结果保持不变:
```
0 1 0 0 0 0 0 1 (65 = 'A')
& 1 1 0 1 1 1 1 1 (掩码)
-------------------
0 1 0 0 0 0 0 1 (65 = 'A')
```
如果要小写字母,可以与 32 做按位或:
```
0 1 0 0 0 0 0 1 (65 = 'A')
| 0 0 1 0 0 0 0 0 (32)
-------------------
0 1 1 0 0 0 0 1 (97 = 'a')
```
同样,原本就是小写字母则保持不变:
```
0 1 1 0 0 0 0 1 (97 = 'a')
| 0 0 1 0 0 0 0 0 (32)
-------------------
0 1 1 0 0 0 0 1 (97 = 'a')
```
如果想翻转大小写,可以与 32 做按位异或:
```
0 1 1 0 0 0 0 1 (97 = 'a')
^ 0 0 1 0 0 0 0 0 (32)
-------------------
0 1 0 0 0 0 0 1 (65 = 'A')
0 1 0 0 0 0 0 1 (65 = 'A')
^ 0 0 1 0 0 0 0 0 (32)
-------------------
0 1 1 0 0 0 0 1 (97 = 'a')
```
最后再教你一招:想获取字母在字母表中的索引,可以与 31 做按位与:
```
0 1 0 0 0 0 0 1 (65 = 'A')
& 0 0 0 1 1 1 1 1 (31)
-------------------
0 0 0 0 0 0 0 1 (1)
0 1 1 1 1 0 1 0 (122 = 'z')
& 0 0 0 1 1 1 1 1 (31)
-------------------
0 0 0 1 1 0 1 0 (26)
```
这之所以能行,是因为 `31` 实际上清除了高三位,只保留低五位。在 ASCII 中,字母的低五位正好对应其在字母表中的位置:`A/a` 结尾是 `00001`,`B/b` 结尾是 `00010`,依此类推,直到 `Z/z` 结尾为 `11010`。
换个角度理解:对于 ASCII 字符编码,`c & 31` 等价于 `c % 32`,因为 32 是 2 的幂。用 `31`(二进制 `00011111`)做掩码,只保留数字除以 32 后的"余数"部分。
```
'A' = 65 → 65 % 32 = 1
'B' = 66 → 66 % 32 = 2
...
'Z' = 90 → 90 % 32 = 26
'a' = 97 → 97 % 32 = 1
'b' = 98 → 98 % 32 = 2
...
'z' = 122 → 122 % 32 = 26
```
现在你知道为什么 ASCII 的设计者要在进入小写字母之前额外放置那 6 个字符了。
相似文章
Unicode 字符串的等价性很奇怪 (2016)
Unicode 字符串等价性很复杂,尤其是涉及校对规则时,会导致意外的结果,例如删除控制字符和非确定性分组。作者讨论了在数据库系统中正确实现 Unicode 支持所面临的挑战。
解读太斯米
本文探讨了阿拉伯文字编排在历史和技术上的挑战,重点论述了连字对太斯米短语的重要性,以及Unicode在解决这一问题中的作用。
波兰字母Ś消失的奇异案例
深入探讨Medium上波兰字母Ś消失的bug,通过打字机历史、共产主义和Unicode规范化追溯问题根源。
Jason Scott 的 ASCII
Jason Scott 关于 ASCII 艺术和数字保存的新项目或纪录片。
ARM64 指令的真正编码方式
一篇科普文章,解释 ARM64 (AArch64) 指令如何以 32 位固定长度字编码,破除常见误解,并通过 Apple Silicon 上的 ADD immediate 指令提供动手解码示例。