C++标准库--IO库(Primer C++ 第五版 · 阅读笔记)
C++标准库--IO库(Primer C++ 第五版 · 阅读笔记)
- 第8章 IO库
- 8.1、IO类
- 8.2、文件输入输出
- 8.3、string流
- 总结:
第8章 IO库
8.1、IO类
为了支持这些不同种类的IO处理操作,在istream
和ostream
之外,标准库还定义了其他一些IO类型。
- 如下图分别定义在三个独立的头文件中:
iostream
定义了用于读写流的基本类型,fstream
定义了读写命名文件的类型,sstream
定义了读写内存string对象的类型。 - 为了支持使用宽字符的语言,标准库定义了一组类型和对象来操纵
wchar_t
类型的数据。宽字符版本的类型和函数的名字以一个w
开始,例如:wcin
、wcout
和wcerr
是分别对应cin
、cout
和cerr
的宽字符版对象。 - 宽字符版本的类型和对象与其对应的普通
char
版本的类型定义在同一个头文件中。
类型
ifstream
和istringstream
都继承自istream
。因此,我们可以像使用istream
对象一样来使用ifstream
和istringstream
对象。
也就是说,我们是如何使用cin
的,就可以同样地使用这些类型的对象。例如,可以对一个ifstream
或istringstream
对象调用getline
,也可以使用>>
从一个ifstream
或istringstream
对象中读取数据。
- IO对象无拷贝或赋值
ofstream out1, out2;
out1 = out2; //错误:不能对流对象赋值
ofstream print(ofstream); //错误:不能初始化ofstream参数
out2 = print(out2); //错误:不能烤贝流对象
- 条件状态
我们可以这样使用这些成员:
//记住cin的当前状态
auto old_state = cin.rdstate(); //记住cin的当前状态
cin.clear(); //使cin有效
process_input(cin); //使用cin
cin.setstate(old_state); //将cin置为原有状态
//复位failbit和badbit,保持其他标志位不变
cin.clear(cin.rdstate() & ~cin.failbit & ~cin.badbit);
- 管理输出缓冲
导致缓冲刷新(即,数据真正写到输出设备或文件)的原因有很多:
- 程序正常结束,作为
main
函数的return
操作的一部分,缓冲刷新被执行。 - 缓冲区满时,需要刷新缓冲,而后新的数据才能继续写入缓冲区。
- 我们可以使用操纵符如
endl
来显式刷新缓冲区。 - 在每个输出操作之后,我们可以用操纵符
unitbuf
设置流的内部状态,来清空缓冲区。默认情况下,对cerr
是设置unitbuf
的,因此写到cerr
的内容都是立即刷新的。 - 一个输出流可能被关联到另一个流。在这种情况下,当读写被关联的流时,关联到的流的缓冲区会被刷新。例如,默认情况下,
cin
和cerr
都关联到cout
。因此,读cin
或写cerr
都会导致cout
的缓冲区被刷新。
刷新输出缓冲区
除了 endl
,它完成换行并刷新缓冲区的工作。IO库中还有两个类似的操纵符: flush
和 ends
。 flush
刷新缓冲区,但不输出任何额外的字符; ends
向缓冲区插入一个空字符,然后刷新缓冲区:
cout << "hi! " << endl; //输出hi和一个换行,然后刷新缓冲区
cout << "hi! " << flush; // 输出hi,然后刷新缓冲区,不附加任何额外字符
cout << "hi! " << ends; //输出hi和一个空字符,然后刷新缓冲区
unitbuf
操纵符
如果想在每次输出操作后都刷新缓冲区,我们可以使用 unitbuf
操纵符。
- 它告诉流在接下来的每次写操作之后都进行一次
flush
操作。 - 而
nounitbuf
操纵符则重置流,使其恢复使用正常的系统管理的缓冲区刷新机制:
cout << unitbuf; //所有输出操作后都会立即刷新缓冲区
//任何输出都立即刷新,无缓冲
cout << nounitbuf; //回到正常的缓冲方式
警告:如果程序崩溃,输出缓冲区不会被刷新
如果程序异常终止,输出缓冲区是不会被刷新的。当一个程序崩溃后,它所输出的数据很可能停留在输出缓冲区中等待打印。
当调试一个已经崩溃的程序时,需要确认那些你认为已经输出的数据确实已经刷新了。否则,可能将大量时间浪费在追踪代码为什么没有执行上,而实际上代码已经执行了,只是程序崩溃后缓冲区没有被刷新,输出数据被挂起没有打印而已。
关联输入和输出流
当一个输入流被关联到一个输出流时,任何试图从输入流读取数据的操作 都会先刷新关联的输出流 。标准库将cout
和 cin
关联在一起,因此下面语句
cin >> ival;
导致cout
的缓冲区被刷新。
交互式系统通常应该关联输入流和输出流。这意味着所有输出,包括用户提示信息,都会在读操作之前被打印出来。
tie
有两个重载的版本:
- 一个版本不带参数,返回指向输出流的指针。如果本对象当前关联到一个输出流,则返回的就是指向这个流的指针,如果对象未关联到流,则返回空指针。
tie
的第二个版本接受一个指向ostream
的指针,将自己关联到此ostream
。即,x.tie( &o)
将流x
关联到输出流o
。
我们既可以将一个istream
对象关联到另一个 ostream
,也可以将一个ostream
关联到另一个ostream
:
cin.tie(&cout); //仅仅是用来展示:标准库将cin和cout关联在一起
// old_tie指向当前关联到cin的流(如果有的话)
ostream* old_tie = cin.tie (nullptr); // cin不再与其他流关联
//将cin与 cerr关联;这不是一个好主意,因为cin应该关联到cout
cin.tie(&cerr); //读取cin会刷新cerr而不是cout
cin.tie(old_tie); //重建cin和cout间的正常关联
8.2、文件输入输出
头文件 fstream
定义了三个类型来支持文件 IO:ifstream
从一个给定文件读取数据,ofstream
向一个给定文件写入数据,以及fstream
可以读写给定文件。
- 我们可以用IO运算符(<<和>>)来读写文件,可以用
getline
从一个ifstream
读取数据
当我们想要读写一个文件时,可以定义一个文件流对象,并将对象与文件关联起来。
- 每个文件流类都定义了一个名为
open
的成员函数,它完成一些系统相关的操作,来定位给定的文件,并视情况打开为读或写模式。 - 创建文件流对象时,我们可以提供文件名(可选的)。如果提供了一个文件名,则open会自动被调用:
- 文件名既可以是库类型
string对象
,也可以是C风格字符数组
ifstream in(ifile); //构造一个ifstream并打开给定文件
ofstream out; //输出文件流未关联到任何文件
用fstream代替iostream&
在要求使用基类型对象的地方,我们可以用继承类型的对象来替代。这意味着,接受一个 iostream
类型引用(或指针)参数的函数,可以用一个对应的fstream
(或sstream
)类型来调用。
- 虽然
read
和print
的调用,定义时指定的形参分别是istream&
和ostream&
,但我们可以向它们传递fstream
对象。
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;
成员函数open和close
如果我们定义了一个空文件流对象,可以随后调用open
来将它与文件关联起来:
ifstream in (ifile); //构筑一个ifstream并打开给定文件
ofstream out; //输出文件流未与任何文件相关联
out.open(ifile + ".copy"); //打开指定文件
if (out) //检查open是否成功
//open成功,我们可以使用文件了
//为了将文件流关联到另外一个文件,必须首先关闭已经关联的文件。
in.close(); //关闭文件
in.open(ifile + "2"); //打开另一个文件
如果open
成功,则open
会设置流的状态,使得good()
为true
,如果调用open
失败,failbit
会被置位。
自动构造和析构
考虑这样一个程序,它的main函数接受一个要处理的文件列表。这种程序可能会有如下的循环:
//对每个传递给程序的文件执行循环操作
for (auto p = argv + l; p != argv + argc; ++p){
ifstream input(*p); //创建输出流并打开文件
if (input) { //如果文件打开成功,“处理”此文件
process (input);
}else
cerr << "couldn't open : " + string(*p);
}//每个循环步input都会离开作用域,因此会被销毁
当一个fstream
对象离开其作用域时,与之关联的文件会自动关闭。在下一步循环中,input
会再次被创建。
当一个
fstream
对象被销毁时,close
会自动被调用。
文件模式
每个流都有一个关联的文件模式(file mode),用来指出如何使用文件。
每个文件流类型都定义了一个默认的文件模式,当我们未指定文件模式时,就使用此默认模式。
与ifstream
关联的文件默认以in
模式打开;
与ofstream
关联的文件默认以out
模式打开;
与fstream
关联的文件默认以in
和out
模式打开。
- 只可以对
ofstream
或fstream
对象设定out
模式。 - 只可以对
ifstream
或fstream
对象设定in
模式。 - 只有当
out
也被设定时才可设定trunc
模式。 - 只要
trunc
没被设定,就可以设定app
模式。在app
模式下,即使没有显式指定out
模式,文件也总是以输出方式被打开。 - 默认情况下,即使我们没有指定
trunc
,以out
模式打开的文件也会被截断。为了保留以out
模式打开的文件的内容,我们必须同时指定app
模式,这样只会将数据追加写到文件末尾;或者同时指定in
模式,即打开文件同时进行读写操作。 ate
和binary
模式可用于任何类型的文件流对象,且可以与其他任何文件模式组合使用。
以out模式打开文件会丢弃已有数据
- 默认情况下,当我们打开一个
ofstream
时,文件的内容会被丢弃。阻止一个ofstream
清空给定文件内容的方法是同时指定app
模式:
//在这几条语句中,file1都被截断
ofstream out ("file1"); // 隐含以输出模式打开文件并截断文件
ofstream out2 ("filel", 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
模式。
每次调用open时都会确定文件模式
- 对于一个给定流,每当打开文件时,都可以改变其文件模式。
ofstream out; //未指定文件打开模式
out.open("scratchpad");//模式隐含设置为输出和截断
out.close (); //关闭 out,以便我们将其用于其他文件
out.open("precious", ofstream::app);//模式为输出和追加
out.close();
在每次打开文件时,都要设置文件模式,可能是显式地设置,也可能是隐式地设置。当程序未指定模式时,就使用默认值。
8.3、string流
sstream
头文件定义了三个类型来支持内存IO,这些类型可以向string
写入数据,从string
读取数据,就像string
是一个IO流一样。
istringstream
从string
读取数据,ostringstream
向string
写入数据,而头文件stringstream
既可从string
读数据也可向string
写数据。- 与
fstream
类型类似,头文件sstream
中定义的类型都继承自我们已经使用过的iostream
头文件中定义的类型。 - 除了继承得来的操作,
sstream
中定义的类型还增加了一些成员来管理与流相关联的string
。下表列出了这些操作,可以对·stringstream·对象调用这些操作,但不能对其他IO类型调用这些操作。
使用istringstream
- 当我们的某些工作是对整行文本进行处理,而其他一些工作是处理行内的单个单词时,通常可以使用
istringstream
。
考虑这样一个例子,假定有一个文件,列出了一些人和他们的电话号码。某些人只有一个号码,而另一些人则有多个——家庭电话、工作电话、移动电话等。我们的输入文件看起来可能是这样的:
//成员默认为公有
struct PersonInfo {
string name ;
vector<string> phones;
};
string line, word; //分别保存来自输入的一行和单词
vector<PersonInfo>people; //保存来自输入的所有记录
//逐行从输入读取数据,直至cin遇到文件尾(或其他错误)
while (getline(cin, line)) {
PersonInfo info; //创建一个保存此记录数据的对象
istringstream record(line); //将记录绑定到刚读入的行
record >> info.name; //读取名字
while(record >> word) //读取电话号码
info.phones.push_back(word); //保持它们
people.push_back(info); //将此记录追加到people末尾
}
使用ostringstream
- 当我们逐步构造输出,希望最后一起打印时,
ostringstream
是很有用的。
例如,对上一节的例子,我们可能想逐个验证电话号码并改变其格式。如果所有号码都是有效的,我们希望输出一个新的文件,包含改变格式后的号码。对于那些无效的号码,我们不会将它们输出到新文件中,而是打印一条包含人名和无效号码的错误信息。
由于我们不希望输出有无效电话号码的人,因此对每个人,直到验证完所有电话号码后才可以进行输出操作。但是,我们可以先将输出内容“写入”到一个内存ostringstream
中:
for (const auto &entry : people){ //对people中每一项
ostringstream formatted, badNums; //每个循环步创建的对象
for(const auto &nums : entry.phones){ //对每个数
if(!valid(nums)){
badNums << " " << nums;//将数的字符串形式存入badNums
} else
//将格式化的字符串“写入”formatted
formatted << " " << format(nums);
}
if (badNums.str().empty()) //没有错误的数
os << entry.name << " " //打印名字
<< formatted.str() << endl; //和格式化的数
else //否则,打印名字和错误的数
cerr << "input error: " << entry.name
<< " invalid number(s) " << badNums.str() << endl;
}
在此程序中,我们假定已有两个函数,valid
和 format
,分别完成电话号码验证和改变格式的功能。
程序最有趣的部分是对字符串流formatted
和 badNums
的使用。
我们使用标准的输出运算符(<<
)向这些对象写入数据,但这些“写入”操作实际上转换为string
操作,分别向formatted
和 badNums
中的string
对象添加字符。
总结:
C++使用标准库类来处理面向流的输入和输出:
iostream
处理 控制台 IOfstream
处理 命名文件 IOstringstream
完成 内存string
的 IO
类
fstream
和stringstream
都是继承自类iostream
的。输入类都继承自istream
,输出类都继承自ostream
。
因此,可以在istream
对象上执行的操作,也可在ifstream
或istringstream
对象上执行。继承自ostream
的输出类也有类似情况。
1、每个IO对象都维护一组条件状态,用来指出此对象上是否可以进行IO操作。
2、如果遇到了错误——例如在输入流上遇到了文件末尾,则对象的状态变为失效,所有后续输入操作都不能执行,直至错误被纠正。
3、标准库提供了一组函数,用来设置和检测这些状态。
注:如有不足,欢迎指正!