做加密网站全站加密的最低成本,可信网站认证购买,莱芜网站建设价格低,想做网站哪个公司比较好文章目录 第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章 输入/输出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中标记为\x1aCtrlZ。CtrlZ不是必需的但如果存在它就标志着文件的结束其后的所有字节都会被忽略。使用CtrlZ的这一习惯继承自DOS而DOS中的这一习惯又是从CP/M早期用于个人计算机的一种操作系统来的。大多数其他操作系统包括UNIX没有专门的文件末尾字符。
二进制文件不分行也没有行末标记和文件末尾标记所有字节都是平等对待的。 向文件写入数据时我们需要考虑是按文本格式存储还是按二进制格式来存储。为了搞清楚其中的差别考虑在文件中存储数32767的情况。一种选择是以文本的形式把该数按字符3、2、7、6、7写入。假设字符集为ASCII那么就可以得到下列5个字节 0011001100110010001101110011011000110111‘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”创建文件用于写文件不能已经存在①“wx”创建文件用于更新文件不能已经存在①“a”打开文件用于追加文件不需要存在“r”打开文件用于读和写从文件头开始“w”打开文件用于读和写如果文件存在就截去“a”打开文件用于读和写如果文件存在就追加
① 从C11开始引入的模式独占的创建-打开模式。 当使用fopen打开二进制文件时需要在模式字符串中包含字母b。表22-3列出了用于二进制文件的模式字符串。 表22-3 用于二进制文件的模式字符串
字符串含义“rb”打开文件用于读“wb”打开文件用于写文件不需要存在“wbx”创建文件用于写文件不能已经存在①“ab”打开文件用于追加文件不需要存在“rb或者rb”打开文件用于读和写从文件头开始“wb或者wb”打开文件用于读和写如果文件存在就截去“wbx或者wbx”创建文件用于更新文件不能已经存在①“ab或者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是3argv[0]指向含有程序名的字符串argv[1]指向字符串names.dat而argv[2]则指向字符串dates.dat。 下面举例一个程序该程序判断文件是否存在如果存在则判断它是否可以打开并读入。在运行程序时用户将给出要检查的文件的名字 canopen file然后程序将显示出file can be opened或者显示出file cant 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分析如下
标志最小栏宽精度长度指定符转换指定符%#012.5Lg 下面对上述这些选项进行详细的描述选项的顺序必须与上面一致: 标志可选项允许多于一个。标志—导致在栏内左对齐而其他标志则会影响数的显示形式。表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、Xsigned char, unsigned charhh①nsigned char *hd、i、o、u、x、Xshort int, unsigned short inthnshort int *lelld、i、o、u、x、Xlong int, unsigned long intlellnlong int *lellcwint_tlellswchar_t *lella、A、e、E、f、F、g、G无作用ll①ell-elld、i、o、u、x、Xlong long int, unsigned long long intll①ell-ellnlong long int *j①d、i、o、u、x、Xintmax_t, uintmax_tj①nintmax_t *z①d、i、o、u、x、Xsize_tz①nsize_t *t①d、i、o、u、x、Xptrdiff_tt①nptrdiff_t *La、A、e、E、f、F、g、Glong 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、Gg会把double类型值转换为f形式或者e形式。当数值的指数部分小于-4或者指数部分大于等于精度值时会选择e形式显示。尾部的零不显示除非使用了#标志且小数点仅在后边跟有数字时才显示出来。G会在F形式和E形式之间进行选择a①、A①使用格式[-]0xh.hhhhp±d的格式把double类型值转换为十六进制科学记数法形式。其中[-]是可选的负号h代表十六进制数位±是正号或者负号d是指数。d为十进制数表示2的幂。如果没有指定精度在小数点后将显示足够的数位来表示准确的数值如果可能的话。a表示用小写形式显示a~fA表示用大写形式显示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%-8d123•••••-123••••%8d••••123••••-123% 8d•••••123••••-123%08d00000123-0000123%-8d123••••-123••••%- 8d•123••••-123••••%08d0000123-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•bogusbuzzword%-6sbogus•buzzword%.4sbogubuzz%6.4s••bogu••buzz%-6.4sbogu••buzz••
表22-10说明了%g转换如何以%e和%f的格式显示数。表中的所有数都用转换说明%.4g进行了书写。前两个数的指数至少为4因此它们是按照%e的格式显示的。接下来的8个数是按照%f的格式显示的。最后两个数的指数小于-4所以也用%e的格式来显示。
表22-10 %g转换的示例
数对数应用转换%.4g的结果123456.000000000001.235e0512345.600000000001.235e041234.560000000001235123.45600000000123.512.3456000000012.351.234560000001.2350.123456000000.12350.012345600000.012350.001234560000.0012350.000123456000.00012350.000012345601.235e-050.000001234561.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、nsigned char , unsigned charhd、i、o、u、x、X、nshort int , unsigned short intlelld、i、o、u、x、X、nlong int , unsigned long intlella、A、e、E、f、F、g、Gdouble *lellc、s、[wchar_t *ll①ell-elld、i、o、u、x、X、nlong long int , unsigned long long intj①d、i、o、u、x、X、nintmax_t , uintmax_tz①d、i、o、u、x、X、nsize_t *t①d、i、o、u、x、X、nptrdiff_t *La、A、e、E、f、F、g、Glong 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中该数可以是无穷大或NaNc匹配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转换指定符和数值转换函数之间的对应关系
转换指定符字符串转换函数d10作为基数的strtol函数i0作为基数的strtol函数o8作为基数的strtoul函数u10作为基数的strtoul函数x、X16作为基数的strtoul函数a、A、e、E、f、F、g、Gstrtod函数 请注意!!编写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:34n 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:34n 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:45n 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:18n scanf(“%[0123456789]”, str);123abc¤n:1 Str: “123”n scanf(“%[0123456789]”, str);abc123¤n:0 str:不变n scanf(“%[^0123456789]”, str);abc123¤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或rewind22.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, Cant open %s\n, argv[1]); exit(EXIT_FAILURE); } if ((dest_fp fopen(argv[2], wb)) NULL) { fprintf(stderr, Cant 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函数对流是文本的还是二进制的很敏感。对于文本流而言要么offsetfseek的第二个参数必须为0要么whencefseek的第三个参数必须是SEEK_SET且offset的值通过前面的ftell函数调用获得。换句话说我们只可以利用fseek函数移动到文件的起始处或者文件的末尾处或者返回前面访问过的位置。对于二进制流而言fseek函数不要求支持whence是SEEK_END的调用。 ftell函数以长整数返回当前文件位置。如果发生错误ftell函数会返回-1L并且把错误码存储到errno24.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_LEN1]; 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的值为4argv[0]将指向程序名argv[1]会指向fooargv[2]会指向barargv[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是字符串的长度这个字符串用于存储保证可以打开的最长的文件名。 问8fflush可以清除同时为读和写而打开的流吗 答根据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则会错误地产生真结果。 问1422.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!