字符编码(三:Unicode 编码系统与字节序)

您所在的位置:网站首页 字符编码方式及判断整理法 字符编码(三:Unicode 编码系统与字节序)

字符编码(三:Unicode 编码系统与字节序)

#字符编码(三:Unicode 编码系统与字节序)| 来源: 网络整理| 查看: 265

全文链接:

字符编码(一:术语及字符编码由来) 字符编码(二:简体汉字编码与 ANSI 编码 ) 字符编码(三:Unicode 编码系统与字节序) 字符编码(四:UTF 系列编码详解) 字符编码(五:网络传输编码 Base64、百分号编码) 1.6 Unicode 编码系统

Unicode(中文:万国码、国际码、统一码、单一码)是计算机科学领域里的一项业界标准。它对世界上大部分的文字系统进行了整理、编码,使得电脑可以用更为简单的方式来呈现和处理文字。

Unicode 伴随着通用字符集的标准而发展,同时也以书本的形式对外发表。Unicode 至今仍在不断增修,每个新版本都加入更多新的字符。目前最新的版本为 2020 年 3 月公布的 13.0.0,已经收录超过 13 万个字符(第十万个字符在 2005 年获采纳)。Unicode 涵盖的数据除了视觉上的字形、编码方法、标准的字符编码外,还包含了字符特性,如大小写字母。

Unicode 发展由非营利机构统一码联盟负责,该机构致力于让 Unicode 方案取代既有的字符编码方案。因为既有的方案往往空间非常有限,亦不适用于多语环境。

Unicode 备受认可,并广泛地应用于电脑软件的国际化与本地化过程。有很多新科技,如可扩展置标语言(Extensible Markup Language,简称:XML )、Java 编程语言以及现代的操作系统,都采用 Unicode 编码。

起源简述

前文讲过,随着计算机发展到世界各地,于是各个国家和地区各自为政,搞出了很多既兼容 ASCII 但互相之间又不兼容的各种编码方案(微软统一称之为 ANSI 编码,具体体现为各种 ANSI 代码页)。

这样一来,同一个二进制编码在不同的 ANSI 编码方案中就有可能被解释成不同的字符,从而对采用不同 ANSI 编码方案的系统之间的数据交换带来了极大的不便。

比如大陆和台湾是只相隔 150 海里、使用着同一种语言的兄弟地区,也分别采用了不同的 DBCS(Double Byte Character Set)双字节字符集编码方案。

以前使用 DOS 操作系统的年代,大陆地区必须装上类似于 “UCDOS 希望汉字系统” 这样的中文处理系统专门来处理采用 GB 类编码方案的汉字的显示、输入问题;而台湾地区由于采用 BIG5 编码方案(统一繁体字编码,俗称大五码,同样使用 2 个字节表示繁体汉字),则必须安装类似于 “ET倚天汉字系统” 这样的繁体中文处理系统才可以正确显示、输入采用 BIG5 编码方案的繁体汉字。

因此,要想打开一个文本文件,就必须首先知道它所采用的 ANSI 编码方案,否则用错误的 ANSI 编码方案进行解码,就会出现乱码。为什么早期的电子邮件常常出现乱码?多半是因为发信人和收信人使用的操作系统所采用的 ANSI 编码方案不一样。

想象一下,如果有一种统一的编码方案,将世界上所有字符都纳入其中,每一个字符都给予一个全球独一无二的编码,那么乱码问题就会消失。于是,全球所有国家和地区使用的所有字符的统一编码方案诞生了。

最初,由世界上多家多语言软件开发商组成的统一码联盟(即 Unicode 组织 ),于1991 年发布了 The Unicode Standard(统一码标准),定义了一个全球统一的通用字符集,习惯简称为 Unicode( Unicode 常被称为统一码、万国码、单一码,严格来说,这些称呼不够严谨或不够准确,因为 Unicode 字符集只是一个编号字符集 CCS,尚未经过字符编码方式 CEF 和字符编码模式 CES 进行编码)。

与此同时,ISO 及 IEC 也于 1993 年联合发布了一个标准号为 ISO/IEC 10646-1 的全球统一的通用字符集,称之为 Universal Multiple-Octet Coded Character Set(通用多八位组编号字符集,习惯翻译为 “通用多八位编码字符集”,但这里将 Coded 翻译为 “编码的” 极易误导人),简称为 Universal Character Set(即通用字符集,缩写为 UCS)。

在这段时间内,统一码联盟与 ISO/IEC 双方都意识到世界上没有必要存在两套全球统一的通用字符集,在 1991 年 10 月两者开始合作,共同为创立一个单一的全球统一的通用字符集而协同工作。到了 Unicode 2.0 标准发布时( 1996 年),Unicode 字符集和 UCS 字符集(即 ISO/IEC 10646-1 )基本保持了一致。

虽然现在这两个标准仍然独立存在,但统一码联盟和 ISO/IEC 都同意保持两者的通用字符集相互兼容,并共同调整未来的任何扩展。

目前,Unicode 的知名度要比 UCS 大得多( sp:按维基百科中的说法,仅仅是因为 Unicode 比 ISO/IEC 10646 这个名称好记忆。。。),实践中的应用也更为广泛得多,已成为了全球统一的通用字符集或编码方案的代名词。因此,Unicode 字符编码方案已经成为了全球统一字符编码方案事实上的标准。

v2-40f9026f33011072cf5fd5f9421cf043_1440w

对于 ASCII 字符,Unicode 保持其原编码不变(准确地说,应该是保持其 “编号不变”,因为在传统字符编码模型中,编号与编码不作区分,说 “编码不变” 也勉强可以,因为在 Unicode 的三大实现中,除了),而 ASCII 字符之外的其他字符则全部重新统一编码。

发展节点

总结字符编码方案的演变,我们大致上可简单地划分为三个阶段:

  ① ASCII 编码方案阶段 → ② ANSI 编码方案阶段 → ③ Unicode / UCS 编码方案阶段。

在第一个阶段的 ASCII 编码方案阶段,ASCII 编码方案主要适用于美国(与美国同文的英国大致上也适用)。

后来计算机发展到欧洲各国以及世界各地,开始发展到了作为第二阶段的 ANSI 编码方案阶段,各个国家和地区先后各自制定了既兼容 ASCII 但又互不兼容的 ANSI 编码方案。

ANSI 编码方案阶段的各自为政,为世界各个国家和地区基于计算机的信息交流带来了极大的不便,痛定思痛之余,终于通过 Unicode / UCS 编码方案的制定发展到了第三阶段,最终 Unicode 编码方案胜出。

这个过程中 Unicode / UCS 编码系统的发展如下(从汉字的视角看):

1984 年,ISO 的文字编码委员会(ISO/TC 97/SC2)决议制订出一套编码规格( ISO 10646 ,即 ISO 646 的升级版意思),,是以交换文字集的方式来统一处理世界的文字。并且成立了工作小组( ISO/TC 97/SC 2/ WG 2 )来负责此事,后可称为 UCS 团队。

这个编码一开始的构想是采用 16 位,而对于日本及中国等国的汉字编码则原封不动地加入。但若如此,中国当时所制订的编码都无法加入( ps:中日韩都有汉字,相同的汉字重复,并且那么多汉字 16 位范围也放不下),因而反对。并于 1989 年,提出了各国的汉字统合集合(Han Character Collection,HCC)的构想。

1987 年,施乐公司的 Joe Becker 和 Lee Collins 开发了统合处理全世界所有文字的统一码,即 Unicode 。

1989 年,发表了统一码概要,其基本方针为以 16 位处理所有文字(此时只有 BMP 平面),并且将会统合中、日、韩文字。

1990 年,完成了基于此方针(统一码概要)的最终草案。

1990 年,工作小组完成了 ISO 10646 的初版草案(DIS 10646),并且当年先后公布了 UCS-2 字符集与 UCS-4 字符集。其不仅给每个字符分配一个代码,而且赋予了一个正式的名字。表示一个 UCS-2 值的十六进制数通常在前面加上 “U+”,例如 “U+0041” 代表字符 “A”。后U+被 Unicode 沿用。

先公布的是 UCS-2,其使用固定 16 位双字节处理所有文字,但是其空间有限。为了解决汉字问题,之后不久又公布了UCS-4 ,其统一将所有的字符固定以 32 位 4 字节来进行表示,并将各国的汉字编码原封不动地加入其中(还有全球的其他语言,这里是中文视角看历史)。这下空间是大到用不完,但是极其浪费存储空间,被各软件公司弃用,以 Windows、Lunix以及 JS 语言为代表开始使用 UCS-2( 后升级为 UTF-16,这两者并不相同,PC 中文方面的字符编码的很多坑,从这时候开始)。

