0?wx_fmt=jpeg


内容简介

1、第一部分第十课:文件读写,海阔凭鱼跃

2、第一部分第十一课预告:小练习,猜单词



文件读写,海阔凭鱼跃


上一课《【C++探索之旅】第一部分第九课:数组威武,动静合一》中,我们学习了动态数组和静态数组,也看到其实字符串很类似字符数组(到了之后的第二部分,学习面向对象,我们会知道其实string是一个类)。


到目前为止,我们写的程序还比较简单,当然了,因为我们刚开始学习C++嘛。但只要加以训练,我们就慢慢地能够写一些真正的应用了。我们也开始逐渐了解C++的基础知识了,不过缺了很重要的一环:与文件交互。


我们已经学会如何将信息输出到控制台(console)以及如何提取用户在控制台中输入的数据(使用cin和cout)。但是,我们岂能就此罢休。想想我们 之前介绍过的一些程序,例如:记事本,一些IDE(VS, CodeBlocks, xCode, Eclipse, etc),绘图软件,等等,都能够读写文件。


在游戏领域就更是如此啦(我知道一帮宅男已经激动了):游戏里的数据要保存,游戏的图片,音乐,道具,等等。都需要存档。


总之,如果一个软件不会与文件交互,那么它的功能是比较有限的。


因此,一起来学习如何读写文件吧。你会发现,如果你掌握了cin和cout的用法,那其实你已经知道大半啦。



写入文件


我们要读写文件,首先需要打开文件。就好像平时我们要记笔记一样,你总得先打开笔记本吧,才能阅读内容,或者往里面写东西。


一旦文件被打开之后,接下来的操作就很类似之前用cin和cout来进行标准输入和输出了。我们又会与老朋友<<和>>见面。


用术语来说,我们会将一个程序和外界的通信方式用"流"来描述。流,英语是stream。记得吗?我们要使用cin和cout,需要用


#include <iostream>


因为cin和cout定义在iostream这个C++的标准库中。而这里的iostream就是input output stream的缩写,表示"输入输出流"。所以,其实我们早就在不知不觉地接触流的概念了。


在这一章中,我们要和文件交互,那么就需要文件流来帮忙了。聪明如你一定想到了,是的,文件的英语是file,那么文件流就是file stream。是不是很简单呢?


因此,我们需要用到fstream这个标准库,fstream就是file stream的缩写。


当然了,如果你不是参加一个程序员的派对,也不需要显得很专业,那么说"读写文件"就可以了。


fstream头文件


在C++中,我们要使用一个功能,需要引入合适的头文件。因此,我们在程序一开始须要这样做:


#include <fstream>


接下来,我们就学习如何创建一个文件流,以便我们能读写文件。


以写模式打开文件


流其实是对象,还记得我们说C++是一门面向对象的语言吗?当然我们现在还不深究,要到第二部分讲面向对象编程时才会畅聊类和对象。暂时只需要知道这些流其实都是C++的对象(不是找对象的对象,少年你想多了)。


也完全无需害怕,因为我们之后还会不断提到流。暂时,只需把其看作比较高级的变量就可以了。这些文件流包含了文件的很多信息,提供给我们很多功能,例如可以关闭文件,在文件中移动,等等。


你 会看到,声明一个流的对象,其实就和我们声明变量一样简单。首先,我们来看看如何创建用于写文件的流,须要用到ofstream,也就是output file stream,因为是从程序向文件输入数据,因此对于程序来说是"出去"的流,因此是output(输出),而不是input(输入)。


话休絮烦。翠花,上"栗子":


#include <iostream>
#include <fstream>
using namespace std;

int main()
{   
    ofstream myStream("C:/Cpp/Files/scores.txt");   
    //声明用于写入文件的流,
    //此文件是在C盘下的Cpp文件夹的子文件夹Files中的scores.txt文件
    
    return 0;
}


在上面程序中,我在myStream后面的括号中指定了文件的路径.这个路径可以有两种形式:


  • 绝对路径:就是文件的所在,不过是从根文件夹开始的路径。例如:C:/Cpp/Files/scores.txt

  • 相对路径:也是文件的所在,不过是相对于你的程序的路径。例如,你的程序位于C:/Cpp/,那么如果你的文件是在C:/Cpp/Files/scores.txt,你在程序里指定文件的相对路径时就要写 Files/scores.txt


