C語言格式化輸出

2020-07-16 10:04:28
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("1234567890123456n");           // 字元位置
printf( "%-10s %sn", "Player", "Score" );      // 表頭
printf( "%-10s %4dn", "John", 120 );   // 欄位寬度:10;4
printf( "%-10s %4dn", "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-%04dn", 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( "%.14sn", msg );       // 精度:14
printf( "%20.14sn", msg );     // 欄位寬度是20;精度是14
printf( "%.8sn", 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 %4Xn", 63, 63, 63, 63 );

該 printf()呼叫會產生下面的輸出:

63  77      3f      3F


修飾符 u、o、x 與 X 把對應的引數解釋為無符號整數。如果引數型別是 int,並且其值是負數,則轉換後輸出的是對應引數按照無符號整數解釋時其位元型樣下的正數值:
printf( "%d %u %Xn", -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   %llXn", 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 %En", x, x, x );

該 printf()呼叫將產生下面的輸出:

12.340000   1.234000e+01    1.234000E+01


在指數表示法中出現的 e 是大寫還是小寫,取決於函數轉換修飾符中所用 e 的大小寫。而且,如上例所示,預設輸出顯示精度為 6 位小數。轉換修飾符中的精度選項可修改這個預設設定:
double value = 8.765;
printf( "Value: %.2fn", value );               // 精度為2:表示輸出為2位小數
printf( "Integer value:n"
        " Rounded: %5.0fn"                     // 欄位寬度為5;精度為0
        " Truncated: %5dn", 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 %.2Len", xxl );