引言

stdio.h 头文件声明了多种函数,用于执行文件和设备(如控制台)的输入输出操作。它是 C 库中最早出现的头文件之一。它声明的函数比其他任何标准头文件都要多,并且由于底层功能的复杂性,也需要更多的解释。

设备无关的输入输出模型多年来得到了显著改善,但其成功很少得到认可。在 1960 年代,FORTRAN II 被宣传为一种机器无关的语言,但实际上将 FORTRAN 程序在不同架构之间迁移几乎是不可能的。在 FORTRAN II 中,你必须在 FORTRAN 语句中直接指定你所使用的设备。因此,在 IBM 7090 上,你使用 READ INPUT TAPE 5 读取磁带,而在其他机器上则使用 READ CARD 来读取卡片图像。FORTRAN IV 引入了更通用的 READWRITE 语句,使用逻辑单元号(LUN)来代替设备名称。设备无关的 I/O 时代到来了。

外设,如打印机,依然对它们所执行的任务有着明确的定义。随后,外设交换工具被发明出来,用来处理各种奇特的设备。当阴极射线管(CRT)显示器出现时,每个控制台的制造商以独立的方式解决了控制台光标移动等问题,进一步增加了复杂性。

Unix 就是在这种氛围下诞生的。Ken Thompson 和 Dennis Ritchie,Unix 的开发者,应该为将大量的创新思想融入操作系统而获得荣誉。他们对设备无关性的处理是最具亮点的之一。

ANSI C <stdio.h> 库基于原始的 Unix 文件 I/O 原语,但它扩展了功能,以适应不同系统之间的最小公分母。

流(Streams)

无论是与物理设备(如终端和磁带驱动器)进行输入输出,还是与支持结构化存储设备的文件进行输入输出,数据都被映射到逻辑数据流中,这些流的属性比它们的输入输出更加统一。支持两种映射形式:文本流和二进制流。

  • 文本流 由一行或多行组成。文本流中的一行由零个或多个字符以及一个终止换行符组成(唯一的例外是,在某些实现中,文件的最后一行不需要终止换行符)。Unix 为所有文本流采用了标准的内部格式。每一行文本都以换行符结束。这是任何程序读取文本时的预期行为,也是程序写入文本时的行为。(这是最基本的约定,如果它不满足附加到 Unix 机器上的文本设备的需求,修正将在系统的外围完成。系统内部不需要做出任何改变。)进入或输出文本流的字符字符串可能需要修改,以符合特定的约定。这会导致文本流输入和输出之间的可能差异。例如,在某些实现中,当输入中的空格字符紧接着换行符时,空格字符会被删除。因此,一般情况下,当数据仅由可打印字符和控制字符(如水平制表符和换行符)组成时,文本流的输入和输出是相等的。

  • 二进制流 则比较直接。二进制流是一个有序的字符序列,可以透明地记录内部数据。写入二进制流的数据将始终与在相同实现下读取的数据相等。然而,二进制流可能会在流的末尾附加一个实现定义的空字符数量。除此之外,二进制流没有需要考虑的其他约定。

在 Unix 中,程序没有任何限制,可以将任意的 8 位二进制代码写入任何打开的文件,或者从适当的存储库中读取而不发生变化。因此,Unix 消除了文本流和二进制流之间的长期区分。

标准流(Standard Streams)

当 C 程序开始执行时,程序会自动打开三个标准流,分别命名为 stdinstdoutstderr。这三个流在每个 C 程序中都会被自动附加。

  • 第一个标准流用于输入缓冲,其他两个用于输出。这些流是字节序列。

考虑以下程序:

 /* 一个示例程序。 */
 int main()
 {
     int var;
     scanf ("%d", &var); /* 使用 stdin 从键盘扫描一个整数。 */
     printf ("%d", var); /* 使用 stdout 打印刚扫描到的整数。 */
     return 0;
 }
 /* 程序结束。 */

默认情况下,stdin 指向键盘,stdoutstderr 指向屏幕。在 Unix 系统下,可能也可以在其他操作系统中通过重定向输入或输出,或者两者都进行重定向。

流指针

出于历史原因,C 语言中表示流的类型被称为 FILE,而不是 stream

<stdio.h> 头文件中定义了一个类型 FILE(通常通过 typedef),它能够处理控制流所需的所有信息,包括文件位置指示符、指向相关缓冲区的指针(如果有的话)、用于记录是否发生了读写错误的错误指示符,以及记录是否已经到达文件末尾的 EOF 指示符。

除非程序员正在编写 <stdio.h> 和其内容的实现,否则直接访问 FILE 的内容被认为是不好的做法。更好的访问方式是通过 <stdio.h> 中的函数来实现。可以说,FILE 类型是面向对象编程的早期示例。

打开和关闭文件

为了打开和关闭文件,<stdio.h> 库提供了三个函数:fopenfreopenfclose

打开文件

#include <stdio.h>
FILE *fopen(const char *filename, const char *mode);
FILE *freopen(const char *filename, const char *mode, FILE *stream);

fopenfreopen 用于打开指定文件,并将其与一个流关联。两者都返回指向控制流的对象的指针,或者在打开操作失败时返回一个空指针。错误指示符和文件结束指示符会被清除,如果打开操作失败,则会设置错误标志。freopenfopen 的不同之处在于,如果 stream 指向的文件已经打开,freopen 会先关闭该文件,并忽略任何关闭时的错误。

mode 参数指定文件打开模式,字符串以以下其中一个序列开始(模式后可以跟附加字符):

  • r 打开一个文本文件进行读取
  • w 截断文件为零长度或创建一个文本文件用于写入
  • a 追加;打开或创建一个文本文件并写入到文件末尾
  • rb 打开一个二进制文件进行读取
  • wb 截断文件为零长度或创建一个二进制文件用于写入
  • ab 追加;打开或创建一个二进制文件并写入到文件末尾
  • r+ 打开文本文件进行更新(读写)
  • w+ 截断文件为零长度或创建一个文本文件进行更新
  • a+ 追加;打开或创建一个文本文件并进行更新
  • r+brb+ 打开二进制文件进行更新(读写)
  • w+bwb+ 截断文件为零长度或创建一个二进制文件进行更新
  • a+bab+ 追加;打开或创建一个二进制文件并进行更新

