【C++】【C++ Primer】8-IO库

1 IO类

目前为止,我们已经使用过以下IO库设施:

  • istream类型:提供输入操作
  • ostream类型:提供输出操作
  • cin:istream类型对象,从标准输入读取数据
  • cout:ostream类型对象,向标准输出写入数据
  • cerr:ostream类型对象,向标准错误输出错误信息
  • >>运算符:用于从istream类型对象读取数据
  • <<运算符:用于向ostream类型对象写入数据
  • getline函数:从给定的istream类型对象读取一行数据,存入给定的string对象中。

默认情况下,这些对象都是关联到控制台窗口的。但应用程序常常要读写文件、使用IO操作处理string中的字符、读写需要宽字符支持的语言。为了支持这些不同种类的的IO操作,在istream和ostream之外,标准库还定义了一些IO类型。

表1 IO库类型和头文件
头文件IO库类型和头文件
iostreamistream、wistream从流读取数据
ostream、wostream向流写入数据
iostream、wiostream读写流
fstreamifstream、wifstream从文件读取数据
ofstream、wofstream向文件写入数据
fstream、wfstream读写文件
sstreamistringstream、wistringstream从string读取数据
ostringstream、wostringstream向string写入数据
stringstream、wstringstream读写string

为了支持使用宽字符的语言,标准库定义了一组类型和对象来操纵wchar_t类型的数据。宽字符版本的类型和函数名以w开始。

1.1 IO类型间的关系

设备类型和字符大小都不会影响我们要执行的IO操作。譬如用>>读取数据,不用管是从console、磁盘文件还是string读取,也不用管读取的字符是存入char对象内还是存入wchar_t对象内。

标准库之所以能忽略不同类型的流之间的差异,是通过继承机制实现的。类型ifstream和istringstream都继承自istream。因此可以像使用istream对象一样使用ifstream和istringstream对象。

1.2 IO对象不能拷贝或赋值

不能拷贝或对IO对象赋值,因此不能将形参或返回类型设置为流类型。进行IO操作的函数通常以引用方式传递和返回流。

读写一个IO对象会改变其状态,所以IO对象的引用不能是const的。

1.3 条件状态

IO操作可能会发生错误。一些错误是可恢复的,发生在系统深处的错误则超出了应用程序可以修复的范围。下表列出了IO类定义的一些函数和标志,用于访问和操纵流的条件状态。

表2 IO库类型和头文件
strm::iostatestrm是表1中的IO类型。iostate是机器相关的类型,提供了表达条件状态的完整功能
strm::badbitstrm::badbit用来指出流已崩溃
strm::failbitstrm::failbit用来指出一个IO操作失败了
strm::eofbitstrm::eofbit用来指出流到达了文件结束
strm::goodbitstrm::goodbit用来指出流未处于错误状态。此值保证为0
s.eof()若流s的eofbit置位则返回true
s.fail()若流s的failbit或badbit置位,则返回true
s.bad()若流s的badbit置位,则返回true
s.good()若s处于有效状态,则返回true
s.clear()若流s中所有条件状态复位,将流的状态设置为有效。返回void
s.clear(flags)根据给定的flags标志位,将流s中对应条件状态位复位。flags的类型为strm::iostate。返回void
s.setstate(flags)根据给定的flags标志位,将流s中对应条件状态位置位。flags的类型为strm::iostate。返回void
s.rdstate()返回流s的当前状态,返回值类型为strm::iostate

如果从标准输入读取的数据和接收数据的变量类型不符,则读操作失败,cin进入错误状态。如果输入EOF,cin也会进入错误状态。

一旦一个流发生错误,后续的IO操作都会失败。只有当一个流处于无错状态时,才能对其读写数据。

由于流可能处于错误状态,因此在使用流之前要先检查。最简单的方式是将其作为条件使用。流处于有效状态时,条件为真。

while (cin >> word)

1.3.1 查询流的状态

IO库定义了一个与机器无关的iostate类型,它提供了表达流状态的完整功能。iostate应作为一个位集合来使用。

