http://blog.csdn.net/longintchar/article/details/51079340

********************************************

字符编码的故事:ASCII,GB2312,Unicode,UTF-8,UTF-16

由于好奇,我非常想搞清楚关于字符编码的疑惑。比如Unicode,UTF-8,UTF-16,以及有BOM,无BOM之前的区别和联系。参考了很多资料后,我终于初步理解了。在这里总结成博文,希望对读者有所帮助。如果有什么错误,还请您不吝赐教。

字符

字符是各种文字和符号的总称,包括各国家文字、标点符号、图形符号、数字等,甚至还可以包括无法显示的字符(比如ASCII标准定义了128个字符,其中33个字符无法显示)。

字符表与字符集

为了使计算机能够处理字符信息,首先要决定选取哪些字符。这样就形成了一个集合,或者说一个表,称为字符表(character repertoire)。当然,也可以认为这就是一个字符集(character set)。
例如,将所有的英文字母放在一起可以组成一个字符集,将所有的汉字放在一起可以组成一个字符集,等等。

代码点(code point or code position)

对一个字符集中的所有字符进行编号(赋予一个非负整数的序号),每个字符的编号在这个字符集里都是独一无二的。例如,由所有英文字母组成的字符集中
有52个字符(小写字母和大写字母各26个),可以将这些字符依次编号为0、1、2、…51。一个字符的编号称为该字符的代码点。

值得注意的是,对字符进行编号有很多种方法,只要保证编号的唯一性即可。

编码字符集(coded character set)

对一个字符集中的所有字符进行编号,这种编号后的字符集叫做编码字符集(这里的编码仅仅指编号,不同于下文中的编码)。常见的编码字符集有ASCII、Unicode、GBK等。

代码点与字符编码的关系

代码点仅仅是一个字符的编号,通常与字符在字符表中的位置有关,但是与它在计算机内的数字表示形式、存储方法无关。

我们在计算机屏幕上看到的是形象的字符,而在计算机内部存放的是二进制的比特序列。所谓字符编码,就是建立一种转换规则或者说映射关系,将一个字符集中的代码点按照某种规则转换成可以在计算机内存储的二进制比特序列。

用图来表示就是:

对于一个字符集来说,要正确编码解码一个字符需要三个关键元素:

  • 字符集(character set)本身
  • 编码字符集(coded character set)
  • 字符编码(character encoding form)

其中字符集规定了计算机能处理哪些字符;编码字符集,即用一个代码点来表示一个字符在字符表中的位置;字符编码,建立一种映射关系:代码点和实际存储数值之间的关系。

需要注意的是:一个字符集可能对应着多种不同的编码方式。

编码单元

对代码点进行编码后的二进制的比特序列,是由一个或者多个编码单元按照一定的序列组成的。一个编码单元由若干个比特构成,例如7比特、8比特等(最常用的是8/16/32比特,便于存储和传输)。

比如说,UTF-8编码,采用1~4个8比特的编码单元;
UTF-16编码,采用1~2个16比特的编码单元;
UTF-32编码,采用1个32比特的编码单元。
前两个属于变长编码,后一个属于等长编码。

从ASCII编码说起

ASCII(英语发音:/ˈæski/,American Standard Code for Information Interchange,美国信息交换标准代码),是基于拉丁字母的一套电脑编码系统。

ASCII至今为止共定义了128个字符,其中33个字符无法显示,且这33个字符多数都已是陈废的控制字符。在33个字符之外的是95个可显示的字符,用键盘敲下空白键所产生的空白字符也算1个可显示字符(显示为空白)。

读了前几节的内容,估计很多读者都会想到一种简单的编码方式:既然字符集中的字符都有自己唯一的代码点,那么直接把代码点作为存储值就可以了。

Yes!ASCII字符集就是这样编码的。例如在ASCII字符集中,字符A的代码点是十进制的65,如果用一个7比特的编码单元来表示的话,就是100 0001,这正是字符A的编码。

