《C++Primer》第八章-标准 IO 库-学习笔记(2)

《C++Primer》第八章-标准 IO 库-学习笔记(2)

日志:
1,2020-02-29笔者提交文章的初版V1.0

作者按:
最近在学习C++ primer,初步打算把所学的记录下来。

传送门/推广
《C++Primer》第二章-变量和基本类型-学习笔记(1)
《C++Primer》第二章-变量和基本类型-学习笔记(2)
《C++Primer》第二章-变量和基本类型-学习笔记(3)
《C++Primer》第三章-标准库类型-学习笔记(1)
《C++Primer》第三章-标准库类型-学习笔记(2)
上一篇
《C++Primer》第八章-标准 IO 库-学习笔记(1)

文件的输入和输出

fstream 头文件定义了三种支持文件 IO 的类型:

  1. ifstream,由 istream 派生而来,提供读文件的功能。
  2. ofstream,由 ostream 派生而来,提供写文件的功能。
  3. fstream,由 iostream 派生而来,提供读写同一个文件的功能

这些类型都由相应的 iostream 类型派生而来,这个事实意味着我们已经知道使用 fstream 类型需要了解的大部分内容了。特别是,可使用 IO 操作符(<<和 >> )在文件上实现格式化的 IO,而且在前面章节介绍的条件状态也同样适用于 fstream 对象。
fstream 类型除了继承下来的行为外,还定义了两个自己的新操作—— openclose,以及形参为要打开的文件名的构造函数。fstream、ifstream 或ofstream 对象可调用这些操作,而其他的 IO 类型则不能调用。

文件流对象的使用

我们的程序已经使用过标准库定义的对象:cin、cout 和 cerr。
需要读写文件时,则必须定义自己的对象,并将它们绑定在需要的文件上
假设ifile 和 ofile 是存储希望读写的文件的名字的 strings 对象,可如下编写代码:

// construct an ifstream and bind it to the file named ifile
ifstream infile(ifile.c_str());  //注解2
// ofstream output file object to write file named ofile
ofstream outfile(ofile.c_str());
//ifile 和 ofile 是存储希望读写的文件的名字的 strings 对象

上述代码定义并打开了一对 fstream 对象。infile 是读的流,而 outfile则是写的流。为 ifstream 或者 ofstream 对象提供文件名作为初始化式,就相当于打开了特定的文件。
上面这种是直接绑定的。下面的这种是先定义后绑定!

ifstream infile; // unbound input file stream
ofstream outfile; // unbound output file stream

上述语句将 infile 定义为读文件的流对象,将 outfile 定义为写文件的对象。这两个对象都没有捆绑具体的文件。在使用 fstream 对象之前,还必须

infile.open("in"); // open file named "in" in the current directory
outfile.open("out"); // open file named "out" in the current directory

调用 open 成员函数将已存在的 fstream 对象与特定文件绑定。为了实现读写,需要将指定的文件打开并定位,open 函数完成系统指定的所有需要的操作。

检查文件打开是否成功

打开文件后,通常要检验打开是否成功,这是一个好习惯:

// check that the open succeeded
if (!infile) {
	cerr << "error: unable to open input file: "<< ifile << endl;
	return -1;
}

这个条件与之前测试 cin 是否到达文件尾或遇到某些其他错误的条件类似。检查流等效于检查对象是否“适合”输入或输出。如果打开(open)失败,则说明 fstream 对象还没有为 IO 做好准备。当测试对象

if (outfile) // ok to use outfile?

返回 true 意味着文件已经可以使用。如果希望知道文件是否未准备好,则对返回值取反来检查流:

if (!outfile) // not ok to use outfile?

将文件流与新文件重新捆绑

fstream 对象一旦打开,就保持与指定的文件相关联。如果要把 fstream 对象与另一个不同的文件关联,则必须先关闭(close)现在的文件,然后打开(open)另一个文件。要点是在尝试打开新文件之前,必须先关闭当前的文件流。open 函数会检查流是否已经打开。如果已经打开,则会设置内部状态,指出发生了错误,接下来使用文件流的任何尝试都会失败。

ifstream infile("in"); // opens file named "in" for reading
infile.close(); // closes "in"
infile.open("next"); // opens file named "next" for reading

清除文件流的状态

考虑这样的程序,它有一个 vector 对象,包含一些要打开并读取的文件名,程序要对每个文件中存储的单词做一些处理。假设该 vector 对象命名为files,程序也许会有如下循环:

// for each file in the vector   vector 对象命名为files
while (it != files.end()) {
	ifstream input(it->c_str()); // open the file;
	// if the file is ok, read and "process" the input
	if (!input)
	break; // error: bail out!
	while(input >> s) // do the work on this file
		process(s);
	++it; // increment iterator to getnext file
}