自此,我们就可以使用这个文件流来写文件啦。


如果文件不存在,那么会被自动创建。不过,至少指定的目录要存在,不然会出现"目录不存在"的错误。在我们上面的例子中,至少目录C:/Cpp/Files必须事先存在。


在打开文件的时候,也会有其他问题。例如文件不属于你,或者磁盘已满,等等,总之,打开失败。因此,我们为了保险起见,总要测试文件是否顺利被打开。我们使用 if (myStream) 的方法来测试。


ofstream myStream("C:/Cpp/Files/scores.txt");  //试着打开这个文件

if(myStream)  //测试打开文件是否成功
{
    //一切顺利,我们可以使用此文件了
}
else
{
    cout << "出错: 无法打开此文件." << endl;
}


至此,我们已经做好了写文件的准备工作。你会看到,接下来的操作还是有点眼熟的。


向流中写入数据


前面我们说过写入文件的操作就和以前我们使用cout类似。因此当我对你说要使用<<运算符来进行操作的时候,你应该不会太惊讶。


#include <iostream>
#include <fstream>
#include <string>
using namespace std;

int main()
{
    string const fileName("C:/Cpp/Files/scores.txt");
    ofstream myStream(fileName.c_str());
    
    if(myStream)    
    {
        myStream << "大家好,我是被写入文件的一句话." << endl;
        myStream << 54.26 << endl;
        int age(23);
        myStream << "我" << age << "岁了." << endl;
    }
    else
    {
        cout << "出错: 无法打开此文件." << endl;
    }
    
    return 0;
}


上面的程序中,可以看到我们首先声明了一个string的变量,里面存放了C:/Cpp/Files/scores.txt这个字符串,不过之后在将其赋给ofstream的对象myStream时,我们却用了c_str()这个函数,这是为什么呢?


其实,ofstream接受的参数是char *(暂时不需要知道是什么,马上我们会学习指针的知识,到时就清楚了),c_str()函数就是用于将string转换成char *


运行此程序,不出意外的话,你的电脑的C:/Cpp/Files/目录下就会多出一个文件 scores.txt, 里面的内容如下所示:


0?wx_fmt=png


你也来试试


你也可以写一个程序,请求用户输入自己的名字和年龄,然后你的程序将这些信息写入文件。


文件的不同打开模式


我们只需要再处理一个小问题:


假如文件已经存在,那怎么办呢?


如果运行上面的已有程序,那么文件的内容会被删除,然后替换为你写入的内容。但是假如我们想要保留文件本来的内容,只是想在文件末尾追加我们的新内容呢?


不用怕,肯定有办法的。只需要在打开文件的时候添加第二个参数,用于指明文件的打开模式,如下所示:


ofstream myStream("C:/Cpp/Files/scores.txt", ios::app);


app是英语append的缩写,表示"追加",也就是说写入的内容不会覆盖原本文件里的内容,而是追加到文件末尾。



读取文件


我们学习了如何写文件,现在来学习如何读取文件内容吧。你会看到,两种操作是很类似的。


以读的形式打开文件


之前我们用了ofstream的对象,那么这次就要用到ifstream的对象了,ifstream是input file stream的缩写。当然也需要测试文件是否顺利被打开。


ifstream myStream("C:/Cpp/Files/scores.txt");  //试着打开文件

if(myStream)
{
    //可以读取文件
}
else
{
    cout << "出错: 无法以读的形式打开此文件." << endl;
}


没有什么新的难点不是吗?


接下来我们就可以读取文件内容了。


要读取文件内容,有三种不同的方式:

  1. 一行一行地读取,用getline()函数

  2. 一个词一个词地读取,用>>

  3. 一个字符一个字符地读取,用get()函数


我们分别来学习这三种方式:


一行一行地读取


第一种方式可以一次读取整一行的内容,将其存储在一个字符串里。举例如下:


string line; // 储存整行内容的字符串变量
getline(myStream, line); //读取整一行,存储到line中


此函数的原理和cin是类似的。


一个词一个词地读取


第二种方式,其实你也早就知道了,毕竟聪慧如你嘛。举例如下:


double number;
myStream >> number; //从文件中读取一个浮点数
string word;
myStream >> word;    //从文件中读取一个单词


