@@ -16,7 +16,7 @@ ASCII 为英文字母、阿拉伯数组、标点符号等 128 个字符,每个
16
16
17
17
例如下面的一串数字:
18
18
19
- ```
19
+ ``` txt
20
20
80 101 110 103
21
21
```
22
22
@@ -195,7 +195,7 @@ UTF-8 把一个码点序列化为一个或多个码位,一个码位用 1 至 4
195
195
196
196
例如 'P' 会被直接存储其 Unicode 值的 80(0x50):
197
197
198
- ```
198
+ ``` txt
199
199
01010000
200
200
```
201
201
@@ -221,7 +221,7 @@ UTF-8 的构造就像一列小火车一样,不同范围内的码位会被编
221
221
222
222
例如下面这一串二进制:
223
223
224
- ```
224
+ ``` txt
225
225
11100110 10000010 10000001
226
226
```
227
227
@@ -231,27 +231,27 @@ UTF-8 的构造就像一列小火车一样,不同范围内的码位会被编
231
231
232
232
对于这种三级列车,4 + 6 + 6 总共 16 位二进制,刚好可以装得下 0xFFFF 内的乘客。
233
233
234
- ```
234
+ ``` txt
235
235
0110 000010 000001
236
236
```
237
237
238
238
编码时则是反过来。
239
239
240
240
乘客需要被拆分成三片,例如对于“我”这个乘客,“我”的码点是 0x6211,转换成二进制是:
241
241
242
- ```
242
+ ``` txt
243
243
110001000010001
244
244
```
245
245
246
246
把乘客切分成高 4 位、中 6 位和低 6 位(不足时在前面补零):
247
247
248
- ```
248
+ ``` txt
249
249
0110 001000 010001
250
250
```
251
251
252
252
加上 ` 1110 ` 、` 10 ` 和 ` 10 ` 前缀后,形成一列火车:
253
253
254
- ```
254
+ ``` txt
255
255
11100110 10001000 10010001
256
256
```
257
257
@@ -271,7 +271,7 @@ UTF-8 的构造就像一列小火车一样,不同范围内的码位会被编
271
271
272
272
如果发现 ` 10 ` 开头的独立车厢,就说明出问题了,可能是火车被错误拦腰截断,也可能是字符串被错误地反转。因为 ` 10 ` 只可能是火车车厢,不可能出现在火车头部。此时解码器应产生一个报错,或者用错误字符“�”替换。
273
273
274
- ```
274
+ ``` txt
275
275
10000010 10000001
276
276
```
277
277
@@ -319,8 +319,8 @@ UTF-8 中,一个码点可能对应多个码位,所以说 UTF-8 是一种**
319
319
320
320
计算机需要外码和内码两种:
321
321
322
- + 外码=硬盘中的文本=UTF-32
323
- + 内码=内存中的文本=UTF-8
322
+ + 外码=硬盘中的文本=UTF-8
323
+ + 内码=内存中的文本=UTF-32
324
324
325
325
### UTF-16
326
326
@@ -332,7 +332,7 @@ UTF-16 的策略是:既然大多数常用字符的码点都在 0x0 到 0xFFFF
332
332
333
333
例如,我们把一个稀有字符“𰻞”,0x30EDE。拆成两个 ` uint16_t ` ,得到 0x3 和 0x0EDE。如果直接存储这两个 ` uint16_t ` :
334
334
335
- ```
335
+ ``` txt
336
336
0x0003 0x0EDE
337
337
```
338
338
@@ -358,19 +358,19 @@ UTF-16 就是利用了这一段空间,他规定:0xD800 到 0xDFFF 之间的
358
358
359
359
然后,写出 0x20EDE 的二进制表示:
360
360
361
- ```
361
+ ``` txt
362
362
00100000111011011110
363
363
```
364
364
365
365
总共 20 位,我们将其拆成高低各 10 位:
366
366
367
- ```
367
+ ``` txt
368
368
0010000011 1011011110
369
369
```
370
370
371
371
各自写出相应的十六进制数:
372
372
373
- ```
373
+ ``` txt
374
374
0x083 0x2DE
375
375
```
376
376
@@ -380,7 +380,7 @@ UTF-16 就是利用了这一段空间,他规定:0xD800 到 0xDFFF 之间的
380
380
381
381
所以,我们将拆分出来的两个 10 位数,分别加上 0xD800 和 0xDC00:
382
382
383
- ```
383
+ ``` txt
384
384
0xD800+0x083=0xD883
385
385
0xDC00+0x2DE=0xDFDE
386
386
```
@@ -403,13 +403,13 @@ UTF-16 就是利用了这一段空间,他规定:0xD800 到 0xDFFF 之间的
403
403
404
404
- 大端派 (bit endian):低地址存放整数的高位,高地址存放整数的低位,也就是大数靠前!这样数值的高位和低位和人类的书写习惯一致。例如,0x12345678,在内存中就是:
405
405
406
- ```
406
+ ``` txt
407
407
0x12 0x34 0x56 0x78
408
408
```
409
409
410
410
- 小端派 (little endian):低地址存放整数的低位,高地址存放整数的高位,也就是小数靠前!这样数值的高位和低位和计算机电路的计算习惯一致。例如,0x12345678,在内存中就是:
411
411
412
- ```
412
+ ``` txt
413
413
0x78 0x56 0x34 0x12
414
414
```
415
415
@@ -425,19 +425,19 @@ UTF-16 就是利用了这一段空间,他规定:0xD800 到 0xDFFF 之间的
425
425
426
426
UTF-16 和 UTF-32 的码位都是多字节的,也会有大小端问题。例如,UTF-16 中的 ` uint16_t ` 序列:
427
427
428
- ```
428
+ ``` txt
429
429
0x1234 0x5678
430
430
```
431
431
432
432
在大端派的机器中,就是:
433
433
434
- ```
434
+ ``` txt
435
435
0x12 0x34 0x56 0x78
436
436
```
437
437
438
438
在小端派的机器中,就是:
439
439
440
- ```
440
+ ``` txt
441
441
0x34 0x12 0x78 0x56
442
442
```
443
443
@@ -488,23 +488,136 @@ UTF-8 是基于单字节的码位,火车头的顺序也有严格规定,火
488
488
489
489
#### GB2312
490
490
491
- GB2312 是一个古老的标准,兼容 ASCII。
491
+ GB2312 发布于 1980 年,是一个古老的汉字编码标准,兼容 ASCII。
492
+
493
+ > {{ icon.fun }} “GB” 表示 Guo Biao(国标)的意思。
492
494
493
495
GB2312 规定了 6763 个汉字和 682 个特殊符号,共 7445 个字符。
494
496
495
- GB2312 使用 2 字节来编码汉字和特殊符号,而 1 字节的编码保持和 ASCII 相同,从而兼容 ASCII 。
497
+ GB2312 让英文和数字采用和 ASCII 相同的单字节编码,而对于汉字和特殊符号采用双字节编码 。
496
498
497
- 2 个字节分别被称为“区码”和“位码”。的部分都在 0xA1 到 0xFE 区间内,与 ASCII 不重合,且避开了 0xFF
499
+ 其中汉字和特殊符号的双字节编码,两个字节都在 0xA1 到 0xFE 的范围内,避免了与单字节的 ASCII 编码空间冲突。
498
500
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。
500
513
501
- “二级汉字”,即生僻汉字,有 3008 个,这些汉字的编码从 0xA1A1 到 0xA9FE 。
514
+ GBK 中汉字的编号和 Unicode 并不是相同的,这也是 GB2312 编码的文本文件用 UTF-8 或 UTF-16 打开会产生乱码的根本原因 。
502
515
503
- GB2312 也规定了一些特殊字符的编码,例如全角空格 ` 0xA1A1 ` ,以及 1 级汉字和 2 级汉字的划分 。
516
+ 例如汉字 “彭” 就属于 “二级汉字”,在 GB2312 中编号为 0xC5ED,Unicode 中编号为 0x5F6D 。
504
517
505
- ### GBK
518
+ 全角空格 “ ”,在 GB2312 中的编号为 0xA1A1,Unicode 中的编号为 0x3000。
506
519
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 内码的工作流上来,回避掉史山的影响。
508
621
509
622
## C/C++ 中的字符编码
510
623
@@ -534,7 +647,7 @@ GBK 是 GB2312 的扩展,他规定了 21003 个汉字和 682 个非汉字,
534
647
535
648
> {{ icon.detail }} 根据 Windows 官方文档的说法,` wchar_t ` 是 UTF-16LE。
536
649
537
- ### 思考:UTF-8 为什么完美兼容 ASCII
650
+ ### 思考:UTF-8 为什么完美能兼容 ASCII
538
651
539
652
UTF-8 的火车头和车厢,都是 ` 1 ` 开头的,而 ASCII 的单体火车头永远是 ` 0 ` 开头。这很重要,不仅火车头需要和 ASCII 区分开来,车厢也需要。考虑这样一个场景:
540
653
@@ -544,13 +657,13 @@ std::u32string path = "一个老伯.txt";
544
657
545
658
“一个老伯” 转换为 Unicode 码点分别是:
546
659
547
- ```
660
+ ``` txt
548
661
0x4E00 0x4E2A 0x8001 0x4F2F
549
662
```
550
663
551
664
如果让他们原封不动直接存储进 char 数组里:
552
665
553
- ```
666
+ ``` txt
554
667
0x4E 0x00 0x4E 0x2A 0x80 0x01 0x4F 0x2F
555
668
```
556
669
@@ -630,7 +743,7 @@ fmt::println("UTF-32 下,前四个字符:{}", s.substr(0, 4));
630
743
// 会打印 “小彭老师”
631
744
```
632
745
633
- 只有当索引来自 ` find ` 的结果时,UTF-8 字符串的切片才能正常工作:
746
+ 只有当索引是来自 ` find ` 的结果时,UTF-8 字符串的切片才能正常工作:
634
747
635
748
``` cpp
636
749
std::string s = " 小彭老师公开课万岁" ;
@@ -681,7 +794,7 @@ strrev(s.data()); // 会把按字符正常反转,得到 “岁万课开公师
681
794
682
795
### 轶事:“ANSI” 与 “Unicode” 是什么
683
796
684
- 在 Windows 官方的说辞中,有“Unicode 编码”和“ANSI 编码”的说法。当你使用 Windows 自带的记事本程序, 保存文本文件时,就会看到这样的选单:
797
+ 在 Windows 官方的说辞中,有“Unicode 编码”和“ANSI 编码”的说法。当你使用 Windows 自带的记事本程序 ( ` notepad.exe ` ) 保存文本文件时,就会看到这样的选单:
685
798
686
799
![ ] ( img/notepad.png )
687
800
@@ -3195,6 +3308,10 @@ base64.b64decode(secret).decode()
3195
3308
3196
3309
总之,如果你输入中文实在有问题,可以考虑先 Base64 转换成纯英文试试看,反正无论谁都兼容 ASCII。如果这个文本框不区分大小写,还可以试试看只有 A-Z 0-9 的 Base32 编码。
3197
3310
3311
+ ### UTF-7
3312
+
3313
+ TODO
3314
+
3198
3315
### 字符编码猜测
3199
3316
3200
3317
TODO
0 commit comments