【C/C++语言入门篇】系列(12)-- 文件操作【上篇】

版权声明:此文章转载自51CTO

原文链接:http://masefee.blog.51cto.com/1737284/813993

如需转载请联系听云College团队成员小尹 邮箱:yinhy#tingyun.com

最近实在是太忙了,这篇整整就推迟了1个月了,实在是对不起。之前本打算这个模块就结束了,文件操作就不写了,但是文件操作又是一个很重要的东西,而且也刚好能够总结之前我们学习的所有知识。同时也为了将文件操作这个初学者认为很神秘的东西给本质化。因此,本篇将逐一介绍C语言的文件操作。(本模块的命名本来是想C/C++一块儿讲解的,但是由于工作、毕业论文、业余时间的充电、还有要完成那个未知的梦等,因此因为时间问题C++就只能放在以后有机会再写了,因此本篇将是本模块的最后一篇,之后将不会再连载了,请大家谅解。)

好了,回到正题,先来看文件操作中的文件。所谓文件(file)一般指存储在外部介质上数据的集合,比如我们经常使用的mp3、mp4、txt、bmp、jpg、exe、rmvb等等。这些文件各有各的用途,我们通常将它们存放在磁盘或者可移动盘等介质中。那么,为什么这里面又有这么多种格式的文件呢?原因很简单,它们各有各的用途,区分就在于这些文件里面存放的数据集合所遵循的存储规则不一样。举个例子比如bmp图片文件,为什么他能够表示一张图片,因为它有固定的格式,哪一段到哪一段,哪个偏移到哪个偏移应该存放什么数据是规定好了的。比如有文件头,一般是一个结构体,存放的文件的一些信息,如图片的大小,像素等等。再后来有数据区。然后我们要显示一张图片,就只需要按照前面所说的规则将文件头结构和数据块读出来,然后将这些数据在屏幕上用颜色表示出来,就成了一张图片。其它文件格式也类似。 

这里要说一个更重要的例子,对我们理解文件有好处。那么这个文件就是exe文件(这里只讨论windows平台),通常我们认为它是一个可执行程序,这无疑是增加了它的神秘度。从本质上来讲exe无非是一种固定的文件格式罢了。既然这样,它就有一套自己的存储规则。跟前面的图片文件一样有规则。此时,你可能会问:你这么说那我就可以纯手工(直接填写数据填充文件)写出一个exe可执行文件了? 面对你这个问题,我只能说你已经习惯思考了,已经习惯给自己提问了,已经很聪明了。那么答案是肯定的,你完全可以用一个编辑器直接填写数据写出一个helloworld.exe文件或者helloworld.dll文件。因为这些具有一定格式规则的文件一般是二进制存储的,于是我们可以用一个二进制编辑器新建一个二进制文件,然后向里面填写数据。然后双击运行输出“helloworld”字符串。你可能会觉得很有成就感,我之前就写过一个exe和dll。这里exe和dll的文件格式也就是著名的PE文件格式。有兴趣你可以去查阅相关资料,此非本文重点。 

总结上面的认识,文件无非就是一段数据的集合,这些数据可以是有规则的集合,也可以是无序的集合。操作系统也就是以文件为单位对数据进行管理的。也就是说,要访问外部介质上的数据,必须先按照文件名进行查找,然后从该文件中读取数据。要想写数据到外部介质,必须得建立一个文件,然后再写入。因此,这样来看,你眼前的文件将是一堆一堆数据而已,也没有什么类型文件之分了,类型只是为了区分而已,假如你把一个exe文件的扩展名改为txt,把它用记事本打开,同样是可行的,只是会执行exe文件里面的东西而已。(这里又不得不提到一点,如果你是一名程序员或者爱好者,那么你不应该将你的文件扩展名给隐藏了,要让它显示出来,如果你隐藏了,无非是增加了它的神秘感,同时在文件操作上不方便。通过上面的本质,我相信你能体会到我为什么这么说。) 

说到这里,你应该知道文件是什么了,那么再来看二进制文件和ASCII文本文件,为什么要分为这两种呢?

首先、文本文件方式存储多用于我们需要明显知道文件里面的内容时,比如ini、h、c等文件都是文本文件,这种文件存储的是字符(ASCII码),比如一个整数10000,类型是short,占2字节,存储文本形式将占用5个字节,一共5个字符。你可以想想更多的例子,体会文本文件方便之处(提示:这里的文本文件不是说是txt文件,而是指所有以文本格式存储的文件。)

其次、二进制文件方式多用于直接将内存里面的数据形式原封不动存放到文件里,比如上面的short 10000,在内存中占2字节,存储内容为10000的二进制数,存到文件后还是占2字节,内容也是10000的二进制。这种方式可以整块数据一块儿存储,同时还可以将内存数据映射到文件里。

