printf字段宽度:字节还是字符?

时间:2010-05-08 01:21:39

标签: c unicode glibc

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

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

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

例如,在

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

该函数是否应该尝试输出5个字节 (普通C chars)(并且你承担了错位的责任 或两个字节导致文本字符的其他问题)?

或者是否应该尝试计算“文本字符”的长度 阵列? (根据当前的语言环境对其进行解码?) (在这个例子中,这相当于发现字符串有 4个unicode字符,因此它会为填充添加一个空格。)

更新:我同意答案,printf家族不合逻辑 将普通C字符与字节区分开来。问题是我的glibc doest似乎没有 完全尊重这个概念,如果先前已经设置了区域设置,并且如果 一个人拥有(今天最常用的)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 似乎有正确的概念来计算width:bytes(c plain chars)而不是unicode chars。没关系。但是,当给定一个在其语言环境中无法解码的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月):在较新版本的glibc中出现了这个有问题的行为has been fixed(似乎是2.17)。使用glibc-2.17-21.fc19它对我来说没问题。

6 个答案:

答案 0 :(得分:3)

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

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

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

我不是说C实现无法处理Unicode。如果CHAR_BITS被定义为32,它可以很容易地执行UTF-32.由于UTF-8是一个可变长度编码,但它几乎可以解决任何问题: - )


根据您的更新,您可能会遇到问题。但是,我没有在我的设置中看到您描述的行为具有相同的区域设置。在我的例子中,我在最后两个printf语句中得到了相同的输出。

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


顺便说一句,通过查看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) - 您刚刚获得|这一事实似乎排除了终端问题)。请澄清一下。

答案 1 :(得分:2)

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

答案 2 :(得分:1)

原始问题(字节或字符?)由几个人正确回答:根据规范和 glibc 实现, printf中的宽度(或精度) C函数计数字节(或普通C字符,这是相同的事情)。因此,在我的第一个示例中,fprintf(f,"%5s",s)意味着绝对“尝试从数组中输出至少 5个字节(普通字符) - 如果不够,则填充空白”< / em>的

字符串(在我的例子中,字节长度为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语义与plain-chars / bytes语义混合。相当丑陋,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 */

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

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

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

答案 3 :(得分:1)

你发现的是glibc中的一个错误。不幸的是,这是一个有意的,开发人员拒绝修复。请参阅此处获取说明:

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

答案 4 :(得分:0)

要便携,请使用mbstowcs转换字符串,然后使用printf( "%6ls", wchar_ptr )进行打印。

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

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

答案 5 :(得分:0)

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

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

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