unicode 只有一种定义.给每个符号都分配一个数字. 然后由不同的具体实现.例如
utf-8 utf-16.... 都是对一种数据结构: byte !!! 字节来编码 规定一种兼容asicc 与现实妥协的方法. 例如utf8 用变长字节,来实现unicode. (伟大的实现) 到现在什么gbk 其他国家的自己编码都基本上绝迹了. 为整个人类节省了劳力.
(0xffffffff 4294967295 肆拾贰亿玖仟肆佰玖拾陆万柒仟贰佰玖拾伍).
目前最高位定为0 则0x7fffffff 2147483647 贰拾壹亿 足够任何字符了.所以每年unicode都会更新. 基本上所有通用符号都在unicode了. 例如 麻将 易经 卦图.
https://fuhaoku.net/block/Mahjong
Emoji字符是Unicode字符集中一部分,特定形象的Emoji表情符号对应到特定的Unicode字节。
clojure 代码(clojure内部调用的是java)
(String. (byte-array [0xF0 0x9F 0x80 0xA1])) => "🀡" ; 麻将九饼的utf-8编码.
(Character/codePointAt "🀡" 0) => 127009 (在unicode中的序号)
大于了0xffffffff 6万5.. 一个char(2个字节)存不下怎么办? java用2个char 4个字节ok了
"\uD83C\uDC21" => "🀡"
(String. (byte-array [0xF0 0x9F 0x80 0xA4])) => "🀤"
(String. (byte-array [0xF0 0x9F 0x82 0xA1])) => "🂡" ; 扑克牌黑桃A
(int \䷁) => 19905
注意: 上面程序中 \ 和 java中的 ' 单引号 字面量写法只支持2个字节的编码
设计JAVA的时候用的是Unicode编码方式,用两个字节的代码宽度=>编码=>世界上所以语言的字符。
char类型是按照Unicode规范实现的一种数据类型,固定16bit大小。现如今,Unicode字符集已经进行了扩展,表示的范围已经超过了16bit。Unicode字符集的数值范围扩大到了[U+0000,U+10FFFF]。
https://www.jianshu.com/p/4c29d96d5e06
十分遗憾的是,现在不可避免的事情发生了,Unicode字符超过了65536个,对此16位的char类型已经不能满足表示所以Unicode字符的需要了。
(int \🀡 => 的时候出错了. 因为🀡的序号是12万多了. 超过了0xffffffff 65536了.是4个字节了..
java 采用了大于6万5的时候 编码到4个字节了. char 字面量 litral 是为了方便书写
超过65536的书写的时候不支持单引号而已了...
大于的时候怎么办, java 解决方案 也是utf-16 . 用2个char 也就是4个字节. ok了
UTF-16的编码方法:
1、如果字符编码U小于0x10000,也就是十进制的0到65535之内,则直接使用两字节表示;
2、如果字符编码U大于0x10000,由于UNICODE编码范围最大为0x10FFFF,从0x10000到0x10FFFF之间 共有0xFFFFF个编码,也就是需要20个bit就可以标示这些编码。用U'表示从0-0xFFFFF之间的值,将其前 10 bit作为高位和16 bit的数值0xD800进行 逻辑or 操作,将后10 bit作为低位和0xDC00做 逻辑or 操作,这样组成的 4个byte就构成了U的编码。
每个字符都分配一个数字编号(编码),
Unicode编码和Unicode实现 这两个是不同的.
例如
"冯" 的数字编号(序号)是 20911 16进制0x51af 然后在计算机中对这个数字 来编码
java中的各种编码 代码实现 sun.nio.cs;
![](https://img.haomeiwen.com/i4260318/9b4ef49d83cda216.png)
sun.nio.cs.UTF_8 extends Unicode 这个类实现了utf8的算法. 根据前一个字节二进制多少个1 确定有几个自己,然后根据utf-8 规范把 codepoint 扣出来. 找到具体的int 或long 数.
通俗实现
https://blog.csdn.net/brk1985/article/details/52126912
先从字符编码讲起。
1、美国人首先对其英文字符进行了编码,也就是最早的ascii码,用一个字节的低7位来表示英文的128个字符,高1位统一为0;
2、后来欧洲人发现尼玛你这128位哪够用,比如我高贵的法国人字母上面的还有注音符,这个怎么区分,得,把高1位编进来吧,这样欧洲普遍使用一个全字节进行编码,最多可表示256位。欧美人就是喜欢直来直去,字符少,编码用得位数少;
3、但是即使位数少,不同国家地区用不同的字符编码,虽然0--127表示的符号是一样的,但是128--255这一段的解释完全乱套了,即使2进制完全一样,表示的字符完全不一样,比如135在法语,希伯来语,俄语编码中完全是不同的符号;
4、更麻烦的是,尼玛这电脑高科技传到中国后,中国人发现我们有10万多个汉字,你们欧美这256字塞牙缝都不够。于是就发明了GB2312这些汉字编码,典型的用2个字节来表示绝大部分的常用汉字,最多可以表示65536个汉字字符,这样就不难理解有些汉字你在新华字典里查得到,但是电脑上如果不处理一下你是显示不出来的了吧。
5、这下各用各的字符集编码,这世界咋统一?俄国人发封email给中国人,两边字符集编码不同,尼玛显示都是乱码啊。为了统一,于是就发明了unicode,将世界上所有的符号都纳入其中,每一个符号都给予一个独一无二的编码,现在unicode可以容纳100多万个符号,每个符号的编码都不一样,这下可统一了,所有语言都可以互通,一个网页页面里可以同时显示各国文字。
6、然而,unicode虽然统一了全世界字符的二进制编码,但没有规定如何存储啊,亲。x86和amd体系结构的电脑小端序和大端序都分不清,别提计算机如何识别到底是unicode还是acsii了。如果Unicode统一规定,每个符号用三个或四个字节表示,那么每个英文字母前都必然有二到三个字节是0,文本文件的大小会因此大出二三倍,这对于存储来说是极大的浪费。这样导致一个后果:出现了Unicode的多种存储方式。
7、互联网的兴起,网页上要显示各种字符,必须统一啊,亲。utf-8就是Unicode最重要的实现方式之一。另外还有utf-16、utf-32等。UTF-8不是固定字长编码的,而是一种变长的编码方式。它可以使用1~4个字节表示一个符号,根据不同的符号而变化字节长度。这是种比较巧妙的设计,如果一个字节的第一位是0,则这个字节单独就是一个字符;如果第一位是1,则连续有多少个1,就表示当前字符占用多少个字节。
8、注意unicode的字符编码和utf-8的存储编码表示是不同的,例如"严"字的Unicode码是4E25,UTF-8编码是E4B8A5,这个7里面解释了的,UTF-8编码不仅考虑了编码,还考虑了存储,E4B8A5是在存储识别编码的基础上塞进了4E25
9、UTF-8 使用一至四个字节为每个字符编码。128 个 ASCII 字符(Unicode 范围由 U+0000 至 U+007F)只需一个字节,带有变音符号的拉丁文、希腊文、西里尔字母、亚美尼亚语、希伯来文、阿拉伯文、叙利亚文及马尔代夫语(Unicode 范围由 U+0080 至 U+07FF)需要二个字节,其他基本多文种平面(BMP)中的字符(CJK属于此类-Qieqie注)使用三个字节,其他 Unicode 辅助平面的字符使用四字节编码。
10、最后,要回答你的问题,常规来看,中文汉字在utf-8中到底占几个字节,一般是3个字节,最常见的编码方式是1110xxxx 10xxxxxx 10xxxxxx。
UTF-8 编解码实现
https://taoshu.in/c-utf-8.html
我们在前文UTF-8往事中提到,Ken 和 Rob 用一个晚上就实现了 UTF-8 编解码的算法。代码非常精炼,很值得一读,分享给大家。
在开始之前,我们先简单回顾一下 UTF-8 的编码规则。先看编码表:
UTF-8 编码回顾
这是 UTF-8 编码规则表:
Bytes Bits Hex Min Hex Max Byte Sequence in Binary
1 7 00000000 0000007f 0vvvvvvv
2 11 00000080 000007FF 110vvvvv 10vvvvvv
3 16 00000800 0000FFFF 1110vvvv 10vvvvvv 10vvvvvv
4 21 00010000 001FFFFF 11110vvv 10vvvvvv 10vvvvvv 10vvvvvv
5 26 00200000 03FFFFFF 111110vv 10vvvvvv 10vvvvvv 10vvvvvv 10vvvvvv
6 31 04000000 7FFFFFFF 1111110v 10vvvvvv 10vvvvvv 10vvvvvv 10vvvvvv 10vvvvvv
第一列表示编码所需字节数,第二列为能表示的 Unicode 的最大二进制位数,第三列和第四列为能表示的 Unicode 范围,最后一列表示编码后的字节布局。把编码中字母 v 表示的部分连接起来就是对应的 Uniocde 编码。
还是举一下前文的例子。汉字「吕」的 Unicode 编码是 U+5415
,对应二进制为 0101010000010101
,总共有 15 位。因为两字节最多表示 11 位,三字节最多表示 16 位,所以使用三字节编码。对应二进制拆成(从低位到高位)三部分,分别是 0101
, 010000
, 010101
,再拼上编码前缀得到 11100101
, 10010000
, 10010101
,对应十六进制为 0xe5
, 0x90
, 0x95
,这就是汉字「吕」的 UTF-8 编码。
现在我们开始读 c 代码。理解代码的关键是理解数据结构。
数据结构
Ken 引入一个 Tab
列表。前面说的编码规则表一共有六条,每条一个 Tab
。每个 Tab
包含五个字段。其中 lmask
和 lval
最简单,对应所能表示的 Unicode 的最大值和最小值。Tab
所在的行号代表表示该范围 Unicode 所需要的字节数。剩下的 cmask
, cval
和 shift
则没那么容易理解了。
还是使用汉字「吕+U5415」讲解。它的 UTF-8 编码是 11100101
, 10010000
, 10010101
。解码的时候,我们先读到第一个字节 11100101
,这时算法需要判后面还有几个字节。判断的依据则是当前字节从高位开始连续 1
的个数。对于 11100101
而言,显然其高位连续 1
的数量为 3,但这个事情对于计算机就没有那么「显然」了。Ken 引入两个辅助数字 cmask = 11110000
和 cval = 11100000
,让计算机判断 11100101&cmask == cval
是否相等。如果相等,则证明该字节的高四位一定是 1110
。
cmask
和 cval
取值规则非常简单。将 UTF-8 首字节的 vvv
部分置 0
就得到 cval
。将 cval
最高位的 0
置 1
就得到了对应的 cmask
。以四字节编码为例,首字节为 11110vvv
,vvv
置 0
,得到对应的 cval
为 11110000
,也就是 0xf0
;将 11110000
的第四位置 1
,得到对应了 cmask
为 111110000
,也就是 0xf8
。其他的依此类推。
shift
是最不容易理解的。我们放在下面分析 UTF-8 编码算法的时候再讲。
数据结构代码如下:
typedef
struct
{
int cmask;
int cval;
int shift;
long lmask;
long lval;
} Tab;
static
Tab tab[] =
{
0x80, 0x00, 0*6, 0x7F, 0, /* 1 byte sequence */
0xE0, 0xC0, 1*6, 0x7FF, 0x80, /* 2 byte sequence */
0xF0, 0xE0, 2*6, 0xFFFF, 0x800, /* 3 byte sequence */
0xF8, 0xF0, 3*6, 0x1FFFFF, 0x10000, /* 4 byte sequence */
0xFC, 0xF8, 4*6, 0x3FFFFFF, 0x200000, /* 5 byte sequence */
0xFE, 0xFC, 5*6, 0x7FFFFFFF, 0x4000000, /* 6 byte sequence */
0, /* end of table */
};
弄清楚了数据结构,我们开始看算法。
解码算法
解码算法就是将 UTF-8 字节序列转化成 Unicode。代码使用 wchar_t
表示 Unicode。在我的 Mac 上一个 wchar_t
占四个字节。代码请看注释。依然使用「吕+U5415」的 UTF-8 编码 11100101
, 10010000
, 10010101
进行讲解。
/* s 指向 UTF-8 字节序列,n 表示字节长度 */
/* p 指向一个 wchar_t 变量 */
/* mbtowc 对 s 进行解码,得到的 Unicode 存到 p 指向的变量 */
int
mbtowc(wchar_t *p, char *s, size_t n)
{
long l;
int c0, c, nc;
Tab *t;
if(s == 0)
return 0;
nc = 0;
if(n <= nc)
return -1;
/* c0 保存第一个字节内容,后面会移动 s 指针,此处备份一下 */
/* 汉字「吕」的编码是 `11100101`, `10010000`, `10010101` */
/* 此时 l = c0 = 11100101 */
c0 = *s & 0xff;
/* l 保存 Unicode 结果 */
l = c0;
/* 根据 UTF-8 的表示范围从小到大依次检查 */
for(t=tab; t->cmask; t++) {
/* nc 以理解为 tab 的行号 */
/* tab 行号跟这个范围内 UTF-8 编码所需字节数量相同 */
nc++;
/* c0 指向第一个字节,不会变化 */
/* l 在 n == 1 和 n == 2 时左移 6 位两次 */
/* 到 nc == 3 时才会进入该分支 */
/* 此时的 l 已经是 11100101+010000+010101 了 */
if((c0 & t->cmask) == t->cval) {
/* lmaxk 表示三字节能表示的 Unicode 最大值 */
/* 使用 & 操作,移除最高位的 111 */
/* 所以 l 最终为 00000101+010000+010101 */
/* 也就是 l = 0x5415,对应 Unicode +U5415 */
l &= t->lmask;
if(l < t->lval)
return -1;
/* 保存结果并反回 */
*p = l;
return nc;
}
if(n <= nc)
return -1;
/* s 指向下一个字节 */
s++;
/* 0x80 = 10000000 */
/* UTF-8 编码从第二个字节开始高两位都是 10 */
/* 这一步是为了把最高位的 1 去掉 */
c = (*s ^ 0x80) & 0xFF;
/* n == 1 时 */
/* c = 00010000 */
/* n == 2 时 */
/* c = 00010101 */
/* 0xc0 = 1100000 */
/* 这一上检查次高位是否为 1,如果是 1,则为非法 UTF-8 序列 */
if(c & 0xC0)
return -1;
/* c 只有低 6 位有效 */
/* 根据 UTF-8 规则,l 左移 6 位,将 c 的低 6 位填入 l */
l = (l<<6) | c;
/* n == 1 时 */
/* l 的值变成 11100101+010000 */
/* n == 2 时 */
/* l 的值变成 11100101+010000+010101 */
}
return -1;
}
解码算法讲完了。继续说编码算法。
编码算法
编码算法就是把 Unicode 转换成 UTF-8 字节序列。还是以汉字「吕+U5415」为例。
/* wc 存有一个 Unicode */
/* s 指向存放 UTF-8 的内存 */
/* wctomb 返回最终 UTF-8 编码的字节长度 */
int
wctomb(char *s, wchar_t wc)
{
long l;
int c, nc;
Tab *t;
if(s == 0)
return 0;
/* l = wc = 101010000010101 */
l = wc;
nc = 0;
/* tab 每一行表示的 Unicode 的范围是递增的 */
for(t=tab; t->cmask; t++) {
nc++; /* 记录行数,也就是字节数 */
/* 找到第一个可以表示的 Tab */
/* 对于「吕+U5412」nc == 3 */
if(l <= t->lmask) {
/* c->shift = 2 * 6 */
/* c->cval = 11100000 */
c = t->shift;
/* UTF-8 共需要 3 个字节 */
/* 第 2 个和第 3 个字节各有 6 个有效位 */
/* 所以将 l 右移 2 * 6 位得到的结果需要存到第一个字节 */
/* 首字节高 4 位还要存储后续字节长度标识 */
*s = t->cval | (l>>c);
/* 处理剩余字节 */
while(c > 0) {
c -= 6;
/* s 依次指向下一个字节 */
s++;
/* 0x3f = 00111111 */
/* 0x80 == 10000000 */
/* c == 0 时,取 l 的低 6 位并将其高两位置成 10 */
/* 此时 s 指向的位置存有 10010101 */
/* c == 1 时,取 l 的次低 6 位并将其高两位置成 10 */
/* 此时 s 指向的位置存有 10010000 */
*s = 0x80 | ((l>>c) & 0x3F);
}
/* 最终 s 最初指向的区域存有 11100000, 10010101, 10010000 */
return nc;
}
}
return -1;
}
网友评论