谈一谈字符编码的事
字符编码是程序员最头疼的问题,有一句话可以形容字符编码的重要性。“大家都统一用UTF,不然最后怎么死的都不知道”。因为文字是用户日常交流的基础,而字符编码是文字在计算机系统中的表示,如果在一个系统中字符编码都没有确定,那么最后很容易出现乱码问题。对于乱码问题,当涉及到的模块增多,系统间的交互增多,解决起来就变得异常困难。字符编码无小事,不要认为你写的程序很小,不涉及到多个模块间的交互,就可以避免字符编码问题。这个想法是完全的错误。编码问题非常基础,如一个字符串拷贝函数就会涉及,估计没有人会不用吧。那么当你使用字符处理函数时,有考虑过内存里面的0101到底采用是什么编码吗?经过操作的字符串最后还能否会被转化为可读的文字。下面我们谈一谈字符编码的问题。
1. 什么是字符编码
字符编码,指对人们使用的文字中的每一个符号,用二进制0和1组成的串进行表示。不同的符号对应的01串不同,每个符号对应的01串就是该符号的编码。
2. 字符编码中的常见概念
A. ASCII码
ASCII(American Standard Code for Information Interchange,美国信息互换标准代码,ASCⅡ)是基于拉丁字母的一套电脑编码。它主要用于显示现代英语和其他西欧语言,它采用单字节编码。
ASCII共定义了128个字符,其中33个字符无法显示,在33个字符之外的是95个可显示的字符,包含用键盘敲下空白键所产生的空白字符也是1个可显示字符(显示为空白)。可以在这个网站查询到ASCII的对照表http://ascii.911cha.com/。
补充:ASCII第一次以规范标准的形态发表是在1967年,最后一次更新则是在1986年,至今为止共定义了128个字符,其中33个字符无法显示(这是以现今操作系统为依据,但在DOS模式下可显示出一些诸如笑脸、扑克牌花式等8-bit符号),且这33个字符多数都是已废的控制字符,控制字符的用途主要是用来操控已经处理过的文字。
B. GB2312
ASCII码是单字节编码方式,对于汉字有10万多个字符的语言,单字节编码仅能表示256个字符。
GB 2312或GB 2312-80是一个简体中文字符集的中国国家标准,全称为《信息交换用汉字编码字符集·基本集》,又称为GB0,由中国国家标准总局发布,1981年5月1日实施。GB2312编码通行于中国大陆。
具体的编码规则就不研究了,开发时不用去了解具体的编码规则。我们只要知道每个汉字及符号以两个字节来表示。同时,GB 2312标准共收录6763个汉字,其中一级汉字3755个,二级汉字3008个,可见生僻字不能显示。
补充:GB 2312还收录了包括拉丁字母、希腊字母、日文平假名及片假名字母、俄语西里尔字母在内的682个全角字符。GB 2312的出现,基本满足了汉字的计算机处理需要,它所收录的汉字已经覆盖中国大陆99.75%的使用频率。新加坡等地也采用此编码。
C. ANSI编码
因为ASCII码的局限性,不同的国家和地区制定了不同的扩展标准,由此产生了 GB2312, BIG5, JIS 等各自的编码标准。在简体中文系统下,ANSI 编码代表 GB2312 编码,在日文操作系统下,ANSI 编码代表 JIS 编码。 不同 ANSI 编码之间互不兼容,当信息在国际间交流时,无法将属于两种语言的文字,存储在同一段 ANSI 编码的文本中。
D. Unicode编码
Unicode也是一种字符编码方法,不过它是由国际组织设计,可以容纳全世界所有语言文字的编码方案。Unicode的学名是"Universal Multiple-Octet Coded Character Set",简称为UCS。UCS可以看作是"Unicode Character Set"的缩写。
UCS有两种格式:UCS-2和UCS-4。顾名思义,UCS-2就是用两个字节编码,UCS-4就是用4个字节(实际上只用了31位,最高位必须为0)编码。
UCS规定了怎么用多个字节表示各种文字。怎样存储、传输这些编码,是由UTF(UCS Transformation Format)规范规定的,常见的UTF规范包括UTF-8、UTF-7、UTF-16。
E. UTF编码
UTF-8就是以8位为单元对UCS进行编码,它是一种变长的编码方式。它可以使用1-4个字节表示一个符号,根据不同的符号而变化字节长度。从UCS-2到UTF-8的编码方式如下:
例如“汉”字的Unicode编码是6C49。6C49在0800-FFFF之间,所以肯定要用3字节模板了:1110xxxx 10xxxxxx 10xxxxxx。将6C49写成二进制是:0110 110001 001001, 用这个比特流依次代替模板中的x,得到:11100110 10110001 10001001,即E6 B1 89。
UTF-16以16位为单元对UCS进行编码。对于小于0x10000的UCS码,UTF-16编码就等于UCS码对应的16位无符号整数。对于不小于0x10000的UCS码,定义了一个算法。不过由于实际使用的UCS2,或者UCS4的BMP必然小于0x10000,所以就目前而言,可以认为UTF-16和UCS-2基本相同。UCS-2只是一个编码方案,UTF-16却要用于实际的传输,所以就不得不考虑字节序的问题。
F. 字节序(Little endian和Big endian)和BOM
UTF-8以字节为编码单元,没有字节序的问题。UTF-16以两个字节为编码单元。
UTF-16使用的两个字节存在一个顺序的问题。比如“奎”的Unicode编码是594E,“乙”的Unicode编码是4E59。那么当我们收到UTF-16字节流“594E”,那么这是“奎”还是“乙”?
字节序就是规定UTF-16所用两个字节的顺序。以汉字”严“为例,Unicode码是4E25,需要用两个字节存储,一个字节是4E,另一个字节是25。存储的时候,4E在前,25在后,就是Big endian方式;25在前,4E在后,就是Little endian方式。
在UCS编码中有一个叫做"ZERO WIDTH NO-BREAK SPACE"的字符,它的编码是FEFF。而FEFF在UCS中是不存在的字符,所以不应该出现在实际传输中。UCS规范建议我们在传输字节流前,先传输字符"ZERO WIDTH NO-BREAK SPACE"。
这样如果接收者收到FEFF,就表明这个字节流是Big-Endian的;如果收到FFFE,就表明这个字节流是Little-Endian的。因此字符"ZERO WIDTH NO-BREAK SPACE"又被称作BOM。
UTF-8不需要BOM来表明字节顺序,但可以用BOM来区分编码方式。字符"ZERO WIDTH NO-BREAK SPACE"的UTF-8编码是EF BB BF(可用前面介绍的编码方法计算)。所以如果接收者收到以EF BB BF开头的字节流,就知道这是UTF-8编码了。
Windows就是使用BOM来标记文本文件的编码方式的。Windows中的Unicode字符一般指UCS2的UTF16-LE编码。
3. 编码间如何转化
A. ANSI编码和UCS-2 LE编码
我们以汉字“你好”为例进行说明,先看一下“你好”的各种编码:
汉字 | ANSI | UTF-8 | UCS-2 BE | UCS-2 LE |
你好 | C4 E3 BA C3 | EF BB BF (BOM区分编码) E4 BD A0 E5 A5 BD | FE FF (BOM表示字节序) 4F 60 59 7D | FF FE (BOM表示字节序) 60 4F 7D 59 |
输出编码程序:
1) ANSI编码
char* pchar = "你好";
int len = strlen(pchar);
for ( int i=0; i<len; i++ )
{
char each = pchar[i];
printf("Hex: %x\r\n",each );
}
printf("===========================\r\n");
2) UCS-2 LE
wchar_t* pwchar = L"你好";
int wlen = wcslen( pwchar );
int bytelen = wlen * sizeof( wchar_t );
char* point = (char*) pwchar;//强制转化为char*,按照一个字节一个字节的方式输出内存中的内容
for ( int i=0; i<bytelen; i++ )
{
char each = point[i];
printf("Hex: %x\r\n",each );
}
printf("===========================\r\n");
B. ANSI与UCS-2 LE之间的转换
1) Ansi转化为Unicode
MultiByteToWideChar返回的是字符数,需要乘以sizeof(wchar_t)转化为内存大小。返回的字符数包含了字符串的结尾字符’\0’需要的一个字符。
所以输出结果:
2) Unicode转化为Ansi
WideCharToMultiByte返回的是需要的字节数,可以直接分配内存,返回的字节数包含目标ANSI以’\0’为结尾的字节数。
所以输出结果:
C. UTF-8与UCS-2 LE之间的转换
1) Unicode转化为UTF-8
WideCharToMultiByte返回的是需要的字节数,可以直接分配内存,返回的字节数包含目标UTF-8以’\0’为结尾的字节数。
所以输出结果:
UTF-8’\0’以单字节表示
4. 不同字符编码下字符串如何结尾?
字符串都以\0结束,但是不同编码存储\0所用的字节数不同。通过上面的实验可以看到,ANSI和UTF-8采用单字节存储\0,Unicode采用双字节存储\0。
5. URLENCODE是字符编码吗?
URLENCODE与前面的ANSI,Unicode,编码不同。URLENCODE是对字符经过ANSI、Unicode编码后的结果,再次转化便于URL传输。
比如:
“中文” -> GB2312编码为D6D0CEC4,经过Encode -> %D6%D0%CE%C4
“中文”-> UTF-8编码为E4B8ADE69687,经过Encode -> %E4%B8%AD%E6%96%87