printf() 格式字串的使用方法
Tags: c, printf, cpp
Published: 2022-01-19 07:37:38

printf() 最重要的功能就是可以印出格式化的字串, 而其中的關鍵則是第一個參數 -- **[格式字串 (format string)](http://www.open-std.org/jtc1/sc22/WG14/www/docs/n1256.pdf#page=290)**。格式字串大家都多少會用, 但是有些細節卻未必了解, 本文就針對格式字串詳細說明。

## 格式字串 (format string)

格式字串是內含**轉換規格 (conversion specification)** 的字串, 每一個轉換規格都對應到叫用 printf() 時從第 2 個開始的參數, 說明要如何呈現該參數的內容, 像是浮點數資料要印出幾位小數等等。每一個轉換規格的指定方法如下:

```
%[旗標][寬度][.精準度][調整詞]格式代碼
```

其中只有一開頭的**百分比符號**和最後的**格式代碼 (format)** 是必要的, 其餘項目都可視需要再加上。

## 格式代碼 (format)

格式代碼有許多種, 都以單一英文字母來表示。我們先以最簡單、用來轉換字串資料的 `s` 來舉例, 並藉此說明轉換規格中其他項目的用法。

### 轉換字串的格式代碼 --`s`

```c
#include <stdio.h>

int main(){
  char s[] = "hello";
  printf("%s:\n", s);

  return 0;
}
```

轉換規格 "%s" 會將對應的字串型別參數帶入, 所以實際印出的內容為：

```
hello:

```

我們故意在轉換規格後加一個 ':', 以便能看出來單一參數轉換後的結束位置。


#### 寬度 (width)

如果你希望印出像是表格的樣式, 讓個別參數佔據特定欄寬, 可以在轉換規格加上**寬度 (width)**, 例如：

```c
#include <stdio.h>

int main(){
  char s[] = "hello";
  printf("12345678901234567890\n");
  printf("%10s:\n", s);

  return 0;
}
```

在百分比符號和 's' 間加上寬度 10 後, 結果變成：

```
12345678901234567890
     hello:

```

我們特意先印出一列序號讓大家方便看出列印位置, 你可以看到 "hello" 的前面多了 5 個空格, 這是因為寬度是 10, 但 "hello" 只有 5 個字元, 不足的部分預設會在左邊補上空白字元。

你也可以用星號 `*` 指定寬度, 這樣就可以透過額外的整數參數來指定寬度, 例如：

```c
#include <stdio.h>

int main(){
  char s[] = "hello";
  printf("12345678901234567890\n");
  printf("%*s:\n", 10, s);

  return 0;
}
```

注意到用來提供寬度的參數必須在要印出的資料前, 結果如下：

```
12345678901234567890
     hello:
```

#### 旗標 (flags)

如果你希望字串長度不足時把空白補在右邊, 可以加上 `-` 旗標：

```c
#include <stdio.h>

int main(){
  char s[] = "hello";
  printf("12345678901234567890\n");
  printf("%-10s:\n", s);

  return 0;
}
```

結果如下：

```
12345678901234567890
hello     :
```

`-` 旗標表示要將資料靠左對齊欄位開頭。

#### 精確度 (precision)

如果資料的長度超過指定的寬度, 轉換後並不會截掉超過的部分, 仍然會列印出來, 例如：

```c
#include <stdio.h>

int main(){
  char s[] = "hello";
  printf("12345678901234567890\n");
  printf("%4s:\n", s);

  return 0;
}
```

雖然指定的寬度是 4, 但仍然會印出完整 5 個字元的內容。如果你希望列印時不要超過欄寬, 可以在轉換規格加上**精確度 (precision)**, 例如：

```c
#include <stdio.h>

int main(){
  char s[] = "hello";
  printf("12345678901234567890\n");
  printf("%4.4s:\n", s);

  return 0;
}
```

有小數點開頭的就是精確度, 表示最多只要印出原字串內的前幾個字元, 因此雖然字串的長度是 5, 但因為精確度是 4, 所以只會印出前 4 個字元：

```
12345678901234567890
hell:
```

精確度也一樣可以用星號 `*` 表示要由參數來決定實際的位數, 例如：

```c=
#include <stdio.h>

int main(){
  char s[] = "hello";
  printf("12345678901234567890\n");
  printf("%4.*s:\n", 4, s);

  return 0;
}
```

會得到一樣的結果。

### 以 10 進位呈現有號整數的格式代碼 -- `d`/`i`

如果要列印的是**有號整數 (signed integer)**, 可以用格式代碼 `d` 或 `i` 轉換成 10 進位數字：

```C
printf("%10d:\n", 300);
printf("%-10d:\n", -300);
```

結果如下：

```
       300:
-300      :
```

#### 旗標

如果希望正數的前方顯示 + 號, 可以加上 `+` 旗標：

```C
printf("%+10d:\n", 300);
```

結果如下：

```
      +300:
```

你也可以加上 `0` 旗標, 改用 '0' 填補不足寬度的部分：

```c
printf("%010d:\n", -300);
printf("%-010d:\n", 300);
```

結果如下：

```
-000000300:
300       :
```

請特別留意正負號現在會出現在最左端, 另外如果有使用 `-` 旗標, 右側空位並不會補 0, 否則會造成誤解。

如果你不想顯示正號, 但又希望若是正數, 可以在符號的位置空一格, 那麼可以使用空白字元 ` ` 旗標：

```c
printf("% 010d:\n", 300);
```

結果如下：

```
 000000300:
```

#### 精確度

你也可以使用精確度來指定最少要顯示的位數, 預設為 1, 若實際的位數不足, 會在前面補 0：

```c
printf("%10.6d:\n", 300);
printf("%010.6d:\n", 300);
```

結果如下：

```
    000300:
    000300:
```

要注意的是, 和 `0` 旗標並用的話, 不足寬度的部分會補空白, 不是補 0。

另外, 如果轉換後的值是 0, 而且精確度也是 0, 那就不會列印任何內容。

#### 長度調整詞 (length modifier)

如果格式代碼 d/i 對應的參數是 long 型別, 就必須要在格式代碼前加上 `l` 長度調整詞, 否則當數值超過 int 範圍時就會出錯：

```c
#include <stdio.h>

int main(){
  long l1 = 300l;
  long l2 = 0x1FFFFFFFFl; //8589934591
  printf("int is %d bytes.\n", sizeof(int));
  printf("long is %d bytes.\n", sizeof(long));
  printf("%d:\n", l1);
  printf("%d:\n", l2);
  printf("%ld:\n", l2);

  return 0;
}
```

結果如下：

```
int is 4 bytes.
long is 8 bytes.
300:
-1:
8589934591:
```

由於 l2 超過 int 的範圍, 所以若是用格式代碼 d 轉換, 只會取最低的 4 個位元組 0xFFFFFFFF, 轉換結果就變成 -1 了, 只要加上 `l` 長度調整詞, 就會取得完整的 long 資料轉換成正確的值了。

在某些平台或編譯器上, int 和 long 佔的位元組數一樣, 加或不加 `l` 長度調整詞結果雖然一樣, 但為了程式的相容性, 請都還是養成加上長度調整詞的好習慣。

如果資料是 short, 就要使用 `h` 長度調整詞；若資料是 long long, 就要改用 `ll` 長度調整詞。

### 以 10 進位呈現無號整數的格式代碼 -- `u`

格式代碼 `u` 的用法和 `d` 一樣, 只是它會把對應的參數視為無號整數, 例如：

```c
#include <stdio.h>

int main(){
  int i1 = 300;
  int i2 = -1;
  unsigned int i3 = 0xFFFFFFFF;
  unsigned long l = 0xFFFFFFFFFFFFFFFFl; //8589934591
  printf("%u:\n", i1);
  printf("%u:\n", i2);
  printf("%d:\n", i3);
  printf("%u:\n", i3);
  printf("%u:\n", l);
  printf("%lu:\n", l);

  return 0;
}
```

結果如下：

```
300:
4294967295:
-1:
4294967295:
4294967295:
18446744073709551615:
```

你可以看到 i2 雖然是 -1, 但用格式代碼 `u` 列印卻是正整數；反之, i3 雖然是無號整數, 但是若是使用格式代碼 `d` 列印, 它會當成是有號整數處理, 反而印出 -1 了。

對於無號長整數, 一樣要加上 `l` 長度調整詞, 否則只會截取到部分資料, 印出錯誤的值。

### 以 16 進位呈現無號整數的格式代碼 -- `x`/`X`

`x`/`X` 的用法就如同 `u`, 但是會用 16 進位的格式, 例如：

```c
#include <stdio.h>

int main(){
  int i1 = 300;
  int i2 = -1;
  unsigned long l = 0xFFFFFFFFFFFFFFFFl; //8589934591
  printf("%08x:\n", i1);
  printf("%X:\n", i2);
  printf("%X:\n", l);
  printf("%lx:\n", l);

  return 0;
}
```

結果如下, `X` 與 `x` 的差別就是使用大寫還是小寫 a~z 英文字母來表示 10 進位的 10~15：

```
0000012c:
FFFFFFFF:
FFFFFFFF:
ffffffff:
```

#### 替代格式 (alternative implementation) 旗標 -- `#`

如果加上 `#` 旗標, 就會額外加上 '0x' 或是 '0X' 字首, 讓列印結果更清楚是 16 進位。

```c
printf("%#x:\n", i2);
```

結果如下：

```
0xffffffff:
```

### 以 10 進位呈現雙精度浮點數的格式代碼 -- `f`/`F`

`f` 或 `F` 功用相同, 可用來列印浮點數, 這時精確度指的就是要列印的小數位數, 會以四捨五入的方式截掉多餘的位數, 沒有指定精確度時, 預設是 6 位。要特別留意的是小數點也會占掉一個字元, 指定寬度時要算進去。範例如下：

```c
#include <stdio.h>
#include <math.h>

int main(){
  printf("%10f:\n", 300.0);
  printf("%010.0f:\n", 300.3f);
  printf("%#10.0f:\n", 300.3);
  printf("%010.4f:\n", log2(3));

  return 0;
}
```

結果如下：

```
300.000000:
0000000300:
      300.:
00001.5850:
```

若精確度設為 0, 因為沒有小數, 預設就不會印出小數點, 但只要加上替代格式旗標 `#`, 就還是會印出小數點, 明確表示這是浮點數。

由於[預設引數型別提升規則](http://www.open-std.org/jtc1/sc22/WG14/www/docs/n1256.pdf#page=83)的關係, 傳給 printf() 的 float 資料會先被轉成 double 才傳入 printf(), 因此雖然 `f` 格式代碼處理的對象是 double, 但傳入 float 型別的資料也能正確處理。

如果資料是 long double, 就要加上 `L` 長度調整詞。

### 以科學記號顯示雙精度浮點數的格式代碼 -- `e`/`E`

`e`/`E` 功能同 `f`/`F`, 但改成以科學記號型式, 其中 `e`/`E` 的差異就在於用 'e' 還是 'E' 表示指數, 指數部分至少會有 2 位數字：

```c
#include <stdio.h>
#include <math.h>

int main(){
  printf("%10e:\n", 30.0);
  printf("%010.0e:\n", 300.3f);
  printf("%#10.0E:\n", 300.3);
  printf("%010.4E:\n", log2(3));

  return 0;
}
```

結果如下, 注意到精確度設定的是實數的小數位數, 指數的位數無法變更：

```
3.000000e+01:
000003e+02:
    3.E+02:
1.5850E+00:
```

在 Windows 平台上, 不知道是什麼原因, 指數至少會有 3 位數。如果希望能和其他平台一致, 可以使用[以下函式](https://docs.microsoft.com/en-us/cpp/c-runtime-library/set-output-format?view=msvc-170)設定成 2 位：

```c
_set_output_format(_TWO_DIGIT_EXPONENT);
```

本文都假設採用 C 語言標準規格, 指數至少 2 位。

### 以精簡格式顯示雙精度浮點數格式代碼 -- `g`/`G`

這種格式會依據轉換的數值從 `f`/`F` 或 `e`/`E` 挑選格式使用, 假設精確度是 P, 若未指定精確度時預設是 6, 若精確度設定為 0, 會自動調升為 1；而使用 `e`/`E` 格式轉換後的指數部分為 X, 規則如下：

1. 若 P > X ≧ -4, 就以精確度 P - 1 - X 採用 `f`/`F` 格式。
2. 否則就以精確度 P - 1 依照格式代碼大小寫套用 `e`/`E` 格式。

請看以下範例：

```c
#include <stdio.h>

int main(){
  printf("e:%10.4e\n", 10.12345);
  printf("f:%10.4f\n", 10.12345);
  printf("g:%10.4g\n", 10.12345);
  return 0;
}
```

結果如下：

```
e:1.0123e+01
f:   10.1235
g:     10.12
```

在這個例子中, P 是 4, X 是 1, 符合 P > X ≧ -4 的條件, 所以會套用 `f`/`F` 格式, 並設定精確度為 4 - 1 - 1, 也就是 2, 因此小數有 2 位。若是將範例改成以下：

```c
#include <stdio.h>

int main(){
  printf("e:%10.4e\n", 0.000012345);
  printf("f:%10.4f\n", 0.000012345);
  printf("g:%10.4g\n", 0.000012345);
  return 0;
}
```

結果就會變成：

```
e:1.2345e-05
f:    0.0000
g: 1.235e-05
```

這是因為雖然 P 還是 4, 但 X 是 -5, 不再符合 P > X ≧ -4 的條件, 所以會套用 `e` 格式, 並設定精確度為 4 - 1, 也就是 3, 因此以小數 3 位的科學記號表示法呈現。

有些書籍或是教學文章說 `g`/`G` 格式是挑選 `e`/`E` 和 `f`/`F` 兩者轉換後較短的結果, 這並不正確, 從上面的例子就可以看到不但不一定是採用較短的結果, 連精準度都不一樣。

`g`/`G` 格式還有一個很重要的特色就是會幫你把小數尾端的 0 自動去除, 例如：

```c
#include <stdio.h>

int main(){
  printf("e:%10.6e\n", 0.55);
  printf("f:%10.6f\n", 0.55);
  printf("g:%10.6g\n", 0.55);
  return 0;
}
```

結果如下：

```
e:5.500000e-01
f:  0.550000
g:      0.55
```

P 是 6, X 是 1, 所以 `g` 格式的精確度應該是  6 - 1 - 1 為 4, 但是實際看到的小數只有 2 位, 因為尾端的 00 被刪除了。如果要**保留小數尾端的 0**, 可以加上**替代格式旗標 `#`**：

```c
#include <stdio.h>

int main(){
  printf("e:%10.6e\n", 0.55);
  printf("f:%10.6f\n", 0.55);
  printf("g:%#10.6g\n", 0.55);
  return 0;
}
```

結尾的 0 就會出現了：

```
e:5.500000e-01
f:  0.550000
g:  0.550000
```

### 以字元呈現整數的格式代碼 -- `c`

這個格式會將對應的參數先轉型為 unsigned char, 再顯示對應的字元：

```c
#include <stdio.h>

int main(){
  printf("%c\n", 65);
  printf("%c\n", 'A');

  return 0;
}
```

不論傳入整數或是字元, 都可以正常顯示：

```
A
A
```

### 顯示指位器 (pointer) 的格式代碼 -- `p`

如果需要列印變數的位址, 那 `p` 格式就非常好用：

```c
#include <stdio.h>

int main(){
  int a = 20;
  printf("%p\n", &a);

  return 0;
}
```

會以 16 進位格式印出位址：

```
0X000000000061FE1C
```

### 顯示目前轉換字元數的格式代碼 -- `n`

如果想知道已經處理了多少字元, 可以使用 `n` 格式, 和之前說明過的格式代碼不同, 對應的參數必須是指位器, 它會將字數放入所指向的位址：

```c
#include <stdio.h>

int main(){
  int numOfChars;

  printf("1234567890\n");
  printf("%5d :%n\n", 10, &numOfChars);
  printf("%5d\n", numOfChars);

  return 0;
}
```

結果如下, `n` 不會列印任何字：


```
1234567890
   10 :
    7
```

由於到 ':' 共 7 個字元, 因此會將 7 寫入 numOfChars 變數內。

## 在 Arduino 中使用 printf()

如果你想在 Arduino 中使用 printf(), 會發現在序列埠監控視窗中看不到任何輸出, 這是因為 printf() 是輸出到 stdout, 而不是序列埠。

### 使用 sprintf()

要將 stdout 設定成序列埠比較費工, 我們可以改用 sprintf() 先將格式化輸出的結果放置在自訂的暫存區中, 再使用 Serial.println() 送至序列埠即可：

```c
void setup() {
  // put your setup code here, to run once:
  char buf[40];

  Serial.begin(9600);
  sprintf(buf, "%10d, %06.3f", 20, 3.14159);
  Serial.println(buf);
}

void loop() {
  // put your main code here, to run repeatedly:

}
```

序列埠監控視窗看到的結果如下：

```
        20,      ?

```

咦, 浮點數的結果怎麼變問號了？這是因為 Arduino UNO 的 AVR 晶片工具鏈預設連結的是精簡版程式庫, 為了減少程式碼的大小, 所以並不支援格式代碼 `f`/`F`。

### 讓 sprintf() 支援完整的格式

只要加上必要的編譯器選項, 就可以連結支援浮點數格式的程式庫：

```
compiler.c.elf.extra_flags=-Wl,-u,vfprintf -lprintf_flt -lm
```

你可以加在 Arduino 安裝資料夾下 \hardware\arduino\avr 路徑的 platform.txt 檔案中, 也可以在同路徑下新增 platform.local.txt 檔案, 並在此檔中加入上述編譯器選項, 後者的好處是可以講客製的選項獨立出來, 不會跟預設的選項混在一起。重新編譯後就可以看到正確的結果了：

```
        20, 03.142
```

不果這樣的功能是要付出代價的, 不支援浮點數格式時程式碼大小如下：

```
草稿碼使用了 3028 bytes (9%) 的程式儲存空間。上限為 32256 bytes。
全域變數使用了 200 bytes (9%) 的動態記憶體，剩餘 1848 bytes 給區域變數。上限為 2048 bytes 。

```

支援浮點數格式後的程式碼大小為：

```
草稿碼使用了 4500 bytes (13%) 的程式儲存空間。上限為 32256 bytes。
全域變數使用了 200 bytes (9%) 的動態記憶體，剩餘 1848 bytes 給區域變數。上限為 2048 bytes 。
```

程式碼足足多了近 1.5KB。

### 使用 dtostrf()/dtostre()

如果剛剛那 1.5K 一定要省, 可以改用 [dtostrf()](https://www.nongnu.org/avr-libc/user-manual/group__avr__stdlib.html#ga060c998e77fb5fc0d3168b3ce8771d42)/[dtostre()](https://www.nongnu.org/avr-libc/user-manual/group__avr__stdlib.html#ga6c140bdd3b9bd740a1490137317caa44) 來達成 `f`/`e` 格式的功能, 例如：

```c
void setup() {
  // put your setup code here, to run once:
  char buf[40];

  Serial.begin(9600);
  dtostrf(3.14159, 6, 3, buf);
  Serial.println(buf);
  dtostre(3.14159, buf, 3, DTOSTR_PLUS_SIGN);
  Serial.println(buf);
}

void loop() {
  // put your main code here, to run repeatedly:

}
```

```
 3.142
+3.142e+00
```

這兩個函式的說明可以在 AVR 的[參考網頁](https://www.nongnu.org/avr-libc/user-manual/group__avr__stdlib.html#ga6c140bdd3b9bd740a1490137317caa44)找到, 你也可以在 Arduino 安裝資料夾下 hardware\tools\avr\avr\include 的 stdlib.h 檔案中找到, 不過要注意的是 Arduino 給 dtostre() 的旗標定義名稱是 DTOSTR_XXXX, 不是 AVR 網頁中的 DTOSTRE_XXXX, 不要弄錯。使用這兩個函式的程式碼大小為：

```
草稿碼使用了 3412 bytes (10%) 的程式儲存空間。上限為 32256 bytes。
全域變數使用了 188 bytes (9%) 的動態記憶體，剩餘 1860 bytes 給區域變數。上限為 2048 bytes 。

```

可以看到少了 1KB 了。