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

4

printf/fprintf/sprintf系列函数支持在格式说明符中使用宽度字段。对于(非宽字符)char数组参数,我有一个疑问:

宽度字段是指字节还是字符?

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

例如,在

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

这个函数是试图输出仅有5个字节(普通C字符)吗?(如果两个字节结果是文本字符,则您需要承担不对齐或其他问题的责任)?
还是它试图计算数组的“文本字符”长度?(根据当前区域设置进行解码?)
(在此示例中,这将导致找出字符串有4个Unicode字符,因此会添加填充空格。)
更新:我同意答案,printf系列不区分普通C字符和字节是合理的。问题是,如果先前设置了区域设置,并且如果使用(今天最常用的)LANG/LC_CTYPE=en_US.UTF-8,则我的glibc似乎没有完全遵守这个概念。
实例:
#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 plain chars)而不是Unicode字符为单位。这很好。但是,当给出一个在他的语言环境中无法解码的字符数组时,它会静默恐慌(它会中止 - 第一个“|”后没有打印任何内容 - 没有错误消息)......只有当它需要计算某些宽度时才会发生。我不明白为什么它甚至尝试从utf-8解码字符串,当它不需要/没有必要。这是glibc的bug吗?
测试使用glibc 2.11.1(Fedora 12)(也是glibc 2.3.6)
注意:这与终端显示问题无关 - 您可以通过管道输出进行检查:$ ./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(从2.17开始)已经修复了这种可疑行为(详见此处)。在我使用的glibc-2.17-21.fc19版本中,它可以正常工作。

6个回答

4

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

ISO术语中,8位值被称为八位组(octet)。

在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)——你只得到|似乎排除了终端问题)。请澄清一下。

1
你的 LC_TYPE 是否设置为 UTF-8?无论如何,我已经添加了我的输出。而且我认为我刚刚把问题追溯到这个 glib 问题(不是 bug...他们说)http://sources.redhat.com/bugzilla/show_bug.cgi?id=649 - 请见最后一条评论。那很糟糕... - leonbloy
@leonbloy:你可以将错误评论中的引用添加为答案,以便其他人更容易找到。 - jfs
好的,我在我的回答中发布了我的发现。 - leonbloy

3

字节(字符)。没有内置的Unicode语义支持。您可以将其想象为至少会导致调用五次fputc


1

原始问题(字节还是字符?)得到了几位正确回答:根据规范和实现,printf C函数中的宽度(或精度)计算字节(或普通C字符,它们是相同的)。因此,在我的第一个示例中,fprintf(f,"%5s",s)绝对意味着“尝试输出至少5个字节(普通字符)从数组s - 如果不够,则用空格填充”

无论字符串(在我的示例中,字节长度为5)是否表示以UTF8编码的文本,并且实际包含4个“文本(Unicode)字符”,都无关紧要。对于printf()来说,内部只有5个(普通)C字符,这就是重点。

好的,这似乎非常清楚。但这并不能解释我的另一个问题。那么我们一定漏掉了什么。

在glibc错误跟踪器中搜索,我发现了一些相关的(相当古老的)问题 - 我不是第一个被这个...特性困住的人:

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.

这个问题是否是一个 bug(也许是在解释或 ISO 规范本身中)还有待商榷。但是现在 glibc 的做法已经很清楚了。

回想一下我的有问题的语句:printf("|%.*s|\n",15,s3)。在这里,glibc 必须找出 s3 的长度是否大于 15,并且如果是,则将其截断。为了计算这个长度,它根本不需要处理编码。但是,如果必须截断,glibc 就会努力小心:如果它只保留前 15 个字节,它可能会将多字节字符分成两半,从而产生无效的文本输出(我可以接受这种情况 - 但是 glibc 坚持其奇怪的 ISO C99 解释)。因此,它不幸地需要解码 char 数组,使用环境区域设置来查找真正的字符边界。因此,例如,如果 LC_TYPE 说 UTF-8 而数组不是有效的 UTF-8 字节序列,则会中止(不太好,因为然后 printf 返回 -1;不太好,因为它仍然打印字符串的一部分,所以很难恢复干净)。

显然,只有在为字符串指定精度并存在截断可能性的情况下,glibc才需要将一些Unicode语义与普通字符/字节语义混合使用。在我看来相当丑陋,但事实就是如此。

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

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月):在较新版本的glib中,这种(我认为)可疑的行为已经被更改(修复)。请参见主要问题。


1
你所描述并引用评论的glibc(不是glib)行为是一个有意的错误。C99 不允许或允许实现将字符串截短到精度以下以避免写入“部分字符”。尽管格式字符串必须是有效的多字节字符字符串,但%s仅按字节指定,从未涉及多字节字符。glibc开发人员错误引用的文本描述了%ls(用于wchar_t字符串)的行为,而不是%s - R.. GitHub STOP HELPING ICE

1

0
为了实现可移植性,使用mbstowcs将字符串转换后再用printf("%6ls", wchar_ptr)打印输出。 %lsPOSIX中宽字符串的格式说明符。
通常并没有"事实上"标准。如果操作系统及语言环境已被配置为将其视为UTF-8文件,则我期望stdout可以接受UTF-8编码,但我认为printf对于多字节编码一无所知,因为在这些方面没有明确定义。

0

除非您确保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。 - R.. GitHub STOP HELPING ICE

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接