C++子字符串多字节字符

4

我有一个std::string,其中包含跨越多个字节的一些字符。

当我在此字符串上执行子串操作时,输出无效,因为这些字符被计算为2个字符。我的观点是应该使用wstring,因为它将把这些字符存储为一个元素而不是多个元素。

所以我决定将字符串复制到wstring中,但这显然没有意义,因为字符仍然被分成两个字符。这只会使情况变得更糟。

是否有好的解决方案可以将字符串转换为wstring并将特殊字符合并为1个元素而不是2个?

谢谢


这里有一个相关的问题:https://dev59.com/-G445IYBdhLWcg3wiq8i - Brady
1
你的字符串采用什么编码?我假设是UTF-8。 - SirDarius
@SirDarius,确实是UTF-8。但我认为这个问题适用于任何将多个字节用于一个字符的编码,不是吗? - W. Goeman
当然,但重要的是要知道,因为某些编码需要使用32位大小的字符宽字符串。您可能想使用诸如libiconv之类的库。 - SirDarius
1
@W.Goeman:一个重要的问题是:std::wstring 不幸地 取决于实现(在Windows上为16位宽字符,在Linux上为32位宽字符),因此它是不够的 - Matthieu M.
“_UTF-8 indeed_” 应该使用 NFC 还是 NFD? - curiousguy
6个回答

7

简化版本。基于Marcelo Cantos提供的获取UTF-8编码的std::string实际长度?解决方案。

std::string substr(std::string originalString, int maxLength)
{
    std::string resultString = originalString;

    int len = 0;
    int byteCount = 0;

    const char* aStr = originalString.c_str();

    while(*aStr)
    {
        if( (*aStr & 0xc0) != 0x80 )
            len += 1;

        if(len>maxLength)
        {
            resultString = resultString.substr(0, byteCount);
            break;
        }
        byteCount++;
        aStr++;
    }

    return resultString;
}

不幸的是,这个解决方案只是部分正确的。如果你看一下我的答案,你会发现你成功地处理了1.(不在代码点中间切割),但是你在2.(不将代码点与其变音符号分开)和3.(不在语义字符中间切割,比如西班牙语中的LL)方面失败了。后两种情况确实比较少见,但是...嗯,正确处理边缘情况是必要的。 - Matthieu M.

5

std::string对象不是字符的字符串,而是字节的字符串。它完全没有所谓“编码”的概念。对于std::wstring也是如此,只不过它是16位值的字符串。

为了执行需要处理不同字符的文本操作(例如当您想要取子串时),您需要知道用于您的std::string对象的编码。

更新:现在您明确了您的输入字符串是UTF-8编码,您仍然需要决定用于输出std::wstring的编码。UTF-16是个不错的选择,但这实际上取决于您将传递std::wstring对象给哪个API。假设UTF-16是可接受的,您有各种选择:

  1. 在Windows上,您可以使用MultiByteToWideChar函数;不需要额外的依赖。
  2. UTF8-CPP库声称提供了一种处理UTF-*编码字符串的轻量级解决方案。我自己从未尝试过,但我一直听到好的东西。
  3. 在Linux系统上,使用libiconv库是相当常见的。
  4. 如果您需要处理各种疯狂的编码,并想要全面的编码字作为编码的起点和终点,请查看ICU

2
std::wstring 是一个由 wchar_t 组成的字符串,它可能是 16 位或 32 位。 - Some programmer dude
我知道这一点,也知道我的编码方式。问题是如何使用该编码方式进行转换。 - W. Goeman
@W.Goeman:我现在更新了我的答案,并提供了一些建议,如何将UTF-8转换为其他编码(即使使用std::wstring,您仍然需要决定使用哪种编码)。 - Frerich Raabe

1

根据this,我编写了我的UTF8子字符串函数:

void utf8substr(std::string originalString, int SubStrLength, std::string& csSubstring)
{
    int len = 0, byteIndex = 0;
    const char* aStr = originalString.c_str();
    size_t origSize = originalString.size();

    for (byteIndex=0; byteIndex < origSize; byteIndex++)
    {
        if((aStr[byteIndex] & 0xc0) != 0x80)
            len += 1;

        if(len >= SubStrLength)
            break;
    }

    csSubstring = originalString.substr(0, byteIndex);
}

1

Unicode很难。

  1. std::wstring不是代码点列表,而是wchar_t列表,其宽度是实现定义的(VC++通常为16位,gcc和clang为32位)。是的,这意味着它对于可移植代码是无用的...
  2. 一个字符可能会被编码为多个代码点(因为变音符号
  3. 在某些语言中,两个不同的字符在一起形成一个“单元”,这个单元并不能真正地分开(例如,在西班牙语中,LL被认为是一个字母)。

所以...有点难。

解决第3个问题可能很昂贵(需要特定的语言/用法注释);解决第1和第2个问题是绝对必要的...并且需要支持Unicode的库或编写自己的库(并且可能会出错)。

  • 1) 很容易解决:编写一个将UTF-8转换为CodePoint的例程是微不足道的(CodePoint可以用uint32_t表示)
  • 2) 更困难,需要一个变音符号列表,并且子例程必须知道在变音符号之前不能切割(它们跟随其所修饰的字符)

