C++ Primer 学习笔记 第八章 IO库

C++不直接处理输入输出,而是通过一族定义在标准库中的类型来处理IO。这些类型支持从设备读取数据、向设备写入数据的IO操作,设备可以是文件、控制台窗口等。

IO库定义了读写内置类型值的操作。此外,一些类,如string,通常也会定义类似的IO操作,来读写自己的对象。

之前已经使用的IO库设施:istream类型、ostream类型、cin、cout、cerr、>>运算符、<<运算符、getline函数。

之前使用的IO类型和对象都是操纵char数据的,其对象默认都是关联到用户的控制台窗口的,我们不能限制实际应用程序仅从控制台窗口进行IO操作,应用程序常常需要读写命名文件。此外,还有内存IO,即从string读取写入数据。为支持这些不同类型的IO处理操作,标准库还定义了很多IO类型:

头文件 类型
iostream istream/wistream 从流读数据
ostream/wostream 向流写数据
iostream/wiostream 读写流
fstream ifstream/wifstream 从文件读数据
ofstream/wofstream 向文件写数据
fstream/wfstream 读写文件
sstream istringstream/wistringstream 从string读数据
ostringstream/wostringstream 向string写数据
stringstream/wstringstream 读写string

为支持使用宽字符的语言,标准库定义了一组类型和对象来操纵wchar_t类型数据。宽字符版本的数据和函数以一个w开始,如wcin对应cin的宽字符版对象。宽字符版本的IO类型和对象与普通char版本的类型和对象定义在同一个文件中,如头文件fstream定义了ifstream和wifstream类型。

设备类型和字符大小不会影响我们要执行的IO操作,如我们可以用>>读取数据,而不用管是从一个控制台窗口、一个磁盘文件还是一个string中读取。类似地,我们也不用管读取的字符能存入一个char对象内还是需要一个wchar_t对象来存储。

标准库能使我们忽略这些不同类型的流之间的差异,这是通过继承机制实现的。利用模板,我们可以使用具有继承关系的类,而不必了解继承机制如何工作的细节。简单来说,继承机制使我们可以声明一个特定类型的类继承自另一个类。我们通常可以将一个派生类(继承类)对象当做其基类(所继承的类)对象使用。

类型ifstream和istringstream都继承自istream,因此,我们可以像使用istream对象一样来使用ifstream和istringstream对象。

IO对象不能拷贝或赋值:

ofstream out1, out2;
out1 = out2;    //错误,流对象不能赋值
ofstream print(ofstream);    //错误,不能初始化ofstream参数,函数不能以ostream为返回类型
out2 = print(out2);    //错误,不能拷贝流对象

由于不能拷贝IO对象,我们不能将形参或返回类型设置为流类型,进行IO操作的函数通常以引用方式传递和返回流。读写IO对象会改变其状态,因此传递和返回的引用不能是const的。

IO操作可能会发生各种错误,IO类定义了一些函数和标志,可以以帮助我们访问和操纵流的条件状态:
在这里插入图片描述
在这里插入图片描述
例子:

int ival;
cin >> ival;    //若此时输入B,本期待读取一个int,却得到了B,cin会进入错误状态

类似地,当我们输入文件结束标识时,cin也会进入错误状态。一旦流发生错误,其上后续的IO操作都会失败。确定一个流是否处于良好状态的最简单方法:

while (cin >> word) ;

将流作为条件使用,只能告诉我们流是否有效,而无法告诉我们具体发生了什么,如是输入了文件结束标识符还是IO设备故障。

IO库定义了与机器相关的iostate类型,它提供了表达流状态的完整功能,这个类型应作为一个位集合来使用。IO库定义了四个iostate类型的constexpr值,表示特定的位模式,这些值表示特定的IO条件,可以与位运算符一起使用来一次性检测或设置多个标志位。

badbit表示系统级错误,如不可恢复的读写错误,通常,一旦badbit被置位,流就再也无法使用了。在发生可恢复错误后,failbit被置位,如期望读取数值却读出一个字符等错误,这种问题通常是可以修正的,流还可以继续使用。如果到达文件结束位置,eofbit和failbit都会被置位。goodbit值为0表示流未发生错误。如果badbit、failbit和eofbit任一个被置位,则检测流状态的条件会失败。

fail函数在badbit或failbit被置位时都返回true,因此,我们将流当做条件使用时等价于!strm.fail()。

流对象的rdstate成员返回一个iostate值,对应流当前状态。setstate操作将给定条件位置位,表示发生了对应的错误。clear成员是一个重载成员:它有一个不接受参数的版本,另一个是接受一个iostate类型参数的版本。它不接受参数的版本复位所有错误标志位,执行clear()后,调用good会返回true(即goodbit为0)。使用例子:

auto old_state = cin.rdstate();    //记录当前cin状态
cin.clear();    //使cin有效
process_input(cin);    //使用cin
cin.setstate(old_state);    //将cin置为原有状态
cin.clear(old_state);    //将cin置为原有状态,与上句代码作用相同

