一、前提
本篇博客将循序渐进的模拟实现C语言中,简单版的对FILE和一些简单的文件操作接口的实现,在上一篇文件操作原理篇我们知道,实际的实现就是对系统接口的封装,为了巩固和练习之前学到的概念,我们也来简单的实现对接口的封装设计,要是对接口不太熟悉或者对原理还不太清楚,可以参考我上一篇的内容整理,链接:
http://t.csdnimg.cn/l5KbAhttp://t.csdnimg.cn/l5KbA
二、代码设计
1.前提准备
先创建好一个工程的基本模版,采用声明和定义分离的方式,先创建好基本的文件
main.c 、 mystdio.c 、mysdtio.h 、 makefile
然后把makefile写好
myfile:main.c mystdio.cgcc -o $@ $^.PHONY:clean
clean:rm -f myfile
2.头文件的设计
首先,我们的目的是自己封装一个FILE类型(简单版)去实现一些简单的文件操作c接口,所以先设计一下头文件中的声明
我们需要自己封装一个MY_FILE类型的结构体,里面至少要有fd,还有一个缓冲区,以及管理缓冲区内容的参数,还有就是缓冲区的刷新方式
然后就是相关的接口声明,这里简单实现三个基本接口,fopen、fwrite、fclose
最后是我们一定会用到的一个接口,fflush
#pragma once#include<stdio.h>
//缓冲区大小
#define NUM 1024
//刷新策略
#define BUFF_NONE 0x01
#define BUFF_ROW 0x02
#define BUFF_ALL 0x04typedef struct MY_FILE
{int _fd;char _buff[NUM];//缓冲区int _buffsize;int _flags;//刷新策略
}MY_FILE;MY_FILE* my_fopen(const char* path,const char *mode);
size_t my_fwrite(const void* ptr,size_t size,size_t nmemv,MY_FILE* stream);
int my_fclose(MY_FILE *fp);
int my_fflush(MY_FILE* fp);
3.接口实现
(1)my_fopen
我们本质要做的是对open函数的封装,因此在代码设计上,首先我们要比较我们一下fopen的参数和open的参数对比
fopen的使用是给路径和使用方式参数
open则是要给路径,并且带上标志位表示以哪种使用方式打开,然后是在创建新文件的时候要给到权限码
因此我们要做的就是根据fopen传的第二个参数,将其转换成open认识的参数形式,这里模拟实现简单一点,只实现三个最常见的“r”、“w”、“a”,我们用一个flags去根据不同情况去选择标志位,最后,再判断一下是否需要创建新的文件即可
MY_FILE* my_fopen(const char* path,const char* mode)
{int flag = 0;//标志位//根据不同情况记录标志位if(strcmp(mode,"r") == 0) flag |= O_RDONLY;else if(strcmp(mode,"w") == 0) flag |= (O_CREAT | O_WRONLY | O_TRUNC);else if(strcmp(mode,"a") == 0) flag |= (O_CREAT | O_WRONLY | O_APPEND );else{// other operator...}//默认权限码0666;mode_t m = 0666;//打开文件int fd = 0;if(flag & O_CREAT) fd = open(path,flag,m);else open(path,flag);//打开失败if(fd < 0) return NULL;//打开后需要设计返回值,FILE,初始化MY_FILE* ret = (MY_FILE*)malloc(sizeof(MY_FILE));if(ret == NULL){close(fd);//这里如果malloc失败,记得关闭文件perror("malloc error");return NULL;}ret->_fd = fd;ret->_flags = BUFF_ROW;memset(ret->_buff,'\0',sizeof(ret->_buff));ret->_buffsize = 0;return ret;
}
(2)my_fclose
第二个实现的我们选择关闭文件,这样实现完成后可以选择局部先简单测试是否有问题
fclose函数主要做了四个事情:
1.清空缓冲区
这里我们单独实现一个my_fflush函数去完成,因为后续我们还会用到刷新缓冲区的操作、
2.关闭文件
close直接关闭即可
3.释放指针堆空间
free释放掉堆空间
4.将指针置空
fp = NULL;
int my_fclose(MY_FILE* fp)
{assert(fp);//1.清空缓冲区if(fp->_buffsize > 0) my_fflush(fp);//2.关闭文件close(fp->_fd);//3.释放堆空间free(fp);//4.指针置空fp = NULL;return 0;
}
(3)my_fwrite
首先我们先根据参数设计一下如何写代码
fwrite的参数设计是,第一个参数是数据所在的空间地址,第二个是数据块的大小,第三个是数据块的个数(这里可以简单理解,第二第三个参数相乘就是需要传入数据的字节数大小),第四个是传入的文件地址FILE
write的参数设计是第一个是文件描述符(fd),第二个是要传入数据的起始地址,第三个是数据字节大小
两者的参数设计类似,但是c语言中,fwrite得到的数据并不是直接通过write写入到文件中,而是会先到C语言自己封装的缓冲区中,根据不同文件而有不同的缓冲策略,但是这里我们简单模拟实现,默认行缓冲。
综上,代码设计的思路就是:
1.首先我们需要将得到的数据写入到缓冲区中,写入缓冲区时注意不要忘记调整相关的参数_buffsize(增删都要)
ps:我们简化了写入的策略,实际写入中,当数据超过缓冲区大小时,系统会自动帮我们清空缓冲区,并多次调用,直到将内容写完,不需要外部多次调用,这里我们做一个简化,我们规定,当数据量大于缓冲区剩余空间时,我们将其写满后停下,并且将写了多少字节的信息返回(实际返回的是成功写入的数据块个数)
2.检测当前的刷新策略,根据不同策略进行刷新
对应行刷新的策略,我们正常是需要检测这个字符内容中哪部分具有'\n'的,但这里也简单做一个简化,只检测倒数最后一个字符是否为“\n”
3.返回成功写入的字节数(简化调整)
//返回值简化一下,返回成功写入的字节数
size_t my_fwrite(const void *ptr,size_t size,size_t nmemb,MY_FILE* stream)
{//先检查缓冲区是否已经满了,满了就刷新if(stream->_buffsize == NUM) my_fflush(stream);int my_size = size*nmemb;//用户要输入的数据大小int buff_capacity = NUM - stream->_buffsize;//缓冲区剩余的空间大小int ret = 0;if(buff_capacity >= my_size){memcpy(stream->_buff + stream->_buffsize,ptr,my_size);stream->_buffsize += my_size;ret = my_size;}else{memcpy(stream->_buff + stream->_buffsize,ptr,buff_capacity);stream->_buffsize += buff_capacity;ret = buff_capacity;}//简化版的刷新策略if(stream->_flags & BUFF_ALL){if(stream->_buffsize == NUM){my_fflush(stream);}}else if(stream->_flags & BUFF_ROW){if(stream->_buff[stream->_buffsize-1] == '\n'){my_fflush(stream);}}return ret;
}
(4)my_fflush
我们发现,实际真正写入到文件,是刷新缓冲区的时候,刷新缓冲区的步骤很简单,直接根据用write,将缓冲区的内容写入到文件中即可,稍微检测一下,当缓冲区里没内容时就不写,注意,写完后要调整管理缓冲区大小的参数_buffsize,将其置为0
int my_fflush(MY_FILE* fp)
{assert(fp);if(fp->_buffsize != 0){write(fp->_fd,fp->_buff,fp->_buffsize);}fp->_buffsize = 0;return 0;
}
三、测试结果
其实,测试这个步骤是在写代码的过程中,每完成一个小模块就要去测试的,这里我们简单的用main函数去测试一下基本的文件读写功能,若是发现问题就慢慢调试找bug
四、参考代码
1.mystdio.h
#pragma once#include<stdio.h>
//缓冲区大小
#define NUM 1024
//刷新策略
#define BUFF_NONE 0x01
#define BUFF_ROW 0x02
#define BUFF_ALL 0x04typedef struct MY_FILE
{int _fd;char _buff[NUM];//缓冲区int _buffsize;int _flags;//刷新策略
}MY_FILE;MY_FILE* my_fopen(const char* path,const char *mode);
size_t my_fwrite(const void* ptr,size_t size,size_t nmemv,MY_FILE* stream);
int my_fclose(MY_FILE *fp);
int my_fflush(MY_FILE* fp);
2.mystdio.c
#include"mystdio.h"
#include<string.h>
#include<sys/types.h>
#include<fcntl.h>
#include<malloc.h>
#include<unistd.h>
#include<assert.h>MY_FILE* my_fopen(const char* path,const char* mode)
{int flag = 0;if(strcmp(mode,"r") == 0) flag |= O_RDONLY;else if(strcmp(mode,"w") == 0) flag |= (O_CREAT | O_WRONLY | O_TRUNC);else if(strcmp(mode,"a") == 0) flag |= (O_CREAT | O_WRONLY | O_APPEND );else{// other operator...}mode_t m = 0666;int fd = 0;if(flag & O_CREAT) fd = open(path,flag,m);else open(path,flag);if(fd < 0) return NULL;MY_FILE* ret = (MY_FILE*)malloc(sizeof(MY_FILE));if(ret == NULL){close(fd);perror("malloc error");return NULL;}ret->_fd = fd;ret->_flags = BUFF_ROW;memset(ret->_buff,'\0',sizeof(ret->_buff));ret->_buffsize = 0;return ret;
}//返回值简化一下,返回成功写入的字节数
size_t my_fwrite(const void *ptr,size_t size,size_t nmemb,MY_FILE* stream)
{//先检查缓冲区是否已经满了,满了就刷新if(stream->_buffsize == NUM) my_fflush(stream);int my_size = size*nmemb;//用户要输入的数据大小int buff_capacity = NUM - stream->_buffsize;//缓冲区剩余的空间大小int ret = 0;if(buff_capacity >= my_size){memcpy(stream->_buff + stream->_buffsize,ptr,my_size);stream->_buffsize += my_size;ret = my_size;}else{memcpy(stream->_buff + stream->_buffsize,ptr,buff_capacity);stream->_buffsize += buff_capacity;ret = buff_capacity;}//简化版的刷新策略if(stream->_flags & BUFF_ALL){if(stream->_buffsize == NUM){my_fflush(stream);}}else if(stream->_flags & BUFF_ROW){if(stream->_buff[stream->_buffsize-1] == '\n'){my_fflush(stream);}}return ret;
}int my_fflush(MY_FILE* fp)
{assert(fp);if(fp->_buffsize != 0){write(fp->_fd,fp->_buff,fp->_buffsize);}fp->_buffsize = 0;return 0;
}int my_fclose(MY_FILE* fp)
{assert(fp);//1.清空缓冲区if(fp->_buffsize > 0) my_fflush(fp);//2.关闭文件close(fp->_fd);//3.释放堆空间free(fp);//4.指针置空fp = NULL;return 0;
}
3.main.c(这个可以自己测试)
#include"mystdio.h"
#include<stdio.h>
#include<string.h>int main()
{MY_FILE* fp = my_fopen("test.txt","w");if(fp == NULL){perror("open error");return -1;}const char* s = "hello haha\n";my_fwrite(s,strlen(s),1,fp);my_fclose(fp);return 0;
}
4.makefile
myfile:main.c mystdio.cgcc -o $@ $^.PHONY:clean
clean:rm -f myfile
总结
本次简单的模拟实现了一下FILE的封装和一些简单c库中文件操作接口的实现,加深对文件操作各个接口和系统调用的理解