每一次循环都构造了名为 input 的 ifstream 对象,打开并读取指定的文件。构造函数的初始化式使用了箭头操作符对 it 进行解引用,从而获取 it 当前表示的string 对象的 c_str 成员文件由构造函数打开,并假设打开成功,读取文件直到到达文件结束符或者出现其他的错误条件为止。
在这个点上,input 处于错误状态。任何读 input 的尝试都会失败。因为 input是 while 循环的局部变量,在每次迭代中创建。这就意味着它在每次循环中都以干净的状态即input.good()为 true,开始使用。

如果希望避免在每次 while 循环过程中创建新流对象,可将 input 的定义移到 while 之前。这点小小的改动意味着必须更仔细地管理流的状态。如果遇到文件结束符或其他错误,将设置流的内部状态,以便之后不允许再对该流做读写操作。关闭流并不能改变流对象的内部状态如果最后的读写操作失败了,对象的状态将保持为错误模式直到执行 clear 操作重新恢复流的状态为止。调用 clear 后,就像重新创建了该对象一样。

如果打算重用已存在的流对象,那么 while 循环必须在每次循环进记得关闭(close)清空(clear)文件流:

ifstream input;  //这个流在下面这个循环外面定义的,所以不同的文件会对其进行重用。
vector<string>::const_iterator it = files.begin(); //迭代器
// for each file in the vector
while (it != files.end()) {
	input.open(it->c_str()); // open the file
	// if the file is ok, read and "process" the input
	if (!input)
	break; // error: bail out!
	while(input >> s) // do the work on this file
		process(s);
	input.close(); // close file when we're done with it
	input.clear(); // reset state to ok
	++it; // increment iterator to get next file
}

如果忽略 clear 的调用,则循环只能读入第一个文件。要了解其原因,就需要考虑在循环中发生了什么:
首先打开指定的文件。假设打开成功,则读取文件直到文件结束或者出现其他错误条件为止。在这个点上,input 处于错误状态。如果在关闭(close)该流前没有调用 clear 清除流的状态,接着在 input 上做的任何输入运算都会失败。一旦关闭该文件,再打开 下一个文件时,在内层while 循环上读 input 仍然会失败——毕竟最后一次对流的读操作到达了文件结束符,事实上该文件结束符对应的是另一个与本文件无关的其他文件。

如果程序员需要重用文件流读写多个文件,必须在读另一个文件之前调用 clear 清除该流的状态。

文件模式

在打开文件时,无论是调用 open 还是以文件名作为流初始化的一部分,都需指定文件模式(file mode)每个 fstream 类都定义了一组表示不同模式的值,用于指定流打开的不同模式。与条件状态标志一样,文件模式也是整型常量,在打开指定文件时,可用位操作符设置一个或多个模式。文件流构造函数open 函数都提供了默认实参设置文件模式。默认值因流类型的不同而不同。此外,还可以显式地以模式打开文件。

文件模式 含义
in 打开文件做读操作
out 打开文件做写操作
app 在每次写之前找到文件尾
ate 打开文件后立即将文件定位在文件尾
trunc 打开文件时清空已存在的文件流
binary 以二进制模式进行 IO 操作
表 1 文件模式及其含义

out、trunc 和 app 模式只能用于指定与 ofstream 或 fstream 对象关联的文件;in 模式只能用于指定与 ifstream 或 fstream 对象关联的文件。
所有的文件都可以用 ate 或 binary 模式打开。ate 模式只在打开时有效:文件打开后将定位在文件尾。以binary 模式打开的流则将文件以字节序列的形式处理,而不解释流中的字符。

默认时,与 ifstream 流对象关联的文件将以 in 模式打开,该模式允许文件做读的操作:与 ofstream 关联的文件则以out 模式打开,使文件可写。以out 模式打开的文件会被清空:丢弃该文件存储的所有数据。
从效果来看,为 ofstream 对象指定 out 模式等效于同时指定了 out 和 trunc 模式。对于用 ofstream 打开的文件,要保存文件中存在的数据,唯一方法是显式地指定app 模式打开:

// output mode by default; truncates file named "file1"
ofstream outfile("file1");
// equivalent effect: "file1" is explicitly truncated
ofstream outfile2("file1", ofstream::out | ofstream::trunc);
// append mode; adds new data at end of existing file named "file2"
ofstream appfile("file2", ofstream::app);

outfile2 的定义使用了按位或操作符将相应的文件同时以out 和 trunc 模式打开。

对同一个文件作输入和输出运算