这 个方法会读取当前所在的文件位置处的内容和之后的一个空格("词"并不是我们平时说的一个单词,而是以空格来分隔的,假如中间没有空格,那么就是一个词, 例如heusyg3这是一个词,但是heu syg3却被认为是两个词,因为中间存在空格)。读取的内容根据变量的类型会被转换成double,int,string,等等。


一个字符一个字符地读取


第三种方式,我们之前没学过,不过也很简单就是了。举例如下:


char a;
myStream.get(a);


上面的代码读取一个字符,将其存储在char型变量a中。


这个方法可以读取所有字符,不管是字母,空格,回车符,制表符,等等。


还记得在【C++探索之旅】第一部分第五课:简易计算器中, 我们学习过cin的用法吗?还记得我们说过在cin>>和getline之间需要使用cin.ignore()吗?因此,这里我们从一个词一 个词地读取(用cin>>)转换到一行一行地读取(用getline()),也需要在之间加入ignore()。不过,因为我们这里是在读取 文件,所以不能用cin.ignore(),而要使用ifstream的ignore方法,如下所示:


ifstream myStream("C:/Cpp/Files/scores.txt");

string word;
myStream >> word;          //读取一个词

myStream.ignore();        //改变读取方式

string line;
getline(myStream, line); //读取一整行


一次读取整个文件


很多时候,我们会希望读取整个文件。我们已经学习了如何读取文件,但是还没学习当到达文件结尾时,如何停止。


为 了获知我们是否还可以继续读取,可以用getline函数的返回值。getline函数的返回值是一个bool(布尔值),如果等于true,还可以继续 读,说明还没到文件末尾;如果等于false,那么说明已经读取了文件的最后一行或者出错了。在false的情况下,就不能再继续读取了。


还记得我们学过的循环吗?只要还没到达文件末尾(getline函数返回是true),我们就继续读取文件。while循环就是最好的选择啦。看如下例子:


#include <iostream>
#include <fstream>
#include <string>
using namespace std; 

int main()
{
   ifstream file("C:/Cpp/Files/scores.txt"); // 尝试打开文件
   
   if(file)
   {
      //文件顺利打开,可以读取了
      
      string line;  //存储读取的一整行的变量
      
      while(getline(file, line))  //只要没到达文件末尾,我们就一直一行一行地读取
      {
         cout << line << endl;
         //在控制台显示读取的行
         //或者随便你拿这一行干什么,由你决定
      }
   }
   else
   {
      cout << "出错: 无法以读的形式打开此文件." << endl;
   }
   
   return 0;
}


一旦我们读取了这些行,我们就可以非常方便地操作它们了。在上面的例子中,我们只是把读取的每一行显示在控制台中,但是你可以随便怎么用。



一些小技巧


这一课的最后,我们来学习几个小技巧,这样文件读写我们就学习得差不多了。


提前关闭文件


我们已经知道怎么打开一个文件,但还没演示如何关闭文件。倒不是因为我忘记了,而是之前关闭文件显得没有那么必要。一旦我们跳出了文件流声明的区块,打开的文件就会被自动关闭。例如:


void f()
{
   ofstream myStream("C:/Cpp/Files/scores.txt"); //打开文件
   
   // 操作文件
   
}  //当我们跳出这个函数,文件就自动被关闭了


因此,并不需要做任何操作来显式地关闭文件。


但是,有时候我们想要提前关闭文件,在它被自动关闭前。为了达到这个目的,我们必须"不择手段"... 哦,不是,是使用close函数。例如:


void f()
{   
    ofstream myStream("C:/Cpp/Files/scores.txt");  
    //打开文件C:/Cpp/Files/scores.txt
    
    //使用文件   
    
    myStream.close();  //关闭文件
    //自此,我们将不能再往文件里写东西了
}


同样地,我们也可以推迟打开文件。用open函数。例如:


void f(){   
    ofstream myStream;  //声明文件流,但没有绑定文件
    myStream.open("C:/Cpp/Files/scores.txt");  
    //打开文件C:/Cpp/Files/scores.txt

    //使用文件   

   myStream.close();  //关闭文件   
   //自此,我们将不能再往文件里写东西了
}


正如你所见,以上的操作都很简单。然而,在大部分时候,没必要使用open和close函数来显示地打开和关闭文件。


文件里的游标


我们再来深入一些技术细节,"研究"一下文件的读取是怎么运作的。


