字符集编码的发展与简介
[TOC]
1. 文前
刚刚入职的时候,我们所有项目都是“使用多字节字符集”的,当时负责的内容也是在比较独立的库内,不涉及到接口相关的东宫西,所以入职一年的时间里,我对字符串和字符集编码的相关知识都是比较模糊的,仅仅停留在“能跑就行”的阶段。
后来在组内负责的内容更多了,开始编写一些接口函数、导出函数和回调函数等等,也遇到了 CString 类型、LRCSTR 类型跨库调用时返回乱码的问题,所以开始意识到有必要花时间研究一下字符串和字符集的相关内容。后来也开始接一些乱七八糟的活儿,涉及到读写 excel、txt、网络通讯等内容,这些方面对字符串的使用更多,也更五花八门,我也开始意识到,十分有必要弄明白字符集编码相关的内容了,至少把各个专业名字及其之间的关系弄明白。于是从 2023 年 3 月份开始,断断续续的研究字符串和字符集编码的内容,一直到 2023 年的 10 月份,已经基本弄明白了字符集编码模型、VS 中字符串的使用方法等内容。但是随着研究的深入,更多的问题冒了出来,所以后期还会继续补充和完善字符串相关的内容。
相关的文档我自己已经整理出了两篇,这篇是比较完善的“字符集编码”相关的内容。发出来,也算是一个总结和分享。
本文依次介绍了ASCII、ANSI 和 UNICODE 三种字符集的发展史。穿插介绍了国标码(GB)、GBK 编码方式、多字节字符集(MBCS)、Unicode三种编码形式、BOM、Unicode 与 UCS 的关系等问题。最后以 Unicode 为例总结了字符集编码模型,该模型适用于目前所有的字符集。
全文的参考文献在每章节的最后都全部列出了。
2. 字符集编码
在不知道字符串是什么编码的情况下使用字符串是没有意义的。尤其是在跨库、跨语言、跨平台编程,接口的制作、调用,读写文件、数据库时,明确字符串的编码就尤其重要。
在使用字符串相关函数的时候,例如 _tcslen(),参数的使用也与字符集编码有关。
编码(ecodng)是指以某种形式表示信息的过程。1
例如:人类语言就是一种编码系统,我们通过约定好的语言或手势来表示信息。文字是一种衍生的编码系统,通过这种编码系统,文字与语言(或手势)相对应,可以正确“解码”出要传递的信息。
对计算机来说,他所知道的唯一表示方式就是二进制数,而我们在计算机上要传递的信息其实是文字,那么文字怎样让计算机识别,就需要一套编码系统,于是衍生出了字符集编码(character set encoding)的概念。
字符集编码至少包含两个组成部分:一组需要表达的字符(字符集)和一个编码系统,该系统能够将这套字符转为计算机内部可以识别的单元。
3. ASCII码
3.1 ASCII发展史
美国国家标准学会(American National Standard Institute,简称为 ANSI)于1968年发布了著名的ASCII编码标准(American Standard Code for Information Interchange,美国信息交换标准码)2。
ASCII 是单字节编码,即用1个字节(8位)来编码英文字母(最前面的1位是 0,用于奇偶校验),也就是说,将英文字母和一些常用的字符用7位来对应,有128(2^7)个不同的组合。比如说大写字母“A”对应的十进制数字为“65”,对应的二进制为 “01000001”,转换成十六进制为 “0x41”。
大家把 ASCII 看作最原始的(当然还有更早的,但是我们没有必要知道)编码格式。由于我们现在常用的字符数(emoji 之类的也是字符)远远多于 128,所以现在几乎没人直接使用 ASCII 编码了。另外,目前所有新的编码全都兼容 ASCII。
4. ANSI
4.1 ANSI发展史
随着个人计算机的发展和普及,软硬件制造商面临着走出美国、标准国际化的问题。于是便出现了一个问题:一开始的 ASCII 编码标准无法表示其他国家的语言符号。即使是在主流语言使用拉丁字母的欧洲,也存在很多拓展体,比如法语中的é,挪威语中的 Å,都无法用 ASCII 表示。 2
于是大家开始把 ASCII 码中最前面未使用的那一个 bit 拿来使用,原来的128种状态就变成了256种状态。这样既完美兼容了原先的ASCII,又能表达欧洲国家特定字符。
比如 IBM 为 DOS 开发了 codepage 437,采用 ASCII 并添加英式英语和一些主要欧洲语言所需的字符,以及图形字符(如用于创建用户界面元素的 DOS 应用程序的线条绘制字符)。IBM 还创建了其他 DOS 代码页,以满足使用其他语言或脚本的其他市场。例如,codepage 852用于使用拉丁字母的东欧语言,codepage 855用于俄语和其他一些使用西里尔字母的东欧语言等。当微软开始开发 Windows 时,开发了 codepage 1252用于编码西欧字符。苹果、施乐等也都制定了自己的 codepage 标准。 1 2
但是这样依然存在两个问题,一是各个公司各自制定自己的扩展编码,相互之间互不兼容;二是即便是256个字符,仍然无法在一个 codepage 中表达所有的欧洲字符。
为了解决这两个问题,国际标准化组织 ISO 和 IEC 联合制定了一组标准叫 ISO/IEC 8859(简称 ISO 8859)。这里需要注意的是,ISO 8859 并不是一个具体的字符集编码标准,而是一组字符集的合称,称为ISO 8859-n,n=1,2,3,…,15,16(其中12未定义,所以共15个代码页)。其中使用最广泛的是美国国家标准协会(ANSI)起草的标准 ISO/IEC 8859-1,又称 latin 1,收录了西欧常用字符,包括德语、意大利语、葡萄牙语、西班牙语等。 2
上述的字符集编码标准都是 单字节编码 。而且各个字符集编码标准对多出来的128个字符的定义是不同的,即各个 codepage 之间是并不兼容的。
当计算机厂商进入东亚市场时,对于中日韩这种表意文字,256的代码空间根本就不够用。因此,中文、日文和韩文的代码页使用双字节编码,即混合使用一个或两个字节序列来表示字符。 1
4.2 术语解释:代码页、代码点、代码空间、ANSI
有必要再解释一下术语微软代码页(Windows codepages)和 ANSI 的由来:
微软在开发Windows时,美国国家标准协会(ANSI)正在起草一项标准,该标准最终被国际标准化组织收录并成为 ISO 8859-1 “Latin 1”。微软基于该标准创建了代码页 1252,应用于早期的 Windows,并开始将这套标准称之为“ ANSI 代码页(codepage)” 1 3。而且微软在后面收录其它字符集编码标准时,仍然将这些采用了以8位为编码单元的字符集称为代码页(codepage)。
也就是说,**目前在Windows上下文中,术语“ANSI文本”、“代码页”或“ANSI代码页”应该被理解为使用8位编码单元的微软代码页而不是Unicode编码的文本。1
这些代码页(codepage)不同的地方在于其收录的编码字符的个数和种类。在一个代码页中,每个字符在当前代码页中的整数序号,称之为代码点(codepoint),或者说的专业一点,代码点表示给定字符的编码表示形式。编码标准中代码点的有效范围称为代码空间(codespace)。1
4.3 国标码(GB)
国标码(GB码)是中文系统的ANSI编码,作为中国公民,有必要稍微深入了解一下GB系列编码标准。参考文献 2 对GB码进行了比较深入的介绍,如想了解请转至该文章。
(1)GB2312
中国国家标准总局于1980年发布,7445个字符。属于变长编码,用 1~2 个字节表示字符,而字节最高位的值决定了字符占用的字节数。2
(2)GBK
中国国家标准总局于1995年指定,是汉语拼音GuoBiaoKuozhan(国标扩展)的首字母缩写。GBK完全兼容GB2312-1980,同时收录了Big5中全部的繁体字,以及GB2312中没有的其他汉字 2。共收录了21886个字符,在微软代码页中的编号为CP936 4。
(3)GB18030
随着字符集编码国际标准ISO/IEC 10646 和Unicode的不断发展,越来越多的字符被纳入其中,而GBK一共才2万多个字符(并且主要是汉字),有点赶不上时代了。于是中国国家标准总局于2000年又指定了新的标准GB18030-2000,用来代替GBK,后序又发布了GB18030-2005,补充了一些字符。2
GB18030的目标是向Unicode/UCS对齐。GB18030在GBK基础上增加了CJK(中日韩)统一汉字扩充的汉字,还包含多种我国少数民族文字(如藏、蒙古、傣、彝、朝鲜、维吾尔文等)。GB18030共有70000多个字符。2
GB18030包含三种长度的编码:单字节的 ASCII、双字节的 GBK(略带扩展)、以及用于填补所有Unicode 码位的四字节UTF区块。2
在微软代码页中的编号为CP54936。4
(4)Big5
Big5,是台湾使用的编码标准,编码了台湾使用的繁体汉字,大概有8千多个。4
(5)HKSCS
HKSCS,是中国香港使用的编码标准,字体也是繁体,但跟Big5有所不同。4
4.4 国标码编码形式
由上面的内容可知,中国大陆主要有三种ANSI编码:GB2312、GBK和GB18030,这三种字符集的编码形式都是一样的,只是字符集的代码空间不同。因此,接下来以GBK编码为例,介绍一下国标码的编码形式。
GBK中是用1个字节或2个字节来表示一个字符:GBK的中文编码是双字节来表示的,英文编码是用ASCII码表示的,即用单字节表示。但GBK编码表中也有英文字符的双字节表示形式,所以英文字母可以有两种GBK表示方式。为区分中文,将其最高位都定成1。英文单字节最高位都为0。当用GBK解码时,若高字节最高位为0,则用ASCII码表解码;若高字节最高位为1,则用GBK编码表解码。5
GB2312编码是基于区位码的,在一个双字节编码中,第一个字节表示“区号”,后一个字节表示“位号”。中文汉字的编号区号是从16开始的,位号从1开始。前面的区号有一些符号、数字、字母、注音符号(台)、制表符、日文等等。5
对于汉字的双字节编码来说,其编码方式位:0xA0+区号,0xA0+位号。如汉子“安”,区位号是1618(十进制),那么“安”字的GB2312编码就是 0xA0+16 0xA0+18 也就是 0xB0 0xB2 。根据区位码表,GB2312的汉字编码范围是0xB0A1~0xF7FE。5
双字节符号可以表达的64K空间如下图所示。绿色和黄色区域是GBK的编码,红色是用户定义区域。没有颜色区域是不正确的代码组合:5
4.5 MBCS (多字节字符集)
MBCS(Muilti-Bytes Charecter Set, 多字节字符集) 是一种统称,是微软引入的概念。其包含两种形式,分别是单字节字符集(SBCS)和双字节字符集(DBCS)。最常见的 MBCS 实现是双字节字符集 (DBCS)。 通常,Visual C++对 DBCS 完全启用 6。且在MFC中,MBCS仅支持DBCS 7。
单字节字符集(Single-Byte Character Set, 简写为SBCS):它所有字符都只有一个字节的长度。常见字符集有:ASCII码和扩展ASCII码。SBCS字符串由一个零字节 ‘/0’ 结尾,数据类型是char。 8 9
双字节字符集(Double-Byte Character Set, 简写为DBCS):这个方案是为了解决中日韩三国象形文字符号很多而诞生的,DBCS的代码空间扩展到了两个字节,也就是2^16位。其中0 ~ 127是与ASCII一致的,仅用一个字节就可以表示;但是高于127的代码点就需要用两个字节来表示了 10 。当宽度是两个字节时,第一个字节是前导字节,与尾随字节一起指定一个唯一的字符编码 7 。前导字节的范围是固定的,哪个范围的字节可以是前导字节取决于正在使用的代码页。 例如,日语代码页 932 将 0x81 到 0x9F 的范围用作前导字节,而朝鲜语代码页 949 使用不同的范围 6 。根据本文上一节的内容,汉字的前导字节范围为0xA0~0xFF。
因此在DBCS下,当拿到一个字符串的时候,就需要检查每个字节以确定它是否为双字节存储单元的首个字节。
在微软下,不论哪个地区,多字节字符集(MBCS)仅支持双字节字符集(DBCS)。7
综上所述,多字节字符集 (MBCS) 是一种支持无法用单字节表示的字符集(中日韩字符)的旧方法。 在MSDN中,建议在新的开发中,对所有文本字符串(最终用户不会看到的系统字符串也许可以除外)使用 Unicode。 MBCS 是旧技术,不建议用于新开发。6
5. Unicode
5.1 什么是Unicode
ANSI 编码解决了不同国家地区的编码需求,但是不适合跨地区交流,Unicode 应运而生:对于地球上任意一个字符,都给它一个唯一的数值。11
Unicode 为世界上所有字符都分配了一个唯一的数字编号,因此又被称为万国码 12 。整个编码范围 从 U+0000 到 U+10FFFF 包括 1,114,112 个代码点。减去 66 个作为非字符位置,减去 2,048 个为代理保留的范围,Unicode 代码空间包括 1,111,998 个可分配的代码点。13
Unicode 编码字符集(coded character set,可简写为CCS) 根据整数值进行编码,按照惯例,Unicode 代码点以十六进制表示法表示,至少四位数字,前面有 “U+”;因此,例如,“U+0345”、“U+10345” 和 “U+20345”。同样按照惯例,任何超过四位数字的前面的0都会被省略,即 “U+0456” 是合法的,但 “U+03456” 则是非法的。Unicode 中的每个字符都可以通过其 代码点(codepoint) 或名称进行唯一标识。13 14
Unicode 代码空间(codespace) 的范围从 U+0000 到 U+10FFFF。借用 ISO/IEC 10646 的术语,代码空间以 17个平面(每个平面64K个代码点)来描述。因此,平面 0 包括代码点 U+0000 ~ U+FFFF,平面 1 包括代码点 U+10000 ~ U+1FFFF,其他平面也依此类推。13
在 Unicode 的原始设计中,所有字符的代码点范围都在 U+0000 ~ U+FFFF 范围内。与此一致,平面 0 被设置为代码空间中所有最常用字符被编码的部分,并被指定为 基本多语言平面(Basic Multilingual Plane,简写为BMP) 。代码空间的其余部分,即平面 1 到 16,统称为 补充平面(Supplementary Planes) 。在 TUS 3.0.1 之前,字符仅在 BMP 中分配,在 TUS 3.1中,字符才首次在补充平面中分配。13
简单来说,Unicode 就是编码字符集,建立了字符与编号之间的联系。至于编号怎么对应到二进制,就需要建立一套规则,现在 Unicode 有三种编码形式:UTF-8,UTF-16,UTF-32。(UTF: Unicode Transformation Format)
关于 Unicode,参考文献 14 是 Unicode 官网提供的一些学习资料,其中还有一些关于 Unicode 的学习视频和演讲,如想了解转至该文献。
5.2 Unicode的三种编码形式
(1)UTF-16
由于 Unicode 的早期历史和最初的设计目标是具有统一的16位编码,因此今天许多人认为 Unicode 仅为 16 位编码。Unicode 现在支持三种不同的编码形式,但通常没有一种比其他形式更优先。但是,UTF-16 可能被认为具有特殊的重要性,因为它是与 Unicode 的流行印象相匹配的编码形式 13,尽管这种印象是有误的。
目前的 UTF-16 使用变长字节表示,使用一个或两个 16 位代码单元(UTF-16 的每个代码单元是 2 字节,也就是 16 位)。平均而言,最常用的字符是在基本多语言平面中编码的。因此,对于许多文本来说,从来没有必要引用 U+FFFF 以上的字符。也就是说,大部分字符可以用 2 个字节(16 位)表示,只有一小部分不常用、编号高的字符需要用 4 个字节(32 位)来表示。
为了表示 Unicode 引入的增补平面中的码点(平面 1 ~ 16,码点范围为 0x10000 ~ 0x10FFFF ), UTF-16 中使用 “代理机制” 来解决这个问题。其基本思想就是使用两个基本平面中未定义(或未使用)的码点合起来代替一个增补平面的码点。15
基本平面中的字符(除 U+D800 ~ U+DFFF 外)仍然使用固定两字节直接映射的方式编码,即基本平面字符的字符编号(码点值)就是字符编码。增补平面的字符使用“代理机制”这一编码算法进行编码,使用代理机制后需要使用 2 个代码单元(4 字节)来表示 Unicode 增补平面中的码点。15
基本平面中用来代替增补平面码点的未使用的码点区域被称为代理区(surrogate code units),其码点范围为 0xD800 ~ 0xDFFF,共 2048 个码点。UTF-16 将这些分为两半:0xD800 ~ 0xDBFF 被称为高代理码点(high surrogates),0xDC00 ~ 0xDFFF 被称为低代理码点(low surrogates)。这两个范围中每个区域中都有 1024 个代码值,则有 1024×1024 = 1048576 种可能的组合,这与补充平面中的代码点数完全匹配。因此,在 UTF-16 中,以一对代理码点,或称为 代理项对(surrogate pair) 的方式对补充平面中的字符进行编码。13
应该指出的是,代理对必须由高代理项和低代理项组成,如果在数据中遇到未配对的高或低代理项,则将其视为格式不正确,不得解释为字符。13
参考文献 15 中详细介绍了UTF-16的编码原理,如想了解转至该文章。
目前,UTF-16 是使用最多的编码形式,Windows 底层中都是使用的 Unicode 的 UTF-16 编码形式。3
(2)UTF-8
一开始开发 UTF-8 编码形式是为了使一些8位的软件也能正确使用 Unicode 字符集。特别是对于某些文件系统,其中某些字节值具有特殊意义(例如,0x2A 在 ASCII 中是 “*”,通常用于表示通配符)13。目前,UTF-8 在 HTML 和类似协议中很受欢迎 14 。
对 UTF-8 而言,一个代码单元为 8 位。UTF-8 使用变长字节表示,即使用的字节数可变,从 1 到 4 个字节长度不等,代码点小的使用的字节就少,代码点大的使用的字节就多。 UTF-8 中,序列初始字节和非初始字节有明显不同的标记,因此可以立即确定 UTF-8 代码单元是序列中的初始字节还是后续字节。其次,UTF-8 序列中的第一个字节根据其范围提供了序列长度的明确指示。13
上述的两个特征结合起来使得处理 UTF-8 序列非常高效。与 UTF-16 一样,这种编码形式比各种遗留的多字节编码(ANSI 编码)要高效得多。13
UTF-8 编码方式的一个有趣的附加效果是:ASCII 编码的数据也自动符合 UTF-8 13 。这也就是说,只有 UTF-8 兼容 ASCII;UTF-16 和 UTF-32 都不兼容 ASCII,因为他们没有单字节编码。
| Unicode 代码点范围(十六进制) | UTF-8 字节流(二进制) |
|---|---|
| U+0000 ~ U+007F | 0xxxxxxx |
| U+0080 ~ U+07FF | 110xxxxx 10xxxxxx |
| U+0800 ~ U+FFFF | 1110xxxx 10xxxxxx 10xxxxxx |
| U+10000 ~ U+10FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
上表是 UTF-8 对 Unicode 字符集的编码形式,对于前 0x7F 的字符,UTF-8 编码和 ASCII 码是一一对应的。如果一个字符在 0x0800 ~ 0xFFFF 之间,那转化到 UTF-8 需要用三字节模板,使用 16 个码位,每个 x 就是一个码位。16
(3)UTF-32
UTF-32 编码形式很容易解释:每个代码点都使用等于代码点标量值的 32 位整数进行编码 13 。也就是说,不管什么字符直接用四个字节来存储。比如 “马” 的 Unicode 为:U+9A6C,直接转化为二进制,就表示为:00000000 00000000 10011010 01101100。
(4)各种编码形式的对比分析
| 代码点范围 | UTF-8 | UTF-16 | UTF-32 |
|---|---|---|---|
| U+0000 ~ U+007F(2进制:0 ~ 2^7^ ;10进制:0 ~ 128) | 1 byte | 2 byte | 4 byte |
| U+0080 ~ U+07FF(2进制:2^7^ ~ 2^11^;10进制:129 ~ 2048) | 2 byte | 2 byte | 4 byte |
| U+0800 ~ U+FFFF(2进制:2^11^ ~ 2^16^;10进制:2049 ~ 65536) | 3 byte | 2 byte | 4 byte |
| U+10000 ~ U+10FFFF(2进制:2^16^ ~ 17×2^16^;10进制:65537 ~ 1114112) | 4 byte | 4 byte | 4 byte |
从上表中也可以看出,当程序中大量使用英文字母时,UTF-8 是更为合适的,因为其占用了更少的字节数,并且与 ASCII 编码完全兼容。尤其是当程序需要与仅支持 8 位数据的软件相互支持时,UTF-8 就成为了 Unicode 的唯一选择。
而对于大量使用基本多语言平面(BMP)内字符的程序,例如大量使用中文的软件中,对于 UTF-16 的编码形式,所有字符全部编码为 16 位,可以非常快速的完成代理项的测试。在存储方面也为多语言提供了良好的平衡。在这些因素下,许多支持Unicode的应用程序将 UTF-16 作为主要的编码形式。13
UTF-32 的优点在于每个字符的大小完全相同,永远不需要测试代码单元的值来确定它是否是序列的一部分,读取简单。但是其数据整体很大,浪费存储空间。UTF-32 的实际使用是非常少的。
5.3 Unicode与UCS发展史
Unicode 项目始于 1988 年,是由来自多家公司的代表合作开发的、一种可以支持世界上所有脚本的单一字符集编码标准。1991年1月成立了 Unicode 联盟,并于同年 10 月发布了 Unicode 标准的第一版——TUS 1.0(Unicode 标准的编号版本通常引用为 “TUS x.y”,其中 x.y 是版本号。例如 TUS 3.1)。13
但其实早在 1984 年,一个 ISO/IEC 联合工作组就已经成立了,并开始制定支持世界上所有书写系统的国际字符集标准,这被称为通用字符集(Universal Character Set,简称为UCS)。在 1989 年,该国际组织发布了 ISO/IEC 10646 字符集,该标准使用 31 位代码空间,允许超过 2 亿个字符。13
后来人们意识到这两个组织正在努力实现类似的目标,即一个所有人都可以使用的通用标准。于是在 1991 年,ISO/IEC 工作组和 Unicode 联盟开始讨论这两个标准的合并,合并的完整细节经过多年制定,但最重要的问题很早就解决了。
Unicode 联盟一开始发布的 TUS 1.0 中是使用统一的 16 位编码形式设计的,最多允许 65,536 个字符,但与 ISO/IEC 10646 的 2 亿个字符还是差别很大。在合并初期,即 TUS 1.1 开始,这 65,536 个字符被合并整理为了 ISO/IEC 10646 的子集。换句话说,Unicode 定义的 65,536 个字符中的任何一个都可以在 ISO/IEC 10646 中被找到,但对于 64K 以上的编码空间, Unicode 是无法使用的,只能以 ISO/IEC 10646 的格式来定义字符。
在合并过程中,Unicode 联盟意识到 65,536 个字符不足以覆盖所有的中国文字,因此需要放弃一开始统一的16位编码形式,来提高字符集的容量。最终的解决方案是:两种标准都采用了经修订的16 位编码形式,即 UTF-16 和 UCS-2 。UCS-2 在本质上等同于 UTF-16 ,字符集也与 TUS 1.1 中的内容相同,但 UCS-2 没有代理项(关于这一名词见参考文献 [12] 的 4.1 和参考文献 [16] ),也就是说 UCS-2 只能表示基本平面(即 BMP,第 0 平面)中的 2^16 个码点。换句话说,UCS-2 是固定两个字节的,而 UTF-16 是变长的,即可能是两个字节,也可能是四个字节。
这种编码形式允许支持超过一百万个字符。ISO/IEC 10646 仍然可以容纳更多的字符,但是人们承认一百万个字符已经足够了。最终,ISO/IEC 标准通过永久保留代码空间的其余部分,正式限制了可分配字符的数量,使与 Unicode 字符集保持一致。
但在当时,还有很多 8 位的进程,无法正确处理 16 位数据。于是开发了一种 8 位编码形式,称为 UTF-8,它能够支持全部的潜在字符,并且被两个标准采用。
Unicode 中原始的单一统一 16 位编码被UTF-16和UTF-8取代,在 Unicode 的TUS 2.0 中正式确定(Unicode Consortium 1996)。该版本的语言仍然将 16 位编码形式作为主要表示形式,但其实内部发生了根本的变化。
自首次发布以来,ISO/IEC 10646 一直支持 32 位编码形式,称为 UCS-4,支持整个 ISO/IEC 10646 的代码空间。于是,Unicode 引入了一种 32 位编码形式,称为 UTF-32(见上一节),并在 Unicode 的 TUS 3.1 标准中正式采用。实际上,UCS-4 等同于 UTF-32,但代码空间除外:根据定义,UCS-4 可以表示 U+0000 ~ U+7FFFFFFFFF(整个 ISO/IEC 10646 代码空间)范围内的代码点,而 UTF-32 只能表示 U+0000 ~ U+10FFFF(整个 Unicode 代码空间)范围内的代码点。 13
合并的主要结果是 Unicode 现在与 ISO/IEC 10646 保持同步,并且现在支持基于 8 位、16 位和 32 位代码单元的三种编码形式。13
到目前为止,Unicode 标准与国际标准 ISO/IEC 10646(即通用字符集,简称UCS)密切合作,以确保这两个标准是随时协调同步的,保证Unicode 编码形式与 ISO/IEC 10646 中定义的编码形式完全对应。14
Unicode 标准一直在继续发展到现在,工作仍在继续,目的是使该标准更加完整,涵盖世界上更多的书写系统,纠正细节上的错误,并使其更好地满足实施者的需求。Unicode 3.0 版于 2000 年发布(The Unicode Consortium 2000),引入了 10,000 多个新字符 13 。Unicode 3.1 版于 2001 发布,此版本又添加了 44,946 个新字符,使字符总数达到 94,140 个编码字符 13 。最新的版本是 Unicode15.0.0,于 2022.9.13 发布,此版本增加了 4,489 个字符,包括 20 个 emoji 和 4,193 个中日韩字符,目前 Unicode 的字符总数达到了 149,186个 17 。
5.4 Unicode编码方案
在上面两节中简单提到了,16 位和 32 位编码形式引发了与字节排序相关的问题(具体描述可以参考文献13 )。因此,当涉及 16 位或 32 位代码单元时,这些代码单元可能会作为一组字节进行处理,并且这些字节在通过线路传输或存储在磁盘上之前必须按串行顺序排列。13
有两种方法可以对构成 16 位或 32 位代码单元的字节进行排序。一种是从高阶(最重要)字节开始,以低位(最不重要)字节结束,这通常被称为大端序(big-endian, BE)。另一种方式正好相反,通常被称为小端序(little-endian, LE)。对于16位和32位编码形式,特定编码形式的规范以及特定的字节顺序称为字符编码方案(character encoding scheme)。13
对于 UTF-16 编码形式的数据,只能通过两种方式之一进行序列化。就它的实际组织方式而言,它必须是大端序或小端序。但是,Unicode允许以三种方式描述数据的编码方案:大端序、小端序和未指定端序,UTF-16 和 UTF-32 都是如此。13
因此,Unicode 提供了七种字符编码方案(Character encoding scheme,简称CES):
- UTF-8
- UTF-16BE
- UTF-16LE
- UTF-16
- UTF-32BE
- UTF-32LE
- UTF-32
这里需要注意的是,“UTF-8”、“UTF-16” 和 “UTF-32” 可以通过两种含义使用:作为编码形式(encoding form)或作为编码方案(encoding scheme)。在大多数情况下,它的意思要么是明确的,要么是无关紧要的。但是在某些情况下,我们需要明确这代表什么含义。
5.5 BOM
为了解决程序在读取数据时,仍然不知道字节顺序问题,Unicode 将代码点 U+FEFF 指定为字节顺序标记(byte order mark, BOM)。因为不管是正序还是相反字节顺序,代码点 U+FEFF 和 U+FFFE 始终被保留为非字符。在文件或数据流的开头读取该标记,即可明确使用哪个字节顺序。13
例如,在 UTF-16BE 下,BOM 的字节序列为 0xFE 0xFF;在 UTF-16LE 下,BOM 被编码为 0xFF 0xFE;在 UTF-8 下,BOM 被编码为 0xEF 0xBB 0xBF;在 UTF-32BE 下,BOM 的字节序列为 0x00 0x00 0xFE 0xFF;在 UTF-32LE 下,BOM 的字节序列为 0xFE 0xFF 0x00 0x00。
通过上述标识,即可判断使用的是哪种 Unicode 编码方案。
因为在大多数传统编码标准(ANSI)中,字节序列 0xFE 0xFF 和 0xFF 0xFE 的可能性极小。因此,当文件以此值开头,程序基本可以推断数据是 Unicode 字符集,并且还能够推断出编码形式。13
当以这种使用 BOM 的方式来标识数据的字符集编码时,它被称为编码签名(encoding signature)。
6. 字符集编码模型
6.1 抽象字符库 Abstract character repertoire (ACR)
抽象字符库 (ACR) 是要编码的字符的无序集合。在给定的标准中,该集合可能是封闭的(不能有新的字符添加到其中),或者可能是开放的,随着时间的推移可以添加新字符(但也对最终可能添加的字符总数有一些强加的限制)。例如,Unicode 有一个开放的曲目,为了使标准更具通用性,会定期添加该曲目。1
对字符是 “抽象的” 需要特别解释一下。首先,它们不是直接存在于计算机系统中的东西,甚至都不是现实世界中具体的物体。而是一个概念对象,就像对应于单词 “character” 中字母 “r” 的概念,而不是字母 “r” 本身。此外,字符也不一定对应于给定书写系统中的字素,例如,西班牙语字母 “ñ” 中的上方的波浪号可能被视为一个不同的字符,这种情况下,该西班牙字母 “ñ” 就需要用两个字符 “n” 和 “~” 来表示。最后,还应该注意的是,字符不一定是图形对象,例如:“零宽度空格”,该空格没有图形表示形式,而是一个控制换行的文本运行控制字符。1
6.2 编码字符集 Coded character set (CCS)
编码字符集是从一组抽象字符库到一组唯一的数字指示符的映射。 数字指示符通常为整数(integers),在某些标准中则是一对的整数(pairs of integers)。 1
数字指示符称为代码点(codepoint) ,抽象字符与代码点的对应关系称为编码字符(encoded character)。需要提醒的是,这些代码点不绑定到计算机中的任何表示形式,代码点不是字节,它们只是整数(或整数对)。代码点的范围在编码标准中通常是有限的,这个有效的范围称为代码空间(codespace)。一套编码字符的集合称为编码字符集,也称为一个代码页(codepage)。 1
要注意的是,一些行业标准是在CCS级别运行的。它们标准化了一套编码字符集,并且为每个字符都指定了一个名称和属性,但它们没有标准化计算机中这些字符的编码表示形式。东亚地区使用的几种标准就是这种情况,例如 GB2312-80(简体中文)、CNS 11643-1992(台湾繁体中文)、JIS X 0208(日语)和KS X 1001(韩语)。这些标准都为每个字符分配了独一无二的代码点,但是没有明确每个代码点是怎样在计算机中表示的。想要将这些标准应用到计算机上,还依赖于其他单独的标准,也就是模型中的下一个级别:编码形式。
6.3 字符编码形式 Character encoding form (CEF)
这个层面上,我们开始考虑计算机中的实际表示。字符编码形式指的是从CCS中的代码点到固定数据类型的值的序列的映射,这些值称为代码单元(code units)。原则上,代码单元可以是任何大小:8位、11位、或者其他任何值。编码单元最常见的大小是8位、16位和 32位。1
在这里需要解释一下,“代码页” 在某些非正式的场合中也指的是某个应用于特定编码字符集(CCS)的字符编码形式(CEF)。1
CCS 中的代码点与 CEF 中的代码单元不一定是一对一的。在许多情况下,一个代码点是映射到一个有着多个代码单元的序列上的,例如 ANSI 的 “双字节” 编码,一个代码单元为8位,但是通常需要两个代码单元才能表示一个代码点(也就是一个字符)。
每个字符也不一定会映射到长度一致的代码单元序列上。例如罗马字符 “s” 被编码为单字节 0x73,但汉字 “山” 被编码为两个字节的序列:0xA4 0x73。
简单来说,CEF 需要做的是:为每个代码点都映射到唯一的代码单元序列上。1
有的字符集仅支持一种编码形式,例如一些西欧的常见字符集,单字节 8 位就可以表示完整的字符集,自然也就不需要其他的编码形式了。
有的编码形式也只用于一种字符集,例如 Big5 编码仅适用于 Big5 字符集,UTF-8 编码仅用于 Unicode 字符集。
另一方面,很多字符集是支持多种编码形式的,例如 GB 2312-80 字符集可以使用 GBK 编码形式、ISO/IEC 2022 编码形式或 EUC 编码形式进行存储。
此外,一些编码形式已经应用于多个字符集。例如,EUC 编码的变体对应于 GB 2312-80(简体中文)、CNS 11643-1992(台湾繁体中文)、JIS X 0208(日文)和其他几个字符集。
一个编码形式甚至可以同时采用多个字符集,ISO/IEC 2022 标准就是为此而制定的。在该标准下,每个字符集都对应了一个特定的转义序列。默认情况下,ISO/IEC 2022 数据使用 ASCII 字符集进行解释。当遇到各种转义序列中的任何一个时,将根据相应的字符集解释后续数据,直到遇到新的转义序列或恢复默认状态。ISO/IEC 2022 标准是为了提供一种可能支持所有字符集的单一编码。它使用许多机制来控制数据的解释方式,因此很复杂。
6.4 字符编码方案 Character encoding scheme (CES)
随着计算机技术的发展,8 位字节在许多系统中具有特殊的意义。特别是,在最低级别,许多用于数据存储和传输的系统以 8 位字节的方式运行。当将 16 位或 32 位数据单元放入 8 位字节上下文中时,数据单元可以很容易地分成 8 位块,因为它们的大小是 8 位的整数倍。然而,这些数据块的排序顺序有两种逻辑可能性:小端序(低序字节优先)、大端序(高阶字节在前面)。1
因此对于 16 位和 32 位的编码形式,字符编码方案通过指定是使用大端序还是小端序来解决字节顺序中的歧义。
除了 Unicode 之外,16 位和 32 位的编码形式并不常见。因此,除了 Unicode 之外,通常不会出现字节序列顺序的问题。
全文参考文献
-
Computers & Writing Systems. Character set encoding basics [DB/OL]. (2001-06-13). ↩︎ ↩︎2 ↩︎3 ↩︎4 ↩︎5 ↩︎6 ↩︎7 ↩︎8 ↩︎9 ↩︎10 ↩︎11 ↩︎12 ↩︎13 ↩︎14
-
知乎. 字符集编码(一):Unicode之前 [DB/OL]. (2023-04-24). ↩︎ ↩︎2 ↩︎3 ↩︎4 ↩︎5 ↩︎6 ↩︎7 ↩︎8 ↩︎9 ↩︎10
-
Microsoft文档. 支持多字节字符集 (MBCS) [DB/OL]. (2023-06-16). ↩︎ ↩︎2 ↩︎3
-
Microsoft文档. Unicode 和多字节字符集 (MBCS) 支持 [DB/OL]. (2023-04-03). ↩︎ ↩︎2 ↩︎3
-
Charles Petzold. Windows程序设计(第7版)[M]. 北京: 清华大学出版社, 2000. ↩︎
-
CSDN. 你真的懂Unicode和UTF-8是什么关系吗?来看看这个就彻底懂了![DB/OL]. (2018-11-19). ↩︎
-
Computers & Writing Systems. Understanding Unicode [DB/OL]. (2001-06-13). ↩︎ ↩︎2 ↩︎3 ↩︎4 ↩︎5 ↩︎6 ↩︎7 ↩︎8 ↩︎9 ↩︎10 ↩︎11 ↩︎12 ↩︎13 ↩︎14 ↩︎15 ↩︎16 ↩︎17 ↩︎18 ↩︎19 ↩︎20 ↩︎21 ↩︎22 ↩︎23 ↩︎24 ↩︎25
-
UNICODE官网. Unicode标准:技术介绍 [DB/OL]. (2019.08.23). ↩︎ ↩︎2 ↩︎3 ↩︎4
-
CSDN. Unicode编码详解(四):UTF-16编码[DB/OL]. (2022-02-12). ↩︎ ↩︎2 ↩︎3
-
曹晖.字符集与字符编码标准J.西北民族大学学报(自然科学版),2006,(03):36-4. ↩︎
-
JOEL SPOLSKY. The Absolute Minimum Every Software Developer Absolutely Positively Must Know About Unicode and Character Sets[DB/OL]. (2003-10-08) – Joel on Software ↩︎





