printf中的宽度说明符与带重音符号的字符不兼容。

7

我正在尝试使用宽度限定符和printf函数格式化c语言中一些字符串的输出。然而,我遇到了一些问题,无法得到期望的行为。似乎每次printf遇到字符å、ä或ö时,字符串所保留的宽度就会减少一个位置。

以下是一个代码片段以说明:

#include <stdio.h>

int main(void)
{
  printf(">%-10s<\n", "aoa");
  printf(">%-10s<\n", "aäoa");
  printf(">%-10s<\n", "aäoöa");
  printf(">%-10s<\n", "aäoöaå");

  return 0;
}

我的Ubuntu Linux Bash Shell中的输出。

>aoa       <
>aäoa     <
>aäoöa   <
>aäoöaå <

我希望能得到有关如何处理这个问题的建议。我的要求是上面片段中的所有字符串都以10个字符为宽度进行空格填充,并按照以下方式打印:

>aoa       <
>aäoa      <
>aäoöa     <
>aäoöaå    <

我也希望能够了解为什么会发生这种情况,如果其他设置没有这个问题,也请提供反馈意见。

你是否正在使用UTF-8编码?这些字符需要2个字节,而printf可能不支持UTF-8。 - user694733
1
https://dev59.com/xmUp5IYBdhLWcg3wF0dq - 123
@user694733 是的,我正在使用utf-8。 - Erik Göök
3个回答

7

使用宽字符串和 wprintf

#include <cwchar>
#include <locale.h>

int main(void)
{
  // seems to be needed for the correct output encoding
  setlocale(LC_ALL, "");

  wprintf(L">%-10ls<\n", L"aoa");
  wprintf(L">%-10ls<\n", L"aäoa");
  wprintf(L">%-10ls<\n", L"aäoöa");
  wprintf(L">%-10ls<\n", L"aäoöaå");

  return 0;
}

我在示例中使用了字面字符串来保持简洁。在我的实际问题中,我从结构中获取字符串。我想我得使用mbstowcs()或类似的方法将这些字符串转换为宽字符字符串?我的意思是,我显然不能这样做:wprintf(L">%-10ls<\n", Lsome->member); - Erik Göök

6

为什么会发生这种情况?

请查看《每个软件开发者绝对必须知道的有关 Unicode 和字符集的绝对最低限度》

除了使用宽字符和 UTF8,您可以使用此函数计算非 ASCII 字符的数量,然后将结果添加到 printf 的宽度说明符中作为替代方法:

#include <stdio.h>

int func(const char *str)
{
    int len = 0;

    while (*str != '\0') {
        if ((*str & 0xc0) == 0x80) {
            len++;
        }
        str++;
    }
    return len;
}

int main(void)
{
    printf(">%-*s<\n", 10 + func("aoa"), "aoa");
    printf(">%-*s<\n", 10 + func("aäoa"), "aäoa");
    printf(">%-*s<\n", 10 + func("aäoöa"), "aäoöa");
    printf(">%-*s<\n", 10 + func("aäoöaå"), "aäoöaå");
    return 0;
}

输出:

>aoa       <
>aäoa      <
>aäoöa     <
>aäoöaå    <

1
尽管我认为在长期运行中使用wprintf更合理,但最终我还是采用了您的建议。这个链接非常有用。我将其标记为已接受。 - Erik Göök
请注意,此方法仅适用于显示宽度为1的字符。 - Cyker
@Cyker,你说得对,但这更与终端字体有关,即使使用宽字符:wprintf(L">%-10ls<\n", L"aäoöa包"); 输出也无法与其他行对齐。 - David Ranieri
1
@KeineLust 几乎所有字体都以双倍宽度显示CJK字符。除了这些字符,还有一些符号根据所使用的字体而单倍或双倍显示。我曾经遇到过类似的问题,考虑过这种方法,直到我需要处理CJK字符。但是OP要求重音字符。我不是语言专家,但我认为在所有主要语言中它们可能都是单倍宽度。 - Cyker

3

Alter Mann's accepted answer基本正确,但不应该只是硬编码一个自定义函数来计算多字节字符串中未编码为可见字符的字节数:您应该使用setlocale(LC_ALL, "")或类似方法来本地化代码,并使用strlen(str) - mbstowcs(NULL, str, 0)来计算字符串中未编码为可见字符的字节数。

setlocale()是标准C(C89、C99、C11)中的一部分,也在POSIX.1中定义。mbstowcs()是标准C99和C11中的一部分,并且也在POSIX.1中定义。两者都在Microsoft C库中实现,因此基本上可以在任何地方使用。

考虑以下示例程序,它打印在命令行上指定的C字符串:

#include <stdlib.h>
#include <string.h>
#include <locale.h>
#include <stdio.h>

/* Counts the number of (visible) characters in a string */
static size_t ms_len(const char *const ms)
{
    if (ms)
        return mbstowcs(NULL, ms, 0);
    else
        return 0;
}

/* Number of bytes that do not generate a visible character in a string */
static size_t ms_extras(const char *const ms)
{
    if (ms)
        return strlen(ms) - mbstowcs(NULL, ms, 0);
    else
        return 0;
}

int main(int argc, char *argv[])
{
    int arg;

    /* Default locale */
    setlocale(LC_ALL, "");

    for (arg = 1; arg < argc; arg++)
        printf(">%-*s< (%zu bytes; %zu chars; %zu bytes extra in wide chars)\n",
               (int)(10 + ms_extras(argv[arg])), argv[arg],
               strlen(argv[arg]), ms_len(argv[arg]), ms_extras(argv[arg]));

    return EXIT_SUCCESS;
}

如果您将上述内容编译为example,并运行。
./example aaa aaä aää äää aa€ a€€ €€€ a ä € 

该程序将输出

>aaa       < (3 bytes; 3 chars; 0 bytes extra in wide chars)
>aaä       < (4 bytes; 3 chars; 1 bytes extra in wide chars)
>aää       < (5 bytes; 3 chars; 2 bytes extra in wide chars)
>äää       < (6 bytes; 3 chars; 3 bytes extra in wide chars)
>aa€       < (5 bytes; 3 chars; 2 bytes extra in wide chars)
>a€€       < (7 bytes; 3 chars; 4 bytes extra in wide chars)
>€€€       < (9 bytes; 3 chars; 6 bytes extra in wide chars)
>a         < (1 bytes; 1 chars; 0 bytes extra in wide chars)
>ä         < (2 bytes; 1 chars; 1 bytes extra in wide chars)
>€         < (3 bytes; 1 chars; 2 bytes extra in wide chars)
>         < (4 bytes; 1 chars; 3 bytes extra in wide chars)

如果最后一个<与其他字符不对齐,那是因为所使用的字体不是固定宽度:表情符号比普通字符如Ä更宽。这就是全部原因。怪字体吧。
最后一个字符是U+1F608带角笑脸,来自Emoticons unicode block,如果您的操作系统/浏览器/字体无法显示它。在Linux中,我拥有的所有终端(包括控制台(非图形系统控制台))中,上述所有><都正确对齐,尽管控制台字体没有表情符号的字形,而只是显示成了菱形。
Alter Mann's answer不同,这种方法是可移植的,并且不假设当前用户实际使用的字符集是什么。

很好的回答,您对可移植性是完全正确的,我的函数假定为UTF8。 - David Ranieri

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