文件访问

UNIX系统程序cat可以说明访问的文件是否已经连接到程序,它把一批命名文件串联后输出到标准输出上
cat可用来在屏幕上打印文件,对于无法通过名字访问文件的程序而言,它还可以用作通用的输入收集器

如:命令行 cat x.c y.c 将在标准输出上打印文件x.c和y.c的内容

对一个访问文件的程序而言,关键的问题在于如何设计命名文件的读取过程,即如何将用户需要使用的文件的外部名同读取数据的语句联系起来
方法其实很简单:
在读写一个文件之前,必须通过库函数fopen()打开该文件

fopen函数原型:FILE* fopen(char* name,char* mode);

fopen()用类似于x.c或y.c这样的外部名与操作系统进行某些必要的连接和通信,并返回一个随后可以用于文件读写操作的指针
该指针称为文件指针,指向一个包含文件信息的结构,这些信息包括:

  • 一个指向缓冲区位置的指针,通过它可以一次读入文件的一大块内容;
  • 一个记录缓冲区中剩余字符数的计数器;
  • 一个指向缓冲区下一个字符的指针;
  • 文件描述符;
  • 描述读/写模式的标志;
  • 描述错误状态的标志等

用户无需关心这些细节的实现,因为stdio.h中已经定义了一个包含这些信息的结构FILE
在程序中只需按照基本格式声明一个文件指针即可:FILE*fp;
上面的fp是一个指向结构FILE的指针,并且fopen函数返回一个指向FILE的指针
注意FILE像int一样是一个类型名而不是结构标记,它是通过typedef定义的
在程序中一种常见的形式是fp=fopen(name,mode);
其中fopen的第一个参数是一个字符串,包含文件名,第二个参数是访问模式,也是一个字符串,用于指定文件的使用方式
允许的模式包括:

  • 读(“r”)、写(“w”)以及追加(“a”),某些系统还区分文本文件和二进制文件,对后者的访问需要追加字符’b’
  • 如果打开一个不存在的文件用于写或追加,该文件将被创建(如果可行的话)
  • 当以写模式打开一个已经存在的文件时,该文件原来的内容将被覆盖,以追加模式打开一个文件,则该文件原来的内容保持不变

读一个不存在的文件将导致错误,其他一些操作也可能导致错误,如试图读取一个无读取权限的文件,如果发生错误fopen将返回NULL

文件输入输出

文件打开后,需要考虑使用哪种方式对文件进行读写
有多种方法可供考虑,其中最为简单的是getc()和putc()

getc函数原型为:int getc(FILE*fp);

getc()从文件中返回下一个字符,它需要知道文件指针,以确定对那个文件执行操作
getc()返回fp指向的输入流中下一个字符,如果到达文件尾或出现错误,该函数返回EOF
相对应的,putc()是一个输出函数

putc函数原型:int putc(int c,FILE*fp);

putc()将字符c写入到fp指向的文件中,并返回写入的字符,如果发生错误则返回EOF
当启动一个c程序时,操作系统环境负责打开三个文件,并将三个文件的指针提供给该程序
这三个文件分别是标准输入、标准输出和标准错误,相应的文件指针常量分别为stdin、stdout、stderr(声明在stdio.h中)
在大多数环境中,stdin指向键盘,stdout和stderr指向默认输出终端(stdin和stdout可以被重定向到其他文件中或管道中)
类似于getchar()和putchar()以及getc()和putc()的实现方式一般是宏而不是函数(不然对每个字符都需要进行一次函数调用的话开销过大)
并且实际上getchar()和putchar()可以通过getc()、putc()、stdin、stdout进行定义

如:#define getchar() getc(stdin)
   #define putchar(c) putc((c),stdout)
例:<stdio.h>的部分典型片段
#define NULL 0
#define EOF (-1)
#define BUFSIZ 1024
#OPEN_MAX 20                 //一次最多可打开的文件数
typedef struct _iobuf{
    int cnt;                 //剩余的字符数
    char*ptr;                //下一个字符的位置
    char*base;               //缓冲区的位置
    int flag;                //文件访问模式
    int fd;                  //文件描述符
}FILE;
extern FILE _iob[OPEN_MAX];
#define stdin (&_iob[0])
#define stdout (&_iob[1])
#define stderr (&_iob[2])
enum _flags{
    _READ=01;                //以读写方式打开文件
    _WRITE=02;               //以写方式打开文件
    _UNBUF=04;               //不对文件进行缓冲
    _EOF=010;                //已到文件的末尾
    _ERR=020;                //该文件发生错误
};
int _fillbuf(FILE*);
int _flushbuf(int,FILE*);
#define feof(p) (((p)->flag&_EOF)!=0)   //p一般为传入的FILE*,当p->flag的第二个位字段为0时(p)->flag&_EOF)=0
#define ferror(p) (((p)->flag&_ERR)!=0) //同feof(p)
#define getc(p) (--(p)->cnt>=0?(unsigned char)*(p)->ptr++:_fillbuf(p))
#define putc(x,p) (--(p)->cnt>=0?*(p)->ptr++=(x):_flushbuf((x),p)
#define getchar() getc(stdin)
#define putchar(x) putc((x),stdout)

宏getc一般先将计数器减1,将指针移到下一个位置,然后返回字符,
如果计数器变为负值,getc就调用函数_fillbuf填充缓冲区,重新初始化结构的内容,并返回一个字符
返回的字符为unsigned long类型,确保所得字符为正值
宏putc的操作与getc非常类似,当缓冲区满时调用_flushbuf
对文件的格式化输入或输出,同样有函数fscanf和fprintf
它们与printf和scanf的区别仅仅在于它们的第一个有名参数是一个指向所要读写文件的指针,第二个有名参数才是格式串
(格式串的转换法则基本相同,区别在于需要指定向哪个文件中进行读写)
函数原型分别为:

