文字版PDF文档链接:现代C++新特性(文字版)-C++文档类资源-CSDN下载
在C++11标准中添加两种新的字符类型char16_t和char32_t,它们分别用来对应Unicode字符集的UTF-16和UTF-32两种编码方法。在正式介绍它们之前,需要先弄清楚字符集和编码方法的区别。
通常我们所说的字符集是指系统支持的所有抽象字符的集合,通常一个字符集的字符是稳定的。而编码方法是利用数字和字符集建立对应关系的一套方法,这个方法可以有很多种,比如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中却是一个令人头痛的问题。表1-1展示了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开始 |
对于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字节,所以会编译失败。
在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的出现解决了这个问题,它们明确规定了其所占内存空间的大小,让代码在任何平台上都能够有一致的表现。
由于字符类型增多,因此我们还需要了解一下字符串连接的规则:如果两个字符串字面量具有相同的前缀,则生成的连接字符串字面量也具有该前缀,如表1-2所示。如果其中一个字符串字面量没有前缀,则将其视为与另一个字符串字面量具有相同前缀的字符串字面量,其他的连接行为由具体实现者定义。另外,这里的连接操作是编译时的行为,而不是一个转换。
源代码 |
等同于 |
源代码 |
等同于 |
源代码 |
等同于 |
||
---|---|---|---|---|---|---|---|
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中,我们可以通过包含
除此之外,C++标准库的字符串也加入了对新字符类型的支持,例如:
using u16string = basic_string;
using u32string = basic_string;
using wstring = basic_string;
使用char类型来处理UTF-8字符虽然可行,但是也会带来一些困扰,比如当库函数需要同时处理多种字符时必须采用不同的函数名称以区分普通字符和UTF-8字符。C++20标准新引入的类型char8_t可以解决以上问题,它可以代替char作为UTF-8的字符类型。char8_t具有和unsigned char相同的符号属性、存储大小、对齐方式以及整数转换等级。引入char8_t类型后,在C++17环境下可以编译的UTF-8 字符相关的代码会出现问题,例如:
char str[] = u8"text"; // C++17编译成功;C++20编译失败,需要char8_t
char c = u8'c';
当然反过来也不行:
char8_t c8a[] = "text"; // C++20编译失败,需要char
char8_t c8 = 'c';
另外,为了匹配新的char8_t字符类型,库函数也有相应的增加:
size_t mbrtoc8(char8_t* pc8, const char* s, size_t n, mbstate_t* ps);
size_t c8rtomb(char* s, char8_t c8, mbstate_t* ps);
using u8string = basic_string;
后需要说明的是,上面这些例子只是C++标准库为新字符类型新增代码的冰山一角,有兴趣的读者可以翻阅标准库代码,包括