编程文汇

跨平台c++中处理UTF字符串

  • 基本概念
  1. 跨平常用的字符集:utf-8 utf-16 utf-32
  2. 大端、小端:这时编码顺序的问题,是字节顺序,以字节为单位,正反存储。就像说话正着说、倒着说一样,0x1234,在存储时可以存为0x1234或者0x3412,如果实际存储顺序和我们正常阅读的顺序一致,那就是大端存储,否则就是小端。只有utf8不存在大小端问题。utf16 utf32都有大小端问题,两个系统采用相同的编码,但是大小端可能不一样,那编码就不能直接通用。其实计算机中的整数、浮点数,也一样,也可以正着存、反着存,所以不同计算机、不同系统、不同语言交互时才会需要统一序列化格式。
  3. 字符集选择:如果用与存储、传输,utf8是首选,因为体积小。如果是字符串处理,首选utf-16(它的常用字符集6万多,是固定的),utf-32,固定宽度,算法简单。如果只选一个,选utf-16把,常用字符集6万,操作也方便。网页里一般都是utf8,这样网页的体积小,因为utf8里英文字符都是单字节,光这一项,体积就小很多。
  4. locale:时区,把字符串输出到控制台时,很有可能要用到这东西了,这东西需要的是什么编码,就要把自己的字符串转成什么编码输出。
  5. unicode和utf:unicode是一个不断演进的编码体系,具体到编程上,unicode编码是16位固定编码,比较老的了,现在一般不用,麻烦,搞不懂。utf(8,16,32)编码都是相对比较新、设计考量比较全面。新的开发语言、新的操作系统,现代的应用开发,一般都会采用utf编码,我们跟随潮流走,就行了。

这个库的用法和java中的字符串用法基本一样。可以查找、比较、查询字节数、查询字符串长度、正则表达式查找、字符集之间的相互转换,基本上java里能干的它都能干,感觉很强大。

  • 利用宿主语言:

如果c++代码是作为动态库存在,被别的语言调用,比如c#,那么字符串处理,可以在c#中进行,c++至需要存放相应编码(最好直接使用宿主语言的内部编码,这样在使用时不必进行编码转换)的二进制。注意,往数据库存储、或者往网络上发送时,要统一编码(各个操作系统、各种宿主语言的内部编码不一样),一般用utf8,这就要求在发送之前,做一次转码(从宿主语言的内部编码c# utf16=>utf8),接受方收到后,如果要使用或者打印,需要做一次反向转码(utf8=>java utf16 ),如果确认双方编码一致,可以直接统一使用utf-16,无须来回转换。

参考文献

  1. 维基百科,utf16
  2. 字节顺序

更新2020.1.17

根据这里的讨论:

可以看出,std::string是直接支持u8,包括正则表达式操作。但是不知道字符个数,不会截取string。还是utf32吧。

Unicode词汇表

Unicode是一个庞大而复杂的主题。我不希望在那儿走得太深,但是有必要提供快速的词汇表:

  1. 代码点 :代码点是Unicode的基本构建块,代码点只是映射为 含义 的整数。整数部分可容纳32位(实际上是24位),其含义可以是字母,变音符号,空格,符号,笑脸,半个旗标,甚至可以是“下一部分从右到左读取”。
  2. 字素簇 :字素簇是语义相关的代码点的组,例如unicode中的一个标志是通过关联两个代码点来表示的;孤立的这两个中的每一个都没有意义,但是在一个词素簇中关联在一起,它们代表一个标志。在某些脚本中,字素簇还用于将字母与变音符号配对。

这是Unicode的基础。代码点和字素簇之间的区别大部分可以被掩盖,因为对于大多数现代语言而言,每个“字符”都映射到单个代码点(常用的字母+音符组合带有专用的重音形式)。不过,如果您冒险使用笑脸,旗帜等,那么您可能必须注意区别。

UTF入门

然后,必须对一系列Unicode代码点进行编码。通用编码为UTF-8,UTF-16和UTF-32,后两种以Little-Endian和Big-Endian形式存在,总共有5种通用编码。

在UTF-X中,X是 代码单位 的大小,每个代码点取决于其大小,表示为一个或几个代码单位:

  • 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)字素簇边界。前者可以很容易地自己处理,后者需要使用Unicode感知库。

采摘 std::string 还是 std::u32string

如果需要考虑性能,则可能 std::string 由于其较小的内存占用而表现更好。尽管大量使用中文可能会改变交易。一如既往,简介。

如果Grapheme Clusters没问题,那么它 std::u32string 具有简化事情的优势:1个代码单位-> 1个代码点意味着您不会意外地拆分代码点,并且所有 std::basic_string 工作功能均即开即用。

如果您使用 std::string 或或 char* / 与软件交互 char const* ,则请坚决 std::string 避免来回转换。否则会很痛苦。

std::string中的UTF-8 。

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。

参考虚幻引擎

虚幻字符串的内部编码是UCS-2,固定两个字节。
如果按照虚幻的做法,就比较简单了:
只需要进行u8和ucs2的转换就可以了:

std::wstring_convert<std::codecvt_utf8<char16_t>, char16_t> ucs2conv;

内部字符串处理用ucs2,外部数据交换用u8