Liunx下用C语言模拟实现 —— 封装文件操作接口

发布于:2024-04-26 ⋅ 阅读:(26) ⋅ 点赞:(0)

一、前提

本篇博客将循序渐进的模拟实现C语言中,简单版的对FILE和一些简单的文件操作接口的实现,在上一篇文件操作原理篇我们知道,实际的实现就是对系统接口的封装,为了巩固和练习之前学到的概念,我们也来简单的实现对接口的封装设计,要是对接口不太熟悉或者对原理还不太清楚,可以参考我上一篇的内容整理,链接:

http://t.csdnimg.cn/l5KbAicon-default.png?t=N7T8http://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库中文件操作接口的实现,加深对文件操作各个接口和系统调用的理解