C++应用开发-文件系统编程-文件IO操作封装

2007/12/10 c++编程

Linux系统内核针对文件系统操作处理提供了一系列的系统调用方法,通过这些系统调用为用户应用程序与系统内核搭建一个操作接口。开发者可以根据系统内核提供的文件API,从而轻松的在Linux系统应用程序中处理文件。Linux系统文件API主要包含文件创建、删除、打开、读写以及控制等,下面将会详细介绍Linux系统内核提供的文件操作API的应用情况。

1.Linux系统文件API简介
介绍Linux系统文件API之前,需要对Linux下文件描述符有一个认识。Linux下文件描述符为一个整数,用来表示每个被当前应用所打开的文件。随后的所有针对文件的操作基本都通过该文件描述符来进行。描述符为一个非负的整数,从0开始依次递增标识每个被打开的对应文件,当然Linux系统针对每个应用进程打开的文件数量有一个最大值的限制,这些值通常通过相应的配置可以作修改。

系统中的描述符有着一定的划分,比如0表示标准输入、1表示标准输出以及2表示标准错误等。系统会维护一个文件描述符的数组来组织和保存相应打开文件的描述符,用户只需要通过相应的系统调用获取打开文件的描述符就可以针对文件作其它所需要的操作。

Linux系统针对文件系统操作提供的API主要包括文件创建、打开、读写、关闭、删除、定位以及文件操作的控制和文件锁相关的系统调用方法。对应的系统函数调用包括creat(创建文件)、open(打开文件)、read(读取文件)、write(写入文件)、close(关闭文件)、unlink(删除文件)、lseek(文件定位)、fcntl(文件控制)以及针对文件锁提供的flock方法等。下面将会针对提及常用的文件操作系统调用作使用讲述。

2.Linux系统调用之文件创建与打开操作
Linux系统调用针对文件创建于打开操作提供两个系统方法,分别为creat与open函数,其中open函数根据需要可以设定为创建或者打开已经存在的文件。两个系统调用原型如下所示。


    #include <sys/types.h>
    #include<sys/stat.h>
    #include<fcntl.h>
    int creat(constchar *pathname,mode_t mode);
    int umask(int )
    int open(constchar *pathname,int flags);
    int open(constchar *pathname,int flags,mode_t mode);

该类系统调用需要包含一些头文件定义,其中sys/types.h头文件定义包含系统提供的数据类型原始定义,sys/stat.h头文件主要包含文件状态的定义,头文件fcntl.h则包含文件控制定义内容。文件创建与打开系统调用包含creat与两个open重载函数定义。

函数creat与open调用成功时都会返回相应文件描述符,否则返回值为-1表示失败。其中方法creat主要参数有两个,第一个为需要创建文件指定的目录与文件名,第二个参数表示文件创建的模式定义。不同的文件模式指定可以针对文件处理作出多种限制。其中umask系统调用主要用于屏蔽掉指定文件操作权限,通常与creat方法配合使用。

open函数主要用于创建或者打开存在的文件,当指定打开的文件不存在时,根据flags参数设定可以重新创建一个指定文件并打开。该函数调用成功依然会返回一个文件描述符,失败时返回值为-1,具体系统提供的错误号可以根据man来查询该方法的帮助信息。open函数提供两个重载实现方法,第一个只包含两个参数,一个参数为指定文件全名,另一个参数则为文件创建或打开的方式标志。该标志值有多种分别表示文件打开读写方式、文件打开一般方式以及文件同步方式三类。

第一类文件打开读写主要包含3个标志值,分别为O_RDONLY文件只读方式、O_WRONLY文件只写方式以及O_RDWR文件读写方式三种。而第二类则主要包含O_CREAT、O_EXCL、O_APPEND以及O_TRUNC等多种文件操作方式。常见文件操作应用中该类前4个标志值比较常用,其余可以通过man帮助查询其使用说明。

其中O_CREAT标志表示打开文件操作调用时,如果指定的文件不存在,则创建并打开该文件;而O_EXCL一般与O_CREAT一起通过或运算使用,表明如果打开的文件不存在则创建指定的文件,如果指定文件已经存在则报错;O_APPEND标志表明文件打开将会以追加的方式,即保存原始指定文件内容,直接从原文件末尾开始文件操作;而与之相反的O_TRUNC标志值则表明存在的文件打开后会被清空其内容。以上后两个设置应用在日志文件中有着应用,比如保存当天日志文件时,遇到需要记录的数据打开文件后需要采用追加的方式,如此才能保证日志的连续性。

