为了提高可移植性,将通用IO接口经过再封装就形成了标准IO,标准IO不仅适用于Unix环境,也兼容非Unix环境,这也是为什么说我们应该尽可能的使用标准IO,通用IO通过文件描述符fd来与文件交互,为了以示区分,标准IO定义了流的概念作为与文件交互的入口
文件流
文件流是一个FILE*类型的变量,FILE是C标准定义的一个结构体,内部维护了进程对于文件的各种信息,其中就包括了文件描述符,文件流对于标准IO非常重要,几乎所有的标准IO接口都需要使用到文件流,就如同几乎所有的通用IO需要用到文件描述符一样。
标准流
标准流是比较特殊的文件流,分别是标准输出、标准输入和标准错误,这三个文件流在程序启动时就会默认打开,这也是为什么我们在包含头文件之后可以直接进行scanf和printf的原因(回想一下,你使用scanf和printf时几乎不需要接触流这个概念吧)
标准中分别用stdin、stdout、stderr三个宏常量来表示标准流
缓冲区
标准IO相比于通用IO最显著的特点就是缓冲区,这里的缓冲区是用户缓冲区,缓冲机制使得标准IO真实读写磁盘的次数大大减少从而大大提高了效率
缓冲区策略
- 页缓冲:缓冲区中的数据超过一页大小(4096bytes)时进行一次落盘
- 行缓冲:遇到换行符进行一次落盘(超过一页也会落盘)
- 不缓冲:有数据就落盘
默认情况下 磁盘文件采用页缓冲;标准输入输出文件采用行缓冲;标准错误文件不缓冲
可以通过setbuf和setvbuf显式设置对应文件流的缓冲策略
void setbuf(FILE* fp,char* buf);
int setvbuf(FILE* fp,char* buf,int mode,size_t size); //return no-zero if error
setbuf可以决定是否采用缓冲策略
buf为空时不缓冲,否则采用行缓冲或页缓冲(根据文件特点),buf若非空长度必须指向长度为BUFSIZE的缓冲区(C标准提供的常量)
setvbuf可以更精确设置缓冲策略
通过buf和size的配合,可以灵活的设置大小为size的缓冲区,mode的取值决定缓冲策略(如果buf为空将由系统自动分配用户缓冲区)
mode取值
- _IOFBF 页缓冲
- _IOLBF 行缓冲
- _IONBF 不缓冲
强制刷盘
int fllush(FILE* fp);
//return no-zero if error
调用fllush可以暂时无视缓冲区策略,直接对缓冲区中的数据进行刷盘操作
文件流操作
打开流
FILE* fopen(const char* pathname,const char* type);
FILE* freopen(const char* pathname,const char* type,FILE* fp);
FILE* fdopen(int fd,const char* type);
//return NULL if error
freopen可以复用FILE*变量,在一个已经打开的文件流上再次打开一个文件流,原先打开的文件流会先关闭
由于fopen一般只用于打开普通文件,对于一些只能用open打开的文件可以先获取其文件描述符,再通过fdopen将文件描述符与一个流结合,后续以同一的文件流进行读写
打开模式
b表示二进制模式打开
关闭流
int fclose(FILE* fp);
//return no-zero if error
读写流
字符读写
int getc(FILE* fp);
int fgetc(FILE* fp);
int getchar();
//return ASCII if succeed,or EOF(-1)
int putc(int c,FILE* fp);
int fputc(int c,DILE* fp);
int putchar(int c);
//return ASCII if succeed,or EOF
getc和fgetc等价
getchar用于从标准输入流读取
putc和fputc等价
putchar用于打印至标准输出流
行读写
char* fgets(char* buf,int n,FILE* fp);
//return buf if succeed,or NULL
int fputs(const char* str,FILE* fp);
int puts(const char* str);
//return no-zero if succeed ,or NULL
对于 fgets,必须指定缓冲的长度n。它一直读到下一个换行符为止,但是不超过 n-1个字符,读入的字符被送入缓冲区。该缓冲区以**\0结尾**。如若该行包括最后一个换行符的字符数超过 n-1,则 fgets 只返回一个不完整的行,但是,缓冲区总是以 -\0结尾。对 fgets的下一次调用会继续读该行
eg:
#include <stdio.h>
int main()
{
char buf[4]={0};
while(1){
fgets(buf,4,stdin);
puts(buf); //这里使用puts的自动换行功能可以更明显看出fgets对于一次输入分2次完成
}
return 0;
}
多用fputs而非puts
puts直接在标准输出上显示内容,它会自动添加换行符,但是C语言中大多数输出没有这种特性,fputs不会自动添加换行符,更有C风格
结束与错误
int ferror(FILE* fp);
int feof(FILE* fp);
void clearerr(FILE* fp);
如果文件出错ferror返回非0值;如果文件读完则feof返回非0值
FILE结构中维护了出错标志和结束标志,当我们使用ferror或feof判断返回EOF的原因并进行问题处理后,一般希望将这2位归置,可以通过clearerr(FILE fp)*方法
二进制读写
size_t fread(void* ptr,size_t size,size_t n,FILE* fp);
size_t fwrite(const void* ptr,size_t size,size_t n,FILE* fp);
//返回实际读写的个数(单位为size)
非二进制读写都规定了\0作为结束符,二进制读写则没有规定结束符,它严格地按照指定读取个数(n)来读取数据,n的单位不是字节,而是第二个参数size
eg1:fwrite和fputs
int main()
{
char buf[BUFSIZ]="123\0 456";
fputs((const char*)buf,stdout); //只会打印123
puts("");
fwrite((const void*)buf,1,10,stdout); //打印123\0 456
return 0;
}
eg2:二进制读写结构
typedef struct student{
int id;
char name[16];
}student;
int main()
{
student s[3]={{1,"zhangsan "},{2,"lisi "},{3,"wangwu "}};
fwrite((const void*)s,sizeof(student),3,stdout);
return 0;
}
定位流
long ftell(FILE* fp);
int fseek(FILE* fp,long offset,int whence);
void rewind(FILE* fp);//回到起始位置
格式读写
int printf(const char* format,...);
int fprintf(FILE* fp,const char* format,...);
int snprintf(char* buf,size_t n,const char* format,...);//格式化字符串
//返回输出字符数,出错则为负值
int scanf(const char* format,...);
int fscanf(FILE* fp,const char* format,...);
int sscanf(const char* buf,const char* format,...);
//返回输入字符数,出错则为负值
格式化写占位符
格式化读占位符
内存流
所谓内存流就是不在磁盘上开辟空间,而是在内存上开辟空间存放数据,这些数据都是临时性的,因为内存没有持久化的功能,fmemopen使得用户可以像读写文件一样读写内存
FILE* fmemopen(void* buf,size_t size,const char* type);
内存流的打开模式稍显特殊。
-
以追加写方式打开内存流时,当前文件位置设为缓冲区中的第一个\0字节。如果缓冲区中不存在\0字节,则当前位置就设为缓冲区结尾的后一个字节。因此内存流并不适合存储二进制数据(二进制数据在数据尾端之前就可能包含多个 null 字节)。
-
如果buf参数是空指针,打开流进行读或者写都没有任何意义。因为在这种情况下缓冲区是通过 fmemopen进行分配的,没有办法找到缓冲区的地址,只写方式打开流意味着无法读取已写入的数据,同样,以读方式打开流意味着只能读取那些我们无法写入的缓冲区中的数据。