C++中的Unicode字符串索引

6

我来自Python,你可以使用“string[10]”访问序列中的字符。如果该字符串编码为Unicode,则会给出预期结果。但是,在C++中对字符串使用索引时,只要字符是ASCII,它就可以工作。但是当我在字符串中使用Unicode字符并使用索引时,输出将以八进制表示形式显示,例如/201。

string ramp = "ÐðŁłŠšÝýÞþŽž";
cout << ramp << "\n";    
cout << ramp[5] << "\n";

输出:

ÐðŁłŠšÝýÞþŽž
/201

为什么会发生这种情况,我该如何访问字符串表示中的该字符,或者如何将八进制表示转换为实际字符?

2
我建议使用std::wstringstd::wcout - Cory Kramer
5
@CoryKramer 我不会无条件地推荐这样做,比如可以看看这篇文章。我更倾向于建议使用合适的库。 - Baum mit Augen
@BaummitAugen 很好的观点。字符编码让我对自己所知道的关于编程的一切都产生了怀疑 :/ - Cory Kramer
2
C++没有真正的本地Unicode支持。 - Puppy
@Puppy: ICU 有。但是C++没有本地支持GUI或音频处理,但这并不意味着它不适合这项工作。;-) - DevSolar
显示剩余2条评论
5个回答

12

标准C++并不适用于Unicode的正确处理,会给你带来像你观察到的那样的问题。

问题在于C++比Unicode早得多。这意味着即使是你的字符串文字也将以一种实现定义的方式解释,因为这些字符未在基本源字符集中定义(基本上是ASCII-7字符减去@$和反引号)。

C++98根本没有提到Unicode。它提到了wchar_t,以及基于它的wstring,指定wchar_t能够“表示当前语言环境中的任何字符”。但这造成了更多的伤害...

Microsoft将wchar_t定义为16位,这足以表示Unicode代码点当时的范围。然而,自那以后,Unicode已经扩展到16位范围之外......并且Windows的16位wchar_t不再是“宽”的,因为您需要两个wchar_t来表示超出BMP的字符——而Microsoft文档对于wchar_t是指UTF-16(带代理对的多字节编码)还是UCS-2(不支持BMP以外字符的宽编码)常常含糊不清。

与此同时,Linux的wchar_t是32位,足以容纳UTF-32......

C++11在这个问题上做出了重大改进,添加了char16_tchar32_t,包括它们相关的string变量,以消除歧义,但仍然不能完全满足Unicode操作的需求

举个例子,试着将德语单词"Fuß"转换为大写字母,你就会明白我的意思了。(单个字母'ß'需要扩展为'SS',而标准函数——一次处理一个字符——无法做到这一点。)

然而,有帮助可寻。国际Unicode组件(ICU)库完全能够处理C++中的Unicode。至于在源代码中指定特殊字符,则必须使用u8""u""U""来强制将字符串文字解释为UTF-8、UTF-16和UTF-32,使用八进制/十六进制转义或依赖编译器实现适当处理非ASCII-7编码。

即使对于std::cout << ramp[5],您也将获得整数值,因为对于C ++,字符只是具有语义含义的整数。 ICU的ustream.hicu :: UnicodeString类提供了operator<<重载,但ramp[5]只是一个16位无符号整数(1),如果他们的unsigned short突然被解释为字符,人们会对您产生怀疑。您需要使用C-API u_fputs() / u_printf() / u_fprintf()函数。

#include <unicode/unistr.h>
#include <unicode/ustream.h>
#include <unicode/ustdio.h>

#include <iostream>

int main()
{
    // make sure your source file is UTF-8 encoded...
    icu::UnicodeString ramp( icu::UnicodeString::fromUTF8( "ÐðŁłŠšÝýÞþŽž" ) );
    std::cout << ramp << "\n";
    std::cout << ramp[5] << "\n";
    u_printf( "%C\n", ramp[5] );
}

使用g++ -std=c++11 testme.cpp -licuio -licuuc编译。

ÐðŁłŠšÝýÞþŽž
353
š

(1) ICU在内部使用UTF-16编码,而UnicodeString::operator[]返回的是代码单元(code unit),而不是代码点(code point),因此您可能会得到代理对的一半。请查阅API文档,了解其他索引Unicode字符串的方法。

哪个 ICU 发行版最适合在 Mac 上的 Xcode 中使用? - Bahman Eslami
哦...你指的是哪个发行版?我不明白你的意思。由于Mac没有二进制包,我想你需要获取最新的源代码并编译/安装它? - DevSolar
您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - Bahman Eslami
@RickJames:这就是关于非BMP字符和UTF-16代理对的部分,即使你使用UTF-32编码,也有组合字符。 - DevSolar
(否则,我们可能是在互相认同。) - Rick James
显示剩余5条评论

5

C++本身没有实用的本地化Unicode支持。几乎肯定需要使用外部库,如ICU。


2
在我看来,处理字符串的最佳解决方案是使用迭代器执行任何任务。我无法想象出真正需要索引字符串的情况:如果你需要像你的示例中的 ramp[5] 这样的索引,那么 5 通常会在代码的其他部分计算,并且通常你也会扫描所有前面的字符。这就是标准库在其 API 中使用迭代器的原因。
如果您想获取字符串的大小,则会遇到类似的问题。它应该是字符(或代码点)计数还是仅字节数?通常,您需要大小来分配缓冲区,因此字节计数更可取。您只有非常非常少的时候需要获取 Unicode 字符计数。
如果您想使用迭代器处理 UTF-8 编码的字符串,那么我强烈推荐使用 UTF8-CPP

2

要单独访问代码点,请使用u32string,它将字符串表示为char32_t类型的UTF-32码元序列。

u32string ramp = U"ÐðŁłŠšÝýÞþŽž";
cout << ramp << "\n";    
cout << ramp[5] << "\n";

是的,但你应该提到C++11。 - Basile Starynkevitch
1
有趣的是,在 coliru 上,使用 G++ 或 Clang++ 编译时 cout << ramp << "\n"; 会出现编译错误。 - NathanOliver
1
@NathanOliver 说得对,char32_t 不是 char,而这正是 std::cout 处理的。 - Baum mit Augen
由于wcout处理的是wchar_t,而在Windows上并不是char32_t,因此我们可以看到标准C++仍然不能很好地处理Unicode。虽然比C++98好,但如果您想走完整个路程,仍然需要ICU。 - DevSolar

0

关于正在发生的事情,cplusplus.com 明确指出:

请注意,该类独立于所使用的编码处理字节:如果用于处理多字节或可变长度字符序列(例如 UTF-8),则该类的所有成员(如长度或大小)以及其迭代器仍将按字节(而不是实际编码字符)操作。

关于解决方案,其他人已经说得很对了:如果您没有使用 C++11,则使用 ICU;如果您使用了 C++11,则使用 u32string


1
即使是 u32string 也不是完整的答案,而且空间效率也不高。建议在可用 C++11 版本时仍使用 ICU。 - DevSolar

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