Skip to content

Commit 4c2d94d

Browse files
committed
updategbk
1 parent ff579f0 commit 4c2d94d

File tree

3 files changed

+155
-34
lines changed

3 files changed

+155
-34
lines changed

docs/extra.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ h4 {
1414
font-weight: 520;
1515
margin: 1em auto;
1616
}
17+
h5 {
18+
font-weight: 500;
19+
margin: 1em auto;
20+
}
1721
body {
1822
font-weight: 480;
1923
}

docs/functions.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ void compute()
2929
}
3030
```
3131

32-
> {{ icon.warn }} 对于有返回值的函数,必须写 return 语句,如果漏写,会出现可怕的未定义行为 (undefined behaviour)。编译器不会报错,而是到运行时才出现崩溃等现象,建议 GCC 用户开启 `-Werror=return-type` 让编译器检测此类错误。更多未定义行为可以看我们的[未定义行为列表](/undef)章节。
32+
> {{ icon.warn }} 对于有返回值的函数,必须写 return 语句,如果漏写,会出现可怕的未定义行为 (undefined behaviour)。编译器不会报错,而是到运行时才出现崩溃等现象,建议 GCC 用户开启 `-Werror=return-type` 让编译器检测此类错误。更多未定义行为可以看我们的[未定义行为列表](undef.md)章节。
3333
3434
### 接住返回值
3535

docs/unicode.md

Lines changed: 150 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ ASCII 为英文字母、阿拉伯数组、标点符号等 128 个字符,每个
1616

1717
例如下面的一串数字:
1818

19-
```
19+
```txt
2020
80 101 110 103
2121
```
2222

@@ -195,7 +195,7 @@ UTF-8 把一个码点序列化为一个或多个码位,一个码位用 1 至 4
195195

196196
例如 'P' 会被直接存储其 Unicode 值的 80(0x50):
197197

198-
```
198+
```txt
199199
01010000
200200
```
201201

@@ -221,7 +221,7 @@ UTF-8 的构造就像一列小火车一样,不同范围内的码位会被编
221221
222222
例如下面这一串二进制:
223223

224-
```
224+
```txt
225225
11100110 10000010 10000001
226226
```
227227

@@ -231,27 +231,27 @@ UTF-8 的构造就像一列小火车一样,不同范围内的码位会被编
231231

232232
对于这种三级列车,4 + 6 + 6 总共 16 位二进制,刚好可以装得下 0xFFFF 内的乘客。
233233

234-
```
234+
```txt
235235
0110 000010 000001
236236
```
237237

238238
编码时则是反过来。
239239

240240
乘客需要被拆分成三片,例如对于“我”这个乘客,“我”的码点是 0x6211,转换成二进制是:
241241

242-
```
242+
```txt
243243
110001000010001
244244
```
245245

246246
把乘客切分成高 4 位、中 6 位和低 6 位(不足时在前面补零):
247247

248-
```
248+
```txt
249249
0110 001000 010001
250250
```
251251

252252
加上 `1110``10``10` 前缀后,形成一列火车:
253253

254-
```
254+
```txt
255255
11100110 10001000 10010001
256256
```
257257

@@ -271,7 +271,7 @@ UTF-8 的构造就像一列小火车一样,不同范围内的码位会被编
271271

272272
如果发现 `10` 开头的独立车厢,就说明出问题了,可能是火车被错误拦腰截断,也可能是字符串被错误地反转。因为 `10` 只可能是火车车厢,不可能出现在火车头部。此时解码器应产生一个报错,或者用错误字符“�”替换。
273273

274-
```
274+
```txt
275275
10000010 10000001
276276
```
277277

@@ -319,8 +319,8 @@ UTF-8 中,一个码点可能对应多个码位,所以说 UTF-8 是一种**
319319

320320
计算机需要外码和内码两种:
321321

322-
+ 外码=硬盘中的文本=UTF-32
323-
+ 内码=内存中的文本=UTF-8
322+
+ 外码=硬盘中的文本=UTF-8
323+
+ 内码=内存中的文本=UTF-32
324324

325325
### UTF-16
326326

@@ -332,7 +332,7 @@ UTF-16 的策略是:既然大多数常用字符的码点都在 0x0 到 0xFFFF
332332

333333
例如,我们把一个稀有字符“𰻞”,0x30EDE。拆成两个 `uint16_t`,得到 0x3 和 0x0EDE。如果直接存储这两个 `uint16_t`
334334

335-
```
335+
```txt
336336
0x0003 0x0EDE
337337
```
338338

