【C++基础】第四十八课:[IO库]文件输入输出

ifstream,ofstream,fstream,open(),close(),is_open(),in,out,app,ate,trunc,binary

Posted by x-jeff on August 9, 2022

【C++基础】系列博客为参考《C++ Primer中文版(第5版)》C++11标准)一书,自己所做的读书笔记。
本文为原创文章,未经本人允许,禁止转载。转载请注明出处。

1.文件输入输出

头文件fstream定义了三个类型来支持文件IO:ifstream从一个给定文件读取数据,ofstream向一个给定文件写入数据,以及fstream可以读写给定文件。

这些类型提供的操作与我们之前已经使用过的对象cin和cout的操作一样。特别是,我们可以用IO运算符(<<>>)来读写文件,可以用getline从一个ifstream读取数据。

除了继承自iostream类型的行为之外,fstream中定义的类型还增加了一些新的成员来管理与流关联的文件。下表列出了这些操作,我们可以对fstream、ifstream和ofstream对象调用这些操作,但不能对其他IO类型调用这些操作。

explicit

2.使用文件流对象

当我们想要读写一个文件时,可以定义一个文件流对象,并将对象与文件关联起来。每个文件流类都定义了一个名为open的成员函数,它完成一些系统相关的操作,来定位给定的文件,并视情况打开为读或写模式。

创建文件流对象时,我们可以提供文件名(可选的)。如果提供了一个文件名,则open会自动被调用:

1
2
ifstream in(ifile); //构造一个ifstream并打开给定文件
ofstream out; //输出文件流未关联到任何文件

在新C++标准中(C++11),文件名既可以是库类型string对象,也可以是C风格字符数组。旧版本的标准库只允许C风格字符数组。

2.1.用fstream代替iostream&

在要求使用基类型对象的地方,我们可以用继承类型的对象来替代。这意味着,接受一个iostream类型引用(或指针)参数的函数,可以用一个对应的fstream(或sstream)类型来调用。

比如我们之前定义了read和print函数,在下面这个例子中,我们假定输入和输出文件的名字是通过传递给main函数的参数来指定的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ifstream input(argv[1]); //打开销售记录文件
ofstream output(argv[2]); //打开输出文件
Sales_data total; //保存销售总额的变量
if (read(input, total)) { //读取第一条销售记录
	Sales_data trans; //保存下一条销售记录的变量
	while (read(input, trans)) { //读取剩余记录
		if (total.isbn() == trans.isbn()) //检查isbn
			total.combine(trans); //更新销售总额
		else {
			print(output, total) << endl; //打印结果
			total = trans; //处理下一本书
		}
	}
	print(output, total) << endl; //打印最后一本书的销售额
} else
	cerr << "No data?!" << endl;

虽然read和print函数定义时指定的形参分别是istream&和ostream&,但我们可以向它们传递fstream对象。

再举个例子,假设t.txt里的内容如下:

1
2
3
1 2 3
4 5 6
7 8 9

代码如下:

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
#include <iostream>
#include <fstream>

using namespace std;

class Sales_data
{
public:
    int bookNo;
    int units_sold;
    double revenue;
};

istream &read(istream &is, Sales_data &item)
{
    double price = 0;
    is >> item.bookNo >> item.units_sold >> price;
    item.revenue = price * item.units_sold;
    return is;
}

int main(int argc, const char * argv[]) {
    system("pwd\n");
    ifstream input("t.txt");
    Sales_data trans;
    while(read(input,trans))
    {
        cout << trans.bookNo << " "<<trans.units_sold<<endl;
    }
    //上面的while循环输出为:
    //1 2
    //4 5
    //7 8
    //相当于是每循环一次,就会读取一行
    if(read(input,trans)) //因为while循环已经读到文件末尾了,所以此处if判断失败,直接跳到return语句
    {
        while(read(input,trans))
        {
            cout << trans.bookNo << " "<<trans.units_sold<<endl;
        }
    }
    return 0;
}

2.2.成员函数open和close

如果我们定义了一个空文件流对象,可以随后调用open来将它与文件关联起来:

1
2
3
ifstream in(ifile); //构筑一个ifstream并打开给定文件
ofstream out; //输出文件流未与任何文件相关联
out.open(ifile + ".copy"); //打开指定文件

如果调用open失败,failbit会被置位。因为调用open可能失败,进行open是否成功的检测通常是一个好习惯:

1
2
if (out) //检查open是否成功
	//open成功,我们可以使用文件了

这个条件判断与我们之前将cin用作条件相似。如果open失败,条件会为假,我们就不会去使用out了。

一旦一个文件流已经打开,它就保持与对应文件的关联。实际上,对一个已经打开的文件流调用open会失败,并会导致failbit被置位。随后的试图使用文件流的操作都会失败。为了将文件流关联到另外一个文件,必须首先关闭已经关联的文件。一旦文件成功关闭,我们可以打开新的文件:

