字符集、字符编码和编码转换
概述
字符集(Character Set)是字符的集合。字符在字符集中的位置称为码位(Code Point)。不同的字符集通常包含不同数量的字符,且同一个字符在不同的字符集中的码位也往往不同。常用的字符集有 GB2312、GB18030 和 Unicode 等。
字符编码(Character Encoding)是将字符的码位转换为计算机中实际存储和传输的字节序列(编码表示)的规则和过程。只有字符集和字符编码都确定后,字符的编码表示才能确定。很多字符集都只支持一种编码,因此它们的名称既表示一个字符集,也表示一种字符编码,比如 ASCII 和 GB2312。常用的字符编码还有 UTF-8、UTF-16 和 UTF-32 等。
代码页
一个字符集中的所有字符以及它们的一种编码表示构成一个代码页(Code Page)。Windows 代码页又称 ANSI 代码页。在 Windows 系统中,代码页的索引称为 代码页标识符(Code Page Identifier)。例如,包含字符集 GB2312 中的所有字符及其编码表示的代码页的标识符为 936。
历史上,不同的国家和地区制定了不同的字符集和字符编码。因此,不同国家和地区的 Windows 用户通常使用不同的代码页。例如,新加坡和中国大陆地区用户使用 936 代码页,对应的字符集为 GB2312;港澳台地区用户使用 950 代码页,对应的字符集为 Big5。在 Windows 系统中,计算机当前所处的地理位置称为系统区域(System locale),对应的代码页称为系统默认代码页。很多程序在读写文本文档时都按系统默认代码页处理,因此系统默认代码页会影响程序解读文本文档的内容。有两种方式可以更改系统区域:
- 控制面板 - 更改日期、时间或数字格式 - 管理 - 更改系统区域设置
- 设置 - 时间和语言 - 语言 - 管理语言设置 - 更改系统区域设置
常用字符集
ASCII
ASCII(American Standard Code for Information Interchange,美国信息交换标准码)由 ANSI(American National Standards Institute,美国国家标准学会)制定,用 7 比特表示一个字符,共包含 128 个字符。
Latin1
即 ISO-8859-1,用 8 比特表示一个字符,共包含 256 个字符。Latin1 兼容 ASCII,即前 128 个字符(0x00-0x7F)与 ASCII 完全一致。兼容 ASCII 的字符集都称为 ASCII 的扩展字符集。
GB2312
即信息交换用汉字编码字符集,用 1 到 2 个字节表示一个字符,兼容 ASCII。
GB2312 将字符集分成 94 个区,每个区包含 94 个字符。每个字符的分区编号(01-94)和位置编号(01-94)组成它的区位码。例如,汉字「好」位于 0x1A 区 0x23 位,故它的区位码就是 0x1A23。
区位码加上 0x20 就得到国标码。例如,汉字「好」的国标码就是 0x3A43。国标码是每个字符的唯一编号。加上 0x20 是为了重用 ASCII 的前 32 个控制字符。只重用前 32 个控制字符,是因为剩下的 ASCII 字符都已经包含在 GB2312 中了。
国标码加上 0x80(最高位置 1)就得到机内码。例如,汉字「好」的机内码就是 0xBAC3。可以认为,机内码 = 区位码 + A0。汉字是以机内码的形式在计算机中存储和传播的。
GB18030 和 GBK
GB18030 兼容 GBK,GBK 又兼容 GB2312。因此,GB18030、GBK、GB2312 都是 ASCII 的扩展字符集。
UCS
UCS(Universal Multiple-Octet Coded Character Set,通用字符集)是能够容纳世界上所有字符的字符集。字符在 UCS 中的码位称为通用字符名(Universal Character Name,UCN),记作 U+hhhh
,其中 hhhh
是 16 进制数。例如,汉字「好」的通用字符名是 U+597d
。
在 HTML 中,通用字符名记作 &#ddddd;
,其中 ddddd
是 10 进制数,例如:
1 | <!-- 你好 --> |
组合用字符
顾名思义,组合用字符(Combining Character)专门用来和其他字符结合成一个新的字符。例如,带抑音符的小写字母 à
也可由小写字母 a
与组合用抑音符 U+0300
组合而成。
Unicode | HTML | Charactor |
---|---|---|
U+0061 |
a |
a |
U+0061 + U+0300 |
à |
à |
U+00E0 |
à |
à |
UTF
UCS 支持多种编码,这些编码统称为 UTF(Unicode Transformation Format,UCS 转换格式),共有 UTF-8、UTF-16 和 UTF-32 三种。UTF-8 使用 1 到 4 个字节表示一个字符;UTF-16 使用 2 或 4 个字节表示一个字符。使用 UTF-8 编码时,对于一个 n
字节的字符:
- 当
n
等于1
时,字节的最高位为0
。 - 当
n
大于等于 2 时,最高字节的高n
位为1
,其余字节的最高2
位固定为10
。
Unicode | Length | Bytes |
---|---|---|
0x00 - 0x7F |
1 | 0*** **** |
0x0080 - 0x07FF |
2 | 11** **** 10** **** |
0x0800 - 0xFFFF |
3 | 111* **** 10** **** 10** **** |
0x00010000 - 0x0010FFFF |
4 | 1111 **** 10** **** 10** **** 10** **** |
例:汉字「好」的通用字符名是 U+597d
,介于 0x0800
至 0xFFFF
之间,要用 3 个字节表示:
1 | 5 9 7 d |
BOM
字节序由字节流开头的 BOM(Byte Order Mark,字节顺序标记)指示。
Encoding | Encoded BOM |
---|---|
UTF-8 | EF BB BF |
UTF-16 big-endian | FE FF |
UTF-16 little-endian | FF FE |
UTF-32 big-endian | 00 00 FE FF |
UTF-32 little-endian | FF FE 00 00 |
数据结构
根据每个字符占用的字节数,可以把字符集分为以下 3 类:
- 单字节字符集(Single-Byte Character Set,SBCS),每个字符占用 1 字节,如 ASCII 和 Latin1。
- 双字节字符集(Double-Byte Chactacter Set,DBCS),每个字符占用 1 到 2 字节,如 GB2312 和 GBK。
- 多字节字符集(Multi-Byte Character Set,MBCS),每个字符占用 1 到 4 个字节,如 GB18030 和 UTF-8。
把字符分为以下 2 类:
- 以 1 个字节为存储单位,占用 1 到 4 个字节的字符称为多字节字符(Multi-byte Character)。由多字节字符组成的字符串称为多字节字符串(Multi-byte string)。
- 以 2 个字节为存储单位,占用 2 或 4 个字节的字符称为宽字符(Wide character)。由宽字符组成的字符串称为宽字符串(Wide string)。
C/C++ 提供多种数据类型用于存储字符。根据单位数据占用的字节数,可以把这些数据类型划分成两类:
- 窄字符型:单位数据占用 1 字节,只有
char
一种。 - 宽字符型:单位数据占用 2 或 4 个字节,有
wchar_t
、char16_t
和char32_t
三种。
在 Windows 中,数据类型为 char
、使用系统默认代码页的多字节字符串又称 ANSI 字符串;数据类型为 wchar_t
、使用 UTF-16 编码的宽字符串又称 Unicode 字符串。
用 MSVC 编译 C/C++ 代码时,可以用 /source-charset
选项指示源字符集,用 /execution-charset
选项指示执行字符集。源字符集就是源文件使用的字符集。只有源字符集符合实际,预处理器才能正确加载源文件的内容。执行字符集就是程序使用的字符集。源文件中的字符串字面量在编译前会被转换成使用执行字符集的版本。
字符串字面量的编码可以通过添加前缀单独指定:
1 | const char s[] = "A好"; // 41 ba c3 (GB2312) |
字符可以用通用字符名表示,记作 \uhhhh
或 \Uhhhhhhhh
,例如:
1 | const wchar_t ws[] = L"\u597d\U0000597d"; // L"好好" |
通用字符名还可以记作 3 位的 8 进制数 \ooo
或 16 进制数 \xhh...
,例如:
1 | const wchar_t ws[] = L"\041\x597d"; // L"A好" |
编码转换
在不同的操作系统上进行字符编码转换的方式不同;C/C++ 标准库也提供了一组用来支持 UTF 的库函数。
Windows
在 Windows 上进行编码转换可以借助 MultiByteToWideChar()
和 WideCharToMultiByte()
这两个函数。
MultiByteToWideChar()
函数可以把多字节字符串转换成 UTF-16 编码的宽字符串,原型如下:
1 | int MultiByteToWideChar( |
CodePage
是在转换过程中使用的代码页或编码,它决定了目标字符串的编码,可以是本机支持的任何代码页,也可以是下列宏定义:
CP_ACP
系统默认代码页,在中国大陆地区为 GB2312 编码。CP_UTF8
UTF-8 编码。
dwFlags
可以改变函数的行为,可以是 0
。常用的标志如下:
MB_ERR_INVALID_CHARS
若遇到非法字符,则转换失败。
多字节字符串的开头由 lpMultiByteStr
指示、长度由 cbMultiByte
指示。当多字节字符串以字符串结束符 \0
结尾时,cbMultiByte
可以是 -1
。
用于保存宽字符串的内存空间的起始地址由 lpWideCharStr
指示、大小由 cchWideChar
指示。当 cchWideChar
为 0
时,函数返回以字符为单位的字符串长度,不执行转换工作。
下面的例子将 GB2312 编码的多字节字符串 "好"
转换成 UTF-16 编码的宽字符串:
1 | const char lpMultiByteStr[] = "好"; // ba c3 |
WideCharToMultiByte()
函数可以把 UTF-16 编码的宽字符串转换成多字节字符串,原型如下:
1 | int WideCharToMultiByte( |
如果源字符串包含代码页不包含的字符,函数就会用 lpDefaultChar
指示的字符替换这些字符,同时把 lpUsedDefaultChar
设为 TRUE
。
下面的例子将 UTF-16 编码的宽字符串 "好"
转换成 UTF-8 编码的多字节字符串:
1 | const wchar_t lpWideCharStr[] = L"好"; // 597d |
Linux
在 Linux 上进行编码转换可以使用库 iconv(Internationalization Conversion)提供的 iconv()
函数。
使用库 iconv 需要包含头文件 iconv.h
。
1 |
要使用 iconv()
函数,必须先调用 iconv_open()
函数创建一个转换器。iconv_open()
函数的两个参数分别指示目标字符集和源字符集,返回值是转换器的描述符。转换器在使用完毕后,必须用 iconv_close()
函数释放其占用的资源。下面的例子用于创建从 UTF-8 到 UTF-16 的转换器:
1 | iconv_t cd = iconv_open("utf16", "utf8"); |
iconv()
函数的原型如下:
1 | size_t iconv(iconv_t cd, |
cd
是转换器的描述符。inbuf
和 outbuf
是两个指针的地址:作为入参,它们分别指向一个源字符串和一段内存空间的开头;作为出参,它们分别指向源字符串和内存空间的剩余部分。inbytesleft
和 outbytesleft
是两个整型变量的地址:作为入参,它们分别指示源字符串和内存空间的总长度;作为出参,它们分别指示源字符串和内存空间的剩余长度。
下面的例子用于获取 "A好"
的 UTF-16 编码:
1 | char in[] = u8"A好"; |
"A好"
的 UTF-8 编码是 41 e5 a5 bd 00
,共 5 字节,转换成小端字节序的 UTF-16 编码是 41 00 7d 59 00 00
,共 6 字节。由于 iconv()
还会在开头插入 BOM,因此,最终的结果是 ff fe 41 00 7d 59 00 00
,共 8 字节。outbytesleft
最终的值是 247 = 255 - 8
。
标准库
mbrtoc32()
能够将一个 UTF-8 编码的多字节字符转换成 UTF-32 编码的宽字符。
1 | size_t mbrtoc32(char32_t *pc32, const char *s, size_t n, mbstate_t *ps); |
如果从 s
开始的 n
字节包含一个 UTF-8 编码的多字节字符,mbrtoc32()
就会向缓冲区 pc32
写入一个 UTF-32 编码的宽字符,并返回已处理的字节数。其他返回值的含义如下:
0
遇到字符串结束符,并且已经将其写入pc32
。-1
遇到非法的多字节字符。-2
遇到不完整的多字节字符,状态已经记录在ps
中,下一次调用应该提供余下字节。-3
上一次调用遇到需要用代理对(Surrogate Pair)表示的多字节字符,本次调用向缓冲区写入代理对的第二部分,不执行其他操作。使用 UTF-32 编码时,这种情况不可能发生,因此mbrtoc32()
不可能返回-3
。
1 |
|