Fork me on GitHub

C++ Primer Plus 学习笔记五

关于文件I/O的一些内容

简单文件输入/输出

文本I/O和文本文件

我们这里讨论一下关于文本I/O的概念。当我们使用cin进行输入时,程序将输入视为一系列的字节,其中每个字节都被解释为字符编码。不管目标数据类型是什么,输入一开始都是字符数据——文本数据。然后,cin对象负责将文本转换为其他类型。例如:
假设有如下输入行:

1
38.5 19.2

首先,我们看一下char数据类型的情况:

1
2
char ch;
cin >> ch;

输入行中的第一个字符被赋给ch。这里,第一个字符为3,其字符编码(二进制)被存储在变量ch中。输入和目标变量都是字符,因此不需要进行转换。注意,这里存储的数值3,而是字符3的编码。执行上述输入语句后,输入队列的下一个字符为字符8,下一个输入操作将对其进行处理。
接下来康康int类型:

1
2
int n;
cin >> n;

在这种情况下,cin将不断读取,直到遇到非数字字符。也即是,它将读取3和8,这样句点将成为输入队列的下一个字符,cin通过计算发现,这两个字符对应数值38,因此将38的二进制编码复制到变量n中。
接下来便是double类型了:

1
2
double x;
cin >> x;

这种情况下,cin将不断读取,直至遇到第一个不属于浮点数的字符。也就是说,cin读取3、8、句点和5使得空格成为输入队列的下一个字符。cin通过计算发现,这四个字符对应数值38.5,因此将38.5的二进制编码(浮点格式)复制到变量x中。
那么char数组又如何?

1
2
char word[50];
cin >> word;

这种情况下,cin将不断读取,直到遇到空白字符。也即是,它读取3、8、句点和5。使得空格成为输入队列的下一个字符。然后,cin将这4个字符的编码存储到数组word中,并在末尾加上一个空字符。这里不需要进行任何转换。
最后便是另一种使用char数组的情况:

1
2
char word[50];
cin.getline(word, 50);

这种情况下,cin将不断读取直至遇上换行符(示例输入少于50个字符)。所有字符都将被存储到数组word中,并在末尾加上一个空字符。换行符被丢弃,输入队列中的下一个字符是下一行中的第一个字符。
我们这里讨论的I/O相当于控制台I/O,因此仅适用于文本文件。

写入到文本文件

对于文件输入,C++使用类似于cout的东西。下面来复习一下:

  • 必须包含头文件iostream。
  • 头文件iostream定义了一个用于处理输出的ostream类。
  • 头文件iostream声明了一个名为cout的ostream变量(对象)。
  • 必须指明名称空间std;例如,为引用元素cout和endl,必须使用编译指令using或前缀std::。
  • 可以结合使用cout和运算符<<来显示各种类型的数据。

文件输出与此十分类似:

  • 必须包含头文件fstream。
  • 头文件fstream定义了一个用于处理输出的ofstream类。
  • 需要声明一个或多个ofstream变量(对象),并以自己喜欢的方式对其进行命名,条件是遵守常用的命名规则。
  • 必须指明名称空间std;例如,为引用元素ofstream,必须使用编译指令using或前缀std::。
  • 需要将ofstream对象与文件关联起来。为此,方法之一是使用open()方法。
  • 使用完文件后,应使用方法close()将其关闭。
  • 可结合使用ofstream对象和运算符<<来输出各种类型的数据。

注意,虽然头文件iostream提供了一个预先定义好的名为cout的ostream对象,但必须声明自己的ofstream对象,为其命名,并将其与文件关联。例如:

1
2
ofstream outFile;	// outFile an ofstream object
ofstream fout; // fout an ofstream object

下面演示了如何将这种对象与特定的文件关联起来:

1
2
3
4
outFile.open("fish.txt");	// outFile used to write to the fish.txt file
char filename[50];
cin >> filename; // user specifies a name
fout.open(filename); // fout used to read specified file

注意,方法open()接受一个C-风格字符串作为参数,这可以是一个字面字符串,也可以是存储在数组中的字符串。
下面演示了如何使用这种对象:

1
2
3
4
double wt = 125.8;
outFile << wt; // write a number to fish.txt
char line[81] = "Objects are closer than they appear.";
fout << line << endl; // write a line of text

