为什么这样的字符串{"data":"颸颸"},JSON 库(如jsoncpp)会解析失败?

为什么软件界面上的韩文会显示乱码?

ASCII 和 ANSI 有什么区别?

相信不少人在字符编码上面摔过跟头,这篇文章针对开发中需要了解的字符编码知识进行了简要的讲解,希望能够对大家有所帮助。

1. ASCII 及其扩展

1.1 什么是 ASCII 字符集

字符集就是一系列用于显示的字符的集合。

ASCII 字符集由美国国家标准协会(American National Standard Institute)于 1968 年制定一个字符映射集合。

ASCII 使用 7 位二进制位来表示一个字符,总共可以表示 128 个字符(即2^7,二进制000 0000 ~ 111 1111,十进制0~127)。

ASCII 字符集中每个数字对应一个唯一的字符,如下表:

因为其对应关系非常简单,不需要特殊的编码规则,所以严格来讲 ASCII 不能算字符编码,因为它没有规定编码规则。我们只是习惯性的将 ASCII 字符集称之为 ASCII 码、ASCII 编码。

1.2 ASCII 的扩展

1.2.1 最高位扩展 - ISO/IEC 8859

ASCII 字符集是美国人发明的,其中的字符完全是为其自己量身定制的。随着计算机技术普及到欧洲(如法国、德国)各国,欧洲很多国家使用的字符除了 ASCII 表中的 128 个字符之外,还有一些各国特有的字符,此时欧洲人民发现 ASCII 字符集不能完全表达他们所要表达的内容。怎么办了?他们发现 ASCII 只使用了一个字节(8 位)之中的低 7 位,于是欧洲各国开始各显神通,打起了那 1 个最高位(第 0 位)的主意,将最高位利用了起来,这样又多了 128 个字符,从而满足了欧洲人民的需要。

但因为每个国家的需求不一样,各国都设计了不同的方案。为了结束这种混乱的局面,国际标准化组织(ISO)及国际电工委员会(IEC)联合制定了一系列 8 位字符集的标准,统称为 ISO 8859(全称 ISO/IEC 8859)。注意,这是一系列字符集的统称,如 ISO/IEC 8859-1(也就是常听到的 Latin-1)支持西欧语言,ISO/IEC 8859-4(Latin-4)支持北欧语言等。

完整列表如下(摘自百度百科):
ISO/IEC 8859-1 (Latin-1) - 西欧语言
ISO/IEC 8859-2 (Latin-2) - 中欧语言
ISO/IEC 8859-3 (Latin-3) - 南欧语言,世界语也可用此字符集显示。
ISO/IEC 8859-4 (Latin-4) - 北欧语言
ISO/IEC 8859-5 (Cyrillic) - 斯拉夫语言
ISO/IEC 8859-6 (Arabic) - 阿拉伯语
ISO/IEC 8859-7 (Greek) - 希腊语
ISO/IEC 8859-8 (Hebrew) - 希伯来语(视觉顺序)
ISO 8859-8-I - 希伯来语(逻辑顺序)
ISO/IEC 8859-9 (Latin-5 或 Turkish) - 它把 Latin-1 的冰岛语字母换走,加入土耳其语字母。
ISO/IEC 8859-10 (Latin-6 或 Nordic) - 北日耳曼语支,用来代替 Latin-4。
ISO/IEC 8859-11 (Thai) - 泰语,从泰国的 TIS620 标准字集演化而来。
ISO/IEC 8859-13 (Latin-7 或 Baltic Rim) - 波罗的语族
ISO/IEC 8859-14 (Latin-8 或 Celtic) - 凯尔特语族
ISO/IEC 8859-15 (Latin-9) - 西欧语言,加入 Latin-1 欠缺的芬兰语字母和大写法语重音字母,以及欧元符号。
ISO/IEC 8859-16 (Latin-10) - 东南欧语言。主要供罗马尼亚语使用,并加入欧元符号。

我们在数据库中常见到的 Latin-1、2、5、7 其实就是上面提到的针对特定语言的 ASCII 扩展字符集。

1.2.2 多字节扩展 - GB 系列

前面讲到了,欧洲各国有效利用闲置的最高位对 ASCII 字符集进行了扩展。可是欧洲人民没有想到在大洋彼岸有着一个拥有五千年历史的伟大民族,她拥有着成千上万的汉字,1 个字节显然不够表达如此深厚的文化底蕴。

于是当计算机引入到中国之初,国家技术监督局就设计了 GB 系列编码方案(GB 为 guo biao 的简称)。
GB 编码方案使用 2 个字节来表达一个汉字。同时为了兼容 ASCII 编码,规定各个字节的最高位(首位)必须为 1,从而避免了和最高位为 0 的 ASCII 字符集的冲突。

