软件开发中的字符编码问题的思考
字符编码问题从上层到底层是个非常大的系统工程,但是从普通程序员的角度,只需要了解其核心和主干的知识。
TL;DR
字符与编码
计算机的世界是0和1构成的, 所有的字符如'A','0','道'最终都将表示为0-1的序列来进行处理、存储和传输。
例如"Hello World"使用ASCII 编码将会表示为
01001000 - H
01100101 - e
01101100 - l
01101100 - l
01101111 - o
00100000 - ' '
01010111 - W
01101111 - o
01110010 - r
01101100 - l
01100100 - d
这个从字符到到二进制串(比特流)的过程就是编码(encoding), 反之则是解码(decoding)。
在实际中,情况要复杂很多,因为ASCII 只有7位参与的编码(第一位是0),最多只能表示128个字符,这在现实中显然不够,光汉字就有6000多个了,更何况还有其他语言也有大量的字符。也因此各个组织和国家制定了许多编码标准,例如我国的GB2312, GBK等等。 这些标准相互不完全兼容,带来很多问题,常见的就是乱码问题,例如GBK编码的文件用UTF-8打开就是一团乱码。为了国际上的沟通便利,也为了降低程序员的开发维护成本,Unicode 出现了,它最开始是个字符集(charset),把人类现在使用的符号都纳入其中,并且规定了唯一字符编号(称为码点,code point)。Unicode 现在已经是国际通行的标准了。
Unicode只定义了字符集和唯一字符编号,但是没有定义编码方式(即如何用二进制表示)。这个又涉及了很多考虑因素,例如单byte最多只能编码256个字符,定长的n字节可以编码(2^(8n))个字符,但是这会带来不必要浪费,在处理、存储、通信的各个环节都会增大开销。因此比较好的编码方案是平均码长比较小的方案,通常都是变长(长度变化, variable-length)的:
UTF-32 使用定长的 32 bits来编码,每个字符有四个字节,这非常简单但是对空间是巨大的浪费
UTF-16 和 UTF-8 是变长编码,UTF-8 会使用1-4个字节进行编码,是互联网上应用最广泛的unicode 编码方式。UTF-8 还对ASCII编码是二进制兼容的。
关于Unicode 和UTF-8的关系,的回答非常精妙,如果学过通信的应该很容易get:
用通信理论的思路可以理解为:unicode是信源编码,对字符集数字化;utf8是信道编码,为更好的存储和传输。
小结: unicode 提供了一个统一的字符集和字符代码(码点),解决了字符集不统一的问题,提供了跨语言跨平台的方案; utf8 提供了压缩的编码; UTF-8 编码对Unicode 进行了进一步的编码,有效地节省了传输的带宽和存储空间。
UTF-8
UTF-8 的编码规则二条:
@Note: UTF-8通过第一个字节有多少个1指定了该字符有多少个字节,从而实现了变长
@Note:在UTF-8,中日韩字符(Unicode 0x4E00 - 0x9FFF)是三个字节的。少数罕见字符可能例外。
值得一提的是,在具体的二进制表示中还有的问题,也就是通常说的大端小端的问题(little-endian & big-endian)。Unicode 规范定义,每一个文件的最前面分别加入一个表示编码顺序的字符,这个字符的名字叫做"零宽度非换行空格"(zero width no-break space),也称为BOM(Byte Order Mark),用表示。这正好是两个字节,而且FF比FE大1。如果一个文本文件的头两个字节是,就表示该文件采用大头方式;如果头两个字节是,就表示该文件采用小头方式。
最佳实践
在实际开发过程中,至少面对着三种字符编码问题:
对于1,外部IO,通用性第一,因此 UTF-8是最好的选择。UTF-8被浏览器和编辑器广泛支持。UTF-8用来作为持久化和交换的编码是更优的选择。 btw,UTF-8与ASCII 完全兼容,也是个优点,可以节省空间。这也适用于前后端交换数据的情况。
对于2,程序的内部处理,这种情况比较复杂了。和具体的业务约束有关系,也和所用的平台、语言和框架有关系。Unix 世界中默认的系统编码是UTF-8, 而Windows默认UTF-16, 其历史遗留问题给开发者带来了很多误解。就语言来说,像python和java原生支持unicode, 就比较舒服。而对于C/C++,要稍微痛苦一些,char只代表一个字节的内容,std::string可以理解为字节的容器,并没有编码方式的属性,字节实际被解析为什么字符还是取决于系统的编解码方式。在这种情况下,直接用UTF-8可能在中英文混用的时候无法得到正确的字符串长度。因为有的编码是多字节,甚至是变长字节的。如果使用std::string存储多字节编码的字符串,则 和 分割方法可能都无法如预期工作。例如:
#include
#include
using namespace std;
int main() {
const string love_cpp = u8"你好C++";
cout << love_cpp.length() << endl;// 9
}
因为在UTF-8 编码中"你好 "占用3*2个字节, 因此 占用9个字节,所以string.lenght()的结果是9。
Note: 程序中处理的不是字符,是字节流。 从现在的视角来看,char类不再表示字符类型,表示"字节"更加恰当。
C++中可以使用第三方库提供的带有编码信息的字符串类。例如Qt实现了自己的QString 方法,内部用存储的是unicode,并且提供了一些编码转换的工具类。在Qt的程序中,内部统一用QString就不会遇到编码问题。第三方库 提供了跨平台的Unicode和全球化的支持。
更是主张在程序内部也使用utf-8的编码,并且在其网站首页列了长文说明这种理由。
对于3, 如果内外部不一致的话,一定要做好编码方式的转换,C++11 以后提供了
可以进行字符串编码的转换。