C++ char16_t和char32_t字符类型详解
在 C++11 标准中添加两种新的字符类型 char16_t 和 char32_t,它们分别用来对应 Unicode 字符集的 UTF-16 和 UTF-32 两种编码方法。
正式介绍 char16_t 和 char32_t 之前,需要先弄清楚字符集和编码方法的区别。
编码方法是利用数字和字符集建立对应关系的一套方法,这个方法可以有很多种,比如 Unicode 字符集就有 UTF-8、UTF-16 和 UTF-32 这 3 种编码方法。除了 Unicode 字符集,我们常见的字符集还包括 ASCII 字符集、GB2312 字符集、BIG5 字符集等,它们都有各自的编码方法。
字符集需要和编码方式对应,如果这个对应关系发生了错乱,那么我们就会看到计算机世界中令人深恶痛绝的乱码。不过,现在的计算机世界逐渐达成了一致,就是尽量以 Unicode 作为字符集标准,那么剩下的工作就是处理 UTF-8、UTF-16 和 UTF-32 这 3 种编码方法的问题了。
UTF-8、UTF-16 和 UTF-32 简单来说是使用不同大小内存空间的编码方法。
UTF-32 是最简单的编码方法,该方法用一个 32 位的内存空间(也就是 4 字节)存储一个字符编码,由于 Unicode 字符集的最大个数为 0x10FFFF(ISO 10646),因此 4 字节的空间完全能够容纳任何一个字符编码。UTF-32 编码方法的优点显而易见,它非常简单,计算字符串长度和查找字符都很方便;缺点也很明显,太占用内存空间。
UTF-16 编码方法所需的内存空间从 32 位缩小到 16 位(占用 2 字节),但是由于存储空间的缩小,因此 UTF-16 最多只能支持 0xFFFF 个字符,这显然不太够用,于是 UTF-16 采用了一种特殊的方法来表达无法表示的字符。
简单来说,从 0x0000~0xD7FF 以及 0xE000~0xFFFF 直接映射到 Unicode 字符集,而剩下的 0xD800~0xDFFF 则用于映射 0x10000~0x10FFFF 的 Unicode 字符集,映射方法为:字符编码减去 0x10000 后剩下的 20 比特位分为高位和低位,高 10 位的映射范围为 0xD800~0xDBFF,低 10 位的映射范围为 0xDC00~0xDFFF。
例如 0x10437,减去 0x10000 后的高低位分别为 0x1 和 0x37,分别加上 0xD800 和 0xDC00 的结果是 0xD801 和 0xDC37。
幸运的是,一般情况下 0xFFFF 足以覆盖日常字符需求,我们也不必为了 UTF-16 的特殊编码方法而烦恼。UTF-16 编码的优势是可以用固定长度的编码表达常用的字符,所以计算字符长度和查找字符也比较方便。另外,在内存空间使用上也比 UTF-32 好得多。
最后说一下我们最常用的 UTF-8 编码方法,它是一种可变长度的编码方法。
由于 UTF-8 编码方法只占用 8 比特位(1 字节),因此要表达完数量高达 0x10FFFF 的字符集,它采用了一种前缀编码的方法。这个方法可以用 1~4 字节表示字符个数为 0x10FFFF 的 Unicode(ISO 10646)字符集。为了尽量节约空间,常用的字符通常用 1~2 字节就能表达,其他的字符才会用到 3~4 字节,所以在内存空间可以使用 UTF-8,但是计算字符串长度和查找字符在 UTF-8 中却是一个令人头痛的问题。
下表展示了 UTF-8 对应的范围:
到了 C++11,char16_t 和 char32_t 的出现打破了这个尴尬的局面。除此之外,C++11 标准还为 3 种编码提供了新前缀用于声明 3 种编码字符和字符串的字面量,它们分别是 UTF-8 的前缀 u8、UTF-16 的前缀 u 和 UTF-32 的前缀 U:
讨论到这里读者会产生一个疑问,既然已经有了处理宽字符的字符类型,那么为什么又要加入新的字符类型呢?
没错,wchar_t 确实在一定程度上能够满足我们对于字符表达的需求,但是起初在定义 wchar_t 时并没有规定其占用内存的大小。于是就给了实现者充分的自由,以至于在 Windows 上 wchar_t 是一个 16 位长度的类型(2 字节),而在 Linux 和 macOS 上 wchar_t 却是 32 位的(4 字节)。这导致了一个严重的后果,我们写出的代码无法在不同平台上保持相同行为。而 char16_t 和 char32_t 的出现解决了这个问题,它们明确规定了其所占内存空间的大小,让代码在任何平台上都能够有一致的表现。
如果其中一个字符串字面量没有前缀,则将其视为与另一个字符串字面量具有相同前缀的字符串字面量,其他的连接行为由具体实现者定义。另外,这里的连接操作是编译时的行为,而不是一个转换。
需要注意的是,进行连接的字符依然是保持独立的,也就是说不会因为字符串连接,将两个字符合并为一个,例如连接 "\xA" "B"的结果应该是 "\nB"(换行符和字符B),而不是一个字符 "\xAB"。
C11 在中增加了 4 个字符的转换函数,包括:
当然 C++11 中也添加了 C++ 风格的转发方法 std::wstring_convert 以及 std::codecvt。使用类模板 std::wstring_convert 和 std::codecvt 相结合,可以对多字节字符串和宽字符串进行转换。不过这里并不打算花费篇幅介绍这些转换方法,因为它们在 C++17 标准中已经不被推荐使用了,所以应该尽量避免使用它们。
除此之外,C++ 标准库的字符串也加入了对新字符类型的支持,例如:
正式介绍 char16_t 和 char32_t 之前,需要先弄清楚字符集和编码方法的区别。
字符集和编码方法
通常我们所说的字符集是指系统支持的所有抽象字符的集合,通常一个字符集的字符是稳定的。编码方法是利用数字和字符集建立对应关系的一套方法,这个方法可以有很多种,比如 Unicode 字符集就有 UTF-8、UTF-16 和 UTF-32 这 3 种编码方法。除了 Unicode 字符集,我们常见的字符集还包括 ASCII 字符集、GB2312 字符集、BIG5 字符集等,它们都有各自的编码方法。
字符集需要和编码方式对应,如果这个对应关系发生了错乱,那么我们就会看到计算机世界中令人深恶痛绝的乱码。不过,现在的计算机世界逐渐达成了一致,就是尽量以 Unicode 作为字符集标准,那么剩下的工作就是处理 UTF-8、UTF-16 和 UTF-32 这 3 种编码方法的问题了。
UTF-8、UTF-16 和 UTF-32 简单来说是使用不同大小内存空间的编码方法。
UTF-32 是最简单的编码方法,该方法用一个 32 位的内存空间(也就是 4 字节)存储一个字符编码,由于 Unicode 字符集的最大个数为 0x10FFFF(ISO 10646),因此 4 字节的空间完全能够容纳任何一个字符编码。UTF-32 编码方法的优点显而易见,它非常简单,计算字符串长度和查找字符都很方便;缺点也很明显,太占用内存空间。
UTF-16 编码方法所需的内存空间从 32 位缩小到 16 位(占用 2 字节),但是由于存储空间的缩小,因此 UTF-16 最多只能支持 0xFFFF 个字符,这显然不太够用,于是 UTF-16 采用了一种特殊的方法来表达无法表示的字符。
简单来说,从 0x0000~0xD7FF 以及 0xE000~0xFFFF 直接映射到 Unicode 字符集,而剩下的 0xD800~0xDFFF 则用于映射 0x10000~0x10FFFF 的 Unicode 字符集,映射方法为:字符编码减去 0x10000 后剩下的 20 比特位分为高位和低位,高 10 位的映射范围为 0xD800~0xDBFF,低 10 位的映射范围为 0xDC00~0xDFFF。
例如 0x10437,减去 0x10000 后的高低位分别为 0x1 和 0x37,分别加上 0xD800 和 0xDC00 的结果是 0xD801 和 0xDC37。
幸运的是,一般情况下 0xFFFF 足以覆盖日常字符需求,我们也不必为了 UTF-16 的特殊编码方法而烦恼。UTF-16 编码的优势是可以用固定长度的编码表达常用的字符,所以计算字符长度和查找字符也比较方便。另外,在内存空间使用上也比 UTF-32 好得多。
最后说一下我们最常用的 UTF-8 编码方法,它是一种可变长度的编码方法。
由于 UTF-8 编码方法只占用 8 比特位(1 字节),因此要表达完数量高达 0x10FFFF 的字符集,它采用了一种前缀编码的方法。这个方法可以用 1~4 字节表示字符个数为 0x10FFFF 的 Unicode(ISO 10646)字符集。为了尽量节约空间,常用的字符通常用 1~2 字节就能表达,其他的字符才会用到 3~4 字节,所以在内存空间可以使用 UTF-8,但是计算字符串长度和查找字符在 UTF-8 中却是一个令人头痛的问题。
下表展示了 UTF-8 对应的范围:
代码范围十六进制 | UTF-8二进制 | 注释 |
---|---|---|
000000~00007F 128 个代码 |
0ZZZZZZZ | ASCII 字符范围,字节由零开始 |
000080~0007FF 1920 个代码 |
110yyyyy 10zzzzzz | 第 1 字节由 110 开始,接着的字节由 10 开始 |
000800~00D7FF 00E000~00FFFF 61440 个代码 |
1110xxxx 10yyyyyy 10zzzzzz | 第 1 字节 1110 开始,接着的字节由 10 开始 |
010000~10FFFF 1048576 个代码 |
11110WWW 10XXXXXX 10yyyyyy 10zzzzzz |
将由 11110 开始,接着的字节从 10 开始 |
char16_t和char32_t
对于 UTF-8 编码方法而言,普通类型似乎是无法满足需求的,毕竟普通类型无法表达变长的内存空间。所以一般情况下我们直接使用基本类型 char 进行处理,而过去也没有一个针对 UTF-16 和 UTF-32 的字符类型。到了 C++11,char16_t 和 char32_t 的出现打破了这个尴尬的局面。除此之外,C++11 标准还为 3 种编码提供了新前缀用于声明 3 种编码字符和字符串的字面量,它们分别是 UTF-8 的前缀 u8、UTF-16 的前缀 u 和 UTF-32 的前缀 U:
char utf8c = u8'a'; // C++17标准 //char utf8c = u8'好'; char16_t utf16c = u'好'; char32_t utf32c = U'好'; char utf8[] = u8"你好世界"; char16_t utf16[] = u"你好世界"; char32_t utf32[] = U"你好世界";在上面的代码中,分别使用 UTF-8、UTF-16 和 UTF-32 编码的字符和字符串对变量进行了初始化,代码很简单,不过还是有两个地方值得一提。
char utf8c = u8'a';
在 C++11 标准中实际上是无法编译成功的,因为在 C++11 标准中 u8 只能作为字符串字面量的前缀,而无法作为字符的前缀。这个问题直到 C++17 标准才得以解决,所以上述代码需要 C++17 的环境来执行编译。char utf8c = u8'好';
是无法通过编译的,因为存储“好”需要 3 字节,显然 utf8c 只能存储 1 字节,所以会编译失败。wchar_t存在的问题
在 C++98 的标准中提供了一个 wchar_t 字符类型,并且还提供了前缀 L,用它表示一个宽字符。事实上 Windows 系统的 API 使用的就是 wchar_t,它在 Windows 内核中是一个最基础的字符类型:HANDLE CreateFileW( LPCWSTR lpFileName, ... ); CreateFileW(L"c:\\tmp.txt",...);上面是一段在 Windows 系统上创建文件的伪代码,可以看出 Windows 为创建文件的 API 提供了宽字符版本,其中 LPCWSTR 实际上是 const wchar_t 的指针类型,我们可以通过 L 前缀来定义一个 wchar_t 类型的字符串字面量,并且将其作为实参传入 API。
讨论到这里读者会产生一个疑问,既然已经有了处理宽字符的字符类型,那么为什么又要加入新的字符类型呢?
没错,wchar_t 确实在一定程度上能够满足我们对于字符表达的需求,但是起初在定义 wchar_t 时并没有规定其占用内存的大小。于是就给了实现者充分的自由,以至于在 Windows 上 wchar_t 是一个 16 位长度的类型(2 字节),而在 Linux 和 macOS 上 wchar_t 却是 32 位的(4 字节)。这导致了一个严重的后果,我们写出的代码无法在不同平台上保持相同行为。而 char16_t 和 char32_t 的出现解决了这个问题,它们明确规定了其所占内存空间的大小,让代码在任何平台上都能够有一致的表现。
新字符串连接
由于字符类型增多,因此我们还需要了解一下字符串连接的规则:如果两个字符串字面量具有相同的前缀,则生成的连接字符串字面量也具有该前缀,如下表所示。源代码 | 等同于 | 源代码 | 等同于 | 源代码 | 等同于 | ||
---|---|---|---|---|---|---|---|
u"a" u"b" | u"ab" | U"a" U"b" | U"ab" | L"a" L"b" | L"ab" | ||
u"a" "b" | u"ab" | U"a" "b" | U"ab" | L"a" "b" | L"ab" | ||
"a" u"b" | u"ab" | "a" U"b" | U"ab" | "a" L"b" | L"ab" |
如果其中一个字符串字面量没有前缀,则将其视为与另一个字符串字面量具有相同前缀的字符串字面量,其他的连接行为由具体实现者定义。另外,这里的连接操作是编译时的行为,而不是一个转换。
需要注意的是,进行连接的字符依然是保持独立的,也就是说不会因为字符串连接,将两个字符合并为一个,例如连接 "\xA" "B"的结果应该是 "\nB"(换行符和字符B),而不是一个字符 "\xAB"。
库对新字符类型的支持
随着新字符类型加入 C++11 标准,相应的库函数也加入进来。C11 在中增加了 4 个字符的转换函数,包括:
size_t mbrtoc16(char16_t* pc16, const char* s, size_t n, mbstate_t* ps); size_t c16rtomb(char* s, char16_t c16, mbstate_t* ps); size_t mbrtoc32(char32_t* pc32, const char* s, size_t n, mbstate_t* ps); size_t c32rtomb(char* s, char32_t c32, mbstate_t* ps);它们的功能分别是多字节字符和 UTF-16 编码字符互转,以及多字节字符和 UTF-32 编码字符互转。在 C++11 中,我们可以通过包含
<cuchar>
来使用这 4 个函数。当然 C++11 中也添加了 C++ 风格的转发方法 std::wstring_convert 以及 std::codecvt。使用类模板 std::wstring_convert 和 std::codecvt 相结合,可以对多字节字符串和宽字符串进行转换。不过这里并不打算花费篇幅介绍这些转换方法,因为它们在 C++17 标准中已经不被推荐使用了,所以应该尽量避免使用它们。
除此之外,C++ 标准库的字符串也加入了对新字符类型的支持,例如:
using u16string = basic_string; using u32string = basic_string; using wstring = basic_string;