跳轉到內容

C 程式設計/stdio.h/printf

來自華夏公益教科書,自由的教科書
printf 函式的一個例子。

Printf 函式(代表“print formatted”)是一類通常與某些型別 函式 相關的 程式語言。它們接受一個名為格式字串字串 引數,該引數指定了一種將任意數量的不同資料型別引數呈現為字串的方法。此字串預設情況下會列印到 標準輸出 流,但存在執行其他任務的變體。格式字串中的字元通常會逐字複製到函式的輸出中,其他引數會呈現到由格式說明符標記的結果文字中,這些說明符通常以 % 字元 開頭。

時間線

[編輯 | 編輯原始碼]

許多 程式語言 都實現了printf 函式,用於輸出格式化的 字串。它起源於 C 程式語言,在 C 語言中,它的 原型類似於以下程式碼

int printf(const char *format, ...)

format 字串常量提供輸出的描述,並使用以 "%" 跳脫字元 標記的 佔位符,以指定函式應該產生的輸出的相對位置和型別。返回值是列印的字元數。

Fortran,COBOL

[編輯 | 編輯原始碼]

Fortran 的 可變引數 PRINT 語句引用了一個不可執行的 FORMAT 語句。

      PRINT 601, 123456, 1000.0, 3.1415, 250
  601 FORMAT (8H RED NUM I7,4H EXP,E8.1, ' REAL' F5.2,'; VALUE=',I4)

將列印以下內容(如果指向列印裝置,則在前進到新行之後,因為如果指向列印裝置,則因為前導空格字元)[1]

 RED NUM 123456 EXP 1.0E 03 REAL 3.14; VALUE= 250

COBOL 透過分層資料結構規範提供格式化。

 01 out-rec.
   02 out-name   picture x(20).
   02 out-amount picture  $9,999.99.

...

    move me to out-name.
    move amount to out-amount.
    write out-rec.

1960 年代:BCPL、ALGOL 68、Multics PL/I

[編輯 | 編輯原始碼]

C 的 可變引數 printf 的起源可以追溯到 BCPLwritef 函式。

ALGOL 68 草案和最終報告包含 infoutf 函式,後來這些函式被從原始語言中修訂並替換為現在更熟悉的 readf/getfprintf/putf

printf(($"Color "g", number1 "6d,", number2 "4zd,", hex "16r2d,", float "-d.2d,", unsigned value"-3d"."l$,
            "red", 123456, 89, BIN 255, 3.14, 250));

Multics 有一個名為 ioa_ 的標準函式,它具有各種控制程式碼。它基於 Multics 的 BOS(引導作業系統)中的機器語言功能。

 call ioa_ ("Hello, ^a", "World!");

1970 年代:C、Lisp

[編輯 | 編輯原始碼]
 printf("Color %s, number1 %d, number2 %05d, hex %x, float %5.2f, unsigned value %u.\n",
        "red", 123456, 89, 255, 3.14159, 250);

將列印以下行(包括換行符,\n)

Color red, number1 123456, number2 00089, hex ff, float  3.14, unsigned value 250.

printf 函式返回列印的字元數,如果發生輸出錯誤,則返回負值。

Common Lispformat 函式。

 (format t "Hello, ~a" "World!")

在標準輸出流上列印 "Hello, World!"。如果第一個引數是 nil,則 format 將字串返回給呼叫者。第一個引數也可以是任何輸出流。format 在 1978 年的 麻省理工學院ZetaLisp 中引入,它基於 Multicsioa_,後來被採用到 Common Lisp 標準中。

1980 年代:Perl、Shell

[編輯 | 編輯原始碼]

Perl 也有一個 printf 函式。 Common Lisp 有一個 format 函式,其作用方式與 printf 相同,但使用不同的字元進行輸出轉換。 GLib 庫包含 g_print,它是 printf 的實現。

一些 Unix 系統有一個 printf 程式,用於 shell 指令碼。這可以在後者不可移植的情況下用作 echo 的替代品。例如

echo -n -e "$FOO\t$BAR"

可以可移植地重寫為

printf "%s\t%s" "$FOO" "$BAR"

1990 年代:PHP、Python