重要的是,声明一个ofstream对象并将其同文件关联起来后,便可以像使用cout那样使用它。所有可用于cout的方法(如<<、endl和setf())都可用于ofstream对象(如前述示例中的outFile和fout)。
总之,使用文件输出的主要步骤如下:

  1. 包含头文件fstream。
  2. 创建一个ofstream对象。
  3. 将该ofstream对象同一个文件关联起来。
  4. 就像使用cout那样使用该ofstream对象。

下列程序演示了这种方法。它要求用户输入信息,然后将信息显示到屏幕上,再将这些信息写入到文件中。读者可以使用文本编辑器来查看该输出文件内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
//outfile.cpp -- writing to a file
#include <iostream>
#include <fstream>
int main()
{
using namespace std;

char automobile[50];
int year;
double a_price;
double d_price;

ofstream outFile; // create object for output
outFile.open("carinfo.txt"); // associate with a file

cout << "Enter the make and model of automobile: ";
cin.getline(automobile, 50);
cout << "Enter the model year: ";
cin >> year;
cout << "Enter the original asking price: ";
cin >> a_price;
d_price = 0.913*a_price;

// display information on screen with cout

cout << fixed;
cout.precision(2);
cout.setf(ios_base::showpoint);
cout << "Make and model: " << automobile << endl;
cout << "Year: " << year << endl;
cout << "Was asking $" << a_price << endl;
cout << "Now asking $" << d_price << endl;

// now do exact same things using outFile instead of cout

outFile << fixed;
outFile.precision(2);
outFile.setf(ios_base::showpoint);
outFile << "Make and model: " << automobile << endl;
outFile << "Year: " << year << endl;
outFile << "Was asking $" << a_price << endl;
outFile << "Now asking $" << d_price << endl;

outFile.close();
return 0;
}

该程序的最后一部分与cout部分相同,只是将cout替换为outFile而已。下面是该程序的运行情况:
输出
屏幕输出使用的是cout的结果。如果我们查看源文件outfile.cpp的目录,将看到一个名为carinfo.txt的新文件,其中包含outFile生成的输出。打开文本编辑器,显示如下:
文本输出
正如你们所见,outFile将屏幕上的内容写入到了文件carinfo.txt中。

程序说明

在程序中,声明一个ofstream对象后,便可以使用方法open()将该对象和特定文件关联起来:

1
2
ofstream outFile;			// create object for output
outFile.open("carinfo.txt"); // associate with a file

程序使用完该文件后,应当将其关闭:

1
outFile.close();

需要注意的是,方法close()不需要参数,这是因为outFile已经同特定的文件关联起来了。如果忘记关闭文件,程序正常终止时将自动关闭它。
outFile可以使用cout可使用的任何方法。它不但能够使用运算符<<,还可以使用各种格式化方法,如self()和precision()。这些方法只影响调用它们的对象。例如,对于不同的对象,可以提供不同的值:

1
2
cout.precision(2);		// use a precision of 2 for the display
outFile.precision(4); // use a precision of 4 for the display

需要记住的是,创建好ofstream对象后(如outFile)后,便可以像使用cout一样使用它。
回到open():

1
outFile.open("carinfo.txt");

在这里,运行程序之前,文件carinfo.txt并不存在,这种情况下,方法open()将新建一个名为carinfo.txt的文件。如果在此时运行该程序,则文件carinfo.txt存在,情况何如?默认情况下,open()将截断该文件,即将其长度截短到零——丢弃原有的内容,然后将新的输出加入到该文件中。之后我们将学习如何更改这种默认行为。
警告:打开已有的文件,以接受输出时,默认将它长度截短为零,因此原来的内容将丢失。
打开文件用于接受输入时可能失败。例如,指定的文件可能已经存在,但禁止对其进行访问。因此,细心的程序员将检查打开文件的操作是否成功,这将在下一个例子中介绍。

读取文本文件

