目录算法
一、字符和编码编程
二、ISO/IEC 10646和Unicode网络
三、UTF-16编码
四、UTF-8spa
五、UTF-32debug
六、UCS-2和UCS-4设计
附录1:16进制数与二进制数、10进制数的换算调试
附录2:查看字符编码的方法code
汉字、英文字母、数字符号、标点符号等文字符号统称为字符。计算机本质上只能处理数值,每一个字符在计算机中只能用一个整数来表示,这种整数称为字符编码。字符编码固然必须统一制定,但历史上各类文字的字符编码都是由使用该种文字的国家或地区自行制定,例如美国为英文字符制定的编码ASCII,中国大陆为汉字制定的编码GB2312和GBK,港澳台地区使用的繁体汉字编码Big5等等。这些编码使得文字能够在计算机中处理和存储,而且在使用一样编码的地区能够互相交流计算机文字文档。可是使用这种编码的文字一旦在不一样的地区之间互相交流,就会出现问题。由于编码是各自独立编制的,编码空间不免出现重叠,即,一样的一些编码在不一样的编码标准中将表示不一样的字符。内存
以汉字为例。1981年发布的汉字编码标准GB2312只收录简体汉字,共有7445个字符,全体字符划分为94个区,每一个区94个字符,每一个字符由其所在的区号和区内位号惟一肯定。区号加位号能够做为一种编码,这种编码称为区位码。例如,汉字“码”位于34区的第75位,其区位码为3475。区位码不能直接在机器中使用,由于它的编码空间与ASCII重叠,若是文字中既有汉字又有英文,就会产生矛盾。例如,“码”的区位码在机器中将是两个字节,第一个字节是34,第二个字节是75。然而在ASCII中,编码34表示字符“””,75表示字符“K”,因而,34和75两个数字到底是表明汉字“码”,仍是表明两个ASCII字符“””和“K”?为了解决这个问题,GB2312的字符在机器中其实是采用机内码来表示。机内码和区位码有简单的对应关系:将区位码的区号和位号分别存放在两个字节中,而后每一个字节都加上0xA0(16进制数)便可。机内码使得汉字编码的空间为0xA1A1到0xF7FE,每一个汉字的编码为两个字节,每一个字节的编码在0xA1到0xFE之间,而ASCII的编码是一个字符一个字节,每一个字节的编码在0x00到0x7F之间,这样就使得英文和汉字的编码空间再也不重叠。
释疑:本章中出现的数字,凡是用“0x”开头的即为16进制数,没有的则为10进制数。不熟悉16进制数的读者可参阅本章的附录。
GB2312收集的汉字太少,而且只有简体字。1995年电子科技部质量司和国家技术监督局标准化司颁布了一个指导性规范GBK,其中包含了GB2312的所有字符,而且这些字符的编码就是原来的机内码,同时还包括了Big5中的所有字符以及日文、韩文中使用的汉字。Microsoft的Windows中文简体版从Windows 95开始全面支持GBK,这使得GBK成为事实上的工业标准。GBK的编码空间从0x8140到0xFEFE,每一个汉字的编码仍为两字节,其中第一个字节8位二进制数的最高位始终是1,这使得GBK的编码空间能够避开ASCII的编码空间。机器只需从文档的第一个字节开始判别,若是这个字节的最高位是0,那么这是一个ASCII字符,若是最高位是1,那么连同其后的一个字节表示一个GBK字符。
可是GBK的编码与Big5的编码不一样,而且Big5的编码空间是0xA140到0xFEFE,二者的编码空间是重叠的,这使得一样一个编码,在GBK中表示一个字符,而在Big5中表示的倒是另一个字符。
解决这种问题的最好办法是统一编码,无论简体字仍是繁体字,还有日文、韩文中使用的汉字,所有统一编码,使得每一个编码对应的汉字是惟一的,而且与世界上全部其它语言的字符编码不重叠。
ISO/IEC 10646和Unicode都是全球字符统一编码标准,ISO/IEC 10646是国际标准化组织颁布的标准,Unicode是一个称为Unicode协会的工业组织发布的标准。这两套标准并非彻底相同的,可是在字符编码方面,二者是相同的。ISO/IEC 10646和Unicode都会不断更新并推出新版本,但这两个标准将在字符编码方面保持一致,而且版本更新也保持同步。例如,Unicode 3.0与ISO/IEC 10646-1第二版同步,Unicode 4.0与ISO/IEC 10646第三版同步。
ISO/IEC 10646和Unicode收集了世界上全部书写语言所使用的字符,包括当前正在使用的和古代的,将全部这些字符统一编码。其编码方法是首先将全部的字符排列在若干个平面上,每一个平面划分为256行,每行256列,从而每一个平面存放64K(1K=1024)个字符。每一个字符所在的平面号、行号和列号称为码位(code point),码位可做为字符的编码。例如,“码”字在编号为0的平面上,位于第120行第1列,其平面号、行号和列号分别用十六进制数表示就是0x00、0x78和0x01,所以其码位为0x007801。每一个平面的行号和列号都从0开始数起,所以行号和列号的范围都是0x00到0xFF,平面内字符的行号加列号编码范围从0x0000到0xFFFF。因而平面0的字符码位是0x000000~0x00FFFF,平面1的字符码位是0x010000~0x01FFFF,平面2的字符码位是0x020000~0x02FFFF,余类推。
平面0是最重要的平面,称为BMP(Basic Multilingual Plane),其中基本上包括了当今世界上各类语言使用的字符,固然也包含中、日、韩三种文字中使用的全部汉字。
BMP中的第0行的字符及其码位编码彻底与Latin 8859-1相同,特别是,其中码位编码0x0000到0x007F及其对应的字符彻底与ASCII中的字符及其编码相同。
Unicode中进行编码的汉字不只有中国大陆、港澳台地区使用的汉字,也有日文、韩文中使用的汉字,这三个民族使用的汉字尽管有些字的字形相同,但其含义却千差万别。为此,对这些汉字按必定的规则进行甄别和统一,被承认为同一个字的字符给予惟一的一个码位,不承认的字,即便字形相同,也做为不一样的字符给予不一样的码位。这样聚集起来的汉字字符称为中日韩统一汉字(CJK Unified Ideographs)。在BMP中,CJK统一汉字的码位范围以下:
0x3000~0x303F:CJK标点符号,例如【、】、《、》等。
0x3190~0x319F:CJK笔划。
0x3200~0x32FF:用圆圈圈起来的一些字符和数字,例如㈠、㈥、㊣等。
0x3300~0x33FF:CJK兼容汉字。这是一些表示日期、时间和度量衡的符号,做为单个汉字,例如㎎、㎏、㏎等。
0x4E00~0x9FFF:CJK统一汉字,涵盖了GB23十二、GB8565和《现代汉语通用字表》的所有汉字,Big5和台湾电报码的所有汉字,还有日本JIS、韩国KSC中的所有汉字。
平面0中的码位0xD800~0xDFFF没有对应任何字符,这个区段称为替换区(surrogate),留给UTF-16编码使用。
虽然ISO/IEC 10464和Unicode对全球字符统一地安排了码位,可是若是直接将码位做为编码,则在机器中使用仍存在一些问题。理论上ISO/IEC 10646预留了64K个平面,这样平面号的编码范围将为0x0000~0xFFFF,加上平面内的行号和列号,字符码位的范围将是0x00000000~0xFFFFFFFF,也就是说,必须用4个字节来存放一个字符的码位。Unicode仅使用编号0x00到0x10的17个平面,其码位范围为0x000000~0x10FFFF,也须要用3个字节来存放。可是咱们实际使用的字符绝大多数都在BMP中,只是偶尔使用其它平面的字符,所以能够设计一种两字节的编码,使得BMP中的字符编码就是它们在BMP中的码位,而其它平面的字符用一种代替的方法来表示,这样的编码就是UTF-16。
RFC2781详细描述了UTF-16的编码算法,这个算法并不复杂:
设当前要编码的字符为ch,其Unicode码位为U,,
1) 若是U<0x10000,即ch是BMP中的字符,则U自己就是ch的UTF-16编码。换句话说,BMP中的字符,其UTF-16编码就是码位。
2) 不然,令。因为,因此必有,于是U2必定能够用20位二进制数来表示。设U2对应的二进制数为yyyyyyyyyyxxxxxxxxxx。
3) 构造两个16位整数W1和W2:W1=110110yyyyyyyyyy,W2=110111xxxxxxxxxx,这两个整数就是字符ch的UTF-16编码,总共4个字节:
110110yy yyyyyyyy 110111xx xxxxxxxx
注意对于BMP以外的字符,对应的W1和W2分别知足和,都落在平面0的替换区内,不会与其它BMP字符的码位重叠。
将UTF-16编码转换为Unicode码位的逆算法也是简单的:
设当前要转换的16位UTF-16编码是W1,
1) 若是W1<0xD800或者W1>0xDFFF,则W1表示的字符落在BMP上,W1自己即为Unicode码位,算法结束。不然,
2) W1和随后的一个16位整数W2构成一个字符的UTF-16编码,检查是否W1落在0xD800~0xDBFF之间,而且W2落在0xDC00~0xDFFF之间,不然编码错误,算法结束。否则,
3) 去掉W1开头的110110,W2开头的110111,余下的20位二进制数再加上0x10000就是码位。
UTF-16编码是双字节编码,这种编码在内存中存放、经过网络传送、或者存储到磁盘文件中时,都只能以字节为单位,这就存在一个字节顺序的问题。对于多字节数据,计算机硬件系统从来就存在两种字节顺序,一种称为“高位在前,低位在后”(big-endian),即最高位的数据字节先存放或传送,而后才是低位的字节。例如,一个两字节的整数0x1234,其字节顺序是先0x12,而后才是0x34。另一种字节顺序是“低位在前,高位在后”(little-endian),最低位的数据字节先存放或传送,而后才是高位的字节。对于0x1234,其字节顺序是0x34在前面,而后才是0x12。IBM PC机的字节顺序是little-endian,学过汇编语言的读者对此应该是很熟悉的。
为便于各类硬件系统的处理,UTF-16编码的字符可使用任何一种字节次序,可是在网络上传送或存储时,必须指明所使用的字节次序。方法是在字节流开头使用一个称为字节序标志(Byte Order Mark, BOM)的代码0xFEFF,对于big-endian次序,开头两个字节必须依次为0xFE和0xFF;对于little-endian次序,则依次为0xFF和0xFE。BOM不只指出字节次序,同时也能够顺便做为UTF-16的编码标志:若是一个文字文件的开头两字节是BOM,则这个文件的文字编码就是UTF-16。
如前所述,ASCII字符在BMP第0行中,它们的UTF-16编码即为其码位。因为行号是0,因此码位两个字节中第一个字节一定是0x00,第二个字节与ASCII相同。例如,字母A的ASCII码是0x41,对应的UTF-16编码为0x0041。字节0x00将会带来麻烦。咱们知道,C语言的字符串是以0x00做为字符串结束标志的,目前大量的应用程序是用C语言编写的,若是将UTF-16编码的字符串交给这些程序处理,则其中出现的0字节就会致使错误。改写全部的应用程序显然是不现实的,可行的办法是采起另外一种编码方法,这就是UTF-8。
UTF-8采起不定长的编码方法,ASCII字符的编码只用一个字节,而且其编码就是原来的ASCII码,其它字符的编码从2个字节到4个字节不等。每一个字符编码的第一字节指出该字符编码的字节数:其中第一个二进制数0的左边有多少个1,该字符编码就有多少个字节;若是第一位就是0,则该字符编码只有一个字节。例如,某个字符编码第一字节是0xxxxxxx(x表示任意的二进制数),则该字符的UTF-8编码只有一个字节。若某个字符编码第一字节为110xxxxx,则该字符的编码将有两个字节。对于两个字节及其以上的UFT-8编码,从第二字节开始,每一个字节开头两个二进制数都是10。
RFC3629描述了详细的编码算法。如表1所示,其中最左边一栏是字符在ISO/IEC 1064六、Unicode编码空间中的码位,用16进制数表示为0xAAAABBCC的形式,其中AAAA是平面号,BB是行号,CC是列号。表中每一行是一个Unicode码位的范围,该范围内码位占用的二进制位数列在第3栏中,只需将字符码位转换为二进制数,位数按第3栏的数目,而后将第2栏中的每一个x用码位中对应的二进制数填上去,就获得对应的UTF-8编码。
例如,字符“A”的码位是0x00000041,落在表1第一行范围内,所以转换为7位二进制数1000001,将这些二进制数依次填到第二栏的每一个x中,就获得其UTF-8编码01000001,这其实就是字符“A”的ASCII码。
又如,汉字“码”的码位是0x00007801,位于第三行范围内,转换为16位二进制数0111100000000001,由第二栏知,对应的UTF-8编码为3个字节:11100111 10100000 10000001。
表1:UTF-8编码
字符码位(16进制) |
UTF-8编码(二进制) |
二进制位数 |
0x00000000 – 0x0000007F |
0xxxxxxx |
7 |
0x00000080 – 0x000007FF |
110xxxxx 10xxxxxx |
11 |
0x00000800 – 0x0000FFFF |
1110xxxx 10xxxxxx 10xxxxxx |
16 |
0x00010000 – 0x0010FFFF |
11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
21 |
注意码位0x0000D800~0x0000DFFF是留给UTF-16使用的替换区,这个范围内的代码不容许转换为UTF-8。若是是将UTF-16转换为UTF-8,则必须还原为Unicode的码位而后再按表1进行转换。
UTF-8将全部的ASCII字符仍编码为ASCII码,而且只有ASCII的空字符的编码是0x00,其它任何字符的任何字节都不是0,这就解决了C字符串的问题,使得这种编码的字符串能够被现有的应用程序使用。
因为CJK统一汉字的码位在0x00003000~0x00009FFF之间,因此每一个汉字的UTF-8编码将是3个字节。
UTF-8的编码逆算法不难实现,按表1反过来计算便可。可是要注意计算结果是否与表1第一栏指定的范围一致,例如,将两个字节0xC0,0x80按表1计算对应的码位,因为这两个字节用二进制表示是11000000 10000000,因而按表1第二行转换为0x0000。这是错误的,由于0x0000并不在第二行的码位范围内。事实上,第二行最小的码位是0x0080,对应的UTF-8编码是0xC二、0x80,所以任何UTF-8编码字节都不多是0xC0或0xC1。经过相似的计算还能够知道,0xF5~0xFF也不会出如今任何合法的UTF-8编码字节中。
不像UTF-16,UTF-8编码是以字节为单位的,因此不存在字节次序的问题。可是能够利用BOM做为编码标识,以便应用程序识别出当前字节流是UTF-8编码。作法相似于UTF-16,在编码字节流的开头放置BOM,但如今不是0xFEFF,而是BOM对应的UTF-8编码,它是三个字节:0xEF, 0xBB, 0xBF。
UTF-32用4个字节(32位二进制数)表示一个字符,直接使用ISO/IEC 10646和Unicode所定义的码位做为编码,没必要进行任何变换。可是,BMP中的替换区0xD800~0xDFFF中的码位不表明任何字符,所以排除在外,另外,UTF-32属于Unicode编码,而Unicode只使用码位不超过0x10FFFF的字符,所以UTF-32的编码不能大于0x10FFFF。
UTF-32一样用BOM指出编码的字节次序,只不过其BOM是4个字节。若“高位在前,低位在后”,则BOM的4个字节依次为0x00、0x00、0xFE和0xFF;若“低位在前,高位在后”,则依次为0xFF、0xFE、0x00和0x00。前者又称为UTF-32BE(BE表示Big-Endian),后者又称为UTF-32LE(LE表示Little-Endian)。
UCS是“全球字符集”(Universal Character Set)的英文缩写,表示ISO/IEC 10646和Unicode所定义的所有字符。
UCS-2和UCS-4相似于UTF-16和UTF-32,是UCS的两种编码方式。
UCS-2是两个字节的编码,只能对BMP中的字符编码,而且其编码就是这些字符在BMP平面内的码位。所以,对于BMP中的字符,UCS-2和UTF-16实际上是同样的,可是对于其它平面的字符,UCS-2没法表示,而UTF-16能够用替换的方式表示。
UCS-4是4个字节编码,其编码就是字符码位。UCS-4与UTF-32是同样的,二者几乎能够认为是同义词,只不过UCS-4没有限制编码不能超过0x10FFFF。
综上所述,UCS-4和UTF-32直接将字符码位做为字符编码,虽然直截了当,可是每一个字符要占用4个字节,浪费存储空间。UTF-16和UCS-2每一个字符仅用两个字节,对于BMP中的字符,二者也是直接将码位做为字符编码。UTF-8采用的是不定长编码,字符编码字节数不固定,编码较为复杂,但UTF-8使得ASCII字符的编码仍为一个字节的ASCII码,这对于网络上的应用是很重要的,由于不少网络协议在处理URI、域名、邮件地址等重要信息时,是只认ASCII字符的。
当咱们只须要说起字符码位,没必要涉及编码时,咱们能够用符号“U+”接上表示码位的十六进制数来表示这个字符,例如,U+7801表示汉字“码”,U+41表示英文字母“A”。
计算机文献中常用16进制数是由于16进制数和二进制数的转换很是方便,其规则是:4位二进制数对应一位16进制数。附表一列出全部4位二进制数对应的16进制数和10进制数。
附表一:
二进制 |
16进制 |
10进制 |
0000 |
0 |
0 |
0001 |
1 |
1 |
0010 |
2 |
2 |
0011 |
3 |
3 |
0100 |
4 |
4 |
0101 |
5 |
5 |
0110 |
6 |
6 |
0111 |
7 |
7 |
1000 |
8 |
8 |
1001 |
9 |
9 |
1010 |
A |
10 |
1011 |
B |
11 |
1100 |
C |
12 |
1101 |
D |
13 |
1110 |
E |
14 |
1111 |
F |
15 |
例如,8位二进制数10110101转换16进制时,由上表可查出1011对应B,0101对应5,所以转换的结果是16进制数B5。
16进制转换为10进制则只需计算加权和。设某个16进制数为,其中诸为0..F之间的某个16进制数,表示0..15的数值,则对应的10进制数为
例如,16进制数AE,转换为10进制数即为。
附表二是常见的2的整数次幂及其对应16进制数和10进制数,在计算编码时常常会用到这些常数。
附表二:
2的n次方 |
10进制 |
16进制 |
n=1 |
2 |
2 |
n=2 |
4 |
4 |
n=3 |
8 |
8 |
n=4 |
16 |
10 |
n=5 |
32 |
20 |
n=6 |
64 |
40 |
n=7 |
128 |
80 |
n=8 |
256 |
100 |
n=9 |
512 |
200 |
n=10 |
1024 |
400 |
n=16 |
65536 |
10000 |
Windows的“记事本”可用来输入文字,并保存为纯文本文件。保存时可选择字符编码,其选择项及含意是:
ANSI:文本中的英文字符用ASCII,汉字用GBK
Unicode: 全部字符都用UTF-16编码,字节顺序为低位在前,自动加BOM
Unicode big endian:全部字符都用UTF-16编码,字节顺序为高位在前,自动加BOM
UTF-8:全部字符都用UTF-8编码,自动加BOM
而后咱们能够利用一些软件检查文件中的编码。例如利用Ultra Edit,这个软件能够查看文件中的二进制内容(用16进制数表示),还能够直接编辑文件中的二进制数。
也能够利用Windows自带的一个DOS程序Debug,如下是操做方法,假定你的文件存放路径及文件名为d:\code\test.txt:
l 启动DOS窗口
l 在DOS窗口中输入cd\
l 在DOS窗口中输入debug
l debug启动后,在光标位置输入n d:\code\test.txt。这个命令的做用是指定文件名,注意这是DOS程序,文件名和目录名不能用汉字,且不能超过8个字符。
l 输入L 3000:0。这个命令的做用是将指定的文件装入内存,3000:0是内存地址。
l 输入D 3000:0。这个命令的做用是显示内存中的数据,你将看到相似以下的数据:
3000:0000 C2 EB B0 A1 00 00 00 00-00 00 00 00 00 00 00 00 ................
3000:0010 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
3000:0020 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
3000:0030 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
3000:0040 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
3000:0050 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
3000:0060 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
3000:0070 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
-
其中左边一栏是内存地址,中间一栏是文件的二进制数据,用16进制表示,每两个16进制数字对应一个字节的数据。右边一栏显示这些数据对应的字符,但只能显示ASCII字符,没法显示的字符用“.”代替。
l 最后,输入q,这个命令将退出程序,返回DOS窗口。
l 输入exit关闭窗口。
注意GBK是两字节的编码,Windows存储GBK编码的字节次序是高位在前、低位在后。UTF-16编码的文件,开头有两字节的BOM可说明字节次序,UTF-8没有字节次序的问题,但文件开头有3个字节的BOM。
debug也能够修改内存数据并存盘。这个程序原本是用来调试汇编程序的,咱们只是利用其部分功能。