[編輯 | 編輯原始碼]

1991 年:Python% 運算子在插值元組的內容時與 printf 的語法類似。例如,此運算子可與 print 函式一起使用

print("%s\t%s" % (foo,bar))

Python 2.6 版本包含 str.format(),它優於過時的 %,後者可能會在未來的 Python 版本中消失

print("If you multiply five and six you get {0}.".format(5*6))

1995 年:PHP 也擁有 printf 函式,其規範和用法與 C/C++ 中的一致。 MATLAB 沒有 printf,但有其兩個擴充套件 sprintffprintf,它們使用相同的格式字串。sprintf 返回格式化的字串,而不是生成視覺輸出。

2000 年代:Java

[編輯 | 編輯原始碼]

2004 年:Java 從 1.5 版本開始支援 printf,作為 PrintStream[2] 的成員,使其具有 printf 和 fprintf 函式的功能。與此同時,sprintf 的功能透過新增 format(String, Object... args) 方法被新增到 String 類中。 [3]

// Write "Hello, World!" to standard output (like printf)
System.out.printf("%s, %s", "Hello", "World!"); 
// create a String object with the value "Hello, World!" (like sprintf)
String myString = String.format("%s, %s", "Hello", "World!");

與大多數其他實現不同,Java 的 printf 實現遇到格式字串錯誤時會丟擲 異常

[編輯 | 編輯原始碼]

ANSI C 標準指定了 printf 的若干變體,用於輸出流不是預設值、引數列表形式不同、輸出目標是記憶體而不是 檔案描述符 等情況。 printf 函式本身通常只是這些變體的包裝器,具有預設值。

int fprintf(FILE *stream, const char *format, ...)

fprintf 使得 printf 輸出可以寫入任何檔案。程式設計師經常使用它來列印錯誤,方法是寫入 標準錯誤 裝置,但它可以與任何使用 fopen(或 fdopen)函式開啟的檔案一起使用。

int sprintf (char *str, const char *format, ...)

sprintf 列印到字串(char 陣列)而不是 標準輸出sprintf 的使用者必須透過計算或 保護頁 確保結果字串不會大於為 str 分配的記憶體。未能確保這一點會導致 緩衝區溢位

在諸如 PHP 之類的更高階語言中,sprintf 函式沒有 str 引數。相反,它返回格式化的輸出字串。PHP 中的原型如下

string sprintf (const string format, ...)

緩衝區安全性和 sprintf

[編輯 | 編輯原始碼]

在 ISO C99 中,snprintf 被引入作為sprintf 的替代方案,可以幫助避免緩衝區溢位風險

int snprintf(char *str, size_t size, const char * restrict format, ...)

snprintf 保證不會向 str 中寫入超過 size 個位元組,因此使用它可以幫助避免緩衝區溢位風險,如以下程式碼片段所示

#define BUFFER_SIZE 50
char buf[BUFFER_SIZE];
int n;
n = snprintf(buf, BUFFER_SIZE, "Your name is %s.\n", username);
if (n < 0 || n >= BUFFER_SIZE)
   /* Handle error */

如果上述示例中的 username 使得 result 超過 49 位元組,該函式將透過截斷最終位元組來限制儲存在 buf 中的字串(截斷)。空終止符將始終寫入第 50 個位置,因此結果始終以空終止符結尾。此外,snprintf 的返回程式碼指示該函式如果存在足夠的空間會向字串寫入多少個位元組(不包括空)。系統可以使用此資訊在需要整個字串時分配新的(更大的)緩衝區。

許多 snprintf 實現偏離了上述描述,特別是許多 Windows 庫、版本 2.0.6 之前的 glibc 以及 Solaris。最常見的錯誤是在截斷時返回 -1 而不是所需的長度。更麻煩的是,有些實現沒有在截斷時寫入空終止符,或者返回 size-1(使得無法檢測到截斷)。這些偏差使得使用 snprintf 編寫可移植的安全的程式碼比應有的難度更大。

另一個安全的 sprintf 替代方案是asprintf,它是 GNU 的擴充套件

int asprintf(char **ret, const char *format, ...)