带参数的clear版本接受一个iostate值,表示流的状态,为复位单一条件状态位,我们首先用rdstate读出当前条件状态,然后用位操作将所需位复位来生成新的状态。如下例,将failbit和badbit复位,但保持eofbit不变:

cin.clear(cin.rdstate() & ~cin.failbit & ~cin.badbit);    //复位failbit和badbit,其他不变

在控制台输入文件结束符(Ctrl+Z)时,cin的badbit被置位:

istream& ReadFile(istream& is) {
    char c;
    auto old_state = cin.rdstate();
    while (is >> noskipws >> c) {
        cout << c;
    }
    is.clear();
    return is;
}

int main() {
    ReadFile(cin);
    if (cin.good()) {    //true
        cout << "cin可以正常使用。" << endl;
    }
}

每个输出流都管理一个缓冲区,用来保存程序读写的数据,如:

os << "Please enter a value:";

文本串可能立即被打印出来,但也有可能被操作系统保存在缓冲区中,随后再打印。有了缓冲机制,操作系统就可以将程序的多个输出操作组合成单一的系统级写操作。由于设备的写操作可能很耗时,允许操作系统将多个输出组合为单一的设备写操作可以带来很大的性能提升。

导致缓冲刷新(数据真正写出到设备或文件)的原因:
1.程序正常结束,作为main函数的return操作的一部分,缓冲刷新被执行。
2.缓冲区满时,需要刷新缓冲,后面的数据才能继续写入缓冲区。
3.使用操作符,如endl来刷新缓冲区。
4.每个输出操作之后,可以用操纵符unitbuf设置流的内部状态,来清空缓冲区。默认情况下,对cerr是设置unitbuf的,因此写到cerr的内容都是立即刷新的。
5.一个输出流可能被关联到另一个流。这种情况下,在读写被关联的流时,关联的流的缓冲区会被刷新。如默认情况下,cin和cerr都关联到cout,因此读cin和写cerr都会导致cout缓冲区被刷新。

刷新输出缓冲区:

cout << "hi!" << endl;    //输出内容和一个换行,然后刷新缓冲区
cout << "hi!" << flush;    //输出内容,然后刷新缓冲区
cout << "hi!" << ends << 1;    //输出内容和一个空字符,然后刷新缓冲区。此例输出hi! 1

如果想每次输入后都刷新缓冲区:

cout << unitbuf;    //所有输出操作后都会立即刷新缓冲区
cout << nounitbuf;    //回到正常缓冲方式

如果程序异常终止,输出缓冲区不会被刷新。当调试一个已经崩溃的程序时,要确认你认为已经输出的数据已经输出了,否则可能浪费大量时间追踪代码输出问题。

当一个输入流被关联到一个输出流时,任何试图从输入流读取数据的操作都会先刷新关联的输出流。标椎库将cout和cin关联在一起:

cin >> ival;    //cout被刷新

交互式系统应该关联输入输出流,这样能保证所有输出,都会在读操作之前被打印出来,否则可能出现输入后的回应是输入前的输出的情况。

tie函数有两个重载版本。一个不带参数,返回指向本对象当前被关联到的一个输出流的指针(如没有关联到流,则返回空指针);另一个接受一个指向ostream的指针,将自己关联到ostream。我们可以把一个istream关联到另一个ostream,也可以将一个ostream关联到另一个ostream:

cin.tie(&cout);    //标准库将cin和cout关联在一起
ostream *old_tie = cin.tie(nullptr);    //cin不再与其他流关联,old_tie指向当前cin关联的流(如果有的话)
cin.tie(&cerr);    //将cin与cerr关联,这不是一个好主意,cin应该关联到cout
cin.tie(old_tie);    //重建cin与cout的正常关联

fstream头文件中除了继承自iostream的行为外,还增加了新的成员管理与流关联的文件,fstream、ifstream、ofstream类型对象可以调用这些操作:
在这里插入图片描述
读写文件时,定义一个文件流对象,并将对象与文件关联起来。每个文件流类都定义了一个open函数,它完成一些系统相关的操作,定位给定的文件,并视情况打开为读或写默认。创建文件流对象时,我们可以提供文件名(可选),如提供了文件名,则open会自动调用:

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

上例中,对文件名类型来说,C++11标准允许的类型为string对象和C风格字符数组,但旧版本的标准库只允许C风格字符数组。

在使用基类对象的地方,我们可以用继承类型的对象来替代。这意味着接受一个iostream类型引用或指针参数的函数,可以用一个对应的fstream(或sstream)类型来调用。即有一个函数接受一个ostream&参数时,我们可以传递给它一个ofstream对象,对istream&和ifstream也类似。(因为流对象不能赋值和拷贝,因此传参时只能传引用或指针)

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

ifstream in(ifile);    //构筑一个istream对象并打开指定文件
ofstream out;    //输出文件流,未关联文件
out.open(ifile + ".copy");    //打开指定文件