另外flags参数还包括一类文件方式设定,即文件同步方式。主要包含如O_SYNC表示打开的文件作一些写入操作都会阻塞进程,直到文件写入操作完成之后等这类标志值,实际使用中并不需要记忆如此多的标志值,只需要在使用时学会通过man来查询其说明即可。

open函数第三个参数mode用于创建新文件时,指定其新文件访问方式控制。主要包含当前文件属主访问权限、文件属组内其它成员访问权限以及Linux系统其它用户访问权限的设定。该标志值通过八进制数表示,提供了一系列的标志值来表明不同情况下文件访问权限的控制,供文件打开调用时设定。同样这些标志值可以通过man open查询系统中指定调用说明来获取,具体标志值描述此处不作多讲述,这里主要介绍文件处理操作相关方法。

3.封装实例
1)准备实例
打开UE工具,创建新的空文件并且另存为chapter1803.cpp。该代码文件随后会同makefile文件一起通过FTP工具传输至Linux服务器端,客户端通过scrt工具访问操作。程序代码文件编辑如下所示。


    /**
    * 实例chapter1803
    * 源文件chapter1803.cpp
    * 封装创建和打开文件操作实例
    */
    #include
    #include
    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>
    using namespace std;

    /**
     *打开或者创建文件的方法枚举定义.
     *内部定义几种文件创建或打开的标记值.
     */
    enum OpenOrCreateMethod{OpenOrCreate_Exist, //只打开存在的文件
           OpenOrCreate_Append, //以追加方式打开文件,并且保存文件内容
           OpenOrCreate_Excl, //只创建不存在的文件
           OpenOrCreate_Trunc};//打开或创建新的文件,并且销毁原来文件内容

    /**
     *文件读或者写的模式枚举定义.
     *内部定义两种文件写或读基本属性标记值.
     */
    enum FileReadOrWriteMode{Read_Access  = 0x01, //申请读权限标记
        Write_Access = 0x02};   //申请写权限标记

    /**
     *文件属性枚举定义.
     *内部定义几种文件基本属性标记值.
     */
    enum FileAttributes{ FileExists  = 0x01,  //文件存在标记
               RegularFile= 0x02,  //普通文件标记
               DirectoryFile=0x04, //目录文件标记
               FileHidden= 0x08,   //隐藏文件标记
               FileReadOnly= 0x10}; //文件只读标记

    /**
     *打开文件方法.
     * @param     path       输入处理文件名参数.
     * @param       accessFileMode     输入存取文件标记参数.
     *@param    openMethod              输入文件打开或者创建方式
     *@param    fileAttributes             输入文件属性标记
     * @return 处理成功返回打开文件的描述符,否则返回-1
     */
    int openFile(const string& path, const intaccessFileMode,
                   constOpenOrCreateMethod openMethod,
                   constint fileAttributes)
    {
         intflags = 0;  //定义标记变量
         intpermFlags = 0;   //定义文件权限标记变量
          //判断是否读写权限模式打开文件
         if ((accessFileMode & Read_Access) && (accessFileMode &Write_Access) )
         {
            flags|= O_RDWR;  //标记变量与读写权限标志值之间进行按位或运算
                     //如果if判断结构中条件为真,则flags标记值赋值为读
                     //写都打开的标记值
         }
         //判断是否只读权限模式打开文件
         else if (accessFileMode & Read_Access)
         {
               //如果if判断结构中条件为真,则flags标记值赋值为只
               //读标记值
               flags |= O_RDONLY;
         }
         //判断是否只写权限模式打开文件
         else if (accessFileMode & Write_Access)
        {
                //如果if判断结构中条件为真,则flags标记值赋值为只
                 //写标记值
               flags |= O_WRONLY;
         }
         else
         {
             //否则输出相应文件打开错误信息
            cout<<"file open error"<< errno <<endl;           
            return -1; //返回整型-1值表明文件打开失败
       }
       switch(openMethod)   //选择文件打开方式
       {
           //打开已经存在的文件,如果设置存取文件的模式为写权限,
           //同时定位文件末尾进行写数据
            case OpenOrCreate_Exist:  
                 //设置相应的标记值
                if (accessFileMode &Write_Access) flags |= O_APPEND;
                      break;
            //打开文件,如果该文件已经存在,同时定位至文件末尾进行数据写入
            //否则新创建一个文件
            case OpenOrCreate_Append:                     
                flags |= O_CREAT | O_APPEND;      //设置相应的标记值
                      break;
            //创建文件,如果创建的文件已经存在,则打开文件出错;
            //否则创建新的文件
            case OpenOrCreate_Excl:   
                flags |= O_CREAT | O_EXCL; //设置相应的标记值
                break;
            //如果该文件已经存在,并且以可写的方式打开时,
            //此标志位会令文件长度清为0,里面的数据清除;否则创建新文件
            case OpenOrCreate_Trunc:                         
                flags |= O_CREAT | O_TRUNC; //设置相应的标记值
                break;
      }

      //判断打开方式,设置文件权限
      if (openMethod == OpenOrCreate_Excl ||openMethod == OpenOrCreate_Trunc)
      {
            if (fileAttributes & FileReadOnly)  //判断文件设置为只读权限
                permFlags = S_IRUSR; //则设置permFlags权限标记为只读标记值
            else
                permFlags = (S_IRUSR | S_IWUSR);//否则设置为可读写标记值
      }

      //调用系统提供的打开文件操作,传入文件路径名,文件创建
      //或者打开方式标记,以及权限标记
      int fd = open(path.c_str(), flags,permFlags);
      //系统文件打开操作将会返回文件句柄值,通过判断该标记值
      //判断文件打开是否失败
      if (fd == -1)  
      {
            //如果文件创建、打开失败,则输出错误信息
            cout<<"fileopen(path.c_str(), flags, permFlags) error= "<< errno <<endl;      
            return -1;                                                        
      }
      return fd; //否则文件打开或者创建成功,返回句柄供操作该文件的方法使用
}