fstream 对象既可以读也可以写它所关联的文件。fstream 如何使用它的文件取决于打开文件时指定的模式。**默认情况下,fstream 对象以 in 和 out 模式同时打开。**当文件同时以 in和 out 打开时不清空。如果打开 fstream 所关联的文件时,只使用 out 模式,而不指定 in 模式,则文件会清空已存在的数据。如果打开文件时指定了trunc模式,则无论是否同时指定了 in 模式,文件同样会被清空。
下面的定义将copyOut 文件同时以输入和输出的模式打开:

// open for input and output
fstream inOut("copyOut", fstream::in | fstream::out);

模式是文件的属性而不是流的属性

每次打开文件时都会设置模式

ofstream outfile;
// output mode set to out, "scratchpad" truncated
outfile.open("scratchpad", ofstream::out);
outfile.close(); // close outfile so we can rebind it
// appends to file named "precious"
outfile.open("precious", ofstream::app);
outfile.close();
// output mode set by default, "out" truncated
outfile.open("out");

第一次调用 open 函数时,指定的模式是 ofstream::out。当前目录中名为“scratchpad”的文件以输出模式打开并清空。而名为“precious”的文件,则要求以添加模式打开:保存文件里的原有数据,所有的新内容在文件尾部写入。
在打开“out”文件时,没有明确指明输出模式,该文件则以 out 模式打开,这意味着当前存储在“out”文件中的任何数据都将被丢弃。

只要调用 open 函数,就要设置文件模式,其模式的设置可以是显式的也可以是隐式的。如果没有指定文件模式,将使用默认值。

C++ 中的文件名

由于历史原因,IO 标准库使C 风格字符串而不是C++strings 类型的字符串作为文件名。在创建 fstream 对象时,如果调用open 或使用文件名作初始化式,需要传递的实参应为 C 风格字符串,而不是标准库 strings 对象。程序常常从标准输入获得文件名。通常,比较好的方法是将文件名读入 string 对象,而不是 C 风格字符数组。假设要使用的文件名保存在 string 对象中,则可调用 c_str 成员获取 C 风格字符串。

打开模式的有效组合

并不是所有的打开模式都可以同时指定。有些模式组合是没有意义的,例如同时以 in 和 trunc 模式打开文件,准备读取所生成的流,但却因为 trunc 操作而导致无数据可读。

文件模式组合 含义
out 打开文件做写操作,删除文件中已有的数据
out | app 打开文件做写操作,在文件尾写入
out | trunc 与 out 模式相同
in 打开文件做读操作
in | out 打开文件做读、写操作,并定位于文件开头处
in | out | trunc 打开文件做读、写操作,删除文件中已有的数据
表 2. 有效的模式组合及其含义。

上述所有的打开模式组合还可以添加 ate 模式。对这些模式添加 ate 只会改变文件打开时的初始化定位,在第一次读或写之前,将文件定位于文件末尾处。

一个打开并检查输入文件的示例程序

很多程序都要打开给定文件用于输入。由于需要在多个程序里做这件工作,我们编写一个名为 open_file 的函数实现这个功能。这个函数有两个引用形参,分别是 ifstream 和 string 类型,其中 string 类型的引用形参存储与指定 ifstream 对象关联的文件名:

// open_file 的函数 实现给定文件的输入  两个引用形参,分别是 ifstream 和 string 类型
//而且确保ifstream流是可用的!
// opens in binding it to the given file
ifstream& open_file(ifstream &in, const string &file)
{
	in.close(); // close in case it was already open
	in.clear(); // clear any existing errors  //确保ifstream流是可用的!
	// if the open fails, the stream will be in an invalid state
	in.open(file.c_str()); // open the file we were given
	return in; // condition state is good if open succeeded
}

由于不清楚流 in 的当前状态,因此首先调用 close 和 clear 将这个流设置为有效状态。然后尝试打开给定的文件。如果打开失败,流的条件状态将标志这个流是不可用的。最后返回流对象 in,此时,in 要么已经与指定文件绑定起来了,要么处于错误条件状态。

参考资料

【1】C++ Primer 中文版(第四版·特别版)

注解

【2】 c++中的c_str()用法
语法: c_str()函数返回一个指向正规C字符串的指针, 内容与本string串相同.

const char *c_str();

这是为了与c语言兼容,在c语言中没有string类型,故必须通过string类对象的成员函数c_str()把string 对象转换成c中的字符串样式。

本文许可证

本文遵循 CC BY-NC-SA 4.0(署名 - 非商业性使用 - 相同方式共享) 协议,转载请注明出处,不得用于商业目的。
CC BY-NC-SA 4.0

发布了52 篇原创文章 · 获赞 72 · 访问量 4万+

猜你喜欢

转载自blog.csdn.net/engineerxin/article/details/104585029