GB 系列字符集经历下面的几个发展过程:

编码名称 发布时间 字节数 汉字范围
GB2312 1980 年 变字节(ASCII 1 字节,汉字 2 个字节) 6763 个汉字
GB13000 1993 年第一版 变字节(ASCII 1 字节,汉字 2 个字节) 20902 个汉字
GBK Windows95 中 2 个字节 21886 个汉字和图形符号(含 GB2312,BIG5 中所有字符)
GB18030 2000 年第一版 变字节(ASCII 1 字节,汉字 2 个或 4 个字节) 27484 个汉字

每一次迭代,支持的字符数量都会增加,而且每一次迭代都会保留之前版本支持的编码,所以做到了向上兼容。

1.2.3 全角与半角

因为汉字在显示器上的显示宽度要比英文字符的宽度要宽一倍,在一起排版显示时不太美观。所以 GB 编码不仅仅加入了汉字字符,而且包括了 ASCII 字符集中本来就有的数字、标点符号、字母等字符。这些被编入 GB 编码的数字、标点、字母在显示器上的显示宽度比 ASCII 字符集中的宽度宽一倍,所以前者称为全角字符,后者称为半角字符。

2. ANSI

2.1 ANSI 与代码页

前面说到了世界各国针对 ASCII 的扩展方案(如欧洲的 ISO/IEC 8859,中国的 GB 系列等),这些 ASCII 扩展编码方案的特点是:他们都兼容 ASCII 编码,但他们彼此之间是不兼容的。微软将这些编码方案统称为 ANSI 编码。故 ANSI 并不是特指某一种编码方案,只有知道了在哪个国家,哪个语言环境下,才知道它具体表示哪个编码方案。

在 Windows 操作系统上,默认使用 ANSI 来保存文件(从Windows 10开始默认使用UTF8编码)。操作系统是如何知道 ANSI 到底应该表示哪种编码了,是 GBK还是 ASCII,或者还是 EUC-KR 了? Windows 通过一个叫”Code Page”(翻译为中文叫代码页)的东西来判断系统的默认编码。

简体中文操作系统默认的代码页是 936,它表示 ANSI 使用的是 GBK 编码。
GB18030 编码对应的 windows 代码页为 CP54936。

可以使用命令chcp来查看系统默认的代码页.

汉字“𤭢”(念 suì)只包含在 GB18030 中,GB2312、GB13000、GBK 中均不包含。默认情况下,在 Visual Studio 中输入该汉字,visual studio 会使用 CP936(即 GBK)来保存代码文件,但如果在代码文件中输入该汉字,visual studio 弹出如下提示要求用户选择代码页:

2.2 更改默认代码页

2.2.1 chcp 命令

可以使用chcp命令来更改默认代码页,如chcp 437将默认代码页更改为 437(美国)。

2.2.2 控制面板

在“控制面板”–>“区域和语言”–> “更改系统区域设置”中更改系统默认的代码页为“中文(简体,中国)”。

2.2.3 代码修改

也可以通过代码更改默认的代码页:

1
2
3
4
char *setlocale(
int category,
const char *locale
);

3. Unicode

3.1 Unicode 产生背景

各个国家使用不同的编码规则,虽然他们都是兼容 ASCII 的,但它们相互却是不兼容的。

试想法国人 Jack 写了一封名为”love_you.txt”的信,传给了他的德国朋友 Rose,Rose 想要在 windows 系统上打开这个文件,她需要知道德国使用的字符编码是 Latin-1,然后还要确保她的计算机上安装了该编码,才能顺利的打开这个文件。
如果上面这些还能忍受,那么随着网络的发展,你从互联网上获取的文件,你很有可能不知道它来自哪个国家,使用的哪种编码。这也就是 Email 刚诞生时常常出现乱码的原因,发信人和收信人使用的编码可能是不一样的。

于是The Unicode Standard(统一码标准)横空出世,它由 The Unicode Consortium 于 1991 年发布,我们习惯称它为 Unicode 字符集。

Unicode 字符集和 ASCII 字符集一样,也只是一个字符集合,标记着字符和数字之间的映射关系,它不包含任何编码规则和方案。和 ASCII 不一样的是,Unicode 字符集支持的字符数量是没有限制的(具体可以参考 Unicode 规范)。

我们通常认为的 Unicode 字符固定占用 2 个字节的观点是错误的。如“𤭢”(念 suì)Unicode 码为D852 DF62

那么 Unicode 字符是怎样被编码成内存中的字节的了?它是通过 UTF(Unicode Transformation Formats)实现的,比较常见得有 UTF-8,UTF-16。

在 windows 系统上汉字默认使用 CP936(即 GBK 编码),占 2 个字节。而大多数 Unicode 字符的 Unicode 码值也占 2 个字节,所以大多数人误以为汉字字符串在内存中的值就是 Unicode 值,这是错误的。
可以从 站长工具-Unicode 查询汉字的 Unicode 码值。

