【问题标题】:Compatibility of printf with utf-8 encoded stringsprintf 与 utf-8 编码字符串的兼容性
【发布时间】:2019-11-07 10:04:08
【问题描述】:

我正在尝试使用 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 有据可查的限制?

【问题讨论】:

    标签: gcc unicode utf-8 printf glibc


    【解决方案1】:

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

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

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

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

    【讨论】:

    • 那么使用wchar.h的效果是不是链接了不同的printf()
    • @u.windl:头文件和库是两个完全不同的东西。您声明的内容对链接的内容没有影响:printfprintf,无论您使用什么标头。但是wchar.h声明wprintf和朋友,确实是不同的功能。
    • 所以你的答案忘了提到要使用wprintf()
    • @U.Windl:这就是“wchar.h 格式化输出函数”的意思。
    • @U.Windl:我添加了文档链接。也许这样会更清楚。
    【解决方案2】:

    让我引用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 字符进行测试是有意义的。古希腊字符看起来足够“宽”,可以看出使用固定宽度字体需要多少空间。输出可能如下所示:

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

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

    【讨论】:

      猜你喜欢
      • 2011-07-14
      • 2012-01-14
      • 1970-01-01
      • 1970-01-01
      • 2014-03-18
      • 1970-01-01
      • 2011-05-26
      • 2011-12-14
      • 2016-04-27
      相关资源
      最近更新 更多