使用printf()让字符串居中

33

默认情况下,printf() 似乎会将字符串向右对齐。

printf("%10s %20s %20s\n", "col1", "col2", "col3");
/*       col1                 col2                 col3 */

我也可以像这样将文本对齐到左侧:

printf("%-10s %-20s %-20s", "col1", "col2", "col3");

有没有快速居中文本的方法?或者我必须编写一个函数,将类似于 test 的字符串转换为 (space)(space)test(space)(space),如果该列的文本宽度为8?

10个回答

36

printf本身无法完成这个技巧,但您可以使用“间接”宽度进行操作,该宽度通过从参数中读取来指定。让我们尝试一下(好吧,不是完美的)

void f(char *s)
{
        printf("---%*s%*s---\n",10+strlen(s)/2,s,10-strlen(s)/2,"");
}
int main(int argc, char **argv)
{
        f("uno");
        f("quattro");
        return 0;
}

我在我编写的用于创建表格的一些C++代码中进行了测试,它截断了有效文本,我不得不找出原因。基于我解决问题的方式,我提供了另一种替代答案。 - clearlight
确实,我警告说这个解决方案“不完美”,因为它包含硬编码的常量。我的目标只是给出一个提示,你的解决方案完成了这个想法。 - Giuseppe Guerrini
* 需要一个 intstrlen() 将返回 size_t。当 SIZE_MAX>INT_MAX 时,此函数会导致 UB。可能的解决方案:将其更改为 (int) ((10+strlen(s)/2)&INT_MAX) - 12431234123412341234123
正确。在大多数情况下,将strlen直接转换为int就足够了。一个被设计为居中的字符串很少会超过INT_MAX的长度。 - Giuseppe Guerrini

14

@GiuseppeGuerrini的建议是有帮助的,他提出了如何使用打印格式说明符并将空白字符分隔开的建议,但不幸的是,这可能会截断文本。

以下解决了截断问题(假设指定的字段足够大以容纳文本)。

void centerText(char *text, int fieldWidth) {
    int padlen = (fieldWidth - strlen(text)) / 2;
    printf("%*s%s%*s\n", padLen, "", text, padlen, "");
} 

3
请注意,根据fieldWidth-strlen(text)是偶数还是奇数,最终的总宽度会有所不同。 - rtpax
添加了一个修改版,它始终打印 fieldWidth 个字符(例如用于表格等)。 - zeawoas
1
(fieldWidth - strlen(text)) / 2; 的结果超出 int 范围时,其行为是由实现定义的。当 SIZE_MAX>INT_MAX 并且 strlen(text)>fieldWidth 时,可能会出现这种情况,导致 (fieldWidth - strlen(text)) == SIZE_MAX-(strlen(text)-fieldWidth-1) - 12431234123412341234123
@12431234123412341234123 这让我想起了一句古老的格言:“仅仅因为你能够做某事,并不意味着你应该这样做。” - clearlight

2

有两种解决方案,第一种类似于上述方法,通过在printf中插入宏来实现;第二种是自定义宏,通过snprintf提前计算格式化字符串的长度,然后调用printf函数进行输出。

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

#define LEFT(str, w) \
    ({int m = w + strlen(str); m % 2 ? (m + 1) / 2 : m / 2;})

#define RIGHT(str, w) \
({ int m = w - strlen(str); m % 2 ? (m - 1) / 2 : m / 2; })

#define STR_CENTER(str, width) \
    LEFT(str, width), str, RIGHT(str, width), ""

#define PRINTF_CENTER(width, start, fmt, end, ...) ({ \
    int n = snprintf(NULL, 0, fmt, __VA_ARGS__);     \
    int m = width - n;                               \
    int left = m % 2 ? (m + 1) / 2 : m / 2;          \
    int right = m % 2 ? (m - 1) / 2 : m / 2;         \
    printf(start "%*s" fmt "%*s" end, left, "",      \
           __VA_ARGS__, right, "");                  \
})

#define MYFORMAT_CENTER(width, fmt, ...)  \
    PRINTF_CENTER(40, "[", fmt  , "]\n", __VA_ARGS__)

int main(int argc, char const *argv[])
{
    printf("%*s%*s\n\n", STR_CENTER("--- Hello World ---", 40));
    printf("[%*s%*s]\n", STR_CENTER("I am okay today", 40));
    
    MYFORMAT_CENTER(40, "%d, e is %f", 1, 2.71828);
    MYFORMAT_CENTER(40, "%d, pi is %f", 2, 3.1415926);
    MYFORMAT_CENTER(40, "%s %d.", "This is such a long string that it exceeds the given size:", 40);

    return 0;
}

输出:

           --- Hello World ---

[             I am okay today            ]
[            1, e is 2.718280            ]
[            2, pi is 3.141593           ]
[           This is such a long string that it exceeds the given size: 40.           ]

2

没有 printf() 格式说明符可以居中文本。

您需要编写自己的函数或找到一个提供所需功能的库。


9
你的机会去做更多,提供解决方案,嗯? - clearlight

2
你可以尝试编写自己的函数来解决这个问题。
/**
 * Returns a sting "str" centered in string of a length width "new_length".
 * Padding is done using the specified fill character "placeholder".
 */
