C语言格式化输出

C 语言通过 printf()函数系列来格式化地输出数据。本文采用相应的示例说明常用的格式化选项。

printf()函数系列

printf()函数以及多种它的相关函数都能够提供数据的格式化输出功能,它们通过使用格式化字符串(format string)作为函数参数来指定具体格式。然而,不同的函数具有不同的输出目的,以及对所需输出数据的访问方法。下面的 printf()函数系列可用于处理字节导向流:
int printf(const char*restrict format,...);
写入标准输出流,stdout。

int fprintf(FILE*restrict fp,const char*restrict format,...);
写入 fp 指定的输出流。printf()函数可以视为 fprintf()的特殊版本。

int sprintf(char*restrict buf,
const char*restrict format,...);
将格式化数据写入 buf 指向的 char 数组,并在后面加上一个标志结尾的空字符。

在上述函数原型中出现的省略号(...),表示还可有更多参数,但这些参数是可选的。还有一些 printf()函数系列需要一个指针参数,以指向一个参数列表,而不是在函数调用时直接接收数量可变的参数。这些函数的名称都以一个 v 开始,表示“variable argument list”(可变参数列表)的意思:
int vprintf( const char * restrictformat, va_list argptr );
int vfprintf( FILE * restrict fp, const char * restrict format,
              va_list argptr );
int vsprintf( char * restrict buf, const char * restrict format,
              va_list argptr );
int vsnprintf( char * restrict buffer, size_t n,
               const char * restrict format, va_list argptr );

如果想使用支持可变参数列表的函数,除了头文件 stdio.h 以外,还必须包含头文件 stdarg.h。

上述函数都有相应的宽字符导向流版本。针对宽字符的 printf()函数名称中包括字符串 wprintf 而不是 pintf,例如,vfwprintf()和 swprintf()等。但有一个例外:没有 snwprintf()函数。而是采用 snprintf()对应到 swprintf(),该函数采用一个参数来指定最大输出长度。

C11 标准为这些函数都提供了一个新的“安全”的版本。这些对应的新函数均以后缀 _s(例如,fprintf_s())。新函数测试它们接收的所有指针参数是否为空指针。

格式化字符串

格式化字符串是每个 printf()系列函数都具有的一个参数。格式化字符串定义了数据的输出格式,并包含了一些普通字符和转换说明(conversion specification)。每个转换说明都定义了函数该如何将可选参数转换并格式化,以供输出。printf()函数将格式化字符串写入到输出,使用对应可选参数的格式化值来替代转换说明。

转换说明以百分号 % 开始,并以一个字母结尾,这称为转换修饰符(conversion specifier)。(为了在输出中表示 %,需要一个特殊的转换修饰符:%%。printf()将该符号转换成一个单独的百分号。)

转换说明的语法以转换修饰符作为结尾。在本文中,我们将使用这两个术语来讨论调用函数 printf()和 scanf()时所使用的格式化字符串。

转换修饰符决定了转换的类型,并且必须符合对应的可选参数。如下例所示:
int score = 120;
char player[ ] = "Mary";
printf( "%s has %d points.\n", player, score );

在调用 printf()时所使用的格式化字符串包含两个转换说明:%s 和 %d。对应的两个可选参数也分别被指定:一个字符串,匹配转换修饰符 s(表示“string”),以及一个 int 数值,匹配转换修饰符 d(表示“decimal”)。示例中的函数调用,会在标准输出设备中写入下面的字符串:

Mary has 120 points.


所有的转换说明(但 %% 是例外)都具有下面的通用格式:

%[标记][字段宽度][.精度][长度修饰符]修饰符


方括号内的这部分语法都是可选的,但是若要使用它们,就必须遵循上述次序。下面一节会详细解释每个参数类型合法的转换说明。所有转换说明都可包含“字段宽度”(field width)。然而,并非所有的转换类型都有“精度”(precision)这个选项,对不同的修饰符来说,精度意义是不一样的。

字段宽度

进行格式化的表格输出时,字段宽度选项非常有用。如果包括该选项,字段宽度必须是正的十进制整数(或者是一个星号,下面会介绍)。字段宽度指定对应的数据项所输出的最少字符数量。默认情况下,字段中的被转换数据为右对齐(right-justified),左边多的位置用空格填补。如果标记包含减号(-),则为左对齐(left-justified),超出的字段宽度采用空格向右填补。

下面的例子先输出一行位置编号,然后展示字段宽度选项对输出的作用:
printf("1234567890123456\n");           // 字符位置
printf( "%-10s %s\n", "Player", "Score" );      // 表头
printf( "%-10s %4d\n", "John", 120 );   // 字段宽度:10;4
printf( "%-10s %4d\n", "Mary", 77 );

上述语句会生成一个简单表格:

1234567890123456
Player     Score
John        120
Mary            77


如果输出转换的结果比所指定的宽度具有更多的字符,那么字段会做必要的扩充,以输出完整的数据。

如果字段是右对齐的,可以采用 0 而非空格填充。要实现这样的效果,在转换说明标记中包括一个 0(指数字零)。下面的例子以 mm-dd-yyyy 的格式输出日期:
int month = 5, day = 1, year = 1987;
printf( "Date of birth: %02d-%02d-%04d\n", month, day, year );

该 printf()调用会产生下面的输出:

Date of birth: 05-01-1987


