C++:IO流
在C语言中,有printf,scanf进行基础的IO,也有fprintf,sprintf等文件操作,字符流操作。C++作为面向对象语言,通过面向对象思想,将C语言的整个IO体系进行了重构,建立了一套IO体系。本博客将讲解C++中的IO类基本结构,部分重要的机制,以及文件,字符流的IO方法。
IO类框架

- ios:定义了基本的IO框架,比如IO的错误标志,其中- io_base是所有IO类的基类
- iostream:包含了标准输入输出,最常用的头文件
- fstream:用于文件输入输出
- sstream:用于操作字符串,可以非常方便的进行字符串格式化与解析
IO流机制
在C++中,额外增加了许多IO的特殊机制,这些机制都在ios头文件中。
C/C++缓冲区同步
C++是在C语言基础上改进的语言,要兼容C语言的绝大部分内容,自然包括C语言本身的IO机制。对于标准输入而言,C++使用cin、cout,而C语言使用printf、scanf。它们的功能相同,挑选一个使用即可,但是免不了会有用户两个版本交替使用,此时就可能带来同步问题。
因此C++中cout没有自己的缓冲区,而是与printf共用缓冲区,这样就算混用两者,也不会造成输出错误。
但是正因为兼容了printf,cout就要去做很多额外的工作,那么cout的效率就会下降,这也就是为什么很多竞赛不使用cout,cin这一套的原因。

为此,C++也提供了接口sync_with_stdio用于处理C/C++之间的同步问题。默认sync = true,也就是C/C++之间要进行同步。此时可以通过该函数手动调整为false,接触同步关系:
            
            
              cpp
              
              
            
          
          sync_with_stdio(false);此时C/C++就不兼容了,如果再混用cout和printf,就可能导致输出顺序错误。但是相应的,cout、cin的效率会提高。
错误状态标志 iostate
每一个IO对象,内部都存储了字节的错误状态标志,其为四个比特位,分别是goodbit、eofbit、failbit、badbit,四者含义如下:
| iostate | 描述 | good | eof | fail | bad | 
|---|---|---|---|---|---|
| goodbit | 没有错误 | 1 | 0 | 0 | 0 | 
| eopbit | 文件读取结束 | 0 | 1 | 0 | 0 | 
| failbit | 逻辑错误 | 0 | 0 | 1 | 0 | 
| badbit | 读写错误 | 0 | 0 | 1 | 1 | 
后面的比特位,表示对应错误发生时,每个位的状态。
比如说将一个char变量输入到int中,此时就会发生逻辑错误。如果想要查看对应的比特位,可以通过函数good、eof、fail、bad查看,比如:
            
            
              cpp
              
              
            
          
          cout << "good = " << cin.good() << endl;
cout << "eof  = " << cin.eof() << endl;
cout << "fail = " << cin.fail() << endl;
cout << "bad  = " << cin.bad() << endl;一般情况下输出结果为:
            
            
              cpp
              
              
            
          
          good = 1
eof  = 0
fail = 0
bad  = 0执行如下代码:
            
            
              cpp
              
              
            
          
          int i;
cout << "请输入数字:";
cin >> i;
cout << "good = " << cin.good() << endl;
cout << "eof = " << cin.eof() << endl;
cout << "fail = " << cin.fail() << endl;
cout << "bad = " << cin.bad() << endl;次数输入一个不是数字的内容,比如字符x:
            
            
              cpp
              
              
            
          
          请输入数字:s
good = 0
eof = 0
fail = 1
bad = 0此时就出现了逻辑错误,也就是把一个char赋值给了int。
一旦出现错误,这个cin对象就无法再使用了,必须通过clear来重置错误状态标志。

当IO对象内部的错误状态标志不正确,那么再次进行IO时就无法正常进行。clear用于将错误状态标志重置为goodbit = 1,其余位变为0,这样IO对象就可以再次使用了。
示例:
            
            
              cpp
              
              
            
          
          int i;
cout << "请输入数字:";
cin >> i;
cout << "good = " << cin.good() << endl;
cout << "eof = " << cin.eof() << endl;
cout << "fail = " << cin.fail() << endl;
cout << "bad = " << cin.bad() << endl;
cin.clear();
cin.get();
cout << "请输入数字:";
cin >> i;
cout << "good = " << cin.good() << endl;
cout << "eof = " << cin.eof() << endl;
cout << "fail = " << cin.fail() << endl;
cout << "bad = " << cin.bad() << endl;简单改动了刚才的代码,这一次第一次输入字母x,在clear之后再输入数字1。
此处有一个小问题,我在clear之后紧跟了一个get,这是因为之前输入的字符x没有被读取走,发生了错误后,字符依然留在缓冲区中,如果clear后直接cin >>,那么cin还会读取到留在缓冲区中的x,导致一样的错误!所以要用get先把缓冲区中的字符读走,再让用户输入。
输出结果:
            
            
              cpp
              
              
            
          
          请输入数字:x
good = 0
eof = 0
fail = 1
bad = 0
请输入数字:1
good = 1
eof = 0
fail = 0
bad = 0第二次输入数字就可以正常读取了。
tie

