一、前提
本篇博客将循序渐进的模拟实现C语言中,简单版的对FILE和一些简单的文件操作接口的实现,在上一篇文件操作原理篇我们知道,实际的实现就是对系统接口的封装,为了巩固和练习之前学到的概念,我们也来简单的实现对接口的封装设计,要是对接口不太熟悉或者对原理还不太清楚,可以参考我上一篇的内容整理,链接:
http://t.csdnimg.cn/l5KbAhttp://t.csdnimg.cn/l5KbA
二、代码设计
1.前提准备
先创建好一个工程的基本模版,采用声明和定义分离的方式,先创建好基本的文件
main.c 、 mystdio.c 、mysdtio.h 、 makefile
然后把makefile写好
myfile:main.c mystdio.c
gcc -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 0x04
typedef 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 0x04
typedef 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.c
gcc -o $@ $^
.PHONY:clean
clean:
rm -f myfile
总结
本次简单的模拟实现了一下FILE的封装和一些简单c库中文件操作接口的实现,加深对文件操作各个接口和系统调用的理解