大家好,在这一小节中呢,我们来看一下文件操作。 说起这个文件呢,大家可能会觉得,这是我们在 操作计算机的时候最熟悉,但是偶尔又觉得很陌生的一个概念。 那么我们在这一小节中呢就具体来看一看如何利用C++语言来处理文件的一些操作。 我们知道呢,我们所有的数据 在计算机上保存的时候它其实都是具有一定的这样层次化的结构的, 当然呢,我们一个数据在计算机实质上被保存的就是这样的一个个0,1的 比特位。它是每位这样存放的。 但是呢我们具体如果去处理每个比特位的话呢那么可能在很多时候 这个数据在构建的时候就会变得非常的繁琐,并且具有很强的不规律性。 所以我们进一步就把这8个比特位呢构成的称之为叫做字节。 那么每一个byte呢它对应描述了一定的内容, 而这些各个字节组成的一些具体的内容呢我们又把它称之为叫做域或者记录。 这里我们举了一个学生记录的例子。 在这个记录中间呢它就包含了每一个学生的学号, 姓名,年龄,以及它相应所在的班或者年纪的一个排位。 那么有了这样一系列的数据之后呢我们就会看到,其实我们会把所有的这些 数据的记录呢顺序的写入到了一个相应的文件里头。 那么我们就会把这样的一个文件称之为叫做顺序文件。 那么顺序文件它的本质呢实际上就是一个由 有限的字符构成的一个顺序的字符流,它其实就是一个 流的结构。那么我们之前呢 一直在使用C++中间这样一个流的读入和读出这样的一个操作, 那么文件呢实际上你也可以把它看成就是一个顺序的字符流。 所以呢在C++标准库中间的话呢,有相应的这样三个类, 分别是叫做ifstream,ofstream,和fstream 这样三个类,来去操作整个的文件。 那么我们就把这样的三个类呢称之为叫做文件流类。 那么文件流类本身呢对于这个ifstream来讲的话, 它主要就是用于将文件呢读取,从文件中间呢,去读取数据。 而ofstream呢它主要就是去实现将文件中呢写入数据。 那么相应的这个fstream呢它呢既可以用于从文件中间呢去读取数据, 又可应用于向文件中呢写入数据。 那么我们来具体看一下这样的三个类的相互关系。 我们可以用这样的一张图来进行表示。 那么可以看到呢,ifstream这个类 它呢是和这个fstream这个类都是由这个istream这个类 派生出来的。 所以我们说呢,本身这个istream这个类它所包含的成员函数呢 这个ifstream这个派生出来的新类呢也一样都包含。 同样的呢,对于ofstream来说呢,也是一样。 它和ofstream呢都是由这个ostream 来派生出来的。所以呢,在这样三类中间的一些成员函数比如说这个 operater 这个流提取符,还有相应的这个operater 流插入符,以及呢我们之前可能常用的一些像 getline,这样的函数呢它都是可以去进行使用的。 那么要注意呢,我们在做这个文件的操作的时候 一定呢,在做任何操作的时候都会要遵循这样的一个顺序。 首先第一步就是要去打开一个文件。 不管你是去使用这个文件,还是去创建一个新的文件, 都必须要这样。首先要先打开一个文件。在打开这个文件的过程中间呢, 需要去通过指定这样的一个文件名去建立呢文件本身和这个文件的流对象 直接的一个关联 的关系。那么同时呢还要去具体的指明这个文件的使用方式, 当然我们会在后面的这个iii中间具体的看到。 那么第二步呢就是去具体的读写这个文件。 我们会在每个文件当中呢有个这样的所谓读写指针 的这样一个指针,用来呢去做 对于这个文件当中相应的这个位置来进行一些读写的操作。 那么最后一步就记得说呢我们每创建或者使用一个文件之后, 无论对它是否进行了任何的操作,那么我们最后都要去进行关闭。我们必须把这个文件close起来 才可以。我们来具体看一下。在建立这个顺序文件的时候呢,我们通常会有这样的 一个操作。当然我们首先呢就是要去引入本身的这个 文件的处理的这个类,那么它呢就是fstream,保护这样的一个头文件, 那么我们通过这个fstream中间呢派生出来的这个 ofstream这个类呢去定义了它相应的一个对象outFile, 我们看到,ofstream呢实际上是在fstream中间定义的一个类, 那么outFile呢是可以看成是ofstream类的一个对应的具体的对象。 那么我们用这个对象具体的 怎么样去建立一个文件呢?我们要 在这个对应的参数里面去传递要建立的这个文件的这个 文件名,比方在这里呢,我们就要去建立一个叫做clients.dat的一个文件。 那么同时呢,我们还需要去定义这样的一个所谓叫做打开并建立文件的一个选项, 用来去描述以什么样的方式去处理这个文件。 在这里头呢,它可能会有一系列的这种选择项,比如在这个中间我们标注,ios::out 那么它就是表示呢输出的这个文件本身 它是要输出到这个文件并且呢要删除原有的一些内容。 那么如果你去写这个ios:app的话呢,就说 我们呢会要希望输出到这个文件,但是会要保留已有 的内容。那么新加入的内容呢,就写在这个文件的尾部。 同时呢,你还可以去定义说我是以什么样的形式去打开这个文件。 比方在我们这个例子中间呢,我们希望它是以 二进制的形式来打开具体这个文件。那么我们就要去写ios::binary 这样的一个标记。 除了刚才那样的方式, 就是去定义一个对应的 ofstream的一个对象,然后呢通过去初始化传递参数建立 相应的这个文件的话呢,我们还有第二种方式,第二种方式呢 我们可以直接首先这样显示的去定义一个对象, 直接去创建一个对象。有了这个对象之后呢,我们利用 这样的open的一个函数作用在fout这样的 一个对象上,用open函数本身呢去打开相应的我们 需要操作的这个文件。这里呢,我们可能需要去操作 test.out这样的一个文件。那么注意就是我们呢 去创建打开一个文件的时候我们通常需要去判定一下 它是否能够成功的打开,如果呢 这个fout本身呢它没有成功打开的话,那么我们就需要去 输出一个提示说,File open error!这通常是我们用来去做文件操作的一个基本的环节。 那么另外还要注意的就是我们在这里头给出的这个文件名, 那当然在这两个例子里头给出的都是一个相对的文件的一个名称, 那么你也可以给出一个绝对的路径,既可以是绝对的路径,也可以是相对的一个路径。那么如果没有去 交代路径信息的话,在现在我们这两个例子中间,那么你就在当前的这个文件夹下面去找相应的文件就可以了。 我们刚才提到说,在第二步,文件已经被打开之后, 那么我们在对这个文件进行读写的时候呢,实际上会存在这样的一个叫做读写的指针, 我们通过对输入的文件呢有一个读指针, 或是对于一个输出的文件有一个写指针。 那么对于既有输入又有输出的这样一个文件的话呢 我们需要呢去建立一个读写指针。 通过利用这个指针呢,就可以标识当前这个文件操作的一个位置。 那么指针指到哪里,我们的读写操作呢就在哪里来进行。 我们来看一下,我们在文件读写当中如何去利用指针来进行相应的操作。 比方在这个一系列的代码中间呢,我们就可以看到, 我们首先呢就是去打开了一个文件,a1.out, 我们去定义了一个ofstream类型的一个对象fout 那么之后呢,我们就去要用这样的一个函数呢叫做tellp 我们去获得了对应这个写指针的一个位置。我们利用tellp这个函数作为一个fout 这个对象上,那么获得的相应的这个location他就是对应的这个写指针的一个位置 那么有了这样的一个location之后呢,我们呢也 可以对它进行相应的赋值,至于这也location呢,它是属于一个long类型的 所以我们在对它进行赋值的时候,给出的相应的这个常量值后面要标记一下L 那么有了相应的这个指针位置的时候,我们就 可以在对应的这个文件当中去做一系列这样的一个操作 比方说呢,在这样的一个语句中间,我们可以看到我们想做的事呢是将这个指针 移动到我这个location所指定的位置上。在这个例子当中呢 应该我们对location赋值为10lL,我一呢我们就是把它相应的移动 第十个字节的地方。那么我们记着,我们对指针进行操作的时候可以标用这样的一个函数 叫做seekp,seekp到某一个位置上 那么同样的,出了具体去指定这个位置 之外呢,我们还可以去指定这个指针的位置相应是从哪里来 开始计数的。比方说我们可以去定义他 这个起始位置呢是从头开始,那么我们就可以在这个seekp 后面的这个指针,除了具体的 指针的这个位置数值之外呢,去我们还可以定义它相应的起始点 在这个例子当中呢ios::beg就 说明它是从beginning开始的,是从相应的这个文件头开始数起的 那么你也可以呢 从这个count位置,也就是标志ios:: cur呢就可以指出我相应的这个位置呢,相应的 我要去做的这个指针的操作是从当前这个location开始偏移 具体的这个 location值就可以了。那么同样呢,你也可以从 末位开始数起。那么只要用ios::end就可以了 那么注意,location本身呢可以为负值的。这样也保证呢我们可以从末位开始数起 那么同理,我们干才讲到的是写指针 那么我们对于读指针而言呢,为I时还有完全一样的这个操作 只不过刚才呢是这个tellp,那么在读指针中间的话呢,我们就要 tellg,但是注意一点就是,我们在调用tellg的时候呢要用 他的这个对象fin,那么他呢是这个ifstream的一个类的 一个具体地识别化我们要调用这个不同的类 来声明相应的这个对象,才能获取相应的这个 读写指针。那么在读指针的位置上之后呢我们就要去 使用这个ifstream的类具体的识别化 那么同理呢也是一样,我们可以定义一系列的这个seekg的一个 操作。那么这个操作呢他都是去作用在这个fin 这样的一个对象上面的 那么我们对二进制文件来进行读写的时候呢 我们有了相应的这个指针的位置的指向之后 我们就可以进一步的来去做事。比方说我们有了一个写指针具体位置的一个 定义。我们从beginning开始 希望呢写指针呢从beginning开始之后的20个字节位置开始写起 那么具体怎么写,我们就可以去 调用相应的这个成员函数write去把相应的我们需要写入的这部分 比如说某一个字串,那么就具体写进去通过这个 参数传递进来写进去,而具体写的大小呢可以是 对应的这个sizeof(int)这样的一个大小 那么同理呢,我们也可以用这个 read的这个函数去去读相应的这个文件 那么相应的这个文件的话呢,我们就可以去利用这个 seekg这个函数去获得这个读指针相应的 位置,那么在这个例子里面呢,我们从文件的头开始 然后依次呢去读出相应的我们需要看到的数据 那么要注意一点就是二进制文件的读写呢,他 本身是直接去写或者读二进制数据的 那么如果我们写入一个二进制数据的时候呢,你如果直接拿记事本来看呢,有些时候是未必正确的 我们来看一个具体的例子 我们呢希望利用这个下面的程序使得从键盘 键入的几个学生的姓名和成绩呢以二进制的形式 把文件呢,就是以二进制文件的形式呢来进行存放 那么我们相应的就去include这个iostream fstream以及cstring 那么完了之后呢我们就定义了这样的一个类叫做cstudent 那么cstudent这个类呢,它很简单。它就包含了两个成员 的变量。一个是szname,一个放姓名的一个 字符型的数组,完了呢相应的是存放他相应每个学生的一个成绩nscore 我们看一下,我们如何最简单的去 写一个这样的一个学生姓名和成绩的这样的一个文件 我们同样呢在main函数里面呢定义了一个cstudent对象s 那么我们接着呢就要去做打开文件这个操作 那么在这里头呢我们是去利用定义了一个ofstream类型的一个对象outfile 那么在outfile初始化的时候呢去传递了相应的文件名以及他相应的这个打开的 形式 那么它实际上就是一个在c盘目录下存放的 pap文件的一个叫student.pap的一个文件 那么这个文件本身呢他的这个 我们写入的形式呢是以,注意,是以二进制形式来进行写入的 那么有了这样的一个文件打开的操作之后呢,我们就可以从外界 键入这个相应的学生的姓名和成绩。我们在while循环当中呢 每一次去输入相应的这个学生的姓名和成绩 一旦输入成功之后呢,就近到这个while循环里面去。当然你中间还要去 判断一步因为我们最后希望我们的这个程序呢,是以有了这个exit的字符 来进行终止。所以我们就要在这里判断一步,看看这个 stncmp这个函数,用这个函数去看一看 s.szName本身是不是跟exit相 一致的。如果是一致的话呢,则表 明说我这个文件的输入已经结束了,那么我们就break了 那么否则的话我们每一次传递进来的这个szName和这个 nScore之后呢,那么我会相应的把这个对应的这个对象 写入到我刚才打开的这个文件里面去。我利用的就是这个OutFile的对象 调用它相应的这个write函数 那么我写入的大小是什么呢,我写入这个一个相应的字串。那么这个字串它实际上就是 s这个对象的引用,那么具体的大小呢,我们可以sizeof(s)一下 那么有了这样一系列的输入之后直到我们到了exit这个字串 通过比较之后发现说这个,诶,程序已经终止 那么我们就可以再调用OutFile.close 这样的一个函数,去关闭相应我们刚刚打开的这个students.pap的这个file 有了这样的一个操作之后呢,整个文件的这个写入过程就结束了 我们看到,我们的输入如果是这样的。 四行,分别是Tom 60, Jack 80, Jane 40,以及最后的exit 0 那么有了这样的一个有一系列输入之后呢,我们通过刚才那个程序就可以相应的 把这一系列的输入写入到我自己定义的这样的一个文件当中 当然注意,因为呢写入的这个形式是以二进制的形式来写入的 所以如果呢,你拿这个记事本打开的话,你会发现一系列的这个 看起来比较乱码的一个形式 那么在这一头呢我们也是要解释一下,对于一个文本文件和二进制文件来讲的话 那么他打开文件的时候在某些时候是会有区别的 那么如果他在Uriz或者Lirux下,那么这两者呢是没有去别的 但是在Windows情况的平台下,那么每一个文本的文件它是以这个 |r和|n,这样两个转义字符来作为换行符的 那么当你读出的时候,系统呢会自动的将这个 0x0d0a,将这个十六进制的这个0d0a自动转换,读成一个0a 那么同时写入的时候呢也会相应的对于这个0a 这样的一个值呢,系统会自动地认为他变成了一个换行符,所以会在前面 添加一个iiii,所以因为有了这样的操作呢可能会导致我们的二进制文件本身 如果以文本形式打开呢显示会和你的预期呢有一定的差别, 所以我们说呢我们对二进制文件来进行读写的时候呢,最好是能够保持一致, 也就是说以二进制的形式去写入那么就以二进制的形式来读出, 那么有了刚才的那个写入的操作之后呢如果我们想具体看一看这个student. dat文件当中的存放 到底你写进去了哪些内容的话呢?我们就可以自己写一个这样二进制文件读取的一个程序, 除了刚才这个cstudent类之外呢那么我们main函数里头 也同样的确定一个ifstream类的一个对象, inFile啊这个file本身呢我们定义,它是以这个二进制形式来进行读取的 当然我们还要来判断一下说这个本身打开这个文件是否成功啊, 而如果本身这个打开的操作呢是有问题的是否的, 那我们就要相应的cout一个error, 如果没有任何问题呢那么我们就去相应的去调用这个 inFile.read这个函数,那么把对应这个和S这个大小 相应的内容呢把它读取出来那么在这个屏幕上呢 显示出来,也就是说,我们去cout s.szName 和 s.score 那么注意就在这里头呢我们还额外展示一个语句就是说除了呢 inFile除了去调用read这个函数之外呢,我还可以去调用相应的这个 gcount这样的一个函数,那么这个函数作用在inFile这个对象上,那么你就可以看到说我刚刚 读了多少个字节,当然这个本身这个语句对我整个这个程序没有任何的这个影响,我们在这里头只是 就简单的说明一下,啊最后呢有了这一系列的这个读出的操作结束之后, 注意一定要去close相应的这样file 那么就是inFile.close. 那么有了这样的一个程序之后,我们就可以相应的把刚才一系列的输入 读取出来就会看到,这样对应的二进制读出方式呢是没有任何问题的 那么我们在两个例子当中呢又看到我们如何对一个文件进行二进制 形式的一个写入或者是对一个文件具体以二进制的形式来进行读出, 那么在很多时候呢我们通常会对一个文件同时要进行这个读或者写的操作 那么在这种情况下我们来看第三个例子, 我们在这个程序当中呢希望实现把这个之前这个student.dat文件中间保存的 第三个学生的名字Jane呢改为Mike, 那么这就会需要呢我们对相应的student.dat 文件呢进行一个写入的操作, 同时呢我们还要希望去 review一下我们写入的这个结果是否正确也就是说把相应 我们修改后的文件呢来进行一个读出的操作 那么就要求呢我们对这个二进制文件呢要同时进行写或者读的操作, 那么来看一下具体的程序实现, 还是刚才那个cstudent类啊,而这个类里面的还是包含了相应的学生名以及他的分数, 我们用cstudent类呢去定义了一个对象S, 那么有了这个s之后呢,我们又定义了相应的 这个fstream类型的一个对象称为叫做iofile啊, 这个iofile呢它不同于刚才的两个例子当中的 具体是这个ifstream的对象,或者是ofstream的对象, 它是这个fstream的对象,那么这个iofile本身呢,这个对象它是同时可以进行读或者写操作的, 那么它用来操作的文件呢还是student.dat这个文件, 那么我们看到它可以读可以写, 然后呢可以呢是以这个二进制的形式来进行操作的, 那么同样我们也去判断一下这个文件的打开是否会有问题, 如果没有问题之后呢那么我们就具体看一下说我们的这个 相应的写指针,啊我们第一步当然是去判断我对应 需要去修改Mike的那个位置,所以我们都要去seekp 这个函数呢在iofile这个对象身上,那么它 通过去适用这个函数实现这个效果呢是我希望能够从文件的头开始 数过两个这样student,这样cstudenta这样s的这样一个记录呃, 那么也就是让我们的写指针对应定位在这个第三个记录的起始位置, 那么当你一旦确定了相应的写指针的位置之后呢我就可以调用 相应的write函数,那么我们write函数写入的字串呢就是我们希望 去新写入Mike这个字串, 那么有了这个写入之后呢我还希望看一看说 我的整个的这个写入过程是否有效或者说写入的是否正确, 那么我希望能够去读取整个新的这个student.dat, 那么我就需呢再去用iofile. seekg啊我们需要去定位这个读指针, 那么这个读指针从哪里开始呢,我们就从这个文件的开始, 那么填一量呢是0, 那么有了这样一个读指针的确定位置之后呢,我就可以相应的去调用这个read函数, 那么依次地把我对应的这个文件当中的所有的这个cstudent类型的对象 s的这样一个记录呢依次地读取出来, 那么在读取之后呢我就把它们相应的cout在屏幕上啊 分别是cout, s.szName 以及 s.nScore, 那么有了这样的一系列操作之后呢,全部读取完毕之后,那么我就记着一定要 这个不管是这个是同时可以读取的文件还是说读文件或者写文件, 最后都要去做这个Close的操作,那么就iofile.close, 那么有了这样的一个程序的执行之后呢 我们就会看到刚才我们在student.dat文件里面所记录的 Jane的记录呢中间的姓名就会被修改为Mike。 那么这里呢我们强调一下,我们呢 不管是处理什么样的文件是写还是读啊我们都要最后 去显示关闭这个文件,因为如果文件呢不能够 正常关闭的话呢可能会引起写入内容呢产生一些问题, 所以呢我们要在每一个这个呃 写入的文件或者是读出的文件当中呢都让它,最后都要去写这个 fin.close 或者是fout.close. 我们最后来看一个例子啊,我们希望完成的一个工作呢是说 我们是去自己实现一个mycopy的一个程序,那么它能够完成的工作是什么呢, 我们可以去调用mycopy这个程序之后实现把这个原文件拷贝 到这个目标文件中间去啊,也就是说去把src.dat 拷到这个dest.dat的这个文件当中去, 那么如果dest.dat呢本身里面是有数据的话呢,那么原来这个数据文件呢会被覆盖掉, 那么有了这样一个呃希望能够实现的 一个目标之后我们来具体看一看这个程序是怎么样去实现的, 我们之前呢在第一节就郭老师在很久远的第一讲当中有给大家介绍这个 命令行程序的一个写法,我们知道说呢每一个main函数呢后面实际上它对应是由 两个参数的,它分别去记录你的这个命令行这个操作的时候 相应的这个参数的个数以及相应的这个参数的字串, 那么在这个程序当中呢,我们就希望能够在命令行参数中间去输入这样的 一个语句,那么它就可以直接实现这样的一个拷贝的工作, 那么我们在这个main函数里面当然首先第一步就要去判断一下 我的这个命令时候正确,如果我输进来的这个参数 不止三个或者是少于三个,也就是说不符合我这个对应的语句的话,那么我就会报错,file name missing, 那么如果通过正常的输入了这个对应的呃我们这个指令之后, 命令行参数之后呢,那么我首先要做的第一件事 就是去打开我用来去读取的这个文件啊,我们要去 首先要去读相应的文件,那么我当然知道要去定义这个ifstream类这么一个对象, 这个inFile啊,那么具体这个file的名称是什么呢就是我命令行参数传进来的这个 第二个参数也就是这个argv[1]啊, 那么下面相应的我说以这个二进制形式来进行读取的, 那么同理我也呢对这个要写入的这个文件 也要进行打开的操作,我必须同时去打开相应的这个写文件 才能狗把这个相应的我要拷贝的内容复制进去啊所以呢我在这里头呢就 用ofstream这个类去定义了相应的这个对象outFile, 那么outFile它打开的文件名是什么呢,是我传进来的第三个参数啊argv[2], 那么它也是以二进制的形式来进行传递的, 那么有了这样的两步这个输入和输出相应的这个文件的这个打开之后 那么我们就可以做依次进行拷贝的这样一个工作啊,我们定义了一个对应的char类型的一个字符c, 那么我在这个程序当中呢,每一次都会让这个 inFile呢去调用它相应的这个操作的函数get啊, 这个get它的作用呢就是去读取一个字符,啊读取一个c, 之后呢再把这个c对应地写到这个outFile.put里面,去调用 put这个函数,相应地把每一个字符呢写进去,那么依次呢这样的循环直到 这个文件当中呢所有字符被写入为止,那么有了这样的一个操作完了之后呢, 我就可以去相应地去close掉我的这个outfile和这个 inFile啊,有了这样两步操作呢那么我们就整个的这个 文件读写过程呢就能够完成,那么也可以相应的实现 将我的这个source.dat拷贝给这个desternation.的这个dat的作用, 那么要注意呢,我们在这个例子当中呢只是为了给大家描述一下我们这个文件去做打开, 然后去做拷贝,并且最后把它关闭这样的一个过程,当然我们在程序设计的时候呢,实际当中你根本不可能 去写对于每一个文件的拷贝只每一次去拷贝这样一个字符,当大家可以回去想一想说 我应该以什么样的方式去做这个文件拷贝的工作 会看起来会更有效一些