IO库定义了4个iostate类型的constexpr值,表示特定的位模式。这些值用于表示特定类型的IO条件,可以与位运算符一起使用来一次性检测或设置多个标志位。

  • badbit:系统级错误,如不可恢复的读写错误。一旦badbit被置位,流就无法使用了。
  • failbit:可恢复错误,如期望读取数值却读出字符。这种问题通常可以修复,流可以继续使用。
  • eofbit:到达文件结束位置,eofbit被置位。此时failbit也会被置位。
  • gootbit:goodbit值为0时,表示流未发生错误。

badbit、failbit、eofbit中的任意一个被置位,检测流状态的条件都会失败。

标准库还定义了一组函数来查询这些标志位的状态。s.good()在所有错误均未置位时返回true。s.bad()、s.fail()、s.eof()在对应错误被置位时返回true。badbit置位时,s.fail()也会返回true。因此,使用s.good()或s.fail()来确定流的总体状态。将流作为条件使用的代码就等价于!s.fail()。

1.3.2 管理流的状态

s.rdstate()返回一个iostate值,表示流的当前状态。

s.setstate()操作将给定条件位置位,表示发生了对应错误。

s.clear()是一个重载成员:

  • s.clear()复位所有错误标志位。执行后s.good()会返回true。
  • s.clear(flags)接受一个iostate值,表示流的新状态。需要复位单一条件状态位时,先用rdstate读出当前条件状态,然后用位操作将所需位复位,生成新的状态,然后再通过s.clear(flags)复位。
cin.clear(cin.rdstate() & ~cin.failbit & ~cin.badbit);

1.4 管理输出缓冲

每个输入流都管理一个缓冲区,用来保存程序读写的数据。有了缓冲机制,OS可以将程序的多个输出操作组合成单一的系统级写操作。由于系统级写操作比较耗时,缓冲机制可以带来很大的性能提升。

刷新缓冲的原因有以下几种:

  • 程序正常结束,main函数的return操作会刷新缓冲。
  • 缓冲区满,会刷新缓冲,以容纳新的数据。
  • 使用操纵符显式刷新缓冲区。
  • 使用操纵符unitbuf设置流,使流立即刷新。cerr默认是设置unibuf的,因此写入cerr的数据会立刻刷新。
  • 一个输出流可以关联到另一个流。当读写关联的流时,该输出流的缓冲区会被刷新。默认情况下cin和cerr关联到cout。因此读cin或写cerr都会导致cout的缓冲区被刷新。

1.4.1 刷新输出缓冲区

共有三个操纵符可以刷新缓冲区。

表3 刷新缓冲区的操纵符
操作符功能
endl换行,然后刷新缓冲区
ends向缓冲区插入一个空字符,然后刷新缓冲区
flush刷新缓冲区,不输出任何额外字符

示例代码:

cout << "hi" << endl;     // 输出hi和一个换行,然后刷新缓冲区
cout << "hi" << ends;     // 输出hi和一个空字符,然后刷新缓冲区
cout << "hi" << flush;    // 输出hi,然后刷新缓冲区

1.4.2 unitbuf操纵符

每次输出都写上操纵符比较繁琐,我们可以使用unitbuf操纵符设置流,之后每次写操作都会自动做一次flush。

使用nounitbuf操纵符设置流则可以使其恢复正常的缓冲区刷新机制。

cout << unitbuf;      // 设置任何输出后立即刷新
// 此时任何输出都会立即刷新,无缓冲
cout << nounitbuf;    // 恢复正常缓冲机制

1.4.3 关联输入流和输出流

当一个输入流被关联到一个输出流时,从输入流读取数据的操作会先刷新关联的输出流。交互式系统应当将输入流和输出流关联。从而确保所有输出都在读操作之前被打印出来。

标准库将cout和cin关联在一起,因此以下语句会导致cout的缓冲区被刷新:

cin >> ival;

使用tie函数来关联流。

不带参数的tie函数返回指向输出流的指针。如果本对象当前关联到一个输出流,则返回指向这个流的指针。如果对象未关联到流,则返回空指针。

