【问题标题】:printf field width : bytes or chars?printf 字段宽度:字节还是字符?
【发布时间】:2011-02-17 01:35:00
【问题描述】:

printf/fprintf/sprintf 系列支持 其格式说明符中的宽度字段。我有个疑问 对于(非宽)字符数组参数的情况:

宽度字段应该表示字节还是字符?

如果 char 数组是什么(正确的事实)行为 对应于(比如说)一个原始的 UTF-8 字符串? (我知道通常我应该使用一些宽字符类型, 这不是重点)

例如,在

char s[] = "ni\xc3\xb1o";  // utf8 encoded "niño"
fprintf(f,"%5s",s);

该函数是否应该尝试仅输出 5 个字节 (纯 C 字符)(并且您承担错位的责任 或其他问题,如果两个字节导致文本字符)?

或者它是否应该尝试计算“文本字符”的长度 数组的? (解码它...根据当前的语言环境?) (在这个例子中,这相当于找出字符串有 4 个 unicode 字符,因此它会添加一个空间用于填充)。

更新:我同意答案,printf 系列不符合逻辑 将纯 C 字符与字节区分开来。问题是我的 glibc 似乎没有 充分尊重这一概念,如果之前已经设置了语言环境,并且如果 一个有(今天最常用的)LANG/LC_CTYPE=en_US.UTF-8

例子:

#include<stdio.h>
#include<locale.h>
main () {
        char * locale = setlocale(LC_ALL, ""); /* I have LC_CTYPE="en_US.UTF-8" */
        char s[] = {'n','i', 0xc3,0xb1,'o',0}; /* "niño" in utf8: 5 bytes, 4 unicode chars */
        printf("|%*s|\n",6,s); /* this should pad a blank - works ok*/
        printf("|%.*s|\n",4,s); /* this should eat a char - works ok */
        char s3[] = {'A',0xb1,'B',0}; /* this is not valid UTF8 */
        printf("|%s|\n",s3);     /* print raw chars - ok */
        printf("|%.*s|\n",15,s3);     /* panics (why???) */
}

因此,即使设置了非 POSIX-C 语言环境,printf 似乎仍然具有计算宽度的正确概念:字节(c 普通字符)而不是 unicode 字符。没关系。但是,当给定一个在他的语言环境中不可解码的 char 数组时,它会默默地恐慌(它中止 - 在第一个 '|' 之后没有打印任何内容 - 没有错误消息)......只有当它需要计算一些宽度时。我不明白为什么它甚至在不需要/必须的时候尝试从 utf-8 解码字符串。这是 glibc 中的错误吗?

使用 glibc 2.11.1 (Fedora 12) 测试(也是 glibc 2.3.6)

注意:这与终端显示问题无关 - 您可以通过管道检查输出到 od :$ ./a.out | od -t cx1 这是我的输出:

0000000   |       n   i 303 261   o   |  \n   |   n   i 303 261   |  \n
         7c  20  6e  69  c3  b1  6f  7c  0a  7c  6e  69  c3  b1  7c  0a
0000020   |   A 261   B   |  \n   |
         7c  41  b1  42  7c  0a  7c

更新 2(2015 年 5 月):这种可疑行为 has been fixed 在较新版本的 glibc(似乎是 2.17)中。使用glibc-2.17-21.fc19 对我来说没问题。

