首先你需要更好地理解Unicode。你问题的具体答案在底部。
概念
你需要比介绍编程课程时所需的非常简单的文本处理更细致的概念集。
字节是可寻址的最小存储单位。通常为8位,能够存储多达256个不同的值。按定义,char就是一个字节。
代码单元是用于存储文本的最小固定大小的数据单元。当你真的不关心文本的内容,只想将其复制到某个地方或计算文本使用了多少内存时,你关心的是代码单元。否则,代码单元没什么用处。
代码点表示字符集中的一个不同成员。无论字符集中有什么“字符”,它们都被分配一个唯一的数字,每当你看到特定的数字被编码时,你就知道你正在处理字符集的哪个成员。
抽象字符是语言系统中具有含义并且与其表示或任何指定给该含义的代码点不同的实体。
用户感知字符是用户在使用的任何语言系统中认为的字符。
在早期,char代表了所有这些东西:按定义,char是一个字节,在char*字符串中,代码单元是char,字符集很小,所以char可表示的256个值足以表示每个成员,并且支持的语言系统很简单,因此字符集的成员大多直接表示用户想要直接使用的字符。
但是,char代表几乎所有内容的这种简单系统不足以支持更复杂的系统。
首先遇到的问题是有些语言使用超过256个字符。因此引入了“宽”字符。宽字符仍然使用单一类型来表示上述四个概念之一:代码单元、代码点、抽象字符和用户感知字符。但是,宽字符不再是单字节的了。这被认为是支持大型字符集最简单的方法。
代码基本上可以保持不变,只需处理宽字符而不是char。
但是,事实证明许多语言系统并不那么简单。在某些系统中,不必将每个用户感知字符都用单个抽象字符在字符集中表示是有意义的。因此,使用Unicode字符集的文本有时使用多个抽象字符表示用户感知字符,或者使用单个抽象字符表示多个用户感知字符。
宽字符存在另一个问题。由于它们增加了代码单元的大小,因此也增加了每个字符使用的空间。如果想要处理可以用单字节代码单元充分表示的文本,但必须使用宽字符系统,则使用的内存量比单字节代码单元的情况更高。因此,希望宽字符不要太宽。同时,宽字符需要足够宽,以为字符集中的每个成员提供唯一值。
Unicode当前包含约100,000个抽象字符。这需要比大多数人想使用的宽字符更宽。结果,直接使用大于一个字节的代码单元来存储码点值的宽字符系统是不可取的。
总之,最初不需要区分字节、代码单元、代码点、抽象字符和用户感知字符。然而,随着时间的推移,需要区分每个概念。
编码方式:在上述之前,文本数据的存储很简单。每个用户感知字符对应于一个抽象字符,该字符具有一个码点值。字符数足够少,256个值就足够了。因此,可以直接将对应于所需用户感知字符的码点数存储为字节。稍后,使用宽字符时,对应于用户感知字符的值将直接存储为更大尺寸的整数,例如16位。
但由于以这种方式存储Unicode文本会使用比人们愿意花费的更多的内存(每个字符需要三到四个字节),Unicode“编码”不是通过直接存储码点值来存储文本,而是使用可逆函数计算一些代码单元值来存储每个代码点。
例如,UTF-8编码可以使用一个单字节代码单元表示最常用的Unicode代码点。较少使用的代码点使用两个单字节代码单元进行存储。仍然不太常见的代码点则使用三个或四个代码单元进行存储。
这意味着通常可以使用UTF-8编码存储常见文本所需的内存量要少于16位宽字符方案,但也意味着存储的数字不一定直接对应于抽象字符的码点值。如果想知道存储的抽象字符,则必须“解码”存储的代码单元。如果要了解用户感知字符,则必须进一步将抽象字符转换为用户感知字符。
有许多不同的编码方式,为了将使用这些编码方式的数据转换成抽象字符,您必须知道正确的解码方法。如果不知道编码是如何将码点值转换为代码单元的,则存储的值实际上是没有意义的。
编码方式的一个重要影响是您需要知道对编码数据进行特定操作是否有效或有意义。
举例来说,如果你想获取一个字符串的“大小”,你是在计算字节、代码单元、抽象字符还是用户感知字符?std::string::size()
计算的是代码单元数量,如果你需要不同的计数,则必须使用另一种方法。
另一个例子是,如果你要拆分一个编码过的字符串,你需要知道是否以这种方式进行拆分仍然有效,并且数据的含义没有无意间改变。例如,你可能会在属于同一代码点的代码单元之间进行拆分,从而产生无效编码。或者你可能会在必须组合以表示用户感知字符的代码点之间进行拆分,从而产生用户将看到为不正确数据。
答案
今天,char
和wchar_t
只能被视为代码单元。即使char
只有一个字节,也不能阻止它表示需要两个、三个或四个字节的代码点。你只需按顺序使用两个、三个或四个char
即可。这就是UTF-8的工作原理。类似地,使用两个字节的wchar_t
来表示UTF-16的平台只需在必要时使用两个wchar_t
即可。实际上,char
和wchar_t
的实际值并不分别表示Unicode代码点。它们表示由编码代码点产生的代码单元值。例如,Unicode代码点U+0400在UTF-8中被编码为两个代码单元- > 0xD0 0x80
。同样,Unicode代码点U+24B62也被编码为四个代码单元0xF0 0xA4 0xAD 0xA2
。
因此,你可以使用std::string
来保存UTF-8编码的数据。
在Windows上,main()
不仅支持ASCII,而且还支持系统char
编码。现在,即使Windows支持以UTF-8作为系统char
编码,并且不再局限于旧的编码方式。你可能需要对Windows进行配置;我不确定它是否是默认设置,如果不是,希望它成为默认设置。
你还可以使用Win32 API调用直接访问UTF-16命令行参数,而不使用main()
的argc
和argv
参数。请参见GetCommandLineW()
和CommandLineToArgvW
。
wmain()
函数的argv
参数完全支持Unicode。在Windows上,wchar_t
中存储的是16位代码单元,即UTF-16代码单元。Windows API本地使用UTF-16,因此在Windows上很容易使用。然而,wmain()
是非标准的函数,过度依赖它将导致无法移植。
示例:
#include <iostream>
#include <string>
int main() {
std::string s = "CJK UNIFIED IDEOGRAPH-24B62: \U00024B62";
std::cout << s << '\n';
auto space = s.rfind(' ');
std::cout << "Encoded bytes: ";
for (auto i = space + 1, end = s.size(); i != end; ++i) {
std::cout << std::hex << static_cast<int>(static_cast<unsigned char>(s[i])) << " ";
}
}
如果编译器使用UTF-8作为窄执行编码,那么
s
将包含UTF-8数据。如果您正在使用支持UTF-8的终端运行已编译程序,已配置为使用它并使用支持该字符的字体,则应该看到该程序打印出该字符。
在Windows上,我使用/utf-8标志构建了
cl.exe /EHsc /utf-8 tmp.cpp
,并运行了命令将控制台设置为UTF-8
chcp 65001
,这导致此程序打印出正确的数据。虽然由于字体缺乏支持,字符显示为带有问号的方框。从控制台复制文本并将其粘贴到具有适当支持的位置将显示正确的字符。
使用
/utf-8
,您还可以直接在字符串文字中编写UTF-8数据,而不是使用
\Uxxxxxxxx
语法。
在GCC中,您可以使用
-fexec-charset=utf-8
标志构建此程序,尽管它应该是默认值。
-finput-charset=utf-8
允许您直接在字符串文字中编写UTF-8编码的数据。
Clang不支持除UTF-8以外的任何内容。