如何在标准的C++字符串中使用3个字节和4个字节的Unicode字符?

13

在标准C++中,我们有charwchar_t来存储字符。 char可以存储介于0x000xFF之间的值。而wchar_t可以存储介于0x00000xFFFF之间的值。因此,std::string使用char,只能存储1字节的字符。而std::wstring使用wchar_t,所以可以存储宽度为2字节的字符。这就是我对C++字符串的了解,请在这一点上纠正我如果我说错了什么。

我阅读了UTF-8的维基百科文章,并学到一些Unicode字符需要多达4字节的空间。例如,汉字具有Unicode代码点0x24B62,在内存中占用3字节的空间。

有没有用于处理这些字符的STL字符串容器?我正在寻找类似于std::string32的东西。此外,我们有main()用于ASCII入口点,wmain()用于16位字符支持的入口点;我们使用哪个入口点来支持3和4字节Unicode支持的代码?
您能添加一个小例子吗?
(我的操作系统:Windows 7 x64)

什么操作系统?有些比其他的更好用... - BoBTFish
5个回答

26

首先你需要更好地理解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()计算的是代码单元数量,如果你需要不同的计数,则必须使用另一种方法。

另一个例子是,如果你要拆分一个编码过的字符串,你需要知道是否以这种方式进行拆分仍然有效,并且数据的含义没有无意间改变。例如,你可能会在属于同一代码点的代码单元之间进行拆分,从而产生无效编码。或者你可能会在必须组合以表示用户感知字符的代码点之间进行拆分,从而产生用户将看到为不正确数据。

答案

今天,charwchar_t只能被视为代码单元。即使char只有一个字节,也不能阻止它表示需要两个、三个或四个字节的代码点。你只需按顺序使用两个、三个或四个char即可。这就是UTF-8的工作原理。类似地,使用两个字节的wchar_t来表示UTF-16的平台只需在必要时使用两个wchar_t即可。实际上,charwchar_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()argcargv参数。请参见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以外的任何内容。

4
wchar_t 的大小和意义是由实现定义的。在Windows上,它是16位的,就像你说的那样;在类Unix系统中,它通常是32位的,但并不总是这样。
此外,编译器可以自己决定选择与系统不同的 wchar_t 大小,但这将无法与系统的其余部分兼容。
C++11提供了 std::u32string,用于表示unicode代码点字符串。我相信最近的Microsoft编译器包括它。这有点受限,因为Microsoft的系统函数期望16位宽字符(又名UTF-16le),而不是32位的unicode代码点(又名UTF-32,UCS-4)。
虽然您提到了UTF-8:UTF-8编码的数据可以存储在普通的std::string 中。当然,由于它是一种可变长度的编码方式,您只能通过索引访问字节,而不能访问unicode代码点。但是,即使使用u32string,您通常也不需要按索引访问代码点。由于Unicode存在组合标记,Unicode代码点与可打印字符(“字形”)不是一一对应的,所以学习编程时使用字符串的许多小技巧(如反转字符串、搜索子字符串)在处理Unicode数据时不太容易,无论您将它存储在哪里。
字符是,就像你说的\u24B62。它被UTF-8编码为一系列个字节,而不是三个字节:F0 A4 AD A2。在UTF-8编码数据和Unicode代码点之间进行转换需要一些努力(尽管不是很大的努力,库函数可以帮助您完成)。最好将“编码数据”和“Unicode数据”视为不同的东西。您可以使用任何您找到的最方便的表示方法,直到您需要(例如)将文本呈现到屏幕上为止。在那时,您需要(重新)将其编码为输出目标理解的编码方式。

(总是吗?)不是的。例如,AIX使用16位的wchar_t和UTF-16。 - bames53
一个很好的例子是原始的 Android NDK,它使用了一个 8 位宽的 wchar_t 类型。 - Captain Obvlious

4

Windows使用UTF-16编码。其中,范围在U+0000至U+D7FF以及U+E000至U+FFFF之间的任何代码点都将被直接存储;而超出这些范围的代码点将根据UTF-16编码规则拆分为两个16位值。

例如,0x24B62将被编码为0xd892,0xdf62。

您可以将字符串转换为任何希望的格式,但是Windows API仍然需要并提供UTF-16编码,因此这可能是最方便的方法。


3
在标准C++中,我们有char和wchar_t来存储字符。char可以存储从0x00到0xFF之间的值。而wchar_t可以存储从0x0000到0xFFFF之间的值。
并不完全正确:
sizeof(char)     == 1   so 1 byte per character.
sizeof(wchar_t)  == ?   Depends on your system 
                        (for unix usually 4 for Windows usually 2).

Unicode字符占用多达4个字节的空间,并不完全正确。Unicode不是一种编码方式,而是一个标准,定义了每个代码点是什么以及代码点限制为21位。前16位定义了代码平面上的字符位置,而接下来的5位则定义了字符所在的平面。
有几种Unicode编码(UTF-8、UTF-16和UTF-32是最常见的),这就是你如何将字符存储在内存中。三者之间存在实际差异。
- UTF-8: 适用于存储和传输(因为它很紧凑) 缺点是长度可变 - UTF-16: 几乎在所有方面都很糟糕 始终较大且长度可变 (任何不属于BMP的内容都需要编码为代理对) - UTF-32: 适用于内存中的表示,因为它具有固定大小 缺点是每个字符需要4个字节,通常会过度杀伤。
我个人会使用UTF-8进行传输和存储,使用UTF-32在内存中呈现文本。

1
你用什么来表示一串音素簇?;-p - Steve Jessop
@SteveJessop:是的,grapheme(难拼的单词)簇实际上将多个字形放在同一位置上,并且是一个面板。 - Martin York
2
对于任何困惑的观众 - 问题在于完全通用的Unicode文本处理,例如“反转字符串”或“仅打印前10个字符”的能力实际上需要将UTF-32视为可变长度编码。 - Steve Jessop

1

charwchar_t并不是用于文本字符串的唯一数据类型。C++11引入了新的char16_tchar32_t数据类型以及相应的STL std::u16stringstd::u32string,它们是std::basic_string的typedefs,旨在解决wchar_t类型的歧义问题,因为在不同的平台上,wchar_t的大小和编码方式都不同。在某些平台上,wchar_t是16位的,适用于UTF-16编码,但在其他平台上,它是32位的,适用于UTF-32编码。而char16_t则是专门为16位的UTF-16编码设计的,char32_t则是专门为32位的UTF-32编码设计的,在所有平台上都是如此。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接