标准I/O库
标准I/O库以及他的头文件,提供了一个到底层I/O系统调用的一个万能接口.这个库并不是ANSI标准C的一部分,而我们在前面所谈到的系统调用也不是,但是这个库却提供了许多复杂的函数用来处理格式化输出以及描述输入.他同时也会小心的处理设备所要求的缓冲区.
在许多方式上,我们可以用使用低层文件描述符的方式来使用这个库.我们需要打开文件建立访问路径.这会返回一个值,并会作为一个调用其他I/O库函数的参数.这个与低层文件描述符等同的被称之类流(stream),并且是作为一个指向结构的指针,FILE*,来实现的.
当一个程序启动时会自动打开三个文件流.他们是stdin,stdout,stderr.这些是在stdio.h中定义,分别代表标准输入.标准输出和标准错误输出,相对的,他们分别与低层的文件描述符0,1,2相对应.
在下一部分中,我们将会看到下面的一些内容:
fopen, fclose
fread, fwrite
fflush
fseek
fgetc, getc, getchar
fputc, putc, putchar
fgets, gets
printf, fprintf, sprintf
scanf, fscanf, sscanf
fopen
fopen库函数是低层的open系统调用的模拟.我们主要将他用于文件或是终端输入与输出.然而在我们需要显示的控制设备的地方,我们最好是使用低层的系统调用,因为他们可以削除由库所造成的潜在的不良因素,如输入/输出缓冲区.
其语法格式如下:
#include <stdio.h>
FILE *fopen(const char *filename, const char *mode);
fopen打开由filename参数所指定的文件,并建立一个与其相关的流.mode参数指出如何来打开这个文件.他可以是下列字符串中的一个:
"r"或"rb":以只读方式打开
"w"或"wb":以只写方式打开
"a"或"ab":以读方式打开,添加到文件的结尾处
"r+"或"rb+"或"r+b":打开更新(读和写)
"w+"或"wb+"或"w+b":打开更新,将其长度变为零
"a+"或"ab+"或"a+b":打开更新,添加到文件结尾处
b表明这个文件是二进制文件而不是文本文件.
在 这里我们要注意,与MS-DOS不同,Unix和Linux并不会在文本文件与二进制文件之间进行区别.Unix与Linux将所有文件看成是一样的,尤 其是二进制文件.另外要注意的一点就是mode参数必须是一个字符串,而不是一个字符.我们要总是使用"r",而绝不可以是'r'.
如果函数调用成功,fopen会返回一个非空的文件指针.如果失败,他会返回NULL,这是在stdio.h中定义的.
fread
fread 库函数可以用来从一个文件流中读取数据.由stream流中读取的数据将会放在由prt所指定的数据缓冲区中.fread和fwrite都处理数据记录. 这些是由块的尺寸size,读取的次数nitems来指定要传送的记录块的.如果成功则返回值为实际读入到数据缓冲区中的块数,而不是字节数.在文件的结 尾处,也许会返回少于nitems的值,包括零.
其语法格式如下:
#include <stdio.h>
size_t fread(void *ptr, size_t size, size_t nitems, FILE *stream);
与所有要写入到缓冲区中的标准I/O函数一样,程序员要负责分配数据空间以及检查错误.
fwrite
fwrite调用一个与fread相类似的函数接口.他将会从指定的数据区读取数据记录并写入到输出流中.他的返回值为成功写入的记录数.
其语法格式如下:
#include <stdio.h>
size_t fwrite (const void *ptr, size_t size, size_t nitems, FILE *stream);
在这里我们要注意就是我们并不推荐在使用结构数据使用fread与fwrite.一部分的原因就是因为用fwrite写入的文件潜在的存在着在不同的机器间不兼容的问题.
fclose
fclose 库函数关闭指定的文件流,并将所有未写入的数据写入文件中.使用fclose是相当重要的,因为stdio库会缓存数据.如果程序需要确定已经完整的写入 了所有的数据,这时就应调用fclose.然而,当一个程序正常结束时,fclose就自动调用,从而关闭所有仍然打开的文件流.当然,在这样的情况下, 我们就没有机会来检查由fclose报告的错误.与文件描述符所有的限制一样,可用的流数目也是有限制的.实际的限制是FOPEN_MAX,这是在 stdio.h中定义,而且至少为8个.
其语法格式如下:
#include <stdio.h>
int fclose(FILE *stream);
fflush
fflush 库函数可以使得所有未写入文件流中的数据立刻写入文件流中.例如,我们可以使用这个函数来保证在试图读取一个输入之前已经将交互提示发送到了终端.这个函 数对于保证在继续操作之前已经将所有重要的数据写入了磁盘文件.在调试程序时我们有时也可以用这个函数来保证程序正在写入文件而不是在进行空操作.另外我 们要注意的一点就是当我们调用fclose时会隐含地调用flush操作,所以我们在fclose之前并不需要调用fflush.
其语法格式如下:
#include <stdio.h>
int fflush(FILE *stream);
fseek
fseek 函数是与lstat系统调用作用等同的一个函数操作.他会设置在这个文件流中下一次要读或是写的位置.其中offset与whence的含义与用法与我们 在前面所提到的lseek的用法相同.然而lseek返回的是off_t,而fseek则会返回一个整数:如果成功,则返回0,失败则返回-1,并且使用 errno来表明错误.所以这就会更为的标准.
其语法格式如下:
#include <stdio.h>
int fseek(FILE *stream, long int offset, int whence);
fgetc,getc,getchar
fgets函数会从一个文件流中作为一个字符返回下一个字节.当他到达文件结尾处或是有错误发生时,则会返回EOF.我们必须使用ferror或是feof来辨别这两种情况.
其语法格式如下:
#include <stdio.h>
int fgetc(FILE *stream);
int getc(FILE *stream);
int getchar();
getc 函数与fgetc函数作用相同,所不同的只是前者是作为一个宏来实现的,在这种情况下,stream参数必须没有边界影响(side effects)(例如:他不可以影响局部变量或是作为参数传递给函数的变量).同时,我们也不可以使用getc的地址作为一个函数指针.
getchar函数与getc(stdin)的作用相同并且从标准输入读取下一个字符.
fputc,putc,putchar
fputc函数向输出文件流中写入一个字符.他会返回他写入的值,如果失败则为EOF.
其语法格式如下:
#include <stdio.h>
int fputc(int c, FILE *stream);
int putc(int c, FILE *stream);
int putchar(int c);
与fgetc/getc相类似,putc函数的作用与fputc相同,但是也许他会作为一个宏来实现.GNU C编译器就是这样做,而且我们可以在stdio.h头文件中看到他的定义.
putchar 函数与putc(c,stdout)的作用相同,是向标准输出写入一个单独的字符.在这里我们要注意的putchar是将字符作为整数而不是字符进行处 理,这与getchar的返回字符的结果相同.这样就可以允许文件结束标记符(EOF)在超出字符数目边界时使用-1进行标记.
fgets,gets
fgets 函数从输入文件流中读取一串字符.他会将所读到的字符放入由s所指向的位置,直到碰到一个新行,其传输了n-1个字符,或者是遇到文件结尾时返回,而这是 会首先发生的情况.任何遇到的换行符都会传送到接收字符串,并且会添加一个结束字节/0.在任何一次调用中最多只可以传输n-1个字符,因为必须要添加一 个空字节来结束字符从而构成n个字符.
其语法格式如下:
#include <stdio.h>
char *fgets(char *s, int n, FILE *stream);
char *gets(char *s);
如果函数调用成功,则会返回一个指向s的指针.如果这个流到达了文件结尾,他会为流设置EOF标记符并且会返回一个空指针.如果遇到读错误发生,fgets会返回一个空指针并且设置errno来表明错误类型.
gets函数与fgets相类似,所不同的只是前者是从标准输入读取字符串并且会忽略任何换行符.他会在接收到的字符中添加一个结束符.
在这里我们要注意的是gets函数并不会限制所传输的字符数,所以他会超出他们的传输缓冲区.所以,我们要避免使用这个函数并且要使用fgets函数进行替换.所以我们要小心使用这个函数.
格式化输入与输出
有许多的库函数可以按我们所希望的方式产生输出,而如果我们有过一些C语言编程的经验,我们就会对于这些格式感到熟悉.这些函数包括prinf以及其他的一些向文件流中写入数据的函数以及scanf和其他的一些函数从文件流中读取数据的函数.
printf,fprintf,sprintf
printf函数家族可以格式化并输出不同类型的变量参数.在输出流中所代表的每一个函数的工作方式是由format参数来控制的,这个参数包含要打印的普通的字符串和代码,也就是称之为转义字符的部分,这些用来表明要如何和在哪里打印其余的参数.
其语法格式如下:
#include <stdio.h>
int printf(const char *format, ...);
int sprintf(char *s, const char *format, ...);
int fprintf(FILE *stream, const char *format, ...);
printf 函数在标准输出上产生他的输出.fprintf函数在一个指定的流上产生他的输出,而sprintf函数会将他的输出与一个结束空字符写入字符串s,而s 是作为参数来进行传递的.这个字符必须足够的大来包含所有的输出.另外还有一些printf函数家族中其他的函数可以用来以不同的方式来处理不同的参数. 我们可以通过查看printf手册页得到更为详细的内容.
普通的字符在传递到输出后并不会发生改变.转义字符会使得printf取回并格式化传递的其余参数.他们能通常是以%字符开头的.如下面的一些例子:
printf(“Some numbers: %d, %d, and %d/n”, 1, 2, 3);
他在标准输出的结果如下:
Some numbers: 1, 2, and 3
如果我们要打印一个%字符,我们必须使用%%,这样就不会与一个转义字符发生混淆了.
下面是一些最常的转义字符:
%d,%i:以十进制数打印一个整数
%o,%x:以八进制,十六进制数打印
%c:打印一个字符
%s:打印一个字符串
%f:打印一个浮点数(单精度数)
%e:以定点数的格式打印一个双精度数
%g:以普通格式打印一个双精度数
在format 字符串传递给printf函数与转义字符相匹配的参数类型和个数是非常重要的.一个可选的尺寸标识可以用来表明整数参数的类型.这个可以是h,例如,% hd用来表明short int,或是可以是l,例如,%ld用来表明long int.一些编译器可以检查printf的这些参数,但是他们并不是绝对可靠的.如果我们正在使用GNU编译器gcc,我们可以使用-Wformat来做 到这一点.
如下面的例子:
char initial = ‘A’;
char *surname = “Matthew”;
double age = 14.5;
printf(“Hello Miss %c %s, aged %g/n”, initial, surname, age);
这个例子的结果如下:
Hello Miss A Matthew, aged 14.5
如果我们使用域标识,我们就可以更多的控制打印的方式.这些扩展了转义字符从而可以控制输出中的空格.一个常用的用法是可以用来为浮点数的打印指定一个十进制数的空间或是为一个字符串指定一个打印的空间.
域标识是在转义字符的%字符后面以数字的方式来指定的.下面的这个表中包含了一些转义字符的例子以及他们的输出结果.
Format Argument | Output |
%10s “Hello” | Hello|
%-10s “Hello” |Hello |
%10d 1234 | 1234|
%-10d 1234 |1234 |
%010d 1234 |0000001234 |
%10.4f 12.34 | 12.3400|
%*s 10,”Hello” | Hello|
所 有的这些例子以10个字符的宽度进行打印.在这里我们要注意就是在域宽度中负数用来表明打印的内容要左对齐.一个变化的区域宽度可以用一个通配符*来指 定.在这样的情况下,下一个参数用来指定宽度.开头的0用来要打印的内容以0开头的.根据POSIX的说明,printf函数并不会截断一个要打印的域, 而是进行扩展来进行填充.所以,如果我们要打印一个比我们所指定的域长的内容,那么这个域会进行增长.
如下面的表格所示:
Format Argument | Output |
%10s “HelloTherePeeps” |HelloTherePeeps|
printf会返回一个整数,用来表明写入的字符个数.这在sprintf函数中并不包括结束字符null.如果出现错误,则会返回一个负数并且会设置errno.
scanf,fscanf,sscanf
scanf函数家族的工作方式也printf组的工作方式相类似,所不同的只是这些函数是从一个流中读取内容或者是在作为参数传递的指针地址处放置变量值.他们以同样的方式使用格式字符串来控制输入转换,而这些转义字符中的许多都是相同的.
其语法格式如下:
#include <stdio.h>
int scanf(const char *format, ...);
int fscanf(FILE *stream, const char *format, ...);
int sscanf(const char *s, const char *format, ...);
在这里很重要的一点就是用来存放由scanf函数所读入值的变量必须是正确的类型而且必须与格式字符串进行精确的匹配.如果不是这样,我们的内存就会泄漏而我们的程序就可能崩溃.这些并不会出现编译错误,如果我们幸运的话,有可能会得到警告信息.
scanf及其相关的函数的格式字符串包含有普通字符和转义字符,而这些与printf相类似.然而普通字符是用来指定必须在输入中出现的字符.
如下面的一个简单的例子:
int num;
scanf(“Hello %d”, &num);
这 个scanf调用只有当在标准输入中的下五个字符与Hello匹配才会成功.然后,如果下一个字符形成一个可以识别的十进制数,这个数就会被读入而他的值 将会赋给变量num.在格式字符串中的空格是用来忽略输入中转义字符之间的任何空格符(空格,Tab,或是新行).这就意味着如果我们指定下面的输入形式 中的任何一个都会成功并会将1234存入变量num中:
Hello 1234
Hello1234
在通常情况下,当转义开始时,空格符也会被忽略掉.这就意味着格式字符串%d将会一直从输入中读入,跳过任何空格以及新行直到发现一个数字充列.如果没有出现所希望的字符,转义就会失败,scanf函数返回.
如果我们不小心这样就会导致问题,如果在我们的程序中读入整数而输入中并没有数字字符,这样就会导致一个死循环.
其他的一些转义字符如下:
%d:读入一个十进制整数
%o,%x:读入一个八进制,十六进制整数
%f,%e,%g:读入一个浮点数
%c:读入一个字符(并不会跳过空格)
%s:读入一个字符串
%[]:读入一个字符集
%%:读入一个%字符
与printf 相类似,scanf转义字符也有一个宽度域来限制输入数量.一个尺寸标识(h代表shor,而l代表long)一个正接收的参数是否短于或是长于默认的情 况.这就意味着%hd表示short int,而%ld代表long int,%lg代表前面所说的双精度浮点数.
一个标识符如果以*开始则表明所有的内容都会被忽略掉.这就意味着所输入的信息并不会被保存,所以我们也就并不需要一个变量来进行接收.
我们使用%c来从输入中读取一个单一的字符,这并不会跳过初始的空格符.
我 们使用%s来读取一个字符串,但是我们必须小心.他会跳过开头的空格符,但是却会停在字符串中的第一个空格符处.所以我们最好使用他来读取一个单词而不是 通常的字符串.同时没有指定区域宽度标识符,所以他可能读取的字符串的长度并没有限制,所以这个接收字符串必足够的大来存放输入流中最长的字符串.我们最 好使用区域宽度标识,或者是组合使用fgets和sscanf来读入一行输入.这样就可以尽量防止怀有恶意的用户所造成的缓冲区溢出.
我们使用% []标识可以读入由一个字符集合所组成的字符串.格式串%[A-Z]可以读入有大写字母组成的字符串.如果在这个集合中的第一个字符为^,那么则会读入由 不在集合中的字符所组成的字符串.所以如果要读入含有空格但是却在第一个逗号处结束的字符串,我们可以格式串%[^,].
我们可以输入下面的输入行:
Hello, 1234, 5.678, X, string to the end of the line
这个scanf调用会正确的读入四个内容:
char s[256];
int n;
float f;
char c;
scanf(“Hello,%d,%g, %c, %[^/n]”, &n,&f,&c,s);
scanf函数会返回他所成功读取的内容数,如果第一个内容失败则会返回零值.如果与第一个内容匹配之前已经到达输入的结尾,则会返回EOF.如果在文件流上发生读错误,则会设置文件流错误标记,而错误变量将errno将会进行设置来表明错误类型.
在通常的情况下,scanf以及一些相关的函数并不会推荐使用,这是由于下面的三个原因:
传统的原因是因为这些函数的实现存在一些bug
他们的使用并不灵活
他们会使得正是分析的程序难于理解.
我们可以试着使用一些其他的函数,如fread或是fgets.
其他的一些流函数
还有许多其他的stdio的库函数,这些函数或者使用流参数,或者是使用标准的stdin,stdout,stderr参数:
fgetpos:得到在文件流中的当前地址
fsetpos:在文件流中设置当前的地址
ftell:返回一个流中当前文件的偏移量
rewind:在一个流中重新设置文件地址
frepoen:重用一个文件流
setvbuf:为一个流设置缓冲方案
remove:与unlink等价,所不同的只是其参数为一个目录,在这种情况下,他与rmdir的作用相同
这些库函数都在手册页中的第三部分有详细的说明.
我们现在可以使用文件流函数来重新实现一个文件复制的程序,在这里我们要使用库函数.我们来看一下下面的例子程序copy_stdio.c.
这个程序与前面的一个版本的程序相类似,但是现在这里的一个字符一个字符的拷贝是由stdio.h中的函数引用来实现的:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int c;
FILE *in, *out;
in = fopen(“file.in”,”r”);
out = fopen(“file.out”,”w”);
while((c = fgetc(in)) != EOF)
fputc(c,out);
exit(0);
}
如果我们要像前面一样来运行这个程序,我们可以得到下面的输出信息:
$ TIMEFORMAT=”” time copy_stdio
0.29user 0.02system 0:00.35elapsed 87%CPU
...
这 一次,这个程序的运行时间为0.35秒,并不如底层块版本的运行速度快,但是这要比一次拷贝一个字符的版本有了很大的改进.这是因为stdio库使用 FILE结构来维护一个内部的缓冲区,而且只有这个缓冲区满时才会调用底层的系统调用.我们可以使用一行一行的拷贝与块拷贝来实现,从而可以与我们这里运 行的几个版本进行相应的比较.
流错误
要标识一个错误,许多的stdio库函数会返回一个越界的值,例如空指针或者是定值EOF.在这些情况下,这些错误是由外部的变量errno来标识的:
#include <errno.h>
extern int errno;
在这里我们要注意的,许多的函数会改变errno的值.只有当一个函数的调用失败时,他的值才是可用的.我们应在一个函数标识失败后立刻检测errno的值.我们应在使用他之前要将他的值拷贝到另一个变量中,因为一些打印函数,如fprintf也许会修改他的值.
我们也可以通过检测文件流的状态来决定是否发生了错误,或者是已经达到文件结尾.
#include <stdio.h>
int ferror(FILE *stream);
int feof(FILE *stream);
void clearerr(FILE *stream);
ferror函数会为一个流检测错误标识符,如果进行了设置则会返回零值,否则返回非零值.我们可以像下面的样子来使用这个函数:
if(feof(some_stream))
/* We’re at the end */
clearerr函数会清除stream指针所指的文件流的文件结束或是错误标识符.这个函数并没有返回值也没有定义的错误.我们可以使用这个函数来在流上由错误条件进行恢复.这个函数应的一个例子也许就是当发生磁盘满时会将数据重新写入文件流中.
流与文件描述符
每一个文件流都是与底层的文件描述符相对应的.我们可以混合使用底层的输入和输出与高层的文件流操作,但是通常而言这是不明智的,因为缓冲区的影响是不可预知的.
#include <stdio.h>
int fileno(FILE *stream);
FILE *fdopen(int fildes, const char *mode);
我们可以通过调用fileno函数来得知一个文件流正在使用哪一个底层的文件描述符.他会为指定的文件流返回一个文件描述符,如果失败则会返回-1.如果我们需要底层的访问一个打开的流,我们可以使用这个函数,如使用fstat.
我们可以通过调用fdopen函数来在一个已经打开的文件描述符的基础上创建一个新的文件流.实质上,这个函数会为一个已经打开的文件描述符提供一个stdio的缓冲区,这也许会是一个用来进行解释的一个较为简单的方式.
fdopen 函数与fopen的操作方式相类似,所不同的只是他所使用的为一个底层的文件描述符.如果我们需要使用open来创建一个文件,也许是为了更好的权限控 制,但是却希望使用文件流进行写操作时,这个函数就会显得尤为有用.mode参数与fopen函数的参数相同,而且必须与这个文件最初打开时所建立的文件 访问方式相兼容.fdopen会返回一个新的文件流,如果失败则会返回NULL.
文件与目录维护
标准库与系统调用对于文件的创建与维护提供了完全的控制.
chmod
我们可以使用chmod系统调用改变一个文件或是目录的权限.这构成了Shell编程的基本内容.
其语法如下:
#include <sys/stat.h>
int chmod(const char *path, mode_t mode);
由path所指定的文件将会具有由mode所指定的权限.在这里所指定的mode与open系统调用中的相同,是一个所需权限的位或.除非是这个程序被指定了合适的权限,否则只有这个文件的所有者或是超级用户才可以改变他的权限.
chown
超级用户可以使用chown系统调用来改变一个文件的所有者.
其语法如下:
#include <unistd.h>
int chown(const char *path, uid_t owner, gid_t group);
这个调用使用用户ID或是组ID的数值(可以由getuid和getgid调用得到)和一个常量来谁可以来改变文件的所有者.如果设置了合适的权限我们就可以改变一个文件的所用者和所属的组.
unlink,link,symlink
我们可以使用unlink来移除一个文件.
unlink可以为一个文件移除目录实体并减少他的连接数量.如果函数调用成功则返回0,失败则会返回-1.我们必须在所要执行命令的目录中有写和执行的权限,因为文件对于这个函数调用有他自己的目录实体.
其语法如如下:
int unlink(const char *path);
int link(const char *path1, const char *path2);
int symlink(const char *path1, const char *path2);
如 果连接数量达到0而没有进程打开文件,这个文件则会被删除.事实上,一个目录实体总是会被删除,但是这个文件的空间并不会被回收,直到关闭最后一个相关的 进程.rm程序使用这个调用.在通常情况下我们可以使用ln程序来为一个文件创建一个链接.我们可以使用link系统为一个文件有计划的创建链接.
link系统调用为一个已存在的文件path1创建一个新的链接.新的目录实体是由path2来指定的.我们可以用相类似的方式使用symlink来创建一个符号链接.在这里我们要注意的就是一个文件的符号链接不会像硬链接那样阻止一个文件的删除.