/*主程序入口*/
int main()
{
    stringpathfile;   //主程序中定义字符串对象表示文件路径名
    intfd; //定义文件句柄变量
    cout<<"Pleaseinput pathfile:"<<endl; //输出输入文件路径名的提示信息
    cin>>pathfile;  //从屏幕输入该文件路径名
    //调用文件打开操作,并且设置相应的读写、以及打开文件方式
    fd =openFile(pathfile,Read_Access|Write_Access,OpenOrCreate_Excl,RegularFile);
    cout<<"Filefd:"<<fd<<endl; //打印获取的文件句柄值,查看文件打开是否成功
    return0;
}

本实例主要通过枚举定义Linux文件相关属性参数,采用C++语言封装灵活的Linux文件创建与打开操作。本程序主要由主函数与文件创建、打开两个函数组成,具体程序讲解见程序剖析部分。

2)编辑makefile
Linux平台下需要编译的源文件为chapter1803.cpp,相关联的makefile工程文件内容编辑如下。


OBJECTS=chapter1803.o
CC=g++

chapter1803: $(OBJECTS)
    $(CC)$(OBJECTS) -g -o chapter1803

clean:
    rm -fchapter1803 core $(OBJECTS)

submit:
    cp -f -rchapter1803 ../bin
    cp -f -r*.h ../include

上述makefile文件套用前面的模板格式,主要替换了代码文件、程序编译中间文件、可执行程序等。在编译命令部分-g选项的加入,表明程序编译同时加入了可调式信息。

3)编译运行程序
当前shell下执行make命令,生成可执行程序文件,随后make submit提交程序文件至本实例bin目录中,随后通过cd命令定位至实例bin目录,该可执行程序运行结果如下所示。


[developer@localhost src]$make
    g++    -c -ochapter1803.o chapter1803.cpp
    g++ chapter1803.o -g -o chapter1803

[developer@localhost src]$make submit
    cp -f -r chapter1803 ../bin
    cp -f -r *.h ../include

[developer@localhost src]$cd../bin

[developer@localhost src]$./chapter1803
    Please input pathfile:
    /home/ocs/users/wangfeng/Linux_c++/chapter18/chapter1803/bin/testFile.txt
    File fd:3

