现代C++:Streams-state bits与buffer

403 阅读3分钟

state bits

实现函数:stringToInteger (1)

现在让我们利用istringstream实现一个将字符串转换成整数的函数,先写出第一版代码:

int stringToInteger(const string& str) {
    istringstream iss(str);
    int result;
    iss >> result;
    return result;
}

这版代码的一个很明显的问题就是他没有错误检查,我们不清楚iss >> result;这行语句是否成功执行了,如果输入的字符串为Foo,那result里的值就是未初始化的垃圾值,我们的预期结果应该是该函数无法成功执行,抛出异常。

对流的状态检查

我们需要一种方式来检查流的状态:当前流是否课正常读写?类型转换是否成功?
有四个状态位来表示流的状态。

  • Good bit: 可以正常读/写
  • Fail bit: 上一次对流进行的操作(previous operation)失败了,后续的操作全部中止/冻结(frozen)
  • EOF bit: 上一次对流的操作到达buffer的末尾,the end of buffer content
  • Bad bit: 当前流发生了可能无法恢复的错误
    这里面需要注意的一点是:流状态为后面三个任何一种时,后续的操作都会被中止(frozen),也就是说:
    istringstream iss("Foo");
    int result;
    string bar;
    iss >> result;

    iss >> bar; // <- 这个语句不会执行

由于iss >> result;这个语句执行后导致fail bit为on,导致后面对流的后续操作的iss >> bar;不会执行,无论后面的语句是否能成功执行都不会执行。

导致标志位开启的常见原因

image.png

  • Good bit: 流读写正常,在其他标志位关闭时开启
  • Fail bit: 常见的开启原因:
    • 类型不能正确匹配(type mismatch)
    • 文件无法打开
    • seekg方法失败
  • EOF bit: 到达buffer末尾
  • Bad bit: 一般情况是外部错误或程序本身就有bug

我们可以让流从Fall bit和EOF bit状态恢复到Good bit,但很难从Bad bit恢复到Good bit。

标志位之间的关系

image.png
Good bit是在其他三个标志位都为off的时候才会开启的标志位,因此他与其中任何一个都不是取反相等的关系。

我们通常希望检查的状态是Fail bit和EOF bit。按理说你应该很少需要用到Good bit来做检查。 我们现在来简单测试几个例子:

#include<sstream>
#include<iostream>

using namespace std;

void printStateBits(istringstream& stream){
    cout << (stream.good() ? "G" : "-");
    cout << (stream.fail() ? "F" : "-");
    cout << (stream.eof() ? "E" : "-");
    cout << (stream.bad() ? "B" : "-") << endl;
}

int main(){
    string input;
    while(cin >> input){
    istringstream iss(input);
    printStateBits(iss);
    int result;
    iss >> result;
    printStateBits(iss);
    cout << result << endl;
    }

    return 0;
}

image.png

实现函数:stringToInteger (2)

那么现在有了标志位来告诉我们流的当前状态,我们应该如何实现stringToInteger的检查呢?
考虑以下几个case,他们分别会让流的标志位变成什么?

  1. "120",很普通的可转换成int的字符串,在转换后流的状态会变成EOF bit
  2. "8 a",中间有空白字符的字符串,写入result的值只有一个8,流内还有可读取的字符串,因此流的状态仍为Good bit
  3. "10000000000000",超过int类型最大可表示的整数,流的状态会变成Fail bit + EOF bit 因此我们写出了第二版代码:
int stringToInteger(const string& str) {
    istringstream iss(str);
    int result;
    iss >> result;
    if(iss.fail()) throw std::domain_error("no valid int in beginning");
    char temp;
    iss >> temp;
    if(!iss.fail()) throw std::domain_error("more than one valid int ");
    return result;
}

这里有个问题,为什么第二次检测字符串内是否存在多个可转换字符时使用的方法为再读取一个字符检测iss.fail(),而不是直接检测是否有iss.eof()呢?因为我们之后要使用iostream,对于iostream来说遇到EOF位开启的情况比较少,我们希望代码逻辑保持一致。

在上一节链式调用>><<时我们提到,iss >> result;返回的是这个流本身,而这个返回值在条件判断处可以转换成布尔类型,当fail bit关闭时为true
因此,我们可以进一步简化代码:

int stringToInteger(const string& str) {
    istringstream iss(str);
    int result;
    if(!(iss >> result))
        throw std::domain_error("no valid int in beginning");
    char temp;
    if(iss >> temp)
        throw std::domain_error("more than one valid int ");
    return result;
}

这种idiom在c++中是很常见的,你也会看到类似下面的语句:

while(file >> something){
...

实际上这个循环判断条件就是file流的fail位是否开启。

Buffer

image.png 考虑下面的代码:

    cout << "CS";
    somethingspendtime();
    cout << "106";
    somethingspendtime();
    cout << "L";
    somethingspendtime();
    cout << endl;

"CS106L"会同时打印到控制台上,而不是CS、106、L这样打印三次。
实际上,标准输入输出的开销是十分昂贵的(注:这实际上涉及到系统调用,从用户态到内核态的切换等,开销很大),因此cout流会在buffer满或强制刷新buffer的时候将buffer内的字符一齐输出到控制台上。
但标准错误流不会进行换成std::cerr流会直接输出。(这点和python是一样的)。

自动触发buffer刷新的操作

实际上,有一些操作是会自动触发buffer的刷新的,如cin,在用户输入之前应该把cout buffer内的数据输出到控制台。

manipulators

有一些特殊的关键字可以改变流的行为(behavior)

image.png

image.png
这些操作符都相对比较容易查询,所以课程并不会对其进行讲解,可以自行Google。不要去背/记这些操作符,你正常使用这些操作符时完全用到哪个就去查询对应的文档

总结

  1. state bits用于检测流的状态,我们通常需要检测的是fail位和eof位。
  2. 流一旦fail位开启,就会中止后续对这个流的操作。
  3. while(iss >> something)实际上检测的是iss流的fail位,开启为false,关闭为true。
  4. cout流是有缓冲区的,出于性能考虑,他会在buffer满或强制刷新buffer时才输出。