在存储的时候,由于字节(8比特)是计算机中的常用单位,故仍以一个字节来存放字符的编码,每个字节中多余出来的最高位,统一规定为0.

非ASCII编码

用ASCII字符集来表示英语是没有问题的,但是若用它来表示其他语言,肯定是不行的。比如,在法语中,字母上方有注音符号,它就无法用ASCII字符表示。

于是,一些欧洲国家就决定,利用字节中闲置的最高位编入新的字符。比如,法语中的é的编码为130(二进制为1000 0010)。这样一来,这些欧洲国家使用的编码体系,可以表示最多256个字符。

但是,这里又出现了新的问题。不同的国家有不同的字母,因此,哪怕都使用256个字符的编码方式,代表的字母却不一样。比如,130在法语编码中代表了é,在希伯来语编码中却代表了字母Gimel(ג)。

至于亚洲国家的文字,使用的字符就更多了,汉字就多达10万左右。假如把这10万个汉字从1开始编号,那就是10万个代码点,如果直接把代码点作为存储值,一个字节只能表示256种字符,肯定是不够的,所以很容易想到用8个以上的比特(连续的多个字节)来表示一个字符。

GB2312

GB2312是中国国家标准简体中文字符集,共收录6763个汉字,其中一级汉字3755个,二级汉字3008个;此外,GB2312还收录数
学符号、拉丁字母、希腊字母、日文平假名及片假名字母、俄语西里尔字母等682个字符。这些非汉字字符有些来自ASCII字符集,但被重新编码为双字节,
并称为”全角”字符;ASCII原字符则称为”半角”字符。例如,全角a编码为0xA3E1,半角a则编码为0x61。

对ASCII字符集的兼容

对于ASCII字符集,编码不变,从0x00~0x7F.

区位码

GB2312中对所收汉字进行了“分区”处理,编号为01区至94区;每区含有94个字符,编号为01位至94位。这种表示方式也称为区位码。每一个字符都由与其唯一对应的区号和位号所确定。
例如:“啊”的编号为16区01位,它的区位码就是1601(可以认为这就是一个代码点)。
01–09区为特殊符号。
16–55区为一级汉字,按拼音排序。
56–87区为二级汉字,按部首/笔画排序。

举例来说,“啊”字是第16区中的第1个字符,它的区位码就是1601;“琛”字是第72区的第41个字符,它的区位码就是7241。

对非ASCII字符如何编码

GB2312的编码单元是8比特,刚好是一个字节。对于非ASCII字符,每个字符由2个编码单元组成。第一个编码单元称为“高位字节”,第二个编码单元称为“低位字节”。
高位字节 = 区号 + 0xA0
低位字节 = 位号 + 0xA0

举例:“啊”的区位码是1601,
高位字节 = 0x10(=16) + 0xA0 = 0xB0
低位字节 = 0x01(=1) + 0xA0 = 0xA1
所以,“啊”的编码就是0xB0(第一个编码单元)0xA1(第二个编码单元)。

再比如,“琛”的区位码是7241,
高位字节 = 0x48(=72) + 0xA0 = 0xE8
低位字节 = 0x29(=41) + 0xA0 = 0xC9
所以,“琛”的编码就是0xE8(第一个编码单元)0xC9(第二个编码单元)。

说明:“高位字节”使用了0xA1–0xF7(把01–87区的区号加上0xA0),“低位字节”使用了0xA1–0xFE(把01–94加上0xA0)。不管是高位字节还是低位字节,它们的最高位都是1,这样就可以和ASCII字符的编码区分开。

字节序

在计算机系统中,存储对象一般是以字节为单位的,每个地址单元都对应着一个字节。
对于单字节对象,存放方法和传输方式一般相同。
对于多字节对象,如整数(32位机中一般占4字节),情况就不同了。我们必须建立两个规则:

  • 这个对象的地址是什么
  • 在存储器中如何排列这些字节

在几乎所有机器上,多字节对象都被存储为连续的字节序列,对象的地址为所使用字节中最小的地址。
关于字节的排列,一般有两种规则:小端法(little endian)与大端法(big endian)。