asprintf 自動分配足夠的記憶體來儲存最終字串。它將 *ret 設定為指向結果字串的指標,或者如果發生錯誤則設定為未定義的值(glibc 值得注意的是,它是唯一一個在錯誤時不總是將 *ret 設定為 NULL 的實現)。使用 asprintf 的程式設計師有責任在使用後釋放分配的記憶體。雖然不是任何標準的一部分,但 asprintf 出現在幾個作業系統的 C 庫中(包括 OpenBSDFreeBSD 以及 NetBSD)以及 libiberty 庫中的其他平臺上。

GLib 提供了另一個安全的替代方案:g_strdup_printf,它分配足夠的記憶體,但與 asprintf 不同,它將結果字串作為返回值返回,而不是透過第一個引數返回。

C++ 中 sprintf 的替代方案,用於數字轉換

[編輯 | 編輯原始碼]

C++ 中用於字串格式化和將其他型別轉換為字串的標準方法是 iostream。與 printf 不同,iostream 標準庫是型別安全的,並且可擴充套件。

常見的程式設計任務是將數字型別轉換為字串(char 緩衝區)。sprintf 系列雖然有用,但對於這樣一個簡單的任務來說可能過於繁瑣。此外,許多使用這些函式的程式並非旨在處理 區域設定 更改時輸出的變化。

在 C/C++ 中已經開發了許多替代方法

vprintf、vfprintf、vsprintf、vsnprintf 和 vasprintf

[編輯 | 編輯原始碼]
#include <stdio.h>
/* va_list versions of above */
int vprintf(const char *format, va_list ap);
int vfprintf(FILE *stream, const char *format, va_list ap);
int vsprintf(char *str, const char *format, va_list ap);
int vsnprintf(char *str, size_t size, const char *format, va_list ap);
int vasprintf(char **ret, const char *format, va_list ap);

這些類似於上面沒有 v 的函式,除了它們使用 可變引數 列表。這些函式為程式設計師提供了建立自己的 printf 變體的能力。例如,程式設計師可以編寫一個函式

void fatal_error(const char *format, ...)

該函式將使用 va_start 宏從額外的引數中獲取 va_list 變數,使用 vfprintf 在標準錯誤裝置上列印訊息,使用 va_end 宏清理 va_list 變數之後,最後執行必要的任務來乾淨地關閉程式。

這些函式的另一個常見應用是編寫一個自定義 printf,它列印到與檔案不同的目標。例如,圖形庫可能提供一個類似 printf 的函式,帶 X 和 Y 座標

int graphical_printf(int x, int y, const char *format, ...)

該函式將透過使用 vsnprintfvasprintf 將字串臨時儲存到私有緩衝區來實現。

格式佔位符

[edit | edit source]

格式化是透過格式字串中的佔位符進行的。例如,如果一個程式想列印一個人的年齡,它可以透過在前面加上 "Your age is" 來呈現輸出。為了表示我們想要在該訊息後立即顯示年齡的整數,我們可以使用格式字串

"Your age is %d."

佔位符的語法為

%[引數][標誌][寬度][.精度][長度]型別
  • 引數 可以省略或可以是
字元 描述
n$ n 是使用此格式說明符顯示的引數編號,允許提供給的引數使用不同的格式說明符或以不同的順序多次輸出。這是一個 POSIX 擴充套件,不在 C99 中。示例:printf("%2$d %2$#x; %1$d %1$#x",16,17) 會產生

"17 0x11; 16 0x10"

  • 標誌 可以是零個或多個(按任意順序)
字元 描述
-
(減號)
左對齊此佔位符的輸出(預設是右對齊輸出)。
+
(加號)
在正的帶符號數字型別前面加上一個加號。正數 = '+',負數 = '-'。(預設情況下,不會在正數前面加任何東西)。
 
(空格)
在正的帶符號數字型別前面加上一個空格。正數 = ' ',負數 = '-'。如果存在 '+' 標誌,則忽略此標誌。(預設情況下,不會在正數前面加任何東西)。
0
(零)
當指定了寬度選項時,在數字前面加上零。(預設情況下,會在前面加上空格)。

示例:printf("%2d", 3) 會產生 " 3",而 printf("%02d", 3) 會產生 "03"。