与此同时,中国也不同意 UCS-4 方案,认为若各国各自为汉字编码,将不利于统一处理汉字,因而反对。为了日后关于汉字编码的讨论及方针能顺利进行,并呼吁 WG 2 (即 UCS 团队)特别设置中日韩联合研究小组( CJK-JRG,Joint Research Group,为表意文字小组),以持续讨论。

还值得一提的是这份初版草案中有一个非必须的附录,名为 UTF( Unicode Transformation Format ,字符编码转换格式 ),UCS-4 方案包括在其内。 而 UTF 名称也由而来。

1991 年 1 月,大致同意统一码最终草案的企业成立了统一码联盟,即 Unicode 团队,这份草案中、日、韩中类似的汉字使用约二万多个字。为了未来扩展,保留了三万个汉字以供其它用途。

同 1991 年,各国希望能以一致的方式处理文字,如统一码(这时候草案刚出)这般,因而否决了 ISO/IEC 10646 的初版草案。基于中国与统一码联盟的提议,ISO 10646 和统一码共同成立了中日韩联合研究小组。中日韩联合研究小组将基于各国的汉字编码,独自定义定规范、制作 ISO 10646 和统一码的统一汉字编码。年尾,完成了 Unified Repertoire and Ordering(URO)。

1991 年 10 月,统一码联盟与 ISO/IEC JTC1/SC2 同意保持 Unicode 码表与 ISO 10646 标准保持兼容并密切协调各自标准近一步的扩展。不包含 CJK 统一汉字集(即 URO ) 的 Unicode 1.0 发布。

1992 年初,其他社会团体公司为创建良好的字节串编码系统以供多字节字符集使用,开始了关于 UTF-8 的研究。X/Open委员会的 XoJIG 发起,Unix 系统实验室(USL)的 Dave Prosser 为此提出了一个编码系统的建议。再流传到贝尔实验室九号项目操作系统工作小组,由肯·汤普逊与罗勃特·派进行编码设计与改进。

1992 年 6 月,URO 加入 ISO 10646 的第二版草案,统一码也确定 Unicode 1.0.1 同步。但是,发现了一些缺失,之后进行了修正。

1993 年 1 月,由贝尔实验室九号项目操作系统工作小组的肯·汤普逊( Ken )与罗勃特·派克在USENIX会议首次公布了 UTF-8( Unicode 实现之一)。

1993 年 6 月,CJK 统一汉字集的制定完成(即中日韩统一表意文字 CJK),整合后发布了正式版 ISO 10646-1:1993,即 Unicode 1.1。

1993 年 7 月,微软发布 Windows NT 3.1 并开始使用 UCS-2 字符编码(其从 1988 年开始研发,自然使用更早的 UCS-2,而不是 UTF-8 )。

1996 年, Unicode 2.0 标准发布,Unicode 字符集和 UCS 字符集(即 ISO/IEC 10646-1 )基本保持了一致。这一年,Unicode 公布了 UTF-16( Unicode 实现之一),且直接将它称为 UCS-2 超集。同年,微软开始兼容 UTF-8。

Unicode 自版本 2.0 开始保持了向后兼容,即新的版本仅仅增加字符,原有字符不会被删除或更名。

2000 年 2 月,UTF-16 被标准化为RFC 2781 。同年 7 月,微软发布的 Windows 2000 中将 UCS-2 ,升级为 UTF-16。

2002 年 3 月,Unicode 3.2 中定义了 UTF-32 ( Unicode 实现之一),同年 ISO/IEC JTC 1/SC 2 WG2 (即 UCS 团队) 申明规定 UCS-4 将来所有的字符分配将被限制在 Unicode 范围内。 所以 UTF-32 与 UCS-4 能表示的字符是相同的,即 UTF-32 等于 UCS-4。

2003 年 11 月 ,UTF-8 被标准化为 RFC 3629。同时将 Unicode 限制为仅支持 U+10FFFF 以内的码位(另外U+D800 到 U+DFFF 范围内也被保留使用)。UTF-8 也就相应的最大字节只能为 4 字节了。

这是因为 UTF-16 空间的代理码点有限,考虑到将来的兼容问题,强行限制了 Unicode 最大范围到 U+10FFFF,详见后文。

从 2009 年以来,UTF-8 一直是万维网的最主要的编码形式,并由 WHATWG 宣布为强制性的 “适用于所有事物”。

经过那么些年的发展,UCS 团队从 ISO/IEC 10646-1:1993 一直按年数命名发布新版,直到 ISO/IEC 10646:2020。按维基百科上的说法是没有 Unicode 名称好记。所以 Unicode 使用更多,其内容是一样的。

10 大设计原则

《The Unicode Standard Version 6.2 – Core Specification》文档给出了 Unicode 的十大设计原则:

Universality:提供单一、综合的字符集,编码一切现代与大部分历史文献的字符。 Efficiency:易于处理与分析。 Characters, not glyphs:字符,而不是字形。 Semantics:字符要有良好定义的语义 Plain text:仅限于文本字符 Logical order:默认内存表示是其逻辑序 Unification:把不同语言的同一书写系统(scripts)中相同字符统一起来。 Dynamic composition:附加符号可以动态组合。 Stability:已分配的字符与语义不再改变。 Convertibility:Unicode 与其他著名字符集可以精确转换。 字符平面映射

Unicode 字符集的目标是涵盖目前人类使用的所有字符,并为每个字符分配唯一的字符编号(即码点编号、码点值),一一对应于编号空间( Code Space 代码空间、码空间、码点空间)里的码点( Code Point 代码点)。

目前的Unicode字符分为 17 组编排,每组称为平面(Plane),而每平面拥有 65536 个代码点。然而目前只用了少数平面,尚有大量编号空间未被使用。

其中大致划分两类,其中第 0 号平面空间(即基本多文种平面 BMP)基本涵盖了当今世界上正在使用中的常用字符。其他第 1 ~ 16 号空间都统称为辅助平面,使用量较少。

平面 Unicode 范围 中文名称 英文名称 0 号平面 U+0000 - U+FFFF 基本多文种平面 Basic Multilingual Plane,简称 BMP 1 号平面 U+10000 - U+1FFFF 多文种补充平面 Supplementary Multilingual Plane,简称SMP 2 号平面 U+20000 - U+2FFFF 表意文字补充平面 Supplementary Ideographic Plane,简称 SIP 3 号平面 U+30000 - U+3FFFF 表意文字第三平面 Tertiary Ideographic Plane,简称 TIP 4 号平面至 13 号平面 U+40000 - U+DFFFF (尚未使用) 14 号平面 U+E0000 - U+EFFFF 特别用途补充平面 Supplementary Special-purpose Plane,简称 SSP 15 号平面 U+F0000 - U+FFFFF 保留作为私人使用区( A区) Private Use Area-A,简称 PUA-A 16 号平面 U+100000 - U+10FFFF 保留作为私人使用区( B区) Private Use Area-B,简称 PUA-B

Unicode 字符集不仅给每个字符根据其所在的码点分配了一个唯一的码点值(即码点编号,不严格地来讲,也勉强可认为是字符编号,注意不要跟 UTF-16、UTF-8 等字符编码方式 CEF 混淆了概念),而且赋予了一个正式的名称:在表示一个Unicode 码点编号(或 UCS 码点编号)的十六进制数的前面加上 “U+”,称之为码点名称,或字符名称(考虑到有部分码点实际上并未分配字符,因此应称为 “码点名称” 更为合适,但很多文章中都习惯称为 “字符名称”)。

比如,U+0041 表示英语大写字母 A,U+4E25 表示汉字 “严”。具体的字符对应表,可以查询unicode.org,其中汉字部分也可查询专门的中日韩汉字 Unicode 编码表。

Unicode 字符集中的 U+0000~U+007F(即十进制的 0~127 )与 ASCII 字符集(即 ISO/IEC 646 标准,ps:并不包括 ISO/IEC 646 的其他国家变异版本(开放的)。)是一致的,U+0000~U+00FF(即十进制的 0~255 )与 ISO/IEC 8859-1 字符集(即 Latin-1 字符集)也是一致的。

基本多文种平面 BMP