如果文件不存在或无法读取,使用读取模式(r)打开文件会失败。

使用追加模式(a)打开文件会强制所有随后的写操作都写入到当前文件末尾,而不受 fseek 函数调用的影响。在某些实现中,使用追加模式打开二进制文件时,文件位置指示符可能最初会被置于最后写入的数据之后,这可能是由于空字符填充的原因。

使用更新模式(+)打开文件时,可以在关联的流上进行输入和输出操作。然而,输出操作后不能直接跟输入操作,除非先调用 fflush 函数或文件定位函数(fseekfsetposrewind);输入操作后也不能直接跟输出操作,除非遇到文件末尾。某些实现中,打开(或创建)文本文件时使用更新模式可能会打开(或创建)二进制流。

当打开文件时,只有在可以确定文件不指向交互设备时,流才会是完全缓冲的。

关闭文件

#include <stdio.h>
int fclose(FILE *stream);

fclose 函数将刷新流指向的文件,并关闭文件。任何未写入的缓冲数据都会被提交到宿主环境并写入文件;任何未读取的缓冲数据将被丢弃。流与文件解除关联。如果相关缓冲区是自动分配的,它将被释放。如果流成功关闭,函数返回零;如果检测到任何错误,返回 EOF

流缓冲区函数

  • fflush 函数
#include <stdio.h>
int fflush(FILE *stream);

如果 stream 指向的是输出流或更新流(且最近一次操作不是输入),fflush 函数会将该流中未写入的数据推送到宿主环境中写入文件。对于输入流,fflush 的行为是未定义的。

如果 stream 是空指针,fflush 函数将对所有定义了行为的流执行此刷新操作。

fflush 函数在写入错误发生时返回 EOF,否则返回零。

fflush 函数的存在是因为 C 语言中的流可能具有缓冲输入/输出;即,写入文件的函数实际上是将数据写入 FILE 结构中的缓冲区。如果缓冲区已满,写入函数将调用 fflush 以将缓冲区中的数据实际写入文件。由于 fflush 仅偶尔调用,操作系统的原始写入调用被最小化。

  • setbuf 函数
#include <stdio.h>
void setbuf(FILE *stream, char *buf);

除了不返回值外,setbuf 函数等价于使用 _IOFBF 模式和 BUFSIZ 大小调用 setvbuf 函数,或者(如果 buf 是空指针)使用 _IONBF 模式调用 setvbuf

  • setvbuf 函数
#include <stdio.h>
int setvbuf(FILE *stream, char *buf, int mode, size_t size);

setvbuf 函数只能在流与一个已打开的文件关联之后,且在对该流执行任何其他操作之前调用。参数 mode 决定了流的缓冲方式,如下:

  • _IOFBF:完全缓冲输入/输出
  • _IOLBF:行缓冲输入/输出
  • _IONBF:不缓冲输入/输出

如果 buf 不是空指针,它指向的数组可被用作 setvbuf 函数分配的缓冲区(该缓冲区的生命周期必须至少与打开的流一样长,因此在流关闭之前,包含自动存储持续时间的缓冲区不能被释放)。size 参数指定数组的大小。数组的内容在任何时候都是不确定的。

setvbuf 函数成功时返回零,如果模式无效或请求无法满足,则返回非零值。

修改文件位置指示符的函数

除了读取或写入操作外,stdio.h 库还有五个函数会影响文件位置指示符:fgetposfseekfsetposftellrewind

fseekftell 函数早于 fgetposfsetpos

fgetposfsetpos 函数

#include <stdio.h>
int fgetpos(FILE *stream, fpos_t *pos);
int fsetpos(FILE *stream, const fpos_t *pos);

fgetpos 函数将 stream 指向的流的当前文件位置指示符的值存储在 pos 指向的对象中。存储的值包含未指定的信息,fsetpos 函数可以利用这些信息将流的位置恢复到调用 fgetpos 时的位置。

如果成功,fgetpos 函数返回零;如果失败,fgetpos 函数返回非零值,并将一个实现定义的正值存储在 errno 中。

fsetpos 函数根据 pos 指向的对象的值设置 stream 指向的流的文件位置指示符,pos 应为之前调用 fgetpos 函数时获取的值,且应指向相同的流。

成功调用 fsetpos 函数时,会清除流的文件结束标志,并撤销 ungetc 函数对同一流的影响。在 fsetpos 调用之后,更新流的下一个操作可以是输入或输出。

如果成功,fsetpos 函数返回零;如果失败,fsetpos 函数返回非零值,并将一个实现定义的正值存储在 errno 中。

fseekftell 函数

#include <stdio.h>
int fseek(FILE *stream, long int offset, int whence);
long int ftell(FILE *stream);

fseek 函数用于设置 stream 指向的流的文件位置指示符。

对于二进制流,新的位置是通过将 offset 加到由 whence 指定的位置来获得。stdio.h 中有三个宏,分别是 SEEK_SETSEEK_CURSEEK_END,它们扩展为唯一的值。如果 whence 的值为 SEEK_SET,指定的位置是文件的开头;如果 whence 的值为 SEEK_END,指定的位置是文件的末尾;如果 whence 的值为 SEEK_CUR,指定的位置是当前文件位置。对于二进制流,fseek 调用中 whence 值为 SEEK_END 可能不被有效支持。

对于文本流,offset 要么为零,要么是之前调用 ftell 函数时返回的值,且 whence 必须为 SEEK_SET

fseek 函数仅在无法满足请求时返回非零值。

ftell 函数获取 stream 指向的流的当前文件位置指示符的值。对于二进制流,该值是从文件开头开始的字符数;对于文本流,文件位置指示符包含未指定的信息,可由 fseek 函数使用,用于将文件位置指示符恢复到 ftell 调用时的位置。两个此类返回值之间的差异不一定是读取或写入字符数的有意义度量。

