标准IO库

  • IO标准库类型和头文件

出于某些原因,标准库类型不允许做复制或赋值操作。

ofstream out1, out2;
    out1 = out2;   // error: cannot assign stream objects
    // print function: parameter is copied
    ofstream print(ofstream);
    out2 = print(out2);  

这个要求有两层特别重要的含义。只有支持复制的元素类型可以存储在 vector 或其他容器类型里。由于流对象不能复制,因此不能存储在 vector(或其他)容器中(即不存在存储流对象的 vector 或其他容器)

第二个含义是:形参或返回类型也不能为流类型。如果需要传递或返回 IO 对象,则必须传递或返回指向该对象的指针或引用

ofstream &print(ofstream&);              // ok: takes a reference, no copy
    while (print(out2)) { /* ... */ }

一般情况下,如果要传递 IO 对象以便对它进行读写,可用非 const 引用的方式传递这个流对象。对 IO 对象的读写会改变它的状态,因此引用必须是非 const 的。

  • 条件状态

IO 标准库管理一系列条件状态(condition state)成员,用来标记给定的 IO 对象是否处于可用状态,或者碰到了哪种特定的错误。

考虑下面 IO 错误的例子:

 int ival;
     cin >> ival;

如果在标准输入设备输入 Borges,则 cin 在尝试将输入的字符串读为 int 型数据失败后,会生成一个错误状态。类似地,如果输入文件结束符(end-of-file),cin 也会进入错误状态。而如果输入 1024,则成功读取,cin 将处于正确的无错误状态。

流必须处于无错误状态,才能用于输入或输出。检测流是否可用的最简单的方法是检查其真值

if (cin)
               // ok to use cin, it is in a valid state

          while (cin >> word)
               // ok: read operation successful ...

if 语句直接检查流的状态,而 while 语句则检测条件表达式返回的流,从而间接地检查了流的状态。如果成功输入,则条件检测为 true

许多程序只需知道是否有效。而某些程序则需要更详细地访问或控制流的状态,此时,除了知道流处于错误状态外,还必须了解它遇到了哪种类型的错误。例如,程序员也许希望弄清是到达了文件的结尾,还是遇到了 IO 设备上的错误。

所有流对象都包含一个条件状态成员,该成员由 setstateclear 操作管理。这个状态成员为 iostate 类型,这是由各个 iostream 类分别定义的机器相关的整型。该状态成员以二进制位(bit)的形式使用。

每个 IO 类还定义了三个 iostate 类型的常量值,分别表示特定的位模式。这些常量值用于指出特定类型的 IO 条件,可与位操作符一起使用,以便在一次操作中检查或设置多个标志。

badbit 标志着系统级的故障,如无法恢复的读写错误。如果出现了这类错误,则该流通常就不能再继续使用了。如果出现的是可恢复的错误,如在希望获得数值型数据时输入了字符,此时则设置 failbit 标志,这种导致设置 failbit 的问题通常是可以修正的。eofbit 是在遇到文件结束符时设置的,此时同时还设置了 failbit

流的状态由 badfaileofgood 操作提示。如果 badfail 或者 eof 中的任意一个为 true,则检查流本身将显示该流处于错误状态。类似地,如果这三个条件没有一个为 true,则 good 操作将返回 true

clearsetstate 操作用于改变条件成员的状态。clear 操作将条件重设为有效状态。在流的使用出现了问题并做出补救后,如果我们希望把流重设为有效状态,则可以调用 clear 操作。使用 setstate 操作可打开某个指定的条件,用于表示某个问题的发生。除了添加的标记状态,setstate 将保留其他已存在的状态变量不变。

可以如下管理输入操作

int ival;
    // read cin and test only for EOF; loop is executed even if there are other IO failures
    while (cin >> ival, !cin.eof()) {
        if (cin.bad())         // input stream is corrupted; bail out
            throw runtime_error("IO stream corrupted");
        if (cin.fail()) {                        // bad input
            cerr<< "bad data, try again";        // warn the user
            cin.clear(istream::failbit);         // reset the stream
            continue;                            // get next input
        }
        // ok to process ival
    }

这个循环不断读入 cin,直到到达文件结束符或者发生不可恢复的读取错误为止。循环条件使用了逗号操作符:首先计算它的每一个操作数,然后返回最右边操作数作为整个操作的结果。因此,循环条件只读入 cin 而忽略了其结果。该条件的结果是 !cin.eof() 的值。如果 cin 到达文件结束符,条件则为假,退出循环。如果 cin 没有到达文件结束符,则不管在读取时是否发生了其他可能遇到的错误,都进入循环。

rdstate 成员函数返回一个 iostate 类型值,该值对应于流当前的整个条件状态:

// remember current state of cin
     istream::iostate old_state = cin.rdstate();
     cin.clear();
     process_input();  // use cin
     cin.clear(old_state); // now reset cin to old state

