跳转到内容

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]
华夏公益教科书