跳转到内容

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.
[编辑 | 编辑源代码]
华夏公益教科书