【问题讨论】:

    标签: c unicode glibc


    【解决方案1】:

    这将导致输出五个字节。和五个字符。在 ISO C 中,字符和字节之间没有区别。字节不一定是 8 位,而是定义为 char 的宽度。

    8 位值的 ISO 术语是八位字节。

    就 C 环境而言,您的“niño”字符串实际上是五个字符宽(当然,没有空终止符)。如果您的终端上只显示四个符号,那几乎可以肯定是终端的功能,而不是 C 的输出功能。

    我并不是说 C 实现不能处理 Unicode。如果 CHAR_BITS 定义为 32,它可以很容易地使用 UTF-32。UTF-8 会更难,因为它是可变长度编码,但几乎可以解决任何问题 :-)


    根据您的更新,您似乎遇到了问题。但是,我没有在具有相同语言环境设置的设置中看到您描述的行为。就我而言,我在最后两个 printf 语句中得到了相同的输出。

    如果您的设置只是在第一个 | 之后停止输出(我假设这就是您所说的中止,但如果您的意思是整个程序中止,那要严重得多),我会向GNU 提出问题(首先尝试您的特定发行版错误程序)。你已经完成了所有重要的工作,比如生成了一个最小的测试用例,所以如果你的发行版没有完全到达那里(大多数都没有),那么有人甚至应该很乐意在最新版本上运行它。


    顺便说一句,我不确定您检查od 输出是什么意思。在我的系统上,我得到:

    pax> ./qq | od -t cx1
    0000000   |       n   i 303 261   o   |  \n   |   n   i 303 261   |  \n
             7c  20  6e  69  c3  b1  6f  7c  0a  7c  6e  69  c3  b1  7c  0a
    0000020   |   A 261   B   |  \n   |   A 261   B   |  \n
             7c  41  b1  42  7c  0a  7c  41  b1  42  7c  0a
    0000034
    

    所以你可以看到输出流包含 UTF-8,这意味着它是终端程序必须解释它。 C/glibc 根本没有修改输出,所以也许我只是误解了你想说的。

    虽然我刚刚意识到你可能会说 你的 od 输出也只有该行的起始栏(不像我的似乎没有有问题),这意味着它 在 C/glibc 中出现问题,终端静默删除字符并没有问题(老实说,我希望终端删除整行或只是有问题的字符(即输出|A)-您刚刚得到|这一事实似乎排除了终端问题)。请澄清一下。

    【讨论】:

    • 你有 UTF-8 作为 LC_TYPE 吗?无论如何,我添加了我的输出。而且我想我刚刚将问题追溯到这个 glib 问题(不是错误......他们说)sources.redhat.com/bugzilla/show_bug.cgi?id=649 - 请参阅最后一条评论。太恶心了……
    • @leonbloy:您可以在错误评论中添加引用作为其他人更容易找到的答案。
    • 好的,我在自己的答案中发布了我的发现。
    【解决方案2】:

    字节(字符)。没有对 Unicode 语义的内置支持。您可以想象它导致至少 5 次调用 fputc

    【讨论】:

      【解决方案3】:

      您发现的是 glibc 中的一个错误。不幸的是,这是开发人员拒绝修复的故意问题。有关说明,请参见此处:

      http://www.kernel.org/pub/linux/libs/uclibc/Glibc_vs_uClibc_Differences.txt

      【讨论】:

        【解决方案4】:

        几个人正确地回答了最初的问题(字节还是字符?):根据规范和 glibc 实现,printf 中的宽度(或精度) em> C 函数 counts bytes (或纯 C 字符,它们是相同的)。所以,fprintf(f,"%5s",s) 在我的第一个示例中,绝对意味着 “尝试从数组 s 中输出至少 5 个字节(纯字符) - 如果不够,请用空格填充”.

        字符串(在我的示例中,字节长度为 5)是否表示以 -say- UTF8 编码的文本以及是否包含 4 个“文本(unicode)字符”并不重要。对于 printf(),在内部,它只有 5 个(普通)C 字符,这很重要。

        好的,这看起来很清楚。但这并不能解释我的其他问题。那么我们一定是遗漏了什么。

        在 glibc bug-tracker 中搜索,我发现了一些相关的(相当老的)问题 - 我不是第一个被此功能发现的问题:

        http://sources.redhat.com/bugzilla/show_bug.cgi?id=6530

        http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=208308

        http://sources.redhat.com/bugzilla/show_bug.cgi?id=649

        最后一个链接中的这句话在这里特别相关:

        ISO C99 requires for %.*s to only write complete characters that fit below the
        precision number of bytes.  If you are using say UTF-8 locale, but ISO-8859-1
        characters as shown in the input file you provided, some of the strings are
        not valid UTF-8 strings, therefore sprintf fails with -1 because of the
        encoding error. That's not a bug in glibc.
        

        这是否是一个错误(可能是在解释中或在 ISO 规范本身中)是有争议的。 但是 glibc 正在做什么现在很清楚了。

        回想一下我有问题的陈述:printf("|%.*s|\n",15,s3)。在这里,glibc 必须找出s3 的长度是否大于15,如果是,则截断它。为了计算这个长度,它根本不需要弄乱编码。但是,如果它必须被截断,glibc 会努力小心:如果它只保留前 15 个字节,它可能会将多字节字符分成两半,从而产生无效的文本输出(I'可以接受 - 但 glibc 坚持其好奇的 ISO C99 解释)。 因此,不幸的是,需要使用环境语言环境对 char 数组进行解码,以找出真正的字符边界在哪里。因此,例如,如果 LC_TYPE 表示 UTF-8 并且数组不是有效的 UTF-8 字节序列,它会中止(还不错,因为 printf 返回 -1 ;不太好,因为它打印了无论如何都是字符串,所以很难干净地恢复)。

        显然只有在这种情况下,当为字符串指定精度并且有可能被截断时,glibc 需要将一些 Unicode 语义与纯字符/字节语义混合。相当丑陋,IMO,但确实如此。

        更新:请注意,此行为不仅适用于原始编码无效的情况,还适用于截断后的无效代码。例如:

        char s[] = "ni\xc3\xb1o";  /* "niño" in UTF8: 5 bytes, 4 unicode chars */
        printf("|%.3s|",s); /* would cut the double-byte UTF8 char in two */
        

        这会将字段截断为 2 个字节,而不是 3 个字节,因为它拒绝输出无效的 UTF8 字符串:

        $ ./a.out
        |ni|
        $ ./a.out | od -t cx1
        0000000   |   n   i   |  \n
                7c 6e 69 7c 0a
        

        更新(2015 年 5 月)这个 (IMO) 有问题的行为已在新版本的 glib 中更改(修复)。请参阅主要问题。

        【讨论】:

        • 您描述和引用评论的glibc(不是glib)行为是一个故意的错误。 C99 要求或允许实现将字符串剪切成比精度更短的字符串以避免写入“部分字符”。尽管 格式字符串 必须是一个有效的多字节字符串,%s 纯粹是根据字节指定的,而不是多字节字符。 glibc 开发人员错误引用的指定此错误行为的文本来自描述 %ls 行为的不同部分(用于 wchar_t 字符串),而不是 %s
        【解决方案5】:

        为了便于移植,使用mbstowcs 转换字符串并使用printf( "%6ls", wchar_ptr ) 打印。

        %ls 是根据POSIX 的宽字符串的说明符。

        没有“事实上的”标准。通常,如果操作系统和语言环境已配置为将其视为 UTF-8 文件,我希望 stdout 接受 UTF-8,但我希望 printf 不知道多字节编码,因为它没有定义在这些方面。

        【讨论】:

          【解决方案6】:

          除非您还确保 wchar_t 至少为 32 位长,否则不要使用 mbstowcs。 否则你可能会最终使用 UTF-16,它具有 UTF-8 的所有缺点和 UTF-32 的所有缺点。

          我不是说避免使用 mbstowcs,我只是说不要让 Windows 程序员使用它。

          使用 iconv 转换为 UTF-32 可能更简单。

          【讨论】:

          • mbstowcs 被指定为对mbtowc 的多次调用,并且由于 API 的工作方式,后者 不能 输出 UTF-16 代理项。如果 Windows 的 mbstowcs 输出 UTF-16,则它不符合标准。使用 16 位 wchar_t 的一致实现本质上仅限于 BMP。
          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2013-03-09
          • 2011-08-24
          • 1970-01-01
          • 2014-09-04
          • 2018-03-12
          • 1970-01-01
          相关资源
          最近更新 更多