字符集与编码

Last Updated on 2022年9月29日

近义名词

  • 多字节字符(Multibyte char) ~ 变长编码, 一个字符可能由多个字节(字节数不定)表示, 因此每个需要按一定规则添加额外的信息,以分割字符.
  • 宽字节字符(Wide char) ~ 定长编码, 使用定长的字节表示字符,因此字符之间没有额外的分割信息.

字符绘制基础

  • 假设有函数drawChar(charSet,code_point), drawChar将根据(charSet,code_point)两个参数在字体数据中定位一个字符的几何信息(称为字形),将其在屏幕上画出来.

"Char Set"与"Code Point"

  • charSet 对应的概念为"字符集",顾名思义,是字符的集合,从数学角度看,它是一个有序集(每个元素都有确定的位置)
  • code_point 对应的概念为"代码点",是字符集内每个字符的唯一ID标记
    • 大部分字符集都保证对ASCII字符集的兼容.即对大部分字符集,Code Point为0-127对应的字符都是一样的.
  • Unicode字符集目前是如同基石一般的存在,它是一个不断发展的字符集,目前包含了大约12万个字符,并且定期更新其标准,且保证向前兼容.
    • 习惯上使用U+FFFFF这样的形式表示某字符在Unicode中的代码点
  • GB系列字符集
    • gb2312是中国强制推广的第一套字符集.
    • gbk是微软兼容gb2312设计的字符集.
    • gb18030兼容gbk,是中国强制推广的第二套字符集.
  • 字体文件不止存储字形, 还要为支持的字符集准备好代码点数据,使得字形和code point能映射起来. 例如,字体如果同时支持GBK和Unicode,那就必须额外提供对应的两组代码点数据.
  • What you see may not be what you write.
      1. 部分字体可能会在显示阶段做一些融合性hack,例如将'!='显示成一个不等号'≠',包含两个字符的一个字符串,显示出来却只有一个字符,这种case一般可以通过换字体或修改设置来解决.
      1. 部分看起来一模一样的字符可能是不同的字符,例如电阻的单位欧姆符号'Ω',和希腊字母 omega 'Ω', 其实是两个字符. 这种case可以通过类似python3的unicodedata.normalize(my_str)来进行规范化,处理后将仅剩omega
      1. 部分文字自身支持字符的融合,例如将'cafe\N{COMBINING ACUTE ACCENT}'显示成'café'. 这种case也可以用unicodedata.normalize(my_str),因为融合后的字符一般在unicode中都有独立的代码点.
    • 正因于此,在进行字符处理,尤其是涉及比较/排序时,使用一些与显示无关的特殊算法往往是有益的.

编码

  • 由于种种原因,我们在读/写字符串时,有时候并不是直接连续存储Code Point,而是设计一种可逆算法data=encode(code_point) , code_point=decode(data),把代码点转换一下进行存储/读取.
    • 定长方案的编码方案中,存储字符的data所占的字节数是确定的,在连续存储时,字符之间的界限是清晰的. 使用定长方案时, 编码/解码算法一般什么也不干,存储的就是code_point的值
    • 变长编码方案中,存储字符的data所占的字节数是不确定的,为了支持连续存储,必须通过编码算法对数据打上标记,以实现字符分割.
  • 对于开发者,实际开发中几乎不会去接触编/解码系统(以及字符绘制系统),只是对字符进行读/写,所以我们总是在处理编码值,而非Code Point值.
  • 在C++中,char[]型容器用于存储最短为1字节的编码(ASCII/UTF-8等),w_char[]用于存储长度最短为2字节的编码(UCS-2/UTF-16等).
    • char[]w_char[]仅仅是数据容器,并不能确定是定长/变长编码.
    • UTF-32/UCS-4暂时没有标准库支持,一般是使用uint32_t作为数据容器
  • 注意:"编/解码"算法显然是和"字符集"独立的概念,但是我们习惯上会把特定的编码/解码算法和某个字符集对应起来.
    • 例如,从实现上说,UTF-8可以对任意字符集进行编码,而非仅是Unicode.使用UTF-8对GBK字符集中字符的Code Point进行编码/解码是完全可以进行的,但是没有人会这么做.
    • 例如,当我们使用GBK字符集时,总是会认为存储的数据是按照ANSI方案编码的

注:编解码算法的设计一般需要考虑:1.容量,算法能支持多少code_point的映射;2.空间性能,是否能尽量节省空间;3.时间性能,编码/解码速度是否方便.

现有编码概述

  • 现有的编码可以直观的表达为 Unicode系 v.s. ANSI系

