- 介绍
- fopen
- fclose
- fgetc\fputc
- fgets\fputs
- fread\fwrite
介绍
IO是一切实现的基础
stdio
:标准io
sysio
:系统调用io(文件io)
- 关系:标准io是用系统调用io实现的
- 使用原则:能用标准io就用标准io(移植性好、可以加速)
标准IO:
FILE
类型贯穿始终
fopen();
fclose();fgetc();
fputc();
fgets();
fputs();
fread();
fwrite();printf();
scanf();fseek();
ftell();
rewind();fflush();getline();临时文件:
tmpnam();
tmpfile();
man手册:1章基本命令;2章系统调用;3章标准io;7章机制(epoll/tcp/socket) man 7 epoll
fopen
##include<stdio.h>FILE *fopen(const char *pathname, const char *mode);
FILE *fopen(int fd, const char *mode);
FILE *fopen(const char *pathname, const char *mode, FILE *stream);
目前只介绍第一个原型
- 返回值:成功返回
FILE *
,失败返回errno
perror("提示信息")
strerror(errno)
- 参数: 文件路径
pathname
; 打开方式mode
示例代码:
##include<stdio.h>
##include<stdlib.h>
##include<errno.h>
##include<string.h>int main()
{FILE *fp;fp = fopen("tmp", "r");if(fp == NULL){//#include<errno.h>fprintf(stderr, "fopen() failed! errno = %d\n", errno);perror("fopen()");//#include<string.h>fprintf(stderr, "fopen()%s\n:", strerror(errno));exit(1);}puts("OK");flcose();exit(0);
}
运行结果:
关于 mode
的要点:
-
权限描述:
r 只读;文件指针在开头;无不创建,报错 r+ 读并写;文件指针在开头;无不创建,报错 w 写;文件指针在开头;有则清空,无则创建 w+ 读并写;文件指针在开头;有则清空,无则创建 a 读并写;文件指针末尾的下一个;有则添加,无则创建 a+ 读并写;文件指针看情况;有则添加,无则创建 -
mode is a string begining with one of above sequences 所以
fopen("tmp", "readwrite")
不会报错,而是会被识别为fopen("tmp", "r")
-
是否加
b
:表示二进制:在windows环境下,stream分为”文本stream”和”二进制stream”,但是在linux环境下统一为stream,所以可以不添加 -
关于
errno
errno
在一开始是一个 全局 的整型变量,不同的值对应不同的错误。问题在于共用,所以如果不立刻打印就会被其他进程占据。
现在的
errno
是一个私有的宏,映射到私有空间,不会产生数据冲突。//验证代码 #include<errno.h> errno;
终端执行:
gcc -E errno.c
其中-E
表示预处理运行结果:
-
mode
手册内容 -
fopen
创建文件时,被创建文件的权限linux中,默认文件创建权限为
0666
(0表示八进制),文件夹创建权限为0777
系统中有一个掩码
umask
,一般为0002
,文件创建权限为0666 & ~umask
【或简单理解为0666 - umask
】tmp权限为0664 = 0666-0002
fclose
fopen
的返回的指针存放在哪里:栈、静态区、堆
- 如果存放在栈中,那么
fopen
执行结束后栈的空间会被销毁,那么必然拿不到指针,所以不会在栈中 - 如果存放在静态区中
static FILE*
,那么由于静态区的变量只会被创建一次,那么打开多个文件时,就会使用同一个FILE *
,这也不合理,所以不会放在静态区中 - 放在堆中,可以解决上述问题,但是需要手动
malloc
和free
,malloc
在fopen
中,那么free
必然是在fclose
中
*fclose
一般不会失败,所以不检查返回值*
是资源一定有上限, fopen
最大能打开的文件数量也有限
测试代码:
##include<stdio.h>
##include<stdlib.h>
##include<errno.h>int main()
{FILE *fp = NULL;int cnt = 0;while(1){ fp = fopen("tmp","w");if(fp == NULL){perror("fopen()");break;}cnt++;} printf("cnt = %d\n", cnt);exit(0);
}
执行结果:
由于进程产生时默认打开三个stream, stdin
stdout
stderr
,所以最大打开的流数目是1024个
可以通过 ulimit -a
查看,通过 ulimit -n xxx
修改
fgetc\fputc
man fgetc
或 man fputc
可知:
getchar = getc(stdin) = fgetc(stdin)
putchar = putc(c,stdout) = fputc(c,stdout)
并且根据man手册,getc和fgetc,putc和fputc的函数原型是一样的,那么二者有什么区别呢,实际上没有区别,只不过最早的时候getc是作为宏,而fgetc是作为函数。
宏和函数什么区别?宏占用编译时间,函数占用调用时间,内核中的链表大多使用宏和内联函数,就是为了节省调用时间。
函数原型:
int fgetc(FILE *stream);
int fputc(int ch, FILE *stream);
注意,虽然读的是字符,但是为了防止出错,会被转换为 int
类型,所以需要用 int
类型接受 fgetc
的返回值,以及传给 fputc
例子1:编写 mycopy.c 实现 cp
的功能
示例代码:
##include<stdio.h>
##include<stdlib.h>int main(char argc, char **argv)
{FILE *fps, *fpd;int ch; if(argc < 3){ fprintf(stderr, "usage: %s <sourc_file> <dest_file>\n", argv[0]);exit(1); } fps = fopen(argv[1], "r");if(fps == NULL){ perror("fopen()");exit(1);} fpd = fopen(argv[2], "w");if(fpd == NULL){ perror("fopen()");exit(1);}while(1){ch = fgetc(fps);if(ch == EOF)break;fputc(ch, fpd);}fclose(fpd);fclose(fps);exit(0);
}
运行结果:
例子2:编写一个程序,计算一个文件中的有效字符
示例代码:
##include<stdio.h>
##include<stdlib.h>int main(char argc, char **argv)
{ if(argc < 2){ fprintf(stderr, "Usage: %s <file_name>", argv[0]);exit(1);}FILE *fp;int cnt = 0;fp = fopen(argv[1], "r");if(fp == NULL){ perror("fopen()");exit(1);} while(fgetc(fp) != EOF)cnt++;printf("cnt = %d\n", cnt);fclose(fp);exit(0);
}
运行结果:
fgets\fputs
函数原型:
char *fgets(char *s, int size, FILE *stream);
int fputs(const char *s, FILE *stream);
fgets()
:
- 参数:缓冲区指针,缓冲区大小,读取的流
- 返回值:成功返回读取到的字符串,失败返回
NULL
fputs()
:
- 参数:待写入的字符串、流
- 返回值:成功返回一个非负数,失败返回
EOF
【一般不检查】
fgets()
与gets
的关系 : char *gets()
没有指定缓冲区大小,所以会出现缓冲区溢出等问题, fgets()
通过设定缓冲区大小解决了这一点。
fputs()
与 puts()
的关系: puts(char *s)
无法输出流,默认 stdout
, fputs()
可以
fgets()
正常结束有两种情况:
- 读取到
size-1
个字符,最后添加\0
- 读取到
\n
缓冲区大小为5时,abcd实际上要读两次
例子:用 fgets()
和 fputs()
实现指令 cp
示例代码:
##include<stdio.h>
##include<stdlib.h>
##define BUFSIZE 1024int main(char argc, char **argv)
{ FILE *fps, *fpd;char buf[BUFSIZE];if(argc < 3){fprintf(stderr, "usage: %s <sourc_file> <dest_file>\n", argv[0]);exit(1);} fps = fopen(argv[1], "r");if(fps == NULL){ perror("fopen()");exit(1);} fpd = fopen(argv[2], "w");if(fpd == NULL){ perror("fopen()");exit(1);}while(fgets(buf, BUFSIZE, fps) != NULL)fputs(buf, fpd);fclose(fpd);fclose(fps);exit(0);
}
运行结果:
fread\fwrite
函数原型:数据块读写
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);
fread
从stream中读取数据到ptr,每次读取的大小为size,读取nmemb个数据块
fwrite
将ptr中的数据写入到stream中,每次写入的大小为size,写入nmemb个数据块
返回值:成功读取或写入的 数据块 的个数
举例说明:
- 对于
fread(buf, 1, 10, fp)
数据量足够时返回10, 表示成功读取10个数据块,即读取10字节
数据量不足时返回成功读取的数据块个数x,即读取x个字节
- 对于
fread(buf, 10, 1, fp)
数据量足够时返回1, 表示成功读取1个数据块,即读取10个字节
数据量不足时返回0, 此时读取0个字节
结论:一般场景下,推荐1个字节一个字节地读取,当作 fgetc()
来用,来保证读入文件的所有内容。
例子:用 fread()
和 fwrite()
实现指令 cp
示例代码:
while((n = fread(buf, 1, BUFSIZE, fps)) > 0)fwrite(buf,1,n,fpd);
执行结果:
其他的话:
在linux中io很重要:一切皆文件,所有的一切都抽象为文件进行控制,而程序员控制文件的方式就是io。比如socket传的是fd,fd可以通过 fdopen()
转换为 FILE,从而传唤为一个stream,然后程序员通过对stream的io来操控socket。