文章目录
- 第22章 输入/输出
- 22.1 流
- 22.1.1 文件指针
- 22.1.2 标准流和重定向
- 22.1.3 文本文件与二进制文件
- 22.2 文件操作
- 22.2.1 打开文件
- 22.2.2 模式
- 22.2.3 关闭文件
- 22.2.4 为打开的流附加文件
- 22.2.5 从命令行获取文件名
- 22.2.6 临时文件
- 22.2.7 文件缓冲
- 22.2.8 其他文件操作
- 22.3 格式化的输入/输出
- 22.3.1 ...printf函数
- 22.3.2 ...printf转换说明
- 22.3.3. C99对...printf转化说明的修改(C99)
- 22.3.4 ...printf转换说明示例
- 22.3.5 ...scanf函数
- 22.3.6 ..scanf格式串
- 22.3.7 ...scanf转换说明
- 22.3.8 C99对...scanf转换说明的改变(C99)
- 22.3.9 scanf示例
- 22.3.10 检测文件末尾和错误条件
- 22.4 字符的输入/输出
- 22.4.1 输出函数
- 22.4.2 输入函数
- 22.4.2.1 程序——复制文件
- 22.5 行的输入/输出
- 22.5.1 输出函数
- 22.5.2 输入函数
- 22.6 块的输入/输出
- 22.7 文件定位
- 22.7.1 程序——修改零件记录文件
- 22.8 字符串的输入/输出
- 22.8.1 输出函数
- 22.8.2 输入函数
- 问与答
- 写在最后
第22章 输入/输出
——在人与机器共存的世界中,懂得思变的一定是人,别指望机器。
C
语言的输入/输出库是标准库中最大且最重要的部分。由于输入/输出是C
语言的高级应用,因此这里将用一整章(篇幅最长)来讨论<stdio.h>
头——输入/输出函数的主要存储位置。从第
2
章开始,我们已经在使用<stdio.h>
了,而且已经对printf
函数、scanf
函数、putchar
函数、getchar
函数、puts
函数以及gets
函数的使用有了一定的了解。本章会提供有关这6
个函数的更多信息,并介绍一些新的用于文件处理的函数。值得高兴的是,许多新函数和我们已经熟知的函数有着紧密的联系。例如,fprintf
函数就是printf
函数的“文件版”。本章将首先讨论一些基本问题:流的概念、
FILE
类型、输入和输出重定向,以及文本文件和二进制文件的差异(22.1节)
。随后将讨论特别为使用文件而设计的函数,包括打开和关闭文件的函数(22.2节)
。在讨论完printf
函数、scanf
函数以及与“格式化”输入/输出相关的函数(22.3节)
后,我们将着眼于读/写非格式化数据的函数。
- 每次读写一个字符的
getc
函数、putc
函数以及相关的函数(22.4节
)。 - 每次读写一行字符的
gets
函数、puts
函数以及相关的函数(22.5节
)。 - 读/写数据块的
fread
函数和fwrite
函数(22.6节
)。
随后,
22.7节
会说明如何对文件执行随机的访问操作。最后,22.8节
会描述sprintf
函数、snprintf
函数和sscanf
函数,它们是printf
函数和scanf
函数的变体,后两者分别用于写入和读取一个字符串。本章涵盖了
<stdio.h>
中的绝大部分函数,但忽略了其中8
个函数。perror
函数是这8
个函数中的一个,它与<errno.h>
头紧密相关,所以把它推迟到24.2节
讨论<errno.h>
头时再来介绍。26.1节
涵盖了其余7
个函数(vfprintf
、vprintf
、vsprintf
、vsnprintf
、vfscanf
、vscanf
和vsscanf
)。这些函数依赖于va_list
类型,该类型在26.1节
介绍。在
C89
中,所有的标准输入/输出函数都属于<stdio.h>
。但从C99
开始有所不同,有些输入/输出函数在<wchar.h>头(25.5节)
中声明。<wchar.h>
中的函数用于处理宽字符而不是普通字符,但大多数函数与<stdio.h>
中的函数紧密相关。<stdio.h>
中用于读或写数据的函数称为字节输入/输出函数,而<wchar.h>
中的类似函数则称为宽字符输入/输出函数。
22.1 流
在
C
语言中,术语流(stream)
表示任意输入的源或任意输出的目的地。许多小型程序(就像前面章节中介绍的那些)都是通过一个流(通常和键盘
相关)获得全部的输入,并且通过另一个流(通常和屏幕
相关)写出全部的输出。
较大规模的程序可能会需要额外的流。这些流常常表示存储在不同介质(如硬盘驱动器、CD
、DVD
和闪存)上的文件,但也很容易和不存储文件的设备(如网络端口、打印机等)相关联。这里将集中讨论文件,因为它们常见且容易理解。但是,请千万记住一点:<stdio.h>
中的许多函数可以处理各种形式的流,而不仅限于表示文件的流。
22.1.1 文件指针
C
程序中对流的访问是通过文件指针(file pointer)
实现的。此指针的类型为FILE *
(FILE
类型在<stdio.h>
中声明)。用文件指针表示的特定流具有标准的名字;如果需要,还可以声明另外一些文件指针。例如,如果程序除了标准流之外还需要两个流,则可以包含如下声明:
FILE *fp1, *fp2;
虽然操作系统通常会限制可以同时打开的流的数量,但程序可以声明任意数量的FILE *
类型变量。
22.1.2 标准流和重定向
<stdio.h>
提供了3
个标准流(见表22-1
)。这3
个标准流可以直接使用,不需要对其进行声明,也不用打开或关闭它们。
表22-1 标准流
文件指针 | 流 | 默认的含义 |
---|---|---|
stdin | 标准输入 | 键盘 |
stdout | 标准输出 | 屏幕 |
stderr | 标准误差 | 屏幕 |
前面章节使用过的函数(printf
、scanf
、putchar
、getchar
、puts
和gets
)都是通过stdin
获得输入,并且用stdout
进行输出的。默认情况下,stdin
表示键盘,stdout
和stderr
表示屏幕。然而,许多操作系统允许通过一种称为重定向(redirection)
的机制来改变这些默认的含义。
通常,我们可以强制程序从文件而不是从键盘获得输入,方法是在命令行中放上文件的名字,并在前面加上字符
<
:
demo <in.dat
这种方法叫作输入重定向(input redirection)
,它本质上是使stdin
流表示文件(此例中为文件in.dat
)而非键盘。重定向的绝妙之处在于,demo
程序不会意识到正在从文件in.dat
中读取数据,它会认为从stdin
获得的任何数据都是从键盘输入的。
输出重定向(output redirection)
与之类似。对stdout
流的重定向通常是通过在命令行中放置文件名,并在前面加上字符>
实现的:
demo >out.da
现在所有写入stdout
的数据都将进入out.dat
文件中,而不是出现在屏幕上。
顺便说一下,我们还可以把输出重定向和输入重定向结合使用:
demo <in.dat >out.da
字符<
和>
不需要与文件名相邻,重定向文件的顺序也是无关紧要的,所以下面的例子是等效的:
demo < in.dat > out.dat
demo >out.dat <in.d
输出重定向的一个问题是,会把写入stdout
的所有内容都放入文件中。如果程序运行失常并且开始写出错消息,那么我们在看文件的时候才会知道,而这些应该是出现在stderr
中的。通过把出错消息写到stderr
而不是stdout
中,可以保证即使在对stdout
进行重定向时,这些出错消息仍能出现在屏幕上。(不过,操作系统通常也允许对stderr
进行重定向。)
22.1.3 文本文件与二进制文件
<stdio.h>
支持两种类型的文件:文本文件和二进制文件。在文本文件(text file)
中,字节表示字符,这使人们可以检查或编辑文件。例如,C
程序的源代码是存储在文本文件中的。另外,在二进制文件(binary file)
中,字节不一定表示字符,字节组还可以表示其他类型的数据,比如整数和浮点数。如果试图查看可执行C
程序的内容,你会立刻意识到它是存储在二进制文件中的。
文本文件具有2
种二进制文件没有的特性:
- 文本文件分为若干行。文本文件的每一行通常以一两个特殊字符结尾, 特殊字符的选择与操作系统有关。在
Windows
中,行末的标记是回车符('\x0d')
与一个紧跟其后的回行符('\x0a')
。在UNIX
和Macintosh
操作系统(Mac OS
)的较新版本中,行末的标记是一个单独的回行符。旧版本的Mac OS
使用一个单独的换行符。 - 文本文件可以包含一个特殊的“文件末尾”标记。一些操作系统允许在文本文件的末尾使用一个特殊的字节作为标记。在
Windows
中,标记为'\x1a'(Ctrl+Z)
。Ctrl+Z
不是必需的,但如果存在,它就标志着文件的结束,其后的所有字节都会被忽略。使用Ctrl+Z
的这一习惯继承自DOS
,而DOS
中的这一习惯又是从CP/M
(早期用于个人计算机的一种操作系统)来的。大多数其他操作系统(包括UNIX
)没有专门的文件末尾字符。
二进制文件不分行
,也没有行末标记和文件末尾标记
,所有字节都是平等对待
的。
向文件写入数据时,我们需要考虑是按文本格式存储还是按二进制格式来存储。为了搞清楚其中的差别,考虑在文件中存储数
32767
的情况。一种选择是以文本的形式把该数按字符3
、2
、7
、6
、7
写入。假设字符集为ASCII
,那么就可以得到下列5
个字节:
00110011 | 00110010 | 00110111 | 00110110 | 00110111 |
---|---|---|---|---|
‘3’ | ‘2’ | ‘7’ | ‘6’ | ‘7’ |
另一种选择是以二进制的形式存储此数,这种方法只会占用2
个字节:
01111111 11111111
[在按小端顺序(20.3节)
存储数据的系统中,这两个字节的顺序相反。]从上述示例可以看出,用二进制形式存储数可以节省相当大的空间。
编写用来读写文件的程序时,需要考虑该文件是文本文件还是二进制文件。在屏幕上显示文件内容的程序可能要把文件视为文本文件。但是,文件复制程序就不能认为要复制的文件是文本文件。如果那样做,就不能完全复制含有文件末尾字符的二进制文件了。在无法确定文件是文本形式还是二进制形式时,安全的做法是把文件假定为二进制文件。
22.2 文件操作
简单性是输入和输出重定向的魅力之一:不需要打开文件、关闭文件或者执行任何其他的显式文件操作。可惜的是,重定向在许多应用程序中受到限制。当程序依赖重定向时,它无法控制自己的文件,甚至无法知道这些文件的名字。更糟糕的是,如果程序需要在同一时间读入两个文件或者写出两个文件,重定向都无法做到。
当重定向无法满足需要时,我们将使用<stdio.h>
提供的文件操作。本节将探讨这些文件操作,包括打开文件、关闭文件、改变缓冲文件的方式、删除文件以及重命名文件。
22.2.1 打开文件
FILE *fopen(const char * restrict filename, const char * restrict mode);
如果要把文件用作流,打开时就需要调用fopen
函数。fopen
函数的第一个参数是含有要打开文件名的字符串。(“文件名”
可能包含关于文件位置的信息,如驱动器符或路径。)第二个参数是“模式字符串”
,它用来指定打算对文件执行的操作。例如,字符串"r"
表明将从文件读入数据,但不会向文件写入数据。
注意!!在fopen
函数的原型中,restrict关键字(17.8节)
出现了两次。restrict
是从C99
开始引入的关键字,表明filename
和mode
所指向的字符串的内存单元不共享。C89
中的fopen
原型不包含restrict
,但也有这样的要求。restrict
对fopen
的行为没有影响,因此通常可以忽略。
请注意!!提醒
Windows
程序员:在fopen
函数调用的文件名中含有字符\
时,一定要小心。这是因为C
语言会把字符\
看作转义序列(7.3节)
的开始标志。fopen("c:\project\test1.dat", "r");
以上调用会失败,因为编译器会把
\t
看作转义字符。(\p
不是有效的转义字符,但看上去像。根据C
标准,\p
的含义是未定义的。)有两种方法可以避免这一问题。一种方法是用``\代替\
:fopen("c:\\project\\test1.dat", "r"); //另一种方法更简单,只要用/代替\就可以了: fopen("c:/project/test1.dat", "r");
Windows
会把/
认作目录分隔符。
fopen
函数返回一个文件指针。程序可以(且通常)把此指针存储在一个变量中,稍后在需要对文件进行操作时使用它。fopen
函数的常见调用形式如下所示,其中fp
是FILE*
类型的变量:
fp = fopen("in.dat", "r"); /* opens in.dat for reading */
当程序稍后调用输入函数从文件in.dat
中读数据时,会把fp
作为一个实际参数。
当无法打开文件时,fopen
函数会返回空指针。这可能是因为文件不存在
,也可能是因为文件的位置不对
,还可能是因为我们没有打开文件的权限
。
请注意!!永远不要假设可以打开文件,每次都要测试
fopen
函数的返回值以确保不是空指针。
22.2.2 模式
给
fopen
函数传递哪种模式字符串不仅依赖于稍后将要对文件采取的操作,还取决于文件中的数据是文本形式还是二进制形式。要打开一个文本文件,可以采用表22-2
中的一种模式字符串:
表22-2 用于文本文件的模式字符串
字符串 | 含义 |
---|---|
“r” | 打开文件用于读 |
“w” | 打开文件用于写(文件不需要存在) |
“wx” | 创建文件用于写(文件不能已经存在)① |
“w+x” | 创建文件用于更新(文件不能已经存在)① |
“a” | 打开文件用于追加(文件不需要存在) |
“r+” | 打开文件用于读和写,从文件头开始 |
“w+” | 打开文件用于读和写(如果文件存在就截去) |
“a+” | 打开文件用于读和写(如果文件存在就追加) |
① 从C11
开始引入的模式(独占的创建-打开模式)。
当使用
fopen
打开二进制文件时,需要在模式字符串中包含字母b
。表22-3
列出了用于二进制文件的模式字符串。
表22-3 用于二进制文件的模式字符串
字符串 | 含义 |
---|---|
“rb” | 打开文件用于读 |
“wb” | 打开文件用于写(文件不需要存在) |
“wbx” | 创建文件用于写(文件不能已经存在)① |
“ab” | 打开文件用于追加(文件不需要存在) |
“r+b"或者"rb+” | 打开文件用于读和写,从文件头开始 |
“w+b"或者"wb+” | 打开文件用于读和写(如果文件存在就截去) |
“w+bx"或者"wb+x” | 创建文件用于更新(文件不能已经存在)① |
“a+b"或者"ab+” | 打开文件用于读和写(如果文件存在就追加) |
① 从C11
开始引入的模式(独占的创建-打开模式)。
从表22-2
和表22-3
可以看出<stdio.h>
对写数据和追加数据进行了区分。当给文件写数据时,通常会对先前的内容进行覆盖。然而,当为追加打开文件时,向文件写入的数据添加在文件末尾,因而可以保留文件的原始内容。另外,带有字母“x”
的打开模式是从C11
才开始引入的,这个字母表示独占模式
。在这种模式下,如果文件已经存在或者无法创建,fopen
函数将执行失败;否则文件将以独占(非共享)模式打开。
顺便说一下,当打开文件用于读和写(模式字符串包含字符+
)时,有一些特殊的规则。如果没有先调用一个文件定位函数(22.7节)
,那么就不能从读模式转换成写模式,除非读操作遇到了文件的末尾。类似地,如果既没有调用fflush
函数(稍后会介绍)也没有调用文件定位函数,那么就不能从写模式转换成读模式。
22.2.3 关闭文件
int fclose(FILE *stream);
fclose
函数允许程序关闭不再使用的文件。fclose
函数的参数必须是文件指针,此指针来自fopen
函数或freopen
函数(本节稍后会介绍)的调用。如果成功关闭了文件,fclose
函数会返回零;否则,它会返回错误代码EOF
(在<stdio.h>
中定义的宏)。
为了说明如何在实践中使用fopen
函数和fclose
函数,下面给出了一个程序的框架。此程序打开文件example.dat
进行读操作,并要检查打开是否成功,然后在程序终止前再把文件关闭:
#include <stdio.h>
#include <stdlib.h> #define FILE_NAME "example.dat" int main(void)
{ FILE *fp; fp = fopen(FILE_NAME, "r"); if (fp == NULL) { printf("Can’t open %s\n", FILE_NAME); exit(EXIT_FAILURE); } ... fclose(fp); return 0;
}
当然,按照C
程序员的编写习惯,通常也可以把fopen
函数的调用和fp
的声明结合在一起使用:
FILE *fp = fopen(FILE_NAME, "r");
还可以把函数调用与NULL
判定相结合:
if ((fp = fopen(FILE_NAME, "r")) == NULL) ...
22.2.4 为打开的流附加文件
FILE *freopen(const char * restrict filename, const char * restrict mode, FILE * restrict stream);
freopen
函数为已经打开的流附加一个不同的文件。最常见的用法是把文件和一个标准流(stdin、stdout 或stderr)
相关联。例如,为了使程序开始往文件foo
中写数据,可以使用下列形式的freopen
函数调用:
if (freopen("foo", "w", stdout) == NULL) { /* error; foo can’t be opened */
}
在关闭了先前(通过命令行重定向或者之前的freopen
函数调用)与stdout
相关联的所有文件之后,freopen
函数将打开文件foo
,并将其与stdout
相关联。
freopen
函数的返回值通常是它的第三个参数(一个文件指针)。如果无法打开新文件,那么freopen
函数会返回空指针。(如果无法关闭旧的文件,那么freopen
函数会忽略错误。)
从C99
开始新增了一种机制。如果filename
是空指针,freopen
会试图把流的模式修改为mode
参数指定的模式。不过,具体的实现可以不支持这种特性;如果支持,则可以限定能进行哪些模式改变。
22.2.5 从命令行获取文件名
当正在编写的程序需要打开文件时,马上会出现一个问题:
如何把文件名提供给程序呢?
把文件名嵌入程序自身的做法不太灵活,而提示用户输入文件名的做法也很笨拙。通常,最好的解决方案是让程序从命令行获取文件的名字。例如,当执行名为demo
的程序时,可以通过把文件名放入命令行的方法为程序提供文件名:
demo names.dat dates.dat
在13.7
节中,我们了解到如何通过定义带有两个形式参数的main
函数来访问命令行参数:
int main(int argc, char *argv[])
{ ...
}
argc
是命令行参数的数量,而argv
是指向参数字符串的指针数组。argv[0]
指向程序的名字,从argv[1]
到argv[argc-1]
都指向剩余的实际参数,而argv[argc]
是空指针。在上述例子中,argc
是3
,argv[0]
指向含有程序名的字符串,argv[1]
指向字符串"names.dat"
,而argv[2]
则指向字符串"dates.dat"
。
下面举例一个程序,该程序判断文件是否存在,如果存在,则判断它是否可以打开并读入。在运行程序时,用户将给出要检查的文件的名字:
canopen file
然后程序将显示出file can be opened
或者显示出file can't be opened
。如果在命令行中输入的实际参数的数量不对,那么程序将显示出消息usage: canopen filename
来提醒用户canopen
需要一个文件名。
/*
canopen.c
--Checks whether a file can be opened for reading
*/
#include <stdio.h>
#include <stdlib.h> int main(int argc, char *argv[])
{ FILE *fp;if (argc != 2) { printf("usage: canopen filename\n"); exit(EXIT_FAILURE); }if ((fp = fopen(argv[1], "r")) == NULL) { printf("%s can’t be opened\n", argv[1]); exit(EXIT_FAILURE); } printf("%s can be opened\n", argv[1]); fclose(fp); return 0;
}
注意!!可以使用重定向来丢弃canopen
的输出,并简单地测试它返回的状态值。
22.2.6 临时文件
FILE *tmpfile(void);
char *tmpnam(char *s);
现实世界中的程序经常需要产生临时文件,即只在程序运行时存在的文件
。例如,C
编译器就常常产生临时文件。编译器可能先把C
程序翻译成一些存储在文件中的中间形式,稍后把程序翻译成目标代码时,编译器会读取这些文件。一旦程序完全通过了编译,就不再需要保留那些含有程序中间形式的文件了。<stdio.h>
提供了两个函数用来处理临时文件,即tmpfile
函数和tmpnam
函数。
tmpfile
函数创建一个临时文件(用"wb+"
模式打开),该临时文件将一直存在,除非关闭它或程序终止。tmpfile
函数的调用会返回文件指针,此指针可以用于稍后访问该文件:
FILE *tempptr;
...
tempptr = tmpfile(); /* creates a temporary file */
//如果创建文件失败,tmpfile函数会返回空指针。
虽然tmpfile
函数很易于使用,但它有两个缺点:
- 无法知道
tmpfile
函数创建的文件名是什么; - 无法在以后使文件变为永久的。如果这些缺陷导致了问题,备选的解决方案就是用
fopen
函数产生临时文件。当然,我们不希望此文件拥有和前面已经存在的文件相同的名字,因此需要一种方法来产生新的文件名。这也是tmpnam
函数出现的原因。
tmpnam
函数为临时文件产生名字。如果它的实际参数是空指针,那么tmpnam
函数会把文件名存储到一个静态变量中,并且返回指向此变量的指针:
char *filename;
...
filename = tmpnam(NULL); /* creates a temporary file name */
否则,tmpnam
函数会把文件名复制到程序员提供的字符数组中:
char filename[L_tmpnam];
...
tmpnam(filename); /* creates a temporary file name */
在后一种情况下,tmpnam
函数也会返回指向数组第一个字符的指针。L_tmpnam
是<stdio.h>
中的一个宏,它指明了保存临时文件名的字符数组的长度。
请注意!!确保
tmpnam
函数所指向的数组至少有L_tmpnam
个字符。此外,还要当心不能过于频繁地调用tmpnam
函数。宏TMP_MAX
(在<stdio.h>
中定义)指明了程序执行期间由tmpnam
函数产生的临时文件名的最大数量。如果生成文件名失败,tmpnam
返回空指针。
22.2.7 文件缓冲
int fflush(FILE *stream);
void setbuf(FILE * restrict stream, char * restrict buf);
int setvbuf(FILE * restrict stream, char * restrict buf, int mode, size_t size);
向磁盘驱动器传入数据或者从磁盘驱动器传出数据都是相对较慢的操作。因此,在每次程序想读或写字符时都直接访问磁盘文件是不可行的。获得较好性能的诀窍就是缓冲(buffering)
:把写入流的数据存储在内存的缓冲区域内;当缓冲区满了(或者关闭流)时,对缓冲区进行“清洗”(写入实际的输出设备)。输入流可以用类似的方法进行缓冲:缓冲区包含来自输入设备的数据;从缓冲区读数据而不是从设备本身读数据。缓冲可以大幅提升效率,因为从缓冲区读字符或者在缓冲区内存储字符几乎不花什么时间。当然,把缓冲区的内容传递给磁盘,或者从磁盘传递给缓冲区是需要花时间的,但是一次大的“块移动”比多次小字节移动要快很多。
<stdio.h>
中的函数会在缓冲有用时自动进行缓冲操作。缓冲是在后台发生的,我们通常不需要关心它的操作。然而,极少的情况下
我们可能需要更主动。如果真是如此,可以使用fflush
函数、setbuf
函数和setvbuf
函数。
当程序向文件中写输出时,数据通常先放入缓冲区中。当缓冲区满了或者关闭文件时,缓冲区会自动清洗。然而,通过调用
fflush
函数,程序可以按我们所希望的频率来清洗文件的缓冲区。调用
fflush(fp); /* flushes buffer for fp */
为和fp
相关联的文件清洗了缓冲区。调用
fflush(NULL); /* flushes all buffers */
清洗了全部输出流。如果调用成功,fflush
函数会返回零;如果发生错误,则返回EOF
。
setvbuf
函数允许改变缓冲流的方法,并且允许控制缓冲区的大小和位置。函数的第三个实际参数指明了期望的缓冲类型,该参数应为以下三个宏之一:
_IOFBF(满缓冲)
。当缓冲区为空时,从流读入数据;当缓冲区满时,向流写入数据。_IOLBF(行缓冲)
。每次从流读入一行数据或者向流写入一行数据。_IONBF(无缓冲)
。直接从流读入数据或者直接向流写入数据,而没有缓冲区。
(所有这三种宏都在<stdio.h>
中进行了定义。)对于没有与交互式设备相连的流来说,满缓冲是默认设置。
setvbuf
函数的第二个参数(如果它不是空指针的话)是期望缓冲区的地址。缓冲区可以有静态存储期、自动存储期,甚至可以是动态分配的。使缓冲区具有自动存储期可以在块退出时自动为其重新申请空间。动态分配缓冲区可以在不需要时释放缓冲区。setvbuf
函数的最后一个参数是缓冲区内字节的数量。较大的缓冲区可以提供更好的性能,而较小的缓冲区可以节省空间。
例如,下面这个setvbuf
函数的调用利用buffer
数组中的N
个字节作为缓冲区,而把stream
的缓冲变成了满缓冲:
char buffer[N];
...
setvbuf(stream, buffer, _IOFBF, N);
请注意!!
setvbuf
函数的调用必须在打开stream
之后(流在前,缓冲在后),在对其执行任何其他操作之前。
用空指针作为第二个参数来调用setvbuf
也是合法的,这样做就要求setvbuf
创建一个指定大小的缓冲区。如果调用成功,那么setvbuf
函数返回零。如果mode
参数无效或者要求无法满足,那么setvbuf
函数会返回非零值。
setbuf
函数是一个较早期的函数,它设定了缓冲模式和缓冲区大小的默认值。如果buf
是空指针,那么setbuf(stream, buf)
调用就等价于
(void) setvbuf(stream, NULL, _IONBF, 0);
否则,它就等价于
(void) setvbuf(stream, buf, _IOFBF, BUFSIZ);
这里的BUFSIZ
是在<stdio.h>
中定义的宏。我们把setbuf
函数看作陈旧的内容,不建议大家在新程序中使用。
请注意!!使用
setvbuf
函数或者setbuf
函数时,一定要确保在释放缓冲区之前已经关闭了流(流在前,缓冲在后)。特别是,如果缓冲区是局部于函数的,并且具有自动存储期,一定要确保在函数返回之前关闭流。
22.2.8 其他文件操作
int remove(const char *filename);
int rename(const char *old, const char *new);
remove
函数和rename
函数允许程序执行基本的文件管理操作。不同于本节中大多数其他函数,remove
函数和rename
函数对文件名(而不是文件指针)进行处理。如果调用成功,那么这两个函数都返回零;否则,都返回非零值。
remove
函数删除文件:
remove("foo"); /* deletes the file named "foo" */
如果程序使用fopen
函数(而不是tmpfile
函数)来创建临时文件,那么它可以使用remove
函数在程序终止前删除此文件。一定要确保已经关闭了要移除的文件,因为对于当前打开的文件,移除文件的效果是由实现定义的。
rename
函数改变文件的名字:
rename("foo", "bar"); /* renames "foo" to "bar" */
对于用fopen
函数创建的临时文件,如果程序需要使文件变为永久的,那么用rename
函数改名是很方便的。如果具有新名字的文件已经存在了,改名的效果会由实现定义。
请注意!!如果打开了要改名的文件,那么一定要确保在调用
rename
函数之前关闭此文件。对打开的文件执行改名操作会失败。
22.3 格式化的输入/输出
本节将介绍使用
格式串
来控制读/写的库函数。这些库函数包括已经知道的printf
函数和scanf
函数,它们可以在输入时把字符格式的数据转换为数值格式的数据,并且可以在输出时把数值格式的数据再转换成字符格式的数据。其他的输入/输出函数不能完成这样的转换。
22.3.1 …printf函数
int fprintf(FILE * restrict stream, const char * restrict format, ...);
int printf(const char * restrict format, ...);
fprintf
函数和printf
函数向输出流中写入可变数量的数据项,并且利用格式串来控制输出的形式。这两个函数的原型都是以...符号(省略号26.1节)
结尾的,表明后面还有可变数量的实际参数
。这两个函数的返回值是写入的字符数,若出错则返回一个负值。
fprintf
函数和printf
函数唯一的不同就是printf
函数始终向stdout(标准输出流)
写入内容,而fprintf
函数则向它自己的第一个实际参数指定的流中写入内容:
printf("Total: %d\n", total); /* writes to stdout */
fprintf(fp, "Total: %d\n", total); /* writes to fp */
printf
函数的调用等价于fprintf
函数把stdout
作为第一个实际参数而进行的调用。
但是,不要以为fprintf
函数只是把数据写入磁盘文件的函数。和<stdio.h>
中的许多函数一样,fprintf
函数可以用于任何输出流。事实上,fprintf
函数最常见的应用之一(向标准误差流stderr
写入出错消息)和磁盘文件没有任何关系。下面就是这类调用的一个示例:
fprintf(stderr, "Error: data file can’t be opened.\n");
向stderr
写入消息可以保证消息能出现在屏幕上,即使用户重定向stdout
也没关系。
在<stdio.h>
中还有另外两个函数也可以向流写入格式化的输出。这两个函数很不常见,一个是vfprintf
函数,另一个是vprintf函数(26.1节)
。它们都依赖于<stdarg.h>
中定义的va_list
类型,因此将和<stdarg.h>
一起讨论。
22.3.2 …printf转换说明
printf
函数和fprintf
函数都要求格式串包含普通字符或转换说明。普通字符会原样输出,而转换说明则描述了如何把剩余的实参转换为字符格式显示出来。3.1节
简要介绍了转换说明,其后的章节中还添加了一些细节。现在,我们将对已知的转换说明内容进行回顾,并且把剩余的内容补充完整。
...printf
函数的转换说明由字符%
和跟随其后的最多5
个不同的选项构成。假设格式串为%#012.5Lg
,分析如下:
标志 | 最小栏宽 | 精度 | 长度指定符 | 转换指定符 | |
---|---|---|---|---|---|
% | #0 | 12 | .5 | L | g |
下面对上述这些选项进行详细的描述,选项的顺序必须与上面一致:
- 标志(可选项,允许多于一个)。标志
—
导致在栏内左对齐,而其他标志则会影响数的显示形式。表22-4
给出了标志的完整列表。
表22-4 用于…printf函数的标志
标志 | 含义 |
---|---|
- | 在栏内左对齐(默认右对齐) |
+ | 有符号转换得到的数总是以+ 或- 开头(通常,只有负数前面附上- ) |
空格 | 有符号转换得到的非负数前面加空格(+ 标志优先于空格标志) |
# | 以0 开头的八进制数,以0x 或0X 开头的十六进制非零数。浮点数始终有小数点。不能删除由g 或G 转换输出的数的尾部零 |
0(零) | 用前导零在数的栏宽内进行填充。如果转换是d、i、o、u、x 或X ,而且指定了精度,那么可以忽略标志0 (- 标志优先于0 标志) |
-
最小栏宽(可选项)。如果数据项太小以至于无法达到这一宽度,那么会进行填充。(默认情况下会在数据项的左侧添加空格,从而使其在栏内右对齐。)如果数据项过大以至于超过了这个宽度,那么会完整地显示数据项。栏宽既可以是整数也可以是字符
*
。如果是字符*
,那么栏宽由下一个参数决定。如果这个参数为负,它会被视为前面带-
标志的正数。 -
精度(可选项)。精度的含义依赖于转换指定符:如果转换指定符是
d、i、o、u、x、X
,那么精度表示最少位数(如果位数不够,则添加前导零);如果转换指定符是a、A、e、E、f、F
,那么精度表示小数点后的位数;如果转换指定符是g、G
,那么精度表示有效数字的个数;如果转换指定符是s
,那么精度表示最大字节数。精度是由小数点(.)
后跟一个整数或字符*
构成的。如果出现字符*
,那么精度由下一个参数决定。(如果这个参数为负,效果与不指定精度一样。)如果只有小数点,那么精度为零。 -
长度指定符(可选项)。长度指定符配合转换指定符,共同指定传入的实际参数的类型(例如,
%d
通常表示一个int
值,%hd
用于显示short int
值,%ld
用于显示long int
值)。表22-5
列出了每一个长度指定符、可以使用的转换说明以及两者相结合时的类型(表中没有给出的长度指定符和转换指定符的结合会引起未定义的行为)。
表22-5 用于…printf函数的长度指定符
长度指定符 | 转换指定符 | 含义 |
---|---|---|
hh① | d、i、o、u、x、X | signed char, unsigned char |
hh① | n | signed char * |
h | d、i、o、u、x、X | short int, unsigned short int |
h | n | short int * |
l(ell) | d、i、o、u、x、X | long int, unsigned long int |
l(ell) | n | long int * |
l(ell) | c | wint_t |
l(ell) | s | wchar_t * |
l(ell) | a、A、e、E、f、F、g、G | 无作用 |
ll①(ell-ell) | d、i、o、u、x、X | long long int, unsigned long long int |
ll①(ell-ell) | n | long long int * |
j① | d、i、o、u、x、X | intmax_t, uintmax_t |
j① | n | intmax_t * |
z① | d、i、o、u、x、X | size_t |
z① | n | size_t * |
t① | d、i、o、u、x、X | ptrdiff_t |
t① | n | ptrdiff_t * |
L | a、A、e、E、f、F、g、G | long double |
①仅C99
及之后的标准才有。
- 转换指定符。转换指定符必须是
表22-6
中列出的某一种字符。注意f、F、e、E、g、G、a
和A
全部设计用来输出double
类型的值,但把它们用于float
类型的值也可以:由于有默认实参提升(9.3节)
,float
类型实参在传递给带有可变数量实参的函数时会自动转换为double
类型。类似地,传递给...printf
函数的字符也会自动转换为int
类型,所以可以正常使用转换指定符c
。
表22-6 …printf 函数的转换指定符
转换指定符 | 含义 |
---|---|
d、i | 把int 类型值转换为十进制形式 |
o、u、x、X | 把无符号整数转换为八进制(o) 、十进制(u) 或十六进制(x、X) 形式。x 表示用小写字母a~f 来显示十六进制数,X 表示用大写字母A~F 来显示十六进制数 |
f、F① | 把double 类型值转换为十进制形式,并且把小数点放置在正确的位置上。如果没有指定精度,那么在小数点后面显示6 个数字 |
e、E | 把double 类型值转换为科学记数法形式。如果没有指定精度,那么在小数点后面显示6 个数字。如果选择e ,那么要把字母e 放在指数前面;如果选择E ,那么要把字母E 放在指数前面 |
g、G | g 会把double 类型值转换为f 形式或者e 形式。当数值的指数部分小于-4 ,或者指数部分大于等于精度值时,会选择e 形式显示。尾部的零不显示(除非使用了# 标志),且小数点仅在后边跟有数字时才显示出来。G 会在F 形式和E 形式之间进行选择 |
a①、A① | 使用格式[-]0xh.hhhhp±d 的格式把double 类型值转换为十六进制科学记数法形式。其中[-] 是可选的负号,h 代表十六进制数位,± 是正号或者负号,d 是指数。d 为十进制数,表示2 的幂。如果没有指定精度,在小数点后将显示足够的数位来表示准确的数值(如果可能的话)。a 表示用小写形式显示a~f ,A 表示用大写形式显示A~F 。选择a 还是A 也会影响字母x 和p 的情况 |
c | 显示无符号字符的int 类型值 |
s | 写出由实参指向的字符。当达到精度值(如果存在)或者遇到空字符时,停止写操作 |
p | 把void * 类型值转换为可打印形式 |
n | 相应的实参必须是指向int 类型对象的指针。在该对象中存储...printf 函数调用已经输出的字符数量,不产生输出 |
% | 写字符% |
①仅C99
及之后的标准才有。
请注意!!请认真遵守上述规则。使用无效的转换说明会导致未定义的行为。
22.3.3. C99对…printf转化说明的修改(C99)
C99
对printf
函数和fprintf
函数的转换说明做了不少修改:
-
增加了长度指定符。
C99
中增加了hh
、ll
、j
、z
和t
长度指定符。hh
和ll
提供了额外的长度选项,j
允许输出最大宽度整数(27.1节)
,z
和t
分别使对size_t
和ptrdiff_t
类型值的输出变得更方便了。 -
增加了转换指定符。
C99
中增加了F、a
和A
转换指定符。F
和f
一样,区别在于书写无穷数和NaN
(见下面的讨论)的方式。a
和A
转换指定符很少使用,它们和十六进制浮点常量相关,后者在第7章
末尾的“问与答”
部分讨论过。 -
允许输出无穷数和NaN。
IEEE 754
浮点标准允许浮点运算的结果为正无穷数
、负无穷数
或NaN(非数)
。例如,1.0
除以0.0
会产生正无穷数,-1.0
除以0.0
会产生负无穷数,而0.0
除以0.0
会产生NaN
(因为该结果在数学上是无定义的)。在C99
中,转换指定符a、A、e、E、f、F、g
和G
能把这些特殊值转换为可显示的格式。a、e、f
和g
将正无穷数转换为inf
或infinity
(都是合法的),将负无穷数转换为-inf
或-infinity
,将NaN
转换为nan
或-nan
(后面可能跟着一对圆括号,圆括号里面有一系列的字符)。A、E、F
和G
与a、e、f
和g
是等价的,区别仅在于使用大写字母(INF
、INFINITY
、NAN
)。 -
支持宽字符。从
C99
开始的另一个特性是使用fprintf
来输出宽字符。%lc
转换说明用于输出一个宽字符,%ls
用于输出一个由宽字符组成的字符串。 -
之前未定义的转换指定符现在允许使用了。在
C89
中,使用%le
、%lE
、%lf
、%lg
以及%lG
的效果是未定义的。这些转换说明在C99
及其之后都是合法的(l
长度指定符被忽略)。
22.3.4 …printf转换说明示例
现在来看一些示例。在前面的章节中我们已经看过大量日常转换说明的例子了,所以下面将集中说明一些更高级的应用示例。与前面的章节一样,这里将用
·
表示空格字符。
我们首先来看看标志作用于%d
转换的效果(对其他转换的效果也是类似的)。表22-7
的第一行显示了不带任何标志的%8d
的效果。接下来的四行分别显示了带有标志-
、+
、空格
以及0
的效果(标志#
从不用于%d
)。剩下的几行显示了标志组合所产生的效果。
表22-7 标志作用于%d
转换的效果
转换说明 | 对123应用转换说明的结果 | 对-123应用转换说明的结果 |
---|---|---|
%8d | •••••123 | ••••-123 |
%-8d | 123••••• | -123•••• |
%+8d | ••••+123 | ••••-123 |
% 8d | •••••123 | ••••-123 |
%08d | 00000123 | -0000123 |
%-+8d | +123•••• | -123•••• |
%- 8d | •123•••• | -123•••• |
%+08d | +0000123 | -0000123 |
% 08d | •0000123 | -0000123 |
表22-8
说明了标志#
作用于o
、x
、X
、g
和G
转换的效果。
表22-8 标志#
的效果
转换说明 | 对123应用转换说明的结果 | 对123.0应用转换说明的结果 |
---|---|---|
%8o | •••••173 | |
%#8o | ••••0173 | |
%8x | ••••••7b | |
%#8x | ••••0x7b | |
%8X | ••••••7B | |
%#8X | ••••0X7B | |
%8g | •••••123 | |
%#8g | •123.000 | |
%8G | •••••123 | |
%#8G | •123.000 |
在前面的章节中,表示数值时已经使用过最小栏宽和精度了,所以这里不再给出更多的示例,只在表22-9
中给出最小栏宽和精度作用于%s
转换的效果。
表22-9 最小栏宽和精度作用于转换%s
的效果
转换说明 | 对"bogus"应用转换说明的结果 | 对"buzzword"应用转换说明的结果 |
---|---|---|
%6s | •bogus | buzzword |
%-6s | bogus• | buzzword |
%.4s | bogu | buzz |
%6.4s | ••bogu | ••buzz |
%-6.4s | bogu•• | buzz•• |
表22-10
说明了%g
转换如何以%e
和%f
的格式显示数。表中的所有数都用转换说明%.4g
进行了书写。前两个数的指数至少为4
,因此它们是按照%e
的格式显示的。接下来的8
个数是按照%f
的格式显示的。最后两个数的指数小于-4
,所以也用%e
的格式来显示。
表22-10 %g
转换的示例
数 | 对数应用转换%.4g的结果 |
---|---|
123456.00000000000 | 1.235e+05 |
12345.60000000000 | 1.235e+04 |
1234.56000000000 | 1235 |
123.45600000000 | 123.5 |
12.34560000000 | 12.35 |
1.23456000000 | 1.235 |
0.12345600000 | 0.1235 |
0.01234560000 | 0.01235 |
0.00123456000 | 0.001235 |
0.00012345600 | 0.0001235 |
0.00001234560 | 1.235e-05 |
0.00000123456 | 1.235e-06 |
过去,我们假设最小栏宽和精度都是嵌在格式串中的常量。用字符*
取代最小栏宽或精度通常可以把它们作为格式串之后的实际参数加以指定。例如,下列printf
函数的调用都产生相同的输出:
printf("%6.4d", i);
printf("%*.4d", 6, i);
printf("%6.*d", 4, i);
printf("%*.*d", 6, 4, i)
注意!!为字符*
填充的值刚好出现在待显示的值之前。顺便说一句,字符*
的主要优势就是它允许使用宏来指定栏宽或精度:
printf("%*d", WIDTH, i)
我们甚至可以在程序执行期间计算栏宽或精度:
printf("%*d", page_width / num_cols, i)
最不常见的转换说明是
%p
和%n
。%p
转换允许显示指针的值:
printf("%p", (void *) ptr); /* displays value of ptr
虽然在调试时%p
偶尔有用,但它不是大多数程序员日常使用的特性。C
标准没有指定用%p
显示指针的形式,但很可能会以八进制
或十六进制
数的形式显示。
转换
%n
用来找出到目前为止由...printf
函数调用所显示的字符数量。例如,在调用:
printf("%d%n\n", 123, &len)
之后len
的值将为3
,因为在执行转换%n
的时候printf
函数已经显示3
个字符(123
)了。注意,在len
前面必须要有&
(因为%n
要求指针),这样就不会显示len
自身的值。
22.3.5 …scanf函数
int fscanf(FILE * restrict stream, const char * restrict format, ...);
int scanf(const char * restrict format, ...);
fscanf
函数和scanf
函数从输入流读入数据,并且使用格式串来指明输入的格式。格式串的后边可以有任意数量的指针(每个指针指向一个对象)作为额外的实际参数。输入的数据项根据格式串中的转换说明进行转换并且存储在指针指向的对象中。
scanf
函数始终从标准输入流stdin
中读入内容,而fscanf
函数则从它的第一个参数所指定的流中读入内容:
scanf("%d%d", &i, &j); /* reads from stdin */
fscanf(fp, "%d%d", &i, &j); /* reads from fp */
scanf
函数的调用等价于以stdin
作为第一个实际参数的fscanf
函数调用。
如果发生输入失败(即没有输入字符可以读)或者匹配失败(即输入字符和格式串不匹配),那么...scanf
函数会提前返回。(在C99
中,输入失败还可能由编码错误导致。编码错误意味着我们试图按多字节字符的方式读取输入,但输入字符不是有效的多字节字符。)这两个函数都返回读入并且赋值给对象的数据项的数量。如果在读取任何数据项之前发生输入失败,那么会返回EOF
。
在
C
程序中测试scanf
函数的返回值的循环很普遍。例如,下列循环逐个读取一串整数,在首个遇到问题的符号处停止:
//惯用法
while (scanf("%d", &i) == 1) { ...
}
22.3.6 …scanf格式串
...scanf
函数的调用类似于...printf
函数的调用。然而,这种相似可能会产生误导,实际上...scanf
函数的工作原理完全不同于...printf
函数。我们应该把scanf
函数和fscanf
函数看作“模式匹配”函数。格式串表示的就是...scanf
函数在读取输入时试图匹配的模式。如果输入和格式串不匹配,那么一旦发现不匹配函数就会返回。不匹配的输入字符将被“放回”留待以后读取。
...scanf
函数的格式串可能含有三种信息:
- 转换说明。
...scanf
函数格式串中的转换说明类似于...printf
函数格式串中的转换说明。大多数转换说明(%[
、%c
和%n
例外)会跳过输入项开始处的空白字符(3.2节)
。但是,转换说明不会跳过尾部的空白字符。如果输入含有·123¤
,那么转换说明%d
会读取·
、1
、2
和3
,但是留下¤
不读取。(这里使用·
表示空格符,用¤
表示换行符。) - 空白字符。
...scanf
函数格式串中的一个或多个连续的空白字符与输入流中的零个或多个空白字符相匹配。 - 非空白字符。除了
%
之外的非空白字符和输入流中的相同字符相匹配。
例如,格式串"ISBN %d-%d-%ld-%d"
说明输入由下列这些内容构成:字母ISBN
,可能有一些空白字符,一个整数,字符-
,一个整数(前面可能有空白字符),字符-
,一个长整数(前面可能有空白字符),字符-
和一个整数(前面可能有空白字符)。
22.3.7 …scanf转换说明
用于
...scanf
函数的转换说明实际上比用于...printf
函数的转换说明简单一些。...scanf
函数的转换说明由字符%
和跟随其后的下列选项(按照出现的顺序)构成。
- 字符
*
(可选项)。字符*
的出现意味着赋值屏蔽(assignment suppression)
:读入此数据项,但是不会把它赋值给对象。用*
匹配的数据项不包含在...scanf
函数返回的计数中。 - 最大栏宽(可选项)。最大栏宽限制了输入项中的字符数量。如果达到了这个最大值,那么此数据项的转换将结束。转换开始处跳过的空白字符不进行统计。
- 长度指定符(可选项)。长度指定符表明用于存储输入数据项的对象的类型与特定转换说明中的常见类型长度不一致。
表22-11
列出了每一个长度指定符、可以使用的转换说明以及两者相结合时的类型(表中没有给出的长度指定符和转换指定符的结合会引起未定义的行为)。
表22-11 用于...scanf
函数的长度指定符
长度指定符 | 转换指定符 | 含义 |
---|---|---|
hh① | d、i、o、u、x、X、n | signed char , unsigned char |
h | d、i、o、u、x、X、n | short int , unsigned short int |
l(ell) | d、i、o、u、x、X、n | long int , unsigned long int |
l(ell) | a、A、e、E、f、F、g、G | double * |
l(ell) | c、s、[ | wchar_t * |
ll①(ell-ell) | d、i、o、u、x、X、n | long long int , unsigned long long int |
j① | d、i、o、u、x、X、n | intmax_t , uintmax_t |
z① | d、i、o、u、x、X、n | size_t * |
t① | d、i、o、u、x、X、n | ptrdiff_t * |
L | a、A、e、E、f、F、g、G | long double * |
① 仅C99
及之后的标准才有。
- 转换指定符。转换指定符必须是
表22-12
中列出的某一种字符。
表22-12 用于...scanf
函数的转换指定符
转换指定符 | 含义 |
---|---|
d | 匹配十进制整数,假设相应的实参是int * 类型 |
i | 匹配整数,假设相应的实参是int * 类型。假定数是十进制形式的,除非它以0 开头(说明是八进制形式),或者以0x 或0X 开头(十六进制形式) |
o | 匹配八进制整数。假设相应的实参是unsigned int * 类型 |
u | 匹配十进制整数。假设相应的实参是unsigned int * 类型 |
x、X | 匹配十六进制整数。假设相应的实参是unsigned int * 类型 |
a①、A①、e、E、f、F①、g、G | 匹配浮点数。假设相应的实参是float * 类型。在C99 中,该数可以是无穷大或NaN |
c | 匹配n 个字符,这里的n 是最大栏宽。如果没有指定栏宽,那么就匹配一个字符。假设相应的实参是指向字符数组的指针(如果没有指定栏宽,就指向字符对象)。不在末尾添加空字符 |
s | 匹配一串非空白字符,然后在末尾添加空字符。假设相应的实参是指向字符数组的指针 |
[ | 匹配来自扫描集合的非空字符序列,然后在末尾添加空字符。假设相应的实参是指向字符数组的指针 |
p | 以...printf 函数的输出格式匹配指针值。假设相应的实参是指向void* 对象的指针 |
n | 相应的实参必须指向int 类型的对象。把到目前为止读入的字符数量存储到此对象中。没有输入会被吸收进去,而且...scanf 函数的返回值也不会受到影响 |
% | 匹配字符% |
① 仅C99
及之后的标准才有。
数值型数据项可以始终用符号(+
或-
)作为开头。然而,说明符o
、u
、x
和X
把数据项转换成无符号的形式,所以通常不用这些说明符来读取负数。
说明符[
是说明符s
更加复杂(且更加灵活)的版本。使用[
的完整转换说明格式是%[集合]
或者%[^集合]
,这里的集合可以是任意字符集。(但是,如果]
是集合中的一个字符,那么它必须首先出现。)%[集合]
匹配集合(即扫描集合)中的任意字符序列。%[^集合]
匹配不在集合中的任意字符序列(换句话说,构成扫描集合的全部字符都不在集合中)。例如,%[abc]
匹配的是只含有字母a
、b
和c
的任何字符串,而%[^abc]
匹配的是不含有字母a
、b
或c
的任何字符串。
...scanf
函数的许多转换指定符和<stdlib.h>
中的数值转换函数(26.2节)
有着紧密的联系。这些函数把字符串(如"-297"
)转换成与其等价的数值(-297
)。例如,说明符d
寻找可选的+
号或-
号,后边跟着一串十进制的数字。这样就与把字符串转换成十进制数的strtol
函数所要求的格式完全一样了。表22-13
展示了转换指定符和数值转换函数之间的对应关系。
表22-13 ...scanf
转换指定符和数值转换函数之间的对应关系
转换指定符 | 字符串转换函数 |
---|---|
d | 10 作为基数的strtol 函数 |
i | 0 作为基数的strtol 函数 |
o | 8 作为基数的strtoul 函数 |
u | 10 作为基数的strtoul 函数 |
x、X | 16 作为基数的strtoul 函数 |
a、A、e、E、f、F、g、G | strtod 函数 |
请注意!!编写
scanf
函数的调用时需要十分小心。scanf
格式串中无效的转换说明就像printf
格式串中的无效转换说明一样糟糕,都会导致未定义的行为。
22.3.8 C99对…scanf转换说明的改变(C99)
从
C99
开始的标准对scanf
和fscanf
的转换说明做了一些改变,但没有...printf
函数那么多。
- 增加了长度指定符。从
C99
开始增加了hh
、ll
、j
、z
和t
长度指定符,它们与...printf
转换说明中的长度指定符相对应。 - 增加了转换指定符。从
C99
开始增加了F
、a
和A
转换指定符,提供这些转换指定符是为了与...printf
相一致。...scanf
函数把它们与e
、E
、f
、g
和G
等同看待。 - 具有读无穷数和NaN的能力。正如
...printf
函数可以输出无穷数和NaN
一样,...scanf
函数可以读这些值。为了能够正确读出,这些数的形式应该与...printf
函数相同,忽略大小写(例如,INF
或inf
都会被认为是无穷数)。 - 支持宽字符。
...scanf
函数能够读多字节字符,并在存储时将之转换为宽字符。%lc
转换说明用于读出单个的多字节字符或者一系列多字节字符;%ls
用于读取由多字节字符组成的字符串(在结尾添加空字符)。%l[集合]
和%l[^集合]
转换说明也可以读取多字节字符串。
22.3.9 scanf示例
下面三个表格包含了
scanf
的调用示例。每个示例都把scanf
函数应用于它右侧的输入字符。用高亮
显示的字符会被调用吸收。调用后变量的值会出现在输入的右侧。
表22-14
中的示例说明了把转换说明、空白字符以及非空白字符组合在一起的效果。在这三种情况下没有对j
赋值,所以j
的值在scanf
调用前后保持不变。表22-15
中的示例显示了赋值屏蔽和指定栏宽的效果。表22-16
中的示例描述了更加深奥的转换指定符(即i
、[
和n
)。
表22-14 scanf
示例(第一组)
scanf函数的调用 | 输入 | 变量 |
---|---|---|
n = scanf(“%d%d”, &i, &j); | 12• ,•34¤ | n:1 i:12 j:不变 |
n = scanf(“%d,%d”, &i, &j); | 12 •,•34¤ | n:1 i:12 j:不变 |
n = scanf(“%d ,%d”, &i, &j); | 12•,•34 ¤ | n:2 i:12 j:34 |
n = scanf(“%d, %d”, &i, &j); | 12 •,•34¤ | n:1 i:12 j:不变 |
表22-15 scanf
示例(第二组)
scanf函数的调用 | 输入 | 变量 |
---|---|---|
n = scanf(“%*d%d”, &i); | 12•34 ¤ | n:1 i:34 |
n = scanf(“%*s%s”, str); | My•Fair •Lady¤ | n:1 str:“Fair” |
n = scanf(“%1d%2d%3d”, &i, &j, &k); | 12345 ¤ | n:3 i:1 j:23 k:45 |
n = scanf(“%2d%2s%2d”, &i, str, &j); | 123456 ¤ | n:3 i:12 str:“34” j:56 |
表22-16 scanf
示例(第三组)
scanf函数的调用 | 输入 | 变量 |
---|---|---|
n = scanf(“%i%i%i”, &i, &j, &k); | 12•012•0x12 ¤ | n:3 i:12 j:10 k:18 |
n = scanf(“%[0123456789]”, str); | 123 abc¤ | n:1 Str: “123” |
n = scanf(“%[0123456789]”, str); | abc123¤ | n:0 str:不变 |
n = scanf(“%[^0123456789]”, str); | abc 123¤ | n:1 Str: “abc” |
n = scanf(“%*d%d%n”, &i, &j); | 10•20 •30¤ | n:1 i:20 j:5 |
22.3.10 检测文件末尾和错误条件
void clearerr(FILE *stream);
int feof(FILE *stream);
int ferror(FILE *stream);
如果要求...scanf
函数读入并存储n
个数据项,那么希望它的返回值就是n
。如果返回值小于n
,那么一定是出错了。一共有三种可能情况:
- 文件末尾。函数在完全匹配格式串之前遇到了文件末尾。
- 读取错误。函数不能从流中读取字符。
- 匹配失败。数据项的格式是错误的。例如,函数可能在搜索整数的第一个数字时遇到了一个字母。
但是如何知道遇到的是哪种情况呢?在许多情况下,这是无关紧要的,程序出问题了,可以把它舍弃。然而,有时候需要查明失败的原因。
每个流都有与之相关的两个指示器:错误指示器(error indicator)
和文件末尾指示器(end-of-file indicator)
,当打开流时会清除这些指示器。遇到文件末尾就设置文件末尾指示器,遇到读错误就设置错误指示器。(输出流上发生写错误时也会设置错误指示器。)匹配失败不会改变任何一个指示器。
一旦设置了错误指示器或者文件末尾指示器,它就会保持这种状态直到被显式地清除(可能通过
clearerr
函数的调用)。clearerr
会同时清除文件末尾指示器和错误指示器:
clearerr(fp); /* clears eof and error indicators for fp *///某些其他库函数因为副作用可以清除某种指示器或两种都可以清除,
//所以不需要经常使用clearerr函数。
我们可以调用feof
函数和ferror
函数来测试流的指示器,从而确定出先前在流上的操作失败的原因。如果为与fp
相关的流设置了文件末尾指示器,那么feof(fp)
函数调用就会返回非零值。如果设置了错误指示器,那么ferror(fp)
函数的调用也会返回非零值。而其他情况下,这两个函数都会返回零。
当scanf
函数返回小于预期的值时,可以使用feof
函数和ferror
函数来确定原因。如果feof
函数返回了非零的值,那么就说明已经到达了输入文件的末尾。如果ferror
函数返回了非零的值,那么就表示在输入过程中产生了读错误。如果两个函数都没有返回非零值,那么一定是发生了匹配失败。不管问题是什么,scanf
函数的返回值都会告诉我们在问题产生前所读入的数据项的数量。
为了明白
feof
函数和ferror
函数可能的使用方法,现在来编写一个函数。此函数用来搜索文件中以整数起始的行。下面是预计的函数调用方式:
n = find_int("foo");
其中,"foo"
是要搜索的文件的名字,函数返回找到的整数的值并将其赋给n
。如果出现问题(文件无法打开或者发生读错误,再或者没有以整数起始的行),find_int
函数将返回一个错误代码(分别是-1
、-2
或-3
)。我们假设文件中没有以负整数起始的行。
int find_int(const char *filename)
{ FILE *fp = fopen(filename, "r"); int n; if (fp == NULL) return –1; /* can’t open file */ while (fscanf(fp, "%d", &n) != 1) { if (ferror(fp)) { fclose(fp); return –2; /* input error */ } if (feof(fp)) { fclose(fp); return –3; /* integer not found */ } fscanf(fp, "%*[^\n]"); /* skips rest of line */ } fclose(fp); return n;
}
while
循环的控制表达式调用fscanf
函数的目的是从文件中读取整数。如果尝试失败了(fscanf
函数返回的值不为1
),那么find_int
函数就会调ferror
函数和feof
函数来了解是发生了读错误还是遇到了文件末尾。如果都不是,那么fscanf
函数一定是由于匹配错误而失败的,因此find_int
函数会跳过当前行的剩余字符并尝试下一行。请注意用转换说明%*[^\n]
跳过全部字符直到下一个换行符为止的用法。(我们对扫描集合已有所了解,可以拿出来显摆一下了!)
22.4 字符的输入/输出
本节将讨论用于读和写单个字符的库函数。这些函数可以处理文本流和二进制流。
请注意!!本节中的函数把字符作为
int
类型而非char
类型的值来处理。这样做的原因之一就是,输入函数是通过返回EOF
来说明文件末尾(或错误)情况的,而EOF
又是一个负的整型常量。
22.4.1 输出函数
int fputc(int c, FILE *stream);
int putc(int c, FILE *stream);
int putchar(int c);
putchar
函数向标准输出流stdout
写一个字符:
putchar(ch); /* writes ch to stdout */
fputc
函数和putc
函数是putchar
函数向任意流写字符的更通用的版本:
fputc(ch, fp); /* writes ch to fp */
putc(ch, fp); /* writes ch to fp */
虽然putc
函数和fputc
函数做的工作相同,但是putc
通常作为宏来实现(也有函数实现),而fputc
函数则只作为函数实现。putchar
本身通常也定义为宏:
#define putchar(c) putc((c), stdout)
标准库既提供putc
又提供fputc
,看起来很奇怪。但是,正如在14.3节
看到的那样,宏有几个潜在的问题。C
标准允许putc
宏对stream
参数多次求值,而fputc
则不可以。虽然程序员通常偏好使用putc
,因为它的速度较快,但fputc
作为备选也是可用的。
如果出现了写错误,那么上述这3
个函数都会为流设置错误指示器并且返回EOF
。否则,它们都会返回写入的字符。
22.4.2 输入函数
int fgetc(FILE *stream);
int getc(FILE *stream);
int getchar(void);
int ungetc(int c, FILE *stream);
getchar
函数从标准输入流stdin
中读入一个字符:
ch = getchar(); /* reads a character from stdin */
fgetc
函数和getc
函数从任意流中读入一个字符:
ch = fgetc(fp); /* reads a character from fp */
ch = getc(fp); /* reads a character from fp */
这3
个函数都把字符看作unsigned char
类型的值(返回之前转换成int
类型)。因此,它们不会返回EOF
之外的负值。
getc
和fgetc
之间的关系类似于putc
和fputc
之间的关系。getc
通常作为宏来实现(也有函数实现),而fgetc
则只作为函数实现。getchar
本身通常也定义为宏:
#define getchar() getc(stdin)
对于从文件中读取字符来说,程序员通常喜欢getc
胜过fgetc
。因为getc
一般是宏的形式,所以它执行起来的速度较快。如果getc
不合适,那么可以用fgetc
作为备选。(标准允许getc
宏对参数多次求值,这可能会有问题。)
如果出现问题,那么这3
个函数的行为是一样的。如果遇到了文件末尾,那么这3
个函数都会设置流的文件末尾指示器,并且返回EOF
。如果产生了读错误,则它们都会设置流的错误指示器,并且返回EOF
。为了区分这两种情况,可以调用feof
函数或者ferror
函数。
fgetc
函数、getc
函数和getchar
函数最常见的用法之一就是从文件中逐个读入字符,直到遇到文件末尾。一般习惯使用下列while
循环来实现此目的:
//惯用法
while ((ch = getc(fp)) != EOF) { ...
}
在从与fp
相关的文件中读入字符并且把它存储到变量ch
(它必须是int
类型的)之中后,判定条件会把ch
与EOF
进行比较。如果ch
不等于EOF
,则表示还未到达文件末尾,就可以执行循环体。如果ch
等于EOF
,则循环终止。
请注意!!始终要把
fgetc
、getc
或getchar
函数的返回值存储在int
类型的变量中,而不是char
类型的变量中。把char
类型变量与EOF
进行比较可能会得到错误的结果。还有另外一种字符输入函数,即
ungetc
函数。此函数把从流中读入的字符“放回”并清除流的文件末尾指示器。如果在输入过程中需要往前多看一个字符,那么这种能力可能会非常有效。比如,为了读入一系列数字,并且在遇到首个非数字时停止操作,可以写成
while (isdigit(ch = getc(fp))) { ...
}
ungetc(ch, fp); /* pushes back last character read */
通过持续调用ungetc
函数而放回的字符数量(不干涉读操作)依赖于实现和所含的流类型。只有第一次的ungetc
函数调用保证会成功。调用文件定位函数(即fseek
、fsetpos
或rewind
)(22.7节
)会导致放回的字符丢失。
ungetc
返回要求放回的字符。如果试图放回EOF
或者试图放回超过最大允许数量的字符数,则ungetc
会返回EOF
。
22.4.2.1 程序——复制文件
下面的程序用来进行文件的复制操作。当程序执行时,会在命令行上指定原始文件名和新文件名。例如,为了把文件
f1.c
复制给文件f2.c
,可以使用命令:
fcopy f1.c f2.c
如果命令行上的文件名不是两个,或者至少有一个文件无法打开,那么程序fcopy
将产生出错消息。
/*
fcopy.c
--Copies a file
*/
#include <stdio.h>
#include <stdlib.h> int main(int argc, char *argv[])
{ FILE *source_fp, *dest_fp; int ch; if (argc != 3) { fprintf(stderr, "usage: fcopy source dest\n"); exit(EXIT_FAILURE); } if ((source_fp = fopen(argv[1], "rb")) == NULL) { fprintf(stderr, "Can't open %s\n", argv[1]); exit(EXIT_FAILURE); } if ((dest_fp = fopen(argv[2], "wb")) == NULL) { fprintf(stderr, "Can't open %s\n", argv[2]); fclose(source_fp); exit(EXIT_FAILURE); } while ((ch = getc(source_fp)) != EOF) putc(ch, dest_fp); fclose(source_fp); fclose(dest_fp); return 0;
}
采用"rb"
和"wb"
作为文件模式,使fcopy
程序既可以复制文本文件也可以复制二进制文件。如果用"r"
和"w"
来代替,那么程序将无法复制二进制文件。
22.5 行的输入/输出
下面将介绍读和写行的库函数。虽然这些函数也可有效地用于二进制的流,但是它们多数用于文本流。
22.5.1 输出函数
int fputs(const char * restrict s, FILE * restrict stream);
int puts(const char *s);
我们在13.3节
已经见过puts
函数,它是用来向标准输出流stdout
写入字符串的:
puts("Hi, there!"); /* writes to stdout */
在写入字符串中的字符以后,puts
函数总会添加一个换行符。
fputs
函数是puts
函数的更通用版本。此函数的第二个实参指明了输出要写入的流:
fputs("Hi, there!", fp); /* writes to fp */
不同于puts
函数,fputs
函数不会自己写入换行符,除非字符串中本身含有换行符。
当出现写错误时,上面这两种函数都会返回
EOF
。否则,它们都会返回一个非负的数。
22.5.2 输入函数
char *fgets(char * restrict s, int n, FILE * restrict stream);
在13.3节
中已经见过在新标准中废弃的gets
函数了。
fgets
函数是gets
函数的更通用版本,它可以从任意流中读取信息。fgets
函数也比gets
函数更安全,因为它会限制将要存储的字符的数量。下面是使用fgets
函数的方法,假设str
是字符数组的名字:
fgets(str, sizeof(str), fp); /* reads a line from fp */
此调用将导致fgets
函数逐个读入字符,直到遇到首个换行符时或者已经读入了sizeof(str)-1
个字符时结束操作,这两种情况哪种先发生都可以。如果fgets
函数读入了换行符,那么它会把换行符和其他字符一起存储。(因此,gets
函数从来不存储换行符,而fgets
函数有时会存储换行符。)
如果出现了读错误,或者是在存储任何字符之前达到了输入流的末尾,那么gets
函数和fgets
函数都会返回空指针。(通常,可以使用feof
函数或ferror
函数来确定出现的是哪种情况。)否则,两个函数都会返回自己的第一个实参(指向保存输入的数组的指针)。与预期一样,两个函数都会在字符串的末尾存储空字符。
现在已经学习了
fgets
函数,那么建议大家用fgets
函数来代替gets
函数。对于gets
函数而言,接收数组的下标总有可能越界,所以只有在保证读入的字符串正好适合数组大小时使用gets
函数才是安全的。在没有保证的时候(通常是没有的),使用fgets
函数要安全得多。注意!!如果把stdin
作为第三个实参进行传递,那么fgets
函数就会从标准输入流中读取:
fgets(str, sizeof(str), stdin);
22.6 块的输入/输出
size_t fread(void * restrict ptr, size_t size, size_t nmemb, FILE * restrict stream);
size_t fwrite(const void * restrict ptr, size_t size, size_t nmemb, FILE * restrict stream);
fread
函数和fwrite
函数允许程序在单步中读和写大的数据块。如果小心使用,fread
函数和fwrite
函数可以用于文本流,但是它们主要还是用于二进制的流。
fwrite
函数用来把内存中的数组复制给流。fwrite
函数调用中第一个参数是数组的地址,第二个参数是每个数组元素的大小(以字节为单位),第三个参数是要写的元素数量,第四个参数是文件指针,此指针说明了要写的数据位置。例如,为了写整个数组a
的内容,就可以使用下列fwirte
函数调用:
fwrite(a, sizeof(a[0]), sizeof(a) / sizeof(a[0]), fp);
没有规定必须写入整个数组,数组任何区间的内容都可以轻松地写入。fwrite
函数返回实际写入的元素(不是字节)的数量。如果出现写入错误,那么此数就会小于第三个实参。
fread
函数将从流读入数组的元素。fread
函数的参数类似于fwrite
函数的参数:数组的地址、每个元素的大小(以字节为单位)、要读的元素数量以及文件指针。为了把文件的内容读入数组a
,可以使用下列fread
函数调用:
n = fread(a, sizeof(a[0]), sizeof(a) / sizeof(a[0]), fp);
检查fread
函数的返回值是非常重要的。此返回值说明了实际读的元素(不是字节)的数量。此数应该等于第三个参数,除非达到了输入文件末尾或者出现了错误。可以用feof
函数和ferror
函数来确定出问题的原因。
请注意!!不要把
fread
函数的第二个参数和第三个参数搞混了。思考下面这个fread
函数调用:fread(a, 1, 100, fp);
这里要求
fread
函数读入100
个元素,且每个元素占1
字节,所以它返回0~100
范围内的某个值。下面的调用则要求fread
函数读入一个有100
字节的块:fread(a, 100, 1, fp);
此情况中
fread
函数的返回值不是0
就是1
。
当程序需要在终止之前把数据存储到文件中时,使用fwrite
函数是非常方便的。以后程序(或者另外的程序)可以使用fread
函数把数据读回内存中来。不考虑形式的话,数据不一定要是数组格式的。fread
函数和fwrite
函数都可以用于所有类型的变量,特别是可以用fread
函数读结构或者用fwrite
函数写结构。例如,为了把结构变量s
写入文件,可以使用下列形式的fwrite
函数调用:
fwrite(&s, sizeof(s), 1, fp);
请注意!!使用
fwrite
输出包含指针值的结构时需要小心。读回时不能保证这些值一定有效。
22.7 文件定位
int fgetpos(FILE * restrict stream, fpos_t * restrict pos);
int fseek(FILE *stream, long int offset, int whence);
int fsetpos(FILE *stream, const fpos_t *pos);
long int ftell(FILE *stream);
void rewind(FILE *stream);
每个流都有相关联的文件位置(file position)
。打开文件时,会将文件位置设置在文件的起始处。(但如果文件按“追加”
模式打开,初始的文件位置可以在文件起始处,也可以在文件末尾,这依赖于具体的实现。)然后,在执行读或写操作时,文件位置会自动推进,并且允许按照顺序贯穿整个文件。
虽然对许多应用程序来说顺序访问是很好的,但是某些程序需要具有在文件中跳跃的能力,即可以在这里访问一些数据,然后到别处访问其他数据。例如,如果文件包含一系列记录,我们可能希望直接跳到特定的记录处,并对其进行读或更新。<stdio.h>
通过提供5
个函数来支持这种形式的访问,这些函数允许程序确定当前的文件位置或者改变文件的位置。
fseek
函数改变与第一个参数(即文件指针)相关的文件位置。第三个参数说明新位置是根据文件的起始处、当前位置还是文件末尾来计算。<stdio.h>
为此定义了3
种宏:
SEEK_SET
:文件的起始处。SEEK_CUR
:文件的当前位置。SEEK_END
:文件的末尾处。
第二个参数是个(可能为负的)字节计数。例如,为了移动到文件的起始处,搜索的方向将为SEEK_SET
,而且字节计数为0
:
fseek(fp, 0L, SEEK_SET); /* moves to beginning of file */
为了移动到文件的末尾,搜索的方向应该是SEEK_END
:
fseek(fp, 0L, SEEK_END); /* moves to end of file */
为了往回移动10
个字节,搜索的方向应该是SEEK_CUR
,并且字节计数为-10
:
fseek(fp, -10L, SEEK_CUR); /* moves back 10 bytes */
注意!!字节计数是long int
类型的,所以这里用0L
和-10L
作为实参。(当然,用0
和-10
也可以,因为参数会自动转换为正确的类型。)
通常情况下,
fseek
函数返回0
。如果产生错误(例如,要求的位置不存在),那么fseek
函数就会返回非零值。顺便提一句,文件定位函数最适用于二进制流。
C
语言不禁止程序对文本流使用这些定位函数,但考虑到操作系统的差异,要小心使用。fseek
函数对流是文本的还是二进制的很敏感。对于文本流而言,要么offset
(fseek
的第二个参数)必须为0
,要么whence
(fseek
的第三个参数)必须是SEEK_SET
,且offset
的值通过前面的ftell
函数调用获得。(换句话说,我们只可以利用fseek
函数移动到文件的起始处或者文件的末尾处,或者返回前面访问过的位置。)对于二进制流而言,fseek
函数不要求支持whence
是SEEK_END
的调用。
ftell
函数以长整数返回当前文件位置。[如果发生错误,ftell
函数会返回-1L
,并且把错误码存储到errno(24.2节)
中。]ftell
可能会存储返回的值并且稍后将其提供给fseek
函数调用,这也使返回前面的文件位置成为可能:
long file_pos;
...
file_pos = ftell(fp); /* saves current position */
...
fseek(fp, file_pos, SEEK_SET); /* returns to old position */
如果fp
是二进制流,那么ftell(fp)
调用会以字节计数来返回当前文件位置,其中0
表示文件的起始处。但是,如果fp
是文本流,ftell(fp)
返回的值不一定是字节计数,因此最好不要对ftell
函数返回的值进行算术运算。例如,为了查看两个文件位置的距离而把ftell
返回的值相减不是个好做法。
rewind
函数会把文件位置设置在起始处。调用rewind(fp)
几乎等价于fseek(fp, 0L, SEEK_SET)
,两者的差异是rewind
函数不返回值,但会为fp
清除错误指示器。
fseek
函数和ftell
函数都有一个问题:它们只能用于文件位置可以存储在长整数中的文件。为了用于非常大的文件,C
语言提供了另外两个函数:fgetpos
函数和fsetpos
函数。这两个函数可以用于处理大型文件,因为它们用fpos_t
类型的值来表示文件位置。fpos_t
类型值不一定就是整数,比如,它可以是结构。
调用fgetpos(fp, &file_pos)
会把与fp
相关的文件位置存储到file_pos
变量中。调用fsetpos(fp, &file_pos)
会为fp
设置文件的位置,此位置是存储在file_pos
中的值。(此值必须通过前面的fgetpos
调用获得。)如果fgetpos
函数或者fsetpos
函数调用失败,那么都会把错误码存储到errno
中。当调用成功时,这两个函数都会返回0
;否则,都会返回非零值。
下面是使用
fgetpos
函数和fsetpos
函数保存文件位置并且稍后返回该位置的方法:
fpos_t file_pos;
...
fgetpos(fp, &file_pos); /* saves current position */
...
fsetpos(fp, &file_pos); /* returns to old position */
22.7.1 程序——修改零件记录文件
下面这个程序打开包含
part
结构的二进制文件,把结构读到数组中,把每个结构的成员on_hand
置为0
,然后再把此结构写回到文件中。注意,程序用"rb+"
模式打开文件,因此既可读又可写:
/*
invclear.c
--Modifies a file of part records by setting the quantity
on hand to zero for all records
*/
#include <stdio.h>
#include <stdlib.h> #define NAME_LEN 25
#define MAX_PARTS 100 struct part { int number; char name[NAME_LEN+1]; int on_hand;
} inventory[MAX_PARTS]; int num_parts; int main(void)
{ FILE *fp; int i; if ((fp = fopen("inventory.dat", "rb+")) == NULL) { fprintf(stderr,"Can’t open inventory file\n"); exit(EXIT_FAILURE); } num_parts = fread(inventory, sizeof(struct part), MAX_PARTS, fp); for (i = 0; i < num_parts; i++) inventory[i].on_hand = 0; rewind(fp);fwrite(inventory, sizeof(struct part), num_parts, fp); fclose(fp); return 0;
}
顺便说一下,这里调用rewind
函数是很关键的。在调用完fread
函数之后,文件位置是在文件的末尾。如果没有先调用rewind
函数,就调用fwrite
函数,那么fwrite
函数将在文件末尾添加新数据,而不会覆盖旧数据。
22.8 字符串的输入/输出
本节里描述的函数有一点不同,因为它们与数据流或文件并没有什么关系。相反,它们允许我们使用
字符串作为流
读写数据。sprintf
和snprintf
函数将按和写到数据流一样的方式写字符到字符串,sscanf
函数从字符串中读出数据就像从数据流中读数据一样。这些函数非常类似于printf
和scanf
函数,也都是非常有用的。sprintf
和snprintf
函数可以让我们使用printf
的格式化能力,不需要真的往流中写入数据。类似地,sscanf
函数也可以让我们使用scanf
函数强大的模式匹配能力。下面将详细讲解sprintf
、snprintf
和sscanf
函数。
3
个相似的函数(vsprintf
、vsnprintf
和vsscanf
)也属于<stdio.h>
头,但这些函数依赖于在<stdarg.h>
中声明的va_list
类型。我们将推迟到26.1节
讨论该头时再来介绍这3
个函数。
22.8.1 输出函数
int sprintf(char * restrict s, const char * restrict format, ...);
int snprintf(char *restrict s, size_t n, const char * restrict format, ...); //C99新增
sprintf
函数类似于printf
函数和fprintf
函数,唯一的不同就是sprintf
函数把输出写入(第一个实参指向的)字符数组而不是流中。sprintf
函数的第二个参数是格式串,这与printf
函数和fprintf
函数所用的一样。例如,函数调用
sprintf(date, "%d/%d/%d", 9, 20, 2010);
会把"9/20/2010"
复制到date
中。当完成向字符串写入的时候,sprintf
函数会添加一个空字符,并且返回所存储字符的数量(不计空字符)。如果遇到错误(宽字符不能转换成有效的多字节字符),sprintf
返回负值。
sprintf
函数有着广泛的应用。例如,有些时候可能希望对输出数据进行格式化,但不是真的要把数据写出。这时就可以使用sprintf
函数来实现格式化,然后把结果存储在字符串中,直到需要产生输出的时候再写出。sprintf
函数还可以用于把数转换成字符格式。
snprintf
函数与sprintf
一样,但多了一个参数n
。写入字符串的字符不会超过n-1
,结尾的空字符不算;只要n
不是0
,就会有空字符。(我们也可以这样说:snprintf
最多向字符串中写入n
个字符,最后一个是空字符。)例如,函数调用
snprintf(name, 13, "%s, %s", "Einstein", "Albert");
会把"Einstein, Al"
写入到name
中。
如果没有长度限制,snprintf
函数返回需要写入的字符数(不包括空字符)。如果出现编码错误,snprintf
函数返回负值。为了查看snprintf
函数是否有空间写入所有要求的字符,可以测试其返回值是否非负且小于n
。
22.8.2 输入函数
int sscanf(const char * restrict s, const char * restrict format, ... );
sscanf
函数与scanf
函数和fscanf
函数都很类似,唯一的不同就是sscanf
函数是从(第一个参数指向的)字符串而不是流中读取数据。sscanf
函数的第二个参数是格式串,这与scanf
函数和fscanf
函数所用的一样。
sscanf
函数对于从由其他输入函数读入的字符串中提取数据非常方便。例如,可以使用fgets
函数来获取一行输入,然后把此行数据传递给sscanf
函数进一步处理:
fgets(str, sizeof(str), stdin); /* reads a line of input */
sscanf(str, "%d%d", &i, &j); /* extracts two integers */
用sscanf
函数代替scanf
函数或者fscanf
函数的好处之一就是,可以按需多次检测输入行,而不再只是一次
,这样使识别替换的输入格式和从错误中恢复都变得更加容易了。下面思考一下读取日期的问题。读取的日期既可以是月/日/年的格式,也可以是月-日-年的格式。假设str
包含一行输入,那么可以按如下方法提取出月、日和年的信息:
if (sscanf(str, "%d /%d /%d", &month, &day, &year) == 3) printf("Month: %d, day: %d, year: %d\n", month, day, year);
else if (sscanf(str, "%d -%d -%d", &month, &day, &year) == 3) printf("Month: %d, day: %d, year: %d\n", month, day, year);
else printf("Date not in the proper form\n");
像scanf
函数和fscanf
函数一样,sscanf
函数也返回成功读入并存储的数据项的数量。如果在找到第一个数据项之前到达了字符串的末尾(用空字符标记),那么sscanf
函数会返回EOF
。
问与答
问1:如果我使用输入重定向或输出重定向,那么重定向的文件名会作为命令行参数显示出来吗?
答:不会。操作系统会把这些文件名从命令行中移走。假设用下列输入运行程序:
demo foo <in_file bar >out_file baz
argc
的值为4
,argv[0]
将指向程序名,argv[1]
会指向"foo"
,argv[2]
会指向"bar"
,argv[3]
会指向"baz"
。
问2:我一直认为行的末尾都是以换行符标记的,现在你说行末标记根据操作系统的不同而不同。如何解释这种差异呢?
答:C
库函数使得每一行看起来都是以一个换行符结束的。不管输入文件有回车符、回行符,还是两者都有,getc
等库函数都只会返回一个换行符。输出函数执行相反的操作。如果程序调用库函数向文件中输出换行符,函数会把该字符转换成恰当的行末标记。C
语言的这种实现使得程序的可移植性更好,也更易编写。我们处理文本文件时不需要担心行的末尾到底是怎么表示的。注意,对以二进制模式打开的文件进行输入/输出操作时,不需要进行字符转换——回车符、回行符跟其他字符同等对待。
问3:我正打算编写一个需要在文件中存储数据的程序,该文件可供其他程序读取。就数据的存储格式而言,文本格式和二进制格式哪种更好呢?
答:这要看情况。如果数据全部是文本,那么用哪种格式存储没有太大的差异。然而,如果数据包含数,那么决定就比较困难一些了。
通常二进制格式更可取,因为此种格式的读和写都非常快。当存储到内存中时,数已经是二进制格式了,所以将它们复制给文件是非常容易的。用文本格式写数据相对就会慢许多,因为每个数必须要转换成字符格式(通常用fprintf
函数)。以后读取文件同样要花费更多的时间,因为必须要把数从文本格式转换回二进制格式。此外,就像在22.1节
看到的那样,以二进制格式存储数据常常能节省空间。
然而,二进制文件有两个缺点。一是很难阅读,这也就妨碍了调试过程;二是二进制文件通常无法从一个系统移植到另一个系统,因为不同类型的计算机存储数据的方式是不同的。比如,有些机器用2
字节存储整数,而有些机器则用4
字节来存储。字节顺序(大端/小端)也是一个问题。
问4:用于
UNIX
系统的C
程序好像从不在模式字符串中使用字母b
,即使待打开的文件是二进制格式也是如此。这是什么原因呢?
答:在UNIX
系统中,文本文件和二进制文件具有完全相同的格式,所以不需要使用字母b
。但是,UNIX
程序员仍应该包含字母b
,这样他们的程序将更容易移植到其他操作系统上。
问5:我已经看过调用
fopen
函数并且把字母t
放在模式字符串中的程序了。字母t
意味着什么呢?
答:C
标准允许其他的字符在模式字符串中出现,但是它们要跟在r
、w
、a
、b
或+
的后边。有些编译器允许使用t
来说明待打开的文件是文本模式而不是二进制模式。当然,无论如何文本模式都是默认的,所以字母t
没有任何作用。在可能的情况下,最好避免使用字母t
和其他不可移植的特性。
问6:为什么要调用
fclose
函数来关闭文件呢?当程序终止时,所有打开的文件都会自动关闭,难道不是这样吗?
答:通常情况下是这样的,但如果调用abort函数(26.2节)
来终止程序就不是了。即使在不用abort
函数的时候,调用fclose
函数仍有许多理由。首先,这样会减少打开文件的数量。操作系统对程序每次可以打开的文件数量有限制,而大规模的程序可能会与此种限制相冲突。(定义在<stdio.h>
中的宏FOPEN_MAX
指定了可以同时打开的文件的最少数量。)其次,这样做使程序更易于理解和修改。通过寻找fclose
函数,读者更容易确定不再使用此文件的位置。最后,这样做很安全。关闭文件可以确保正确地更新文件的内容和目录项。如果将来程序崩溃了,至少该文件不会受到影响。
问7:我正在编写的程序会提示用户输入文件的名字。我要设置多长的字符数组才可以存储这个文件名字呢?
答:这与使用的操作系统有关。好在你可以使用宏FILENAME_MAX
(定义在<stdio.h>
中)来指定数组的大小。FILENAME_MAX
是字符串的长度,这个字符串用于存储保证可以打开的最长的文件名。
问8:
fflush
可以清除同时为读和写而打开的流吗?
答:根据C
标准,当流(1)
为输出打开,或者(2)
为更新打开并且最后一个操作不是读时,调用fflush
的结果才有定义。在其他所有情况下,调用fflush
函数的结果是未定义的。当传递空指针给fflush
函数时,它会清除所有满足(1)
或(2)
的流。
问9:在
...printf
函数或...scanf
函数调用中,格式串可以是变量吗?
答:当然。它可以是char *
类型的任意表达式。这个性质使...printf
函数和...scanf
函数比我们想象的更加多样。请看下面这个来自Kernighan
和Ritchie
所著的《C
程序设计语言》一书的经典示例。此示例显示程序的命令行参数,以空格分隔:
while (--argc > 0) printf((argc > 1) ? "%s " : "%s", *++argv);
这里的格式串是表达式(argc > 1) ? "%s " : "%s"
,其结果是除了最后一个参数以外,对其他所有命令行参数都会使用"%s "
。
问10:除了
clearerr
函数,哪些库函数可以清除流的错误指示器和文件末尾指示器?
答:调用rewind
函数可以清除这两种指示器,就好像打开或重新打开流一样;调用ungetc
函数、fseek
函数或者fsetpos
函数仅可以清除文件末尾指示器。
问11:我无法使
feof
函数工作。这是因为即使到了文件末尾,它好像还是返回0
。我做错了什么吗?
答:当前面的读操作失败时,feof
函数只会返回一个非零值。在尝试读之前,不能使用feof
函数来检查文件末尾。相反,你应该首先尝试读,然后检查来自输入函数的返回值。如果返回的值表明操作不成功,那么你可以随后使用feof
函数来确定失败是不是因为到了文件末尾。换句话说,最好不要认为调用feof
函数是检测文件末尾的方法,而应把它看作确认读取操作失败是因为到了文件末尾的方法。
问12:我始终不明白为什么输入/输出库除了提供名为
fputc
和fgetc
的函数以外,还提供名为putc
和getc
的宏。依据21.1节
的介绍,putc
和getc
已经有两种版本了(宏和函数)。如果需要真正的函数而不是宏,我们可以通过取消宏的定义来显示putc
函数或getc
函数。那么,为什么要有fputc
和fgetc
存在呢?
答:这是历史原因造成的。在标准化以前,C
语言没有规则要求用真正的函数在库中备份每个带参数的宏。putc
函数和getc
函数传统上只作为宏来实现,而fputc
函数和fgetc
函数则只作为函数来实现。
问13:把
fgetc
函数、getc
函数或者getchar
函数的返回值存储到char
类型变量中会有什么问题?我不明白为什么判断char
类型变量的值是否为EOF
会得到错误的结果。
答:有两种情况可能导致该判定得出错误的结果。为了使下面的讨论更具体,这里假设使用二进制补码存储方式。
首先,假定char
类型是无符号类型。(回忆一下,有些编译器把char
作为有符号类型来处理,而有些编译器则把它看成无符号类型的。)现在假设getc
函数返回EOF
,把该返回值存储在名为ch
的char
类型变量中。如果EOF
表示-1
(通常如此),那么ch
的值将为255
。把ch
(无符号字符)与EOF
(有符号整数)进行比较就要求把ch
转换为有符号整数(在这个例子中是255
)。因为255
不等于-1
,所以与EOF
的比较失败了。
反之,现在假设char
是有符号类型。如果getc
函数从二进制流中读取了一个含有值255
的字节,这样会产生什么情况呢?因为ch
是有符号字符,所以把255
存储在char
类型变量中将为它赋值-1
。如果判断ch
是否等于EOF
,则会(错误地)产生真结果。
问14:
22.4节
描述的字符输入函数要求在读取用户输入之前看到回车键。如何编写能直接响应键盘输入的程序?
答:我们注意到,getc
、fgetc
和getchar
都是分配缓冲区的,这些函数在用户按下回车键时才开始读取输入。为了实时读取键盘输入(这对某些类型的程序很重要),需要使用适合你的操作系统的非标准库。例如,UNIX
中的curses
库通常提供这一功能。
问15:正在读取用户输入时,如何跳过当前输入行中剩下的全部字符呢?
答:一种可能是编写一个小函数来读入并且忽略第一个换行符之前的所有字符(包含换行符):
void skip_line(void)
{ while (getchar() != '\n') ;
}
另外一种可能是要求scanf
函数跳过第一个换行符前的所有字符:
scanf("%*[^\n]"); /* skips characters up to new-line */
scanf
函数将读取第一个换行符之前的所有字符,但是不会把它们存储下来(*
表示赋值屏蔽)。使用scanf
函数的唯一问题是它会留下换行符不读,所以可能需要单独丢弃换行符。
无论做什么,都不要调用fflush
函数:
fflush(stdin); /* effect is undefined */
虽然某些实现允许使用fflush函数来“清洗”未读取的输入,但是这样做并不好。fflush
函数是用来清洗输出流
的。C
标准规定fflush
函数对输入流的效果是未定义的。
问16:为什么把
fread
函数和fwrite
函数用于文本流是不好的呢?
答:困难之一是,在某些操作系统中对文本文件执行写操作时,会把换行符变成一对字符(详细内容见22.1节
)。我们必须考虑这种扩展,否则就很可能搞错数据的位置。例如,如果使用fwrite
函数来写含有80
个字符的块,因为换行符可能被扩展,所以有些块可能会占用多于80
字节的空间。
问17:为什么有两套文件定位函数(即
fseek
/ftell
和fsetpos
/fgetpos
)呢?一套函数难道不够吗?
答:fseek
函数和ftell
函数作为C
库的一部分已有些年头了,但它们有一个缺点:它们假定文件位置能够用long int
类型的值表示。由于long int
通常是32
位的类型,当文件大小超过2147483647
字节时,fseek
函数和ftell
函数可能无法使用。针对这个问题,创建C89
标准时在<stdio.h>
中增加了fsetpos
和fgetpos
。这两个函数不要求把文件位置看作数,因此就没有long int
的限制了。但是也不要认为必须使用fsetpos
和fgetpos
,如果你的实现支持64
位的long int
类型,即使对很大的文件也可以使用fseek
和ftell
。
问18:为什么本章不讨论屏幕控制,即移动光标、改变屏幕上字符颜色等呢?
答:C
语言没有提供用于屏幕控制的标准函数。标准只发布那些通过广泛的计算机和操作系统可以合理标准化的问题,而屏幕控制超出了这个范畴。在UNIX
中解决这个问题的习惯做法是使用curses
库,这个库支持不依赖终端方式的屏幕控制。
类似地,也没有标准函数可以用来构建带有图形用户界面的程序。不过,可以用C
函数调用来访问操作系统中的窗口API(应用程序接口)
。
写在最后
本文是博主阅读《C语言程序设计:现代方法(第2版·修订版)》时所作笔记,日后会持续更新后续章节笔记。欢迎各位大佬阅读学习,如有疑问请及时联系指正,希望对各位有所帮助,Thank you very much!