@@ -358,19 +358,19 @@ UTF-16 就是利用了这一段空间,他规定:0xD800 到 0xDFFF 之间的
358358

359359
然后,写出 0x20EDE 的二进制表示:
360360

361-
```
361+
```txt
362362
00100000111011011110
363363
```
364364

365365
总共 20 位,我们将其拆成高低各 10 位:
366366

367-
```
367+
```txt
368368
0010000011 1011011110
369369
```
370370

371371
各自写出相应的十六进制数:
372372

373-
```
373+
```txt
374374
0x083 0x2DE
375375
```
376376

@@ -380,7 +380,7 @@ UTF-16 就是利用了这一段空间,他规定:0xD800 到 0xDFFF 之间的
380380

381381
所以,我们将拆分出来的两个 10 位数,分别加上 0xD800 和 0xDC00:
382382

383-
```
383+
```txt
384384
0xD800+0x083=0xD883
385385
0xDC00+0x2DE=0xDFDE
386386
```
@@ -403,13 +403,13 @@ UTF-16 就是利用了这一段空间,他规定:0xD800 到 0xDFFF 之间的
403403

404404
- 大端派 (bit endian):低地址存放整数的高位,高地址存放整数的低位,也就是大数靠前!这样数值的高位和低位和人类的书写习惯一致。例如,0x12345678,在内存中就是:
405405

406-
```
406+
```txt
407407
0x12 0x34 0x56 0x78
408408
```
409409

410410
- 小端派 (little endian):低地址存放整数的低位,高地址存放整数的高位,也就是小数靠前!这样数值的高位和低位和计算机电路的计算习惯一致。例如,0x12345678,在内存中就是:
411411

412-
```
412+
```txt
413413
0x78 0x56 0x34 0x12
414414
```
415415

@@ -425,19 +425,19 @@ UTF-16 就是利用了这一段空间,他规定:0xD800 到 0xDFFF 之间的
425425
426426
UTF-16 和 UTF-32 的码位都是多字节的,也会有大小端问题。例如,UTF-16 中的 `uint16_t` 序列:
427427

428-
```
428+
```txt
429429
0x1234 0x5678
430430
```
431431

432432
在大端派的机器中,就是:
433433

434-
```
434+
```txt
435435
0x12 0x34 0x56 0x78
436436
```
437437

438438
在小端派的机器中,就是:
439439