1
2
in.close(); //关闭文件
in.open(ifile + "2"); //打开另一个文件

如果open成功,则open会设置流的状态,使得good()为true。

2.3.自动构造和析构

考虑这样一个程序,它的main函数接受一个要处理的文件列表。这种程序可能会有如下的循环:

1
2
3
4
5
6
7
8
9
10
//对每个传递给程序的文件执行循环操作
for (auto p = argv + 1; p != argv + argc; ++p)
{
	ifstream input(*p); //创建输出流并打开文件
	if (input) 
	{
		process(input);
	} else
		cerr << "couldn't open: " + string(*p);
} //每个循环步input都会离开作用域,因此会被销毁

因为input是for循环的局部变量,它在每个循环步中都要创建和销毁一次。当一个fstream对象离开其作用域时,与之关联的文件会自动关闭。在下一步循环中,input会再次被创建。

当一个fstream对象被销毁时,close会自动被调用。

3.文件模式

每个流都有一个关联的文件模式(file mode),用来指出如何使用文件。下表列出了文件模式和它们的含义。

无论用哪种方式打开文件,我们都可以指定文件模式,调用open打开文件时可以,用一个文件名初始化流来隐式打开文件时也可以。指定文件模式有如下限制:

  • 只可以对ofstream或fstream对象设定out模式。
  • 只可以对ifstream或fstream对象设定in模式。
  • 只有当out也被设定时才可设定trunc模式。
  • 只要trunc没被设定,就可以设定app模式。在app模式下,即使没有显式指定out模式,文件也总是以输出方式被打开。
  • 默认情况下,即使我们没有指定trunc,以out模式打开的文件也会被截断。为了保留以out模式打开的文件的内容,我们必须同时指定app模式,这样只会将数据追加写到文件末尾;或者同时指定in模式,即打开文件同时进行读写操作。
  • ate和binary模式可用于任何类型的文件流对象,且可以与其他任何文件模式组合使用。

trunc:如果该文件已经存在,其内容将在打开文件之前被截断,即把文件长度设为0。例如有t.txt:

1
2
3
1 2 3
4 5 6
7 8 9
1
2
3
4
5
ifstream input("t.txt",ifstream::trunc);
if(read(input,trans)) //if判定失败
{
	//...
}

每个文件流类型都定义了一个默认的文件模式,当我们未指定文件模式时,就使用此默认模式。与ifstream关联的文件默认以in模式打开;与ofstream关联的文件默认以out模式打开;与fstream关联的文件默认以in和out模式打开。

微改一下之前的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int main(int argc, const char * argv[]) {
    system("pwd\n");
    ifstream input("t.txt",ofstream::out); //ifstream也可以设定out模式,和书里的解释相悖,在此记录一下
    //ifstream input("t.txt",ifstream::out); 和上一句一样的输出结果
    Sales_data trans;
    istream& status=read(input,trans);
    while(read(input,trans))
    {
        cout << trans.bookNo << " "<<trans.units_sold<<endl;
    }
    //输出少了第一行,为:
    //4 5
    //7 8
    return 0;
}

再举一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
#include <fstream>
using namespace std;
int main()
{
    ifstream file;
    //方式一
    file.open("t.txt",ifstream::in); //用in的方式打开不会清空t.txt中已有的内容,后续程序可以正常输出t.txt中的内容(如果t.txt不存在,则不会自动创建该txt,后续的if判定也是失败的)
    //方式二
    file.open("t.txt",ifstream::in | ifstream::trunc); //搭配trunc使用会使得if判定失败
    if(file)
    {
        string line;
        while(getline(file,line))
        {
            cout << line << endl;
        }
    }
    return 0;
}

3.1.以out模式打开文件会丢弃已有数据

默认情况下,当我们打开一个ofstream时,文件的内容会被丢弃。阻止一个ofstream清空给定文件内容的方法是同时指定app模式:

1
2
3
4
5
6
7
//在这几条语句中,file1都被截断
ofstream out("file1"); //隐含以输出模式打开文件并截断文件
ofstream out2("file1", ofstream::out); //隐含地截断文件
ofstream out3("file1", ofstream::out | ofstream::trunc);
//为了保留文件内容,我们必须显式指定app模式
ofstream app("file2", ofstream::app); //隐含为输出模式
ofstream app2("file2", ofstream::out | ofstream::app);

保留被ofstream打开的文件中已有数据的唯一方法是显式指定app或in模式。

3.2.每次调用open时都会确定文件模式

对于一个给定流,每当打开文件时,都可以改变其文件模式。

1
2
3
4
5
ofstream out; //未指定文件打开模式
out.open("scratchpad"); //模式隐含设置为输出和截断
out.close(); //关闭out,以便我们将其用于其他文件
out.open("precious", ofstream::app); //模式为输出和追加
out.close();

app的全称为append模式。