[developer@localhost src]$ll
    总用量 16
    -rwxr-xr-x   1 ocs      linkage     15923 1月  7 06:56 chapter1803
    -rw-------   1 ocs      linkage         0 1月  7 06:57 testFile.txt

4)程序剖析
本实例实际是在Linux系统文件打开操作系统调用open之上使用C++封装实现了openFile接口,该封装实现的方法主要用于创建或打开指定文件。实例中首先定义三个枚举类型变量,分别表示低级文件操作中的文件打开方式,文件读写方式以及文件的基本属性。三个枚举类型变量中实际为C++的中的标识,该标识用于内部组合采用实际系统调用中的各种权限模式。

创建或打开文件方法openFile共提供四个参数,第一个参数path用于表示指定创建或打开的路径全名,第二个参数accessFileMode则用于指定创建或打开文件的读写模式,第三个参数openMethod为枚举类型OpenOrCreateMethod的对象,用于表示文件创建或打开的方式。最后一个参数fileAttributes表示文件的基本属性,即指定为普通文件、目录或者只读文件。

创建或打开文件方法内部首先定义两个整型变量分别为flags与permFlags表示文件创建或打开的方式以及指定的权限模式,并且初始化为0。内部先根据传入的文件打开时的存取权限信息,根据传入的标志值分别与读取、写入方式作&运算,如果提供两个标志值意味着文件打开方式为可读写即指定flags为O_RDWR。否则分别设置相应传入的打开方式对应的读或者写的方式,如果没有指定则默认输出出错信息,并通过errno号来说明相应错误。

方法内部随后通过switch结构来判断相应的文件创建或打开的基本方式,是以追加的方式O_APPEND打开文件,还是以O_CREAT| O_APPEND组合、O_CREAT | O_EXCL组合还是O_CREAT | O_TRUNC组合方式来创建或者打开文件,根据传入的参数openMethod来决定。后通过if控制结构来设定文件打开时的权限模式。

if结构内部通过比较,文件属性是否指定为只读文件,如果为只读文件,则设置S_IRUSR只读权限,否则设置可读写权限S_IRUSR | S_IWUSR。最后调用Linux下系统调用open方法,传入相应的创建或者打开文件路径全名,传入文件打开的基本方式以及文件创建的基本权限标记,创建或打开指定文件。该方法返回一个文件描述符整型值,如果打开或创建成功将会返回对应的描述符,否则返回-1。最后成功情况下openFile方法返回整型文件描述符fd。

本实例程序主函数中,首先定义字符串对象pathfile表示路径全名,根据输入的路径名加上文件名,调用了openFile方法,本实例指定需要打开或创建的文件为pathfile,文件以可读写的方式打开即Read_Access|Write_Access,并且指定文件打开或创建方法OpenOrCreate_Excl,实际程序内部为O_CREAT | O_EXCL即表示当指定文件不存在时,创建该文件,否则文件openFile调用方法出错。最后指定文件属性为普通文件,即内部实现拥有了可读写权限。

本实例实际运行时,根据提示传入相应的路径全名信息,随后调用openFile方法创建该文件,并通过ll –lst命令查看了新创建文件的权限为可读写。再次执行该程序创建同样文件时,根据设定会调用失败而直接返回。

3.Linux系统调用之文件读写操作
文件创建打开之后,自然需要通过写入以及读取方法来操作文件。Linux系统对于写文件与读文件提供了对应的write与read方法,该方法提供原型定义如下所示。


    #include <unistd.h>
    ssize_t  write(int fd,const void *buf,size_t count);
    ssize_t read(int fd,void *buf,size_t count);

使用系统调用文件读写方法需要包含常量符号定义的头文件unistd.h,才能正确调用涉及该头文件中常量符号的函数。实际上类型ssize_t在其头文件中定义为无符号整型,表明两个函数返回读写数据的字节数。两个方法操作成功时都会返回相应读写文件数据成功的字节数。

函数write主要用于往指定的缓冲区中写入count字节到描述符为fd的文件中,成功写入数据后该函数会返回实际写入数据的字节数,发生错误时会返回-1,相应的错误代码存放在errno中,可以在系统中查询到相关说明。