440-
```
440+
```txt
441441
0x34 0x12 0x78 0x56
442442
```
443443

@@ -488,23 +488,136 @@ UTF-8 是基于单字节的码位,火车头的顺序也有严格规定,火
488488

489489
#### GB2312
490490

491-
GB2312 是一个古老的标准,兼容 ASCII。
491+
GB2312 发布于 1980 年,是一个古老的汉字编码标准,兼容 ASCII。
492+
493+
> {{ icon.fun }} “GB” 表示 Guo Biao(国标)的意思。
492494
493495
GB2312 规定了 6763 个汉字和 682 个特殊符号,共 7445 个字符。
494496

495-
GB2312 使用 2 字节来编码汉字和特殊符号,而 1 字节的编码保持和 ASCII 相同,从而兼容 ASCII
497+
GB2312 让英文和数字采用和 ASCII 相同的单字节编码,而对于汉字和特殊符号采用双字节编码
496498

497-
2 个字节分别被称为“区码”和“位码”。的部分都在 0xA1 到 0xFE 区间内,与 ASCII 不重合,且避开了 0xFF
499+
其中汉字和特殊符号的双字节编码,两个字节都在 0xA1 到 0xFE 的范围内,避免了与单字节的 ASCII 编码空间冲突。
498500

499-
其中“一级汉字”,即常用汉字,有 3755 个,这些汉字的编码从 0xA1A1 到 0xF7FE。
501+
```txt
502+
H i 彭 宝
503+
0x48 0x69 0xC5 0xED 0xB1 0xA6
504+
```
505+
506+
2 个字节分别被称为“区码”和“位码”,范围都是在 0xA1 到 0xFE 区间内。
507+
508+
“特殊符号”,共 682 个,这些字符的编码从 0xA1A1 到 0xA9FE。
509+
510+
“一级汉字”,都是比较常用的汉字,按拼音顺序排序,共 3755 个,这些字符的编码从 0xB0A1 到 0xF7FE。
511+
512+
“二级汉字”,是一些生僻字,按部首/笔画排序,共 3008 个,这些汉字的字符从 0x8140 到 0xA0FE。
500513

501-
“二级汉字”,即生僻汉字,有 3008 个,这些汉字的编码从 0xA1A1 到 0xA9FE
514+
GBK 中汉字的编号和 Unicode 并不是相同的,这也是 GB2312 编码的文本文件用 UTF-8 或 UTF-16 打开会产生乱码的根本原因
502515

503-
GB2312 也规定了一些特殊字符的编码,例如全角空格 `0xA1A1`,以及 1 级汉字和 2 级汉字的划分
516+
例如汉字 “彭” 就属于 “二级汉字”,在 GB2312 中编号为 0xC5ED,Unicode 中编号为 0x5F6D
504517

505-
### GBK
518+
全角空格 “ ”,在 GB2312 中的编号为 0xA1A1,Unicode 中的编号为 0x3000。
506519

507-
GBK 是 GB2312 的扩展,他规定了 21003 个汉字和 682 个非汉字,共 21886 个
520+
#### GB2312 的缺陷
521+
522+
GBK 和 UTF-8 的共同点在于,他们都避开了 0 ~ 127 的 ASCII 空间,所以完全兼容 ASCII。
523+
524+
不同点在于,如果在一个 GBK 编码的中文字符串中查找中文,可能得到错误的结果,而 UTF-8 不会。这是因为 GBK 没有自纠错机制,他前后两个车厢并没有不同。
525+
526+
当 GBK 被切片时,就容易出现连锁反应。
527+
528+
```cpp
529+
std::string s = "沉迷圆神"; // 0xB3 0xC1 0xC3 0xD4 0xD4 0xAD 0xC9 0xF1
530+
std::cout << s.substr(1); // "撩栽采�"
531+
// 复现方式: MSVC 中国区 Windows,/std:c++17,不开启 /utf-8 参数(这时字符串常量都是 GBK 编码的)
532+
```
533+
534+
> {{ icon.fun }} 核反应堆,启动!
535+
536+
特别是当你试图 find 一个中文子字符串时,GBK 编码的多字节字符串可能产生找到了的假象。实际上找到的位置根本是切断了单个完整的中文字符。
537+
538+
```cpp
539+
std::string s = "沉迷圆神"; // 0xB3 0xC1 0xC3 0xD4 0xD4 0xAD 0xC9 0xF1
540+
std::cout << s.find(""); // 5,把“圆”的后半段 0xAD 和“神”的前半段 0xC9 当成一个字“采” (0xAD 0xC9) 了
541+
```
542+
543+
而 UTF-8 则能有效限制错误的传播。
544+
545+
```cpp
546+
std::string u8s = "沉迷圆神"; // 0xE6 0xB2 0x89 0xE8 0xBF 0xB7 0xE5 0x9C 0x86 0xE7 0xA5 0x9E
547+
std::cout << u8s.substr(1); // "�迷圆神"
548+
std::cout << u8s.find(""); // 找不到,返回 -1
549+
```
550+
551+
> {{ icon.detail }} 因为 UTF-8 的头部小火车和尾部车厢采用了独立的编码,find 不可能通过任何合法的 UTF-8 子字符串定位到错误的中间位置。
552+
553+
> {{ icon.detail }} GB2312 和 GBK 的特殊性:他既是字符集,又是字符编码。除 Unicode 外大多数字符集都是这样,自己就是自己的字符编码,没有其他编码方式。只有 Unicode 把字符集和字符编码的概念分的很清楚,因为 Unicode 字符集有 UTF-8、UTF-。
554+
555+
#### GBK
556+
557+
GBK 同样是双字节编码,是对 GB2312 的扩展。
558+
559+
GBK 在保持 GB2312 部分不变的基础上,额外追加了 21886 个汉字。收录的有:
560+
561+
- GB2312 中的全部汉字和特殊符号
562+
- BIG-5(繁体中文的编码)中的全部汉字
563+
- GB13000(即 Unicode)中的其他中日韩汉字
564+
- 其他尚未收录的特殊汉字、部首、符号等
565+
566+
> {{ icon.fun }} “K” 表示 Kuo(扩展)的意思。
567+
568+
- GB2312:首字节 0xA1 到 0xFE,尾字节 0xA1 到 0xFE。
569+
- GBK:首字节 **0x81 到 0xFE**,尾字节 **0x40 到 0xFE**
570+
571+
注意到 GBK 的尾字节进入了 0x40,ASCII 的范围。
572+
573+
这使得 GBK 比 GB2312 更危险,例如 “侰” 这个字,会被编码成 0x82 0x43。
574+
575+
而 0x43 刚好是 ASCII 字符 “C” 的编码。如果发生字符串切片,可能导致 “侰” 变成 “�C”。并且如果刚好程序在 `.find('C')`,那么 “侰” 的后半个字节会被当作 “C” 而找到。
576+
577+
不过,GBK 设计时特意避开了 0x40 以下的 ASCII 字符,例如 0x2F 是 “/” 的 ASCII 编码,常用于文件系统的路径分隔符。
578+
579+
然而,Windows 所用的路径分隔符是反斜杠 “\”,他的 ASCII 编码是 0x5C。
580+
581+
例如 “俓”,GBK 编码 0x82 0x5C,这个字就可能被错误解读成 “�\”,使程序误认为这是一个文件夹的路径,从而导致程序出错,或留下被黑客攻击的隐患。
582+
583+
虽然 Windows 系统内部统一采用 Unicode(UTF-16)来存储和处理路径,但总架不住一些劳保程序,依然执着于 ANSI(GBK),如果用户无意或恶意输入“俓啊.txt”,就有可能产生隐患。
584+
585+
而 UTF-8 同样兼容 ASCII,得益于冗余的自纠错机制,就没有这样的问题。
586+
587+
> {{ icon.fun }} “GBK” 虽然冠以“国标”之名,但实际上是微软擅自推出的!根本不是什么国家标准(你猜他为什么没有国家文件编号)。本来我国官方都打算推出 GB13000 了,直接完全兼容 Unicode 字符集。然而微软自作主张把他 Wendous 的 GB2312 升级到 GBK(比尔盖子以为自己兼容压倒一切,拽死了),本来好好的 GB13000 标准只好作罢。为了兼容已经升级成 GBK 的 Wendous 系统,国家只好重新制作了一份 GB18030 标准,兼容 GBK 和 GB2312。
588+
589+
#### GB18030
590+
591+
于 2000 年 3 月发布的汉字编码国家标准,完全兼容 GBK 诶 GB2312。
592+
593+
采用多字节编码,每个字符可以编码为 1 字节、2 字节、或 4 字节。
594+
595+
其中 1 字节的部分取值范围是 0 到 0x7F,和 ASCII 相同。
596+
2 字节的部分首字节范围 0x81 到 0xFE,尾字节范围 0x40 到 0xFE,和 GBK 相同。
597+
4 字节的部分第一字节范围 0x81 到 0xFE,第二字节范围 0x30 到 0x39,第三字节范围 0x81 到 0xFE,第四字节范围 0x30 到 0x39。
598+
599+
字符处理软件在处理 GB18030 的文本时,从左往右依次扫描每个字节:
600+
601+
- 如果遇到的字节的最高位是 0,那么就会断定该字符只占用了一个字节
602+
- 如果遇到的字节的最高位是 1,那么该字符可能占用了两个字节,也可能占用了四个字节,不能妄下断论,所以还要继续往后扫描:
603+
- 如果第二个字节的高位有两个连续的 0,那么就会断定该字符占用了四个字节
604+
- 如果第二个字节的高位没有连续的 0,那么就会断定该字符占用了两个字节
605+
606+
当字符占用两个或者四个字节时,GB18030 编码总要检测两次,处理效率比 GB2312 和 GBK 都低。而且和 GBK 一样,有着不能自纠错,容易使编码错误连锁反应的问题。
607+
608+
GB18030 编码空间巨大,不仅囊括了中日韩汉字,所有 Unicode 中的字符也都被纳入其中!也就是说,Unicode 字符用 GB18030 编码是无损的,和 UTF-8 一样,而且还完全兼容了 GBK(中国区 Windows 的默认编码)。
609+
610+
所以,GB18030 字符编码对应的字符集实际上是 Unicode。
611+
612+
> {{ icon.fun }} 尽管如此,Windows 至今(我测的是 Win10,不知道 Win11 它们改进没有)采用的依然是 GBK 编码:`A` 系 API 无法正确识别 GB18030 编码的字符串,文件路径名。明明中国政府都给你做完全兼容你的 GBK 了,还收录所有 Unicode 字符了,完全可以无缝增量升级的东西,泥码沟槽的比尔盖子还不快点默认切换到 GB18030,想不通(致敬传奇阴井盖比尔盖子)
613+
614+
#### 总结
615+
616+
字符编码兼容性:UTF-8 != GB18030 > GBK > GB2312 > ASCII
617+
618+
字符集大小:Unicode = GB18030 > GBK > GB2312 > ASCII
619+
620+
总之,GB 系列编码面对切片和查找存在一定的问题,有条件的话,建议 Windows 程序尽快升级到 UTF-8 外码、UTF-16 内码的工作流上来,回避掉史山的影响。
508621

509622
## C/C++ 中的字符编码
510623

@@ -534,7 +647,7 @@ GBK 是 GB2312 的扩展,他规定了 21003 个汉字和 682 个非汉字,
534647

535648
> {{ icon.detail }} 根据 Windows 官方文档的说法,`wchar_t` 是 UTF-16LE。
536649
537-
### 思考:UTF-8 为什么完美兼容 ASCII
650+
### 思考:UTF-8 为什么完美能兼容 ASCII
538651

539652
UTF-8 的火车头和车厢,都是 `1` 开头的,而 ASCII 的单体火车头永远是 `0` 开头。这很重要,不仅火车头需要和 ASCII 区分开来,车厢也需要。考虑这样一个场景:
540653

@@ -544,13 +657,13 @@ std::u32string path = "一个老伯.txt";
544657

545658
“一个老伯” 转换为 Unicode 码点分别是:
546659

547-
```
660+
```txt
548661
0x4E00 0x4E2A 0x8001 0x4F2F
549662
```
550663