你还记得平时用文本编辑器的时候,我们在编辑文本时总会有一个一闪一闪的光标(cursor),指示了我们当前编辑的位置吗?如下图所示:


0?wx_fmt=png

可以看到,目前光标位于Oscar的后面。


在C++中操作文件时,也是同样的原理。有一个游标(cursor)一直指示当前在文件中的位置。


例如,当我们运行这一行的时候:


ifstream file("C:/Cpp/Files/scores.txt");


文件C:/Cpp/Files/scores.txt会被打开,游标会定位于文件最开始处。


如果之后我们读取第一个词,就会读取到Oscar这个词。读取完之后,我们的游标就会位于下一个单词的开始处了,如下图所示:


0?wx_fmt=png

可以看到,现在游标位于is这第二个词的开始处了。然后我们可以接着读取第二个词,第三个,... 一直到文件结束。


但如果这样的话,我们只能按顺序读取文件,这可太束缚了。我们需要自由,需要飞翔,"在你的心上,自由地飞翔~" (小编,你的药已经准备好了...)


幸好,我们能够在文件中移动,说到移动,那就是移动那个cursor(游标)了。例如,我们可以说"我要移动到距离文件开始处20个字符的地方",或者"我要从当前位置前进32个字符"。这样,我们就可以很方便地读取我们真正想要的内容了。


首先,我们要了解游标目前位于哪里。然后才能正确地移动。


获得在文件中的位置


有一个方法可以获知当前我们的游标位于文件的第几个字符处(从文件开始处算起)。不过,对于输入文件流(ifstream)和输出文件流(ofstream),所用的函数不一样,而且名字也有点古怪,我们列在下面:


针对ifstream

针对ofstream

tellg()

tellp()


然而,这两个函数的使用方法完全一样。因此只介绍其中一个就可以了。举例如下:


ofstream file("C:/Cpp/Files/scores.txt");
int position = file.tellp(); //获取当前位置
cout << "目前位于文件中的第" << position << "个字符处." << endl;


在文件中移动


用于在文件中移动的函数也有两个,成对的,每一个对应一种流的形式:


针对ifstream

针对ofstream

seekg()

seekp()


用法和之前的两个函数类似。


这两个函数接受两个参数:一个是在文件中的位置,另一个是相对文件中的位置的距离数(字符数/字节数)。


myStream.seekp(numberOfCharacters, position);


对于此函数的position参数,有三种可能的位置:


  • 文件开始处 : ios::beg ;

  • 文件末尾处 : ios::end ;

  • 当前位置 : ios::cur.


例如,我想要移动到距离文件开始处10个字符的地方,我会这么做:


myStream.seekp(10, ios::beg);


假如我想要移动到距离当前游标所在位置的20个字符处,我会这么做:


myStream.seekp(20, ios::cur);


相信你已经理解啦。


获知文件大小(所包含字节数)


这第三个小技巧需要用到前两个。为了获知文件的大小,我们首先移动到文件末尾,然后询问我们所在的位置。你知道怎么做了吗?一起来看看吧:


#include <iostream>
#include <fstream>
using namespace std;

int main()
{
    ifstream file("C:/Cpp/Files/scores.txt");  //打开文件
    
    file.seekg(0, ios::end);  //移动到文件末尾
    
    int size;
    size = file.tellg();
    //在文件结尾处调用tellg这个函数,以获得目前位于第几个字符处,因此也就知道了文件的大小
    
    cout << "文件的大小是: " << size << "个字节." << endl;
    
    return 0;
}


好了,我们学完了文件读写的大致概念。不过肯定不只于此,还有很多知识点需要慢慢在实践中去探索。



总结


  1. 在C++中,为了能读写文件,需要引入fstream头文件。

  2. 为了写入文件,我们需要创建一个ofstream对象;为了读取文件,我们需要创建一个ifstream对象。

  3. 写入文件的操作其实很类似 cout :  myStream << "文本";  读取文件的操作其实很类似 cout :  myStream >> variable;

  4. 可以用getline()函数一行一行地读取文件。

  5. 游标(cursor)指示了写入操作或读取操作时,在文件中的位置。如果需要,可以移动这个游标。



第一部分第十一课预告


今天的课就到这里,一起加油吧!

下一课我们学习:小练习,猜单词