APUE笔记—C语言标准IO库之文件IO
注:本文参考APUE第五章
1.标准IO库与系统调用文件IO区别
- 标准IO库:面向应用开发,为程序员提供一套可移植的API,并且标准IO库处理了很多细节,如缓冲区分配,以优化的方式执行IO库等。
- 系统调用:面向底层的硬件,在Linux中,标准IO库的函数一把都要调用系统调用函数,系统调用时与操作系统相关的。
2.流和FILE对象
- 标准IO库:是围绕流进行的,当打开一个流时,标准IO函数fopen返回一个指向FILE对象的指针,而FILE是一个结构体,包含着管理该流需要的所有信息,其中就包含文件描述符,指向缓冲区的指针,缓冲区的长度,出错标志等。
- 系统调用:是围绕文件描述符,当打开一个文件,即返回一个文件描述符,然后该文件描述符就用于后续的操作。
3.标准输入、标准输出、标准错误
一个进程默认会定义3个流,并且这3个流会自动被进程使用,分别是:标准输入,标准输出,标准错误。
系统调用时的文件描述符分别对应:
#define STDIN_FILENO 0 //标准输入
#define STDOUT_FILENO 1 //标准输出
#define STDERR_FILENO 2 //标准错误
标准IO库中分别对应3个文件指针,struct _IO_FILE 就是FILE结构体
<stdio.h> //标准输入输出文件
struct _IO_FILE *stdin; //标准输入流
struct _IO_FILE *stdout; //标准输出流
struct _IO_FILE *stderr; //标准错误流
4.缓冲
4.1 缓冲的目的
标准IO库提供缓冲的,目的是尽可能减少使用read和write的调用次数。它也对每个IO流自动的进行缓冲管理,避免了应用程序需要考虑缓冲这一点所带来的麻烦。
4.2 缓冲的类型
- 全缓冲:在标准IO缓冲区填满时或者调用fflush函数才会执行实际IO操作。一般是malloc分配来的空间。
- 行缓冲:在输入和输出遇到换行符时标准IO库才会执行IO操作。
- 不带缓冲:标准IO库不对字符进行缓冲储存。例如标准错误stderr通常就是不带缓冲的,这使得错误信息立即显示出来。
4.3 缓冲的特征
- 当且仅当标准输入和标准输出并不指向交互式设备时,他们才是全缓冲。
- 标准错是不带缓冲的。
- 若指向终端设备的流,则是行缓冲,否则是全缓冲。
4.4 更改缓冲类型的函数
#include<stdio.h>
void setbuf(FILE *stream, char *buf);//打开或者关闭缓冲机制
//BUFSIZE的缓冲区(定义在<stdio.h>)
int setvbuf(FILE *stream, char *buf, int mode, size_t size);//设置缓冲的类型
//返回值:成功为0.出错非0
函数 | mode | buf | 缓冲区及长度 | 缓冲类型 |
---|---|---|---|---|
setbuf | 非空 | 长度为BUFSIZE的用户缓冲区buf | 全缓冲或行缓冲 | |
NULL | (无缓冲区) | 不带缓冲 | ||
setvbuf | _IOFBF | 非空 | 长度为size的用户缓冲区buf | 全缓冲 |
NULL | 合适长度的系统缓冲区buf | 全缓冲 | ||
setvbuf | _IOLBF | 非空 | 长度为size的用户缓冲区buf | 行缓冲 |
NULL | 合适长度的系统缓冲区buf | 行缓冲 | ||
setvbuf | _IONBF | (忽略) | (无缓冲区) | 不带缓冲 |
任何时候,都可以强制冲洗一个流。
#include<stdio.h>
int fflush(FILE *stream);//使所有未写的数据都被传送至内核,若fp为NULL,则所有流被冲洗。
//返回值:成功为0,出错EOF
5. 打开流
下列三个函数打开一个标准的IO流
#include <stdio.h>
FILE *fopen(const char *path, const char *mode);
//打开路径名为path的一个指定文件
FILE *fdopen(int fd, const char *mode);
//取一个已有的文件描述符,使其与标准的IO流结合。常用于创建管道和网络通信通道函数返回的描述符。
FILE *freopen(const char *path, const char *mode, FILE *stream);
//在指定流上打开一个文件,若该流打开则先关闭,若流已定向,则清清除该定向。
//用于将一个指定文件打开为预定义的流:标准输入,标准输出,标准错误
//返回值:若成功,返回文件指针,若出错,返回NULL
type参数指定该IO流的读写方式。ISO C规定了15中不同的值
type | 说明 |
---|---|
r或rb | 为读而打开 |
w或wb | 把文件截断至0长,或为写而创建 |
a或ab | 追加;为在文件尾写而打开,或为写而创建 |
r+或r+b或rb+ | 为读和写而打开 |
w+或w+b或wb+ | 把文件截断至0长,或为读和写而打开 |
a+或a+b或ab+ | 在文件尾读和写而打开或创建 |
当读和写类型打开一个文件时,具有下列限制
- 如果中间没有fflush、fseek、fsetpos或rewind,则咋输出后面不能直接跟输入
- 如果中间没有fseek、fsetpos或rewind,或者一个输入操作没有达到文件尾端,则在输入操作之后不能直接跟随输出
调用fclose关闭一个打开的流
#include <stdio.h>
int fclose(FILE *fp);//关闭文件之前,冲洗缓冲中的输出数据
//返回值:成功为0,出错EOF
6. 读和写流
一旦打开了流,则可在3种不同类型的非格式化IO中进行选择,对其进行读写操作。
1. 每次一个字符的IO。如果流时带缓冲的,标准IO函数处理所有缓冲
2. 每次一行的IO。使用fgets和fputs,每行以一个换行符终止。当调用fgets时,应说明能处理的最大长度
3. 直接IO。fread和fwrite函数支持这类型的IO,每次操作读写某些数量的对象,每个对象具有指定的长度。
6.1 输入函数
下面3个函数一次读一个字符。
#include <stdio.h>
int getc(FILE *stream);
int fgetc(FILE *stream);
int getchar(void);
//返回值:成功返回下一个字符,若已到达文件尾端或错误,返回EOF
//注意,不管出错还是到达文件尾端,这三个函数都返回同样的值,为了区分就必须使用ferror或feof。
int feof(FILE *stream); //检测流上的文件结束符
int ferror(FILE *stream);//检测流上的错误标志
void clearerr(FILE *stream);//可以清除上面两个函数检测到的标志:出错标志和文件结束标志
函数getchar等同于getc(stdin)。
getc和fgetc的区别是:getc可被实现为宏,而fgetc不能实现为宏。意味着:
1. getc的参数不应当是具有副作用的表达式(表达式执行后改变表达式的值),因为它可能被计算很多次。
2. 因为fgetc一定是函数,所以可以得到地址。可将其地址作为函数的参数。
3. 调用fgetc所需时间可能长于getc,因为调用函数所需时间长于调用宏。
6.2 输出函数
对应上面所述的输入函数都有一各输出函数
#include <stdio.h>
int putc(int c, FILE *stream);
int fputc(int c, FILE *stream);
int putchar(int c);
//返回值:若成功,返回c,若出错,返回EOF
与输入函数一样,putchar(c)等同于putc(c, stdout),putc可被实现为宏,而fputc不能实现为宏。
7. 每次一行IO
下面给两个函数提供每次输入一行的功能
#include <stdio.h>
char *fgets(char *s, int size, FILE *stream);
char *gets(char *s);
//返回值:若成功,返回buf,若达到文件尾端或出错,返回NULL
这两个函数都指定了缓冲区的地址,读入的行将送入其中。gets从标准输入读,而fgetc从指定的流读。
- 对于fgets,必须指定缓冲的长度n。此函数一直读到下一个换行符为止,但不超过n-1个字符,读入的字符被送入缓冲区,该缓冲区以NULL结尾,如若改行包括最后一个换行符的字符数超过n-1,则fgets只返回一个不完整的行,但是缓冲区总是以NULL字节结尾。对fgetc的下一次调用会继续读改行。
- 对于gets是一个不推荐使用的函数。可能会造成缓冲区溢出,产生不可预料的后果。
fputs和puts提供一次输出一行的功能。
#include <stdio.h>
int fputs(const char *s, FILE *stream);
int puts(const char *s);
//返回值:若成功,返回非负值,若出错,返回EOF
- 函数fputs将一个以NULL字节终止的字符串写到指定的流,尾端的终止符NULL不写出。每次不一定输出换行符
- 函数puts将一个以NULL字节终止的字符串写到标准输出,终止符不写出。但是,puts会将一个换行符写到标准输出。
8. 二进制IO
上述的getc和putc是一次处理一个字节,如果要操作一个结构必须通过循环整个结构,而fputs和fgets一次处理一行,遇到NULL会停止,而在结构中就可能会包含NULL字符。因此,通过二进制IO,就可以实现一次读或者写一个完整的结构。下面的两个函数可以执行二进制IO操作。
#include <stdio.h>
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
//size指定长度,nmemb为欲写的元素个数。
//返回值:读或写的对象数。
//对于读,如果出错或者到达文件尾端,返回数少于obj,这种情况应用ferror或feof判断是哪一种情况。
//对于写,如果返回数少于要求的nobj,则出错。
这些函数常见的有两种用法:
1. 读或写一个二进制数组
2. 读或写一个结构体。
9. 定位流
#include <stdio.h>
int fseek(FILE *stream, long offset, int whence);
//返回值:若成功,返回0,若出错,返回-1
long ftell(FILE *stream);
//返回值:若成功,返回当前文件位置指示,若出错,返回-1
void rewind(FILE *stream);//将一个流设置到文件的起始位置
- 对于二进制文件,其文件位置指示器是从文件起始位置开始度量,并以字节为度量单位。为了定位一个二进制文件,必须制定一个offset,以及解释这种偏移量的方式,whence的值有三种:SEEK_SET(文件起始位置)、SEEK_CUR(文件当前位置)、SEEK_END(文件尾端)。
- 对于文本文件,它们的文件位置不能以简单的字节偏移量来度量。这主要在非Linux系统中,它们可能以不同的格式存放文本文件。
如果需要移植到非Linux系统上运行应用程序应当使用下面的函数。
int fgetpos(FILE *stream, fpos_t *pos);
int fsetpos(FILE *stream, fpos_t *pos);
//返回值:若成功,返回0,若出错,返回非0
fgetpos将文件位置指示器的当前值存入由pos指向的对象中。在以后的调用fsetpos时,可以使用此值将流重新定位至该位置。
10. 获取文件描述符
如果调用dup或fcntl等函数,则需要该函数。
#include<stdio.h>
int fileno(FILE *fp);
//返回值:与该流相关联的文件描述符