由上面两点,C语言操作文件可以是字节流或者二进制流。它把数据看成是一连串字符(字节),而不需要考虑边界。C语言对文件的存取是以字节为单位的。输入输出的数据流的开始和结束仅受程序控制而不受物理符号(如回车换行符)控制。这种文件通常称为流式文件,大大增加了灵活性。我们可以产生很多自己的文件格式,在游戏程序里面,用得比较多的就是资源包的格式,一般就是自定义的存取规则。我之前也写了一个包文件,存取只需要遵循规则,原理是非常简单的。大家可以试试在脑子里面构造一个包文件。

在ANSI C标准中,使用的是“缓冲文件系统”。所谓缓冲文件系统指系统自动地在内存区为每一个正在使用的文件名开辟一个缓冲区,从内存向磁盘输出数据必须先送到内存中的缓冲区,装满后再一起送到磁盘去。反向也是如此。这里需要说明两个词:“输入”“输出”。输入表示从文件里读数据到程序里,输出表示从程序里写数据到文件中。 

了解了文件及文件存储形式,下面该正式进入文件的读写了,不要太激动,还是慢慢来。细节往往决定成败。在缓冲文件系统中,有一个很重要的一个东西就是文件指针,每个使用的文件都会在内存中开辟一个区,用于存放文件的有关信息,这些文件信息就保存在一个结构体变量中的,这个结构体是由系统定义的,名为FILE,先来看看VC2005在stdio.h下FILE结构体的定义:

struct _iobuf
{
        char *_ptr;               // 指向buffer中第一个未读的字节       
        int   _cnt;                 // 记录剩余未读字节的个数
        char *_base;           // 指向一个字符数组,即这个文件的缓冲
        int   _flag;                // FILE结构所代表的打开文件的一些属性
        int   _file;                 // 用于获取文件描述,可以使用fileno函数获得此文件的句柄。
        int   _charbuf;          // 单字节的缓冲,即缓冲大小仅为1个字节,如果为单字节缓冲,_base将无效
        int   _bufsiz;            // 记录这个缓冲的大小
        char *_tmpfname;    // temporary file (i.e., one created by tmpfile()
                                        // call). delete, if necessary (don't have to on
                                        // Windows NT because it was done by the system when
                                        // the handle was closed). also, free up the heap
                                        // block holding the pathname.
};
typedef struct _iobuf FILE;

好了,上面的结构体就是这样定义的。这里不得不再次提到缓冲: 

1.png

上面结构体中的_flag就标记了缓冲的信息(我们关心这三个):

#define _IOYOURBUF  0x0100      // 使用用户通过setbuf提供的buffer
#define _IOMYBUF      0x0008      // 这个文件使用内部的缓冲 
#define _IONBF          0x0004      // 无缓冲模式
#define _IOLBF           0x0040      // 行缓冲模式
#define _IOFBF           0x0000      // 全缓冲模式
 
同时,_flag也标记了读写模式,比如"r+"、"w+"等。
#define _IOREAD         0x0001    // 只读
#define _IOWRT          0x0002    // 只写
#define _IORW            0x0080    // 可读可写

上面的3中模式就是"r"、"w"、"+"任意组合起来表示的意思。

正因为使用缓冲模式,是为了避免频繁的系统调用开销,有了缓冲就不需要每次都访问实际的文件。当然缓冲也会带来隐患,比如写文件时,先是到缓冲,如果此时系统崩溃或者进程意外退出时,有可能导致文件数据的丢失。因此C语言提供了几个基本的函数,弥补缓冲带来的问题:

int fflush( FILE* stream )  // flush指定文件的缓冲,若参数为NULL,则flush所有文件的缓冲。
int setvbuf( FILE *stream, char* buf,  int mode, size_t size )  // 设定缓冲类型,如上面的表格。
void setbuf( FILE* stream,  char* buf )  // 设置文件的缓冲,等价于( void )setvbuf( stream, buf, _IOFBF, BUFSIZ ).

所谓flush一个缓冲,是指对写缓冲而言,将缓冲内的数据全部写入实际的文件,并将缓冲清空,这样可以保证文件处于最新的状态。之所以需要flush,是因为写缓冲使得文件处于一种不同步的状态,逻辑上一些数据已经写入了文件,但实际上这些数据仍然在缓冲中,如果此时程序意外地退出(发生异常或断电等),那么缓冲里的数据将没有机会写入文件。flush可以在一定程度上避免这样的情况发生。

在这个表中我们还能看到C语言支持两种缓冲,即行缓冲(Line Buffer)和全缓冲(Full Buffer)。全缓冲是经典的缓冲形式,除了用户手动调用fflush外,仅当缓冲满的时候,缓冲才会被自动flush掉。而行缓冲则比较特殊,这种缓冲仅用于文本文件,在输入输出遇到一个换行符时,缓冲就会被自动flush,因此叫行缓冲。 

终于把概念性的东西和准备步骤做完了,下面该看看具体的读写文件了。有了前面的准备工作,读写文件将不是难事了,因为有现成的库函数供我们使用,我们下面的段落将是如何使用这些库函数和一些注意事项而已了。 

