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.
-
- 部分字体可能会在显示阶段做一些融合性hack,例如将
'!='
显示成一个不等号'≠'
,包含两个字符的一个字符串,显示出来却只有一个字符,这种case一般可以通过换字体或修改设置来解决.
- 部分字体可能会在显示阶段做一些融合性hack,例如将
-
- 部分看起来一模一样的字符可能是不同的字符,例如电阻的单位欧姆符号
'Ω'
,和希腊字母 omega'Ω'
, 其实是两个字符. 这种case可以通过类似python3的unicodedata.normalize(my_str)
来进行规范化,处理后将仅剩omega
- 部分看起来一模一样的字符可能是不同的字符,例如电阻的单位欧姆符号
-
- 部分文字自身支持字符的融合,例如将
'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-127
对ASCII
字符集进行编码,最高位为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兼容.
- UCS系列都是直接存储
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 page
和char set
是几乎相同的概念. - 只需换个不同语言的操作系统,就会出现乱码,例如你的简中文本文件放在日文环境下打开一般就是乱码了.
- 换言之,在windows下
- 在微软简中操作系统中,
code page
为CP936
,它对应GBK字符集.
开发中遇到的各个编码环节
- 文本编辑阶段,编辑器的编码/解码设置由编辑器决定. 这仅影响编辑器内的输入/显示是否正确. 换言之,如果你在编辑器里看到的是乱码,那么你应该修改编辑器的设置.
- 编译器在处理源代码时,第一步需要先解析文本, 这一步就需要有正确的解码方案设置,否则可能无法正确解析源码,自然就编译错误了.
- gcc默认使用
utf-8
解析源代码文本. - MSVC会在预处理阶段使用一些trick,自动识别ANSI,UTF-16LE,UTF-8三种编码类型,并隐式的将源文件转为ANSI编码.
- gcc默认使用
- 在能正常解析源码的基础上,编译器一般不会处理源码中的字符串常量,仅仅是把字符串常量数据原封不动的粘贴到可执行文件中,直到具体显示时才由绘制系统进行解码, 一般只要确保字符串数据支持绘制系统即可.
- linux下的渲染系统一般都把字符串作为utf-8解码.
- MS windows则一般按ANSI进行解码.
- 对于msvc,源码中所有
L"中文"
型常量字符串都将转码为UTF-16LE
存储在可执行文件中(对应wchar_t[]
型常量区数据), 这些宽字符都需要使用独立的API进行操作.- windows的API中,一般有
xxxW
或xxxA
,前者接受wchar_t[]
,后者接受char[]
- windows的API中,一般有
- msvc坑2: 虽然C++标准要求
std::string
使用utf-8
进行编码,但在msvc中,其底层仍是ANSI编码- 注意,QT的QString底层使用的是
utf-8
编码的字符串,这些字符串即便拿到了raw pointer
一般不能直接和Windows API
配合使用.另外,QT的toStdString()
在windows下默认不会进行转码,所以得到的仍然是utf-8
的std::string
,仍然不能和Windows API
配合
- 注意,QT的QString底层使用的是
- 在VS中,项目属性内设置
支持Unicode
或支持多字节字符
并不会设定编译器在生成二进制文件时码常量字符串的编码类型(仅由是否有L
决定),仅仅会修改CString
,TCHAR
等数据类型的默认底层数据容器类型.