Unicode 字符集的编号空间中第 0 个平面 BMP( Basic Multilingual Plane,也称为基本多语言平面、基本多文种平面,往往简称为基本平面、平面 0 ),基本涵盖了当今世界上正在使用中的常用字符。我们平常用到的字符,一般都是位于 BMP 平面上的,其 Unicode 范围拥有 65,536 个码点(U+0000~U+FFFF) 。

BMP 平面以外其他的统称为辅助平面、增补平面。其要么用来表示一些非常特殊的字符(比如不常用的象形文字、远古时期的文字等),且多半只有专家在历史和科学领域里才会用到它们;要么被留作扩展之用。

另外,BMP 平面(即第 0 平面)中有一个私用区(也称为专用区,即 PUA:Private Use Area,也写作 PUZ:Private Use Zone ):0xE000~0xF8FF(十进制 57344~63743),共 6400 个码点,被保留为私用,Unicode 官方未将之分配给任何 Unicode 字符,因而可根据需要由合作者之间私下协商将其分配给私有字符(类似于Glyphicons、FontAwesome、fontello这样的私有图标字体字符,其码点就是使用的私用区中的码点);还有一个被称为代理区( Surrogate Zone )的特殊区域:0xD800-0xDFFF(十进制 55296~57343),共 2048 个码点,这些码点被称之为代理码点,目的是用基本平面 BMP 中的两个码点 “代理” 表示 BMP 以外的其他辅助平面中的字符( ps:由 UTF-16 引入代理码点,后文会有 UTF-16 详细介绍)。

注:Unicode 标准到目前为止实际上共定义了三个私用区:一个为如上所述的第 0 平面(即 BMP )中的U+E000~U+F8FF,另外两个几乎包含了整个第 15 平面和第 16 平面,分别为 U+F0000~U+FFFFD 和U+100000~U+10FFFD。私用区相当于可以由 Unicode 官方之外的个人和机构自由定义字符的特殊区域,因此私用区中的同一个码点,可被分配给不同的字符,具体是哪个字符取决于字体文件,从而不同的用户由于安装了不同的字体文件,有可能所看到的字符也不同。)

编码方式

统一码(即 Unicode )的编码方式与 ISO 10646 的通用字符集(即 UCS )概念相对应。最早实际应用的统一码版本对应于 UCS-2(1990 年,ISO 10646 初稿中),使用 16 位的编码空间。也就是每个字符占用 2 个字节。这样理论上一共最多可以表示 216(即 65536 )个字符( ps:才 6w 多个,有点少,而且 2 个字节,扩展差)。基本满足各种语言的使用。

上述 16 位统一码字符构成基本多文种平面 BMP。最新的统一码版本定义了 16 个辅助平面,两者合起来至少需要占据 21 位的编码空间,比 3 字节略少。但事实上辅助平面字符仍然占用 4 字节编码空间,与 UCS-4 保持一致。后来Unicode 2.0 开始扩充到了 ISO 10646-1 实现级别 3,即涵盖 UCS-4 的所有字符。UCS-4 是一个更大的尚未填充完全的 31 位字符集,加上恒为 0 的首位,共需占据 32 位,即 4 字节。理论上最多能表示 2 的 31 次方个字符,完全可以涵盖一切语言所用的符号。

基本多文种平面的字符的编码为 U+xxx,其中每个 x 代表一个十六进制数,与 UCS-2 编码完全相同。而其对应的 4 字节 UCS-4 编码后两个字节一致,前两个字节则所有位均为 0。

实现方式与 UTF 系列

Unicode 的实现方式不同于编码方式。一个字符的 Unicode 编码是确定的。但是在实际传输过程中,由于不同系统平台的设计不一定一致,以及出于节省空间的目的,对 Unicode 编码的实现方式有所不同。Unicode 的实现方式称为 Unicode 转换格式(Unicode Transformation Format,简称为 UTF )。

ps:即把 Unicode 字符转换为某种格式之意。UTF 概念最早是由 1990 年,由 UCS 在ISO/IEC 10646的初稿中有一个非必须的附录,名为 UTF。 —— 来自维基百科历史。

例如,如果一个仅包含基本 7 位 ASCII 字符的 Unicode 文件,如果每个字符都使用 2 字节的原 Unicode 编码传输,其第一字节的 8 位始终为 0。这就造成了比较大的浪费。对于这种情况,可以使用 UTF-8 编码,这是一种变长编码,它将基本 7 位 ASCII 字符仍用 7 位编码表示,占用一个字节(首位补 0 )。而遇到与其他 Unicode 字符混合的情况,将按一定算法转换,每个字符使用 1-3 个字节编码,并利用首位为 0 或 1 进行识别。这样对以 7 位 ASCII 字符为主的西文文档就大幅节省了编码长度(具体方案参见 UTF-8,或者后文详述)。类似的,对之后出现的需要 4 个字节的辅助平面字符和其他 UCS-4 扩充字符,2 字节编码的 UTF-16 也需要通过一定的算法进行转换。

再如,如果直接使用与 Unicode 编码一致(仅限于 BMP 字符)的 UTF-16 编码,由于每个字符占用了两个字节,在麦金塔电脑(Mac)机和个人电脑上,对字节顺序的理解是不一致的。这时同一字节流可能会被解释为不同内容,如某字符为十六进制编码 4E59 ,按两个字节拆分为 4E 和 59,在 Mac 上读取时是从低字节开始,那么在 Mac OS 会认为此 4E59 编码为 594E,找到的字符为 “奎”,而在 Windows 上从高字节开始读取,则编码为 U+4E59 的字符为 “乙”。就是说在 Windows 下以 UTF-16 编码保存一个字符“乙”,在 Mac OS 环境下开启会显示成“奎”。此类情况说明 UTF-16 的编码顺序若不加以人为定义就可能发生混淆,于是在 UTF-16 编码实现方 式中使用了大端序( Big-Endian,简写为 UTF-16 BE)、小端序( Little-Endian,简写为 UTF-16 LE)的概念,以及可附加的字节顺序记号解决方案,目前在 PC 机上的 Windows 系统和 Linux 系统对于 UTF-16 编码默认使用 UTF-16 LE(即小端序)。(具体方案参见UTF-16,或后文详述)

此外 Unicode 的实现方式还包括 UTF-7、Punycode、CESU-8、SCSU、UTF-32、GB18030 等,这些实现方式有些仅在一定的国家和地区使用,有些则属于未来的规划方式。目前通用的实现方式是 UTF-16 小端序(LE)、UTF-16 大端序(BE)和 UTF-8。在微软公司 Windows XP 附带的记事本(Notepad)中,“另存为”对话框可以选择的四种编码方式除去非 Unicode 编码的 ANSI(对于英文系统即 ASCII 编码,中文系统则为 GB2312 或 Big5 编码)外,其余三种为 “Unicode”(对应 UTF-16 LE )、“Unicode big endian”(对应 UTF-16 BE )和 “UTF-8”。

目前辅助平面的工作主要集中在第二和第三平面的中日韩统一表意文字中,因此包括 GBK、GB18030、Big5等简体中文、繁体中文、日文、韩文以及越南喃字的各种编码与 Unicode 的协调性被重点关注。考虑到 Unicode 最终要涵盖所有的字符。从某种意义而言,这些编码方式也可视作 Unicode 的出现于其之前的既成事实的实现方式,如同 ASCII 及其扩展 Latin-1(即 ISO 8859-1 ) 一样,后两者的字符在 16 位 Unicode 编码空间中的编码第一字节各位全为 0,第二字节编码与原编码完全一致。但上述东亚语言编码与 Unicode 编码的对应关系要复杂得多。

非 Unicode 环境

在非 Unicode 环境下,由于不同国家和地区采用的字符集不一致,很可能出现无法正常显示所有字符的情况。微软公司使用了代码页(Codepage)转换表的技术来过渡性的部分解决这一问题,即通过指定的转换表将非 Unicode 的字符编码转换为同一字符对应的系统内部使用的 Unicode 编码。可以在 “语言与区域设置” 中选择一个代码页作为非 Unicode 编码所采用的默认编码方式,如 936 为简体中文GB 码,950 为繁体中文Big5(皆指 PC 上使用的)。在这种情况下,一些非英语的欧洲语言编写的软件和文档很可能出现乱码( ps:这是因为,GB 码与那些非英语的其他编码都属于 ASCII 码的扩展,只向下兼容 ASCII 码,它们之间并不兼容)。而将代码页设置为相应语言中文处理又会出现问题( ps:只兼容 ASCII,其他 ANSI 编码之间并不兼容),这一情况无法避免。只有完全采用统一编码才能彻底解决这些问题。