首先介绍两个概念。

  • 最高有效字节(the Most Significant Byte):指多字节序列中具有最大权重的字节。
  • 最低有效字节(the Least Significant Byte):指多字节序列中具有最小权重的字节。
    比如0x778532,最高有效字节是0x77,最低有效字节是0x32;

小端(最符合人类思维的字节序 )

规则:顺着地址增加的方向,按照从最低有效字节到最高有效字节的顺序存储。
记忆口诀:高址高字节
为什么说是最符合人类思维的字节序?因为从逻辑的角度来说,高有效位在高地址处,低有效位在低地址处,想起来很自然。

大端(最直观的字节序)

规则:顺着地址增加的方向,按照从最高有效字节到最低有效字节的顺序存储。
记忆口诀:高址低字节
为什么说是最直观字节序?假设存储地址从左到右依次增加,请把这个对象读出来,将你读的内容,从左到右填充内到存即可。

假设要把对象0x87654321存储到地址0x1000处,大小端的区别如下图所示:

字节序与字符编码

对于任何字符编码,编码单元之间的顺序是由编码方式指定的,与endian无关。例如GB2312的编码单元是字节,用两个字节表示一个字符。这两个字节的顺序是固定的,不受CPU字节序的影响。

举例来说,汉字“啊”的编码是0xB0(第一个编码单元)0xA1(第二个编码单元),不管在小端系统还是大端系统,存储格式都是0xB0(较小的地址)0xA1(较大的地址)。

UTF-16的编码单元是16比特(一个字),字之间的顺序是编码方式指定的,字内部的字节排列才会受到endian的影响(后文会介绍)。

Unicode

世界上存在着多种编码方式,同一个二进制序列可以被解释成不同的符号。因此,要想正确解码一个文本文件,就必须知道它的编码方式,否则很可能出现乱码。

如果有一种编码,将世界上所有的字符都纳入其中。对于每个字符都给予一个独一无二的编码,那么乱码问题就会解决。于是,Unicode诞生了。

Unicode字符集由多语言软件制造商组成的统一码联盟(Unicode Consortium)与国际标准化组织的ISO-10646工作组制订,为各种语言中的每个字符指定统一且唯一的代码点,以满足跨语言、跨平台转换和处理文本的要求。

Unicode代码点范围为0x0~0x10FFFF,共计1114112个代码点,划分为编号0~16的17个字符平面,每个平面包含65536
个代码点。其中编号为0的平面最为常用,称为基本多语种平面(Basic Multilingual Plane,
BMP);其他平面则称为辅助语言平面。

为了描述一个代码点,可以采用U加十六进制整数的方法。比如,U+0041表示英文大写字母A,U+4E25表示汉字”严”。

常见的Unicode编码方式

Unicode只是一个编码字符集,并没有制定具体的编码方式。

关于编码方式,当然可以采用类似ASCII字符集的编码方式——代码点等值转换法(这是我自己起的名字)。既然Unicode代码点的值的范围是0~0x10FFF,那么可以用一个21比特的编码单元来编码,直接把代码点等值转换成21比特的二进制序列。

但是这存在一个空间使用的问题,例如对于使用英语的人而言,ASCII基本可以满足使用。如果使用ASCII码,只需要1个字节来存储字符,但是若使用刚才的思路,需要将近3个字节来存储,这显然是浪费空间的。

有没有其他的编码方式呢?
Unicode的实现方式称为Unicode转换格式(Unicode Transformation Format,简称为UTF)。UTF-8,UTF-16,UTF-32是三种不同的Unicode编码方式,这三种编码方式都用于编码U+0000~U+D7FF以及U+E000~U+10FFFF的Unicode代码点。

注意:Unicode标准规定U+D800~U+DFFF的代码点不对应于任何字符。

UTF-8

UTF-8(8-bit Unicode Transformation
Format)是一种针对Unicode的可变长度字符编码,也是一种前缀码。它可以用来表示Unicode标准中的任何字符,且其编码中的第一个字节与
ASCII兼容,这使得原来处理ASCII字符的软件无须或只须做少部分修改,即可继续使用。因此,它逐渐成为电子邮件、网页及其他存储或发送文字的应用
中,优先采用的编码。

