通过指针如何读取UTF-8字符?

6
假设我已经将UTF-8内容存储在内存中,那么如何使用指针读取字符? 我认为我需要观察第8位是否表示多字节字符,但是我如何将序列转换为有效的Unicode字符?此外,wchar_t是否是存储单个Unicode字符的合适类型?
这就是我考虑的:

   wchar_t readNextChar (char*& p)
   { 
       wchar_t unicodeChar;
       char ch = *p++;
if ((ch & 128) != 0) { // 这是一个多字节字符,现在该怎么办? // char chNext = *p++; // ... 但我如何组装Unicode字符? ... } ... return unicodeChar; }

4
“Unicode字符的宽度”这个说法没有意义,你需要选择一种编码方式。根据你的平台, wchar_t 的大小可能不同。在类Unix操作系统上,它通常是32位,因此可以将UTF-32编码的Unicode字符存储在其中;而在Windows上,它是16位,因此可以存储UTF-16编码的Unicode字符。 - sbi
除了返回宽字符外,你的 readNextChar 函数必须提供信息以正确更新 p。UTF-8(以及UTF-16)是可变长度编码,你的调用者不能假定指针具有恒定或简单的增量。 - mpez0
5个回答

7
您需要将UTF-8位模式解码为其未编码的UTF-32表示形式。如果您需要实际的Unicode代码点,则必须使用32位数据类型。
在Windows上,wchar_t不够大,因为它只有16位。您必须改用unsigned int或unsigned long。仅在处理UTF-16代码单元时才使用wchar_t。
在其他平台上,wchar_t通常为32位。但是,在撰写可移植代码时,除了绝对必要的情况(例如std :: wstring)之外,应避免使用wchar_t。
尝试使用类似以下内容的代码:
#define IS_IN_RANGE(c, f, l)    (((c) >= (f)) && ((c) <= (l)))

u_long readNextChar (char* &p) 
{  
    // TODO: since UTF-8 is a variable-length
    // encoding, you should pass in the input
    // buffer's actual byte length so that you
    // can determine if a malformed UTF-8
    // sequence would exceed the end of the buffer...

    u_char c1, c2, *ptr = (u_char*) p;
    u_long uc = 0;
    int seqlen;
    // int datalen = ... available length of p ...;    

    /*
    if( datalen < 1 )
    {
        // malformed data, do something !!!
        return (u_long) -1;
    }
    */

    c1 = ptr[0];

    if( (c1 & 0x80) == 0 )
    {
        uc = (u_long) (c1 & 0x7F);
        seqlen = 1;
    }
    else if( (c1 & 0xE0) == 0xC0 )
    {
        uc = (u_long) (c1 & 0x1F);
        seqlen = 2;
    }
    else if( (c1 & 0xF0) == 0xE0 )
    {
        uc = (u_long) (c1 & 0x0F);
        seqlen = 3;
    }
    else if( (c1 & 0xF8) == 0xF0 )
    {
        uc = (u_long) (c1 & 0x07);
        seqlen = 4;
    }
    else
    {
        // malformed data, do something !!!
        return (u_long) -1;
    }

    /*
    if( seqlen > datalen )
    {
        // malformed data, do something !!!
        return (u_long) -1;
    }
    */

    for(int i = 1; i < seqlen; ++i)
    {
        c1 = ptr[i];

        if( (c1 & 0xC0) != 0x80 )
        {
            // malformed data, do something !!!
            return (u_long) -1;
        }
    }

    switch( seqlen )
    {
        case 2:
        {
            c1 = ptr[0];

            if( !IS_IN_RANGE(c1, 0xC2, 0xDF) )
            {
                // malformed data, do something !!!
                return (u_long) -1;
            }

            break;
        }

        case 3:
        {
            c1 = ptr[0];
            c2 = ptr[1];

            switch (c1)
            {
                case 0xE0:
                    if (!IS_IN_RANGE(c2, 0xA0, 0xBF))
                    {
                        // malformed data, do something !!!
                        return (u_long) -1;
                    }
                    break;

                case 0xED:
                    if (!IS_IN_RANGE(c2, 0x80, 0x9F))
                    {
                        // malformed data, do something !!!
                        return (u_long) -1;
                    }
                    break;

                default:
                    if (!IS_IN_RANGE(c1, 0xE1, 0xEC) && !IS_IN_RANGE(c1, 0xEE, 0xEF))
                    {
                        // malformed data, do something !!!
                        return (u_long) -1;
                    }
                    break;
            }

            break;
        }

        case 4:
        {
            c1 = ptr[0];
            c2 = ptr[1];

            switch (c1)
            {
                case 0xF0:
                    if (!IS_IN_RANGE(c2, 0x90, 0xBF))
                    {
                        // malformed data, do something !!!
                        return (u_long) -1;
                    }
                    break;

                case 0xF4:
                    if (!IS_IN_RANGE(c2, 0x80, 0x8F))
                    {
                        // malformed data, do something !!!
                        return (u_long) -1;
                    }
                    break;

                default:
                    if (!IS_IN_RANGE(c1, 0xF1, 0xF3))
                    {
                        // malformed data, do something !!!
                        return (u_long) -1;
                    }
                    break;                
            }

            break;
        }
}

    for(int i = 1; i < seqlen; ++i)
    {
        uc = ((uc << 6) | (u_long)(ptr[i] & 0x3F));
    }

    p += seqlen;
    return uc; 
}

1
@DeadMG:我一定是错过了你的通灵能力,无法从这个问题中得出这些结论。 - sbi
@DeadMG:我现在可以看出我的评论可能会引起歧义。我当时很困惑(显然失败了),想表达的是我不知道你怎么知道OP需要这个用于Windows。 - sbi
@DavidHaim 这并不是“坏掉了”。u8"" 文字使用 const char 元素,所以只需更新代码以使用 const char* 指针迭代字符串,而不是 char* 指针。当然,如果您正在使用 u8"",则应该是使用 C++11 或更新版本,并且有更好的方法来处理 UTF-8。 - Remy Lebeau
@RemyLebeau,你完全误解了我的意思。即使你添加了const,你的函数仍然认为表情符号不是有效的utf-8格式,尽管它实际上是有效的。因此,函数返回-1。 - David Haim
1
@DavidHaim:u8"" 被编码为字节 F0 9F 98 80,这是 U+1F600 GRINNING FACE 的正确 UTF-8 序列。我已经调整了代码以正确处理它。只是在应用 IS_IN_RANGE() 的逻辑错误。 - Remy Lebeau
显示剩余8条评论

4
这是一个快速的宏,用于计算UTF-8字节数。
#define UTF8_CHAR_LEN( byte ) (( 0xE5000000 >> (( byte >> 3 ) & 0x1e )) & 3 ) + 1

这将帮助您检测UTF-8字符的大小,从而更轻松地进行解析。


2
如果你需要解码UTF-8,你需要开发一个UTF-8解析器。UTF-8是一种可变长度的编码(1到4个字节),因此你确实需要编写符合标准的解析器:例如可以参考wikipedia
如果你不想编写自己的解析器,我建议使用一个库。例如在glib中可以找到(我个人使用Glib :: ustring,glib的C ++包装器),但在任何良好的通用库中也能找到。
编辑:
我认为C++0x也将包含对UTF-8的支持,但我不是专家...
以上仅供参考。

1

这是我的解决方案,使用纯 ANSI-C 编写,并包括角落情况的单元测试。

请注意,int 必须至少为 32 位宽。否则,您必须更改 codepoint 的定义。

#include <assert.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>

typedef unsigned char byte;
typedef unsigned int codepoint;

/**
 * Reads the next UTF-8-encoded character from the byte array ranging
 * from {@code *pstart} up to, but not including, {@code end}. If the
 * conversion succeeds, the {@code *pstart} iterator is advanced,
 * the codepoint is stored into {@code *pcp}, and the function returns
 * 0. Otherwise the conversion fails, {@code errno} is set to
 * {@code EILSEQ} and the function returns -1.
 */
int
from_utf8(const byte **pstart, const byte *end, codepoint *pcp) {
        size_t len, i;
        codepoint cp, min;
        const byte *buf;

        buf = *pstart;
        if (buf == end)
                goto error;

        if (buf[0] < 0x80) {
                len = 1;
                min = 0;
                cp = buf[0];
        } else if (buf[0] < 0xC0) {
                goto error;
        } else if (buf[0] < 0xE0) {
                len = 2;
                min = 1 << 7;
                cp = buf[0] & 0x1F;
        } else if (buf[0] < 0xF0) {
                len = 3;
                min = 1 << (5 + 6);
                cp = buf[0] & 0x0F;
        } else if (buf[0] < 0xF8) {
                len = 4;
                min = 1 << (4 + 6 + 6);
                cp = buf[0] & 0x07;
        } else {
                goto error;
        }

        if (buf + len > end)
                goto error;

        for (i = 1; i < len; i++) {
                if ((buf[i] & 0xC0) != 0x80)
                        goto error;
                cp = (cp << 6) | (buf[i] & 0x3F);
        }

        if (cp < min)
                goto error;

        if (0xD800 <= cp && cp <= 0xDFFF)
                goto error;

        if (0x110000 <= cp)
                goto error;

        *pstart += len;
        *pcp = cp;
        return 0;

error:
        errno = EILSEQ;
        return -1;
}

static void
assert_valid(const byte **buf, const byte *end, codepoint expected) {
        codepoint cp;

        if (from_utf8(buf, end, &cp) == -1) {
                fprintf(stderr, "invalid unicode sequence for codepoint %u\n", expected);
                exit(EXIT_FAILURE);
        }

        if (cp != expected) {
                fprintf(stderr, "expected %u, got %u\n", expected, cp);
                exit(EXIT_FAILURE);
        }
}

static void
assert_invalid(const char *name, const byte **buf, const byte *end) {
        const byte *p;
        codepoint cp;

        p = *buf + 1;
        if (from_utf8(&p, end, &cp) == 0) {
                fprintf(stderr, "unicode sequence \"%s\" unexpectedly converts to %#x.\n", name, cp);
                exit(EXIT_FAILURE);
        }
        *buf += (*buf)[0] + 1;
}

static const byte valid[] = {
        0x00, /* first ASCII */
        0x7F, /* last ASCII */
        0xC2, 0x80, /* first two-byte */
        0xDF, 0xBF, /* last two-byte */
        0xE0, 0xA0, 0x80, /* first three-byte */
        0xED, 0x9F, 0xBF, /* last before surrogates */
        0xEE, 0x80, 0x80, /* first after surrogates */
        0xEF, 0xBF, 0xBF, /* last three-byte */
        0xF0, 0x90, 0x80, 0x80, /* first four-byte */
        0xF4, 0x8F, 0xBF, 0xBF /* last codepoint */
};

static const byte invalid[] = {
        1, 0x80,
        1, 0xC0,
        1, 0xC1,
        2, 0xC0, 0x80,
        2, 0xC2, 0x00,
        2, 0xC2, 0x7F,
        2, 0xC2, 0xC0,
        3, 0xE0, 0x80, 0x80,
        3, 0xE0, 0x9F, 0xBF,
        3, 0xED, 0xA0, 0x80,
        3, 0xED, 0xBF, 0xBF,
        4, 0xF0, 0x80, 0x80, 0x80,
        4, 0xF0, 0x8F, 0xBF, 0xBF,
        4, 0xF4, 0x90, 0x80, 0x80
};

int
main() {
        const byte *p, *end;

        p = valid;
        end = valid + sizeof valid;
        assert_valid(&p, end, 0x000000);
        assert_valid(&p, end, 0x00007F);
        assert_valid(&p, end, 0x000080);
        assert_valid(&p, end, 0x0007FF);
        assert_valid(&p, end, 0x000800);
        assert_valid(&p, end, 0x00D7FF);
        assert_valid(&p, end, 0x00E000);
        assert_valid(&p, end, 0x00FFFF);
        assert_valid(&p, end, 0x010000);
        assert_valid(&p, end, 0x10FFFF);

        p = invalid;
        end = invalid + sizeof invalid;
        assert_invalid("80", &p, end);
        assert_invalid("C0", &p, end);
        assert_invalid("C1", &p, end);
        assert_invalid("C0 80", &p, end);
        assert_invalid("C2 00", &p, end);
        assert_invalid("C2 7F", &p, end);
        assert_invalid("C2 C0", &p, end);
        assert_invalid("E0 80 80", &p, end);
        assert_invalid("E0 9F BF", &p, end);
        assert_invalid("ED A0 80", &p, end);
        assert_invalid("ED BF BF", &p, end);
        assert_invalid("F0 80 80 80", &p, end);
        assert_invalid("F0 8F BF BF", &p, end);
        assert_invalid("F4 90 80 80", &p, end);

        return 0;
}

所以我会为您计数。(1) 我包含了 C 头文件而不是 C++ 头文件。(2) 我使用指针而不是引用。(3) 我没有使用命名空间,而是声明了我的函数为 static。(4) 我将循环变量声明为函数范围内的变量。但另一方面,我没有发明奇怪的类型名称(u_longu_char),也没有一致地使用它们(u_char vs. uchar)并且没有声明它们。我还成功地完全避免了任何类型转换(接受的答案大量使用这种方式,这也是 C 风格)。 - Roland Illig
@Roland:通过查看函数的签名?@DeadMG:公平地说,这被宣布为C语言解决方案。 - sbi
2
至少我不会抱怨你的Lua解决方案是糟糕的C++代码。(然而,我会抱怨我的C++编译器无法编译它。) - sbi
@DeadMG:是的,你确实可以。那又怎样? - sbi
@sbi: 关键在于,如果答案不是用原帖语言编写的话,那么这样的答案既没有用处也不可接受。 - Puppy
显示剩余5条评论

1
此外,wchar_t 是存储单个 Unicode 字符的正确类型吗?
在 Linux 上是的。在 Windows 上,wchar_t 表示一个 UTF-16 代码单元,这不一定是一个字符。
即将到来的 C++0x 标准将提供 char16_t 和 char32_t 类型,用于表示 UTF-16 和 UTF-32。
如果在 char32_t 不可用且 wchar_t 不足的系统上,请使用 uint32_t 存储 Unicode 字符。

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