int fscanf(FILE* fp,char* format,…);
int fprintf(FILE* fp,char* format,…);

函数fclose()执行和fopen()相反的操作,它断开由fopen函数建立的文件指针和外部之间的连接,并释放文件指针以供其他文件使用

fclose函数原型:int fclose(FILE*fp);

由于大多数操作系统都限制了一个程序可以同时打开的文件数,所以当文件指针不再需要时就应当释放,这是一个好的编程习惯
对输出文件执行fclose()还有另外一个原因:它将把缓冲区中由putc()正在收集的输出写到文件中
当程序正常终止时,程序实际上会自动为每个打开的文件调用fclose()
(如果不需要使用stdin和stdout,可以将它们关闭掉,同时也可以通过库函数freopen()选择重新指定它们)

错误处理——stderr和exit

如果程序因为某种原因而造成其中的一个文件无法访问,相应的诊断信息要在该连接的末尾(fclose())才能打印出来
当输出到stdout时,这种处理方法尚可接受,但如果需要输出到一个文件或通过管道输出到另一个程序时,就难以接受了(屏幕上看不到出错信息)
为处理这种情况,单独设立一个输出流以与stdin和stdout相同的方式分派给程序,即stderr
而即使对标准输出进行重定向,写到stderr的输出通常也能显示在显示器(默认输出终端)上
标准库函数exit()可以终止调用程序的执行(exit()的作用类似于main中的return,可以终止程序的执行,但exit()可以出现在程序的任何位置)
(注意exit并不是关键字)
当该函数被调用时,任何调用该程序的进程都可以获取exit()的参数值,因此可通过另一个将该程序作为子进程的程序来测试该程序是否执行成功
按照惯例。返回值0表示一切正常,而非0返回值通常表示异常情况
exit()可以为每个已打开的输出文件调用fclose(),以将缓冲区中所有输出写到相应的文件中
在主程序main()中,语句return expr等价于exit(expr),但使用exit()有一个额外的优点,它可以从其他函数中以及程序中调用
函数ferror可以在流fp中出现错误时返回一个非0值

ferror函数原型:int ferror(FILE*fp);

尽管输出错误很少出现,但还是存在的(比如磁盘满时),因此成熟产品程序应当检查这种类型的错误
函数feof()与ferror()类似,如果指定的文件指针到达文件末尾,它将返回一个非0值

feof函数原型:int feof(FILE*fp);

例:cat函数,连接多个文件
void filecopy(FILE*,FILE*);
int catmain(int argc,char*argv[])
{
    FILE*fp;
    char*prog=argv[0];                             //记下程序名,供错误处理用
    if(argc==1)filecopy(stdin,stdout);             //如果没有命令行参数,则复制标准输入
    else while(--argc)
    if(!(fp=fopen(*++argv,"r")))
    {   //如果fp为空输出错误信息
        fprintf(stderr,"%s: can't open %s\n",prog,*argv);
        exit(1);
    }
    else
    {
        filecopy(fp,stdout);
        fclose(fp);
    }
    if(ferror(stdout))
    {
        fprintf(stderr,"%s: error writing stdout\n",prog);
        exit(2);
    }
    exit(0);
}
//filecopy函数:将文件ifp复制到文件ofp
void filecopy(FILE*ifp,FILE*ofp)
{
    int c;
    while((c=getc(ifp))!=EOF)putc(c,ofp);
}
该程序通过两种方式发出出错信息
首先将fprintf()产生的诊断信息输出到stderr上,因此诊断信息会显示在屏幕上而不仅仅输出到管道或输出文件中
诊断信息中包含argv[0]中的程序名,因此当该程序和其他程序一起运行时可以识别错误的来源
其次,使用exit(),当该函数被调用时将终止调用程序的执行(exit实际上是访问系统调用)
在上面的小程序中,主要目的是为了说明问题,因此并不太关心程序的退出状态,但对于任何重要的程序而言,都应让程序返回有意义且有用的值

行输入和行输出

标准库提供了一个输入函数fgets()进行行输入

fgets函数原型:char* fgets(char* line,int maxline,FILE* fp);

fgets()可以从fp指向的文件中读取下一个输入行(包括换行符),并将它放在字符数组line中
最多可以读取maxline-1个字符,读取的行将以’\0’结尾保存在数组中
通常情况下,fgets返回line,但如果遇到文件结尾或发生了错误,则返回NULL
输出函数fputs()将一个字符串(不需要包含换行符)写入到一个文件中

fputs函数原型:int fputs(char* line,FILE* fp);

如果发生错误,该函数将返回EOF,否则返回一个非负值
库函数gets()和puts()的功能与fgets()和fputs()相似,但它们是对stdin和stdout操作
值得注意的是,gets()在读取字符串时将删除结尾的换行符’\n’,而puts()函数在写入字符串时将在结尾处添加一个换行符
(在对待换行符的行为上,gets()、puts()和fgets()、fputs()正好相反)

例:标准库中的fgets()和fputs()
char* fgets(char* s,int n,FILE* iop)
{
    int c;
    char*cs=s;
    while((c=getc(iop))!=EOF)if((*cs++=(char)c)=='\n')break;
    *cs='\0';
    return (cs==s&&c==EOF)?nullptr:s;
}
int fputs(char*s,FILE*iop)
{
    char c;
    while((c=*s++))putc(c,iop);
    return ferror(iop)?EOF:1;
}