4

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 月):这种有问题的行为在较新版本的 glibc 中得到修复(似乎从 2.17 开始)。有了glibc-2.17-21.fc19它对我来说没问题。

4

6 回答 6

4

它将导致输出五个字节。和五个字符。在 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 中有问题,不是有问题终端默默地丢弃字符(老实说,我希望终端丢弃整行或只是有问题的字符(即 output |A) - 你刚刚得到的事实|似乎排除了终端问题)。请澄清这一点。

于 2010-05-08T01:48:12.060 回答
3

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

于 2010-05-08T01:23:41.040 回答
1

几个人正确回答了最初的问题(字节还是字符?):根据规范和glibc实现, printf C 函数中的宽度(或精度)计算字节(或纯 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 个字节,它可能会将多字节字符分成两半,从而产生无效的文本输出(我可以接受 - 但是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 */

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 中更改(修复)。见主要问题。

于 2010-05-09T03:07:03.710 回答
1

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

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

于 2010-09-20T03:29:19.350 回答
0

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

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

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

于 2010-05-08T23:16:19.120 回答
0

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

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

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

于 2010-05-13T09:51:13.033 回答