代码页技术现在广泛为各种平台所采用。UTF-7 的代码页是 65000,UTF-8 的代码页是 65001。

ps:前文《 1.5 ANSI 编码与代码页》有详细代码页介绍。

XML 和 Unicode

XML 及其子集 XHTML 采用 UTF-8 作为标准字集,理论上我们可以在各种支持 XML 标准的浏览器上显示任何地区文字的网页,只要电脑本身安装有合适的字体即可。可以利用&#nnn;的格式显示特定的字符。nnn 代表该字符的十进制 Unicode 代码。如果采用 十六进制 代码,在编码之前加上x字符即可。但部分旧版本的浏览器可能无法识别十六进制代码。

过去电脑编码的 8 位标准,使每个国家都只按国家使用的字符而编定各自的编码系统;而对于部分字符系统比较复杂的语言,如越南语(ps:上镜率真高,兄弟你真的很特殊),又或者东亚国家的大型字符集,都不能在 8 位的环境下正常显示。

只是最近才有在文本中对十六进制的支持,那么旧版本的浏览器显示那些字符或许可能有问题-大概首先会遇到的一个问题只是在对于大于 8 位 Unicode 字符的显示。解决这个问题的普遍做法仍然是将其中的十六进制码转换成一个十进制码(例如:♠用♠代替♠)。

也有一些字符集标准将一些常用的标志存放在字符内码外面,那么你可能使用像—这样的文本标志来表示一个长划(—)的情况,即使它的字符内码已经被使用,这些标准也不包含那个字符。

然而部分由于 Unicode 版本发展原因,很多浏览器只能显示 UCS-2 完整字符集,也即现在使用的 Unicode 版本中的一个小子集 UTF-16 。下表可以检验您的浏览器如何显示各种 Unicode 代码:

代码 字符标准名称(英语) 在浏览器上的显示 A 大写拉丁字母 “A” A ß 小写拉丁字母 “Sharp S” ß þ 小写拉丁字母 “Thorn” þ Δ 大写希腊字母 “Delta” Δ Й 大写斯拉夫字母 “Short I” Й ק 希伯来字母 “Qof” ק م 阿拉伯字母 “Meem” م ๗ 泰文数字 7 ๗ ቐ 埃塞俄比亚音节文字 “Qha” ቐ あ 日语平假名 “A” あ ア 日语片假名 “A” ア 叶 简体汉字 “叶” 叶 葉 繁体汉字 “葉” 葉 엽 韩国音节文字 “Yeop” 엽

在 SGML、HTML、XML 的文本中,使用字符值引用或字符实体引用表示一个 Unicode 字符。

Unicode 等价性与正规化

Unicode 等价性(Unicode equivalence)是为和许多现存的标准能够兼容,Unicode(统一码)包含了许多特殊字符。在这些字符中,有些在功能上会和其它字符或字符序列等价。因此,Unicode 将一些码位序列定义成相等的。Unicode 提供了两种等价概念:标准等价和兼容等价。前者是后者的一个子集。例如,字符 n 后接着组合字符 ~ 会(标准和兼容)等价于 Unicode 字符 ñ。而合字ff 则只有兼容等价于两个 f 字符。

Unicode 正规化是文字正规化的一种形式,是指将彼此等价的序列转成同一列序。此序列在 Unicode 标准中称作正规形式。对于每种等价概念,Unicode 又定义两种形式,一种是完全合成的,一种是完全分解的。因此,最后会有四种形式,其缩写分别为:NFC、NFD、NFKC、NFKD。对于 Unicode 的文字处理程序而言,正规化是很重要的。因为它影响了比较、搜索和排序的意义。

具体内容不详述,请看 《Unicode 等价性》以及链接里面的其他链接,如:《附加符号》、《预组字符》等。

ps:建议没有需要,不用看了。知道有这个概念就行。

Unicode 兼容性

Unicode 在刚开始制订字符编码时,并没有考虑与任何一种现有的字符编码保持完全兼容,比如 GBK 与 Unicode 在汉字的编码上是完全不一样的,没有任何一种简单的算法可以将文本内容在 Unicode 编码和 GBK 编码之间进行直接转换,要转换的话只能通过查表这样低效率的笨办法一个字符对应一个字符地来进行。

即便是 ASCII 字符,也属于不完全的间接兼容或者说半兼容,因为在 Unicode 的三大实现中( UTF-8、UTF-16、UTF-32 ),UTF-16 编码也是用两个字节来表示 ASCII 字符的,虽然其低 7 位与 ASCII 编码保持了一致,其余高位的 9 位均只是占位的 0,但毕竟还是使用了 16 位共两个字节,不同于ASCII 的单字节编码。而 UTF-32 更是固定使用 32 位字节来表示 ASCII 字符,过于浪费存储空间,性能差。 正是鉴于此(当然除此之外还有其他原因),

于是又设计了 UTF-8 字符编码方式,则实现了与ASCII 编码的完全兼容,直接使用其原编码。对于 ASCII 之外的完全重新编码。

从字符集的角度上来讲,Unicode 字符集不同于 ASCII 这样不能再增加字符的封闭字符集,而是一个开放的字符集,是可以不断地增加字符的。因此 Unicode 字符集也在不断发展中(比如随着互联网即时聊天工具的发展而流行起来的很多 Emoji 表情符就不断地被增加到了 Unicode 字符集里),理论上支持的字符数量是没有上限的,未来还可再扩展。

ps:2003 年 11 月,由于 UTF-16 编码形式的限制( UTF-16 代理对是有限的,为了将来兼容 UTF-16 ),RFC 3629 标准将 Unicode 限制为仅支持 U+10FFFF 以内的码位。(另外 U+D800 到 U+DFFF 范围内也被保留使用)。

v2-32f3c0ed33e858c160e38f0023199083_1440w

Unicode 字符集中的 Emoji 表情字符

在 Unicode 标准推出之前,那些做多语言国际软件的公司遇上过很大麻烦。他们为了在不同的国家销售同一套软件,就不得不特别注意字符编码的问题。不仅要处处小心不要搞错,还要把软件中的文字在不同的字符编码中转换来转换去,而Unicode 标准的出现,提供了一个很好的一揽子解决方案。

于是从 Windows NT 3.1( 1993 年) 开始,微软趁机把操作系统改了一遍,把所有的核心代码都改成了采用 Unicode 标准的版本(实际使用的就是 Unicode 标准的 UTF-16 字符编码方式 CEF 下的 Little-Endian 小端序字符编码模式 CES,详见后文解释)。

从 Windows NT 开始,Windows 系统终于不需要再加装各种本土语言系统(比如 “ UCDOS 希望汉字系统”、“ ET 倚天汉字系统” 之类的),就可以直接显示全世界所有的字符了。当然,为了保持兼容性,对于之前的 ANSI 编码方案,Windows 同样进行了支持。

另外, Unicode 字符集(即统一码)与 ISO/IEC 10646 字符集(即通用字符集),从 Unicode 2.0 开始基本保持兼容。

Unicode 缺陷

由于 Unicode 字符集非常大(并且作为开放字符集还在不断扩展之中),有些字符的编号(即码点值)需要两个或两个以上字节来表示,而要对这样的编号进行编码,也必须使用两个或两个以上字节。

比如,汉字 “严” 的 Unicode 编号以十六进制数表示为 4E25,转换成二进制数有 15 位(100 1110 0010 0101),对 “严” 这个字符的编号进行编码的话,至少需要 2 个字节。表示其他更大编号的字符,可能需要 3 个字节或者 4 个字节,甚至更多。

这带来两个问题:

一是:如何才能区别 Unicode 字符和 ASCII 字符的编码?计算机怎么知道三个字节表示的是一个字符,而不是分别表示三个字符呢?

二是:我们知道,英文字母只用一个字节来编码就够了,而如果 Unicode 统一硬性规定,每个字符都用两个、三个或四个字节来编码,那么每个英文字母编码的前面都必然有一个、两个到三个字节全是 0,这对于存储和传输来说是极大的浪费。

这就涉及到了字符编码方式 CEF 的选择问题。Unicode 字符的编码方式目前最常用的是这三种:UTF-8、UTF-16、UTF-32。在具体介绍这些编码方式之前,需要再次深入了解两个概念 —— 码点(Code Point)与码元(Code Unit)。

码点

一个字符集一般可以用一张或多张由多个行和多个列所构成的二维表来表示。

二维表中行与列相交的点,称之为码点(Code Point 代码点),也称之为码位(Code position 代码位);每个码点分配一个唯一的编号,称之为码点值或码点编号,除开某些特殊区域(比如代理区、专用区)的非字符码点和保留码点,每个码点唯一对应于一个字符。

