遍历 Unicode 字符串的每个字符

5
以下的字符串输出的大小不正确。为什么会这样,我该如何修复?
string str = " ██████";
cout << str.size();
// outputs 19 rather than 7

我正在尝试逐个字符地遍历“str”,以便将其读入大小为7的vector<string>中,但由于上述代码输出19,所以我无法实现这一点。

2
你可以使用 wstring 而不是 string - Jean-Baptiste Yunès
3
如果wstring使用UTF-16编码且字符在BMP范围之外,则该方法将无效。对于许多具有变音符号的字符,它也不起作用,因为某些字符是预组合的,而一些可以组合:Å = U+00C5或U+0041加上U+030A,如果没有适当的规范化,调用length可能会返回1或2,而不是人们所期望的一致数字。 - phuclv
@phuclv请看一下被接受的答案。 - Jean-Baptiste Yunès
@Jean-BaptisteYunès,我昨天已经看到了,如果OP想要的是字符数,那么它是不正确的。 - phuclv
@ToasterFrogs 你需要解释一下为什么你期望它有6个字符的大小。你是想获取可见字符的数量吗?这在大多数情况下并不是开发者想要的。而即使在这种情况下,你还必须选择一种规范化方式,因为Å会因视角不同而导致1个或2个字符,并且 ‍‍‍ 在编码下实际上由7个字符组成。你只对含有█或任何其他Unicode字符的字符串感兴趣吗? - phuclv
1
@phuclv,我打错了。我想要的输出是7 - 帖子已经被编辑过了。我有兴趣使用其他 Unicode 字符,例如 ,以及可能的其他框线字符。我想要获取字符串中字符的数量 - 对于你所提到的情况,我希望返回1 - k-a-v
2个回答

9

TL;DR

basic_stringsize()length() 成员函数返回的是底层字符串的大小(以“单位”为单位),而不是可见字符的数量。要获取预期的数量:

  • 对于非BMP、无组合字符和无连字符的简单字符串,使用带有u前缀的UTF-16
  • 对于不包含任何组合或连接字符的简单字符串,使用带有U前缀的UTF-32
  • 对于任意Unicode字符串,进行规范化并计数

"██████"是一个空格,后面跟着一系列6个U+2588字符。你的编译器似乎在使用UTF-8来处理std::string。UTF-8是一种可变长度编码,许多字母都使用多个字节进行编码(因为显然你不能仅用一个字节编码超过256个字符)。在UTF-8中,U+0800和U+FFFF之间的代码点由3个字节编码。因此,在UTF-8中,该字符串的长度为1 + 6*3 = 19字节。"
你可以使用任何Unicode转换器,比如this one,可以看到该字符串在UTF-8中被编码为20 E2 96 88 E2 96 88 E2 96 88 E2 96 88 E2 96 88 E2 96 88,你也可以循环遍历每个字节来检查。
如果你想要字符串中可见字符的总数,则更加棘手,churill的解决方案不起作用。阅读Twitter中的示例。

If you use anything beyond the most basic letters, numbers, and punctuation the situation gets more confusing. While many people use multi-byte Kanji characters to exemplify these issues, Twitter has found that accented vowels cause the most confusion because English speakers simply expect them to work. Take the following example: the word “café”. It turns out there are two byte sequences that look exactly the same, but use a different number of bytes:

café  0x63 0x61 0x66 0xC3 0xA9        Using the “é” character, called the “composed character”.
café  0x63 0x61 0x66 0x65 0xCC 0x81   Using the combining diacritical, which overlaps the “e”
你需要一个Unicode库,比如ICU规范化字符串并进行计数。例如Twitter使用规范化形式C

编辑:

由于你只对框线字符感兴趣,这些字符似乎不会超出BMP范围,也不包含任何组合字符,因此UTF-16和UTF-32都可以使用。像std::string一样,std::wstring也是一个basic_string,没有强制编码。在大多数实现中,它通常是UTF-16(Windows)或UTF-32(*nix),因此您可以使用它,但它不可靠并且取决于源代码编码。更好的方法是使用std::u16stringstd::basic_string<char16_t>)和std::u32stringstd::basic_string<char32_t>)。它们将在不考虑系统和源文件编码的情况下工作。

std::wstring wstr     = L" ██████";
std::u16string u16str = u" ██████";
std::u32string u32str = U" ██████";
std::cout << str.size();    // may work, returns the number of wchar_t characters
std::cout << u16str.size(); // always returns the number of UTF-16 code units
std::cout << u32str.size(); // always returns the number of UTF-32 code units

如果您对如何处理所有Unicode字符感兴趣,请继续阅读下文。
上面提到的“café”问题引发了一个问题,如何计算Tweet字符串“café”的字符数。对于人眼来说,长度显然是四个字符。根据数据的表示方式,这可能是五个或六个UTF-8字节。Twitter不想因为我们使用UTF-8或者API客户端使用更长的表示方式而惩罚用户。因此,无论发送哪种表示方式,Twitter都将“café”视为四个字符。
Twitter使用文本的Normalization Form C(NFC)版本来计算Tweet的长度。这种规范化倾向于使用完全组合的字符(例如咖啡馆示例中的0xC3 0xA9),而不是长形式版本(0x65 0xCC 0x81)。Twitter还计算文本中的码点数量,而不是UTF-8字节数。例如,咖啡馆示例中的0xC3 0xA9是一个码点(U+00E9),编码为两个UTF-8字节,而0x65 0xCC 0x81是两个码点,编码为三个字节。 Twitter - 计算字符数

另请参阅


你是否犯了一个错误:“在大多数实现中,它通常是UTF-16(Windows)或UTF-16(nix)”?在gcc中,std :: wstring和wchar_t默认为32位,在nix中可能也是clang。 - Кое Кто
@КоеКто 是的,谢谢你注意到了。 - phuclv

3

std::string 只包含 1 字节长的字符(通常为8位,包含UTF-8字符),如果你想要实现更多字符集的支持,需要使用 wchar_tstd::wstring

std::wstring str = L" ██████";
std::cout << str.size();

虽然这会打印出7(一个空格和6个Unicode字符),但请注意在字符串文字前面的L,因此它将被解释为宽字符串。

从技术上讲,char 并不需要是8位长的。一些平台可能会有更长的 char - phuclv
1
@churill std::string 对 UTF-8 一无所知,并且除非你确实让它存储 UTF-8,否则它通常不会保存 UTF-8。是否使用 UTF-8 取决于正在使用的平台、编译器设置、源文件的编码等因素。最起码,如果要使用 UTF-8 的字面量,请使用 u8 前缀:string str = u8" ██████"; 但我提到的其他因素仍然会影响它的使用。 - Remy Lebeau
@RemyLebeau 我知道了,我不太擅长找到正确的术语,你可以随意编辑 :) - Lukas-T
2
如上所述,这将返回UTF-16(或根据实现情况为UTF-32)代码单元的数量,而不是字符串中字符的数量(如果这是OP想要的话,因为目前仍不清楚),因此它绝对无法用于像“‍Å”这样的字符串(它将返回UTF-16的9和UTF-32的6)。 - phuclv

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