char *
str_center(char str[], unsigned int new_length, char placeholder)
{
    size_t str_length = strlen(str);

    // if a new length is less or equal length of the original string, returns the original string
    if (new_length <= str_length)
        return str;

    char *buffer;
    unsigned int i, total_rest_length;

    buffer = malloc(sizeof(char) * new_length);

    // length of a wrapper of the original string
    total_rest_length = new_length - str_length;

    // write a prefix to buffer
    i = 0;
    while (i < (total_rest_length / 2)) {
        buffer[i] = placeholder;
        ++i;
    }
    buffer[i + 1] = '\0';

    // write the original string
    strcat(buffer, str);

    // write a postfix to the buffer
    i += str_length;
    while (i < new_length) {
        buffer[i] = placeholder;
        ++i;
    }
    buffer[i + 1] = '\0';

    return buffer;
}

结果:

puts(str_center("A", 0, '-')); // A
puts(str_center("A", 1, '-')); // A
puts(str_center("A", 10, '-')); // ----A-----
puts(str_center("text", 10, '*')); // ***text***
puts(str_center("The C programming language", 26, '!')); // The C programming language
puts(str_center("The C programming language", 27, '!')); // The C programming language!
puts(str_center("The C programming language", 28, '!')); // !The C programming language!
puts(str_center("The C programming language", 29, '!')); // !The C programming language!!
puts(str_center("The C programming language", 30, '!')); // !!The C programming language!!
puts(str_center("The C programming language", 31, '!')); // !!The C programming language!!!

1
很酷,它是一个仅使用c/stdlib实现的教育C语言的例子,但是至少应该用memcpy()bcopy()替换前缀/后缀循环。 - clearlight
用alloca替换malloc... 呃呃 - Martin
alloca在这种情况下不起作用,因为返回的字符串就在那个内存中 - 你不希望它在'str_center'返回的时候被释放!但正如Martin指出的那样,按照现有的写法,这会导致内存泄漏。 - ColinB

1

在处理类似问题时,我尝试将表头居中在带有printf的行中,接下来会提供一些宏,在文本前/后打印这些宏,无论文本本身的长度如何都会对齐。请注意,如果我们有奇数长度的字符串,由于正常的除法会导致缺少空间,因此我们将无法对齐。因此需要进行四舍五入,我认为这是解决该问题的优雅方式:

#define CALC_CENTER_POSITION_PREV(WIDTH, STR) (((WIDTH + ((int)strlen(STR))) % 2) \
       ? ((WIDTH + ((int)strlen(STR)) + 1)/2) : ((WIDTH + ((int)strlen(STR)))/2))
#define CALC_CENTER_POSITION_POST(WIDTH, STR) (((WIDTH - ((int)strlen(STR))) % 2) \
       ? ((WIDTH - ((int)strlen(STR)) - 1)/2) : ((WIDTH - ((int)strlen(STR)))/2))

使用示例:

printf("%*s%*s" , CALC_CENTER_POSITION_PREV(MY_COLUMN_WIDTH, "Header")
                , "Header"
                , CALC_CENTER_POSITION_POST(MY_COLUMN_WIDTH, "Header"), "");

0
是的,你要么得自己编写一个返回 " test " 等的函数,例如。
printf("%s %s %s", center("col1", 10), center("col2", 20), center("col3", 20));

或者你有一个 center_print 函数,类似以下代码:

void center_print(const char *s, int width)
{
        int length = strlen(s);
        int i;
        for (i=0; i<=(width-length)/2; i++) {
                fputs(" ", stdout);
        }
        fputs(s, stdout);
        i += length;
        for (; i<=width; i++) {
                fputs(" ", stdout);
        }
}

1
第一个建议:如何在不泄漏内存的情况下实现这个? - kevinarpe
如果您根据一些看起来不太过分的标准(例如一个 printf 最多只居中 20 个参数,而且所有居中的结果都不超过 200 字节),预先分配一些缓冲区,那么您可以让 center 函数在每次调用时旋转缓冲区。 - hlovdal

0

如果你想使用printf()格式字符串,并且接受被限制于GNU clib,那么你可以通过添加自己的转换说明符来扩展printf()以使字符串居中。使用register_printf_function()添加转换说明符。 请参阅此处的文档:https://www.gnu.org/software/libc/manual/html_node/Customizing-Printf.html 其他答案已经为您提供了如何手动打印居中字符串的解决方案,当您使用自己的转换说明符时仍然需要这个。


0
一个更紧凑的PADYMKO函数版本(仍然会泄漏内存):
char *str_center(char str[], unsigned int new_length, char placeholder)
{
    size_t str_length = strlen(str);
    char *buffer;
    /*------------------------------------------------------------------
     * If a new length is less or equal length of the original string, 
     * returns the original string 
     *------------------------------------------------------------------*/
    if (new_length <= str_length)
    {
        return(str);
    }
    buffer = malloc(sizeof(char) * (new_length + 1));
    memset(buffer, placeholder, new_length);
    buffer[new_length] = '\0';
    bcopy(str, buffer + (( new_length - str_length) / 2), str_length);
    return(buffer);
}

这将整个新分配的缓冲区设置为填充字符,将其以空字符结尾,然后将要居中的字符串放置在缓冲区的中间 - 没有循环或跟踪复制位置。


-2

您可以使用以下两个选项之一:

char name[] = "Name1";

//Option One
printf("%*s", 40+strlen(name)/2, name, 40-strlen(name)/2, "");
puts("");//skip one line

//Option two
printf("%*s", 40+strlen("Name2")/2, "Name2", 40-strlen("Name2")/2, "");

输出结果为:

姓名1(居中)
姓名2(居中)


1
首先,这看起来像是Giuseppe的答案的抄袭,其次,你有比格式说明符更多的printf()参数需要容纳。你甚至测试过吗? - clearlight

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