也可以使用一个变量来指定字段宽度。要实现这样的效果,采用一个星号(*)作为转换说明中的字段宽度,并在 printf()调用时包括一个额外的函数参数。该参数必须具有 int 类型,并且出现在需输出的参数之前。如下例所示:
char str[ ] = "Variable field width";
int width = 30;
printf( "%-*s!\n", width, str );

上例中的 printf 语句在字段靠左边位置输出字符串 str,并且字段宽度由变量 width 决定。结果如下:

Variable field width         !


请注意输出的最后有一个感叹号(!)。感叹号之前的一大段空格并非 str[] 在初始化时被赋值的内容。这些空格而是 printf 语句根据我们的要求为该字符串指定 30 个字符宽度而自动填充的。

输出字符和字符串

printf()中针对字符串的转换修饰符是 s,正如前面代码中所示。针对字符的修饰符是 c(表示 char)。它们总结如表 1 所示。

表1 针对输出字符和字符串的转换修饰符
修饰符 参数类型 表示
c int 一个单独的字符
s char 指针 该指针参数所指向的字符串

下面的例子在一个队员名单中各成员之间输出一个分隔字符:
char *team[ ] = { "Vivian", "Tim", "Frank", "Sally" };
char separator = ';';
for ( int i = 0; i < sizeof(team)/sizeof(char *); ++i )
  printf( "%10s%c ", team[i], separator );
putchar( '\n' );

用转换说明 %c 表示的参数,可以拥有比 int 还小的类型(例如 char)。整数提升会自动地将该类型参数转换成 int。然后函数 printf()将该 int 参数转换为 unsigned char,并输出对应的字符。

对于字符串输出来说,可以指定能被输出的最多字符数量。这时用到转换说明的精度选项,精度表示为一个点后接一个十进制整数。如下例所示:
char msg[] = "Every solution breeds new problems.";
printf( "%.14s\n", msg );       // 精度:14
printf( "%20.14s\n", msg );     // 字段宽度是20;精度是14
printf( "%.8s\n", msg+6 );      // 从字符串msg的第7个字符起输出字符串,精度为8

上述语句会产生下面的输出结果:

Every solution
     Every solution
solution

输出整数

函数 printf()可以把整数值转换为十进制、八进位或十六进制表示。表 2 列出了用于格式化输出整数的转换修饰符。

表2 针对输出整数的转换修饰符
修饰符 参数类型 表示
d,i int 十进制
u unsigned int 十进制
o unsigned int 八进位
x unsigned int 十六进制,用小写的 a、b、c、d、e、f
X unsigned int 十六进制,用大写的 A、B、C、D、E、F

下面的例子展示同一个整数的不同转换方式:
printf( "%4d %4o %4x %4X\n", 63, 63, 63, 63 );

该 printf()调用会产生下面的输出:

63  77      3f      3F


修饰符 u、o、x 与 X 把对应的参数解释为无符号整数。如果参数类型是 int,并且其值是负数,则转换后输出的是对应参数按照无符号整数解释时其位模式下的正数值:
printf( "%d %u %X\n", -1, -1, -1 );

如果 int 为 32 位宽,那么该语句会产生下面的输出:

-1       4294967295    FFFFFFFF


因为参数会受到整数提升的影响,同样的转换修饰符可以被用来格式化 short 和 unsigned short 参数。对于类型是 long 或 unsigned long 的参数,必须在 d、i、u、o、x 和 X 修饰符前面加上长度修饰符 l(小写的 L)。类似地,如果参数是 long long 或 unsigned long long 类型,则其长度修饰符是 ll(两个小写 L)。如下例所示:
long bignumber = 100000L;
unsigned long long hugenumber = 100000ULL * 1000000ULL;
printf( "%ld   %llX\n", bignumber, hugenumber );

上述语句产生下面的输出:

100000 2540BE400

输出浮点数

表 3 列出了函数 printf()用来格式化输出浮点数的转换修饰符。

表3 针对输出浮点数的转换修饰符
修饰符 参数类型 表示
f double 十进制浮点数
e、E double 指数表示法,十进制
g、G double 浮点数或指数表示法,选择其中较短者
a、A double 指数表示法,十六进制

最常用的修饰符是 f 和 e(或 E)。下面的例子展示了它们的用法:
double x = 12.34;
printf( "%f %e %E\n", x, x, x );

该 printf()调用将产生下面的输出:

12.340000   1.234000e+01    1.234000E+01


在指数表示法中出现的 e 是大写还是小写,取决于函数转换修饰符中所用 e 的大小写。而且,如上例所示,默认输出显示精度为 6 位小数。转换修饰符中的精度选项可修改这个默认设置:
double value = 8.765;
printf( "Value: %.2f\n", value );               // 精度为2:表示输出为2位小数
printf( "Integer value:\n"
        " Rounded: %5.0f\n"                     // 字段宽度为5;精度为0
        " Truncated: %5d\n", value, (int)value );

该 printf()调用会产生下面的输出:

Value: 8.77
Integer value:
 Rounded:               9
 Truncated:     8


正如上例所示,printf()会将浮点数按向上或向下取近似值,以便于输出。如果指定精度为0,那么小数点本身则会被省略。如果仅仅想把小数部分直接去掉,而不是取近似值,直接将它转换为整数类型可达到目的。

上述修饰符也可以配合 float 参数使用,因为 float 参数会自动地被提升为 double。但是,如想输出类型为 long double 的参数,必须在转换修饰符之前插入长度修饰符 L,如下例所示:
#include <math.h>
long double xxl = expl(1000);
printf( "e to the power of 1000 is %.2Le\n", xxl );