printf() 最重要的功能就是可以印出格式化的字串, 而其中的關鍵則是第一個參數 -- 格式字串 (format string)。格式字串大家都多少會用, 但是有些細節卻未必了解, 本文就針對格式字串詳細說明。
格式字串 (format string)
格式字串是內含轉換規格 (conversion specification) 的字串, 每一個轉換規格都對應到叫用 printf() 時從第 2 個開始的參數, 說明要如何呈現該參數的內容, 像是浮點數資料要印出幾位小數等等。每一個轉換規格的指定方法如下:
%[旗標][寬度][.精準度][調整詞]格式代碼
其中只有一開頭的百分比符號和最後的格式代碼 (format) 是必要的, 其餘項目都可視需要再加上。
格式代碼 (format)
格式代碼有許多種, 都以單一英文字母來表示。我們先以最簡單、用來轉換字串資料的 s
來舉例, 並藉此說明轉換規格中其他項目的用法。
轉換字串的格式代碼 --s
#include <stdio.h>
int main(){
char s[] = "hello";
printf("%s:\n", s);
return 0;
}
轉換規格 "%s" 會將對應的字串型別參數帶入, 所以實際印出的內容為:
hello:
我們故意在轉換規格後加一個 ':', 以便能看出來單一參數轉換後的結束位置。
寬度 (width)
如果你希望印出像是表格的樣式, 讓個別參數佔據特定欄寬, 可以在轉換規格加上寬度 (width), 例如:
#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 個字元, 不足的部分預設會在左邊補上空白字元。
你也可以用星號 *
指定寬度, 這樣就可以透過額外的整數參數來指定寬度, 例如:
#include <stdio.h>
int main(){
char s[] = "hello";
printf("12345678901234567890\n");
printf("%*s:\n", 10, s);
return 0;
}
注意到用來提供寬度的參數必須在要印出的資料前, 結果如下:
12345678901234567890
hello:
旗標 (flags)
如果你希望字串長度不足時把空白補在右邊, 可以加上 -
旗標:
#include <stdio.h>
int main(){
char s[] = "hello";
printf("12345678901234567890\n");
printf("%-10s:\n", s);
return 0;
}
結果如下:
12345678901234567890
hello :
-
旗標表示要將資料靠左對齊欄位開頭。
精確度 (precision)
如果資料的長度超過指定的寬度, 轉換後並不會截掉超過的部分, 仍然會列印出來, 例如:
#include <stdio.h>
int main(){
char s[] = "hello";
printf("12345678901234567890\n");
printf("%4s:\n", s);
return 0;
}
雖然指定的寬度是 4, 但仍然會印出完整 5 個字元的內容。如果你希望列印時不要超過欄寬, 可以在轉換規格加上精確度 (precision), 例如:
#include <stdio.h>
int main(){
char s[] = "hello";
printf("12345678901234567890\n");
printf("%4.4s:\n", s);
return 0;
}
有小數點開頭的就是精確度, 表示最多只要印出原字串內的前幾個字元, 因此雖然字串的長度是 5, 但因為精確度是 4, 所以只會印出前 4 個字元:
12345678901234567890
hell:
精確度也一樣可以用星號 *
表示要由參數來決定實際的位數, 例如:
#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 進位數字:
printf("%10d:\n", 300);
printf("%-10d:\n", -300);
結果如下:
300:
-300 :
旗標
如果希望正數的前方顯示 + 號, 可以加上 +
旗標:
printf("%+10d:\n", 300);
結果如下:
+300:
你也可以加上 0
旗標, 改用 '0' 填補不足寬度的部分:
printf("%010d:\n", -300);
printf("%-010d:\n", 300);
結果如下:
-000000300:
300 :
請特別留意正負號現在會出現在最左端, 另外如果有使用 -
旗標, 右側空位並不會補 0, 否則會造成誤解。
如果你不想顯示正號, 但又希望若是正數, 可以在符號的位置空一格, 那麼可以使用空白字元 旗標:
printf("% 010d:\n", 300);
結果如下:
000000300:
精確度
你也可以使用精確度來指定最少要顯示的位數, 預設為 1, 若實際的位數不足, 會在前面補 0:
printf("%10.6d:\n", 300);
printf("%010.6d:\n", 300);
結果如下:
000300:
000300:
要注意的是, 和 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;
}
結果如下:
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
一樣, 只是它會把對應的參數視為無號整數, 例如:
#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 進位的格式, 例如:
#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 進位。
printf("%#x:\n", i2);
結果如下:
0xffffffff:
以 10 進位呈現雙精度浮點數的格式代碼 -- f
/F
f
或 F
功用相同, 可用來列印浮點數, 這時精確度指的就是要列印的小數位數, 會以四捨五入的方式截掉多餘的位數, 沒有指定精確度時, 預設是 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;
}
結果如下:
300.000000:
0000000300:
300.:
00001.5850:
若精確度設為 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;
}
結果如下, 注意到精確度設定的是實數的小數位數, 指數的位數無法變更:
3.000000e+01:
000003e+02:
3.E+02:
1.5850E+00:
在 Windows 平台上, 不知道是什麼原因, 指數至少會有 3 位數。如果希望能和其他平台一致, 可以使用以下函式設定成 2 位:
_set_output_format(_TWO_DIGIT_EXPONENT);
本文都假設採用 C 語言標準規格, 指數至少 2 位。
以精簡格式顯示雙精度浮點數格式代碼 -- g
/G
這種格式會依據轉換的數值從 f
/F
或 e
/E
挑選格式使用, 假設精確度是 P, 若未指定精確度時預設是 6, 若精確度設定為 0, 會自動調升為 1;而使用 e
/E
格式轉換後的指數部分為 X, 規則如下:
- 若 P > X ≧ -4, 就以精確度 P - 1 - X 採用
f
/F
格式。 - 否則就以精確度 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;
}
結果如下:
e:1.0123e+01
f: 10.1235
g: 10.12
在這個例子中, 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;
}
結果就會變成:
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 自動去除, 例如:
#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, 可以加上替代格式旗標 #
:
#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, 再顯示對應的字元:
#include <stdio.h>
int main(){
printf("%c\n", 65);
printf("%c\n", 'A');
return 0;
}
不論傳入整數或是字元, 都可以正常顯示:
A
A
顯示指位器 (pointer) 的格式代碼 -- p
如果需要列印變數的位址, 那 p
格式就非常好用:
#include <stdio.h>
int main(){
int a = 20;
printf("%p\n", &a);
return 0;
}
會以 16 進位格式印出位址:
0X000000000061FE1C
顯示目前轉換字元數的格式代碼 -- n
如果想知道已經處理了多少字元, 可以使用 n
格式, 和之前說明過的格式代碼不同, 對應的參數必須是指位器, 它會將字數放入所指向的位址:
#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() 送至序列埠即可:
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()/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:
}
3.142
+3.142e+00
這兩個函式的說明可以在 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 。
可以看到少了 1KB 了。
Top comments (0)