编码及举例

UTF-8的编码单元是8比特,对于每个代码点,采用1~4个编码单元编码。下图摘自维基百科,我只截取了原图的一部分。

举例来说,已知”严”的代码点是U+4E25,(0x4E25=100111000100101b),根据上表,可以发现4E25处在第三行的范围内(U+0800~U+FFFF),因此”严”的UTF-8编码需要三个字节,格式是1110xxxx 10xxxxxx 10xxxxxx。然后,从”严”的最后一个二进制位开始,依次从后向前填入格式中的x,多出的x补0。这样就得到”严”的UTF-8编码是11100100 10111000 10100101,转换成十六进制就是0xE4B8A5

在存储的时候,这三个编码单元的顺序是固定的。不管是大端序还是小端序,都是按照地址增加的方向依次存储0xE4,0xB8,0xA5.

UTF-16

UTF-16的编码单元是16比特,对于每个代码点,采用1个或者2个编码单元来表示,因此这是一个变长表示。

编码及举例

U+0000~U+D7FF以及从U+E000~U+FFFF的代码点

将代码点等值转换为16比特的二进制序列即可。
比如字符$的代码点是U+0024,对应的16比特的二进制序列为0000 0000 0010 0100,即0x0024.

U+10000~U+10FFFF的代码点

辅助平面(Supplementary Planes)中的代码点,在UTF-16中被编码为一对16比特长的码元,称作代理对(surrogate pair),具体方法是:
用代码点减去0x10000,得到的值的范围为20比特长的0x00000~0xFFFFF(不足20比特就在高位填充0);然后把这20比特拆分为高10比特和低10比特,
第一个编码单元 = 高10比特 + 0xD800;
第二个编码单元 = 低10比特 + 0xDC00;

或者说,用高10比特取代下面模板中的x,用低10比特取代下面模板中的y。
110110xxxxxxxxxx 110111yyyyyyyyyy

例如要对代码点U+10437编码:
0x10437减去0x10000,结果为0x00437,二进制为0000 0000 0100 0011 0111。
拆成高10位和低10位:0000000001(=0x0001) 和 0000110111(=0x0037)。
第一个编码单元 = 0x0001 + 0xD800 = 0xD801;
第二个编码单元 = 0x0037 + 0xDC00 = 0xDC37;

UTF-32

UTF-32采用代码点等值转换法,将每个代码点编码为1个32比特的编码单元(四字节),因此空间效率较低,不如其它Unicode编码应用广泛。

字符编码方案(Encoding Scheme)与BOM(Byte Order Mark)

上面的字符$的编码如何存储呢?注意,我们要存储的是16比特的0x0024,这是一个多字节对象。这时候,前面说的大小端就派上用场了。对于大端的系统,低地址存储0x00,高地址存储0x24;对于小端的系统,刚好相反。

假设你的计算机是小端序,计算机里有一个文本文件采用UTF-16编码。把你的文本文件传输到我的计算机上,可是我的计算机是大端序,显然我无法正确解码。这可怎么办?

因为上面的原因,就产生了所谓的UTF-16、UTF-16LE、UTF-16BE、UTF-32、UTF-32LE、UTF-32BE编码方案。编码方案是编码方式的细化,它决定了编码的传输和存储格式。字符编码方案主要关注跨平台处理编码单元宽度超过一个字节的数据。

在UTF-16文件的开始,一般会放置一个代码点为U+FEFF的字符作为BOM(字节顺序标记),这个字符在Unicode字符集中代表的意义是ZERO WIDTH NO-BREAK SPACE,顾名思义,它是个没有宽度也没有断字的空白。

UTF-16BE

表示以大端序存储。我们会发现最开始的两个字节是0xFE0xFF.

UTF-16LE

表示以小端序存储。我们会发现最开始的两个字节是0xFF0xFE.