因此,除开非字符码点和保留码点,码点值(即码点编号)通常来说就是其所对应的字符的编号,所以码点值有时也可以直接称之为字符编号,虽然不够准确,但更为直接。

字符集中所有码点数量的总和,称之为编号空间(Code Space,又被称之为代码空间、编码空间、码点空间、码空间)。

码点值最初用两个字节的十六进制数字表示,比如字母 A 的 Unicode 码点值为 0041,常写作 U+0041,这种形式称为Unicode 码点名称,不严格地来讲,也可称之 Unicode 字符名称(因为存在着非字符码点和保留码点,并非每个码点都分配了字符,所以这种称呼不够准确,不过目前更为普遍)。

后来随着 Unicode 字符集的不断增补扩大(比如现在的 Unicode 字符集至少需要 21 位才能全部表示),码点值也扩展为用三个字节或以上的十六进制数字表示。

例如,ASCII 字符集用 0~127 这连续的 128 个数字编号分别表示 128个 字符。GBK 字符集使用区位码的方式为每个字符编号,首先定义一个 94×94 的矩阵,行称为 “区”,列称为 “位”,然后将所有国标汉字放入矩阵当中,这样每个汉字就可以用唯一的 “区位” 码来标识了。例如 “中” 字被放到 54 区第 48 位,因此其区位码(字符编号)就是 5448。

而目前 Unicode 标准中,将字符按照一定的类别划分到 0~16 这 17 个平面( Plane 层面)中,每个平面中拥有 2^16 = 65536 个码点,因此,目前 Unicode 字符集所拥有的码点总数,也就是 Unicode 的编号空间为 17*65536=1114112。

v2-f5a9bf2bce1a61d10adfdfdf540b5556_1440w

**注意,**网络上的很多文章中,代码点、码点、码点值、码值、代码位、码位、字符码、Unicode 码、字符编号、字符编码、编码方案、编码方式、编码格式等等概念经常互相代替混用,让人困惑。

码元

在计算机存储和网络传输时,码点值(即字符编号)被映射到一个或多个码元( Code Unit 代码单元、编码单元)。

码元可理解为字符编码方式 CEF( Character Encoding Form )对码点值进行编码处理时作为一个整体来看待的最小基本单元(或称为最小基本单位)。

为什么非要引入 “码元” 这个概念?或者说,为什么非要强调 “码元” 这个概念?

码元某种程度上可认为对应于高级语言中的基本数据类型。而高级语言层面的基本数据类型,若要更深入一步地来讲,实质上对应于机器硬件层面(如汇编语言层面)的数据类型 byte 字节、word 字、dword 双字等在硬件中的表达与处理机制。

之所以要强调 “码元” 的概念,是因为字符编码作为一串数字序列,最终还是得通过机器硬件层面的数据类型来表示。

而码元的实质,就是机器硬件层面的数据类型;不同的码元,代表着不同位数的数据类型。当字符编码方案设计人员基于某种考虑(如时间复杂度、空间复杂度等)为某种字符编码方式 CEF 选择了某种码元时,其实质就是选择了某个位数的数据类型。

数据类型,在机器硬件层面上来看,只有作为一个整体来处理的二进制数字位数上的不同(如 byte 字节、word 字、dword 双字等汇编语言的数据类型,都是二进制数字值,只是位数不同而已),而并没有高级语言层面上的数值、布尔、字符等语义的不同,毕竟本质上来讲计算机只 “认识” 由 0 和 1 组成的数字。

正因为如此,字符编码方案设计人员在设计字符编码方式 CEF 时,必然要选择一种机器硬件层面上某个位数的数据类型(如 byte 字节、word 字、dword 双字等)来表示,不是选择这个位数的数据类型,就是选择那个位数的数据类型。当然这种选择会基于各种考虑,比如时间复杂度、空间复杂度等,不过一旦选择了某种数据类型,其所选择的数据类型位数上的不同,同时也就体现为了码元位数上的不同。

所以,不同位数的码元,实质上是机器硬件层面上不同位数的数据类型。(而机器硬件层面上为什么要设计不同位数的多种数据类型出来,这是另一个话题了,有兴趣可参看有关硬件方面的专著。)

由于数据类型有单字节与多字节之分,所以码元也有单字节与多字节之分;而多字节数据类型由于历史的原因,存在着字节序的大端序( Big-Endian )与小端序( Little-Endian )之分,因此多字节码元也存在着大端序与小端序之分(具体详见前文中有关字节序的解释;注意,单字节数据类型是没有字节序的问题的,所以单字节码元也就没有字节序问题)。

这就是之所以要强调 “码元” 这个概念的关键原因。

也可以看这篇 知乎 - 怎样形象理解码元?。

对字符编号(即字符码点值)进行编码的具体实现方式——字符编码方式 CEF,就是由一个或多个码元这样的最小基本单元构成的。

最常用的码元是 8 位(即 1 字节)的单字节码元,另外还有 16 位(即 2 字节)和 32 位(即 4 字节)两种多字节码元,分别相当于汇编语言中的 byte 字节、word 字、dword 双字,以及 C++ 中的无符号整型 BYTE、WORD、DWORD。

注:在 VC++ 6.0 中,这三种数据类型的定义分别为:

typedef unsigned char BYTE; // 1个字节 typedef unsigned short WORD; // 2个字节 typedef unsigned long DWORD; // 4个字节

于是,三种码元对应就有了 Unicode 字符编号的三种 UTF 编码方式(即 Unicode 码转换格式 Unicode Transformation Format,或称通用字符集转换格式 UCS Transformation Format ):

   UTF-8,即 8-bit Unicode/UCS Transformation Format;

   UTF-16,即 16-bit Unicode/UCS Transformation Format;( ps:这个最早)

   UTF-32,即 32-bit Unicode/UCS Transformation Format。

或者反过来说,Unicode 字符编号的三种 UTF 编码方式( UTF-8、UTF-16、UTF-32 )分别采用了不同的码元(单字节、双字节、四字节)来编码。

v2-e970a7c99f764c8a3a767a8373f5d07c_1440w

例如,“汉字” 这两个中文字符的 Unicode 字符编号是 0x6C49 和 0x5B57,其三种 UTF 编码在 VC++ 6.0 中可按如下定义进行 “模拟”:

v2-4638c037d1479b19c084620f6f31424b_1440w

注意,这里之所以说是 “模拟”,其中的一个重要原因,如前文所述,从本质上来讲机器硬件层面上的所有数据类型,只存在着被视作一个整体来处理的比特序列(即二进制序列)的位数不同之分,不存在着高级语言层面上数据类型的数值、字符串、布尔值等的语义不同之分。

因此,机器硬件层面上的数据类型与高级语言层面上的数据类型,严格来讲,在含义上还是有着很大不同的。当然,高级语言层面上的数据类型最终还是会被转化为机器硬件层面上的数据类型,毕竟计算机只 “认识” 由 0 和 1 所组成的比特流。(可同时参见前文中有关字节序的解释)

这里用 BYTE、WORD、DWORD 分别表示无符号 8 位整数、无符号 16 位整数和无符号 32 位整数;因而 UTF-8、UTF-16、UTF-32 可认为分别以 BYTE、WORD、DWORD 作为码元。

“汉字” 这两个中文字符的 UTF-8 编码需要六个 BYTE(共 6 个单字节码元),大小是 6 个字节;UTF-16 编码需要两个WORD(共 2 个双字节码元),大小是 4 个字节;UTF-32 编码需要两个 DWORD(共 2 个四字节码元),大小是 8 个字节。

ps:刚开始看得有点晕,这里需要注意码元定义: “最短的比特组合” 的单元,重点在 “最短” 二字。

UTF-8 中可以有完全兼容 ASCII 的 8 位单字节编码,也有多字节来表示其他字符编码。比如 “汉字”,中的 “汉”,就占了 3 个字节。所以按照码元的定义,UTF-8 是单字节码元,多字节编码的字符集。

UTF-16 最早出现,所有的字符统一按两个字节编码。就连 ASCII 都是使用了两个字节(其低 7 位与 ASCII 编码保持了一致,其余高位的 9 位均只是占位的 0 )的半兼容。所以 UTF-16 是双字节码元,多字节编码的字符集。 UTF-32 同理是四字节码元,多字节编码的字符集。

他们三,码元大小不同,相同点都是多字节编码。。。不能迷糊,得搞清楚这个概念区别。详见下面的:比较一览表。

