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
可以很方便的生成格式化字符串。