下面的表格是UTF-16编码方式下不同编码方案的示例,来自http://www.unicode.org/versions/Unicode4.0.0/ch03.pdfUnicode

注意:BOM不是必需的。当没有BOM的时候,UTF-16和UTF-32编码默认为大端序。

UTF-8需要BOM吗

UTF-8以字节为编码单元,没有字节序问题,BOM仅仅用于表明其编码格式(signature),但不建议如此。因为UTF-8编码特征明显,无需BOM即可检测出是否为UTF-8格式(文件较短时可能不准确)。
Unicode标准并未要求或建议UTF-8编码使用BOM,但确实允许BOM出现在文件开头。带有BOM的Unicode文件有时会带来一些问题:

  • Linux/Unix系统未使用BOM,因为它会破坏现有ASCII文件的语法约定。
  • 某些编辑器不会添加BOM,或者可以选择是否添加BOM(比如NotePad++)。
  • 某些语法分析器可以处理字符串常量或注释中的UTF-8,但无法分析文件开头的BOM。
  • 某些程序在文件开头插入前导字符来声明文件类型等信息,这与BOM的用途冲突。

顺便说一下,当我把汇编源文件变成带BOM的UTF-8格式时,在Linux环境下,NASM编译器根本不认识它,会报错。

【end】


参考资料
[0]《标准C语言指南》(电子工业出版社,李忠)
[1]http://www.herongyang.com/gb2312_gb/GB2312-to-Unicode-Row-72-E8A1-E8FE.html
[2]http://www.letiantian.me/2015-03-02-character-encoding/
[3]http://cenalulu.github.io/linux/character-encoding/
[4]https://zh.wikipedia.org/wiki/GB_2312
[5]http://www.ruanyifeng.com/blog/2007/10/ascii_unicode_and_utf-8.html
[6]http://www.cnblogs.com/jacktu/archive/2008/11/24/1339789.html
[7]https://zh.wikipedia.org/wiki/UTF-8
[8]http://www.cnblogs.com/clover-toeic/p/5291787.html

字符编码的故事:ASCII,GB2312,Unicode,UTF-8,UTF-16的更多相关文章

  1. 字符编码简介:ASCII,Unicode,UTF-8,GB2312

    字符编码简介:ASCII,Unicode,UTF-8,GB2312 1. ASCII码 我们知道,在计算机内部,所有的信息最终都表示为一个二进制的字符串.每一个二进制位(bit)有0和 1两种状态,因 ...

  2. 字符编码笔记:ASCII,Unicode和UT…

    字符编码笔记:ASCII,Unicode和UTF-8 今天中午,我突然想搞清楚Unicode和UTF-8之间的关系,于是就开始在网上查资料. 结果,这个问题比我想象的复杂,从午饭后一直看到晚上9点,才 ...

  3. 字符编码笔记:ASCII,Unicode和UTF-8(转)

    字符编码笔记:ASCII,Unicode和UTF-8 作者: 阮一峰 日期: 2007年10月28日 今天中午,我突然想搞清楚Unicode和UTF-8之间的关系,于是就开始在网上查资料. 结果,这个 ...

  4. 字符编码笔记:ASCII,Unicode 和 UTF-8(理解)

    1.ASCII 码 美国制定的字符编码规则,对英语字符与二进制位之间的关系做了统一规定. 占一个字节,8 位,最多可表示 2^8 = 256 种状态(字符) 实际共有 128 个字符,只占用一个字节的 ...

  5. [转帖]字符编码笔记:ASCII,Unicode 和 UTF-8

    字符编码笔记:ASCII,Unicode 和 UTF-8 http://www.ruanyifeng.com/blog/2007/10/ascii_unicode_and_utf-8.html 转帖 ...

  6. 【转载】字符编码笔记:ASCII,Unicode和UTF-8

    字符编码笔记:ASCII,Unicode和UTF-8 作者: 阮一峰 今天中午,我突然想搞清楚Unicode和UTF-8之间的关系,于是就开始在网上查资料. 结果,这个问题比我想象的复杂,从午饭后一直 ...

  7. (转)字符编码笔记:ASCII,Unicode和UTF-8

    字符编码笔记:ASCII,Unicode和UTF-8 访问地址:http://www.ruanyifeng.com/blog/2007/10/ascii_unicode_and_utf-8.html

  8. 字符编码笔记:ASCII,Unicode和UTF-8

    很久很久以前,有一群人,他们决定用8个可以开合的晶体管来组合成不同的状态,以表示世界上的万物.他们看到8个开关状态是好的,于是他们把这称为"字节". 再后来,他们又做了一些可以处理 ...

  9. 字符编码笔记:ASCII、Unicode、UTF-8、UTF-16、UCS、BOM、Endian

    转载:http://witmax.cn/character-encoding-notes.html 今天中午,我突然想搞清楚Unicode和UTF-8之间的关系,于是就开始在网上查资料. 结果,这个问 ...

  10. 【转】字符编码笔记:ASCII、Unicode、UTF-8 和 Base64

    1. ASCII码 我们知道,在计算机内部,所有的信息最终都表示为一个二进制的字符串.每一个二进制位(bit)有0和1两种状态,因此八个二进制位就可以组合出256种状态(-128~127),这被称为一 ...