tie函数的第二个版本接受一个指向ostream的指针,将自己关联到ostream。即x.tie(&o)将流x关联到输出流o。

不仅可以将一个istream关联到另一个ostream,也可以将两个ostream关联在一起。

如果要将某个流重新绑定到另一个流上,只要将新流的指针传递给tie即可。如果想彻底解开流的关联,调用tie时传入空指针即可。

每个流最多同时关联到一个流,但多个流可以同时关联到一个流。

2 文件输入输出

头文件fstream定义了三个类型,用于支持文件IO:

  • ifstream从一个给定文件读取数据;
  • ofstream向一个给定文件写入数据;
  • fstream读写给定文件。

ifsream、ofstream、fstream和cin、cout的使用方法一样。可以使用IO运算符(<<、>>)来读写文件,也可以用getline从一个ifstream读取数据。除了继承自iostream类型的行为外,fstream中定义的类型还增加了一些新的成员来管理与流关联的文件:

表4 fstream特有的操作
fstream fstrm;创建一个未绑定的文件流。fstream是头文件fstream中定义的类型
fstream fstrm(s);创建一个fstream,并打开名为s的文件。s可以是string类型或指向C风格字符串的指针。这些构造函数都是explicit的。默认的文件模式mode依赖于fstream的类型
fstream fstrm(s, mode);与前一个构造函数类似,但按指定的mode打开文件。
fstrm.open(s)打开名为s的文件,并将文件与fstrm绑定。s可以是string类型或指向C风格字符串的指针。默认的文件mode依赖于fstream的类型,返回void
fstrm.close()关闭与fstrm绑定的文件。返回void
fstrm.is_open()返回一个bool值,指出与fstrm关联的文件是否成功打开且尚未关闭

2.1 使用文件流对象

读写文件时,可以定义一个文件流对象,并将其与文件关联起来。

2.1.1 成员函数open与close

每个文件流函数都定义了名为open的成员函数,定位给定的文件并视情况打开为读或写模式。

如果定义了一个空文件流对象,可以随后调用open函数将其与文件关联起来。

如果在创建文件流对象时提供文件名,则open函数会被自动调用。在C++新标准中,文件名既可以是string对象,也可以是指向C风格字符串的指针。

ifstream in(ifile);    // 构造了一个ifstream对象,并自动调用open函数,打开ifile
ofstream out;          // 构造了一个ofstream对象,但未关联到具体文件
out.open(ofile);       // 将out与ofile关联起来

如果调用open失败,failbit会被置位。考虑到失败的可能性,应检测open是否成功。

if (out) {
	// 执行操作
}

一旦一个文件流成功打开,就会保持与对应文件的关联。对已经打开的文件流再次调用open会失败,此时failbit会被置位,随后对该文件流的操作都会失败。如果想将该文件流关联到其他文件,首先要调用close关闭已经关联的文件,然后重新打开新的文件。

in.close();
in.open(ifile2);

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

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

while (1) {
	ifstream input(ifile);
	if (input) {
		// 执行操作
	}
} // ifstream是局部变量,每次循环执行结束均会销毁

2.2 文件模式

每个流都有一个关联的文件模式,用于指出如何使用文件。

表5 文件模式
in以读方式打开
out以写方式打开
app每次写操作前均定位到文件末尾
ate打开文件后立即定位到文件末尾
trunc截断文件
binary以二进制形式进行IO

无论是使用文件名初始化流来隐式打开文件,还是调用open显式打开文件,都可以指定文件模式。指定文件模式有以下限制:

  • 只能对ofstream或fstream对象设定out模式(不能对ifstream对象设置out模式)
  • 只能对ifstream或fstream对象设定in模式(不能对ofstream对象设置in模式)
  • 只有out模式被设定时,才能设置trunc模式
  • 只要trunc模式没有被设定,就可以设定为app模式。如果app模式被设定,即使不显式指定out模式,文件也总是以输出方式打开。
  • 默认情况下,即使不指定trunc,以out模式打开的文件也会被截断。为了保留以out模式打开的文件的内容,必须同时指定app模式或同时指定in模式。
  • ate和binary模式可用于任何类型的文件流对象,且可以与其他任何文件模式组合使用。