接下来介绍文本文件输入,它是基于控制台输入的。控制台输入涉及多个方面,下面首先来进行总结:

  • 必须包含头文件iostream。
  • 头文件iostream定义了一个用于处理输入的istream类。
  • 头文件iostream声明了一个名为cin的istream变量(对象)。
  • 必须指明名称空间std;例如,为引用元素cin,必须使用编译指令using或前缀std::。
  • 可以结合使用cin和运算符>>来读取各种类型的数据。
  • 可以使用cin和get()方法来读取一个字符,使用cin和getline()来读取一行字符。
  • 可以结合使用cin和eof()、fail()方法来判断输入是否成功。
  • 对象cin本身被用作测试条件时,如果最后一个读取操作成功,它将被转换为布尔值true,否则被转换为false。

文件输出与此极其相似:

  • 必须包含头文件fstream。
  • 头文件定义了一个用于处理输入的ifstream类。
  • 需要声明一个或多个ifstream变量(对象),并以自己喜欢的方式对其进行命名,条件是遵守常用的命名规则。
  • 必须指明名称空间std;例如,为引用元素ifstream必须使用编译指令using或前缀std::。
  • 需要将ifstream对象与文件关联起来。为此,方法之一时使用open()方法。
  • 使用完文件后,应使用close()方法将其关闭。
  • 可结合使用ifstream对象和运算符>>来读取各种类型的数据。
  • 可以使用ifstream对象和get()方法来读取一个字符,使用ifstream对象和getline()来读取一行字符。
  • 可以结合使用ifstream和eof()、fail()等方法来判断输入是否成功。
  • ifstream对象本身被用作测试条件时,如果最后一个读取操作成功,它将被转换为布尔值true,否则被转换为false。

注意,虽然头文件iostream提供了一个预先定义好的名为cin的istream对象,但必须声明自己的ifstream对象,为其命名,并将其同文件关联起来。下面演示了如何声明这种对象:

1
2
ifstream inFile;	// inFile an ifstream object
ifstream fin; // fin an ifstream object

下面演示了如何将这种对象与特定的文件关联起来:

1
2
3
4
inFile.open("bowling.txt");		// inFile used to read bowling.txt file
char filename[50];
cin >> filename; // user specifies a name
fin.open(filename); // fin used to read specified file

注意,方法open()接受一个C-风格字符串作为参数,这可以是一个字面字符串,也可以是存储在数组中的字符串。
下面演示了如何使用这种对象:

1
2
3
4
double wt;
inFile >> wt; // read a number from bowling.txt
char line[81];
fin.getline(line, 81); // read a line of text

重要的是,声明一个ifstream对象并将其同文件关联起来后,便可以像使用cin那样使用它。所有用于cin的操作和方法都可用于ifstream对象(如前述示例中的inFile和fin)。
如果试图打开一个不存在的文件用于输入,情况何如?这种错误将导致后面使用ifstream对象进行输入时失败。检查文件是否被成功打开的首先方法是使用方法is_open(),为此,可以使用类似于下面的代码:

1
2
3
4
5
inFile.open("bowling.txt");
if (!inFile.is_open())
{
exit(EXIT_FAILURE);
}

如果文件被成功地打开,方法is_open()将返回true;因此如果文件没有被打开,表达式!inFile.is_open()的值将为true。函数exit()的原型是在头文件cstdlib中定义的,在该头文件中,还定义了一个用于同操作系统通信的参数值EXIT_FAILURE。函数exit()终止程序。下图是cstdlib对EXIT_FAILURE的定义。
定义
相对的,cstdlib中也存在参数值EXIT_SUCCESS,它的定义是:
定义
方法is_open()是C++中相对较新的内容。如果读者编译器不支持它,可以使用较老的方法good()代替。在后面的学习中会讨论,方法good()在检查可能存在的问题方面,没有is_open()那么广泛。
下列程序打开用户指定的文件,读取器中的数字,然后指出文件中包含多少个值以及它们的和与平均值。正确地设计输入循环至关重要。详细请看后面地“程序说明”。注意,通过使用if语句,该程序受益匪浅。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// sumafile.cpp -- functions with an array argument
#include <iostream>
#include <fstream> // file I/O support
#include <cstdlib> // support for exit()
const int SIZE = 60;
int main()
{
using namespace std;
char filename[60];
ifstream inFile; // object for handling file input
cout << "Enter name of data file: ";
cin.getline(filename, SIZE);
inFile.open(filename); // associate inFile with a file
if (!inFile.is_open()) // failed to open file
{
cout << "Could not open the file " << filename << endl;
cout << "Program terminating.\n";
exit(EXIT_FAILURE);
}
double value;
double sum = 0.0;
int count = 0; // number of items read

inFile >> value; // get first value
while (inFile.good()) // while input good and not at EOF
{
++count; // one more item read
sum += value; // calculate running total
inFile >> value; // get next value
}
if (inFile.eof())
cout << "End of file reached.\n";
else if (inFile.fail())
cout << "Input terminated by data mismatch.\n";
else
cout << "Input terminated by unknown reason.\n";
if (count == 0)
cout << "No data processed.\n";
else
{
cout << "Items read: " << count << endl;
cout << "Sum: " << sum << endl;
cout << "Average: " << sum / count << endl;
}
inFile.close(); // finished with the file
return 0;
}