而函数read与之类似,只是数据走向的过程相反。read函数将指定文件描述符的文件读取count个字节数据,存放至指定的buf缓冲区中。调用该方法成功时返回同样为读取文件的实际字节数,错误则返回-1,错误代码存放入errno中可以查询帮助定位错误原因。

下面将会通过一个C++方法封装实现文件读写操作实例,来演示系统调用read与write函数的使用实际应用方式,同时提供C++封装相关操作的方法思路,实例代码编辑如下所示。


/**
 *低级读取文件操作接口封装.
 * @param       fd               文件描述符.
 * @param       buffer        读取操作缓冲区.
 * @param       len             读取文件数据长度.
 * @return 处理成功返回数据长度,否则返回-1
 */
int readFile(int fd, char* buffer, size_t len)
{
    if (fd < 0 || !buffer) //判断传入文件描述符与缓冲区是否有效
    {
        return -1;     //如果两者中之一不成功则返回-1
    }
    //调用系统文件读取方法,返回读取文件数据长度
    size_t readSize = read(fd, buffer, len);               
    if (readSize < 0) //判断读取文件是否成功
    {
        return -1; //如果读取文件数据长度小于0,表示不成功则返回-1
    }
    return readSize;   //否则返回读取成功的数据长度
}

/**
 *低级读取文件操作接口封装.
 * @param       fd               文件描述符.
 * @param       buffer        写入操作缓冲区.
 * @param       len             写入文件数据长度.
 * @return 处理成功返回0,否则返回-1
 */
int writeFile(int fd, char* buffer, size_t len)
{
       if (fd < 0 || !buffer)   //判断传入的文件描述符与缓冲区是否有效
       {
               return -1;        //如果两者之一存在不符合要求则返回-1
       }
       //定义变量分别表示最终写入数据长度、当前写入数据长度
       int writeSize=0, currWriteSize=0;  
       //判断最终写入数据变量是否超出要求写入数据的长度
       while(writeSize<len)
       {
           //如果没有,则写入len长度的数据
            currWriteSize= write(fd, buffer+writeSize, len-writeSize);  
            //判断写入成功的数据长度是否小于0        
            if(currWriteSize<0)                                                                    
            {
                 //如果写入不成功则打印其错误号
                cout<< "write file error,errno=" << errno <<endl;      
                    return -1;
             }
             //将当前累加的写入数据长度累加至最终写入数据长度变量
             writeSize+= currWriteSize;                          
       }
       return 0;
}

本实例主要采用C++封装实现了两个文件低级操作接口,分别为readFile与writeFile表示文件读取与写入操作功能。本实例接口主要配合上小节openFile方法配合使用,通过获取到指定文件的描述符fd来读取或者写入指定长度的数据,本实例演示由初学者可以配合上小节实例代码来实践验证。

封装实现的两个文件操作接口中,第一个readFile方法接口提供三个参数,参数fd表示指定文件的描述符,参数buffer为读取文件数据后存放的缓冲区,而参数len则表示读取文件数据的长度。该方法内部首先判断获取的文件描述符以及相应的缓冲区指针是否有效,有效后则调用系统提供的read方法,传入文件描述符,缓冲区指针以及相应指定的读取长度,读取文件中数据并返回真正读取的长度值放入readSize中,判断该读取到的数据长度,如果不小于0则直接返回已经读取的长度给调用者。

封装实现的writeFile写入文件数据方法,参数同样为三个,第一个为文件描述符fd,第二个为缓冲区指针,第三个为写入指定的数据长度。内部同样先判断文件描述符与缓冲区指针的有效性,之后定义两个整型变量,一个writeSize表示指定要写入文件数据的步长,另一个currWriteSize表示当前写入数据的长度。所后判断写入文件数据步长小于指定长度,那么调用系统调用write方法,该方法参数指定文件描述符,指定缓冲区从哪里开始将数据写入,并且指定写入多少长度数据。

返回写入数据存放到currWriteSize变量中,所后将已经写入的部分加到步长变量中继续再判断是否全部写入len长度数据,该方法成功返回0,否则返回-1。

4.Linux系统调用之文件关闭与删除操作
与前面介绍的Linux系统调用中创建与打开文件相对应的,系统调用提供了关闭与删除当前普通文件的操作函数。两个系统调用方法在Linux系统提供的原型如下所示。


    #include <unistd.h>
    int close(int fd);
    int unlink(char *pathname);