由于多字节数据类型的数据在计算机存取时存在一个字节序的问题,因此,UTF-16、UTF-32 这两种编码方式所编码出来的逻辑意义上的多字节码元序列,在映射为物理意义上的字节序列时,字节序列的字节序因系统平台的不同而不同。

前面已经多次强调过了,这里再次特别强调一下:由单字节数据类型所组成的多字节数据是不存在字节序的问题的。因此,采用单字节码元进行编码的 UTF-8 编码,虽然 ASCII 字符为单字节编码,而非 ASCII 字符却是多字节编码的,但却并不存在字节序问题,这是跟同样为多字节编码、但却采用多字节码元的 UTF-16、UTF-32 编码的不同之处。详见下表所列。

v2-12bc0522d32b78734d9ea4e9716784b4_1440w

Unicode 字符集三大编码方式( UTF-8、UTF-16、UTF-32 )比较一览表

术语的解耦

前文已经提及,编号字符集 CCS(简称字符集)与字符编码方式 CEF(简称编码方式)这两个概念,在早期阶段(主要是 ASCII 编码方案阶段和 ANSI 编码方案阶段)并没有必要严格区分。

在 Unicode 编码方案出现之前,字符集及其具体的编码方式是绑定耦合在一起的,因此,“字符集”、“编码” 或 “编码方式” 甚至 “编码方案” 这几个概念经常相互指代、彼此混用。

比如,字符集里的字符编号(即码点编号)在很多文章里也称之为字符编码、字符码、码点、码位、码点值、码值等,字符编码也称之为编码实现、编码方案、编码方式、编码格式、编码形式、内码、编码值、码值(你没看错,字符编号与字符编码都有可能被简称为 “码值”,头大了吧),等等,非常混乱。

对于 ASCII、GB 2312、GBK、GB 18030、Big5 之类采用传统字符编码模型的历史遗留方案来说,由于基本上一个字符集只使用一种编码方式,因此这种混用问题还不大。

但在 Unicode 这样采用现代字符编码模型的全新方案出现之后,很多人受上述这些历史遗留方案的影响,从而无法正确地理解字符集和编码方式之间的关系,这导致了概念混乱,引起了大量误解。

然而,对于采用现代字符编码模型的 Unicode 编码方案来说,字符集和编码方式是必须明确区分的。从软件工程的角度来讲,传统字符编码模型中紧密绑定耦合在一起的字符集及编码方式这两个概念,在现代字符编码模型中被分离解耦了,而这种解耦带来了极大的灵活性。

这意味着,对于采用现代字符编码模型的同一个字符集,可以采用多个不同的编码方式对其字符编号进行编码。也因此,作为同一个 Unicode 字符集,目前就定义了 UTF-8、UTF-16 和 UTF-32 等多种可选的编码方式(注:UTF,即Unicode/UCS Transformation Format,Unicode/UCS 转换格式)。

所以,在如今字符编码方案的第三阶段,用 Unicode 来称呼一个编码方式已不合适,并且容易产生误解,引发混乱和导致困惑,而应该用 UTF-8、UTF-16 和 UTF-32 来称呼编码方式,以 Unicode 来称呼字符集,将包括 Unicode 字符集以及各 UTF 编码方式等在内的整体称之为字符编码方案或字符编码系统或字符编码标准。

另外,同一字符编码方式 CEF 的码元序列,在计算机实际处理、存储和传输时,还需再次编码转换为字符编码模式 CES的字节序列。

字符编码方式 CEF 的码元序列可理解为字符编码在数学层面上的逻辑表示形式;相对而言,字符编码模式 CES 的字节序列则可理解为字符编码在计算机实现中的物理表示形式。

而字节序列( Byte Sequence ),则涉及到了不同的字节序( Byte-Order,主要分为大端序 Big-Endian、小端序 Little-Endian )。

1.7 字节序

字节序,又称字节顺序,其英文为 Byte-Order;另外英文中还可称为 Endianness,因此也翻译为端序。

字节顺序,在计算机科学领域中,指存储器中或在数字通信链路中,组成多字节的字的字节的排列顺序。在几乎所有的机器上,多字节对象都被存储为连续的字节序列。

字节的排列方式有两个通用规则。例如,一个多位的整数,按照存储地址从低到高排序的字节中,如果该整数的最低有效字节(类似于最低有效比特)在最高有效字节(类似于最高有效比特)的前面,则称小端序;反之则称大端序。在网络应用中,字节序是一个必须被考虑的因素,因为不同机器类型可能采用不同标准的字节序,所以均按照网络标准转化。

端(endian)的起源