#
(井號)
備用形式。對於 'g' 和 'G',不會刪除尾隨零。對於 'f'、'F'、'e'、'E'、'g'、'G',輸出始終包含一個小數點。對於 'o'、'x'、'X' 或 '0',分別在非零數字前面加上 '0x'、'0X'。
  • 寬度 指定要輸出的最小字元數,通常用於在表格輸出中填充固定寬度的欄位,其中欄位在沒有填充的情況下會更小,儘管它不會導致截斷過大的欄位。寬度值中的前導零被解釋為上面提到的零填充標誌,而負值被解釋為與上面提到的左對齊 "-" 標誌結合使用的正值。
  • 精度 通常指定輸出的最大限制,具體取決於特定的格式型別。對於浮點數型別,它指定輸出應該四捨五入的小數點右側的數字數量。對於字串型別,它限制應輸出的字元數量,之後字串將被截斷。
  • 長度 可以省略或可以是以下任何一個
字元 描述
hh 對於整數型別,導致 printf 期望一個從 char 提升的 int 大小的整數引數。
h 對於整數型別,導致 printf 期望一個從 short 提升的 int 大小的整數引數。
l 對於整數型別,導致 printf 期望一個 long 大小的整數引數。
ll 對於整數型別,導致 printf 期望一個 long long 大小的整數引數。
L 對於浮點數型別,導致 printf 期望一個 long double 引數。
z 對於整數型別,導致 printf 期望一個 size_t 大小的整數引數。
j 對於整數型別,導致 printf 期望一個 intmax_t 大小的整數引數。
t 對於整數型別,導致 printf 期望一個 ptrdiff_t 大小的整數引數。

此外,在 ISO C99 擴充套件廣泛使用之前,還存在一些特定於平臺的長度選項

字元 描述
I 對於帶符號的整數型別,導致 printf 期望 ptrdiff_t 大小的整數引數;對於無符號整數型別,導致 printf 期望 size_t 大小的整數引數。通常在 Win32/Win64 平臺上找到。
I32 對於整數型別,導致 printf 期望一個 32 位(雙字)整數引數。通常在 Win32/Win64 平臺上找到。
I64 對於整數型別,導致 printf 期望一個 64 位(四字)整數引數。通常在 Win32/Win64 平臺上找到。
q 對於整數型別,導致 printf 期望一個 64 位(四字)整數引數。通常在 BSD 平臺上找到。

ISO C99 包含 inttypes.h 標頭檔案,其中包含許多宏,用於在平臺無關的 printf 編碼中使用。示例宏包括

字元 描述
PRId32 通常等效於 I32d (Win32/Win64) 或 d
PRId64 通常等效於 I64d (Win32/Win64)、lld (32 位平臺) 或 ld (64 位平臺)
PRIi32 通常等效於 I32i (Win32/Win64) 或 i
PRIi64 通常等效於 I64i (Win32/Win64)、lli (32 位平臺) 或 li (64 位平臺)
PRIu32 通常等效於 I32u (Win32/Win64) 或 u
PRIu64 通常等效於 I64u (Win32/Win64)、llu (32 位平臺) 或 lu (64 位平臺)
PRIx64 通常等效於 I64x (Win32/Win64)、llx (32 位平臺) 或 lx (64 位平臺)
  • 型別 可以是以下任何一個
字元 描述
d, i int 作為帶符號的 十進位制 數字。'%d' 和 '%i' 在輸出方面是同義詞,但在用於 scanf() 用於輸入時是不同的。
u 列印十進位制 unsigned int
f, F double 以正常(定點)表示法。'f' 和 'F' 僅在列印無限數或 NaN 的字串的方式上有所不同('f' 為 'inf'、'infinity' 和 'nan','F' 為 'INF'、'INFINITY' 和 'NAN')。
e, E double 值以標準形式 ([-]d.ddd e[+/-]ddd) 表示。E 轉換使用字母 E(而不是 e)來引入指數。指數始終包含至少兩位數字;如果值為零,則指數為 00。在 Windows 中,指數預設包含三位數字,例如 1.5e002,但這可以透過 Microsoft 特定的 _set_output_format 函式進行更改。
g, G double 以正常或指數表示法表示,具體取決於其大小更適合哪種表示法。'g' 使用小寫字母,'G' 使用大寫字母。此型別與定點表示法略有不同,因為它不包括小數點右側的無關緊要的零。此外,整數不包含小數點。
x, X unsigned int 作為 十六進位制 數字。'x' 使用小寫字母,'X' 使用大寫字母。
o unsigned int 以八進位制表示。
s 以空字元結尾的字串.
c char(字元)。
p void *(指向 void 的指標)以實現定義的格式表示。
n 不列印任何內容,但將迄今為止成功寫入的字元數量寫入整數指標引數。
% 一個字面值 '%' 字元(此型別不接受任何標誌、寬度、精度或長度)。

寬度和精度格式引數可以省略,也可以是嵌入在格式字串中的固定數字,或者當格式字串中用星號 "*" 表示時,作為另一個函式引數傳遞。例如 printf("%*d", 5, 10) 將導致列印 "   10",總寬度為 5 個字元,而 printf("%.*s", 3, "abcdef") 將導致列印 "abc"。

如果轉換規範的 語法 無效,則行為未定義,並可能導致程式終止。如果提供的 函式引數 太少,不足以提供模板字串中所有轉換規範的值,或者如果引數型別不正確,則結果也未定義。多餘的引數將被忽略。在許多情況下,未定義的行為會導致 "格式字串攻擊" 安全 漏洞

一些編譯器,例如 GNU 編譯器集合,會靜態檢查類似 printf 的函式的格式字串,並警告出現的問題(使用標誌 -Wall-Wformat 時)。如果對函式應用非標準的 "format" __attribute__,則 GCC 也會警告使用者定義的類似 printf 的函式。

在表格輸出中使用欄位寬度與顯式分隔符的風險

[edit | edit source]

僅使用欄位寬度來提供製表符,如格式 "%8d%8d%8d" 用於三個整數的三個 8 個字元的列,將無法保證如果資料中出現大數字,欄位分隔符將被保留。欄位分隔符丟失很容易導致輸出損壞。在鼓勵使用程式作為指令碼中構建塊的系統中,這種損壞的資料通常可以被轉發到進一步處理,並導致進一步損壞,而不管原始程式設計師是否期望輸出只會被人類閱讀。透過在所有表格輸出格式中包含顯式分隔符(即使是空格),可以消除此類問題。只需將前面提到的危險示例更改為 " %7d %7d %7d" 就可以解決這個問題,格式相同,直到數字變大,但隨後由於顯式包含的空格,明確地阻止它們在輸出時合併。類似的策略也適用於字串資料。

自定義格式佔位符

[edit | edit source]

存在一些 printf 類函式的實現,它們允許擴充套件基於跳脫字元迷你語言,從而允許程式設計師為非內建型別建立特定格式化函式。其中最知名的一種是(現已棄用)glibcregister_printf_function()。然而,由於它與靜態格式字串檢查衝突,所以很少使用。另一種是Vstr 自定義格式化器,它允許新增多字元格式名稱,並且可以與靜態格式檢查器一起使用。

一些應用程式(如 Apache HTTP 伺服器)包含它們自己的 printf 類函式,並將其擴充套件嵌入其中。然而,這些函式都傾向於與 register_printf_function() 一樣存在問題。

大多數沒有 printf 類函式的非 C 語言透過使用“%s”格式並將物件轉換為字串表示來解決這一問題。 C++ 是一個值得注意的例外,它繼承了 C 語言的歷史,擁有 printf 函式,但它也擁有一個完全不同的機制,被認為更受歡迎。

具有 printf 的程式語言

[edit | edit source]

參見

[edit | edit source]

備註

[edit | edit source]
  1. "ASA Print Control Characters". Retrieved February 12, 2010.
  2. "PrintStream (Java 2 Platform SE 5.0)". Sun Microsystems Inc. 1994. Retrieved 2008-11-18.
  3. "String (Java 2 Platform SE 5.0)". Sun Microsystems Inc. 1994. Retrieved 2008-11-18.
[edit | edit source]
華夏公益教科書