常常会出现需要设置或清除多个状态二进制位的情况。此时,可以通过多次调用 setstate 或者 clear 函数实现。另外一种方法则是使用按位或(OR)操作符在一次调用中生成“传递两个或更多状态位”的值。按位或操作使用其操作数的二进制位模式产生一个整型数值。对于结果中的每一个二进制位,如果其值为 1,则该操作的两个操作数中至少有一个的对应二进制位是 1。例如:

// sets both the badbit and the failbit
   is.setstate(ifstream::badbit | ifstream::failbit);
  • 输出缓存区的管理

每个 IO 对象管理一个缓冲区,用于存储程序读写的数据。如有下面语句:

os << "please enter a value: ";

系统将字符串字面值存储在与流 os 关联的缓冲区中。下面几种情况将导致缓冲区的内容被刷新,即写入到真实的输出设备或者文件:

程序正常结束。作为 main 返回工作的一部分,将清空所有输出缓冲区;在一些不确定的时候,缓冲区可能已经满了,在这种情况下,缓冲区将会在写下一个值之前刷新;用操纵符显式地刷新缓冲区,例如行结束符 endl;在每次输出操作执行完后,用 unitbuf 操作符设置流的内部状态,从而清空缓冲区;可将输出流与输入流关联(tie)起来。在这种情况下,在读输入流时将刷新其关联的输出缓冲区。

我们的程序已经使用过 endl 操纵符,用于输出一个换行符并刷新缓冲区。除此之外,C++ 语言还提供了另外两个类似的操纵符。第一个经常使用的 flush,用于刷新流,但不在输出中添加任何字符。第二个则是比较少用的 ends,这个操纵符在缓冲区中插入空字符 null,然后后刷新它:

cout << "hi!" << flush;      // flushes the buffer; adds no data
    cout << "hi!" << ends;       // inserts a null, then flushes the buffer
    cout << "hi!" << endl;       // inserts a newline, then flushes the buffer

//cout是输出语句,flush是缓冲区的内容,cout<<flush表示将缓冲区的内容马上送进cout,把输出缓冲区刷新

当程序向输出设备中输出数据时,输出的数据先被存放在计算机缓 冲区(Buffer)内。当缓冲区存满时,这些数据才真正地输出到输出设备。但是,如果输出的字符序列中出现了 endl控制符,那么缓冲区内的所有数据将立即输出到输出设备,而无论缓冲区是否已经存满。因此,endl控制符的作用是将光标移动到输出设备中下一行开 头处,并且清空缓冲区。很有可能出现在程序终止时,并没有输出所有的输出数据的情况。这是因为在程序终止时,缓冲区并不一定是满的,所以也就没有将缓冲区中的数据写到输出设备。在C++中,可以使用flush函数来清空缓冲区,即使缓冲区中的数据不是满的。与endl控制符不同的是,flush函数并不是把光标移到下一行的开头处。总之,flush和endl的功能一样,只是前者不换行而已。

如果需要刷新所有输出,最好使用 unitbuf 操纵符。这个操纵符在每次执行完写操作后都刷新流:

cout << unitbuf << "first" << " second" << nounitbuf;

等价于:

cout << "first" << flush << " second" << flush;

nounitbuf 操纵符将流恢复为使用正常的、由系统管理的缓冲区刷新方式。

警告:如果程序崩溃了,不会刷新缓冲区。如果程序不正常结束,输出缓冲区将不会刷新。在尝试调试已崩溃的程序时,通常会根据最后的输出找出程序发生错误的区域。如果崩溃出现在某个特定的输出语句后面,则可知是在程序的这个位置之后出错。调试程序时,必须保证期待写入的每个输出都确实被刷新了。因为系统不会在程序崩溃时自动刷新缓冲区,这就可能出现这样的情况:程序做了写输出的工作,但写的内容并没有显示在标准输出上,仍然存储在输出缓冲区中等待输出。如果需要使用最后的输出给程序错误定位,则必须确定所有要输出的都已经输出。为了确保用户看到程序实际上处理的所有输出,最好的方法是保证所有的输出操作都显式地调用了 flushendl如果仅因为缓冲区没有刷新,程序员将浪费大量的时间跟踪调试并没有执行的代码。基于这个原因,输出时应多使用 endl 而非 '\n'。使用 endl 则不必担心程序崩溃时输出是否悬而未决(即还留在缓冲区,未输出到设备中)。

  • 文件的输入和输出

fstream 头文件定义了三种支持文件 IO 的类型:ifstream,由 istream 派生而来,提供读文件的功能;ofstream,由 ostream 派生而来,提供写文件的功能;fstream,由 iostream 派生而来,提供读写同一个文件的功能。