Linux系统中文件关闭使用close函数,该方法主要根据传入的参数来关闭指定文件描述符的文件。该函数调用将会返回一个整型值,如果关闭文件成功则会返回整型值0,如果关闭文件出错会返回-1或者一个错误变量值。该错误变量可以为EBADF表明关闭的文件描述符不是一个有效的描述符,EINTR表示方法close调用被一个信号中断,而EIO标志值则表明出现I/O错误。通过系统提供的基本错误标识,可以帮助诊断调用该方法出错时的原因。

而unlink函数用来从文件系统中删除已经存在不需要使用的普通文件,该方法根据输入参数指定的文件全名来删除指定文件。但是需要注意的是指定文件需要在当前文件link的进程数为0的情况下才会真正的删除。如果文件上依然存在进程链接数,只能等到最终链接数为0才会真正的删除文件。该方法同样成功删除文件返回0,否则返回-1或者相应的errno。其中系统针对unlink提供的errno相当的多,通过man 2 unlink可以查询到该方法具体使用以及errno说明。

同样下面依然会采用一个完整实例来演示关闭与删除文件系统调用在实际应用程序中的使用情况,通过关闭删除之前创建的文件来说明该方法的使用,实例代码编辑如下所示。


/**
 *关闭指定文件操作封装.
 * @param    fd   文件描述符.
 * @return 处理成功返回0,否则返回-1
 */
int closeFile(int fd)
{
    //直接判断文件描述符是否有效,调用关闭文件描述符方法
    if (fd < 0 || ::close(fd) < 0)
       {
               return -1;  //如果处理不成功直接返回-1
       }
       return 0; //否则返回0
}

/**
 *删除指定文件操作接口封装.
 * @param  path   文件路径全名.
 * @return 处理成功返回0,否则返回-1
 */
int deleteFile(const string& path)
{
    int result = 0;  //定义结果标记变量
    result = unlink(path.c_str());//直接根据传入的文件全名,调用unlink系统方法删除
    if (result < 0) //判断结果标记
    {
        return -1;  //如果返回的标记值小于0则表示删除失败,返回-1值
    }
    return0;       //成功则返回0
}

本实例同样针对系统调用提供的关闭以及删除文件的方法采用C++实现接口封装,该方法接口实现比较简单,内部通过判断传入的描述符以及获取调用系统调用的结果判断来实现相应的功能。

本实例演示同样可以配合上述小结的实例中封装的文件操作接口来实践,至此关于低级文件操作C++封装接口已经形成一个简单小规模的方法集。稍后将会在最后的综合实例中提供将这些封装的低级系统调用实现的C++应用接口封装成提供文件低级操作的类类型,在应用程序中连接使用,让初学者体会C++面向对象自定义类类型的优势。

5.Linux系统调用之文件控制操作
Linux系统针对指定的文件描述符作不同的控制操作,提供了名为fcntl系统调用函数。该方法将会根据传入的基本命令来操作控制指定描述符的文件。该系统调用方法原型定义如下所示。


    #include <unistd.h>
    #include <fcntl.h>
    int fcntl(int fd,int cmd);
    int fcntl(int fd,int cmd,long arg);
    int fcntl(int fd,int cmd,struct flock *lock);

该方法使用需要包含如上两个头文件定义,函数主要通过传入的cmd命令来操作控制指定的文件描述符fd。其中arg参数为根据cmd命令需要接受的第三个参数设定。其中cmd命令包含多个标志值定义,比如F_DUPFD表示返回一个最小的大于或者等于参数arg并且可用的描述符,该描述符复制现有的描述符;命令F_GETFD与F_SETFD表示获取与设置文件描述符的标志;命令F_GETFL与F_SETFL表示获取与设置文件状态标志;F_GETOWN与F_SETOEN命令表示获取与设置异步I/O的所有权;最后F_GETLK与F_SETLKW则表示获取与设置文件记录锁。

系统调用文件控制操作方法在C++应用程序处理文件中应用相当少,很少需要封装实现这样的底层操作。此处不在列举C++封装操作实例来演示该方法的使用,但是fcntl在文件锁方面的控制在应用程序中有着一定的应用,下面将会就fcntl方法针对文件锁操作部分作详细的讲述。

Search

    Table of Contents