【C++基础】系列博客为参考《C++ Primer中文版(第5版)》(C++11标准)一书,自己所做的读书笔记。
本文为原创文章,未经本人允许,禁止转载。转载请注明出处。
1.文件输入输出
头文件fstream定义了三个类型来支持文件IO:ifstream从一个给定文件读取数据,ofstream向一个给定文件写入数据,以及fstream可以读写给定文件。
这些类型提供的操作与我们之前已经使用过的对象cin和cout的操作一样。特别是,我们可以用IO运算符(<<
和>>
)来读写文件,可以用getline从一个ifstream读取数据。
除了继承自iostream类型的行为之外,fstream中定义的类型还增加了一些新的成员来管理与流关联的文件。下表列出了这些操作,我们可以对fstream、ifstream和ofstream对象调用这些操作,但不能对其他IO类型调用这些操作。
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模式。