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

时间:2019-06-25 14:19:45

标签: gcc unicode utf-8 printf glibc

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

我的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的局限性?

2 个答案:

答案 0 :(得分:1)

printf的确计算字节,而不是多字节字符。如果是错误,则该错误是C标准的,而不是glibc(通常与gcc结合使用的标准库实现)。

为了公平起见,对字符进行计数也不能帮助您对齐unicode输出,因为即使使用固定宽度的字体,unicode字符也不都是相同的显示宽度。 (例如,许多代码点的宽度为0。)

我不会试图证明这种行为是“有据可查的”。标准C的语言环境功能从来没有特别适合该任务,恕我直言,而且它们也从未得到过特别详尽的记录,部分原因是基础模型试图包含这么多可能的编码,而没有在具体示例中扎根,几乎不可能解释。 (...长号已删除...)

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

答案 1 :(得分:0)

让我引用rici:的确,printf会计算字节,而不是多字节字符。如果是错误,则该错误是C标准的,而不是glibc(通常与gcc结合使用的标准库实现)。

但是,请勿混淆wchar_tUTF-8。请参阅wikipedia以掌握前者的含义。相反,UTF-8几乎可以当作旧的ASCII来处理。只是要避免在字符中间被截断。

为了获得对齐,您要计算字符数。然后,将字节数传递给printf。这可以通过使用*精度并传递字节数来实现。例如,由于带重音符号的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;
    }

最后一个测试使用?,希腊语的希腊语thespian三百(U + 1016B)字符。考虑到计数的工作原理,使用连续的非ASCII字符进行测试是有意义的。古希腊字符看起来“很宽”,足以看到使用固定宽度字体需要多少空间。输出可能看起来像:

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

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