Unicode 编码的由来
我们都知道,计算机的内部全部是由二进制数字0, 1 组成的, 那么计算机就没有办法保存我们的文字, 这怎么行呢? 于是美国人就想了一个办法(计算机是由美国人发明的),也把文字转化成数字,计算机不就能够保存文字了,所以美国人就制定了一张表,规定了文字与数字的一一对应,字符A 就对应数字65, 字符B 就对应数字66, 这张表就是著名的ASCII 码表。由于美国人的文字比较少,就是a, b, c d 等等, 对应完了,发现一共使用了128个数字,这也太少了,一个字节都没有使用了,所以就决定用一个字节来表示一个字符, 所以对于ASCII 码来说,一个字符在计算机中就占用一个字节。码表制定好了,生产计算机的时候直接把码表内置到计算机中就可以了。
但是随着计算机的推广,它到达了欧洲,亚洲,这就有点问题了,因为计算机中只有英文,它不可能表达 和书写其他国家的语言,比如汉语,日语等等, 这肯定也是不行的。于是各个国家的政府就制定各自的码表, 好让计算机也能表示本国的语言,就拿我国来说吧,GBK, GB2313 码表就出现了。 这就会出现一个问题,相同的数字在不同的码表中对应的文字可能不同, 这就有可能造成乱码。国际友人发了一封电子邮件过来,打开一看乱码了,各国之间的文件不能够交换使用。这时国际标准化组织就想把各国的字符都统一起来,把它们放到一张码表中,如果计算机中都内置这张表,那就不会出现乱码了。和ASCII码的想法一致,在这张表中,也是给每一个字符都分配一个独一无二的数字。这张表就是Unicode码表或Unicode编码字符集,这每一个字符对应的数字称之为做码点(Code Point). 码表的样子如下
Unicode 字符集在计算机中的实现
Unicode 字符集的基础非常简单,就是给世界上的每一个文字都分配一个独一无二的数字,这个数字称之为码点(code point), 比如 给字符A分配的数字是65, 给字符B 分配的数字是66,那么 A的 码点(code point)就是65, B 的code point 就是66. 但Unicode 相对于其他编码又是非常复杂的,这主要是在于Unicode 字符在计算机中的实现上,这些数字怎么在计算机中表示,用多少个字节?
由于当时提出制定Unicode字符集标准的时候,是在1990s 左右,那时各国的文字都比较少,都对应完了之后,发现并没有超过6万,正好在计算机中,两个字节就可以表示6万多个字,两个字节就是16个位(bit),无符号的话那么最大的数就是16个位都是1,这个数就是 2的16次方即 2^16-1 = 65536-1 = 65535所以就决定用2个字节,16个bit 来表示Unicode 字符, 这就是最早的UCS-2编码, 这16个bit 称之为code uint(码元), 这时 一个码点就对应一个码元。
但是随着时间的推移,尤其是亚洲国家把大量的象形文字也加入到Unicode 编码中,突然发现,65536的数量根本就装不下这些文字,于是Unicode 编码字符集也相应的进行了扩充,它把整个字符集分成了17个平面,每一个平面都能放2^16 个文字, 第一个平面就放置最初的UCS-2编码中所定义的文字,叫做基本多文本平面Basic Multilingual Plane (or BMP), 所以基本多文本平面码点的取值范围是0x0000 - 0xFFFF, 其他的16个平面称为补充平面(Supplementary Planes or Astral Planes), 它们的取值范围从0x10000 开始 (0xFFFF + 1 就等于0x10000, 16进制的计算) 。 针对这种多平面的字符集,出现了不同的在计算机中的实现方式,那就是我们经常听到的UTF-8, UTF-16, UTF-32.
UTF-16 就是对于在BMP 平面的文字用两个字节(16个bit)进行表示,然后对于其他平面的文字用四个字节进行表示,四个字节表示称之为surrogate pairs(代理对), 一对16 bit 来表示一个 字符。 由于一个16bit 称之为码元code unit, 一个字符对应的数字称为码点,这也就意味着,一个码点要有2个码元进行表示。
那怎么才能让一个码点用两个码元来显示呢? 先看一下目前Unicode 非基本面的有多少个字, 一共16个面, 一个面是2^16 个字,那也就是说,一共有16 * 2^16 个字, 就是2^20个字符, 也就需要20个二进制bit 位,一个码元只能16bit 位,那只能把20bit 位进行拆分成2个部分,一个部分占10个bit, 这样一个码元就可以表示了。但这又存在另外一个问题,当计算机去读取字符的时候,它碰到一个字节就去读取一个字节,它并不知道,你这个字节是拆分出来的,你会发现,本来一个字,确显示出了毫不相干的两个字,这怎么办呢?其实在usc-2 的时代,它所定义的Unicode 字符集并没有完全使用了2个字节,2个字节的后面一部分是空的,并没有对应任何字符,这一段是0xD800 到 0xDFFF(55296 - 65535), 这也就意味着,使用这一段上的码元没有任何问题。那我们就想办法,让非基本面的码点对应到这一段上来。对应的逻辑是这样的,首先把0xD800 - 0xDFFF 分为两个部分,0xD800 到 0xDBFF 和0xDC00 到 0xDFFF,然后再用码点减去65536, 把剩下的数用二进制表示,如果二进制不够20位,可以在前面补0. 再把20位二进制分为两个部分,每个部分占用10个bit, 前10个bit 对应到0xD800 - 0xDFFF, 称为高位(H), 后10个bit 对应到0xDC00 到 0xDFFF 称为低位(L), 这样一个码点就可以用两个码元(高位, 低位)进行表示了。映射比较复杂,不过Unicode 提供了一个公式,可以直接通过码点得到高低位码元。
H = Math.floor((c-0x10000) / 0x400)+0xD800 // c 是码点 0x10000 就是65536 0x400 就是1024, 0xD800 就是H 开始位置
L = (c - 0x10000) % 0x400 + 0xDC00 // 0xDC00 L 位开始的位置。
举一个小栗子,汉字"𠮷"的码点为134071, 大于65535, 它要用4个字节(2个码元)进行表示。把码点134071代入到上面的c 中, 由于浏览器的控制台可以直接计算16进制,我们把134071 转化成16进制 0x20BB7
得到十进制55362, 转化成16进制为0xD842, 此为高位
十进制57271,转化成16进制为0xDFB7, 此为低位。
汉字"𠮷"的 UTF-16 编码为0xD842 0xDFB7。
当使用这种方式用两个码元去表示一个码点时,计算机也不会出错,因为当它读取到第一个码元的时候,发现并没有对应的字符,它会就继续向下取第二个码元,两个码元连起来,就是对应的码点,就可以显示一个字了。记住,计算机中只有码元,码点是Unicode 字符集中的概念。
UTF-8也是采用变长字节,不过,它变化幅度有点大,1 , 2, 3, 4 个字节都能用上,它定义了一套规则
UTF-32 则是用32个bit, 4个字节来表示一个字符,所有的字符都占用4个字节, 这肯定没有问题,但也造成了内存空间的浪费, 所以使用不是很广泛。
来源:oschina
链接:https://my.oschina.net/u/4271149/blog/3552495