DEV Community

codemee
codemee

Posted on • Edited on

20 3

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)

Heroku

Build apps, not infrastructure.

Dealing with servers, hardware, and infrastructure can take up your valuable time. Discover the benefits of Heroku, the PaaS of choice for developers since 2007.

Visit Site

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay