跳轉到內容

C 程式設計/stdio.h/printf

來自華夏公益教科書,開放的書籍,開放的世界
printf 函式的一個示例。

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

時間線

[編輯 | 編輯原始碼]

許多程式語言實現了一個printf函式,用於輸出格式化的字串。它起源於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:從 1.5 版本開始,Java 支援printf 作為PrintStream[2] 的成員,賦予它printf 和 fprintf 函式的功能。同時,透過新增format(String, Object... args) 方法,將sprintf 類似的功能新增到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 包含在幾個作業系統(包括 OpenBSDFreeBSDNetBSD)的 C 庫中,以及 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 將字串臨時儲存到私有緩衝區。

格式佔位符

[編輯 | 編輯原始碼]

格式化透過格式字串中的佔位符進行。例如,如果程式想要打印出一個人的年齡,它可以透過在前面新增 "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 通常等效於I32dWin32/Win64)或d
PRId64 通常等效於I64dWin32/Win64),lld32 位平臺)或ld64 位平臺
PRIi32 通常等效於I32iWin32/Win64)或i
PRIi64 通常等效於I64iWin32/Win64),lli32 位平臺)或li64 位平臺
PRIu32 通常等效於I32uWin32/Win64)或u
PRIu64 通常等效於I64uWin32/Win64),llu32 位平臺)或lu64 位平臺
PRIx64 通常等效於I64xWin32/Win64),llx32 位平臺)或lx64 位平臺
  • 型別可以是以下任何一種:
字元 描述
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 以 null 結尾的字串.
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 Server)包含它們自己的類似printf 的函式,並將擴充套件嵌入到其中。但是,它們都傾向於與register_printf_function() 存在相同的問題。

大多數沒有類似printf 函式的非 C 語言透過使用“%s”格式並將物件轉換為字串表示來解決此問題的缺乏。 C++ 提供了一個顯著的例外,因為它從其 C 歷史中繼承了一個printf 函式,但也具有一個更受歡迎的完全不同的機制。

具有 printf 的程式語言

[編輯 | 編輯原始碼]
  1. "ASA 列印控制字元". 檢索於 2010年2月12日.
  2. "PrintStream (Java 2 Platform SE 5.0)". Sun Microsystems Inc. 1994. 檢索於 2008-11-18.
  3. "String (Java 2 Platform SE 5.0)". Sun Microsystems Inc. 1994. 檢索於 2008-11-18.
[編輯 | 編輯原始碼]
華夏公益教科書