如果成功,ftell 函数返回文件位置指示符的当前值。如果失败,ftell 函数返回 -1L 并将一个实现定义的正值存储在 errno 中。

rewind 函数

#include <stdio.h>
void rewind(FILE *stream);

rewind 函数将 stream 指向的流的文件位置指示符设置为文件的开头。它等价于:

(void)fseek(stream, 0L, SEEK_SET);

rewind 还会清除流的错误指示符。

错误处理函数

  • clearerr 函数
#include <stdio.h>
void clearerr(FILE *stream);

clearerr 函数清除 stream 指向的流的文件结束和错误指示符。

  • feof 函数
#include <stdio.h>
int feof(FILE *stream);

feof 函数测试 stream 指向的流的文件结束指示符,仅在文件结束指示符已设置时返回非零值,否则返回零。

  • ferror 函数
#include <stdio.h>
int ferror(FILE *stream);

ferror 函数测试 stream 指向的流的错误指示符,仅在错误指示符已设置时返回非零值,否则返回零。

perror 函数

#include <stdio.h>
void perror(const char *s);

perror 函数将整数表达式 errno 中的错误号映射为一个错误消息。它将一系列字符写入标准错误流,格式如下:首先,如果 s 不是空指针并且 s 指向的字符不是空字符,输出 s 指向的字符串,后跟冒号(:)和空格;然后是适当的错误消息字符串,最后是换行符。错误消息的内容与调用 strerror 函数时传入 errno 返回的内容相同,这些内容是由实现定义的。

其他文件操作

stdio.h 库提供了多种函数,用于执行除读取和写入之外的文件操作。

remove 函数

#include <stdio.h>
int remove(const char *filename);

remove 函数使得 filename 指向的文件不再能够通过该名称访问。随后尝试使用该名称打开文件将失败,除非文件被重新创建。如果文件已打开,则 remove 函数的行为由实现定义。

remove 函数返回零表示操作成功,非零值表示操作失败。

rename 函数

#include <stdio.h>
int rename(const char *old_filename, const char *new_filename);

rename 函数使得 old_filename 指向的文件从此以后以 new_filename 指向的名称存在。原 old_filename 指向的文件将不再能通过该名称访问。如果在调用 rename 函数之前,new_filename 所指向的文件已经存在,则行为由实现定义。

rename 函数返回零表示操作成功,非零值表示操作失败,此时,如果文件之前存在,它仍然可以通过原名称访问。

tmpfile 函数

#include <stdio.h>
FILE *tmpfile(void);

tmpfile 函数创建一个临时的二进制文件,当文件关闭或程序终止时,该文件会自动被删除。如果程序异常终止,是否删除打开的临时文件由实现定义。该文件以 "wb+" 模式打开,用于更新。

tmpfile 函数返回指向新创建文件流的指针。如果无法创建文件,tmpfile 函数返回空指针。

tmpnam 函数

#include <stdio.h>
char *tmpnam(char *s);

tmpnam 函数生成一个有效的文件名,该文件名不同于现有文件的名称。

每次调用 tmpnam 时,它会生成一个不同的字符串,最多调用 TMP_MAX 次(TMP_MAXstdio.h 中定义的宏)。如果调用次数超过 TMP_MAX,行为由实现定义。

实现应当表现得好像没有任何库函数调用 tmpnam 函数。

如果参数为空指针,tmpnam 函数将结果保存在一个内部静态对象中,并返回该对象的指针。后续调用 tmpnam 函数可能会修改同一个对象。如果参数不是空指针,假设它指向一个至少包含 L_tmpnam 个字符的数组(L_tmpnamstdio.h 中的另一个宏);tmpnam 函数将结果写入该数组,并将参数作为返回值。

TMP_MAX 宏的值必须至少为 25。

从文件读取

字符输入函数

  • fgetc 函数
#include <stdio.h>
int fgetc(FILE *stream);

fgetc 函数从 stream 指向的流中获取下一个字符(如果有),该字符被转换为 unsigned char 类型并返回为 int,并且会推进关联的文件位置指示符(如果已定义)。

如果流到达文件结尾或发生读取错误,fgetc 返回 EOFEOF<stdio.h> 中定义的负值,通常为 -1)。必须使用 feofferror 函数来区分文件结束和错误。如果发生错误,errno 全局变量会被设置,以指示错误。

  • fgets 函数
#include <stdio.h>
char *fgets(char *s, int n, FILE *stream);

fgets 函数从 stream 指向的流中最多读取 n-1 个字符,并将其存入由 s 指向的数组中。在遇到换行符(该换行符会保留)或文件结束符时,不会再读取其他字符。一个空字符会立即被写入数组中的最后一个字符之后。

fgets 函数在成功时返回 s。如果遇到文件结束符且没有读取任何字符到数组中,数组内容保持不变,返回空指针。如果在操作过程中发生读取错误,数组内容未定义,并且返回空指针。

警告: 不同的操作系统可能使用不同的字符序列表示行结束符。例如,一些文件系统在文本文件中使用 \r\n 作为行结束符;fgets 可能会读取这些行,去掉 \n,但保留 \r 作为 s 的最后一个字符。这个多余的字符应该在使用字符串 s 之前被移除(除非程序员不在乎)。Unix 系统通常使用 \n 作为行结束符,MS-DOS 和 Windows 使用 \r\n,而 Mac OS X 之前的版本使用 \r。许多非 Unix 或 Linux 操作系统上的编译器会将文本文件中的换行符序列映射为 \n;检查你的编译器文档以了解它在这种情况下的行为。

示例程序:从标准输入读取并写入标准输出

#include <stdio.h>

#define BUFFER_SIZE 100

int main(void)
{
    char buffer[BUFFER_SIZE]; /* 读取缓冲区 */
    while (fgets(buffer, BUFFER_SIZE, stdin) != NULL)
    {
        printf("%s", buffer);
    }
    return 0;
}
/* 程序结束 */

getc 函数

#include <stdio.h>
int getc(FILE *stream);

