由于工作环境经常在各种平台,各种语言和区域环境之间切换,乱码一直是一个讨厌的问题,乱码问题大多源于编码。所以想再这里把编码问题弄清楚,也分享一下学习心得。

从计算机对多国语言的支持角度看,大致可以分为三个阶段:

  • ASCII
  • ANSI
  • UNICODE

编码

  1. ASCII编码 ASCII码一共规定了128个字符的编码,比如空格"SPACE"是32(二进制00100000),大写的字母A是65(二进制01000001)。这128个符号(包括32个不能打印出来的控制符号),只占用了一个字节的后面7位,最前面的1位统一规定为0。 英语用128个符号编码就够了,但是用来表示其他语言,128个符号是不够的。一些欧洲国家就决定,利用字节中闲置的最高位编入新的符号。但是不同的国家有不同的字母,编码字符集是不一样的,比如,130在法语编码中代表了é,在希伯来语编码中却代表了字母Gimel (ג)。

  2. ANSI编码 为使计算机支持更多语言,使用2个字节来表示1个字符。这样不同的国家和地区制定了不同的标准,由此产生了GB2312, BIG5, JIS 等各自的编码标准。这些使用 2 个字节来代表一个字符的各种汉字延伸编码方式,称为ANSI编码。在简体中文系统下,ANSI 编码代表 GB2312 编码,在日文操作系统下,ANSI 编码代表 JIS 编码。

  3. UNICODE 不同 ANSI 编码之间互不兼容,当信息在国际间交流时,无法将属于两种语言的文字,存储在同一段 ANSI 编码的文本中。为了使国际间信息交流更加方便,国际组织制定了UNICODE 字符集,为各种语言中的每一个字符设定了统一并且唯一的数字编号,以满足跨语言、跨平台进行文本转换、处理的要求。Unicode当然是一个很大的集合,现在的规模可以容纳100多万个符号,每个符号的编码都不一样。

编码与存储

说到编码,很容易混淆UnicodeUTF-8,我就很长时间没有分清楚。事实上,Unicode只是一个符号集,它只规定了符号的二进制编码,却没有规定这个二进制编码应该如何存储。而UTF-8是最常用的一种Unicode的存储方式,其他实现方式还包括UTF-16(字符用两个字节或四个字节表示)和UTF-32(字符用四个字节表示)。UTF-8编码是一种变长编码,可以使用1~4个字节表示一个符号。 UTF-8编码的存储规则很简单,只有二条:

  1. 对于单字节的符号,字节的第一位设为0,后面7位为这个符号的Unicode码。对于ASCII字符,UTF-8编码和ASCII码是相同的。
  2. 对于n字节的符号(n>1),第一个字节的前n位都设为1,第n+1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的unicode码。
Unicode符号范围 UTF-8编码方式(十六进制) (二进制)
0000 0000-0000 007F 0xxxxxxx
0000 0080-0000 07FF 110xxxxx 10xxxxxx
0000 0800-0000 FFFF 1110xxxx 10xxxxxx 10xxxxxx
0001 0000-0010 FFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

UTF-8编码非常简单,如果一个字节的第一位是0,则这个字节单独就是一个字符;如果第一位是1,则连续有多少个1,就表示当前字符占用多少个字节。

文件编码

Windows里文本保存编码一般有四个选项:ANSI,Unicode,Unicode big endian 和 UTF-8。

  1. ANSI是默认的编码方式。对于英文文件是ASCII编码,对于简体中文文件是gb2312编码(只针对Windows简体中文版,如果是繁体中文版会采用Big5码)。
  2. Unicode编码指的是UCS-2编码方式,即直接用两个字节存入字符的Unicode码。这个选项用的little endian格式。
  3. Unicode big endian编码与上一个选项相对应。
  4. UTF-8编码,也就是上一节谈到的编码方法。

Unicode规范中定义,每一个文件的最前面分别加入一个表示编码顺序的字符,这个字符的名字叫做"零宽度非换行空格”(ZERO WIDTH NO-BREAK SPACE),用FEFF表示。这正好是两个字节,而且FF比FE大1。如果一个文本文件的头两个字节是FE FF,就表示该文件采用大头方式;如果头两个字节是FF FE,就表示该文件采用小头方式。

跨平台工作会经常遇到文件编码转换的问题,Windows中默认的文件格式是GBK(gb2312),而Linux一般都是UTF-8。

  • 查看文件编码
  1. 在Vim 中:set fileencoding可以直接查看文件编码。 另外在.vimrc中加上set encoding=utf-8 fileencodings=ucs-bom,utf-8,cp936,可以让vim自动识别文件编码,其实就是依照fileencodings提供的编码列表尝试,如果没有找到合适的编码,就用latin-1(ASCII)编码打开。

  2. enca 查看文件编码

1
2
3
    $ enca filename
    filename: Universal transformation format 8 bits; UTF-8
    CRLF line terminators
  • 文件编码转换
  1. 在Vim中直接进行转换文件编码:set fileencoding=utf-8

  2. enconv 转换文件编码,比如要将一个GBK编码的文件转换成UTF-8编码:enconv -L zh_CN -x UTF-8 filename

  3. iconv 转换:iconv -f encoding -t encoding inputfile 比如将一个UTF-8 编码的文件转换成GBK编码:

1
    iconv -f GBK -t UTF-8 file1 -o file2

C/C++字符串常量

在C++代码中,给一个string类型的变量赋值一个中文字符串常量,例如:string s = "中文字符串"。变量s中保存的字节内容是什么?如果源文件的编码格式转换了,比如从GB2312转换为UTF-8,变量s中的内容会发生变化吗?其结果是否与编译器有关?

VC++会试图将Unicode的编码格式转换成对应地区(Locale)的缺省编码(简体中文系统下,为GB2312即代码页CP936),并按照这个编码的内容来确定常量字符串的值。G++不会试图转换常量的字符串编码,会直接使用与源文件字符编码对应的字符串常量。

在VC++中,如果源程序的编码与当前默认ANSI编码不符,则需要使用#pragma setlocale告诉编译器源程序使用的编码:

1
2
    // 如果源程序的编码与当前默认 ANSI 编码不一致,则需要此行,编译时用来指明当前源程序使用的编码
    #pragma setlocale(".936")