printf 与 utf-8 编码字符串的兼容性

Compatibility of printf with utf-8 encoded strings

我正在尝试使用 printf 函数在 C 代码 (char *) 中格式化一些 utf-8 编码的字符串。我需要指定格式的长度。当参数字符串中没有多字节字符时一切正常,但是当数据中有一些多字节字符时结果似乎不正确。

我的 glibc 有点旧 (2.17),所以我尝试了一些在线编译器,结果是一样的。

#include <stdlib.h>
#include <locale.h>

int main(void)
{
    setlocale( LC_CTYPE, "en_US.UTF-8" );
    setlocale( LC_COLLATE, "en_US.UTF-8" );

    printf( "'%-4.4s'\n",   "elephant" );
    printf( "'%-4.4s'\n",   "éléphant" );
    printf( "'%-20.20s'\n", "éléphant" );

    return 0;
}

Result of execution is :

'elep'
'él�'
'éléphant          '

第一行正确(输出中有 4 个字符)

第二行显然是错误的(至少从人的角度来看)

最后一行也是错误的:只写了 18 个 unicode 字符而不是 20 个

似乎 printf 函数在 UTF-8 解码之前计算字符数(计算字节数而不是 unicode 字符数)

这是 glibc 中的错误还是 printf 的有据可查的限制?

确实 printf 计算字节数,而不是多字节字符。如果是bug,那么bug是在C标准中,而不是在glibc(通常与gcc结合使用的标准库实现)中。

公平地说,计算字符数也无助于对齐 unicode 输出,因为即使使用 fixed-width 字体,unicode 字符的显示宽度也不完全相同。 (例如,许多代码点的宽度为 0。)

我不会试图争辩这种行为是 "well-documented"。恕我直言,标准 C 的语言环境设施从来都不是特别适合这项任务,而且它们从来没有被特别详细地记录过,部分原因是底层模型试图包含如此多可能的编码,而没有将自己置于一个具体的例子中,这几乎是不可能的解释。 (……长篇大论删了……)

您可以使用 wchar.h formatted output functions, 以宽字符计。 (这仍然不会为您提供正确的输出对齐方式,但它会按照您期望的方式计算精度。)

让我引用rici:printf 确实计算字节数,而不是多字节字符。如果是bug,那么bug是在C标准中,而不是在glibc(通常与gcc结合使用的标准库实现)中。

但是,不要混淆 wchar_tUTF-8。见wikipedia,把握前者的意思。相反,UTF-8 几乎可以像处理旧的 ASCII 一样处理。只是避免在字符中间截断。

为了对齐,您需要计算字符数。然后,将字节数传递给 printf。这可以通过使用 * 精度并传递字节数来实现。例如,由于 accented e 占用两个字节:

    printf("'-4.*s'\n", 6, "éléphant");

根据 format of UTF-8 characters:

很容易编写计算字节数的函数
    static int count_bytes(char const *utf8_string, int length)
    {
        char const *s = utf8_string;
        for (;;)
        {
            int ch = *(unsigned char *)s++;
            if ((ch & 0xc0) == 0xc0) // first byte of a multi-byte UTF-8
                while (((ch = *(unsigned char*)s) & 0xc0) == 0x80)
                    ++s;
            if (ch == 0)
                break;
            if (--length <= 0)
                break;
        }
        return s - utf8_string;
    }

然而,在这一点上,最终会得到这样的行:

    printf("'-4.*s'\n", count_bytes("éléphant", 4), "éléphant");

必须快速重复字符串两次成为维护的噩梦。至少,可以定义一个宏来确保字符串相同。假设上述函数保存在某个 utf8-util.h 文件中,您的程序可以重写如下:

    #include <stdio.h>
    #include <stdlib.h>
    #include <locale.h>
    #include "utf8-util.h"

    #define INT_STR_PAIR(i, s) count_bytes(s, i), s
    int main(void)
    {
        setlocale( LC_CTYPE, "en_US.UTF-8" );
        setlocale( LC_COLLATE, "en_US.UTF-8" );

        printf( "'%-4.*s'\n",  INT_STR_PAIR(4, "elephant"));
        printf( "'%-4.*s'\n",  INT_STR_PAIR(4, "éléphant"));
        printf( "'%-4.*s'\n",  INT_STR_PAIR(4, "ééphant"));
        printf( "'%-20.*s'\n", INT_STR_PAIR(20, "éléphant"));

        return 0;
    }

最后一个测试使用,希腊首音戏剧三百 (U+1016B) 字符。鉴于计数的工作原理,使用连续的非 ASCII 字符进行测试是有意义的。古希腊字符看起来"wide"足矣,可见使用定宽字体需要多少space。输出可能如下所示:

    'elep'
    'élép'
    'éép'
    'éléphant          '

(在我的终端上,那些 4 个字符的字符串长度相等。)