getc 函数与 fgetc 等效,不同之处在于它可能实现为宏。如果实现为宏,stream 参数可能被多次求值,因此该参数不应是带有副作用的表达式(例如赋值、递增、递减操作符或函数调用)。

getc 函数从 stream 指向的输入流中返回下一个字符。如果流到达文件结束符,文件结束符指示器会被设置,getc 返回 EOFEOF<stdio.h> 中定义的负值,通常为 -1)。如果发生读取错误,错误指示器会被设置,getc 返回 EOF

getchar 函数

#include <stdio.h>
int getchar(void);

getchar 函数等同于 getc 函数,但其参数是 stdin

getchar 函数从 stdin 指向的输入流中返回下一个字符。如果 stdin 达到文件结束符,文件结束符指示器会被设置,getchar 返回 EOFEOF<stdio.h> 中定义的负值,通常为 -1)。如果发生读取错误,错误指示器会被设置,getchar 返回 EOF

gets 函数

#include <stdio.h>
char *gets(char *s);

gets 函数从 stdin 指向的输入流中读取字符,直到遇到文件结束符或换行符为止。任何换行符都会被丢弃,并且一个空字符会被立即写入数组的最后一个字符后面。

如果成功,gets 返回 s。如果遇到文件结束符且没有字符被读取到数组中,数组的内容保持不变,并返回空指针。如果发生读取错误,数组内容未定,返回空指针。

警告: 现在大多数 C 程序员不推荐使用 gets,因为该函数无法知道程序员希望读取到的缓冲区的大小。

Henry Spencer 的《C 程序员十诫(注释版)》中的第五条命令写道:

你应当检查所有字符串(实际上,所有数组)的边界,因为你所键入的 "foo",终有一天有人会键入 "supercalifragilisticexpialidocious"。

它在注释中提到 gets

正如伟大的蠕虫的事例所证明的那样,这条命令的一个后果是,健壮的生产软件绝不应使用 `gets()`,因为它确实是魔鬼的工具。你的接口应始终告知你的助手数组的边界,拒绝遵守或悄悄忽视此建议的助手应立即被送到 `Rm` 地域,在那里他们无法再对你造成伤害。

在 2018 版 C 标准之前,gets 函数已被弃用。现在推荐程序员使用 fgets 函数。

ungetc 函数

#include <stdio.h>
int ungetc(int c, FILE *stream);

ungetc 函数将由 c 指定的字符(转换为 unsigned char)压回到 stream 指向的输入流中。被压回的字符将按其压入顺序被后续的读取操作返回。成功调用(并且该流没有干扰的文件定位操作)会使文件位置指示符不变。

调用 ungetc 至少保证成功压回一个字符。如果在没有读取或文件定位操作的情况下连续多次调用 ungetc,操作可能会失败。

如果 c 的值等于宏 EOF 的值,操作失败,输入流保持不变。

成功调用 ungetc 函数会清除流的文件结束符指示器。读取或丢弃所有压回字符后,流的文件位置指示符应与调用 ungetc 之前的值相同。对于文本流,在成功调用 ungetc 后,文件位置指示符的值未指定,直到所有被压回的字符被读取或丢弃。对于二进制流,每成功调用一次 ungetc,文件位置指示符会减小;如果调用前它的值为零,调用后其值未定义。

ungetc 函数返回转换后的压回字符,如果操作失败,则返回 EOF

EOF 陷阱

使用 fgetcgetcgetchar 时的一个常见错误是将返回结果赋给 char 类型的变量,然后再与 EOF 比较。以下代码片段展示了这种错误,并提供了正确的做法(使用 int 类型):

错误示范

char c;
while ((c = getchar()) != EOF)
    putchar(c);

正确做法

int c;
while ((c = getchar()) != EOF)
    putchar(c);

考虑一个系统,其中 char 类型为 8 位,表示 256 个不同的值。getchar 可能返回这 256 个可能的字符中的任何一个,同时也可能返回 EOF 来表示文件结束,总共有 257 种不同的返回值。

getchar 的结果被赋给一个 char 类型时,char 类型只能表示 256 个不同的值,因此必然会丢失一些信息——当将 257 项放入 256 个位置时,必然会发生冲突。EOF 值转换为 char 后,会变得无法区分与其数值相同的某个字符。如果该字符出现在文件中,以上示例可能会将其误认为是文件结束符;或者,如果 char 是无符号的,则因为 EOF 是负数,永远不可能与任何无符号字符相等,导致上面的示例无法在文件结束时终止。程序将会无限循环,反复打印将 EOF 转换为 char 后的字符。

然而,如果 char 定义为有符号类型(假设 EOF 的常见值为 -1),这种无限循环的失败模式不会发生(C 标准中,char 类型的符号性依赖于实现)。但是,根本问题在于,如果 EOF 值被定义在 char 类型的范围之外,当它被赋给 char 时,这个值会被截断,无法与完整的 EOF 值匹配,从而无法退出循环。另一方面,如果 EOFchar 的范围内,这就保证了 EOF 和某个字符值之间会发生冲突。因此,无论系统类型如何定义,测试 EOF 时永远不要使用 char 类型。

intchar 类型大小相同的系统(例如与 POSIX 和 C99 标准不兼容的系统)中,即使是“良好”的示例也会遭遇 EOF 和某个字符值的不可区分性。正确的处理方式是,在 getchar 返回 EOF 后,检查 feofferror。如果 feof 表示文件未结束,且 ferror 表示没有错误发生,则可以认为 getchar 返回的 EOF 实际上代表了一个字符。由于大多数程序员认为他们的代码不太可能在这些“大字符”系统上运行,因此这些额外的检查很少被执行。另一种方式是使用编译时断言来确保 UINT_MAX > UCHAR_MAX,这样至少可以防止在这种系统上编译此类假设的程序。


直接输入函数:fread 函数

#include <stdio.h>
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);