“endian”一词来源于十八世纪爱尔兰作家乔纳森 · 斯威夫特( Jonathan Swift )的小说《格列佛游记》( Gulliver's Travels )。小说中,小人国为水煮蛋该从大的一端( Big-End )剥开还是小的一端( Little-End )剥开而争论,争论的双方分别被称为 “大端派” 和 “小端派”。以下是 1726 年关于大小端之争历史的描述:

我下面要告诉你的是,Lilliput 和 Blefuscu 这两大强国在过去 36 个月里一直在苦战。战争开始是由于以下的原因:我们大家都认为,吃鸡蛋前,原始的方法是打破鸡蛋较大的一端,可是当今皇帝的祖父小时候吃鸡蛋,一次按古法打鸡蛋时碰巧将一个手指弄破了。因此他的父亲,当时的皇帝,就下了一道敕令,命令全体臣民吃鸡蛋时打破鸡蛋较小的一端,违令者重罚。老百姓们对这项命令极其反感。历史告诉我们,由此曾经发生过 6 次叛乱,其中一个皇帝送了命,另一个丢了王位。这些叛乱大多都是由 Blefuscu 的国王大臣们煽动起来的。叛乱平息后,流亡的人总是逃到那个帝国去寻求避难。据估计,先后几次有 11000 人情愿受死也不肯去打破鸡蛋较小的一端。关于这一争端,曾出版过几百本大部著作,不过大端派的书一直是受禁的,法律也规定该派任何人不得做官。

​ —— 《格列夫游记》 第一卷第4章 蒋剑锋(译)

1980 年,丹尼·科恩(Danny Cohen),一位网络协议的早期开发者,在其著名的论文 " On Holy Wars and a Plea for Peace " 中,为平息一场关于字节该以什么样的顺序传送的争论,而第一次引用了 Big-endian 和 Little-endian 这两个术语,最终它们成为了异构计算机系统之间进行通讯、交换数据时所要考虑的极其重要的一个问题。

注:所谓异构是指不同架构、不同结构、不同构造等,而这里的异构计算机系统,主要指的是采用不同 CPU 和 / 或不同操作系统的计算机系统。

v2-78062b837a79dffae48bff7aea79579f_1440w

字节顺序

在哪种字节顺序更合适的问题上,人们表现得非常情绪化,实际上,就像鸡蛋的问题一样,没有技术上的原因来选择字节顺序规则,因此,争论沦为关于社会政治问题的争论,只要选择了一种规则并且始终如一地坚持,其实对于哪种字节排序的选择是任意的。

对于单一的字节( a byte ),大部分处理器以相同的顺序处理位元( bit ),因此单字节的存放方法和传输方式一般相同。

对于多字节数据,如整数( 32 位机中一般占 4 字节 ),在不同的处理器的存放方式主要有两种,以内存中0x0A0B0C0D 的存放方式为例,分别有以下几种方式:

大端序 BE( Big-Endian ),也称高尾端序; 小端序 LE( Little-Endian ),也称为低尾端序; 中间序 ME( Middle-Endian ),也称为混合序; 低尾端序(小端序 Little-Endian )

就是低位字节(即小端字节、尾端字节)存放在内存的低地址,而高位字节(即大端字节、头端字节)存放在内存的高地址。

这是最符合人的直觉思维的字节序(但却不符合人的读写习惯),因为从人的第一观感来说,低位字节的值小,对应放在内存地址也小的地方,也即内存中的低位地址;反之,高位字节的值大,对应放在内存地址大的地方,也即内存中的高位地址。

v2-156e693023c2366b494bc1a2175717a6_1440w

数据以 8 bit 为单位: 地址增长方向 → ... 0x0D 0x0C 0x0B 0x0A ... 最低位字节是 0x0D 存储在最低的内存地址处。后面字节依次存在后面的地址处。

数据以16 bit 为单位: 地址增长方向 → ... 0x0C0D 0x0A0B ... 最低的 16 bit 单元 0x0C0D 存储在低位。

更改地址的增长方向: 当更改地址的增长方向,使之由右至左时,表格更具有可阅读性。

← 地址增长方向 ... 0x0A 0x0B 0x0C 0x0D ... 最低有效位( LSB )是 0x0D 存储在最低的内存地址处。后面字节依次存在后面的地址处。

← 地址增长方向 ... 0x0A0B 0x0C0D ... 最低的 16 bit 单元 0x0C0D 存储在低位。

高尾端序(大端序 Big-Endian )

就是高位字节(即大端字节、头端字节)存放在内存的低地址,低位字节(即小端字节、尾端字节)存放在内存的高地址。

这是最符合人平时的读写习惯的字节序(但却不符合人的直觉思维),因为不用像在 Little-EndIan 中还需考虑字节的高位、低位与内存的高地址、低地址的对应关系,只需把数值按照人通常的书写习惯,从高位到低位的顺序直接在内存中从左到右或从上到下(下图中就是从上到下)按照由低到高的内存地址,一个字节一个字节地填充进去。

v2-f3827b94d4bf30d8ae3efdae0107cd32_1440w

数据以 8 bit 为单位: 地址增长方向 → ... 0x0A 0x0B 0x0C 0x0D ... 示例中,最高位字节是 0x0A 存储在最低的内存地址处。下一个字节 0x0B 存在后面的地址处。正类似于十六进制字节从左到右的阅读顺序。

数据以 16 bit 为单位: 地址增长方向 → ... 0x0A0B 0x0C0D ... 最高的 16 bit 单元 0x0A0B 存储在低位。

混合序(中间序 Mixed-Endian )

混合序具有更复杂的顺序。以 PDP-11 为例,32 位的 0x0A0B0C0D 被存储为:

32 bit 在 PDP-11 的存储方式 地址增长方向 → ... 0x0B 0x0A 0x0D 0x0C ... 可以看作高 16 bit 和低 16 bit 以大端序存储,但 16 bit 内部以小端存储。 网络序

网络传输一般采用大端序,也被称之为网络字节序,或网络序。IP 协议中定义大端序为网络字节序。

Berkeley 套接字定义了一组转换函数,用于 16 和 32 bit 整数在网络序和本机字节序之间的转换。htonl,htons 用于本机序转换到网络序;ntohl,ntohs 用于网络序转换到本机序。

ps:前文说到 XML和 XHTML 中使用 UTF-8 标准,这些是指本地本机,不是网络传输中,不是一个概念。

处理器体系

Intel 和 AMD 的 X86 平台,以及 DEC( Digital Equipment Corporation 数字设备公司,后与 Compaq 合并,之后Compaq 又与 HP 合并)采用的是 Little-Endian,而像 IBM、Sun 的 SPARC 采用的就是 Big-Endian,有的嵌入式平台是Big-Endian 的,JAVA 字节序也是 Big-Endian 的。

当然,这不代表所有情况。有的 CPU 既能工作于小端序,又能工作于大端序,比如 ARM、Alpha、摩托罗拉的 Power PC、SPARC V9、MIPS、PA-RISC 和 IA6 4等体系结构(具体情形可参考处理器手册),其字节序是可切换的,这种可切换的特性可以提高效率或者简化网络设备和软件的逻辑。

这种可切换的字节序被称为 Bi-Endian(前缀 “Bi-” 表示 “双边的、二重的、两个的” ),用于硬件上指的是计算机存储时具有可以使用两种不同字节序中任意一种的能力。具体这类 CPU 是大端序还是小端序,和具体设置有关。如 Power PC 可支持 Little-Endian 字节序,但其默认配置为 Big-Endian 字节序。

一般来说,大部分用户的操作系统,如 Windows、FreeBSD、Linux 是 Little-Endian 的;少部分,如 Mac OS 是 Big-Endian 的。

具体参见下表:

v2-53c0d3d0c37d5521b4b50970c9934abf_1440w

所以说,Little Endian 还是 Big Endian 与操作系统和 CPU 类型都有关系(可能体现在一个处理器的寄存器、指令集、系统总线等各个层次中)。因此在一个计算机系统中,有可能同时存在大端序和小端序两种字节序的现象。

这一现象为系统的软硬件设计带来了不小的麻烦,这要求系统设计工程师必须深入理解大端序和小端序的差别。

其实,很多技术人员在实际的非跨平台桌面应用开发过程中都很少会直接和字节序打交道,但在跨平台及网络应用开发过程中因为涉及到异构计算机系统间的通讯交流,字节序是很难回避的问题。

字节序标记 BOM

v2-dd04e2869cb7205c20f189bfd384d4d5_1440w

在将逻辑形式的码元序列(或可称之为逻辑编码)映射为物理形式的字节序列(或可称之为物理编码)时,因系统平台的差异,存在一个字节序( Byte-Order 字节顺序)的问题。而字节序问题的存在,导致在某些场合下,需要对文本字符所采用的字节序予以明确说明。Unicode / UCS 规范中所推荐的用于说明字节序的方法是使用 BOM 字节序标记( Byte-Order Mark 字节顺序标记)。

字节序标记 BOM 采用的是 Unicode 码点值为 FEFF(十进制为 65279,二进制为 1111 1110 1111 1111 )的字符,因此BOM 实际上可认为是该字符( U+FEFF )的别名。

最初,字符 U+FEFF 如果出现在字节流的开头,则用来标识该字节流的字节序——是高位在前还是低位在前;如果它出现在字节流的中间,则表达为该字符的原义——零宽度不中断空格( ZERO WIDTH NO-BREAK SPACE 零宽度无断空白)。该字符名义上是个空格,实际上是零宽度的,即相当于是不可见也不可打印字符(平常使用较多的是 ASCII 空格字符,是非零宽度的,需要占用一个字符的宽度,因此为可见不可打印字符)。

不过,从 Unicode 3.2 开始,U+FEFF 只能出现在字节流的开头,且只能用于标识字节序,就如它的别名——字节序标记——所表示的意思一样;除此以外的用法已被舍弃。取而代之的是,使用 U+2060 来表示零宽度不中断空格。

如果 UTF-16 编码的字节序列为大端序,则该字节序标记在字节流的开头呈现为 0xFE 0xFF;若字节序列为小端序,则该字节序标记在字节流的开头呈现为 0xFF 0xFE。如果 UTF-32 编码的字节序列为大端序,则该字节序标记在字节流的开头呈现为 0x00 0x00 0xFE 0xFF;若字节序列为小端序,则该字节序标记在字节流的开头呈现为 0xFF 0xFE 0x00 0x00。

需要特别注意的是,UTF-8 编码本身并不存在字节序的问题,但仍然有可能会用到 BOM——有时被用来标示某文本是UTF-8 编码格式的文本。注意:在 UFT-8 编码格式的文本中,如果添加了 BOM,则只用它来标示该文本是由 UTF-8 编码方式编码的,而不用来说明字节序,因为 UTF-8 编码根本就不存在字节序问题。

许多 Windows 程序(包含记事本)会添加 BOM 到 UTF-8 编码格式的文件中(至于为什么要添加 BOM,请看附文)。然而,在类 Unix 系统中,这种作法则不被建议采用。

因为它会影响到无法识别它的编程语言,如 gcc 会报告源码文件开头有无法识别的字符;而在 PHP 中,如果没有激活输出缓冲( output buffering )的话,则它会使得页面内容开始被送往浏览器(即 http header 头被提交),从而使 PHP 脚本无法再指定 http header 头。

对于已在 IANA 注册的字符编码(实际为字符编码模式 CES )UTF-16BE、UTF-16LE、UTF-32BE 和 UTF-32LE 等来说,不可使用 BOM,因为其名称本身已决定了其字节顺序。而对于已注册的字符编码 UTF-16 和 UTF-32 来说,则必须在文本开头使用 BOM。

不同编码的字节序列中所使用的字节序标记 BOM 本身的字节序列呈现:

v2-b50debff7823004081b786b083914773_1440w

附文 1:理解大端序和小端序

要彻底讲清楚大端序(高尾端序)、小端序(低尾端序),则需要从人读写二进制数的方向和内存地址的增长方向两者相结合讲起:

人读写二进制数的方向为(这是确定不变的):

左 ---> 右,大端 / 头端 / 高位 ---> 小端 / 尾端 / 低位; 或上 ---> 下,大端 / 头端 / 高位 ---> 小端 / 尾端 / 低位;

内存地址的增长方向则为(这是确定不变的):

左 ---> 右,低地址 ---> 高地址; 或上 ---> 下,低地址 ---> 高地址。

不过,计算机在内存中存取数据的方向则不是确定不变的,而是分为两种**(注意,由于人的读写方向和内存地址增长方向是确定不变的,因此这里指的是计算机在内存中 “写入”或**“读取”数据的方向):

左 ---> 右,大端 / 头端 / 高位 ---> 小端 / 尾端 / 低位; 或上 ---> 下,大端 / 头端 / 高位 ---> 小端 / 尾端 / 低位;

这种情况下,站在人的读写方向和内存地址增长方向(这两者的方向刚好一致)的角度来看,则是:大端在左(或在上),所以称之为大端序;或者说尾端在内存高地址,所以称之为高尾端序(即内存高地址存放多字节数据的尾端字节的字节顺序)。

右 ---> 左,大端 / 头端 / 高位 ---> 小端/尾端 / 低位; 或下 ---> 上,大端 / 头端 / 高位 ---> 小端 / 尾端 / 低位。

这种情况下,站在人的读写方向和内存地址增长方向(这两者的方向刚好一致)的角度来看,则是:小端在左(或在上),所以称之为小端序;或者说尾端在内存低地址,所以称之为低尾端序(即内存低地址存放多字节数据的尾端字节的字节顺序)。

【特别提示:大端序、小端序特别容易搞混,不好记忆;因此,建议使用高尾端序、低尾端序,本人是按下面这个方法来记忆的,很容易记住:存储的数据分头和尾 —左头右尾,内存的地址分低和高 — 左低右高;因此,“高尾端”表示内存的高地址存储在数据的尾端,“低尾端”表示内存的低地址存储在数据的尾端。】

ps:下图来自阮一峰老师的《理解字节序》,建议点进去看看,可以结合起来理解。

v2-048187131e32ab903297e9aaab374111_1440w

注意,上文的 “数据” 指的是数据类型意义上的数据,因此,准确地说字节序只跟多字节的整型数据类型有关,比如 int、short、long 型;跟单字节的整型数据类型 byte 无关。

ps:int、short、long 刑,都是 C 语言的数据类型。

那么,为什么就只跟多字节的整型数据有关,而跟单字节的整型数据无关呢?

因为在现代计算机中,字节是计算机数据存储的基本单位,对于整体上的单一字节( a byte ),涉及到的是其 8 个比特位的顺序(即位序、比特序,一般直接由硬件处理,程序员大致了解即可,这里不深入探讨),显然不存在字节序问题。

然而,对于在数据类型这个逻辑意义上作为一个整体的多字节数据而言,它是由计算机物理意义上各个可被单独寻址的字节所组成的(处理器寻址的最小单位一般是 1 个字节),由于历史的原因,其各个字节的存储顺序在不同的系统平台(包括 CPU 和操作系统)上是不同的。

也就是说,如果计算机处理的数据是逻辑意义上的单字节数据类型( byte ),是不存在字节序问题的,因为计算机物理意义上单独寻址的最小单位刚好也是字节,单个字节已经是处理器寻址的最小单位以及存储和传输的最小单元了,存储时单字节数据类型直接进行,读取时也不存在根据前后 2 个字节才能解析出其值的情况,而构造字节流时也不会从一个单字节数据类型的值当中产生 2 个或以上的字节(既然是单字节数据类型,构造字节流时当然只可能产生1个字节)。

但是,如果计算机处理的数据是逻辑意义上的多字节数据类型( int、short、long 等),虽然由于构成它们的 2 个或 2 个以上的字节是作为一个整体来进行处理的(比如以汇编语言中的数据类型 word 或 dword 为单位进行一次性处理,而不是以 byte 为单位分次处理;更深入地来讲,CPU 一般是以字为一个整体来处理数据的,当单个数据不足一个字长时,则将多个数据 “拼成” 一个字再进行处理),但问题是从计算机物理意义上来说,如前所述,字节才是 CPU 对内存寻址的最小单位以及存储和传输的最小单元。

因此,CPU 在读取作为一个整体来进行处理的多字节数据类型的数据时,必须根据前后 2 个或 2 个以上的字节来解析出一个多字节数据类型的值;而且构造字节序列时也会从一个多字节数据类型的值当中产生 2 个或 2 个以上的字节。

这样一来,多字节数据类型的数据内部各字节间的排列顺序,是会影响从字节序列恢复到数值的;反之,也会影响从数值到字节序列的构造。

所以,在存储和读取多字节数据类型的数据时,必须按照计算机系统所规定的字节序来进行(这一点程序员了解即可,计算机会自动处理);而尤其是在跨字节序不同的异构计算机系统进行通讯并交换数据时,通讯的任何一方更是必须明确对方所采用的字节序 ,然后双方将各自接收到的数据按各自的字节序对数据进行转换(有时候需要程序员专门编写转换程序),否则通讯将会出错,甚至直接失败。

实际上,int、short、long 等数据类型一般是高级编程语言层面的概念,更进一步而言,这其实涉及到了机器硬件层面(即汇编语言)中的数据类型 byte 字节、word 字、dword 双字等在硬件中的表达与处理机制(实质上字节序跟 CPU 寄存器的位数、存放顺序密切相关)。具体可看下两个附文:《本质啊本质之一:数据类型的本质》、《寄存器与字、字节》。

附文 2:数据类型的本质

CSDN博客 博主:band_of_brothers 发表于:2007-10-10 22:20

研究一个层面的问题,往往要从更深的层面找寻答案。这就如 C语言与汇编、汇编与机器指令,然而终究要有个底限,这个底限以能使我们心安理得为准,就好比公理之于数学、三大定律之于宏观物理。

在这里就将机器指令作为最后的底限吧,尽管再深入下去还有微指令,但那毕竟是太机器了,可以了。以下所有从 C 代码编译生成汇编代码用的是命令:cl xxx..c /Fa /Ze。

类型的本质

类型这个概念,好多地方都有讲,但说实话,你真的理解吗?什么是类型?类型是一个抽象的概念还是一个真实的存在?开始:

1)“好多相同或相似事物的综合”(辞海)。