否则,您可能会在ICU中找到所需的内容。祝您好运。


1
如果你的语言带有一个不错的标准库,Unicode并不难。C++没有这样的库。此外,“LL”在西班牙语中不是一个字母;由于皇家学院的某些愚蠢,它曾经被认为是一个字母,但他们最终承认它是一个二合字,并且在学术、教科书、西班牙语环境等方面已经很长时间不再被视为一个字母。混淆来自于西班牙字母与音素非常接近,而“LL”和“CH”被用来表示不同的音素。 - Miguel Pérez
@MiguelPérez:很高兴知道这个,我几年前学了西班牙语,那种奇怪的现象让我在使用词典时感到疑惑。不幸的是,这并不是唯一出现这种情况的语言 ;) - Matthieu M.
@curiousguy:无论是以下还是以上,我认为都不会更好。它确实避免了可能性的组合爆炸……并使我们的生活真的很困难 :( - Matthieu M.
我的意思是:使用前缀,如果你读到一个变音符号,你知道它后面必须跟着另一个码点,并且你必须消耗掉一个额外的码点。但是使用后缀,如果你读到一个字母,你什么也不知道。它可能只是一个单独的字母,或者它可能与后面的变音符号组合:你必须查看下一个码点,以防万一(在阻塞流上如何做到这一点?)。 - curiousguy
@curiousguy:使用istream,可以在不取出字符的情况下查看下一个字符:) 不过我同意后来看来,前缀更好。 - Matthieu M.
显示剩余2条评论

1

实际上只有两种可能的解决方案。如果您需要在较长距离上大量执行此操作,则最好将字符转换为单个元素编码,使用 wchar_t(或int32_t或最合适的其他类型)。这不是简单的复制,它会将每个单独的 char 转换为目标类型,而是一个真正的转换函数,它将识别多字节字符并将其转换为单个元素。

对于偶尔使用或较短的序列,可以编写自己的函数以前进 n 字节。 对于 UTF-8,我使用以下内容:

inline size_t
size(
    Byte                ch )
{
    return byteCountTable[ ch ] ;
}

template< typename InputIterator >
InputIterator
succ(
    InputIterator       begin,
    size_t              size,
    std::random_access_iterator_tag )
{
    return begin + size ;
}

template< typename InputIterator >
InputIterator
succ(
    InputIterator       begin,
    size_t              size,
    std::input_iterator_tag )
{
    while ( size != 0 ) {
        ++ begin ;
        -- size ;
    }
    return begin ;
}

template< typename InputIterator >
InputIterator
succ(
    InputIterator       begin,
    InputIterator       end )
{
    if ( begin != end ) {
        begin = succ( begin, end, size( *begin ),
                      std::::iterator_traits< InputIterator >::iterator_category() ) ;
    }
    return begin ;
}

template< typename InputIterator >
size_t
characterCount(
    InputIterator       begin,
    InputIterator       end )
{
    size_t              result = 0 ;
    while ( begin != end ) {
        ++ result ;
        begin = succ( begin, end ) ;
    }
    return result ;
}

需要注意的是,在许多平台上,wchar_t 只有 16 位,因此无法用单个元素表示许多码点。相反,char32_t 是 C++11 提供的一种固定宽度类型,足以在单个元素中表示 Unicode 的所有内容。 - Lucien Greathouse

0

为了简单起见,我假设您的编码是UTF-8。在这种情况下,我们将有一些占用多个字节的字符,就像您的情况一样。 然后您有std :: string,其中存储了这些UTF-8编码的字符。 现在您想要substr()以字符而不是字节为单位。 我会编写一个函数,将字符长度转换为字节长度。对于utf 8情况,它将如下所示:

#define UTF8_CHAR_LEN( byte ) (( 0xE5000000 >> (( byte >> 3 ) & 0x1e )) & 3 ) + 1

int32 GetByteCountForCharCount(const char* utf8Str, int charCnt)
{
    int ByteCount = 0;
    for (int i = 0; i < charCnt; i++)
    {
        int charlen = UTF8_CHAR_LEN(*utf8Str);
        ByteCount += charlen;
        utf8Str += charlen;
    }
    return ByteCount;
}

假设你想要从第7个字符开始使用substr()函数截取字符串,没有问题:

int32 pos = GetByteCountForCharCount(str.c_str(), 7);
str.substr(pos); 

2
为什么要使用宏而不是内联函数?为什么要传递 char const* 而不是 std::string const&?请在 C++ 问题中使用 C++ 成语。 - Matthieu M.

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