fread 函数从 stream 指向的流中读取至多 nmemb 个元素,每个元素的大小为 size,并将其存储到由 ptr 指向的数组中。成功读取的字符数量会使流的文件位置指示符(如果已定义)向前移动。若发生错误,流的文件位置指示符的值将未定义。如果部分元素被读取,其值未定义。

fread 函数返回成功读取的元素数量,如果遇到读取错误或文件结束,返回的数量可能少于 nmemb。如果 sizenmemb 为零,fread 返回零,且数组的内容和流的状态保持不变。


格式化输入函数:scanf 系列函数

#include <stdio.h>
int fscanf(FILE *stream, const char *format, ...);
int scanf(const char *format, ...);
int sscanf(const char *s, const char *format, ...);

fscanf 函数从 stream 指向的流中读取输入,按照由 format 指定的格式进行转换。format 字符串控制可接受的输入序列及如何将其转换为赋值,使用随后的参数作为指向接收转换输入的对象的指针。如果提供的参数不足以满足格式要求,则行为未定义。如果格式字符串被耗尽而仍有参数剩余,则多余的参数会被评估(如常规),但会被忽略。

格式字符串应为一个多字节字符序列,并且应在初始状态下开始和结束。格式由零个或多个指令组成:一个或多个空白字符、一个普通多字节字符(既不是 % 也不是空白字符),或一个转换说明符。每个转换说明符由字符 % 引入。在 % 后,以下内容按顺序出现:

  1. 可选的赋值抑制字符 *
  2. 可选的非零十进制整数,指定最大字段宽度。
  3. 可选的 hl(小写字母 l)或 L,表示接收对象的大小。如果对应的参数是指向 short int 的指针,则转换说明符 din 前应加 h,如果是指向 long int 的指针,则应加 l。同样地,转换说明符 oux 前应加 h(如果对应的参数是指向 unsigned short int 的指针),或者加 l(如果是指向 unsigned long int 的指针)。最后,转换说明符 efg 前应加 l(如果对应的参数是指向 double 的指针),或者加 L(如果是指向 long double 的指针)。如果 hlL 与任何其他格式说明符一起出现,则行为未定义。
  4. 指定要应用的转换类型的字符。有效的转换说明符在下面描述。

fscanf 函数按顺序执行格式中的每个指令。如果某个指令失败,fscanf 函数将返回。失败的原因可能是输入失败(由于无法获取输入字符)或匹配失败(由于输入不符合预期的格式)。

由空白字符组成的指令通过读取输入直到遇到第一个非空白字符(该字符保持未读)或没有更多字符可读取时执行。

由普通多字节字符组成的指令通过读取流中的下一个字符来执行。如果读取到的字符与指令中的字符不同,则该指令失败,且不同的字符及之后的字符保持未读。

