如何在 C 中正确使用 UTF-8 上的 std::string ?

新手上路,请多包涵

我的平台是Mac。我是一名 C++ 初学者,正在从事一个处理中文和英文的个人项目。 UTF-8 是本项目的首选编码。

我在 Stack Overflow 上阅读了一些帖子,其中许多建议在处理 UTF-8 时使用 std::string 并避免使用 wchar_t 因为现在没有 char8_t -8。

However, none of them talk about how to properly deal with functions like str[i] , std::string::size() , std::string::find_first_of() or std::regex as these function usually returns面对 UTF-8 时的意外结果。

我应该继续使用 std::string 还是切换到 std::wstring ?如果我应该继续使用 std::string ,那么处理上述问题的最佳做法是什么?

原文由 Saddle Point 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 1.9k
2 个回答

Unicode 词汇表

Unicode 是一个庞大而复杂的主题。我不想在那里涉水太深,但是需要一个快速的词汇表:

  1. 代码点:代码点是 Unicode 的基本构建块,代码点只是映射到 含义 的整数。整数部分适合 32 位(嗯,真的是 24 位),其含义可以是字母、变音符号、空格、符号、笑脸、半个标志……甚至可以是“下一部分从右到左阅读”。
  2. Grapheme Clusters : Grapheme Clusters 是一组语义相关的代码点,例如 unicode 中的一个标志是通过关联两个代码点来表示的;这两者中的每一个都没有任何意义,但它们在一个 Grapheme Cluster 中关联在一起,它们代表一个标志。在某些脚本中,Grapheme Clusters 还用于将字母与变音符号配对。

这是 Unicode 的基础。 Code Point 和 Grapheme Cluster 之间的区别几乎可以忽略,因为对于大多数现代语言,每个“字符”都映射到一个 Code Point(对于常用的字母 + 变音符号组合有专用的重音形式)。不过,如果您冒险使用笑脸、旗帜等……那么您可能需要注意区别。


UTF 入门

然后,必须对一系列 Unicode 代码点进行编码;常见的编码有 UTF-8、UTF-16 和 UTF-32,后两种既有 Little-Endian 又有 Big-Endian 的形式,共有 5 种常见编码。

在 UTF-X 中,X 是 Code Unit 的比特大小,每个 Code Point 表示为一个或多个 Code Unit,具体取决于其大小:

  • UTF-8:1 到 4 个代码单元,
  • UTF-16:1 或 2 个代码单元,
  • UTF-32:1 个代码单元。

std::stringstd::wstring

  1. 不要使用 std::wstring 如果您关心可移植性( wchar_t 在 Windows 上只有 16 位);使用 std::u32string 代替(又名 std::basic_string<char32_t> )。
  2. 内存表示( std::stringstd::wstring )独立于磁盘表示(UTF-8、UTF-16 或 UTF-32),因此请做好准备在边界处转换(读取和写入)。
  3. 虽然 32 位 wchar_t 确保一个代码单元代表一个完整的代码点,但它仍然不代表一个完整的字素簇。

如果您只是阅读或编写字符串,那么 std::stringstd::wstring 应该没有任何问题。

当您开始切片和切块时,麻烦就开始了,那么您必须注意 (1) 代码点边界(在 UTF-8 或 UTF-16 中)和 (2) Grapheme Clusters 边界。前者可以自己轻松处理,后者需要使用 Unicode 感知库。


选择 std::stringstd::u32string

如果性能是一个问题,那么 std::string 可能会因为其较小的内存占用而表现更好;尽管大量使用中文可能会改变交易。一如既往,简介。

如果 Grapheme Clusters 没有问题,那么 std::u32string 具有简化事情的优点:1 Code Unit -> 1 Code Point 意味着你不会意外拆分 Code Points,以及 std::basic_string 的所有功能 --- 开箱即用。

If you interface with software taking std::string or char* / char const* , then stick to std::string to avoid back-and-forth conversions.否则会很痛苦。


UTF-8 在 std::string

UTF-8 实际上在 std::string 中运行良好。

大多数操作都是开箱即用的,因为 UTF-8 编码是自同步的并且向后兼容 ASCII。

由于代码点的编码方式,查找代码点不会意外匹配另一个代码点的中间:

  • str.find('\n') 有效,
  • str.find("...") 用于逐字节匹配 1 ,
  • str.find_first_of("\r\n") 在搜索 ASCII 字符时 有效。

同样, regex 应该大多开箱即用。由于一个字符序列( "haha" )只是一个字节序列( "哈" ),基本的搜索模式应该是开箱即用的。

但是,请注意字符类(例如 [:alphanum:] ),因为根据正则表达式的风格和实现,它可能匹配也可能不匹配 Unicode 字符。

同样,对非ASCII“字符”应用中继器时要小心, "哈?" 可能只考虑最后一个字节是可选的;在这种情况下,使用括号清楚地描述重复的字节序列: "(哈)?"

1 查找的关键概念是规范化和整理;这会影响所有比较操作。 std::string 将始终逐字节比较(并因此排序),而不考虑特定于语言或用法的比较规则。如果您需要处理完整的规范化/整理,则需要一个完整的 Unicode 库,例如 ICU。

原文由 Matthieu M. 发布,翻译遵循 CC BY-SA 4.0 许可协议

std::stringstd::wstring 都必须使用 UTF 编码来表示 Unicode。特别是在 macOS 上, std::string 是 UTF-8(8 位代码单元),而 std::wstring 是 UTF-32(32 位代码单元);请注意, wchar_t 的大小取决于平台。

对于这两者, size 跟踪代码单元的数量而不是代码点的数量或字素簇。 (代码点是一个命名的 Unicode 实体,其中一个或多个形成一个字形簇。字形簇是用户与之交互的可见字符,如字母或表情符号。)

虽然我不熟悉中文的 Unicode 表示,但很有可能当你使用 UTF-32 时,代码单元的数量往往非常接近字素簇的数量。然而,显然,这是以使用多达 4 倍的内存为代价的。

最准确的解决方案是使用 Unicode 库(例如 ICU)来计算您所追求的 Unicode 属性。

最后,不使用组合字符的人类语言中的 UTF 字符串通常在 find / regex 中表现良好。我不确定中文,但英文是其中之一。

原文由 zneak 发布,翻译遵循 CC BY-SA 4.0 许可协议

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题