前言:与早期C相比,ANSI C 的一个最大的进步就是它的规范里包含了函数库。每个ANSI编译器必须支持一组特定的函数,并规范所要求的接口,而且按照规定的行为工作。ANSI编译器并未禁止在它们的函数库的基础上增加其他函数。但是,标准库函数必须根据标准所定义的方式执行。如果关心可移植性问题,只要避免使用任何非标准函数就可以。
15.1 错误报告
1、perror函数(定义在 stdio.h中) ** 以一种简单的、统一的方式报告错误。ANSI C函数库的许多函数调用操作系统来完成任务,尤其是输入输出函数。当操作系统执行任务时,都存在失败的可能。标准库函数在一个外部整型变量 errno(在errno.h中定义)中保存错误代码**之后把这个信息传递给用户程序,提示操作失败的原因。
2、perror函数原型:void perror( char const *message );
3、如果message不是null指针并且指向一个非空字符串,perror函数就打印出这个字符串,后面再跟一个分号和空格,再打印出解释 errno 当前错误代码的信息。
4、良好的编程实践要求任何可能差生错误的操作都应该再执行之后检查,即使是十拿九稳的操作,它们迟早可能失败。
15.2 终止执行
1、exit函数(原型定义于 stdlib.h中) 原型为:void exit( int status );
2、status参数返回给操作系统,用于提示程序是否完成。这个值和main函数返回的整型状态值相同。
3、预定义符号EXIT_SUCCESS(0),EXIT_FAILURE(1)。分别提示程序的终止是成功还是失败。
4、当程序发生错误无法继续执行下去,通常会在调用 perror函数后面再调用 exit终止函数。
15.3 ANSI I/O概念
头文件stdio.h包含了于ANSI函数库的I/O部分有关的声明。
15.3.1 流(文本流和二进制流)
计算机拥有很多不同的外设,每种设备具有不同的特性和协议。操作系统负责这些不同的设备的通信细节,并向程序员提供了一个更为简单和统一的i/o接口。
ANSI C 进一步对I/O概念抽象,所有的I/O操作都是简单的从程序移进或移出字节,这种字节流被称为流(stream)。程序只需要关心正确的创建输出字节数据,以及正确的解释输入的字节数据。
绝大部分流是完全缓冲的(fully buffered),这意味着读取和写入实际上是从一块称为缓冲区(buffer)的内存区域 来回复制数据。用于输出流的缓冲区只有被写满时才会刷新(flush,物理写入)到外设或文件中。这种方法相较于逐片输出分别写入效率高。与之相对应的输入缓冲区为空时会从外设或者文件读取一块较大的输入,重新填充缓冲区。
15.3.2 文本流(text stream)和二进制流(binary stream)
1、文本流在不同的系统有不同的特征。如文本的最大长度,标准定义最少为254字符。还有像文本行的结束方式,ms-dos系统以
回车’\r’和换行符’\n’结束;再unix系统中只需要换行符’\n’结束。
注:标准规定文本行定义为零个或者若干个字符,后面以一个表示结束的换行符。
2、二进制流的字节将完全根据程序编写他们的形式写入到文件或外设中,完全根据他们从外设和文件读取的形式读入到程序中。
15.3.3 文件
stdio.h包含的声明之一就是FILE结构,注意和磁盘上存储的数据文件区分。**FILE是一个数据结构,用于访问一个流。**如果同时激活几个流,那么没给流都有一个相应的FILE与它相关联。
ANSI C 程序运行时至少提供3个流:标准输入(standard input)、标准输出,标准错误。这些流的名字分别为 stdin, stdout, stderr,它们都是指向FILE结构的指针。
15.3.4 标准I/O常量
EOF常量表示到达了文件尾。
FOPEN_MAX,一个程序至少同时能打开FOPEN_MAX个文件。
FILENAME_MAX 提示一个字符数组有多大,一遍容纳编译器所支持的最长合法文件名。
15.4 流I/O总览
关于文件i/o的一半概况:
1、程序为必须同时初始活动状态的每个文件声明一个指针变量,其类型为:FILE * 由流使用。
2、流通过调用fopen函数打开。为打开一个流,必须指定需要访问的文件或设备以及他们的访问方式 (如:读,写,既读又写)。fopen函数向操作系统验证文件或者设备是否存在,以及是否允许指定的访问方式。并初始化FILE结构。
3、fclose函数关闭流。关闭一个流可以防止与它相关联的文件被再次访问,保证每种形式都有一组特定的函数对他们处理。
标准流的I/O并不需要打开或者关闭。
执行字符,文本行,二进制I/O的函数:
数据类型 | 输入 | 输出 | 描述 |
---|
字符 | getchar | putchar | 读取或写入单个字符 | 文本行 | scanf | puts,printf | puts 文本行未格式化输出。其余格式化输入输出。 | 二进制 | fread | fwrite | 读取或者写入二进制数据 |
1、只作用于标准输入输出:stdin,stdout
2、随作为参数的流使用
3、使用内存的字符串而不是流。
下表解读方法:一个getchar家族有fgetc,getc, getchar 函数,目的和效果都在一行的表中。
输入输出函数家族:
家族名 | 目的 | 可用于所有流 | 只用于stdin,stdout | 内存中的字符串 |
---|
getchar | 字符输入 | fgetc, getc | getchar | 下标引用从内存获取 | putchar | 字符输出 | fputc, putc | putchar | 下标引用向内存写入 | gets | 文本行输入 | fgets | gets --已禁用 | strcpy从内存复制 | puts | 文本行输出 | fputs | puts | strcpy向内存写入 | scanf | 格式化输入 | fscanf | scanf | sscanf | printf | 格式化输出 | fprinf | printf | sprintf |
15.5 打开流
fopen函数打开一个特定的文件,并把一个流和这个文件相关联。它的原型如下:
FILE *fopen( char const *name, char const *mode );
两个字符串参数,name是想要打开文件或设备的名字,mode决定流的模型。
常见的mode:
| 读取 | 写入 | 添加 |
---|
文本流 | “r” | “w” | “a” | 二进制流 | “rb” | “wb” | “ab” |
mode 以r,w,a开头。如果一个打开的文件是用于写,它原先的已经存在,它的内容将被删掉。a开头是用于文件的更新,不会删掉原来的内容。数据只能从文件末尾写入。
如果fopen函数打开成功,它将返回一个指向FILE 结构的指针,该结构代表了讲真个新创建的流。函数失败返回NULL指针,errno会提示错误性质。这里就需要检查fopen 的返回值,确保正确打开文件。
例子:
FILE *input
input = fopen( "data", "r" );
if( input == NULL ){
perror( "data" );
exit( EXIT_FAILURE );
}
//如果打开失败,perror会返回如下信息
data: No such file or directory
freopen函数用于打开或者重新打开一个特定的文件流。原型如下:
FILE *freopen( char const *filename, char const *mode, FILE *stream );
最后的一个参数就是需要打开的流。可能是之前fopen函数返回的值,也肯能是标准流stdin,stdout,stderr。
15.6 关闭流
流是用函数fclose关闭的,原型如下:
int fclose( FILE *f );
对于输出流,fclose函数在文件关闭之前刷新缓冲区。如果fclose关闭成功,则返回零值,反之返回EOF。
实例:
#include <stdlib.h>
#include <stdio.h>
int main( int ac, char **av )
{
int exit_status = EXIT_SUCCESS;
FILe *input;
while( *++av != NULL ){
input = fopen( *av, "r" );
if( input == NULL ){
perror( *av );
exit_status = EXIT_FAILURE;
continue;
}
...
if( fclose( input ) != 0){
perror( "fclose" );
exit( EXIT_FAILURE);
}
}
return exit_status;
}
对文件关闭是否成功也一般需要测试。
15.7 字符I/O
1、字符输入是 getchar家族执行的,它们的原型如下:
int fgetc ( FILE *stream ); //真正的函数
int getc ( FILE *stream ); //通过#define定义的宏
int getchar( void ); //通过#define定义的宏
需要操作的流作为参数传递给 fgetc, getc;getchar始终从标准输入读取。每个函数从流中读取一个字符并返回它的值;如果流中国不存在字符,函数就返回常量值EOF。这些函数返回值为整型是为了允许函数报告文件末尾(EOF);EOF所选择的实际值比一个字符多几位,为了避免二进制的值被错误的解释为EOF。如果返回为一个char型,那么256个字符终必有一个被指定为表示EOF,造成错误。
2、字符输出是putchar家族执行的,它们的原型如下:
int fputc ( int character, FILE *stream ); //真正的函数
int putc ( int character, FILE *stream ); //通过#define定义的宏
int putchar( int character ); //通过#define定义的宏
character参数时需要打印的字符。在打印之前函数会裁剪为一个无符号整数型值。向一个已经关闭的文件流写,导致函数失败吗,返回EOF。
15.7.1 撤销字符I/O
在实际读入之前,无法得知下一个字符是什么,因此必须连续读,直到读到与条件不相符的字符,但是这个字符已经读出,又不想丢弃。这时候需要用到函数 ungetc,它的原型如下:
int ungetc( int character, FILE *stream );
ungetc函数 可以把之前读入的字符返回到字符流中,这样下次读可以重新读入。
例子:
#include <stdio.h>
#include <ctype.h>
int read_int()
{
int value = 0;
int ch;
while( ch = getchar() ) != EOF && isdigit(ch) ){
value *= 10;
value += ch - '0';
}
ungetc( ch, stdin );
return value;
}
注:如果一个流允许退回多个字符,那么这些字符再次被读取的时顺序就以退回时的反序进行。退回字符和流的当前位置有关,如果使用fseek,fsetpos,rewind函数改变了流的位置,退回的字符都将被丢弃。与一个流相关联的外部存储并不受ungetc的影响。
15.8 未格式化的行I/O
1、行I/O可以两种方式执行—未格式化的和格式化的。这两种都用于操作字符串。
2、未格式化的I/O(unformatted line I/O)只是简单的读取或者写入字符串。
3、格式化的I/O 需要执行数字和其它变量在内部和外部表示形式的转换。
gets和puts函数家族用于操作字符串而不是当个字符。它们在处理一行 行文本输入输出程序中非常有用。函数原型如下:
char *fgets( char *buffer, int buffer_size, FILE *stream );
char *gets ( char *buffer );
int fputs( char const *buffer, FILE *stream );
int puts ( char const *buffer );
fgets函数: 从指定的stream流读取字符并把它们复制到buffer中,当它读取一个换行符并存储到缓冲区之后就不再读取,如果缓冲区内存储的字符数达到buffer_size-1个时它也会停止读取。在这种情况并不会出现数据丢失的情况,因为下一次调用fgets时将从流的下一字符开始读取。在任何一种情况下,一个NUL字节将被添加到缓冲区所存储的数据的末尾。 如果刚开始就到了文件尾,缓冲区未进行修改,函数返回一个NULL指针;否则返回指向缓冲区的指针。这个返回值通常作为检查是否达到了文件尾。
fputs函数: 传递给fputs缓冲区必须包含一个字符串,它的字符被写入到流中。这个字符串预期以NUL字节结尾,字符串逐字写入:如果没有包括换行符就没有,有多少换行符就输出多少换行符。如果写入有错误,函数返回EOF;否则返回一个非负数。
gets函数和puts函数 几乎和fgets,fputs函数相同。他们之所以存在是为了允许向后兼容。主要区别:gets读入一行时,它并不在缓冲区中存储换行符。当puts写入一个字符串时,它在字符串写入之后再向输出中添加一个换行符。
注:gets函数没有定义缓冲区的长度参数如果一个长输入行读到一个短的缓冲区,多出来的字符将写到缓冲区接下来的内存位置,破环后面不相干的值。
实例:
/*
把标准输入读取到的文本行逐行复制到标准输出。
不管长行是被一次读取还是分段读取,它所产生的结果文件都是相同的。
缓冲区的长度也就是读取的一行文本的最大长度。
fgets无法把字符串读到一个长度小于两个字符的缓冲区,因为一个字符需要为NUL字节保留。
*/
#include <stdio.h>
#define MAX_LINE_LENGTH 1024
void copyline( FILE *input, FILE *outpt )
{
char buffer[MAX_LINE_LENGTH];
while( fgets( buffer, MAX_LINE_LENGTH, input ) != NULL )
fputs( buffer, output );
}
15.9 格式化的行I/O
15.9.1 scanf家族
这些函数原型如下,每个原型的省略号表示一个长度可变长度的指针列表。从输入转换来的值逐个存储到这些指针参数所指向的内存位置。
int fscanf( FILE *stream, char const *format, ... );
int scanf ( char const *format, ... );
int sscanf( char const *string, char const *format, ... );
这些函数都是从输入源读取字符并根据format字符串给出的格式码对他们进行转换。fscanf的输入源就是作为参数给出的流,scanf从标准输入读取,而sscanf则从第一个参数所给出的字符串中读取字符。
当格式化字符串到达末尾或者读取的输入不再匹配格式化字符串所指定的类型时,输入就停止。一般情况下,被转换的输入值的数目作为函数的返回值返回。如果任何输入被转换之前文件就已经到达尾部,也就是说没有输入一个值,函数就返回常量值EOF。
注:到这里就知道为什么scanf参数前面为什么要加一个&,由于前面的值参数传递机制,把一个内存位置作为参数作为参数传递给函数的唯一方法就是传递给一个指向该位置的指针。如果忘记&,那么变量的值会被当作地址传给函数,她被解引用时会停止程序或修改这个变量值的地址。
15.9.2 scanf格式代码
scanf函数家族,format字符串参数包含下列内容:
1、空白字符----忽略输入中的空白字符
2、格式代码----指定函数怎么解释接下来的输入字符
3、其他字符----当任何其他字符出现在格式字符串时,下一个输入字符必须与它匹配。如果匹配,该输入的字符随后就被丢弃。如果不匹配,函数直接返回。
scanf家族的格式代码都以一个百分号开头,后面可以是一个可选的星号,一个可选的宽度,一个可选的限定符,格式代码。星号将转换后的值被丢弃而不是存储。宽度以一个非负的整数给出,用于限定被读取用于转换的输入字符个数;如果没有给出宽度,函数自动连续读入字符,直到遇见输入中下一个空白字符。限定符用于修改有些格式代码的含义,目的是为了指定参数的长度。
scanf格式代码:
代码 | 参数 | 含义 |
---|
c | char * | 读取和存储单个字符。前导的空白符并不跳过。如果给出宽度,就读取这个数目的字符。字符后面不会添加NUL字节。参数必须指向一个足够大的数组。 | i d | int * | 一个可选的有符号数转换,d把输入解释为十进制数。i根据它第一个字符决定值的基数。 | u o x | unsigned * | 一个可选的有符号数转换,但被存储为无符号数。u 值被解释为十进制,o八进制,x十六进制。 | e f g | float * | 期待一个浮点值,小数点非必须。 | s | char * | 读取一串非空白字符。参数指向足够大的数组,当发现空白时输入就停止。字符串后面会自动加上NUL字节。 | [xxx] | char * | 根据给定组合的字符从输入中读取一串字符,参数指向一个足够大的数组。当出现第一个不在给定组合的字符时停止输入。字符串后面会自动加上NUL字节。例:%[abc],给定的组合为a,b,c。%[^a,b,c]表示除了abc以外的所有字符。 | p | void * | 与printf中的%p组合使用 | n | int * | 到目前为止从输入读取的字符数目将放回,不消耗任何输入。 | % | (无) | %丢弃 |
scanf的限定符,使用限定符的结果
格式代码 | h | l | L |
---|
d i n | short | long | | o u x | unsigned short | unsignged long | | e f g | | double | long double |
提示:在整型长度和short相同的机器上,在转换一个short值时,限定符h没有作用。整型比short长时有必要。
实例1:
#include <stdio.h>
#define BUFFER_SIZE 100
void function( FILE *input )
{
int a, b, c, d, e;
char buffer[ BUFFER_SIZE ];
while( fgets( buffer, BUFFER_SIZE, input ) != NULL ){
if( sscanf( buffer, "%d %d %d %d %d", &a, &b, &c, &d, &e ) != NULL ){
fprintf( stderr, "bad input skipped: %s", buffer );
continue;
}
}
}
实例2:
#include <stdio.h>
#include <stdlib.h>
#define DEFAULT_A 1
#define DEFALUT_B 2
void function ( char *buffer )
{
int a, b, c;
if( sscanf( buffer, "%d %d %d", &a, &b, &b) != 3 ){
a = DEFAULT_A;
if( sscanf( buffer, "%d %d", &b, &c)){
b = DEFALUT_B;
if( sscanf( buffer, "%d", &c) != 1){
fprintf( stderr, "bad input: %s", buffer );
exit( EXIT_FAILURE );
}
}
}
}
15.9.3 printf家族
printf函数家族用于创建格式化的输出。它们的原型如下:
int fprintf( FILE *stream, char const *format, ... );
int printf ( char const *format, ... );
int sprintf( char *buffer, char const *format, ... );
提示:使用fprintf可以输出任何输出流。而sprintf把它的结果作为一个NUL结尾的字符串存储到指定的buffer缓冲区而不是写到流中 这就容易造成输出结果溢出缓冲区。这三个函数返回值是 实际打印或存储的字符的数目。
printf函数原型中的format字符串可能包含格式代码,格式代码右移个%开头,后面跟着零个或者多个标志字符,用于修改有些转换的执行方式;一个可选的最小字段宽度;一个可选的精度;一个可选的修改符;转换类型。
printf格式代码:
代码 | 参数 | 含义 |
---|
c | int | 参数被裁减为unsigned char 类型并打印字符 | i d | int | 参数作为一个十进制数打印。给出精度,位数少于精度,前面填0 | u o x X | unsigned int | 参数作为无符号数打印。u-10 , o-8, x-16 | e E | double | 参数根据指数形式打印。例如 6.023000e23是使用代码e。小数点后面的位数由精度确定,缺省值为6 | f | double | 参数按照常规的浮点数打印。小数点后面的位数由精度确定,缺省值为6 | g G | double | 参数以%f或%e的形式打印。如果指数大于-4小于精度字段,则用%f格式,否则用指数格式%e | s | char * | 打印一个字符串 | p | char * | 与scanf中的%p组合使用 | n | int * | 不产生输出,但目前为止所有函数产生的输出字符数目都将被保存在对应的参数中。 | % | (无) | 打印一个 % |
printf格式标志:
标志 | 含义 |
---|
- | 值在字段中是左对齐,缺省的情况下是右对齐 | 0 | | + | 在正数面前加上‘+’ | 空格 | | # | |
printf格式代码修改符:
修改符 | 用于… | 表示的参数是 |
---|
h | d i u o x X | short型整数 | h | n | 一个指向short型整数的指针 | l | d i u o x X | long型整数 | l | n | 指向Long型整数的指针 | L | e E f g G | long double 型 |
printf转换的其他形式:
用于 | #标志 |
---|
o | 保证产生一个值以零开头 | x X | 在非零值前面加上 0x | e E f | 确保结果时钟包含一个小数点,即使没有小数 | g G | 同e E f |
15.10 二进制 I/O
把数据写到文件中时,效率最高的是使用二进制形式写入。二进制输出避免了数值转换为字符串的过程中所涉及的开销和精度损失。
fread,fwrite分别用于读取二进制和写入二进制。它们原型如下:
size_t fread ( void *buffer, size_t size, size_t count, FILE *stream );
size_t fwrite( void *buffer, size_t size, size_t count, FILE *stream );
15.11 刷新和定位函数
fflush函数 ,它迫使一个输出流的缓冲区内的数据立即根据行物理写入,不管它是不是已经写满。原型如下:
int fflush( FILE *stream );
当需要立即把输出缓存区的数据进行物理写入时,应该使用这个函数。 例如:条用fflush可以保证调试信息实时打印出来,而不是保存在缓冲区中直到以后在打印。
ftell函数 和 fseek函数:
在正常情况下,数据以线性方式写入,这意味着在文件中,后面写入的数据的位置实在以前所有写入数据的后面。C同时支持随机访问I/O,也就是以任意的顺序访问文件的不同位置。随机访问是通过读取或写入前定位文件中需要的位置来实现的。ftell函数 和 fseek函数用于执行这项操作。它们的原型如下:
long ftell( FILE *stream );
int fseek( FILE *stream, long offset, int from );
ftell函数返回流当前的位置,也就是下次开始的位置距离文件起始位置的偏移量。在二进制流中,这个偏移量就是当前文件位置距离起始位置之间的字节数。ftell函数的返回值总是可以用于fseek函数,作为一个距离文件的起始位置的偏移量。
fseek函数允许在一个流中进行定位。这个操作将改变下一个开始的位置。
fseek函数的from参数值:
如果from是… | 将定位到… |
---|
SEEK_SET | 从流的起始位置起offset个字节,offset非负值 | SEEK_CUR | 从流的当前位置起offset个字节 | SEEK_END | 从流的尾部位置起offset个字节 |
还有3个额外的函数执行相同的任务,但限制更严格。它们的原型如下:
void rewind ( FILE *stream );
int fgetpos( FILE *stream, fpos_t *postion );
int fsetpos( FILE *stream, fpos_t const *postion );
rewind函数将读写指针设置到指定流的起始位置,同时清楚流的错误标志。fgetpos在这个position位置存储文件的当前位置,fsetpos把这个postion位置设置为文件的当前的位置。
注:使用一个从fgetpos返回的fpost_t类型的值是唯一的安全的用法,把它作为参数传递给fsetpos函数。
15.12 改变缓冲方式
在指定的流被打开还没有执行任何操作时,能用下面这俩函数改变缓冲方式。
void setbuf ( FILE *stream, char *buf );
int setvbuf( FILE *stream, char *buf, int mode, size_t size );
15.13 流错误函数
下面函数用于判断流的状态:
int feof( FILE *stream );
int ferror( FILE *stream );
void clearerr( FILE *stream );
15.14 临时文件
tmpfile函数使用一个文件来临时保存数据,程序结束时,这个文件就被删除。它的函数原型如下:
FILE *tmpfile( void );
这个函数创建了一个文件,这个文件以wb+模式打开,这使它可以二进制和文本数据。
临时文件的名字可以用tmpnam函数创建,它的原型如下:
char *tmpname( char *name );
15.15 文件操纵函数
两个其他函数,操作文件成功返回0,失败返回非零值,它们的原型如下:
int remove( char const *filename );
int rename( char const *oldname, char const *newname );
|