转换说明符指令定义了一组匹配的输入序列,按以下步骤执行:

  1. 跳过输入的空白字符(如 isspace 函数所定义),除非说明符包括 [cn。空白字符不会计入指定的字段宽度。
  2. 从流中读取一个输入项,除非说明符包括 n。输入项定义为最长的匹配字符序列,除非该序列超过指定的字段宽度,在这种情况下,它将是该长度的初始子序列。如果输入项的长度为零,则指令执行失败;如果是由于流中没有输入字符而导致的失败,则为输入失败。
  3. % 说明符外,输入项会根据转换说明符转换为适当的类型。如果输入项不是匹配的序列,指令执行失败。如果没有使用 * 来抑制赋值,转换结果会被存储到格式参数后第一个未接收转换结果的对象中。如果该对象类型不合适,或者转换结果无法表示在提供的空间中,则行为未定义。

以下是有效的转换说明符:

  • d
    匹配一个可选符号的十进制整数,其格式与使用 strtol 函数时,基数参数为 10 的预期格式相同。相应的参数应为指向整数的指针。

  • i
    匹配一个可选符号的整数,其格式与使用 strtol 函数时,基数参数为 0 的预期格式相同。相应的参数应为指向整数的指针。

  • o
    匹配一个可选符号的八进制整数,其格式与使用 strtoul 函数时,基数参数为 8 的预期格式相同。相应的参数应为指向无符号整数的指针。

  • u
    匹配一个可选符号的十进制整数,其格式与使用 strtoul 函数时,基数参数为 10 的预期格式相同。相应的参数应为指向无符号整数的指针。

  • x
    匹配一个可选符号的十六进制整数,其格式与使用 strtoul 函数时,基数参数为 16 的预期格式相同。相应的参数应为指向无符号整数的指针。

  • e, f, g
    匹配一个可选符号的浮点数,其格式与使用 strtod 函数时的预期格式相同。相应的参数应为指向浮动类型的指针。

  • s
    匹配一串非空白字符。(对于多字节字符没有特殊处理。)相应的参数应为指向一个足够大的字符数组的指针,以容纳该串字符和一个自动添加的终止空字符。

  • [
    匹配一个非空字符序列(对于多字节字符没有特殊处理),字符来源于一个期望字符集(扫描集)。相应的参数应为指向一个足够大的字符数组的指针,以容纳该序列和一个自动添加的终止空字符。转换说明符包含格式字符串中的所有后续字符,直到包括匹配的右方括号(])。括号中的字符(扫描列表)组成扫描集,除非左括号后面跟的是脱字符(^),此时扫描集包含所有不在扫描列表中的字符。如果转换说明符以 [][^] 开头,则右方括号字符在扫描集中,下一个右方括号字符是匹配的右方括号,结束该说明符;否则,第一个右方括号字符结束该说明符。如果扫描列表中包含 - 字符且它不是第一个字符,也不是第一个字符为 ^ 的第二个字符,也不是最后一个字符,则行为是实现定义的。

  • c
    匹配指定字段宽度(如果未指定字段宽度,则默认为1)个字符的字符序列。(对于多字节字符没有特殊处理。)相应的参数应为指向一个足够大的字符数组的指针,以容纳该序列。不会添加终止字符。

  • p
    匹配一个实现定义的序列集,应与 fprintf 函数中 %p 转换所产生的序列集相同。相应的参数应为指向 void 的指针。输入的解释由实现定义。如果输入项是程序执行过程中先前转换的一个值,则所得的指针应与该值相等;否则,%p 转换的行为是未定义的。

  • n
    不消耗任何输入。相应的参数应为指向整数的指针,该整数用于存储到目前为止从输入流中读取的字符数。执行 %n 指令时,赋值计数不会增加。

  • %
    匹配一个单独的 % 字符;不进行转换或赋值。完整的转换说明符应为 %%

如果转换说明符无效,则行为是未定义的。

E, G, X 也是有效的转换说明符,它们的行为分别与 e, g, 和 x 相同。

输入时遇到文件结束

如果在输入时遇到文件结束(EOF),则转换终止。如果在尚未读取任何符合当前指令的字符时(除去允许的前导空白字符),文件结束发生,当前指令的执行终止并发生输入失败;否则,除非当前指令由于匹配失败而终止,执行后续指令(如果有)将终止并发生输入失败。

如果转换由于输入字符冲突而终止,则冲突的输入字符将留在输入流中。尾随空白字符(包括换行字符)将留在输入流中,除非通过指令进行了匹配。文字匹配和抑制赋值的成功性无法直接判断,除了通过 %n 指令。

fscanf 函数的返回值

如果在任何转换之前发生输入失败,fscanf 函数返回宏 EOF。否则,fscanf 函数返回成功赋值的输入项数量,如果发生早期匹配失败,返回的数量可能少于预期。

scanf 函数等同于 fscanf,只是 stdin 作为第一个参数传递给 scanf。它的返回值与 fscanf 类似。

sscanf 函数等同于 fscanf,不同之处在于,它的参数 s 指定了输入字符串,而不是流。到达字符串的末尾等同于遇到 fscanf 函数的文件结束。如果对象之间发生重叠的复制,则行为是未定义的。

写入文件

字符输出函数

  • fputc 函数

    #include <stdio.h>
    int fputc(int c, FILE *stream);
    

    fputc 函数将字符 c(转换为无符号字符)写入由 stream 指针指示的流中,写入位置由关联的文件位置指示器指示(如果已定义),并相应地更新指示器。如果文件无法支持定位请求,或流以追加模式打开,则字符将追加到输出流中。函数返回写入的字符,除非发生写入错误,在这种情况下,流的错误指示器会被设置,fputc 返回 EOF

  • fputs 函数

    #include <stdio.h>
    int fputs(const char *s, FILE *stream);
    

    fputs 函数将字符串 s 写入由 stream 指针指示的流中。终止空字符不会写入。如果发生写入错误,函数返回 EOF,否则返回一个非负值。

  • putc 函数

    #include <stdio.h>
    int putc(int c, FILE *stream);
    

    putc 函数等同于 fputc,但是如果它作为宏实现,可能会多次评估 stream,因此参数永远不应是具有副作用的表达式。函数返回写入的字符,除非发生写入错误,在这种情况下,流的错误指示器会被设置,函数返回 EOF

  • putchar 函数

    #include <stdio.h>
    int putchar(int c);
    

    putchar 函数等同于 putc,第二个参数为 stdout。它返回写入的字符,除非发生写入错误,在这种情况下,stdout 的错误指示器会被设置,函数返回 EOF

  • puts 函数

    #include <stdio.h>
    int puts(const char *s);
    

    puts 函数将字符串 s 写入 stdout 指向的流中,并在输出后追加一个换行符。终止空字符不会写入。如果发生写入错误,函数返回 EOF,否则返回一个非负值。

直接输出函数:fwrite 函数

#include <stdio.h>
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);

fwrite 函数将由 ptr 指针指向的数组中的最多 nmemb 个元素(每个元素的大小由 size 指定)写入由 stream 指针指向的流中。流的文件位置指示器(如果已定义)会按成功写入的字符数量相应更新。如果发生错误,流的文件位置指示器的结果值是不确定的。函数返回成功写入的元素数量,只有在遇到写入错误时,才会少于 nmemb

格式化输出函数:printf系列函数
#include <stdarg.h>
#include <stdio.h>

int fprintf(FILE *stream, const char *format, ...);
int printf(const char *format, ...);
int sprintf(char *s, const char *format, ...);
int vfprintf(FILE *stream, const char *format, va_list arg);
int vprintf(const char *format, va_list arg);
int vsprintf(char *s, const char *format, va_list arg);

注:某些长度说明符和格式说明符是C99新增的,这些在老版本的编译器和遵循C89/C90标准的stdio库中可能不可用。若有新说明符,会标记为(C99)。

fprintf函数

fprintf函数将输出写入由stream指向的流中,格式化输出由由format指向的字符串控制,该字符串指定了如何将后续参数转换为输出。如果格式中参数不足,行为是未定义的。如果格式用尽但参数仍未消耗完,剩余的参数会被评估(如同往常),但其他情况会被忽略。fprintf函数会在遇到格式字符串的末尾时返回。

格式应该是一个多字节字符序列,开始和结束时处于初始移位状态。格式由零个或多个指令组成:普通的多字节字符(非%),会原样复制到输出流中;转换说明符,每个说明符会获取零个或多个后续参数,根据对应的转换说明符将它们转换后再写入输出流。

转换说明符

每个转换说明符由字符%引入。在%之后,按顺序出现以下内容:

  1. 零个或多个标志(可按任意顺序),用于修改转换说明符的含义。
  2. 可选的最小字段宽度。如果转换后的值字符数少于字段宽度,则会使用空格填充(默认)到字段宽度(如果指定了左对齐标志,填充会在右侧进行)。字段宽度的形式为星号*(稍后描述)或十进制整数。(注意,0被视为标志,而不是字段宽度的起始部分。)
  3. 可选的精度,用于指定d、i、o、u、x、X转换时显示的最小数字个数,a、A、e、E、f、F转换时小数点后的数字个数,g和G转换时的最大有效数字个数,或者s转换时从字符串中写入的最大字符数。精度的形式为一个句点(.),后面跟一个星号*(稍后描述)或一个可选的十进制整数;如果只指定句点,精度取为零。如果精度与其他转换说明符一起出现,行为未定义。浮动点数字会根据精度四舍五入;例如,printf("%1.1f\n", 1.19);会输出1.2。
  4. 可选的长度修饰符,指定参数的大小。
  5. 转换说明符字符,指定应用的转换类型。

