13.2 标准I/O
除了可移植之外,标准I/O包低级I/O有两点优势。第一,标准I/O包中包含很多专用的函数,可以方便地处理不同的I/O问题。例如,printf()将各种类型的数据转换成适合终端的字符串输出。第二,对输入和输出进行了缓冲。也就是说,大块地转移信息(通常每次不少于512个字节),而不是每次一个字节进行转移。例如,当程序个文件时,会把一大块数据复制到缓冲区(一块中介存储区)中。这种缓冲大大提高了数据传输率。
程序清单13.1示范了如何使用标准I/O读取统计的字符个数。
程序清单13.1 count.c程序
/*count.c --使用标准I/O*/
#include <stdio.h>
#include <stdlib.h> //ANSI C 的exit()原型
int main(int argc,char *argv[])
{
int ch; //读取时存储每个字符的位置
FILE * fp; //文件指针
long count = 0;
if(argc != 2)
{
printf("Usage: %s filename\n",argv[0]);
exit(1);
}
if((fp=fopen(argv[1],"r")) == NULL)
{
printf("Can't open %s\n",argv[1]);
exit(1);
}
while((ch=getc(fp))!=EOF)
{
putc(ch,stdout); //相当于putchar(ch);
count++;
}
fclose(fp);
printf("File %s has %ld characters\n",argv[1],count);
return 0;
}
13.2.1 检查命令行参数
首先,程序检查argc的值,查看是否有命令行参数。如果没有,程序打印一条用法提示然后退出。字符串argv[0]是该程序的名称。使用argv[0],而不是显示的使用程序名,则在您改变了可执行文件名后,错误消息也会随之自动改变。
exit()函数关闭所有打开的文件并终止程序。通常的约定的是,正常终止的程序传递值0,非正常终止的程序传递非0值。具体地,ANSI C 要求使用值0或宏EXIT_SUCESS来指示程序成功终止,使用宏EXIT_FAILURE指示程序非成功终止。这些宏和exit()原型在stdlib.h头文件中都可以找到。本书遵循通常的约定使用整数退出值。但为了获得最大的可移植性,您应使用宏。
按照ANSI C,在最初调用的main()中使用return和调用exit()的效果相同。所在在main()中我们一直使用的语句:
return 0;
和下面这个语句的作用相同:
exit(0);
但要注意我们所说的是“最初调用”。如果main()在一个递归程序中,exit()仍然会终止程序;但return将控制权移交给递归的前一级,直到最初的一级,此时return才会终止程序。return和exit()的另一个区别在于,即使在除main()之外的函数中调用exit(),它也会终止程序。
13.2.2 fopen( )函数
接下来,程序使用fopen()打开文件。这一函数在stdio.h中声明。它的第一个参数是要打开的文件名;更确切地说,是包含该文件名的字符串的地址。第二个参数是用于指定文件打开模式的一个字符串。C库提供了一些可能的模式。如表13.1.
fopen( )函数的模式字符串
模式字符串 | 意义 |
“r" | 打开一个文本文件,可以读取文件 |
"w" | 打开一个文本文件,可以写入文件,先将文件的长度截为0.如果该文件不存在则先创建之 |
"a" | 打开一个文本文件,可以写入文件,向已有文件的尾部追加内容,如果该文件不存在则先创建之 |
"r+" | 打开一个文本文件,可以进行更新,也即可以读取和写入文件 |
"w+" | 打开一个文本文件,可以进行更新(读取和写入),如果该文件存在则首先将其长度截为0,如果不存在,则先创建之 |
"a+" | 打开一个文本文件,可以进行更新(读取和写入),向已有文件的尾部追加内容,如果该文件不存在则先创建之;可以读取整个文体,但写入时只能追加内容 |
"rb","wb","ab","rb+" "r+b","wb+","w+b","ab+", "a+b" | 与前面的模式相似,只是使用二进制而非文本模式打开文件 |
警告:小心!如果使用任何一种"w"模式打开一个已有的文件,文件内容将被删除,以便程序以一个空文件开始操作!
程序成功打开一个文件以后,fopen()函数返回一个文件指针(file pointer),其他I/O函数用这个指针来指定该文件。文件指针是一种指向FILE的指针;FILE是stdio.h中定义的一种派生类型。指针并不指向实际的文件,而是指向一个关于文件的信息的数据包,其中包含文件I/O使用的缓冲区信息。因为标准库中的I/O函数使用缓冲区,所以它们需要知道缓冲区的位置,还需要知道缓冲区的当前缓冲能力以及所使用的文件。这样这些函数在必要的时候可以再次填充或者清空缓冲区。fp指向的数据包中包含全部这些信息(这个数据包是C结构的一个例子,我们将在14章结构和其他数据形式中讨论该主题)。
如果不能打开文件,fopen()返回空指针(也是在stdio.h中定义的)。如果fp为null,程序将退出。磁盘已满、文件名非法、存取权限不够或者硬件问题等都会导致fopen()函数执行失败。
13.2.3 getc( )函数和putc( )函数
这两个函数的工作方式和函数getchar()与putchar()非常相似,不同之处在于您需要告诉getc()和putc()函数它们要使用的文件。所以,下面的方法从标准输入获得一个字符:
ch = getchar();
但下面的语句表示从指针fp指定的文件中获得一个字符:
ch = getc(fp);
与之类似,以下语句表示将字符ch写入到FILE指针fpout指定的文件中:
putc(ch,fpout);
在putc()函数的参数中,首先是字符,然后是文件指针。
程序清单13.1把stdout作为putc()函数的第二个参数。stdout是在stdio.h中定义的与标准输出相关联的文件指针,所以putc(ch,stdout)与putchar(ch)的作用是一样的。实际上,后者一般是通过前者定义的。类似,getchar()使用作为标准输入的getc()定义。
在13.1的例子中通过使用stdout之类的参数,可以很容易地将这段程序改写为向文件进行输出。
13.2.4 文件结尾
程序怎么才能知道是否已经到达文件结尾了呢?
如果在尝试读入字符时发现已经到达文件结尾,getc()函数会返回一个特殊值EOF。所以C程序只有在读取 超出文件结尾以后 才会发现文件的结尾。
为了避免试图读取空文件带来的问题,应该对文件输入使用入口条件循环(而不是do while)。鉴于getc()函数(以及其他C输入函数)的设计,程序应该在 进入循环体之前尝试进行第一次读取。
//设计范例
int ch; //int 来控制EOF
FILE * fp;
fp=fopen("wacky.txt","r");
ch=getc(fp); //获取初始输入
while(ch!=EOF)
{
putchar(ch); //处理输入
ch=getc(fp); //获取下一个输入
}
这些语句还可以精简为下面的形式:
int ch;
FILE * fp;
fp=fopen("wacky.txt","r");
while((ch=getc(fp))!=EOF)
{
putchar(ch); //处理输入
}
因为输入语句是while判断条件的一部分,所以将在进入循环体之前执行该语句。
这些警告同样适用于其他输入函数,它们也会在遇到文件结尾以后返回一个出错信号(EOF或是NULL指针)。
13.2.5 fclose()函数
fclose(fp)函数关闭由指针fp指定的文件,同时根据需要刷新缓冲区。
更正规的程序也许还要检查是否成功关闭了文件。如果成功关闭,fclose()返回0,否则返回EOF。
if(fclose(fp)!=0)
printf("Error in closing file %s\n",argv[1]);
磁盘已满、磁盘被移走或者出现I/O错误等等都会导致fclose()函数执行失败。
13.2.6 标准指针文件
stdio.h文件把3个文件指针与3个C程序自动打开的标准文件进行了关联,如表13.2所示
标准文件 | 文件指针 | 一般设备 |
标准输入 | stdin | 键盘 |
标准输出 | stdout | 显示器 |
标准错误 | stderr | 显示器 |
这些指针都是FILE指针类型,所以可以用作标准I/O函数的参数,就像示例中的fp那样。