- 介绍
- 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 *,失败返回errnoperror("提示信息")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,所以可以不添加 -
关于
errnoerrno在一开始是一个 全局 的整型变量,不同的值对应不同的错误。问题在于共用,所以如果不立刻打印就会被其他进程占据。
现在的
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。