如上所述,字段宽度、精度或两者都可以由星号表示。在这种情况下,int类型的参数提供字段宽度或精度。指定字段宽度、精度或两者的参数应出现在待转换参数之前(如果有的话)。负数的字段宽度参数会被视为带有负号的字段宽度。负数的精度参数会被视为省略精度。

标志字符及其含义

  • -
    转换结果在字段内左对齐(默认右对齐)。

  • +
    对于带符号的转换,结果总是以加号或减号开头。如果未指定此标志,负数值才会以负号开头。

  • space
    对于带符号的转换,如果转换结果没有符号,或者没有字符显示,结果前会加一个空格。如果同时指定了+和空格标志,空格标志会被忽略。

  • #
    结果采用“替代形式”进行转换。对于o转换,只有在必要时,精度会增加,以保证结果的第一个数字为0(如果值和精度都为0,则输出一个单独的0)。对于x(或X)转换,非零结果会在前面加上0x(或0X)。对于a、A、e、E、f、F、g和G转换,结果始终包含小数点字符,即使后面没有数字。

  • 0
    对于d、i、o、u、x、X、a、A、e、E、f、F、g和G转换,数字前会用零填充(在符号或进制标志之后),直到达到字段宽度;不使用空格填充。如果0和-标志同时出现,0标志会被忽略。

  • hh
    (C99)指定随后的d、i、o、u、x或X转换说明符适用于signed char或unsigned char类型的参数(该参数将根据整数提升规则进行提升,但其值在打印前应转换为signed char或unsigned char);或者,指定随后的n转换说明符适用于指向signed char类型参数的指针。

  • h
    指定随后的d、i、o、u、x或X转换说明符适用于short int或unsigned short int类型的参数(该参数将根据整数提升规则进行提升,但其值在打印前应转换为short int或unsigned short int);或者,指定随后的n转换说明符适用于指向short int类型参数的指针。

  • l (ell)
    指定随后的d、i、o、u、x或X转换说明符适用于long int或unsigned long int类型的参数;指定随后的n转换说明符适用于指向long int类型参数的指针;(C99)指定随后的c转换说明符适用于wint_t类型的参数;(C99)指定随后的s转换说明符适用于指向wchar_t类型的指针;或者,对于随后的a、A、e、E、f、F、g或G转换说明符无影响。

  • ll (ell-ell)
    (C99)指定随后的d、i、o、u、x或X转换说明符适用于long long int或unsigned long long int类型的参数;或者,指定随后的n转换说明符适用于指向long long int类型参数的指针。

  • j
    (C99)指定随后的d、i、o、u、x或X转换说明符适用于intmax_t或uintmax_t类型的参数;或者,指定随后的n转换说明符适用于指向intmax_t类型参数的指针。

  • z
    (C99)指定随后的d、i、o、u、x或X转换说明符适用于size_t或其对应的有符号整数类型的参数;或者,指定随后的n转换说明符适用于指向与size_t类型参数对应的有符号整数类型的指针。

  • t
    (C99)指定随后的d、i、o、u、x或X转换说明符适用于ptrdiff_t或其对应的无符号整数类型的参数;或者,指定随后的n转换说明符适用于指向ptrdiff_t类型参数的指针。

  • L
    指定随后的a、A、e、E、f、F、g或G转换说明符适用于long double类型的参数。

    如果长度修饰符与其他任何未在上面指定的转换说明符一起出现,行为是未定义的。

    转换说明符及其含义:
  • d, i
    将int类型的参数转换为带符号的十进制数,格式为[−]dddd。精度指定显示的最小数字个数;如果转换的值可以用更少的数字表示,则会用前导零填充。默认精度为1。将零值转换为零精度时,不会输出任何字符。

  • o, u, x, X
    将unsigned int类型的参数转换为无符号的八进制(o)、无符号十进制(u)或无符号十六进制(x或X)表示,格式为dddd;x转换使用字母abcdef,X转换使用字母ABCDEF。精度指定显示的最小数字个数;如果转换的值可以用更少的数字表示,则会用前导零填充。默认精度为1。将零值转换为零精度时,不会输出任何字符。

  • f, F
    将double类型的浮动点数转换为十进制表示,格式为[−]ddd.ddd,其中小数点后数字的个数等于精度指定的值。如果没有指定精度,默认为6;如果精度为零且未指定#标志,则不会显示小数点字符。如果显示了小数点字符,则至少有一位数字在其前面。该值将四舍五入至适当的位数。
    (C99)如果double类型参数表示正无穷大或负无穷大,则会转换为[-]inf或[-]infinity形式(具体样式由实现定义)。如果参数为NaN,则转换为[-]nan或[-]nan(n-char-sequence)形式(具体样式及n-char-sequence的含义由实现定义)。F转换说明符会输出INF、INFINITY或NAN,而不是inf、infinity或nan。对于无限值和NaN值,-、+和空格标志保持其常规含义;#和0标志无效。

  • e, E
    将double类型的浮动点数转换为科学计数法表示,格式为[−]d.ddde±dd,其中小数点前有一位非零数字(如果参数不为零),小数点后有指定精度的数字;如果没有指定精度,默认为6;如果精度为零且未指定#标志,则不会显示小数点字符。该值将四舍五入至适当的位数。E转换说明符会将指数部分用E代替e来表示,且指数部分始终有至少两位数字,并根据需要显示更多数字。如果值为零,则指数部分为零。
    (C99)如果double类型参数表示正无穷大或NaN,则转换为f或F转换说明符的样式。

  • g, G
    将表示(有限)浮动点数的double类型参数转换为f或e样式(如果是G转换说明符,则为F或E样式),其中精度指定有效数字的位数。如果精度为零,则视为1。使用的样式取决于转换后的值;只有当转换后的指数小于–4或大于或等于精度时,才使用e样式(或E样式)。如果指定了精度,结果中的尾随零将被去除,除非指定了#标志;只有当小数点后跟着数字时,小数点字符才会显示。
    (C99)如果double类型的参数表示正无穷大或NaN,则转换为f或F转换说明符的样式。

  • a, A
    (C99)将表示(有限)浮动点数的double类型参数转换为样式[−]0xh.hhhhp±d,其中小数点前有一个十六进制数字(如果参数是标准化的浮动点数,则该数字非零,否则未指定),小数点字符前的十六进制数字个数等于精度;如果未指定精度且FLT_RADIX为2的幂,则精度足以精确表示值;如果未指定精度且FLT_RADIX不是2的幂,则精度足以区分(精度p足以区分源类型的值,如果16p–1 > bn,其中b是FLT_RADIX,n是源类型的尾数中的基b数字个数)。如果精度为零且未指定#标志,则不会显示小数点字符。a转换使用字母abcdef,而A转换使用字母ABCDEF。A转换说明符会将x和p替换为X和P。指数部分总是包含至少一个数字,且只包含表示二进制指数所需的数字。如果值为零,则指数为零。
    如果double类型的参数表示正无穷大或NaN,则转换为f或F转换说明符的样式。

  • c
    如果没有l长度修饰符,则int类型的参数将转换为unsigned char,结果字符会被写入。
    (C99)如果存在l长度修饰符,则wint_t类型的参数会按没有精度的ls转换说明符的方式转换,并且该参数指向一个包含两个wchar_t元素的数组,数组的第一个元素包含wint_t参数,第二个元素包含一个空宽字符。

  • s
    如果没有l长度修饰符,则参数必须是指向字符类型数组的初始元素的指针。(对于多字节字符没有特别规定。)数组中的字符会被写入,直到(但不包括)终止的null字符。如果指定了精度,则最多写入该数量的字符。如果未指定精度或精度大于数组大小,则数组必须包含一个null字符。
    (C99)如果存在l长度修饰符,则参数必须是指向wchar_t类型数组的初始元素的指针。数组中的宽字符会被转换为多字节字符(每个字符就像调用wcrtomb函数一样转换,转换状态由mbstate_t对象描述,该对象在转换第一个宽字符之前初始化为零),直到并包括终止的null宽字符。结果的多字节字符会被写入,直到(但不包括)终止的null字符(字节)。如果未指定精度,则数组必须包含一个null宽字符。如果指定了精度,则最多写入该数量的字符(字节)(包括任何转义序列),如果要使多字节字符序列的长度与精度相等,数组必须包含一个超出数组范围的宽字符。在任何情况下,不会写入部分的多字节字符。(如果多字节字符有依赖状态的编码,可能会产生冗余的转义序列。)

  • p
    参数必须是指向void的指针。指针的值将转换为一系列可打印字符,具体方式由实现定义。

  • n
    参数必须是指向有符号整数的指针,写入到该整数的值为到目前为止通过此调用的fprintf函数写入输出流的字符数。没有转换任何参数,但会消耗一个参数。如果转换说明符包括任何标志、字段宽度或精度,行为是未定义的。

  • %
    写入一个%字符。没有转换任何参数。完整的转换说明符应为%%。

