接下来我们就从几个方面来谈谈数据文件的输入输出。
单个字符的输入输出(Single character I/O)
在许多应用程序中,处理文本文件中数据的最佳方式是通过对文件的内容一次读取一个字符。C++库中的输入流支持使用get方法读取单个字符,该方法返回流中的下一个字符。
虽然使用get方法的想法很简单,但是在设计中有一个令人困惑的方面。如果你看看get的原型,你会发现它看起来像这样:
int get();
乍一看,结果类型似乎很奇怪。原型表示get返回一个int型,但是即使返回char的方法似乎更合适。这种设计决定的原因是返回一个字符会使程序更难以检测到文件结尾标记。这里有256个可能的字符代码,而且数据文件可能包含这里面的任何值。所以,没有值或至少没有char类型的值——可以用作条件来指示文件结束条件。通过扩展定义使得get返回一个整数,该实现可以返回超出合法字符代码范围的值,以指示文件结束条件。该值具有EOF的符号名称。
对于输出流,方法put将char值作为参数,并将该字符写入流。 因此,对put方法的典型调用如下所示
outfile.put(ch);
下面我们就写一段代码利用单个字符的输入输出实现文本的读取:
#include <iostream>
#include <fstream>
#include <string>
using namespace std;
int main() {
ifstream infile;
infile.open("F:\\Jabberwocky.txt");
if (infile.fail()){
infile.clear();
cout << "Unable to open that file." << endl;
}else{
cout << "Jabberwocky.txt" << endl;
}
//根据单个字符逐个读取
while (true) {
int ch = infile.get();
if (ch == EOF) break; //EOF是结束符
cout.put(ch);
}
infile.close();
return 0;
}
我们先来分析,我们要输入文本,所以我们声明的是ifstream类型,我的这个文本我放在F盘里面,所以在用open方法的时候,我们要说明它所在的绝对位置(文件之间用\ \号来分割),在while循环中我们用get()方法去获取流中的下一个字符,看看它是否等于结束的符号(注意这是个整型的数),如果不等于,那就用put()方法输出,如果等于那就跳出循环。最后关闭文件与流之间的关联。结果如下:
面向行输出(Line-oriented I/O)
因为文件通常被细分为单独的行,所以我们通常一次读取整行数据。执行此操作的流函数称为getline。getline函数被定义为一个自由函数(即不属于某个类的函数 free funtion),而不是一个方法 - 需要两个引用参数:读取行的输入流和写入结果的字符串变量。 比如我们调用:
getline(infile, str);
将文件的下一行复制到变量str中,直到但不包括表示行尾的换行符。我们可以通过修改上面的while循环来达到按行输出的目的。
while (true) {
string line;
getline(infile, line);
if (infile.fail()) break;
cout << line << endl;
}
该循环的使用getline从文件中读取下一行数据,然后调用fail方法来检查输入流是否已到达文件的末尾。如果是,则break语句退出循环。如果不是,代码使用< <运算符将行的内容发送到cout,紧接着换行字符,运行结果跟上面的一致。
格式化输出(Formatted I/O)
除了处理文件的逐个字符和逐行方法之外,还可以使用文件流中的<<和>>运算符,就像我们常常使用的控制台流 。例如,假设我们要修改一个AddIntegerList程序,要求让从数据是从文件输入而不是从控制台输入。最简单的方法是打开一个数据文件进行输入,并使用ifstream而不是cin来读取输入值。
我们需要做的唯一更改是在输入完成时退出循环的代码。程序的控制台使用某个值来指示输入的结束。 对于从文件读取,循环应该一直继续,直到没有更多的数据值被读取。测试该条件的通常方法是尝试读取一个值,然后调用fail()来检查该操作是否成功。
如果我们把上面的代码改一下(就会变成):
int main() {
ifstream infile;
infile.open("XXXX");//这里是说包含数据的文件,我就用XXX替代了
int total = 0;
while (true) {
int value;
infile >> value;
if (infile.fail()) break;
total += value;
}
infile.close();
cout << "The sum is " << total << endl;
return 0;
}
然而,不幸的是,这种策略却不太理想,虽然在语法上这并没有什么错误的。当所有的数字都以正确的方式进行格式化,程序将得到正确的答案。 但是,如果文件中有无关字符,则在所有输入值已被读取之前,循环将退出。更糟糕的是,该程序将不会显示发生错误。
问题的主要原因在这:
infile >> value;
这行的提取运算符会设置两个故障指示器:
1. 到达文件的末尾,在这一点上没有更多的值读取(Reaching the end of the file, at which point there are no more values to read)
2. 尝试从文件读取无法转换为整数的数据(Trying to read data from the file that cannot be converted to an integer)
当然我们可以通过检查以确保在循环退出时已到达文件的末尾,使事情更好。例如,至少可以通过在while循环之后添加以下行来让用户知道发生错误:
if (!infile.eof()) {
cout << "Data error in file";
}
然而,另一个问题是,提取运算符在其允许的格式方面过于自由。除非另有说明,否则> >运算符将接受任何空白字符序列作为数据分隔符。因此,输入文件中的数据不需要每行一个值,而是可以通过多种方式进行格式化。例如,如果要使用应用程序添加前五个整数,则不需要每行输入一个值,如以下数据文件所示:
它也可以工作,而且可能更方便 - 它的作用就跟将值放在一行一样:
具有这种灵活性的一个问题是,程序更难以检测某些类型的格式化错误。例如,如果您不小心在其中一个整数中包含空格,会发生什么?事实上,应用程序只会将空格之前和之后的数字读取为两个单独的值。在这种情况下,通常更好的是坚持使用更严格的格式化规则来提高数据完整性。就像在上面的图中,如第一个示例文件中一样,坚持每行显示一个数据值。但是,执行该限制是困难的。 一种开始的方法是一次读取数据文件一行,然后将每行转换为整数,然后将其添加到总数。 如果你采用这种方法,主程序将如下所示:
int main() {
ifstream infile;
infile.open("XXXX");
int total = 0;
while (true) {
string line;
getline(infile, line);
if (infile.fail()) break;
total += stringToInteger(line);
}
infile.close();
cout << "The sum is " << total << endl;
return 0;
}
这里唯一缺少的就是stringToInteger函数了,我们可以利用
atof(line.c_str());
进行转换。
虽然C++库包括一个称为atof的函数,它将一个字符串转换为整数,但该函数早于库,因此需要使用不太方便使用的C字符串。如果您能找到一种方法来实现该转换,而完全保留在C ++域中,那将会是什么呢? 您知道C ++库必须包含必要的代码,因为>>操作符必须在从文件读取整数时执行该转换。 如果有一种方法使用相同的代码从字符串中读取一个整数,则stringToInteger的实现将立即结果。C ++流库可以精确地提供该功能,这将在各种各样的应用程序中有用。下篇我们就介绍怎么实现它