内容简介
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, 里面的内容如下所示:
你也来试试
你也可以写一个程序,请求用户输入自己的名字和年龄,然后你的程序将这些信息写入文件。
文件的不同打开模式
我们只需要再处理一个小问题:
假如文件已经存在,那怎么办呢?
如果运行上面的已有程序,那么文件的内容会被删除,然后替换为你写入的内容。但是假如我们想要保留文件本来的内容,只是想在文件末尾追加我们的新内容呢?
不用怕,肯定有办法的。只需要在打开文件的时候添加第二个参数,用于指明文件的打开模式,如下所示:
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;
}
没有什么新的难点不是吗?
接下来我们就可以读取文件内容了。
要读取文件内容,有三种不同的方式:
-
一行一行地读取,用getline()函数
-
一个词一个词地读取,用>>
-
一个字符一个字符地读取,用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),指示了我们当前编辑的位置吗?如下图所示:
可以看到,目前光标位于Oscar的后面。
在C++中操作文件时,也是同样的原理。有一个游标(cursor)一直指示当前在文件中的位置。
例如,当我们运行这一行的时候:
ifstream file("C:/Cpp/Files/scores.txt");
文件C:/Cpp/Files/scores.txt会被打开,游标会定位于文件最开始处。
如果之后我们读取第一个词,就会读取到Oscar这个词。读取完之后,我们的游标就会位于下一个单词的开始处了,如下图所示:
可以看到,现在游标位于is这第二个词的开始处了。然后我们可以接着读取第二个词,第三个,... 一直到文件结束。
但如果这样的话,我们只能按顺序读取文件,这可太束缚了。我们需要自由,需要飞翔,"在你的心上,自由地飞翔~" (小编,你的药已经准备好了...)
幸好,我们能够在文件中移动,说到移动,那就是移动那个cursor(游标)了。例如,我们可以说"我要移动到距离文件开始处20个字符的地方",或者"我要从当前位置前进32个字符"。这样,我们就可以很方便地读取我们真正想要的内容了。
首先,我们要了解游标目前位于哪里。然后才能正确地移动。
获得在文件中的位置
有一个方法可以获知当前我们的游标位于文件的第几个字符处(从文件开始处算起)。不过,对于输入文件流(ifstream)和输出文件流(ofstream),所用的函数不一样,而且名字也有点古怪,我们列在下面:
| 针对 |
---|---|
|
|
然而,这两个函数的使用方法完全一样。因此只介绍其中一个就可以了。举例如下:
ofstream file("C:/Cpp/Files/scores.txt");
int position = file.tellp(); //获取当前位置
cout << "目前位于文件中的第" << position << "个字符处." << endl;
在文件中移动
用于在文件中移动的函数也有两个,成对的,每一个对应一种流的形式:
针对 | 针对 |
---|---|
|
|
用法和之前的两个函数类似。
这两个函数接受两个参数:一个是在文件中的位置,另一个是相对文件中的位置的距离数(字符数/字节数)。
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;
}
好了,我们学完了文件读写的大致概念。不过肯定不只于此,还有很多知识点需要慢慢在实践中去探索。
总结
-
在C++中,为了能读写文件,需要引入fstream头文件。
-
为了写入文件,我们需要创建一个ofstream对象;为了读取文件,我们需要创建一个ifstream对象。
-
写入文件的操作其实很类似 cout : myStream << "文本"; 读取文件的操作其实很类似 cout : myStream >> variable;
-
可以用getline()函数一行一行地读取文件。
-
游标(cursor)指示了写入操作或读取操作时,在文件中的位置。如果需要,可以移动这个游标。
第一部分第十一课预告
今天的课就到这里,一起加油吧!
下一课我们学习:小练习,猜单词