3.3 字符集与字符编码的区别

从 ASCII、GB2312、GBK、GB18030、Big5(繁体中文)、Latin-1 等采用的方案来看,它们都只是定义了单个字符与二进制数据的映射关系,一个字符在一个方案中只会存在一种表示方式,所以我们说 GB2312 是字符集还是字符编码方式都无所谓了。但是 Unicode 不一样,Unicode 作为一个字符集可以采用多种编码方式,如 UTF-8, UTF-16, UTF-32 等。所以自 Unicode 出现之后,字符集与字符编码需要明确区分开来。

3.4 UTF-16 编码的缺点

UTF-16 编码方式规定用两个或四个字节来表示所有的字符。对于 ASCII 字符保持不变,只是将原来的 7 位扩展到了 16 位,其高 9 位永远是 0。如字符’A’:

1
2
ASCII: 100 0001
UTF-16: 0000 0000 0100 0001

可以看到对于 ASCII 字符,UTF-16 的存储空间扩大了一倍,UTF-16 并不是完全兼容 ASCII 字符集。这对于那些 ASCII 字符集已经满足需求的西方国家来说完全是没必要的,而且 ASCII 字符经过 UTF-16 编码之后高字节始终是 0,导致很多 C 语言函数(如strcpy,strlen)会将此字节视为字符串的结束符'\0',从而出现错误的计算结果。
而且,UTF-16 还存在大小端的问题,“𤭢”(念 suì)Unicode 码在大端系统上为D852 DF62,小端系统上为52D8 62DF
因此,UTF-16 一开始推出的时候就遭到很多西方国家的抵制,影响了 Unicode 的推行。于是后来又设计了 UTF-8 编码方式,才解决了这些问题。

3.5. Unicode 字符集常用编码方式:UTF-8

3.5.1 UTF-8 概述

UTF-8 是互联网上使用最广泛的 Unicode 字符集编码方式。UTF-8 编码的最小单位由 8 位(1 个字节)组成,UTF-8 使用一个至四个字节来表示 Unicode 字符。另外,UTF-8 是完美兼容 ASCII 字符集的,这一点可以通过下面的 UTF-8 的编码规则得到证明。

3.5.2 UTF-8 编码规则

UTF-8 编码规则很简单:
(1)对于 ASCII(单字节字符)字符,采用和 ASCII 相同的编码方式,即只使用一个字节表示,且该字节第一位为 0.
(2)对于多字节(2~4 字节)字符,假设字节数为 n(1 < n <= 4),第一个字节:前 n 位都设为 1,第 n+1 位设为 0;后面的 n-1 个字节的前两位一律设为 10。所有字节中的没有提及的其他二进制位,全部为这个符号的 unicode 码。

Unicode 符号范围(十六进制) UTF-8 编码方式(二进制)
单字节:00000000-0000 007F 0xxxxxxx
双字节:00000080-0000 07FF 110xxxxx 10xxxxxx
三字节:00000800-0000 FFFF 1110xxxx 10xxxxxx 10xxxxxx
四字节:0001 0000-0010 FFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

3.5.2 UTF-8 BOM

BOM(byte order mark)从字面意义来看是标记字节顺序的。最早出现的原因是因为 UTF-16 和 UTF-32 编码采用 2 个或 4 个字节表示一个字符,面临大小端的问题。为了区分是使用的大端(Big Endian,简称 BE)还是小端(Little Endian,简称 LE),采用了在串的前面加入指定的字节加以区分,UTF-16 大端加入FE FF,小端加入FF FE. 比如, 字符串“ABC”的 UTF-16 编码为 00 41 00 42 00 43,对应的各种的字节序列如下:

序列 数据
UTF-16BE(withoutBOM) 00 41 00 42 00 43
UTF-16LE(withoutBOM) 41 00 42 00 43 00
UTF-16BE(with BOM) FE FF 00 41 00 42 00 43
UTF-16LE(with BOM) FF FE 41 00 42 00 43 00

因为 UTF-8 和 ASCII 都是单字节序列,二者不好区分,微软采用在 UTF-8 编码的字符串前也加入 BOM(3 个字节EF BB BF)来标记 UTF-8 编码的串。UTF-8 BOM 这一规范大多在 windows 下被使用,在其他平台下用的很少使用,如:Linux 全部采用 UTF-8 编码,不存在要区分的情况;HTTP 协议中可以包含Content-Type:text/html; charset=utf-8这样的说明,也不需要区分。

文章图片带有“CSDN”水印的说明:
由于该文章和图片最初发表在我的CSDN 博客中,因此图片被 CSDN 自动添加了水印。