输入输出函数
错误报告
ANSI C 函数库的许多函数调用操作系统来完成某些任务,任何时候当操作系统根据一些要求执行任务的时候,都存在失败的风险。标准库在errno.h
中定义了一个外部整型变量errno
用来保存错误代码,这个错误代码可以提示操作失败的准确原因。
perror
函数能够简化向用户报告错误的原因,该函数定义在stdio.h
中,原型:
void perror(char const *message);
如果message
不是NULL
并且指向一个非空字符串,那么函数将打印形式为:
message : error information
注意:
- 良好的编程实践在任何可能出错的地方,都要执行检查,确认其是否成功。
- 只有当库函数执行失败的时候
errno
的值才会被修改,这说明我们不能够依赖于errno
的值判断操作是否成功。
终止运行
exit
函数可以终止程序的执行,它定义在stdlib.h
中,原型:
void exit(int status);
status
参数返回给操作系统,用于提示程序是否正常完成,这个值与main
函数返回的整型状态值相同。
预定义符号EXIT_FAILURE
、EXIT_SUCCESS
分别用于提示失败和成功终止。
ANSI I/O 概念
ANSI C建立了一个标准I/O模型,是一个标准函数包和stdio.h
头文件中的定义,具有一定的可移植性。
流
所有的I/O操作都是一种在程序中移进或移出字节的事务,函数库为I/O操作提供的接口称为流。
标准的I/O提供了三种类型的缓存:
- 全缓存:当填满标准I/O缓存后才进行实际的I/O操作。
- 行缓存:当输入或输出中遇到新行符时标准I/O库执行I/O操作。
- 不带缓存:
stderr
就属于这种。
文本流
文本流主要用于字符。
文本流的一些特性在不同的系统可能有所差异:
- 文本行的最大长度,标准规定至少允许254个字符。
- 文本行的结束方式,MS-DOS系统中约定以回车+换行结尾,UNIX系统只用一个换行符结尾。
二进制流
二进制流主要用于二进制数据,字节不经修改地从二进制流读取或者向二进制流写入。
文件
stdio.h
中声明了FILE
结构体,FILE
是一种数据结构,用于访问一个流,每个流都会有一个FILE
与它关联。- ANSI C 程序运行时系统必须至少默认提供三个流:
stdin
、stdout
、stderr
,他们都指向一个FILE
结构的指针。- 标准输入是缺省情况下输入的来源,一般为键盘设备。
- 标准输出为缺省情况下的输出设置,一般为终端或屏幕。
- 许多系统中,标准错误和标准输出在缺省情况下是相同的。
标准I/O常量
stdio.h
中还定义了许多与输入和输出相关的常量:
EOF
:表示到达文件末尾。FOPEN_MAX
:一个程序至少能够同时打开的文件,注意这个常量中包含了三个标准流。FILENAME_MAX
:提示一个字符数组应该多大以便容纳编译器所支持的最长合法文件名,如果对文件名的长度没有实际的限制,那这个常量的值就是文件名的推荐最大长度。
打开流、关闭流
打开流
操作文件之前必须先打开文件;所谓“打开文件”,就是让程序和文件建立连接的过程。
使用stdio.h
头文件中的 fopen
函数即可打开文件,它的用法为:
FILE *fopen(char *filename, char *mode);
filename
为文件名(包括文件路径),mode
为打开方式,它们都是字符串。
控制读写权限的字符串
打开方式 | 说明 |
---|---|
"r" | 以“只读”方式打开文件。只允许读取,不允许写入。文件必须存在,否则打开失败。 |
"w" | 以“写入”方式打开文件。如果文件不存在,那么创建一个新文件;如果文件存在,那么清空文件内容(相当于删除原文件,再创建一个新文件)。 |
"a" | 以“追加”方式打开文件。如果文件不存在,那么创建一个新文件;如果文件存在,那么将写入的数据追加到文件的末尾(文件原有的内容保留)。 |
"r+" | 以“读写”方式打开文件。既可以读取也可以写入,也就是随意更新文件。文件必须存在,否则打开失败。 |
"w+" | 以“写入/更新”方式打开文件,相当于w 和r+ 叠加的效果。既可以读取也可以写入,也就是随意更新文件。如果文件不存在,那么创建一个新文件;如果文件存在,那么清空文件内容(相当于删除原文件,再创建一个新文件)。 |
"a+" | 以“追加/更新”方式打开文件,相当于a和r+叠加的效果。既可以读取也可以写入,也就是随意更新文件。如果文件不存在,那么创建一个新文件;如果文件存在,那么将写入的数据追加到文件的末尾(文件原有的内容保留)。 |
控制读写方式的字符串
打开方式 | 说明 |
---|---|
"t" | 文本文件。如果不写,默认为"t" 。 |
"b" | 二进制文件。 |
注意:
- 读写权限和读写方式可以组合使用,但是必须将读写方式放在读写权限的中间或者尾部(即不能将读写方式放在读写权限的开头)。例如:
- 将读写方式放在读写权限的末尾:"rb"、"wt"、"ab"、"r+b"、"w+t"、"a+t"。
- 将读写方式放在读写权限的中间:"rb+"、"wt+"、"ab+"。
- 整体来说,文件打开方式由 "r"、"w"、"a"、"t"、"b"、"+" 六个字符拼成,各字符的含义是:
- r(read):读
- w(write):写
- a(append):追加
- t(text):文本文件
- b(banary):二进制文件
- +:读和写
- 各种读写权限对文件是否存在的要求:
- "r" 权限要求文件必须存在,否则会报错。
- "w"、"a"权限如果文件不存在,则会创建文件。
- 应该始终检查
fopen
函数的返回值,如果函数失败,它会返回一个NULL
的值,如果程序不检查错误,这个NULL
指针就会传给后续的I/O函数,他们将队这个指针执行间接访问,并最终失败。
freopen
freopen
函数用于打开或重新打开一个特定的文件流,原型:
FILE * freopen(const char *filename, const char *mode,FILE *stream);
filename
:要打开的文件名;mode
:文件打开的模式,和fopen
中的模式(r/w
)相同。stream
:文件指针,通常使用标准流文件(stdin/stdout/stderr
)
这个函数首先试图关闭这个流,然后用指定的文件和模式重新打开这个流。如果打开失败,函数就返回NULL
值,如果打开成功,函数返回它的第3个参数值。
关闭流
文件一旦使用完毕,应该用 fclose()
函数把文件关闭,以释放相关资源,避免数据丢失。fclose()
的用法为:
int fclose(FILE *fp);
文件正常关闭时,fclose()
的返回值为0,如果返回非零值则表示有错误发生。
字符I/O
字符输入
int fgetc(FILE *stream);
int getc(FILE *stream);
int getchar(void);
- 需要操作的流作为参数传递给
fgetc
、getc
,但getchar
始终从标准输入读取。 - 每个函数从流中读取下一个字符,并把它作为参数的返回值返回。
- 如果流中不存在更多的字符,函数就返回常量值
EOF
。
字符输出
int fputc(int character,FILE *stream);
int putc(int character,FILE *stream);
int putchar(int character);
- 第一个参数是要被打印的字符,在打印之前,函数把这个整型参数剪裁为一个无符号字符型值。
- 函数失败将返回
EOF
。
注意:
putchar('abc');
只打印一个字符,至于是哪个,不同的编译器可能不同。
字符I/O宏
fgetc
,fputc
是函数,getc
,getchar
,putc
,putchar
都是#define
指令定义的宏。- 宏定义执行时间效率高,而函数在程序的长度方面更胜一筹。
撤销字符I/O
ungetc
函数把先前读入的字符返回流中,这样它可以在以后被重新读入。
int ungetc(int char, FILE *stream)
char
:这是要被推入的字符。该字符以其对应的int
值进行传递。stream
:这是指向FILE
对象的指针,该FILE
对象标识了输入流。- 如果成功,则返回被推入的字符,否则返回
EOF
,且流 stream 保持不变。
未格式化的行I/O
行I/O可以用两种方式执行:未格式化、格式化,这两种形式用于操作字符串。
区别在于未格式化的I/O简单读取或写入字符串,而格式化的I/O则执行数字和其它变量的内部和外部表示形式之间的转换。
char* fets(char* buffer,int buffer_size,FILE* stream);
char* gets(char* buffer);
int fputs(char const *buffer,FILE *stream);
int puts(char const *buffer);
fgets
fgets
从指定的stream
读取字符并把他们复制到buffer
中。fgets
读取一个换行符并存储到缓冲区之后就不再读取。- 如果缓冲区存储的字符数达到
buffer_size-1
个时它停止读取,在这种情况下,并不会初出现数据丢失的情况,因为下一次调用fgets
将从流的下一个字符开始读取。 - 在任何情况下,一个
NULL
字节将被添加到缓冲区所存储数据的末尾,使之成为一个字符串。 fgets
无法把字符串读入到一个长度小于两个字符的缓冲区,因为其中一个字符需要为NULL
保留。- 如果在任何字符读取前就到达了文件末尾,缓冲区就未进行修改,
fgets
返回一个NULL
指针,否则fgets
返回第一个参,这个返回值通常只用于检查是否到达了文件末尾。
fputs
fputs
缓冲区必须包含一个字符串,它的字符被写入到流中,这个字符串预期以NULL
字节结尾,所以这个函数没有一个缓冲区长度参数。- 这个字符串是逐字写入的,包含几个换行符就会写入几个换行符。
- 如果
fputs
写入错误,将会返回常量值EOF
,否则它将返回一个非负值。
gets
gets
没有缓冲区长度参数,因此无法判断缓冲区的长度,如果一个长输入行读到一个短的缓冲区,多出来的字节将被写入到缓冲区后面的内存位置,这将破坏其他的变量。
gets
只适用于玩具程序,因为无论声明多大的缓冲区,都存在输入行的缓冲区比声明的缓冲区大的风险。
格式化的行I/O
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 格式化代码
%[*][宽度][限定符][格式符]
[*]
- 星号表示将使转换后的值被丢弃而不是进行存储。
宽度
- 宽度以一个非负的整数给出,它限制将被读取用于转换的输入字符的个数。
- 如果未给出宽度,函数就连续读入字符直到遇见输入中的下一个空白字符。
[限定符]
限定符用于修改有些格式代码的含义:
格式符 | h | l | L |
---|---|---|---|
d,i,n | short | long | |
o,u,x | unsigned short | unsigned long | |
e,f,g | double | long double |
[格式符]
格式符用于指定字符如何被解释:
代码 | 参数 | 含义 |
---|---|---|
c | char * | 读取和存储单个字符。前导空白字符并不跳过。如果给出宽度,就读取和存储这个数目的字符。字符后面不会添加一个NULL字节。 参数必须指向一个足够大的字符数组。 |
i,d | int * | 一个可选的有符号整数被转换。d把输入解释为十进制数;i根据它的第一个字符决定值的基数,就像整型字面值常量的表示形式一样。 |
u,o,x,X | unsigned * | 一个可选的有符号整数被转换,但它按照无符号数存储。如果使用u,值被解释为十进制数;如果使用o,值被解释为八进制数; 如果使用x,值被解释为十六进制数。X和x同义。 |
e,E,f,g,G | float * | 期待一个浮点值。 它的形式必须像一个浮点型字面常量,但小数点并非必须。 |
s | char * | 读取一串非空白字符。参数必须指向一个足够大的字符数组。当发现空白时输入就停止,字符串后面会自动加上NULL终止符。 |
[xxx] | char * | 根据给定组合的字符从输入中读取一串字符。 参数必须指向一个足够大的字符数组。当遇到第一个不在给定组合中出现的字符时,输入就停止。字符串后面会自动加上NULL终止符。代码%[abc]表示字符组合包括a、b和c。 如果列表以一个^字符开头,表示字符组合是所列字符的补集 ,所以%[^abc] 表示字符组合为a、b、c之外的所有字符。贪婪性右方括号也可以出现在字符列表中,但它必须是列表的第一个字符。至于横杠是否用于指定某个范围的字符(例如%[a-z]),则因编译器而异。 |
p | void * | 输入预期为一串字符,诸如那些由printf函数的%p格式代码所产生的输出。它的转换方式因编译器而异,但转换结果将和按照上面描述的进行打印所产生的字符的值是相同的。 |
n | int * | 到目前为止通过这个scanf函数的调用从输入读取的字符数被返回。%n转换的字符并不计算在scanf函数的返回值之内。它本身并不消耗任何输入。 |
% | (无) | 含义 %%这个代码与输入中的一个%相匹配,该%符号被丢弃 |
printf 家族
printf
家族函数原型:
int printf(char const *format,...);
int fprintf(FILE *stream,char const *format,...);
int sprintf(char *buffer,char const *format,...);
printf
函数和scanf
一样,无法验证一个值是否具有格式码所表示的正确类型。所以保证他们相互匹配是程序员的责任。
printf 格式化代码
%[对齐][占位符(填充符号)][最小占位宽度][.精度][限定符][格式符]
[对齐]
-
实现左对齐,默认是右对齐。
[占位符(填充符号)]
+
正负号占位符,当用于一个格式化某个有符号值代码时,如果值非负,正号标志就会给它加上一个正号。如果该值为负,就像往常一样显示一个负号。在缺省情况(不填)下,正号并不会显示。- 空格占位符,只用于转换有符号值的代码。当值非负时,这个标志把一个空格添加到它开始的位置。注意这个标志和正号标志是相互排斥的,如果两个同时给出,空格标志便被忽略。
#
数字占位符:o
: 保证产生的值以一个0
开头。x,X
:在非零值前面加0x
前缀(%X
则为0X
)。
e,E,f
:确保结果始终包含一个小数点,即使它后面没有数字。
g,G
:和上面的e,E和f代码相同。另外,缀尾的0并不从小数中去除。
0
零占位符,当有指定参数时,无数字的参数将补上0。默认是关闭此旗标,所以一般会打印出空白字符。%
百分比占位符,
[最小占位宽度]
如其含义,当宽度超过这个范围后,这个限制无效, 当宽度少于最小占位宽度,即用上面的填充字符来填充空下的部分。
[.精度]
- 精度以一个句点开头,后面跟一个可选的十进制数。如果未给出整数,精度值为零。
- 如未给出 [.精度] 这项 ,默认6位小数位。
[限定符] (h/l/L)
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
型值。
代码 | 参数 | 含义 |
---|---|---|
c | int | 参数被裁剪为unsigned char类型并作为字符进行打印。 |
i,d | int | 参数作为一个十进制整数打印。如果给出了最小占位宽度而且值的位数小于最小占位宽度位数,前面就用0填充。 |
u,o,x,X | unsigned int | 参数作为一个无符号值打印,u使用十进制,o使用八进制,x或X使用十六进制,两者的区别是x约定使用abcdef,而X约定使用ABCDEF。 |
e,E | double | 参数根据指数形式打印。例如,6.023000e23是使用代码e,6.023000E23是使用代码E。小数点后面的位数由精度决定,缺省值是6。 |
f | double | 参数按照的常规浮点格式打印。精度字段决定小数点后面的位数,缺省值是6。 |
g,G | double | 参数以%f或%e(如G则%E)的格式打印,取决于它的值。如果指数大于等于-4但小于精度字段就使用%f格式,否则使用指数格式。 |
s | char * | 打印一个字符串。精度字段可以限定该字符串最大的长度。 |
p | void * | 指针值被转换为一串因编译器而异的可打印字符。这个代码主要是和scanf中的%p代码组合使用。 |
n | int * | 这个代码是独特的,因为它并不产生任何输出。相反,到目前为止函数已经产生的输出字符数目将被保存到对应的参数中。 |
% | (无) | 打印一个%字符。 |
二进制I/O
- 把数据写入到文件中最高效的方式使用二进制方式写入。
- 二进制输出方式避免了在数值转换为字符串过程中所涉及的开销和精度损失。
- 二进制数据不能够肉眼阅读,所以这个技巧一般只有当数据被另一个程序顺序读取时才使用。
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);
buffer
:指向保存数据的内存位置的指针。size
:缓冲区每个元素的字节数。count
:读取或写入的元素数。stream
:数据读取或写入的流。- 函数的返回值是实际读取或写入的元素数目,注意不是字节数目。如果输入过程中遇到了文件末尾或者输出过程中出现了错误,这个数字可能比请求的元素数目少。
刷新和定位函数
fflush
int fflush(FILE *stream);
当需要立即把输出缓冲区的数据进行物理写入时,应该使用这个函数。例如调用fflush
函数保证调试信息实际打印出来,而不是保存在缓冲区中直到以后才打印。
定位函数
在正常情况下,数据以线性的方式写入,这意味着后面写入的数据在文件中的位置是在以前所有写入数据的后面。C同时支持随机访问I/O,也就是以任意顺序访问文件的不同位置。随机访问是通过在读取或写入前,先定位到文件中需要的位置来实现的。
long ftell(FILE *stream);
int fseek(FILE *steam,long offset,int from);
ftell
ftell
函数返回流的当前位置。即:下一个读取或写入将要开始的位置距离文件起始位置的偏移量。该函数允许保存一个文件的当前位置。
- 在二进制流中,这个值就是当前位置距离文件起始位置之间的字节数。
- 在文本流中,这个值表示一个位置,但它并不一定准确地表示当前位置和文件起始位置之间的字符数,因为有些系统将对行末字符进行翻译转换。但是,
ftell
函数返回的值总是可以用于fsee
k函数中,作为一个距离文件起始位置的偏移量。
fseek
fseek
函数允许在一个流中定位。这个函数将改变下一个读取或写入操作的位置。它的第1个参数是需要改变的流。它的第2和第3个参数标识文件中需要定位的位置。
from是... | 定位到... |
---|---|
SEEK_SET | 从流的起始位置起offset个字节,offset必须是一个非负值 |
SEEK_CUR | 从流的当前位置起offset个字节,offset的值可正可负 |
SEEK_END | 从流的末尾位置起offset个字节,offset的值可正可负 |
注意:
- 试图定位到一个文件的起始位置之前是一个错误。定位到文件尾并进行写入将扩展这个文件。定位到文件尾之后并进行读取将导致返回一条“到达文件尾”的信息。
- 在二进制流中,从
SEEK_END
进行定位可能不被支持,所以应该避免。 - 在文本流中,如果
from
是SEEK_CUR
或SEEK_END
,offset
必须是零。如果from
是SEEK_SET
,offset
必须是一个从同一个流中以前调用ftell
所返回的值。 - 用fseek改变一个流的位置会带来三个副作用:
- 首先,行末指示字符被清除。
- 其次,如果在
fseek
之前使用ungetc
把一个字符返回到流中,那么这个被退回的字符会被丢弃,因为在定位操作以后,它不再是“下一个字符”。 - 最后,定位允许你从写入模式切换到读取模式,或者回到打开的流以便更新。
限制更严的定位函数
void rewind(FILE *stream);
int fgetpos(FILE *stream,fpos_t *position);
int fsetpos(FILE *stream,fpos_t const *position);
rewind
函数将读/写指针设置回指定流的起始位置,它同时清除流的错误提示标志。fgetpos
、fsetpos
函数分别是ftell
、fseek
函数的替代方案。fgetpos
、fsetpos
函数接受一个指向fpos_t
的指针参数。fgetpos
在这个位置存储文件的当前位置,fsetpos
函数把文件位置设置为存储在这个位置的值。fpos_t
表示一个文件位置的方式并不是由标准定义的。它可能是是文件中的一个字节的偏移量,也可能不是。使用一个从fgetpos
函数返回的fpos_t
类型的值唯一安全的方法是把它作为参数传递给后续的fsetpos
函数。
改变缓冲方式
下面两个函数可以用于对缓冲方式进行修改:
void setbuf(FILE *stream,char *buf);
int setvbuf(FILE *stream,char *buf,int mode,size_t size);
setbuf
setbuf
设置另一个数组,用于对流进行缓冲,这个数组的字符长度必须为BUFSIZ
(在stdio.h
中定义)。- 为一个流自行指定缓冲区可以防止I/O函数库为它动态分配一个缓冲区。
- 如果使用
NULL
参数调用setbuf
,setbuf
函数将关闭流的所有缓冲方式。 - 为缓冲区使用一个自动数组是十分危险的,如果在流关闭之前,程序的执行流离开了数组声明所在的代码块,流会继续使用这块内存,但是此时可能这块内存已经被其它函数使用。
setvbuf
mode
用于指定缓冲的类型。_IOFBF
:指定一个完全缓冲的流。_IONBF
:指定一个不缓冲的流。_IOLBF
:指定一个行缓冲的流。
buf
,size
用于指定所使用的缓冲区,如果buf
是NULL
,那么size
必须是0。- 一般而言,最好使用一个长度为
BUFSIZ
的字符数组作为缓冲区,或者指定其长度为BUFSIZ
的整数倍,这是因为在MS-DOS机器中,缓冲区的大小如果和磁盘簇的大小相匹配,可能效率会高一些。
流错误函数
下面这些函数用于判断流的状态:
int feof(FILE *stream);
int ferror(FILE *stream);
void clearerr(FILE *stream);
- 如果流的当前位置处于文件的末尾,
feof
函数返回真。这个状态可以通过对流执行fseek、rewind、fsetpos
函数来清除。 ferror
函数报告流的错误状态,如果出现任何读/写错误函数就会返回真。clearerr
函数对指定流的错误标志进行重置。
临时文件
偶尔,为了方便,会使用一个文件来保存临时数据,当程序结束的时候,这个文件便被删除,因为它包含的数据不再有用。
FILE *tmpfile(void);
- 函数创建一个文件,当文件被关闭或者程序终止的时候,这个文件自动删除。
- 创建的文件将按照
wb+
模式打开,这使得它可用于二进制文件和文本数据。
临时文件的名字可以用tmpnam
函数创建:
char *tmpnam(cahr *name);
- 如果函数的参数为
NULL
,那么这个函数将返回一个指向静态数组的指针,该数组包含了被创建的文件名。 - 被创建的文件名保证不会与已经存在的文件同名。只要调用次数不超过
TMP_MAX
次,tmpnam
函数每次都能产生一个新的不同的名字。
文件操纵函数
删除指定文件
int remove(char const *filename);
- 如果
remove
调用时文件处于打开状态,其结果取决于编译器。 - 成功返回0,失败返回非0值。
重命名文件
int rename(char const *oldname,char const *newname);
- 如果有一个名
newname
的文件存在,其结果取决于编译器。 - 成功返回0,失败返回非0值,如果函数失败,文件仍然可以使用原来的名字进行访问。