perror函数以一种简单、统一的方式报告错误。
ANSI C 函数库的许多函数调用操作系统来完成某些任务,I/O函数就是如此,当操作系统执行任务的时候,不论任何时候都具有失败的可能性。
所以标准库函数在一个外部整型变量变量errno中保存错误代码之后把这个信息传递给用户程序,提示操作失败的准确原因。
perror函数简化向用户报告这些特定错误的过程,它的原型定义于stdio.h,如下:
void perror( const char* message);
如果message表示NULL并且指向一个非空的字符串,perror函数就打印出这个字符串,后面跟一个分号和一个空格,然后打印出一条用于解释errno当前错误代码的信息。
提示:perror最大的优点就是 容易使用。良好的编程实践要求任何可能产生错误的操作都应该在执行之后检查,确定是否成功执行。即使看上去十拿九稳的也要检查,因为它们迟早可能失败。
还有一个exit函数,它用于终止一个程序的执行,它的原型定义于stdlib.h,如下所示:
void exit(int status);
status参数返回给操作系统,用于提示程序是否正常完成。预定义符号EXIT_SUCCESS和 EXIT_FAILURE分别提示 程序的终止是成功还是失败。
这个函数经常在调用perror之后再调用它终止程序,注意这个函数没有返回值,当exit函数结束时,程序已经消失。
标准I/O函数库具有一组I/O函数,这个函数库对现存的函数进行了扩展,例如为printf函数提供了不同的版本,可以适用于许多场合。
函数库同时引进了缓冲I/O的概念,提高了绝大多数程序的效率。
ANSI C库函数中的I/O函数是旧式标准I/O库函数的直接后代。再设计ANSI函数库时,可移植性和完整性是两个关键的考虑内容,这些函数是对原本的函数进行诸多完善之后的结果,但是它们仍然可以进一步改进。
ANSI C的一个主要优点就是这些修改将通过增加不同的函数的方式实现,而不是通过对现存函数进行修改来实现,因此程序的可移植性不会受到影响。
头文件stdio.h包含了与ANSI函数库的I/O部分有关的声明
4.1流
ANSI C进一步对I/O的概念进行了抽象,就C程序而言,所有的I/O操作只是简单的从程序移进或者移出字节的事情。这种字节流便被称为流。
绝大多数流是完全缓冲的,这意味着“读取”和”写入“实际上是从一块被称为缓冲区(buffer)的内存区域来回复制数据,用于输出流的缓冲区只有当它写满时才会被刷新(flush,物理写入)到设备或文件中。
使用标准输入个输出时,这种缓冲可能会引起混淆,所以只有当操作系统可以断定它们与交互设备无联系时才会进行完全缓冲。
一个常见(但是不普遍)的策略是把标准输入和标准输出联系再一起,就是当请求输入时同时刷新输出缓冲区,这样在用户必须进行输入之前,提示用户进行输入的信息和以前写入到输出缓冲区中的内容将出现在屏幕上。
一、文本流
流分为两种类型,文本流和二进制流,文本(text)流的特性在不同的系统中可能不同。
二、二进制流
二进制流中的字节将完全根据程序编写它们的形式写入到文件或设备中,而且完全根据它们从文件或设备读取的形式读入到程序中。这种类型的流适用于非文本数据。
4.2文件
stdio.h所包含的声明之一就是FILE结构,FILE是一个数据结构,用于访问一个流。如果你同时激活了几个流,每个流都有一个合适的相应的FILE与它关联。
对于每个ANSI C程序运行时必须提供至少三个流——标准输入、标准输出和标准错误。这些流的名字分别是stdin、stdout和stderr,他们都是一个指向FILE结构的指针。标准输入是缺省情况下输入的来源,标准输出是缺省的输出设置,标准错误就是错误信息写入的地方,perror函数也把它的输出写到这个地方。
下面是关于文件I/O的一般概况。
1.程序为必须同时处于活动状态的每个文件声明一个指针变量,其类型为FILE*。这个指针指向这个FILE结构,当它处于活动状态时由流使用。
2.流通过fopen函数打开,为了打开一个流,必须指定需要访问的文件或设备以及他们的访问方式(例如:读,写或既读又写)。fopen函数和操作系统验证文件或设备确实存在,并初始化FILE结构。
3.然后根据需要对该文件进行读取或写入
4.最后,调用fclose函数关闭流,关闭一个流可以防止与它相关联的文件再次被访问,保证在存储于缓冲区的数据被正确的写到文件中并且释放FILE结构使它可以用于另外 的文件。
标准流的I/O更简单,因为它们不需要打开或者关闭。
I/O函数以三种基本的形式处理数据,单个字符、文本行和二进制数据。对于每种形式都有一组特定的函数对他们进行处理。
下图是执字符、文本行和二进制I/O的函数:
1.只用于stdin或stdout
2.随作为参数的流使用
3.使用内存中的字符串而不是流
需要一个流的参数的函数将接受stdin或stdout作为它的函数。
下表示每个家族的函数:
①对指针使用下标引用或间接访问操作从内存获得一个字符(或向内存写入一个字符)
②使用strcpy函数从内存中读取文本行(或向内存中写入文本行)
fopen函数打开一个特定的文件,并把一个流和这个文件相关联,它的原型如下所示:
FILE* fopen(const char* name, const chat* mode);
两个参数都是字符串,name是你希望打开的文件或设备的名字,创建文件名的规则在不同的系统中可能不相同,所以fopen把文件名作为一个字符串而不是作为路径名、驱动器字母、文件扩展名等各准备一个函数。
这个参数指定要打开的文件——FILE*变量的名字是程序用来保存fopen的返回值的。mode(模式)参数提示流是用于只读、只写还是既读又写,以及它是文本流还是二进制流,下面表格是一些常用的模式:
如果一个文件打开是用于读取的,那么它必须是原先已经存在的。
如果一个文件打开是用于写入的,如果它原先已经存在,那么它原来的内容就会被删除,如果它原先不存在,那么就创建一个新文件
如果一个打开用于添加的文件原先并不存在,那么它将被创建,如果它原先已经存在,它原先的内容并不会被删除,无论是哪一种情况下,数据只能从文件的尾部写入。
在mode中添加“a+”表示该文件打开只用于更新,并且流即允许读也允许写。但是如果你已经从该文件读入了一些数据,那么在你写入数据时,你必须调用其中一个文件定位函数(fseek、fsetpos、rewind)
在你向文件写入一些数据之后,如果又想从该文件读取一些数据,必须首先调用fflush函数或某个文件定位函数之一。
如果fopen函数执行成功,它返回一个指向FILE结构的指针,该结构代表这个新创建的流,如果函数执行失败,它就返回一个NULL指针,errno会提示问题的性质。注意,应该使用检查fopen函数的返回值。
freopen函数用于打开(或者重新打开)一个特定的文件流,它的原型如下:
FLIE* freopen(const char* filename, const char* mode, FILE* stream);
最后一个参数就是需要打开的流,它可能是一个向前从fopen函数返回的流,也可能是从标准流stdin、stdout、stderr
这个函数试图关闭这个流,然后用指定的文件和模式重新打开这个流,如果打开失败,函数返回一个NULL值,如果打开成功,函数就返回它的第三个参数值
流是用函数fclose关闭的,它的原型如下:
int fclose(FLIE* f);
对于输出流,fclose函数在文件关闭之前刷新缓冲区,如果它执行成功,fclose函数返回零值,否则返回EOF
注意,fopen函数和fclose函数都应该对它们的返回值进行检查,任何有可能失败的操作都应该进行检查,确定它是否成功执行。
当一个流被打开之后,它可以用于输入和输出。它最简单的形式是字符I/O。
字符输入是由getchar函数家族执行的,它们的原型如下:
int fgetc(FLIE* stream);
int getc(FILE* stream);
int getchar(void);
需要操作的流作为参数传递给getc和fgetc,但是getchar始终从标准输入读取。
每个函数都从流中读取下一个字符并把它作为函数的返回值返回。如果流中不存在更多的字符,函数就返回常量值EOF。
注意,这些函数都用于读取字符,但它们都返回一个int型值而不是char型,这是为了允许函数报告文件的末尾(EOF)。
如果返回值是char型,那么在256个字符中必须由一个被指定用于表示EOF,如果这个字符出现在文件内部,那么这个字符以后的内容将不会读取,因为它被解释为EOF标志,所以EOF被定义为一个整型,它的值在任何可能出现的字符之外。
为了把单个字符写入到流中,你可以使用putchar函数家族,它们的原型如下:
int fputc(int chatacter, FILE* stream);
int putc(int character, FILE* stream);
int putchar(int character);
第一个参数是要打印的字符,在打印之前,函数把这个整型参数裁剪为一个无符号字符型值,所以
putchar('abc');
值打印一个字符,如果由于 任何原因导致函数失败,它们就返回EOF。
8.1字符I/O宏
fgetc和fputc都是真正的函数,但get、putc、getchar和putchar都是通过#define指令定义的宏,宏在执行时间上效率稍高,而函数在程序的长度方面更胜一筹。
8.2撤销字符I/O
ungetc**把一个先前读入的字符返回到流中,这样它可以在以后被重新读入**,函数原型如下:
int ungetc(int character, FILE* stream);
每个流都允许至少一个字符被退回,如果一个流允许退回多个字符,那么这些字符再次被读取的顺序就以退回时的反序进行。
注意把字符退回到流中和写入到流中并不相同,与一个流相关联的外部存储并不受ungetc的影响,
行I/O可以用两种方式执行——未格式化的和格式化的。这两种形式都用于操纵字符串。
区别在于未格式化的I/O简单读取或写入字符串,而格式化的I/O则执行数字和其他变量的内部和外部表示形式之间的转换。
gets和puts函数家族是用于操纵字符串而不是单个字符,函数原型如下:
char* fgets(char* buffer, int buffer_size, FILE* stream);
char* gets(char* buffer);
int fputs(const char* buffer, FILE* stream);
int puts(const char* buffer);
fgets从指定的stream读取字符并把它们复制到buffer中,当它读取一个换行符并存储到缓冲区之后就不再读取。如果缓冲区内存储的字符数达到buffer_size - 1时它就停止读取。
在任何一种情况下,一个NUL字节将被添加到缓冲区所存储数据的末尾,使它称为一个字符串。
如果在任何字符读取前就达到了文件尾,缓冲区就未进行修改,fgets函数返回一个NULL指针,否则fgets函数返回它的第一个参数。
传递给fputs的缓冲区必须包含一个字符串,它的字符被写入到流中,这个字符串以预期的NUL字节结尾,所以这个函数没有一个缓冲区长度参数。这个字符串使逐字写入的:如果它不包含一个换行符,就不会写入换行符。如果它包含了好几个换行符,所有的换行符都被写入。
因此,当fgets每次都读取一整行时,fputs却既可以一次写入一行的一部分,也可以一次写入一整行,甚至可以一次写入好几行。
如果写入时出现了错误,fputs返回常量值EOF,否则它将返回一个非负值。
gets和puts函数几乎和fgets和fputs相同,之所以存在它们是为了允许向后兼容,它们的主要功能性区别在于当gets读取一行输入时,它并不在缓冲区中存储结尾的换行符。当puts写入一个字符串时,它在字符串写入之后向输出再添加一个换行符。
gets函数没有缓冲区长度参数,因此gets无法判断缓冲区的长度。
10.scanf家族
scanf函数家族的原型如下所示,每个原型中的省略号表示一个可变长度大的指针列表。从输入转换而来的值逐个存储到这些指针参数所指向的内存位置。
int fscanf(FILE* stream, const char* format, ...);
int scanf(const char* format, ... );
int sscanf(const char* string, const char* format);
这些函数都是从输入源读取字符并根据format字符串给出的个是代码对它们进行转换。
fscanf的输入源就是作为参数给出的流,scanf从标准输入读取,而sscanf则从第一个参数给出的字符串中读取字符。
当格式化字符串到达末尾或者读取的输入不再匹配格式字符串所指定的类型时,输入就停止,在任何一种情况下,被转换的输入值的数目作为函数的返回值返回。如果再任何输入值被转换之前文件就已到达尾部,函数就返回常量值EOF。
10.2scanf中的格式代码
scanf函数家族中的format字符串参数可能包含如下内容:
1.空白字符——它们与输入中的零个或多个空白字符匹配,在处理过程中将被忽略。
2.格式代码——它们指定函数如何解释接下来的输入字符
3.其他字符——当任何其他字符出现在格式字符串时,下一个输入字符必须与它匹配,如果匹配,该输入的字符随后就被丢弃,如果不匹配,函数就不再读取直接返回。
scan函数家族的格式代码都以一个%开头,后面可以是:
(1)一个可选的星号
(2)一个可选的宽度
(3一个可选的限定符
(4)格式代码
星号将使转换后的值被丢弃而不是进行存储;宽度以一个非负的的整数给出,它限制将被读取用于转换的输入字符的个数。限定符用于修改有些格式代码的含义,在下表列出:
限定符的目的是为了指定参数的长度,如果整型参数比缺省的整型值更短或更长时,在格式代码中省略限定符就是一个常见的错误。对于浮点类型也是如此,如果省略了限定符,可能会导致一个较长变量只有部分被初始化,或者一个较短变量的邻近变量也被修改。而这些都取决于类型的相对长度。
格式代码就是一个单个字符,同于指定输入字符如何被解释,如下表所示:
10.3printf函数
printf函数家庭用于创建格式化的输出,这个家族共有三个函数:fprintf、printf和sprintf。函数原型如下:
int fprintf(FILE* stream, const char* format, ... );
int printf(const char* format, .. );
int sprintf(char* buffer, const chat* format, ... );
printf根据格式代码和format参数中的其他字符对参数列表中的值进行格式化。使用printf,结果输出送到标准输出。使用 fprintf,你可以使用任何输出流,而sprintf把它的结果作为一个NUL结尾的字符串存储到指定的buffer缓冲区而不是写入到流中,这三个函数的返回值是实际打印或存储的字符数。
sprintf函数是一个潜在的错误根源,因为buffer可能会溢出,要解决这个问题,第一种方法是声明一个超级大的缓冲区,但是大型缓冲区并不能保证永不溢出,,第二种方法是对格式进行分析,看看最大可能出现的值被转换后的结果输出将有多长。
10.4 printf格式代码
printf函数原型中的format字符串可能包含格式代码,它是参数列表的下一个值根据指定的方式进行格式化,至于其他的字符则原样逐字打印。
格式代码由一个%开头,后面可以跟:
(1)零个或多个标志字符,用于修改有些转换的执行方式
(2)一个可选的最小字段宽度
(3)一个可选的精度
(4)一个可选的修改符
(5)转换类型
标志和其他字段的准确含义取决于使用何种转换。表一描述了转换类型代码,表二描述了标志字符和它们的含义。
表一:
表二:
字段宽度是一个十进制整数,用于指定出现将出现在结果中的最小字符数,如果值的字符数少于字段宽度,就对它进行填充以增加长度。
对于d、i、u、o、x和X类型的转换,精度字段指定将出现在结果中的最小的数字个数并覆盖为0标志。如果转化后的值位数小于宽度,就在它的前面插入0,如果值为0且精度也为0,则转换结果就不会产生数组。
对于e、E和f类型的转换,精度决定将出现在小数点之后的数字位数。对于g和G类型的转换,它指定将出现在结果中的最大有效位数。当使用s类型的转换时,精度指定将被转换的最多字符数。
精度以一个句点开头,后面根一个可选的十进制整数。如果未给出整数,精度的缺省值未0。
如果用于表示字段宽度和精度的十进制整数由一个星号代替,那么printf的下一个参数(必须是个整数)就提供宽度和(或)精度。
当字符或短整数值作为printf函数的参数时,它们在传递给函数之前先转换为整数,有时候转换可以影响函数产生的输出。
下标的修改符用于指定整数和浮点数参数的准确长度:
在有些环境里,int和short int的长度相等,此时h修改符就没有效果。否则,当short int作为参数传递给函数时,这个被转换的值将升级为(无符号)int类型。
#标志可以用于几种printf格式代码,为转换选择一种替代形式。这些形式的细节入下表:
printf函数可以使用丰富的格式代码、修改符、限定符、替代形式和可选字段,这使得它看上去极为复杂。但是,它们能够在格式化输出时提供极大的灵活性。
把数据写到文件效率最高的方法是用二进制形式写入,二进制输出避免了在数值转换尾字符串过程中所设计的开销和精度损失。
fread函数用于读取二进制数据,fwrite函数用于写入二进制数据。它们的原型如下所示:
size_t fread(void* buffer, size_t size, size_t count, FLIE* stream);
size_t fwrite(void* buffer, size_t size, size_t count, FILE* stream);
buffer是一个指向用于保存数据的内存位置的指针,size是缓冲区中每个元素的字节数,count是读取或写入的元素数,stream是数据读取或写入的流。
buffer参数被解释为一个或多个值的数组,count参数指定数组中由多少个值,所以读取或写入一个标量时,count的值时1.函数的返回值是实际读取或写入的元素数目。
处理物理流时,另外还有一些函数也较为有用。首先是fflush,它迫使一个输出流的缓冲区内的数据进行物理写入,不管它是不是已经写满,原型如下:
int fflush(FILE* stream);
当我们需要立即把输出缓冲区的数据进行物理写入时,应该使用这个函数。
在正常情况下,数据以线性方式写入。后面写入的数据在文件中的位置时在一前所有写入数据的后面。
C同时支持随机访问I/O,也就是以任意顺序访问文件的不同位置。随机访问是通过在读取或写入先前定位到文件中需要的位置来实现的。有两个函数用于执行这项操作,函数原型如下:
long ftell(FILE* stream);
int fseek(FILE* stream, long offset, int from);
ftell函数返回流的当前位置,也就是说,下一个读取或写入将要开始的位置距离文件起始位置的偏移量。这个函数允许你保存一个文件的当前位置,在二进制流中,这个位值就是当前位置距离文件起始位置之间的字节数。
在文本流中,这个值表示一个位置,但它并不一定准确的表示当前位置和文件起始位置之间的字符数,因为有些系统将对末字符进行翻译转换,但是,ftell函数返回的值总是可以用于fseek函数中,作为一个距离文件起始位置的偏移量。
fseek函数允许你在一个流中定位,这个操作将改变下一个读取或写入操作的位置,它的第一个参数是需要改变的流,它的第二个和第三个参数可以使用的方法。
在二进制流中,从SEEK_END进行定位可能不被支持,所以应该避免,在文本流中,如果from是SEEK_CUR或SEEK_END,offser必须是0,如果from是SEEK_SET,offset必须是一个从同一个流中以前调用ftell所返回的值。
fseek参数表:
一个可移植的程序不能根据实际写入字符数的计算结构定位到文本流的一个程序。因为文本流所执行的行末字符映射,文本文件的字节数和程序写入的字节数不同。
用fseek改变一个流的位置会带来三个副作用:
首先,行末指示字符被清初,其次如果在fseek之前使用ungetc把一个字符返回到流中,那么这个被退回的字符会被丢弃,因为在定位操作以后,他不是“下一个字符”,最后,定位允许你从写入模式切换到读取模式,或者回到打开的流以便更新。
另外还有三个额外的函数,用一些限制更严的方式执行相同的任务,函数原型如下:
void rewind(FILE* stream);
int fgetpos(FILE* stream, fops_t* posistion);
int fsetpos(FILE* stream, fpos_t const *posistion);
rewind函数将读/写指针设置回指定流的起始位置,它同时清楚流的错误提示标志。fgetpos和fsetpos函数分别是ftell和fseek函数的替代方案。
它们的主要区别是在于这对函数接受一个指向fpos_t的指针作为参数,fgetpos在这个位置存储文件的当前位置,fsetpos把文件位置设置尾存储在这个位置的值。
用fpos_t表示一个文件位置的方式并不是由标准定义的,它可能是文件中的一个字节偏移量,也可能不是。因此使用一个从fgetpos函数返回的fpos_t类型的值唯一安全的用法是把它作为参数传递给后续的fsetpos函数。
下面两个函数可以用于对缓冲方式进行修改。这两个函数只有当指定的流被打开但还没有在它没有上面执行任何其他的操作前才能被使用。函数原型如下:
void stebuf(FILE* stream, char* buf);
int setvbuf(FILE* stream, char* buf, int mode, size_t size);
setbuf设置了另一个数组,用于对流进行缓冲。这个数组的字符长度必须为BUFSIZ。为另一个流自行指定缓冲区可以防止I/O函数库为它动态分配一个缓冲区。如果用一个NULL参数调用这个函数,setbuf函数将关闭流的所有缓冲方式。字符准确的将程序所归引的方式进行读取和写入。
setvbuf函数更为通用,mode参数用于指定缓冲类型**,_IOFBF指定一个完全缓冲的流,**_IONBF指定一个不缓冲的流,_IOLBF指定一个行缓冲流。所谓行缓冲,就是每当一个换行符写入到缓冲区时缓冲区便执行刷新。
buf和size参数用于指定需要使用的缓冲区。如果buf为NULL,那么size的值必须时0.一般用一个长度为BUFSIZ的字符数组作为缓冲区。
下面函数用于判断流的状态,函数原型如下:
int feof(FILE* stream);
int ferror(FLIE* stream);
void clearerr(FILE* stream);
如果流当前处于文件尾,feof函数返回真。这个状态可以通过对流执行fseek,rewind或fsetpos函数来清除。
ferror函数报告流的错误状态,如果出现任何读/写错误函数就返回真。
最后clearerr函数对指定流的错误标志进行重置。
偶尔会使用一个文件来临时保存数据。程序结束时这个文件便被删除。tmpfile函数就是用于这个目的。
FILE* tmpfile( void );
这个函数创建了一个文件,当文件被关闭或程序终止时这个文件便自动删除。该文件以wb+模式打开,这使它可以用于二进制和文本数据。
如果临时文件必须以其他模式打开或者由一个程序打开但由另一个程序读取,就不适合用tmpfile函数创建。这个时候必须用fopen函数,而当结果文件不再需要时必须使用remove函数显示的删除。
临时文件的名字可以用tmpnam函数创建,函数原型如下:
char* tmpnam(char* name);
如果传递给函数的参数为NULL,那么这个函数便返回一个指向静态数组的指针,该数组包含了被创建的文件名。否则函数便假定是一个指向长度至少为L_tmpnam的字符数组的指针。
无论哪种情况,这个被创建的文件名保证不会与已经存在的文件名同名。只要调用次数不超过TMP_MAX次,这个函数每次调用时都能产生一个新的名字。
有两个函数用于操纵文件但不执行任何输入/输出操作。函数原型如下:
int remove(const char* filename);
int rename(const char* oldname, const char* newname);
如果执行成功,这两个函数都返回零值,如果失败它们都返回非零值。
remove函数删除一个指定的文件,如果但remove被调用时函数处于打开状态,其结果则取决于编译器。
rename函数用于改变一个文件的名字,从oldname改为newname。如果已经由一个名为newname的文件存在,其结果于编译器。如果函数失败,文件仍然可以用原来的名字进行访问。
太鸡儿多了。
完。