ASCII

  • 不可避免的补充一下"ASCII"编码,在ASCII创立的时代,还没有字符集/编码方案这些概念,但是按现在的观点来描述:当我们说"ASCII"编码时, 通常特指:定长1字节编码方案, 使用二进制的0-127ASCII字符集进行编码,最高位为1的情况可以自行分配.
  • 从现代的角度看,仅支持ASCII编码的系统都是资源极度匮乏的系统,如低功耗嵌入式设备.一般的系统都会使用兼容ASCII编码的变长方案.

Unicode系

  • Unicode字符集的编码方案花样繁多. 其发展与多个组织相关,所以名字上也有些混乱.简言之: UCS系列都是定长编码的 ,UTF系列都是变长编码的.
    • UCS系列都是直接存储Code Point,没有编码/解码环节.
    • UTF-16是UCS-2的扩展, 可以占用2*k个字节,在只使用2个字节时,UTF-16与UCS-2基本是一致的. UTF-32与UCS-4的关系类似
    • UTF-16LE,UTF-16BE的包含有BOM以标志字节顺序(标明大端存储或者小端存储)
    • UTF-8使用单字节变长编码,最多使用4个字节,大约可以编码2^27个字符(足够大了),在仅使用一个字节时,和ASCII兼容.

ANSI

  • 非Unicode字符集一般都是使用ANSI方案进行编解码的(如GBK),ANSI编码是变长编码,可以编码大约6万个字符,最多使用2个字节,在仅使用一个字节时,和ASCII兼容.
    • 非Unicode字符集只用于支持某一种/几种文字,所以6万个也一般是足够的.

应用

对于开发者而言,只需要关注编码/解码环节,但是实际应用中可能会存在多个编码/解码环节,某一步出错,就会导致"乱码". 一般要注意的有三点,一是保证编码/解码算法的选择是正确的(可以得到正确的code_point),二是保证字符集是正确的(可以保证code_point对应的是期望的字符), 三是保证字体文件支持该字符集.

  • linux下,Unicode是事实上的标准,所有的软件输入/输出都默认依utf-8进行编/解码,一般不会出问题.
  • 微软自带组件一般使用ANSI编码,却不指定字符集,就是说,该方案是选择了编/解码算法,实际对应的字符集由操作系统决定.具体而言,操作系统先通过locale值确认code page,再依code page选择实际使用的字符集
    • 换言之,在windows下code pagechar set是几乎相同的概念.
    • 只需换个不同语言的操作系统,就会出现乱码,例如你的简中文本文件放在日文环境下打开一般就是乱码了.
  • 在微软简中操作系统中,code pageCP936,它对应GBK字符集.

开发中遇到的各个编码环节

  • 文本编辑阶段,编辑器的编码/解码设置由编辑器决定. 这仅影响编辑器内的输入/显示是否正确. 换言之,如果你在编辑器里看到的是乱码,那么你应该修改编辑器的设置.
  • 编译器在处理源代码时,第一步需要先解析文本, 这一步就需要有正确的解码方案设置,否则可能无法正确解析源码,自然就编译错误了.
    • gcc默认使用utf-8解析源代码文本.
    • MSVC会在预处理阶段使用一些trick,自动识别ANSI,UTF-16LE,UTF-8三种编码类型,并隐式的将源文件转为ANSI编码.
  • 在能正常解析源码的基础上,编译器一般不会处理源码中的字符串常量,仅仅是把字符串常量数据原封不动的粘贴到可执行文件中,直到具体显示时才由绘制系统进行解码, 一般只要确保字符串数据支持绘制系统即可.
    • linux下的渲染系统一般都把字符串作为utf-8解码.
    • MS windows则一般按ANSI进行解码.
  • 对于msvc,源码中所有L"中文"型常量字符串都将转码为UTF-16LE存储在可执行文件中(对应wchar_t[]型常量区数据), 这些宽字符都需要使用独立的API进行操作.
    • windows的API中,一般有xxxWxxxA,前者接受wchar_t[],后者接受char[]
  • msvc坑2: 虽然C++标准要求std::string使用utf-8进行编码,但在msvc中,其底层仍是ANSI编码
    • 注意,QT的QString底层使用的是utf-8编码的字符串,这些字符串即便拿到了raw pointer一般不能直接和Windows API配合使用.另外,QT的toStdString()在windows下默认不会进行转码,所以得到的仍然是utf-8std::string,仍然不能和Windows API配合
  • 在VS中,项目属性内设置支持Unicode支持多字节字符并不会设定编译器在生成二进制文件时码常量字符串的编码类型(仅由是否有L决定),仅仅会修改CString,TCHAR等数据类型的默认底层数据容器类型.