|
1 | 1 | # 字符编码那些事
|
2 | 2 |
|
| 3 | +[TOC] |
| 4 | + |
3 | 5 | ## 字符集
|
4 | 6 |
|
| 7 | +计算机不能直接存储字符,而是用数字来代替,这就是字符集,为每个字符指定一个数字。 |
| 8 | + |
5 | 9 | ### ASCII
|
6 | 10 |
|
7 | 11 | ASCII 为英文字母、阿拉伯数组、标点符号等 128 个字符,每个都用一个 0 到 127 范围内的数字对应。
|
@@ -175,7 +179,7 @@ UTF-8 把一个码点序列化为一个或多个码位,一个码位用 1 至 4
|
175 | 179 |
|
176 | 180 | 序列化规则如下:
|
177 | 181 |
|
178 |
| -#### 0 到 0x7F |
| 182 | +#### 兼容 ASCII |
179 | 183 |
|
180 | 184 | 对于 0 到 0x7F 的字符,这个范围的字符需要 7 位存储。
|
181 | 185 |
|
@@ -255,7 +259,7 @@ UTF-8 的构造就像一列小火车一样,不同范围内的码位会被编
|
255 | 259 |
|
256 | 260 | > {{ icon.fun }} 高级车头装了防弹钢板,载客空间变少,只好匀到后面的车厢。
|
257 | 261 |
|
258 |
| -#### UTF-8 的抗干扰机制 |
| 262 | +#### UTF-8 的抗干扰能力 |
259 | 263 |
|
260 | 264 | 如果发现 `10` 开头的独立车厢,就说明出问题了,可能是火车被错误拦腰截断,也可能是字符串被错误地反转。因为 `10` 只可能是火车车厢,不可能出现在火车头部。此时解码器应产生一个报错,或者用错误字符“�”替换。
|
261 | 265 |
|
@@ -961,7 +965,7 @@ std::string s = u8"你好";
|
961 | 965 | - 应用场景:常见于文字处理需求不大,但有强烈的跨平台需求,特别是互联网方面的软件。他们通常只用到字符串的拼接、查找、切片通常也只是在固定的位置(例如文件分隔符 `'/'`)。也非常适合主要面对的是以 ASCII 为主的“代码”类文本,UTF-8 是对英文类文本压缩率最高的,所以也广泛用于编译器、数据库之类的场景。同时因为 UTF-8 完全兼容 ASCII,使得他能轻易适配远古的 C 语言程序和库。
|
962 | 966 | - 方法:始终以 UTF-8 编码存储和处理字符串。
|
963 | 967 | - 优点:跨平台,在网络传输时无需任何转码,UTF-8 是互联网的主流编码格式,不同平台上运行的 UTF-8 软件可以随意共享文本数据。兼容 ASCII,方便复用现有库和生态。对英文类文本压缩率高,对中文文本也不算太差。
|
964 |
| -- 缺点:对于底层 API 均采用 UTF-16 的 Windows 系统,需要进行字符编码转换,有少量性能损失。且字符串的正确切片、求长度等操作的复杂度会变成 $O(N)$ 而不是通常的 $O(1)$。 |
| 968 | +- 缺点:对于底层 API 均采用 UTF-16 的 Windows 系统,需要进行字符编码转换,有少量性能损失。且字符串的正确切片、求长度等操作的复杂度会变成 $$$O(N)$$$ 而不是通常的 $$O(1)$$。 |
965 | 969 | - 代表作:Rust 语言、Go 语言、CMake 构建系统、Julia 语言等。
|
966 | 970 |
|
967 | 971 | 在 C++ 中,可以通过 `u8"你好"` 创建一个保证内部是 UTF-8 编码的字符串常量,类型为 `char8_t []`。
|
@@ -1049,7 +1053,7 @@ Rust 和 Go:严格区分“字符 (32 位)”和“字节 (8 位)”的概念
|
1049 | 1053 | |Go|`rune`|`byte`|
|
1050 | 1054 | |Julia|`Char`|`UInt8`|
|
1051 | 1055 |
|
1052 |
| -为此,这些语言都为字符串提供了两套 API,一种是按字符索引,一种是按字节索引。按字符索引时,会从头开始,逐个解析码位,直到解析到想要的字符为止,复杂度 $O(N)$。按字节索引时,直接跳到指定字节,无需解析,复杂度 $O(1)$。 |
| 1056 | +为此,这些语言都为字符串提供了两套 API,一种是按字符索引,一种是按字节索引。按字符索引时,会从头开始,逐个解析码位,直到解析到想要的字符为止,复杂度 $$$O(N)$$$。按字节索引时,直接跳到指定字节,无需解析,复杂度 $$O(1)$$。 |
1053 | 1057 |
|
1054 | 1058 | ```rust
|
1055 | 1059 | let s = "你好";
|
@@ -1150,7 +1154,7 @@ for (char32_t c : Utf8Range(s)) {
|
1150 | 1154 | - 应用场景:通常认为,UTF-16 是纯粹的历史遗留糟粕,新软件不应该再使用 UTF-16。只有在和这些糟粕软件的 API 打交道时,才必须转换为 UTF-16。但也有人指出:UTF-16 是纯中文压缩率最高的编码格式,所以 UTF-16 还比较适合纯中文或以中文内容为主的文本数据压缩。
|
1151 | 1155 | - 方法:始终以 UTF-16 编码存储和处理字符串。
|
1152 | 1156 | - 优点:调用 Windows 系统 API 时无需任何转换,直接就能调用,最适合 Windows 本地开发,非跨平台。且对纯中文内容可比 UTF-8 额外节省 33% 空间。
|
1153 |
| -- 缺点:对于 Windows 以外的系统就需要转换回 UTF-8,有少量性能开销。且如果存储的内容主要是纯英文,如 XML 代码等,内存占用会比 UTF-8 翻倍。而且 UTF-16 仍然是变长编码,虽然出现变长的概率较低,但不为 0,仍需要开发者做特殊处理。字符串的按码位反转会导致生僻字符出错,字符串以码点为单位的的正确切片、求长度等操作的复杂度仍然 $O(N)$ 而不是通常的 $O(1)$。并且 UTF-16 有大小端转换的问题。 |
| 1157 | +- 缺点:对于 Windows 以外的系统就需要转换回 UTF-8,有少量性能开销。且如果存储的内容主要是纯英文,如 XML 代码等,内存占用会比 UTF-8 翻倍。而且 UTF-16 仍然是变长编码,虽然出现变长的概率较低,但不为 0,仍需要开发者做特殊处理。字符串的按码位反转会导致生僻字符出错,字符串以码点为单位的的正确切片、求长度等操作的复杂度仍然 $$$O(N)$$$ 而不是通常的 $$O(1)$$。并且 UTF-16 有大小端转换的问题。 |
1154 | 1158 | - 代表作:Windows 系统 API、Java 语言、Windows 文件系统 (NTFS)、Qt、Word、JSON,他们都是 UTF-16 的受害者。
|
1155 | 1159 |
|
1156 | 1160 | 这相当于是把 UTF-16 当作了内码,但 UTF-16 依然是一种变长编码,对常见的中文处理没问题,生僻字就容易出问题,且因为出现概率低,很容易不发现,埋下隐患。
|
@@ -1213,7 +1217,7 @@ fmt::println("{}", s.size()); // 3
|
1213 | 1217 |
|
1214 | 1218 | - 应用场景:适合需要经常处理文字的领域,如文本编辑器、浏览器等。但不适合存储和传输,因为浪费硬盘和网络带宽。字符串一般都长期以 UTF-8 存储,只有在需要频繁索引码位时,才需要转换为 UTF-32。
|
1215 | 1219 | - 方法:始终以 UTF-32 编码存储和处理字符串。
|
1216 |
| -- 优点:字符串的按码位反转、切片、求长度等操作都是 $O(1)$ 的复杂度,可以当作普通数组一样,随意处理。例如你可以设想一个文本编辑框,需要支持“退格”操作,如果是 UTF-8 和 UTF-16 就需要繁琐的判断代理对、各种车厢,而 UTF-32 的字符串只需要一次 `pop_back` 就搞定了。 |
| 1220 | +- 优点:字符串的按码位反转、切片、求长度等操作都是 $$$O(1)$$$ 的复杂度,可以当作普通数组一样,随意处理。例如你可以设想一个文本编辑框,需要支持“退格”操作,如果是 UTF-8 和 UTF-16 就需要繁琐的判断代理对、各种车厢,而 UTF-32 的字符串只需要一次 `pop_back` 就搞定了。 |
1217 | 1221 | - 缺点:浪费空间大,通常在保存时,仍然需要转换回 UTF-8 后再写入文件,有一定性能开销。
|
1218 | 1222 |
|
1219 | 1223 | *总结:要支持 UTF-32 阵营,请全部使用 `char32_t` 和 `std::u32string`。字面量全用 `U"你好"` 的形式书写,读文件时转为 UTF-32,写文件时转回 UTF-8。*
|
@@ -1823,7 +1827,7 @@ TODO
|
1823 | 1827 | //
|
1824 | 1828 | //缺点是这样的软件会无法跨平台,因为 `wchar_t` 在 Linux 上是安全的内码 UTF-32。而 Windows 上是 UTF-16,是不定长的编码,如果存在“𰻞”和“😉”这样超过 0x10000 的生僻字,就会产生两个 `wchar_t`!如果文字处理涉及切片,就会出问题。概率很低,但不为零,软件仍然需要对可能存在的双 `wchar_t` 做特殊处理。若不处理,轻则乱码,重则留下漏洞,被黑客攻击,加重了 Windows 和 Java 程序员的心智负担。
|
1825 | 1829 | //
|
1826 |
| -//如果一个程序(例如 GCC)只适配了 `wchar_t` 是 UTF-32 的平台,想当然的把 `wchar_t` 当作安全的定长内码使用,那移植到 Windows 上后就会丧失处理“𰻞”和“😉”的能力。要么就需要对所有代码大改,把原本 $O(1)$ 的字符串求长度改成 $O(N)$ 的;要么出现乱码,被黑客攻击。 |
| 1830 | +//如果一个程序(例如 GCC)只适配了 `wchar_t` 是 UTF-32 的平台,想当然的把 `wchar_t` 当作安全的定长内码使用,那移植到 Windows 上后就会丧失处理“𰻞”和“😉”的能力。要么就需要对所有代码大改,把原本 $$$O(1)$$$ 的字符串求长度改成 $$O(N)$$ 的;要么出现乱码,被黑客攻击。 |
1827 | 1831 | //
|
1828 | 1832 | //当需要读写二进制文件时,使用 `fstream`,原封不动地按“字节”为单位读取。
|
1829 | 1833 | //
|
@@ -2293,7 +2297,7 @@ stream << "你好,世界\n"; // 写入 QString,QTextStream 会自动将其
|
2293 | 2297 |
|
2294 | 2298 | 如果用 UTF-8 或 UTF-16 来存储的话,会遇到变长编码的固有缺陷:
|
2295 | 2299 |
|
2296 |
| -例如像字符串索引,字符串求长度等操作,要么索引出来的是字节而不是字符了;要么就需要 $O(N)$ 的复杂度,逐一遍历每个字节,才能确定真正的位置;哪怕全是 ASCII 也得这么做,因为万一刚好有一个是中文字符呢? |
| 2300 | +例如像字符串索引,字符串求长度等操作,要么索引出来的是字节而不是字符了;要么就需要 $$$O(N)$$$ 的复杂度,逐一遍历每个字节,才能确定真正的位置;哪怕全是 ASCII 也得这么做,因为万一刚好有一个是中文字符呢? |
2297 | 2301 |
|
2298 | 2302 | 所以,对于经常需要处理字符串的 Python 来说,UTF-8 是无法接受的,似乎只能以 UTF-32 来存储?
|
2299 | 2303 |
|
@@ -2340,7 +2344,7 @@ struct PyUnicodeString {
|
2340 | 2344 |
|
2341 | 2345 | ### Rust `&str` 和 `String`
|
2342 | 2346 |
|
2343 |
| -而 Rust 则采用了字符串全员 UTF-8 的策略,这是因为 Rust 最常用于互联网方面的底层系统软件,互联网最常用的文本编码就是 UTF-8,没有大小端问题,且国际通用。除此之外,互联网基建最常见的平台就是 Linux,使用 UTF-8 存储字符串,调用 Linux 系统 API 无需任何转换。且文本文件基本都可以假定是 UTF-8 编码,写入时无需任何转换,复杂度低至 $O(1)$。作为代价,这导致文本处理上的一些困难,例如字符串的索引,需要区分是按字节索引还是按字符索引,如果确实需要按字符索引的话,复杂度就会是 $O(N)$ 了。 |
| 2347 | +而 Rust 则采用了字符串全员 UTF-8 的策略,这是因为 Rust 最常用于互联网方面的底层系统软件,互联网最常用的文本编码就是 UTF-8,没有大小端问题,且国际通用。除此之外,互联网基建最常见的平台就是 Linux,使用 UTF-8 存储字符串,调用 Linux 系统 API 无需任何转换。且文本文件基本都可以假定是 UTF-8 编码,写入时无需任何转换,复杂度低至 $$$O(1)$$$。作为代价,这导致文本处理上的一些困难,例如字符串的索引,需要区分是按字节索引还是按字符索引,如果确实需要按字符索引的话,复杂度就会是 $$O(N)$$ 了。 |
2344 | 2348 |
|
2345 | 2349 | 无论如何,如果你选择了 UTF-8 流派的话,Rust 字符串的“迭代器双轨制”确实值得称道:
|
2346 | 2350 |
|
|
0 commit comments