每个文件流类型都定义了默认的文件模式,如果我们不显式指定文件模式,就使用默认模式。

  • 与ifstream关联的文件默认以in模式打开
  • 与ofstream关联的文件默认以out模式打开
  • 与fstream关联的文件默认以in和out模式打开
// 以下三条语句,file1都被截断
ofstream out("file1");                                      // 未显式指定mode,默认以out模式打开文件,因此会截断文件
ofstream out2("file1", ofstream::out);                      // 显式将mode指定为out,因此会截断文件
ofstream out3("file1", ofstream::out | ofstream::trunc);    // 显式将mode指定为out+trunc,因此会截断文件

// 为了保留文件内容,必须显式指定app模式
ofstream app("file2", ofstream::app);                       // 显式将mode指定为app,隐含out
ofstream app2("file2", ofstream::out | ofstream::app);      // 显式将mode指定为out+app

对于一个给定流,每次打开文件时都可以改变其文件模式。譬如以下代码,第一次open时没有显式指定mode,默认以out模式打开(out模式还隐含地使用trunc模式)。第二次open时显示指定了app模式。

ofstream out;
out.open("scratchpad");
out.close();

out.open("precious", ofstream::app);
out.close();

3 string流

sstream头文件定义了三个类型来支持内存IO。istringstream从string读取数据,ostringstream向string写入数据,stringstream可以读写string。

头文件sstream中定义的类型都继承自iostream头文件中定义的类型。除了继承的操作,sstream中定义的类型还增加了一些成员,用于管理与流关联的string。

表6 stringstream特有的操作
sstream strm;strm是一个未绑定的stringstream对象
sstream strm(s);strm是一个sstream对象,保存string s的一个拷贝。这个构造函数是explicit的
strm.str()返回strm保存的string的拷贝
strm.str(s)将string s 拷贝到strm中,返回void

4 示例

文件phonebook.txt中存有一些人和他们的电话号码,内容如下:

Morgan 2015552368 8625550123
drew 9735550130
lee 6095550132 2015550175 8005550000

我们的程序读取phonebook.txt中的数据,逐个验证电话号码并改变其格式,如果所有号码都是有效的,则输出到formartted.txt中。如果存在无效的号码,则不输出到formatted.txt中,而是打印包含人名和无效号码的错误信息。

#include <fstream>
#include <iostream>
#include <sstream>
#include <string>
#include <vector>

using std::cin;
using std::cerr;
using std::cout;
using std::endl;
using std::ifstream;
using std::istringstream;
using std::ofstream;
using std::ostringstream;
using std::string;
using std::vector;

struct PersonInfo {
    string name;
    vector<string> phones;
};

void readPhonebook(string infile, vector<PersonInfo> &people)
{
    string line;
    ifstream fin(infile);

    while (getline(fin, line)) {
        PersonInfo info;
        string word;
        istringstream record(line);

        record >> info.name;
        while (record >> word) {
            info.phones.push_back(word);
        }
        people.push_back(info);
    }

    return;
}

bool valid(const string &nums)
{
    return true;
}

const string format(const string &nums)
{
    return nums;
}

void writePhonebook(string outfile, vector<PersonInfo> &people)
{
    ofstream fout(outfile);

    for (const auto &entry : people) {
        ostringstream formatted, badNums;
        for (const auto &nums : entry.phones) {
            if (!valid(nums)) {
                badNums << " " << nums;
            } else {
                formatted << " " << format(nums);
            }
        }
        if (badNums.str().empty()) {
            fout << entry.name << " " << formatted.str() << endl;
        } else {
            cerr << "input error: " << entry.name
                << " invalid number(s) " << badNums.str() << endl;
        }
    }
}

int main(int argc, char **argv)
{
    vector<PersonInfo> people;
    string infile("./phonebook.txt");
    string outfile("./formatted.txt");

    readPhonebook(infile, people);
    writePhonebook(outfile, people);

    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值