首先看如何打开文件,先看代码:

#include <stdio.h>
int main( void )
{
    FILE* pReadFile = fopen( "E://mytest.txt", "r" );   // 打开文件
    if ( pReadFile == NULL )
        return 0;
 
    fclose( pReadFile );     // 关闭文件
 
    return 0;
}

上面的这段代码,只是一个简单的打开文件,如果成功打开后直接关闭。这里打开的是一文本文件,是以只读的方式打开。使用fopen函数打开,第一个参数是文件路径,第二个参数是读写模式,返回值为0表示打开失败。先看看读写模式:

2.png

一、读写字符

C语言为从文件中读写一个字符提供了两个函数:

int __cdecl fgetc( FILE* stream );              // 从文件读入一个字符
int __cdecl fputc( int ch, FILE* stream );   // 写入一个字符到文件

看例子:

#include <stdio.h> 
int main( void )
{
    char cInput;
    FILE* pReadFile = fopen( "E://mytest.txt", "r" );   // 打开文件
    if ( pReadFile == NULL )
        return 0;
 
    while ( ( cInput = fgetc( pReadFile ) ) != EOF )   // 从文件读入一个字符,如果到文件尾部,则返回EOF(-1)
        printf( "%c", cInput );
 
    fclose( pReadFile );     // 关闭文件
    return 0;
}

假如mytest.txt文件的内容是:

masefee
hello
world

三行,那么我们逐个读入每个字符,直到EOF结束,EOF很简单,其实就是#define EOF (-1),WINDOWS为了能够返回失败为-1,因此fgetc的返回值使用是int类型。同时-1也不是某个字符的ASCII,所以不影响,一举两得。上面程序while循环不断从文件中读取单个字符,遇到换行符(WINDOWS下回车符('/r')为13, 换行符('/n')为10),printf输出后变处理成换行符了,因此文件里面3行,逐个读入程序里在终端显示后还是3行。代码很简单,就不用多说了。这里需要提到一点:

问题一:当第一次执行了fgetc后,我们看看pReadFile指针里面的内容与刚执行了fopen函数后的内容有所变化,为什么?

再来看fputc函数:

#include <stdio.h>
 
int main( void )
{
    int i = 0;
    char szOutput[ 32 ] = "masefee/nhello";
    FILE* pWriteFile = fopen( "E://mytest.txt", "w" );   // 打开文件
    if ( pWriteFile == NULL )
        return 0;
 
    while ( szOutput[ i ] != 0 )
    {
        fputc( szOutput[ i ], pWriteFile );    // 写入一个字符到文件
        i++;
    }
 
    fclose( pWriteFile );     // 关闭文件
    return 0;
}

我特意在szOutput数组里写了一个'/n'字符,此字符就是换行符newline,意图是当输出到e之后,便输出一个换行符,让字符串换行。因此最终mytest.txt文件里面的内容如下:

masefee
hello

到这里,你可能会想到第一个fgetc的例子是我们预先在文件中输入3行字符,然后读入到程序中。我们在用记事本输入3行文本的时候,每当换行的时候我们敲键盘是按的回车。

问题二:既然我们敲的是回车,为什么在文件里存储的是'/n'而不是'/r'?

同时,到这里想到第一个问题,我们又来观察一下,当刚使用fopen函数时,pWriteFile里面的内容是:

pWriteFile          0x00437bb0
_ptr                   0x00000000
_cnt                   0 
_base                0x00000000
_flag                  2 
_file                   3
_charbuf            0 
_bufsiz              0 
_tmpfname       0x00000000

而执行了fputs函数,到换行符后我们再看pWriteFile里面的内容:

pWriteFile          0x00437bb0
_ptr                   0x00385019
_cnt                   4087
_base                0x00385010
_flag                  10
_file                   3
_charbuf            0 
_bufsiz              4096
_tmpfname       0x00000000

然后我们再看看_base所在内存的值:

6d 61 73 65 66 65 65 0a 68
 m  a   s   e    f   e    e  /n  h

从这个现象我们能够意识到,FILE结构里面_base所指向的缓冲区,_cnt表示还剩下多少个字节没有写。还可以意识到,我们在不设置任何参数时,默认情况下是采用的全缓冲模式,填充4096字节后自动会写入到文件,在这里我们没有那么多字节,因此在fclose函数执行后,文件里便写入了值。你可以打断点在fclose上,等程序断下来后,观察你磁盘里面的mytest.txt是空的,当执行了fclose后大小就变了。这也能体现缓冲区的一个现象。

同样,如果你想立即将缓冲区的数据写到文件里,可以在fclose函数前面加上:

fflush( pWriteFile );

当执行完此函数后,数据便写进了文件,最后再关闭文件。

想阅读更多技术文章,请访问听云技术博客,访问听云官方网站感受更多应用性能优化魔力。

关于作者

郝淼emily

重新开始,从心开始

我要评论

评论请先登录,或注册