从头到尾的认识 C++ I/O 操作

  • 涉及的内容
  • 面向对象的 I/O 流库
  • ios_base —— 基类
  • ios 类 —— 指示I/O流的状态标志
  • istream 提供的统一操作方式
  • ifstream 与 istringstream 类的使用
  • 输出流的操作
  • 操纵符 —— Manipulator
  • 总结

涉及的内容

在本篇博客中, 我算是 CPP Reference (里面的信息才是最为全面,英语稍微好点可以自己去看) 的搬运工, 再将里面的信息组织和讲解一下,耐心看一遍,“日后” 随便可查阅,有需要自行扩展。

  1. 了解 C++ I/O 的设计与统一的操作方式
  2. 使用操纵符或函数格式化输出

面向对象的 I/O 流库

C++ 提供的I/O类库之间的关系:
从头到尾的认识 C++ I/O 操作_第1张图片
通过上图, 可以发现熟悉的 cin、 cout 分别是 istream、 ostream 的对象(不过你不能实例化喔,因为构造函数是保护的),再看 ifstream、istringstream 和 ofstream、ostringstream 则是对应的派生类, 另外还有将 Input 和 Output 结合的 fstream、stringstream 。 再根据 OOP(面向对象编程) 的思想, 我们可以以使用 cin、 cout 的方式来使用其他类(当然也要了解其中的区别), 接下来也通过 cin、 cout 来推广到其他类的使用。

现在再了解一下 C++ I/O 库的 4 个元素 :

  1. 类 —— ios_base、ios、istream、ostream 等等
  2. 对象 —— cin、cout、cerr、clog
  3. 类型 —— 对内置数据类型的 typedef
  4. 操纵符 —— 修改流的属性或格式

ios_base —— 基类

ios_base 定义了一些不依赖输入流还是输出流的部件, 意思就是一些操作仅属于输出流的,但是也在这里定义了, 所以我们也可以在输入流使用一些输出流的操作(但是结果是没有任何作用的), 例如 cin.width() 是合法但多鱼的。

ios_base 有许多内容, 这里讲解两个部分。

  1. 格式化(输出)
函数 意义
flags 获取、设置(传入 fmtflags)流格式
setf 设置指定的流格式。 配合 unsetf 使用
precision 获取、设置浮点数精度 —— 受 floatfiled 格式控制
width 获取、设置流域的宽度(即一次输出的最小长度)—— 受 adjustfiled 格式控制

使用下面的数值(fmtflags)完成流的格式化

从头到尾的认识 C++ I/O 操作_第2张图片
在这里插入图片描述
上面的表大致描述了流的格式化, 下面通过一个简单的例子了解具体用法:

// 设置整数的写使用 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 对象), 就可以实现进制转换。

  1. 流打开方式和流定位

从头到尾的认识 C++ I/O 操作_第3张图片
流定位
上面是流的打开模式和流定位(读写指针), 看上去有点抽象,因为目前还没有任何相关操作,具体实现在后面的派生类当中, 但是转换到我们熟悉的文件, 那就是确定文件的打开方式,如下面的例子:

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 操作

ios 类 —— 指示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 意一样。 在这里可以把 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 作为条件判断的对象时, 以前面讲到的 “状态标志” 确定是否为真。

下面给出非格式化输入(具体用法请 查阅手册 ):

从头到尾的认识 C++ I/O 操作_第4张图片
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 与 istringstream 类的使用

ifstream 作为 istream 的派生类,用于文件读操作,实际上掌握了 istream (cin) 的用法, ifstream 也是一样, 现在就看一看 ifstream 多了些什么操作:

从头到尾的认识 C++ I/O 操作_第5张图片
简单吧? 相对与 istream , 这里需要我们自己实例化一个对象(因为要打开文件嘛), 所以就这样使用:

//构造函数 打开指定文件(也可以使用 open 函数打算), 另外学过操作系统的知道, 打开文件要指定打开方式, 
//既然这个是 ifstream 类, 默认就是读方式打开, 当然也可以显示传参
ifstream ifs("file_name"); 
ifstream ifs("file_name", ios_base::in);

istringstream 则是以字符串作为流提供的I/O操作, 这里理解为以字符串为源(ifstream 则以文件为源, cin 以键盘为源),也就是我们用字符串实例化一个 istringstream 对象, 然后再从中提取。而且使用更为简单:

从头到尾的认识 C++ I/O 操作_第6张图片
了解构造函数(还是有打开方式的), 和 str (获取或设置流的内容)istringstream 就已经掌握了。

输出流的操作

经过前面较为详细的讲解, 输出流就变得非常简单, 另外输出流的流操作也比较少,掌握 ostream 的用法并了解如何实例化 ofstream 和 ostringstream 就可以了。 下面给出对应的成员函数:

  1. ostream (cout):
    从头到尾的认识 C++ I/O 操作_第7张图片
    这里 flush 是将缓存区刷新, 也就是马上将数据写到对应目标上。

  2. ofstream —— 文件流
    从头到尾的认识 C++ I/O 操作_第8张图片

  3. ostringstream —— 字符串流
    从头到尾的认识 C++ I/O 操作_第9张图片

操纵符 —— Manipulator

与 ios_base 中的作用类似, 目标都是为了格式化输出, 为了统一操作符的名称和 ios_base 的流格式值是一一对应的, iostream 为了区分是操作符还是一个 fmtflags 用了命名空间。 在 std 中的都是操作符, 用类或者对象表示的则是 fmtflags。

操作符分两种, 一个是无参操纵符,另一种是有参操纵符,使用方式一致,见下例:

cout << std::showbase << std::hex << 100 << endl; //输出 0x64

至于有参操纵符, 在头文件 < iomanip > 当中,有如下操纵符:

从头到尾的认识 C++ I/O 操作_第10张图片

总结

iostream 还有其他类没有涉及, 例如 fstream 和 stringstream, 这也就是对应的输入和输出流的结合,同时有读、写指针,使用方法一样,问题是的确定相应的打开模式。 最后还有 streambuf 是负责将相应的流操作 绑定到对应的对象当中, 所有分别有 filebuf 和 stringbuf, 由于 cin、cout的对象固定为标准输入、输出,所有无需 streambuf 对象。

你可能感兴趣的:(学习,C++)