551664
如果让他们原封不动直接存储进 char 数组里:
552665

553-
```
666+
```txt
554667
0x4E 0x00 0x4E 0x2A 0x80 0x01 0x4F 0x2F
555668
```
556669

@@ -630,7 +743,7 @@ fmt::println("UTF-32 下,前四个字符:{}", s.substr(0, 4));
630743
// 会打印 “小彭老师”
631744
```
632745

633-
只有当索引来自 `find` 的结果时,UTF-8 字符串的切片才能正常工作:
746+
只有当索引是来自 `find` 的结果时,UTF-8 字符串的切片才能正常工作:
634747

635748
```cpp
636749
std::string s = "小彭老师公开课万岁";
@@ -681,7 +794,7 @@ strrev(s.data()); // 会把按字符正常反转,得到 “岁万课开公师
681794
682795
### 轶事:“ANSI” 与 “Unicode” 是什么
683796

684-
在 Windows 官方的说辞中,有“Unicode 编码”和“ANSI 编码”的说法。当你使用 Windows 自带的记事本程序保存文本文件时,就会看到这样的选单:
797+
在 Windows 官方的说辞中,有“Unicode 编码”和“ANSI 编码”的说法。当你使用 Windows 自带的记事本程序 (`notepad.exe`) 保存文本文件时,就会看到这样的选单:
685798

686799
![](img/notepad.png)
687800

@@ -3195,6 +3308,10 @@ base64.b64decode(secret).decode()
31953308
31963309
总之,如果你输入中文实在有问题,可以考虑先 Base64 转换成纯英文试试看,反正无论谁都兼容 ASCII。如果这个文本框不区分大小写,还可以试试看只有 A-Z 0-9 的 Base32 编码。
31973310

3311+
### UTF-7
3312+
3313+
TODO
3314+
31983315
### 字符编码猜测
31993316

32003317
TODO

0 commit comments

Comments
 (0)