随机推荐

  1. MySQL Event计划任务刷慢日志

    前言 最近在尝试一个日志系统graylog来收集mysql的慢查询日志提,供后续的分析.监控和报警等.测试步骤已经到日志已成功收集到graylog,测试时需要刷一些慢查询日志出来.为了刷比较多的日志和 ...

  2. Tomcat 监控方法

    Tomcat 监控方法 方法1:.使用tomcat自带的status页 具体方法: 步骤1:修改%tomcat安装路径%\conf \tomcat-users文件,配置admin设置权限.在<t ...

  3. PHP-FastCGI详解

    一.什么是 FastCGI FastCGI是一个可伸缩地.高速地在HTTP server和动态脚本语言间通信的接口.多数流行的HTTP server都支持FastCGI,包括Apache.Nginx和 ...

  4. JDK 生成数字证书

    JDK(keytool.exe)生成数字证书 2010-11-21 15:52 QUOTE: keytool JAVA是个密钥和证书管理工具.它使用户能够管理自己的公钥/私钥对及相关证书,用于(通过数 ...

  5. nginx error: upstream prematurely closed connection while reading response header from upstream

    本篇文章由:http://xinpure.com/nginx-error-upstream-prematurely-closed-connection-while-reading-response-h ...

  6. NYOJ----次方求模

    次方求模 时间限制:1000 ms  |  内存限制:65535 KB 难度:3   描述 求a的b次方对c取余的值   输入 第一行输入一个整数n表示测试数据的组数(n<100)每组测试只有一 ...

  7. 面向对象程序设计(OOP设计模式)-行为型模式之观察者模式的应用与实现

    课程名称:程序设计方法学 实验5:OOP设计模式-行为型模式的应用与实现 时间:2015年12月02日三,第3.4节地点:理 一.实验目的 加深对行为型设计模式的理解以及在开发中的实际应用能力. 二. ...

  8. Android用shareUserID实现多个Activity显示在同一界面

    近来整理文档,发现两年前研究Android多个Activity叠加显示的方案.时光荏苒,一去不回. 虽然后来没有用上,但还是整理如下,Android版本还是2.2的: ActivityGroup描画方 ...

  9. 线程的属性和 线程特定数据 Thread-specific Data

    一.posix 线程属性 POSIX 线程库定义了线程属性对象 pthread_attr_t ,它封装了线程的创建者可以访问和修改的线程属性.主要包括如下属性: 1. 作用域(scope) 2. 栈尺 ...

  10. POSIX 共享内存和 系列函数

    在前面介绍了system v 共享内存的相关知识,现在来稍微看看posix 共享内存 和系列函数. 共享内存简单来说就是一块真正的物理内存区域,可以使用一些函数将这块区域映射到进程的地址空间进行读写, ...