这些类型都由相应的 iostream 类型派生而来,这个事实意味着我们已经知道使用 fstream 类型需要了解的大部分内容了。特别是,可使用 IO 操作符(<<>> )在文件上实现格式化的 IO。fstream 类型除了继承下来的行为外,还定义了两个自己的新操作—— openclose,以及形参为要打开的文件名的构造函数

需要读写文件时,则必须定义自己的对象,并将它们绑定在需要的文件上。假设 ifileofile 是存储希望读写的文件名的 strings 对象,可如下编写代码:

// construct an ifstream and bind it to the file named ifile
    ifstream infile(ifile.c_str());
    // ofstream output file object to write file named ofile
    ofstream outfile(ofile.c_str());

上述代码定义并打开了一对 fstream 对象。infile 是读的流,而 outfile 则是写的流。为 ifstream 或者 ofstream 对象提供文件名作为初始化式,就相当于打开了特定的文件。

ifstream infile;    // unbound input file stream
    ofstream outfile;   // unbound output file stream

上述语句将 infile 定义为读文件的流对象,将 outfile 定义为写文件的对象。这两个对象都没有捆绑具体的文件。在使用 fstream 对象之前,还必须使这些对象捆绑要读写的文件:

infile.open("in");   // open file named "in" in the current directory
    outfile.open("out"); // open file named "out" in the current directory

调用 open 成员函数将已存在的 fstream 对象与特定文件绑定。为了实现读写,需要将指定的文件打开并定位,open 函数完成系统指定的所有需要的操作。

打开文件后,通常要检验打开是否成功,这是一个好习惯:

if (!infile) {
        cerr << "error: unable to open input file: "
             << ifile << endl;
        return -1;
    }

fstream 对象一旦打开,就保持与指定的文件相关联。如果要把 fstream 对象与另一个不同的文件关联,则必须先关闭(close)现在的文件,然后打开(open)另一个文件:要点是在尝试打开新文件之前,必须先关闭当前的文件流。open 函数会检查流是否已经打开。如果已经打开,则设置内部状态,以指出发生了错误。接下来使用文件流的任何尝试都会失败。

ifstream infile("in");      // opens file named "in" for reading
     infile.close();             // closes "in"
     infile.open("next");        // opens file named "next" for reading
  • 清除文件流状态

考虑这样的程序,它有一个 vector 对象,包含一些要打开并读取的文件名,程序要对每个文件中存储的单词做一些处理。假设该 vector 对象命名为 files,程序也许会有如下循环:

// for each file in the vector
    while (it != files.end()) {
        ifstream input(it->c_str());   // open the file;
        // if the file is ok, read and "process" the input
        if (!input)
            break;                  // error: bail out!
        while(input >> s)               // do the work on this file
            process(s);
        ++it;                           // increment iterator to get next file
    }

每一次循环都构造了名为 inputifstream 对象,打开并读取指定的文件。构造函数的初始化式使用了箭头操作符对 it 进行解引用,从而获取 it 当前表示的 string 对象的 c_str 成员。文件由构造函数打开,并假设打开成功,读取文件直到到达文件结束符或者出现其他的错误条件为止。在这个点上,input 处于错误状态。任何读 input 的尝试都会失败。因为 inputwhile 循环的局部变量,在每次迭代中创建。这就意味着它在每次循环中都以干净的状态即 input.good()true,开始使用。

如果希望避免在每次 while 循环过程中创建新流对象,可将 input 的定义移到 while 之前。这点小小的改动意味着必须更仔细地管理流的状态。如果遇到文件结束符或其他错误,将设置流的内部状态,以便之后不允许再对该流做读写操作。关闭流并不能改变流对象的内部状态。如果最后的读写操作失败了,对象的状态将保持为错误模式,直到执行 clear 操作重新恢复流的状态为止。调用 clear 后,就像重新创建了该对象一样

如果打算重用已存在的流对象,那么 while 循环必须在每次循环进记得关闭(close)和清空(clear)文件流:

ifstream input;
    vector<string>::const_iterator it = files.begin();
    //   for each file in the vector
    while (it != files.end()) {
        input.open(it->c_str());  // open the file
        // if the file is ok, read and "process" the input
        if (!input)
            break;                    // error: bail out!
        while(input >> s) // do the work on this file
            process(s);
        input.close();        // close file when we're done with it
        input.clear();        // reset state to ok
        ++it;                 // increment iterator to get next file
    }

如果程序员需要重用文件流读写多个文件,必须在读另一个文件之前调用 clear 清除该流的状态

  • 文件模式

在打开文件时,无论是调用 open 还是以文件名作为流初始化的一部分,都需指定文件模式(file mode)。每个 fstream 类都定义了一组表示不同模式的值,用于指定流打开的不同模式。