2)X86 机器的数据类型有 byte、word、dword、fword、tword、qword,等等。

3)“给内存块一个明确的名字,就象邮件上的收件人一样。给其一个明确的数据类型,就好象说,邮件是一封信,还是一个包裹。”

4)类型就是一次可以操作的块的大小,就是一个单位,就像克、千克、吨一样。双字,一次操作 32 位;字,一次操作16 位;如果没有各种类型,机器只有一个类型单位 — 字节,那么当需要一个 4 字节大小的块时,就需要 4 次操作,而如果有双字这个类型单位,那么只需要一次操作就可以了。

5)类型,是机器层面支持的,不是软的,是硬的,有实实在在的机器码为证。

类型的反汇编

v2-e7e1cc5ac9a531f301ce5a65fd8c948e_1440w

W32dasm 反汇编出来的东西,可以看出不同的类型,机器码不同,说明类型是机器硬件级别支持的,不是通过软件实现的,更不是一个抽象的概念。

Opcodes 上关于 mov 的机器码讲的更清楚:

v2-1a1d5785d9406a20ee442537092b961b_1440w

需要说明的是,一些大的类型单位,如 qword 等,在 mov 等标准指令里是没有的,在一些特殊指令里才能用到,如浮点指令:fmul qword ptr [ 0067FB08 ] 机器码:DC0D08FB6700。

附文 3:寄存器与字节、字、双字

字节:记为 byte,一个字节由 8 个比特( bit )组成,可以直接存在一个 8 位寄存器里,1010 1001 一个字节。

字:记为 word,一个字由 2 个字节(共 16 比特)组成,可以直接存在一个 16 位寄存器里,1010 1001 1010 1001(前面高位字节,后面低位字节)。

双字:记为dword,一个双字由 4 个字节(共 32 比特)组成,可以直接存在一个 32 位寄存器里,1010 1001 1010 1001 1010 1001 1010 1001(依次:高位字节、次高位字节、次低位字节、低位字节)。

一个 8 位寄存器用 2 位十六进制数表示,只能存 1 个字节( 1 个 byte 类型的数据)。

一个 16位 寄存器用 4 位十六进制数表示,可存 1 个字( 1 个 word 类型的数据)或 2 个字节( 2 个 byte 类型的数据)。

一个 32 位寄存器用 8 位十六进制数表示,可存 2 个字( 1 个 dword 类型的数据或 2 个 word 类型的数据)或 4 个字节( 4 个 byte 类型的数据)。

下一篇 字符编码(四:UTF 系列编码详解)



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3