如果转换说明符无效,则行为是未定义的。如果任何参数的类型不正确,导致与相应转换说明符不匹配,则行为是未定义的。

在任何情况下,不存在或过小的字段宽度不会导致字段被截断;如果转换结果的宽度大于字段宽度,字段会扩展以包含转换结果。

对于a和A转换,如果FLT_RADIX是2的幂,值将根据给定精度正确四舍五入为十六进制浮动数。

如果FLT_RADIX不是2的幂,建议做法是:结果应为在十六进制浮动样式中,精度相等的两个相邻数字之一,并额外规定错误应具有当前舍入方向的正确符号。

对于e、E、f、F、g和G转换,建议做法是:如果有效的十进制数字个数最多为DECIMAL_DIG,则结果应正确四舍五入。(对于二进制到十进制的转换,结果格式的值是用给定格式说明符可表示的数字。有效数字个数由格式说明符决定,在固定点转换的情况下,也由源值决定。)如果有效数字个数大于DECIMAL_DIG,但源值恰好可以用DECIMAL_DIG位表示,则结果应是精确表示,并带有尾随零。否则,源值被限定在两个相邻的十进制字符串L < U之间,且它们都包含DECIMAL_DIG位有效数字;结果的十进制字符串D应满足L ≤ D ≤ U,并且额外规定错误应具有当前舍入方向的正确符号。

fprintf函数返回传输的字符数,或者如果发生输出或编码错误,则返回负值。

printf函数等价于fprintf,参数stdout位于printf的参数之前。它返回传输的字符数,或者如果发生输出错误,则返回负值。

sprintf函数等价于fprintf,除了参数s指定一个数组,生成的输入将写入该数组,而不是输出流。写入的字符后会写入一个null字符;该字符不计入返回值。若在重叠的对象之间进行复制,行为是未定义的。该函数返回写入数组的字符数,不包括终止的null字符。

vfprintf函数等价于fprintf,变量参数列表由arg替换,arg应通过va_start宏进行初始化(并可能通过后续的va_arg调用)。vfprintf函数不会调用va_end宏。该函数返回传输的字符数,或者如果发生输出错误,则返回负值。

vprintf函数等价于printf,变量参数列表由arg替换,arg应通过va_start宏进行初始化(并可能通过后续的va_arg调用)。vprintf函数不会调用va_end宏。该函数返回传输的字符数,或者如果发生输出错误,则返回负值。

vsprintf函数等价于sprintf,变量参数列表由arg替换,arg应通过va_start宏进行初始化(并可能通过后续的va_arg调用)。vsprintf函数不会调用va_end宏。如果在重叠的对象之间进行复制,行为是未定义的。该函数返回写入数组的字符数,不包括终止的null字符。

最后修改: 2025年01月12日 星期日 13:28