如果调用open失败,failbit会被置位,要记得检测是否打开文件成功:

if (out) ;    //检查open是否成功

对一个打开的文件流调用open会失败,并会导致failbit被置位,随后的使用文件流的操作都会失败。为了将文件流关联到另一个文件,必须首先关闭已经关联的文件:

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

如果open成功,open会设置good()为true。

在循环中定义的文件流对象会在每个循环步中创建和销毁一次。当一个fstream对象离开作用域时,与之关联的文件会自动关闭,即fstream对象被销毁时,close会自动被调用:

for (auto p = argv + 1; p != argv + argc; ++p) {    //argv[argc]是空指针,程序参数从第二个开始
    ifstream input(*p);
    if (input) {
        process(input);
    }
    else {
        cerr << "couldn't open: " + string(*p);
    }
}    //每次循环结束都会销毁input,并且会自动关闭文件

从文件中读取内容以行保存在vector<string>中:

int main() {
    ifstream ifs("文件路径。");
    if (!ifs) {
        cerr << "打不开文件。" << endl;
    }

    vector<string> vs;
    string s;
    while (getline(ifs,s)) {
        vs.push_back(s);
    }

    for (string line : vs) {
        cout << line << endl;
    }
}

每一个流都有一个关联的文件模式,用来指出如何使用文件:
在这里插入图片描述
指定文件模式的限制:
1.只可以对ofstream或fstream对象设定out模式。
2.只可以对ifstream或fstream对象设定in模式。
3.只有当out也被设定时才可设定trunc模式。
4.只要trunc模式没被设定,就可以设定app模式,app模式下,即使文件没有显式指定out模式,也总是会以out方式打开。
5.默认,即使我们没有指定trunc,以out模式打开文件也会被截断。为了保留out模式打开的文件内容,我们必须同时指定app模式,这样只会将数据追加到文件末尾,或同时指定in模式,这样就能同时读写文件。
6.ate和binary模式可用于任何类型文件,也可与任何文件模式组合。

ifstream关联的文件默认模式为in,ofstream关联的文件默认模式为out,fstream关联的文件默认模式为in和out。

默认情况下,打开一个ofstream时,文件内容被丢弃,阻止其丢弃内容方法为同时指定app模式:

ofstream out("file1");    //file1被截断
ofstream out2("file1", ofstream::out);    //file1被截断
ofstream out3("file1", ofstream::out | ofstream::trunc);    //file1被截断
//以下为保留file1内容
ofstream app("file1", ofstream::app);
ofstream app("file1", ofstream::app | ofstream::out);

保留ofstream打开的文件的内容唯一方法是显式指定app或in模式。

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

ofstream out;
out.open("scratchpad");    //默认模式为输出和截断
out.close();
out.open("precious", ofstream::app);    //指定模式为输出和追加
out.colse();

sstream定义了三个类型来支持内存IO,可以向string写入、读取数据,就像string像个IO流一样。

头文件sstream中定义的类型都继承自我们已经使用过的iostream头文件,除了继承得来的操作,sstream中还定义了一些成员来管理与流相关联的string:
在这里插入图片描述
当我们的某些工作是对整行文本进行处理,而其他工作是处理行内的单个单词时,通常可以使用istringstream。如输入的一行是人名后跟多个联系方式,人名和联系方式之间以空格分隔,我们可以使用以下类描述输入的数据:

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

从控制台读取信息并存入vector<PersonInfo>中:

string line, word;
vector<PersonInfo> people;
while (getline(cin, line)) {
    PersonInfo info;
    istringstream record(line);    //保存line的拷贝到record,line再变record的拷贝也不变
    record >> info.name;
    while (record >> word) {
        info.phones.push_back(word);
    }
    people.push_back(info);
}

当我们逐步构造输出,希望最后一起打印时,可以使用ostringstream对象。例如将上例存的信息输出,如电话号无效输出无效信息:

for (const auto &entry : people) {    //遍历每一条信息
    ostringstream formatted, badNums;
    for (const auto &nums : entry.phones) {    //遍历每一条信息中的电话号码
        if (!valid(nums)) {    //如电话号不合法
            badNums << " " << nums;    //以字符串形式存入badNums
        }
        else {
            formatted << " " << format(nums);    //如合法,将字符串以format函数返回的格式存入formatted
        }
    if (badNums.str().empty()) {    //如没有不合法的电话号
        cout << entry.name << " " << formatted.str() << endl;
    }
    else {
        cerr << "input error: " << entry.name << "invalid number(s) " << badNums.str() << endl;
    }
}    

以上例子中entry和nums类型为const auto&,是因为string对象的拷贝太耗时,声明为引用可以提高效率,同时我们只是打印数据,不必修改数据,所以用的是const。

发布了193 篇原创文章 · 获赞 11 · 访问量 6万+

猜你喜欢

转载自blog.csdn.net/tus00000/article/details/104633328