要运行上述程序,首先必须创建一个包含数字的文本文件。为此,可以使用文本编辑器(如用于编写源代码的文本编辑器)。假设该文件名为scores.txt,包含的内容如下:
文本文档
警告:Windows文本文件的每一行都以回车字符和换行符结尾;通常情况下,C++再读取文件时将这两个字符转换为换行符,并在写入文件时执行相反的转换。有些文本编辑器(如Metroweeks Code Warrior IDE编辑器),不会自动在最后一行末尾加上换行符。因此,如果读者使用的是这种编辑器,请在输入最后的文本后按下回车键,然后在保存文件。
下面是该程序的运行情况:
输出

程序说明

该程序没有使用硬编码文件名,而是将用户提供的文件名存储到字符数组filename中,然后将该数组用作open()的参数:

1
inFile.open(filename);

特别需要注意的是循环的正确设计。读取文件时,有几点需要检查。首先,程序读取文件时不应超过EOF。如果最后一次读取数据时遇到EOF,方法eof()将返回true。其次,程序可能遇到类型不匹配的情况。例如,上述程序期望文件中只包含数字。如果最后一次读取操作发生了类型不匹配的情况,方法fail()将返回true(如果遇到了EOF,该方法也将返回true)。最后,可能出现意外的问题还有很多,如文件受损或硬件故障。如果最后一次读取文件时发生了这样的问题,方法bad()将返回true。不要分别检查这些情况,一种更简单的方法是使用good()方法,该方法在没有发生任何错误时返回true:

1
2
3
4
while (inFile.good())	// while input good and not at EOF
{
...
}

然后,如果愿意,可以用其他方法来确定循环终止的真正原因:

1
2
3
4
5
6
if (inFile.eof())
cout << "End of file reached.\n";
else if (inFile.fail())
cout << "Input terminated by data mismatch.\n";
else
cout << "Input terminated by unknown reason.\n";

这些代码紧跟在循环的后面,用于判断循环为何终止。由于eof()只能判断是否到达EOF,而fail()可用于检查EOF和类型不匹配,因此上述代码首先判断是否到达EOF。这样,如果执行到了else if测试,便可排除EOF,因此,如果fail()返回true,便可断定导致循环终止的原因是类型不匹配。
方法good()指出最后一次读取输入的操作是否成功,这十分重要。这意味着应该在执行读取输入操作后,立刻应用这种测试。为此,一种标准方法是,在循环之前(首次执行循环测试前)放置一条输入语句,并在循环的末尾(下次执行循环测试之前)放置另一条输入语句:

1
2
3
4
5
6
7
// standard file-reading loop design
inFile >> value; // get first value
while (inFile.good()) // while input good and not at EOF
{
// loop body goes here
inFile >> value; // get next value
}

鉴于以下事实,可以将上述代码精简:表达式inFile >> value的结果为inFile,而在需要一个bool值的情况下,inFile的结果为inFile.good(),即true或false。
因此,可以将两条输入语句用一条用作循环测试的输入语句代替。也就是说,可以将上述循环结构替换为如下循环结构:

1
2
3
4
5
6
7
// abbreviated file-reading loop design
// omit pre-loop input
while (inFile >> value) // read and test for success
{
//loop body goes here
// omit end-of-loop input
}

此种设计仍然遵循了在测试之前进行读取的规则,因为要计算表达式inFile >> value的值,程序首先试图将一个数据读取到value中。
至此,我们对文件I/O有了初步的认识。

0%