在本篇博客中, 我算是 CPP Reference (里面的信息才是最为全面,英语稍微好点可以自己去看) 的搬运工, 再将里面的信息组织和讲解一下,耐心看一遍,“日后” 随便可查阅,有需要自行扩展。
C++ 提供的I/O类库之间的关系:
通过上图, 可以发现熟悉的 cin、 cout 分别是 istream、 ostream 的对象(不过你不能实例化喔,因为构造函数是保护的),再看 ifstream、istringstream 和 ofstream、ostringstream 则是对应的派生类, 另外还有将 Input 和 Output 结合的 fstream、stringstream 。 再根据 OOP(面向对象编程) 的思想, 我们可以以使用 cin、 cout 的方式来使用其他类(当然也要了解其中的区别), 接下来也通过 cin、 cout 来推广到其他类的使用。
现在再了解一下 C++ I/O 库的 4 个元素 :
ios_base 定义了一些不依赖输入流还是输出流的部件, 意思就是一些操作仅属于输出流的,但是也在这里定义了, 所以我们也可以在输入流使用一些输出流的操作(但是结果是没有任何作用的), 例如 cin.width() 是合法但多鱼的。
ios_base 有许多内容, 这里讲解两个部分。
函数 | 意义 |
---|---|
flags | 获取、设置(传入 fmtflags)流格式 |
setf | 设置指定的流格式。 配合 unsetf 使用 |
precision | 获取、设置浮点数精度 —— 受 floatfiled 格式控制 |
width | 获取、设置流域的宽度(即一次输出的最小长度)—— 受 adjustfiled 格式控制 |
使用下面的数值(fmtflags)完成流的格式化
上面的表大致描述了流的格式化, 下面通过一个简单的例子了解具体用法:
// 设置整数的写使用 16 进制 (用到 cin 就是写咯)
cout.setf(ios::hex, ios::basefield); // 因为 ios 继承了 ios_base , 所以也有 hex , 也可以直接 cout.hex
cout.setf(ios::showbase); //显示基数前缀,这里是 0x
cout << 100 << endl; // 输出的是 0x64
上面的第一个 setf 有两个参数, 作用是将第二个参数对应的所有域 清0 , 除了第一个参数的之外, 也就是说把默认为 10 进制换成了 16 进制, 灵活使用这些操作(例如将 cout 换成 ostringstream 对象), 就可以实现进制转换。
上面是流的打开模式和流定位(读写指针), 看上去有点抽象,因为目前还没有任何相关操作,具体实现在后面的派生类当中, 但是转换到我们熟悉的文件, 那就是确定文件的打开方式,如下面的例子:
fstream fs("my_file", ios::app); // 追加模式打开,仅能追加输入, 输出流定位失效
// 由于 fstream 继承于 ifstream 和 ofstream 所以 fstream 对象的流定位操作有两种
fs.seekg(); // seek_get 定位读指针位置
fs.seekp(); // seek_put 定位写指针位置
最后有流同步
ios_base::sync_with_stdio(bool sync=true);
// 默认将 iostream 与 cstdio 同步,也就是可以混合使用 c 的 I/O 操作
IO 操作是会发送错误的, 例如给整数变量输入一个字符, 所以我们必须了解发生了什么错误以及如何处理,先看一下表格(以下表格所列的状态继承自 ios_base, 但函数为 ios 所有):
状态 | 意义 | 测试状态(真时返回true) |
---|---|---|
ios::badbit | 指示流已崩溃 | ios::bad() |
ios::failbit | 指示一个IO操作失败 | ios::fail() |
ios::eofbit | 指示流到了文件尾 | ios::eof() |
ios::goodbit | 指示流正常 | ios::good() |
状态处理函数 |
---|
ios::rdstate() 返回当前状态 |
ios::setstate() 修改当前的流状态, 或运算 |
ios::clear(iostate state = goodbit) 设置当前的流状态,默认为正常状态 |
这里超前使用 cin 作为一个例子进行讲解
int integer;
cout << cin.rdstate() << endl; //返回 0 , 状态正常
cin >> integer; // 输入字符 a
cout << cin.rdstate() << endl; // 返回 4 ,指示出现一个 IO 失败
cin.clear(); // 将当前流状态设置为正常,但是不能马上进行输入,因为字符 a 和回车仍在缓冲区, 否则会马上出错
// istream 类的非格式化输入操作之一,提取并丢弃指定字符,直至遇到分隔符
cin.ignore(1024, '\n');
cin >> integer; // 可以重新输入数字
上面的代码 cin.ignore() 和 cin.clear() 位置不能调换, 因为 cin 处于错误状态,无法进行输入,必须先进行状态清 0 。若其他输入流出现了错误,也可以用类似的方式解决。
istream 作为第一个输入流类,描述的是如何操作输入流,也就是由istream所派生的类的操作方式与 istream 意一样。 在这里可以把 istream 看成 cin , 到后面要知道 cin 能做的(即 istream), istringstream、ifstream 都能做。
extern istream cin; // cin 是单例模式
istream 作为输入流,有许多输入方式, 我们熟悉的 >> 是格式化输入, 可以给内置数据赋值, 学习下面的函数:
void read(istream &is)
{
string str;
while( is >> str )
cout << str << endl;
}
istringstream is("123 321 asd"); // 分别输出 123、321和asd
read 函数的参数是 istream , 则调用时可以传入 cin 、 ifstream (读文件流)和 istringsteam (读字符串流), 这里的理解是 “源” 变了, cin 是从键盘读取(会阻塞), ifstream 则是从文件读取。
并且 operator >> 返回的是 istream & ,则当 istream 作为条件判断的对象时, 以前面讲到的 “状态标志” 确定是否为真。
下面给出非格式化输入(具体用法请 查阅手册 ):
istream 提供了很多输入操作, 平常用得比较多的就 get (多重载,看文档)、 getline(读一行)、 ignore(提取并抛弃)、peek(偷看下一个字符) 较多, 其他的建议稍微看下文档了解一下,以后有特定需求时可以瞄一眼。
另外还有 string 当中的 getline 也可操作输入流:
getline(istream &is, string &str);
流定位, istream 已经提供了流定位操作, 也为后面的 ifstream 和 istringstream 的提供统一的接口。
istream::tellg(); // 获取当前读指针位置
istream::seekg(); // 设置读指针位置, 两个重载
istream::seekg(streampos pos); // 给定位置
istream::seekg(streamoff off, ios_base::seekdir way); // 偏移定位
具体定位方式见下表:
seekdir | 定位方式 |
---|---|
ios_base::beg | pos = off |
ios_base::cur | pos = pos + off |
ios_base::end | pos = end - off —— end 为文件尾, 可以认为是文件字符数 |
ifstream 作为 istream 的派生类,用于文件读操作,实际上掌握了 istream (cin) 的用法, ifstream 也是一样, 现在就看一看 ifstream 多了些什么操作:
简单吧? 相对与 istream , 这里需要我们自己实例化一个对象(因为要打开文件嘛), 所以就这样使用:
//构造函数 打开指定文件(也可以使用 open 函数打算), 另外学过操作系统的知道, 打开文件要指定打开方式,
//既然这个是 ifstream 类, 默认就是读方式打开, 当然也可以显示传参
ifstream ifs("file_name");
ifstream ifs("file_name", ios_base::in);
istringstream 则是以字符串作为流提供的I/O操作, 这里理解为以字符串为源(ifstream 则以文件为源, cin 以键盘为源),也就是我们用字符串实例化一个 istringstream 对象, 然后再从中提取。而且使用更为简单:
了解构造函数(还是有打开方式的), 和 str (获取或设置流的内容)istringstream 就已经掌握了。
经过前面较为详细的讲解, 输出流就变得非常简单, 另外输出流的流操作也比较少,掌握 ostream 的用法并了解如何实例化 ofstream 和 ostringstream 就可以了。 下面给出对应的成员函数:
与 ios_base 中的作用类似, 目标都是为了格式化输出, 为了统一操作符的名称和 ios_base 的流格式值是一一对应的, iostream 为了区分是操作符还是一个 fmtflags 用了命名空间。 在 std 中的都是操作符, 用类或者对象表示的则是 fmtflags。
操作符分两种, 一个是无参操纵符,另一种是有参操纵符,使用方式一致,见下例:
cout << std::showbase << std::hex << 100 << endl; //输出 0x64
至于有参操纵符, 在头文件 < iomanip > 当中,有如下操纵符:
iostream 还有其他类没有涉及, 例如 fstream 和 stringstream, 这也就是对应的输入和输出流的结合,同时有读、写指针,使用方法一样,问题是的确定相应的打开模式。 最后还有 streambuf 是负责将相应的流操作 绑定到对应的对象当中, 所有分别有 filebuf 和 stringbuf, 由于 cin、cout的对象固定为标准输入、输出,所有无需 streambuf 对象。