I/O
stdio:标准io(优先使用,系统移植好)
sysio:系统调用io
如printf(标准io)、fopen(标准io)、open(Linux环境下的系统调用)
一、stdio:标准io
fopen
#include <stdio.h>
FILE *fopen(const char *path, const char *mode);//打开文件
//成功返回FILE指针,失败返回NULL以及设置errno
//path:文件路径(相对和绝对都可以)
//mode:r,r+,w,w+,a,a+;只有r与r+要求文件必须存在;
//注意,返回的FLIE指针是存放在堆上的,一般来说,看到返回的是一个指针,并且是有互逆操作的(open-close),一般都是在堆上
例子:
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
int main(){
FILE *fp;
fp = fopen("tmp","r");//我这里目录下没有tmp文件,结果会报错
if(fp == NULL){
fprintf(stderr,"fopen() failed! errno = %d\n", errno);
//perror("fopen()");//用perror可以自动关联全局变量errono,可以把返回的错误码转换成信息
//fprintf(stderr,"fopen():%s\n",strerror(errno));//同理,传入errno可以返回出错信息
exit(1);
}
puts("ok");
exit(0);
}
//结果:fopen() failed! errno = 2
fclose
#include <stdio.h>
int fclose(FILE *fp);//关闭
//成功返回0
fgetc/fputc
字符读写
这里实现一个小功能来运用这些接口
//实现文件复制的功能cp
//用例: ./copyflie sorce dest
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char **argv){
int ch;//存读写字符
if(argc != 3){//参数个数检查
fprintf(stderr, "please use right argc\n");
exit(1);
}
FILE *f1 = fopen(argv[1],"r");//打开原文件
if(f1 == NULL){
perror("fopen()");
}
FILE *f2 = fopen(argv[2],"w");//打开目标文件
if(f2 == NULL){
fclose(f1);//如果f2打开失败,关闭f1,预防内存泄露
perror("fopen()");
}
while(1){//开始读写文件
ch = fgetc(f1);
if(ch == EOF){//read end
break;
}
fputc(ch,f2);//写入目标文件
}
fclose(f1);
fclose(f2);
exit(0);
}
fgets/fputs
字符串读写
用fgets/fputs来改写上面的代码
//实现文件复制的功能cp
//用例: ./copyflie sorce dest
#include <stdio.h>
#include <stdlib.h>
#define MAXSIZE 1024
int main(int argc, char **argv){
char buf[MAXSIZE];//缓存区
if(argc != 3){//参数个数检查
fprintf(stderr, "please use right argc\n");
exit(1);
}
FILE *f1 = fopen(argv[1],"r");//打开原文件
if(f1 == NULL){
perror("fopen()");
}
FILE *f2 = fopen(argv[2],"w");//打开目标文件
if(f2 == NULL){
fclose(f1);//如果f2打开失败,关闭f1,预防内存泄露
perror("fopen()");
}
while(fgets(buf,MAXSIZE,f1) != NULL){//一直读文件直到读完
fputs(buf,f2);
}
fclose(f1);
fclose(f2);
exit(0);
}
fread/fwrite
二进制流读写
#include <stdio.h>
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
/**
ptr:读来的数据放在这块区域里,读的数据有多大,这个区域就有多大,(大小就应该是size * nmemb)
size:每个对象的大小
nmemb:读的对象的数量
stream:从这个stream里面读数据
**/
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
/**
和fread一样的,注意这里的ptr用的是const修饰,表明对于源数据不会进行修改,只会读取
**/
//注意事项:两个函数的返回值,成功为返回成功的读到或者写出的对象个数,不够一个对象则为零。
下面用fread/fwrite改造上面的copyfile程序:
//实现文件复制的功能cp
//用例: ./copyflie sorce dest
#include <stdio.h>
#include <stdlib.h>
#define MAXSIZE 1024
int main(int argc, char **argv){
char buf[MAXSIZE];//缓存区
if(argc != 3){//参数个数检查
fprintf(stderr, "please use right argc\n");
exit(1);
}
FILE *f1 = fopen(argv[1],"r");//打开原文件
if(f1 == NULL){
perror("fopen()");
}
FILE *f2 = fopen(argv[2],"w");//打开目标文件
if(f2 == NULL){
fclose(f1);//如果f2打开失败,关闭f1,预防内存泄露
perror("fopen()");
}
/*
while(fread(buf,1,MAXSIZE,f1)){//如果是这种写法的话,复制的文件不一定会相同,为什么?不能保证最后一次刚好读完1024,因此我们应该判断正确读到的个数
fwrite(buf,1,MAXSIZE,f2);
}
*/
//正确写法如下:----------------------------
while(fread(buf,1,MAXSIZE,f1) > 0){
fwrite(buf,1,n,f2);
}
//----------------------------------------
fclose(f1);
fclose(f2);
exit(0);
}
printf/scanf簇
#include <stdio.h>
int printf(const char *format, ...);
int fprintf(FILE *stream, const char *format, ...);//重定向到stream中去,如:fprintf(stderr,"fopen() failed !");
int sprintf(char *str, const char *format, ...);//以串的形式输出到str
int snprintf(char *str, size_t size, const char *format, ...);//在sprintf的基础上增加了个长度
int scanf(const char *format, ...);
int fscanf(FILE *stream, const char *format, ...);//内容不是从终端获取的了,是从一个指定的流中获取,当然也可以是stdin
int sscanf(const char *str, const char *format, ...);//内容来源于str字符串
fseek/ftell/rewind
#include <stdio.h>
int fseek(FILE *stream, long offset, int whence);//设置文件位置指针的位置
//stream:流
//offset:偏移量
//whence:从哪个位置开始偏移,可选项:SEEK_SET(开始位置),SEEK_CUR(当前位置),SEEK_END(结束位置)
int ftell(FILE *stream);//告诉文件位置指针的当前位置相对与文件首的偏移字节数
void rewind(FILE *stream);//不管你的位置指针现在在哪里,立马移动到最开始处
//这个rewind相当于(void)fseek(stream,0L,SEEK_SET)的封装
int fseeko(FILE *stream, off_t offset, int fromwhere);//这个和fseek的区别只有offset的数据类型不同,相应的处理的偏移量范围就不同
off_t ftello(FILE *stream);//注意,返回的是long int
有了fseek和ftell,我们可以换一种方式来获取一个文件的长度(以前可以使用fgetc/fputc组合count计数来实现)
下面使用fseek于ftell来实现:
#include <stdio.h>
#include <stdlib.h>
//用法用例: ./filesize file
int main(int argc, char **argv){
if(argc != 2){//检查参数个数
fprintf(stderr,"please use right argc\n");
}
FILE *f1 = fopen(argv[1], "r");
if(f1 == NULL){
perror("fopen()");
}
fseek(f1,0,SEEK_END);//把文件位置指针定位到文件末尾,可以使用这句或者使用下面的rewind语句
//rewind(f1);//或者使用rewind同样可以达到上面的fseek的效果,即:不管你的位置指针现在在哪里,立马移动到最开始处
fprintf("file size = %ld\n",ftell(f1));//直接获取该文件位置指针的位置即为文件字节大小
fclose(f1);
exit(0);
}
补充:fseek经常会应用于帮我们完成一个空洞文件,什么是空洞文件呢?--里面全部或者部分充斥着空字符或者\0;空洞文件的用处:比如下载一个电影的时候,你的磁盘上会建立一个文件,那它是一个字节一个字节的增长到2G的吗?不是的,它(空洞文件)会提前占用一片存储空间比如(2G),然后再多线程切分空间,上锁下载数据。
fflush
先看个引入的例子:
#include <stdio.h>
int main(){
printf("before while");
while(1);
printf("after while");
exit(0);
}
//这个程序执行,可能有人会认为,结果会输出before while,然后程序卡在while(1)里,打印不出after while
//但是结果却是什么都不会打印,为什么呢?
//printf碰到标准输出是典型的行缓存模式,也就是一行满了再刷新或者遇到换行符再刷新,这里就刷不出来,因此我们写printf的时候习惯于加\n。
//如果将程序改成printf("before while\n");就能打印出before while了
这里说下缓冲区 缓冲区的作用:大多数情况下是好事,合并系统调用 分类: 1、行缓冲:换行的时候刷新、满了的时候刷新、强制刷新(标准输出是行缓冲,因为它是终端设备)。 2、全缓冲:满了的时候刷新、强制刷新(默认情况下都是全缓冲,终端设备除外)。 3、无缓冲:立即输出,如stderr。
能否改变缓冲的设置呢?--使用setvbuf
fflush的作用就是强制刷新缓冲区
#include <stdio.h>
int fflush(FILE *stream);//传入参数为NULL的时候,刷新所有的流
使用fflush改写上面的程序:
#include <stdio.h>
int main(){
printf("before while");
fflush(stdout);
while(1);
printf("after while");
fflush(NULL);//传入参数为NULL的时候,刷新所有的流
exit(0);
}
getline
一次读一行,不管这一行有多长都会读出来,因为它用的动态内存来实现的,可以不断地往外扩展。(也就是先malloc,再不断地realloc)
#include <stdio.h>
ssize_t getline(char **lineptr, size_t *n, FILE *stream);
//将包含buffer的首地址存到一级指针中
//n:char * 的大小
//stream:文件流
//成功返回读到的字符个数(不包含\0),失败返回-1
下面使用getline来实现读一个文件有多少行 ,每行多少字符:
#define _GNU_SOURCE // 这里不想写看着丑的话,可以加入makefile中
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// 用例格式 ./getline file
int main(int argc, char **argv)
{
char *linebuf;
size_t linesize;
if (argc != 2)
{ // 检查参数数量
fprintf(stderr, "please use right argc\n");
exit(1);
}
FILE *f1 = fopen(argv[1], "r");
if (f1 == NULL)
{
perror("fopen()");
exit(1);
}
/*!!!!!!非常重要,不然可能出bug*/
linebuf = NULL;
linesize = 0;
/*-----------!!!!--------------*/
while (1)
{
if (getline(&linebuf, &linesize, f1) < 0)
{
break;
}
printf("%d\n", strlen(linebuf));//查看每一行的字符数
printf("%d\n", linesize);//看存放字符的内存大小,这里是用来查看动态增长的过程的
}
fclose(f1);
free(linebuf);//这里有个可控的内存泄漏,因为知道getline是用malloc实现,所以这里使用free。
exit(0);
}
tmpnam/tmpfile
创建临时文件,后面用到再说
二、sysio:系统调用/文件io
文件描述符(fd)是在系统调用io中贯穿始终的类型,它是一个整形数,是数组下标,它优先使用当前可以使用的文件描述符中的最小的一个
文件io操作相关函数:open、close、read、write、lseek;
open
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname, int flags);
//
//flags:这里的flags其实是一个bitmap(位图)。必须包含以下内容:O_RDONLY(只读)、O_WRONLY(只写)、O_RDWR(读写)
//成功返回文件描述符,失败返回-1并设置errno
/*和之前的stdio中的fopen中的mode映射下
r:O_RDONLY
r+:O_RDWR
w:O_WRONLY|O_CREAT|O_TRUNC
w+:O_RDWR|O_TRUNC|O_CREAT
*/
int open(const char *pathname, int flags, mode_t mode);
//这个mode会在创建文件的时候按位与上umask取反设置文件权限
//注意:flags中有O_CREAT的话要用三参数的open,反之则用两参数的open;这里的open使用变参实现的,不是重载,c语言本身也不支持重载。
//那么对于两个看上去像重载的函数如何去判断他是重载实现还是变参实现的呢?可以调用这个函数传多个参数试试,看看它是报编译错误(参数数量不对)--这就是重载实现的(因为重载规定了参数),不报错说明是变参,它自己都不知道自己会有多少个参数
close
#include <unistd.h>
int close(int fd);
read/write/lseek
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
//从fd里读,读到buf里去,读count长度的数据
//返回值成功为读到的字节数,失败为负数,等于0则表示读完
ssize_t write(int fd, const void *buf. size_t count);
//将buf里的数据写到fd中,写count长度的数据
off_t lseek(int fd, off_t offset, int whence);//相当于前面的fseek和ftell的综合
//offset:偏移量
//whence:相对位置
//返回文件开头距离定位后的位置的距离
下面使用这些函数来实现之前实现过的filecopy:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define BUFSIZE 1024
int main(int argc, char **argv){
int fd1;//原文件
int fd2;//目标文件
char buf[BUFSIZE];
int len,ret;
if(argc != 3){//参数检查
fprintf(stderr,"please use right argc\n");
exit(1);
}
//打开文件(原文件和目标文件)
fd1 = open(argv[1],O_RDONLY);
if(fd1 < 0){
perror("openfd1");
exit(1);
}
fd2 = open(argv[2],O_WRONLY|O_CREAT|O_TRUNC,0600);
if(fd2 < 0){
close(fd1);//注意这里防止内存泄漏,要把fd1关闭
perror("openfd2");
exit(1);
}
while(1){
len = read(fd1,buf,BUFSIZE);
if(len < 0){
perror("read()");
break;//这里没用exit(1),否则这里挂掉的话会出现内存泄漏
}
if(len == 0){
break;
}
while(1){//只有此次成功读取的数量等于成功写入的数量的时候才会进行下一次读写
ret = write(fd2,buf,len);
if(ret < 0){
perror("write()");
break;
}
//这里再判断下len(读到的数量)和ret(写入成功的数量)是否一致
if(ret != len){
continue;
}
break;
}
}
close(fd1);
close(fd2);
exit(0);
}
文件io与标准io的区别
文件io:响应速度快
标准io:吞吐量大
为什么?因为一次文件io就是一次实打实的系统调用,而标准io由于有缓冲机制的存在,它最大的作用是合并系统调用
注意不要将文件io与标准io混用
文件io与标准io之间的转换涉及的两个函数:fileno、fdopen
#include <stdio.h>
int fileno(FILE *stream);//返回文件描述符
FILE *fdopen(int fd, const char *mode);//把文件io的文件描述符fd操作返回为标准io的FILE操作
dup/dup2
首先看个东西,实现不改变put("hello world !")及其以下代码的情况下,让程序执行后将hello world !写到文件,不写到标准输出:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define FILENAME "/tmp/out"
int main(){
int fd;
close(1);//关闭标准输出
fd = open(FILENAME,O_RDONLY|O_CREAT|O_TRUNC,0600);//这样打开的文件会优先使用最小能使用的,即1
if(fd < 0){
perror("open()");
exit(1);
}
put("hello world !");
exit(0);
}
可以看到上述代码使用了重定向的功能,这里专门有个函数可以实现此功能--dup
#include <unistd.h>
int dup(int oldfd);//拷贝一份传入的文件描述符到当前可用范围内最小位置去
我们用dup改写上述代码:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define FILENAME "/tmp/out"
int main(){
int fd;
fd = open(FILENAME,O_RDONLY|O_CREAT|O_TRUNC,0600);//这样打开的文件会优先使用最小能使用的,即1
if(fd < 0){
perror("open()");
exit(1);
}
close(1);//关闭标准输出
dup(fd);//将fd复制到1
close(fd);//再关闭fd--即1
put("hello world !");
exit(0);
}
但是注意dup操作与close操作,试想一些可能产生的bug,一般多线程下(现在还没涉及,后面会讲多线程),执行完close(1)之后,可能另一个线程恰好开了个文件,那他会占据你原本想占据的位置的文件描述符,然后你dup就会到其他位置,就可能重定向到别的位置去了。又或是另一只情况,假设原本文件描述符1就没有(当然一般来说012都是有的,这里只是假设),那open之后fd会为1,然后close(1)关闭掉了fd,然后fd已经释放,后续使用dup(fd)就会出现问题等等。
那么,引起上述这些问题的根本就是在于这里的dup和close不是原子操作(不可分割的操作,可以解决竞争和冲突),中间可以被打断。下面介绍的dup2就能克服这点:
#include <unistd.h>
int dup2(int oldfd, int newfd);//把newfd作为oldfd的副本,并且newfd如果已经被占用了,那么会把newfd先关掉然后再把oldfd复制过去
//所以我们可以看出这里的dup2其实就是上面代码的“close(1)+dup(fd)”
//并且,如果你的oldfd和newfd相等的话,dup2什么也不会做,直接返回newfd
再用dup2改写下上面的代码:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define FILENAME "/tmp/out"
int main(){
int fd;
fd = open(FILENAME,O_RDONLY|O_CREAT|O_TRUNC,0600);//这样打开的文件会优先使用最小能使用的,即1
if(fd < 0){
perror("open()");
exit(1);
}
//close(1);//关闭标准输出
//dup(fd);//将fd复制到1
dup2(fd,1);//这里的dup2就等于上面两个操作,但dup2是原子操作
//但是这里要是fd就等于1的话,dup2什么都不会做,而下面还要去关fd,所以得进行判断
if(fd != 1){//fd不是1再关闭fd,这样我用的才是1
close(fd);
}
put("hello world !");
exit(0);
}
一些其他函数
同步:sync、fsync、fdatasync
fcntl
ioctl:设备相关的内容
(后面用到再详细说)