outtruncapp 模式只能用于指定与 ofstreamfstream 对象关联的文件;in 模式只能用于指定与 ifstreamfstream 对象关联的文件。所有的文件都可以用 atebinary 模式打开。ate 模式只在打开时有效:文件打开后将定位在文件尾。以 binary 模式打开的流则将文件以字节序列的形式处理,而不解释流中的字符。默认时,与 ifstream 流对象关联的文件将以 in 模式打开,该模式允许文件做读的操作:与 ofstream 关联的文件则以 out 模式打开,使文件可写。以 out 模式打开的文件会被清空:丢弃该文件存储的所有数据。从效果来看,为 ofstream 对象指定 out 模式等效于同时指定了 outtrunc 模式。

对于用 ofstream 打开的文件,要保存文件中存在的数据,唯一方法是显式地指定 app 模式打开

//  output mode by default; truncates file named "file1"
    ofstream outfile("file1");
    // equivalent effect: "file1" is explicitly truncated
    ofstream outfile2("file1", ofstream::out | ofstream::trunc);
    //  append mode; adds new data at end of existing file named "file2"
    ofstream appfile("file2", ofstream::app);

模式是文件属性而不是流属性

  • 字符串流

iostream 标准库支持内存中的输入/输出,只要将流与存储在程序内存中的 string 对象捆绑起来即可。此时,可使用 iostream 输入和输出操作符读写这个 string 对象。标准库定义了三种类型的字符串流:

istringstream,由 istream 派生而来,提供读 string 的功能;ostringstream,由 ostream 派生而来,提供写 string 的功能;stringstream,由 iostream 派生而来,提供读写 string 的功能。要使用上述类,必须包含 sstream 头文件。

sstream 类型除了继承的操作外,还各自定义了一个有 string 形参的构造函数,这个构造函数将 string 类型的实参复制给 stringstream 对象。对 stringstream 的读写操作实际上读写的就是该对象中的 string 对象。这些类还定义了名为 str 的成员,用来读取或设置 stringstream 对象所操纵的 string 值。

前面已经见过以每次一个单词或每次一行的方式处理输入的程序。第一种程序用 string 输入操作符,而第二种则使用 getline 函数。然而,有些程序需要同时使用这两种方式:有些处理基于每行实现,而其他处理则要操纵每行中每个单词。可用 stringstreams 对象实现:

string line, word;      // will hold a line and word from input, respectively
    while (getline(cin, line))   {            // read a line from the input into line
       // do per-line processing
       istringstream stream(line);            // bind to stream to the line we read
       while (stream >> word){          // read a word from line
           // do per-word processing
       }
    }

这里,使用 getline 函数从输入读取整行内容。然后为了获得每行中的单词,将一个 istringstream 对象与所读取的行绑定起来,这样只需要使用普通的 string 输入操作符即可读出每行中的单词

stringstream 对象的一个常见用法是,需要在多种数据类型之间实现自动格式化时使用该类类型。例如,有一个数值型数据集合,要获取它们的 string 表示形式,或反之。sstream 输入和输出操作可自动地把算术类型转化为相应的 string 表示形式,反过来也可以

int val1 = 512, val2 = 1024;
    ostringstream format_message;
    // ok: converts values to a string representation
    format_message << "val1: " << val1 << "\n"
                   << "val2: " << val2 << "\n";

这里创建了一个名为 format_messageostringstream 类型空对象,并将指定的内容插入该对象。重点在于 int 型值自动转换为等价的可打印的字符串。重点在于 int 型值自动转换为等价的可打印的字符串。format_message 的内容是以下字符:

val1: 512\nval2: 1024

相反,用 istringstreamstring 对象,即可重新将数值型数据找回来。读取 istringstream 对象自动地将数值型数据的字符表示方式转换为相应的算术值

// str member obtains the string associated with a stringstream
   istringstream input_istring(format_message.str());
   string dump; // place to dump the labels from the formatted message
   // extracts the stored ascii values, converting back to arithmetic types
   input_istring >> dump >> val1 >> dump >> val2;
   cout << val1 << " " << val2 << endl;  // prints 512 1024

这里使用 。str 成员获取与之前创建的 ostringstream 对象关联的 string 副本。再将 input_istringstring 绑定起来。在读 input_istring 时,相应的值恢复为它们原来的数值型表示形式。

因为输入操作符读取的是所有类型的值,因此读入的对象类型必须和由 stringstream 读入的值的类型一致。在本例中,input_istring 分成四个部分:string 类型的值 val1,接着是 512,然后是 string 类型的值 val2,最后是 1024(对应着input_istring >> dump >> val1 >> dump >> val2;格式)。一般情况下,使用输入操作符读 string 时,空白符将会忽略。于是,在读与 format_message 关联的 string 时,忽略其中的换行符

posted @ 2016-02-18 18:34  _No.47  阅读(431)  评论(0)    收藏  举报