tie是一种特殊的绑定机制,其将一个istream对象绑定到另一个ostream对象上。当istream收到数据时,会刷新对应的ostream的缓冲区。
这有啥用呢?默认情况下cin会绑定到cout上,而很多用户用完cin接收数据后,很有可能会用cout把处理好的数据输出出去。而cout的缓冲区中可能存有之前没有刷新的数据,为了保证这次输出的数据不受影响,在cin读取数据时,就把cout的缓冲区提前刷新一次。
也就是说,通过tid进行绑定的机制,其实就是将未来有可能要使用的缓冲区提前刷新,为后续数据预留空间的机制。就像一个家庭出生了一个婴儿,会提前空出地盘放一张婴儿床一样。
通过函数tie可以发现,其有两个重载,对于无参的tie,其返回该对象绑定到的ostream对象的指针;对于有参的tie,传入一个ostream*指针,将该对象重新绑定到其它ostream对象上,如果传入nullptr,那么相当于解绑,不绑定任何一个ostream对象。
比如可以这样:
            
            
              cpp
              
              
            
          
          *cin.tie() << "hello world!" << endl;无参的cin.tie()返回其绑定的对象指针,也就是默认的cout,在对指针解引用,就相当于cout <<。当然这多此一举了,此处不过想证明cin默认绑定到cout。
解除cin的绑定:
            
            
              cpp
              
              
            
          
          cin.tie(nullptr);由于cin自动绑定到cout,那么每次cin读取数据都会刷新cout的缓冲区,这会影响到cin的效率,所以竞赛中会进行一个这样的解绑操作,让cin读取时不再刷新cout缓冲区,提高IO效率。
输入输出
标准流对象
C++的标准输入输出定义在头文件<iostream>中,其中包含istream和ostream这两个类,分别用于输入输出。其中istream定义了基本的cin对象,用于读取stdin流,ostream定义了cout、cerr、clog对象,其中cout输出到stdout流,cerr与clog输出到stderr流。
在效果上cout、cerr、clog没有区别,都输出到显示器。但是设计者的意图是用户在编码时在语义上将输出,错误,日志三者区分开,便于代码维护。
使用上,我们常用<<和>>这样的操作符来操作标准流对象,这是基于操作符重载,以ostream重载<<为例:

ostream重载了<<,常见的内置类型都已经完成了重载,完成对应类型的输出。由于函数重载后会自动推导类型,所以不用像printf一样考虑变量的类型,再使用%s,%d这样的占位符。
此处operator的返回值ostream&,是为了作用于连续输出:
            
            
              cpp
              
              
            
          
          cout << "hello" << " world!";这个代码本质是operator<<(operator<<(cout, "hello"), " world!"),其中operator<<(cout, "hello")返回了cout,所以输出完hello后代码变成operator<<(cout, " world!"),所以cout可以这样进行连续输出。
另外的,如果想要输出自定义类型,也可以自己重载对应的操作符:
            
            
              cpp
              
              
            
          
          struct date
{
    int _year;
    int _month;
    int _day;
    date(int year, int month, int day)
        : _year(year)
        , _month(month)
        , _day(day)
    {}
};
ostream& operator<<(ostream& out, const date& d)
{
    cout << d._year << "-" << d._month << "-" << d._day;
    return out;
}以上就是给date类重载了ostream的输出,注意函数最后要返回ostream对象的引用。
现在就可以输出date类了:
            
            
              cpp
              
              
            
          
          date d(1949, 10, 1);
cout << d << endl;输出结果:
            
            
              cpp
              
              
            
          
          1949-10-1常见输入策略
在写题时,常需要进行循环输入,一般来说会有以下常用格式:
- 单变量循环输入:
            
            
              cpp
              
              
            
          
          while (cin >> str)
{
	// ...
}先前我说过,operator>>返回的是一个istream对象,所以也可以连续输入:
- 多变量循环输入:
            
            
              cpp
              
              
            
          
          while (cin >> str1 >> str2 >> str3)
{
	// ...
}那么问题来了,while内部需要一个bool类型的值判断真假,此处不论输入几个变量都返回istream对象,为什么可以把这个输入语句直接放到while内部做判断条件?

其实ios类重载了operator bool,也就是到bool的隐式类型转化,因此istream可以隐式转化为bool类型,满足while的判断要求。
文件IO
在C++中,读写一个文件的基本套路如下:
- 定义一个文件流对象
- 打开指定文件
- 进行IO
- 关闭文件
而流文件对象有ifstream和ofstream,分别作用于输入输出。需要头文件<fstream>。
ifstream

ifstream的构造函数如上,最常用的是第二个初始化的构造:
            
            
              cpp
              
              
            
          
          explicit ifstream (const char* filename, ios_base::openmode mode = ios_base::in);- filename:文件名
