为什么 ASCII 中小写字母没有紧跟在大写字母后面?

Lobsters Hottest 新闻

摘要

本文解释了 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)

Lobsters Hottest

Unicode 字符串等价性很复杂,尤其是涉及校对规则时,会导致意外的结果,例如删除控制字符和非确定性分组。作者讨论了在数据库系统中正确实现 Unicode 支持所面临的挑战。

解读太斯米

Hacker News Top

本文探讨了阿拉伯文字编排在历史和技术上的挑战,重点论述了连字对太斯米短语的重要性,以及Unicode在解决这一问题中的作用。

波兰字母Ś消失的奇异案例

Hacker News Top

深入探讨Medium上波兰字母Ś消失的bug,通过打字机历史、共产主义和Unicode规范化追溯问题根源。

Jason Scott 的 ASCII

Hacker News Top

Jason Scott 关于 ASCII 艺术和数字保存的新项目或纪录片。

ARM64 指令的真正编码方式

Lobsters Hottest

一篇科普文章,解释 ARM64 (AArch64) 指令如何以 32 位固定长度字编码,破除常见误解,并通过 Apple Silicon 上的 ADD immediate 指令提供动手解码示例。