DEV Community

codemee
codemee

Posted on • Edited on

printf() 格式字串的使用方法

printf() 最重要的功能就是可以印出格式化的字串, 而其中的關鍵則是第一個參數 -- 格式字串 (format string)。格式字串大家都多少會用, 但是有些細節卻未必了解, 本文就針對格式字串詳細說明。

格式字串 (format string)

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

%[旗標][寬度][.精準度][調整詞]格式代碼
Enter fullscreen mode Exit fullscreen mode

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

格式代碼 (format)

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

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

#include <stdio.h>

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

  return 0;
}
Enter fullscreen mode Exit fullscreen mode

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

hello:

Enter fullscreen mode Exit fullscreen mode

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

寬度 (width)

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

#include <stdio.h>

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

  return 0;
}
Enter fullscreen mode Exit fullscreen mode

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

12345678901234567890
     hello:

Enter fullscreen mode Exit fullscreen mode

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

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

#include <stdio.h>

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

  return 0;
}
Enter fullscreen mode Exit fullscreen mode

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

12345678901234567890
     hello:
Enter fullscreen mode Exit fullscreen mode

旗標 (flags)

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

#include <stdio.h>

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

  return 0;
}
Enter fullscreen mode Exit fullscreen mode

結果如下:

12345678901234567890
hello     :
Enter fullscreen mode Exit fullscreen mode

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

精確度 (precision)

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

#include <stdio.h>

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

  return 0;
}
Enter fullscreen mode Exit fullscreen mode

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

#include <stdio.h>

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

  return 0;
}
Enter fullscreen mode Exit fullscreen mode

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

12345678901234567890
hell:
Enter fullscreen mode Exit fullscreen mode

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

#include <stdio.h>

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

  return 0;
}
Enter fullscreen mode Exit fullscreen mode

會得到一樣的結果。

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

如果要列印的是有號整數 (signed integer), 可以用格式代碼 di 轉換成 10 進位數字:

printf("%10d:\n", 300);
printf("%-10d:\n", -300);
Enter fullscreen mode Exit fullscreen mode

結果如下:

       300:
-300      :
Enter fullscreen mode Exit fullscreen mode

旗標

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

printf("%+10d:\n", 300);
Enter fullscreen mode Exit fullscreen mode

結果如下:

      +300:
Enter fullscreen mode Exit fullscreen mode

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

printf("%010d:\n", -300);
printf("%-010d:\n", 300);
Enter fullscreen mode Exit fullscreen mode

結果如下:

-000000300:
300       :
Enter fullscreen mode Exit fullscreen mode

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

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

printf("% 010d:\n", 300);
Enter fullscreen mode Exit fullscreen mode

結果如下:

 000000300:
Enter fullscreen mode Exit fullscreen mode

精確度

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

printf("%10.6d:\n", 300);
printf("%010.6d:\n", 300);
Enter fullscreen mode Exit fullscreen mode

結果如下:

    000300:
    000300:
Enter fullscreen mode Exit fullscreen mode

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

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

長度調整詞 (length modifier)

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

#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;
}
Enter fullscreen mode Exit fullscreen mode

結果如下:

int is 4 bytes.
long is 8 bytes.
300:
-1:
8589934591:
Enter fullscreen mode Exit fullscreen mode

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

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

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

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

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

#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;
}
Enter fullscreen mode Exit fullscreen mode

結果如下:

300:
4294967295:
-1:
4294967295:
4294967295:
18446744073709551615:
Enter fullscreen mode Exit fullscreen mode

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

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

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

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

#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;
}
Enter fullscreen mode Exit fullscreen mode

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

0000012c:
FFFFFFFF:
FFFFFFFF:
ffffffff:
Enter fullscreen mode Exit fullscreen mode

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

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

printf("%#x:\n", i2);
Enter fullscreen mode Exit fullscreen mode

結果如下:

0xffffffff:
Enter fullscreen mode Exit fullscreen mode

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

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

#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;
}
Enter fullscreen mode Exit fullscreen mode

結果如下:

300.000000:
0000000300:
      300.:
00001.5850:
Enter fullscreen mode Exit fullscreen mode

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

由於預設引數型別提升規則的關係, 傳給 printf() 的 float 資料會先被轉成 double 才傳入 printf(), 因此雖然 f 格式代碼處理的對象是 double, 但傳入 float 型別的資料也能正確處理。

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

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

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

#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;
}
Enter fullscreen mode Exit fullscreen mode

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

3.000000e+01:
000003e+02:
    3.E+02:
1.5850E+00:
Enter fullscreen mode Exit fullscreen mode

在 Windows 平台上, 不知道是什麼原因, 指數至少會有 3 位數。如果希望能和其他平台一致, 可以使用以下函式設定成 2 位:

_set_output_format(_TWO_DIGIT_EXPONENT);
Enter fullscreen mode Exit fullscreen mode

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

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

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

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

請看以下範例:

#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;
}
Enter fullscreen mode Exit fullscreen mode

結果如下:

e:1.0123e+01
f:   10.1235
g:     10.12
Enter fullscreen mode Exit fullscreen mode

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

#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;
}
Enter fullscreen mode Exit fullscreen mode

結果就會變成:

e:1.2345e-05
f:    0.0000
g: 1.235e-05
Enter fullscreen mode Exit fullscreen mode

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

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

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

#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;
}
Enter fullscreen mode Exit fullscreen mode

結果如下:

e:5.500000e-01
f:  0.550000
g:      0.55
Enter fullscreen mode Exit fullscreen mode

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

#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;
}
Enter fullscreen mode Exit fullscreen mode

結尾的 0 就會出現了:

e:5.500000e-01
f:  0.550000
g:  0.550000
Enter fullscreen mode Exit fullscreen mode

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

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

#include <stdio.h>

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

  return 0;
}
Enter fullscreen mode Exit fullscreen mode

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

A
A
Enter fullscreen mode Exit fullscreen mode

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

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

#include <stdio.h>

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

  return 0;
}
Enter fullscreen mode Exit fullscreen mode

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

0X000000000061FE1C
Enter fullscreen mode Exit fullscreen mode

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

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

#include <stdio.h>

int main(){
  int numOfChars;

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

  return 0;
}
Enter fullscreen mode Exit fullscreen mode

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

1234567890
   10 :
    7
Enter fullscreen mode Exit fullscreen mode

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

在 Arduino 中使用 printf()

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

使用 sprintf()

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

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:

}
Enter fullscreen mode Exit fullscreen mode

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

        20,      ?

Enter fullscreen mode Exit fullscreen mode

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

讓 sprintf() 支援完整的格式

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

compiler.c.elf.extra_flags=-Wl,-u,vfprintf -lprintf_flt -lm
Enter fullscreen mode Exit fullscreen mode

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

        20, 03.142
Enter fullscreen mode Exit fullscreen mode

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

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

Enter fullscreen mode Exit fullscreen mode

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

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

程式碼足足多了近 1.5KB。

使用 dtostrf()/dtostre()

如果剛剛那 1.5K 一定要省, 可以改用 dtostrf()/dtostre() 來達成 f/e 格式的功能, 例如:

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:

}
Enter fullscreen mode Exit fullscreen mode
 3.142
+3.142e+00
Enter fullscreen mode Exit fullscreen mode

這兩個函式的說明可以在 AVR 的參考網頁找到, 你也可以在 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 。

Enter fullscreen mode Exit fullscreen mode

可以看到少了 1KB 了。

Top comments (0)