- mode:打开模式
打开模式如下:
| 方式 | 功能 | 
|---|---|
| in | 读取文件内容 | 
| out | 输出内容到文件 | 
| binary | 以二进制形式打开 | 
| ate | 打开后,文件指针初始指向文件末尾 | 
| app | 以追加的形式打开文件 | 
| trunc | 打开时清空文件内容 | 
此处要区分一下app和ate,两者打开文件后,文件指针都在文件末尾,那不都是追加吗?其实还是略有差别的。
- ate:只是初始的时候文件指针在末尾,后续可以随意移动文件指针,自由读写。
- app:每次进行写入操作,文件指针都会强制跳转到文件末尾,也就是说只能操作文件的末尾。
以上模式在ios_base类域中,可以混用,每个模式占一个位,传参时以按位或的形式传入:
            
            
              cpp
              
              
            
          
          ifstream ifs ("test.cpp", ios_base::in | ios_base::binary);以上就是定义了一个ifs对象,打开文件test.cpp,以读取+二进制模式。
由于istream重载了operator>>,ifstream可以继承到,自然也可以通过>>读取文件。
示例:
            
            
              cpp
              
              
            
          
          #include <iostream>
#include <fstream>
#include <windows.h>
using namespace std;
int main()
{
    ifstream ifs("test.cpp", ios_base::in);
    string buf;
    while (ifs >> buf)
    {
        cout << buf;
    }
    
	ifs.close();
	
    return 0;
}这个代码,读取了test.cpp文件,其实就是这个代码本身,读取结果:
            
            
              cpp
              
              
            
          
          #include<iostream>#include<fstream>#include<windows.h>usingnamespacestd;intmain(){ifstreamifs("test.cpp",ios_base::in);stringbuf;while(ifs>>buf){cout<<buf;}ifs.close();return0;}奇怪的事情发生了,为什么所有内容都被压缩为了一行?
重载operator>>在实现时,会自动忽略空格与换行。对于cin、cout来说,这可以很方便的读取到变量,而对于文件来说,就不怎么适合了,所以一般不使用这个重载读取文件。
get
为了控制文件读取,一般会使用istream::get方法:

这个方法定义在istream中,ifstream继承到该方法,所以可以用于读取文件,可以看到其可以以字符为单位读取,也可以指定要读取的字符数量。
示例:
            
            
              cpp
              
              
            
          
          #include <iostream>
#include <fstream>
#include <windows.h>
using namespace std;
int main()
{
    ifstream ifs("test.cpp", ios_base::in);
    char ch;
    while (ifs.get(ch))
    {
        cout << ch;
    }
    
	ifs.close();
    return 0;
}此时按照字符读取内容,空格与换行也会被读取,最后输出的内容也就是格式的内容了。
read
对于二进制文件来说,一般会使用istream::read读取:

当然也可以用read读取字符文件,用get读取二进制文件,只是习惯问题。
ofstream

在打开方式上,ofstream与ifstream差不多,都是传入一个文件名以及一个打开模式。此处两者一致,就不再谈了。
ofstream也继承了operator<<,一般对于文件写入,直接用<<即可。
示例:
            
            
              cpp
              
              
            
          
          ofstream ofs("test.txt", ios_base::out);
ofs << "hello world!" << endl;
ofs << "------------" << endl;
ofs << "hello   C++!" << endl;
ofs.close();直接当作cout用即可。
write
对于二进制文件写入,则使用write多:

字符流转换
在字符串处理方面,C++ 提供了两个非常有用的流类:istringstream 和 ostringstream。这两个类分别用于字符串到数据的解析,以及将数据转换为字符串。需要头文件<sstream>。
istringstream
istringstream 是 C++ 标准库中的一个类,属于 std::istringstream。它继承自 std::istream,可以将一个字符串视为输入流,从中提取各种数据类型。

在构造istringstream对象时,可以直接传入要解析的字符串作为输入流。
示例:
            
            
              cpp
              
              
            
          
          string input = "123 45.67 Hello";
istringstream iss(input);
int integer;
double floating;
string word;
// 从字符串中提取数据
iss >> integer >> floating >> word;在这段代码中,首先创建了一个 std::istringstream 对象 iss,并将 input 作为输入流。通过重载的 >> 操作符,我们可以依次从流中提取整数、浮点数和字符串,并将它们存储到相应的变量中。
我之前讲解过operator>>会自动忽略空格换行符,所以此处可以按照要求把数据一个个读取出来,并且复制到对应的变量中。这种使用方式非常简洁,适合从格式化的字符串中提取数据。
ostringstream
ostringstream 是 C++ 标准库中的另一个重要流类,属于 std::ostringstream。它继承自 std::ostream,主要用于将各种数据类型格式化为字符串。

格式化好的字符串存在ostringstream对象的缓冲区中,如果想要得到格式化后的字符串,则通过str函数。
示例:
            
            
              cpp
              
              
            
          
          int integer = 123;
double floating = 45.67;
string word = "Hello";
ostringstream oss;
oss << "Integer: " << integer << ", Double: " << floating << ", String: " << word;
string output = oss.str();
cout << output << endl;输出结果:
            
            
              cpp
              
              
            
          
          Integer: 123, Double: 45.67, String: Hello可以看到,ostringstream可以很方便的生成格式化字符串。