Cpp: I/O
C++风格的I/O
I/O是一门语言很重要的部分,<iostream>之所以含有相当一部分好用的函数,是因为在I/O实现时各个头文件互相包含,使得只需要包含<iostream>就能够使用很多的东西
但如果要用STL、实现文件交互等,<iostream>并没有包含它们,推荐使用万能头文件<bits/stdc++.h>,包含几乎所有会用到的东西
stdInput
cin
在C++中,istream类的对象cin(在<iostream>中声明)替代了scanf,它自动创建标准输入缓冲区,从标准输入流提取输入
这些对象被称为流对象,它将流和对象联系起来,一个非iostream,fstream类流对象只和一个缓冲区、两端(程序端、输出输入端)相关联
最简单的获取输入的代码如下,它是格式化流插入符(即会将读取的字符串转换成后接对象的相应类型,除了它,其它方法都是非格式化读取),接受一个引用:
1 | cin >> identifier; // 有回显、有缓冲、忽略空格与换行,遇空格或回车停止 |
自然想到,如何读取空格,实现面向行的输入?cin有两种方法可以实现:
1 | cin.getline(char*,std::streamsize);// 读取size-1个字符后或遇到换行符停止,且丢弃换行符 |
std::streamsize是long int的重命名,即有符号整型;它们的共同特性是都不会将换行符读入到字符串中,且可以添加第三个char型参数表示自定义终止字符
如果需要只读取缓冲区内的任意一字符,cin有重载的get()方法,其一的用法类似getchar():
1 | ch = cin.get(); // 读取一字符,返回它的字符编码;如遇到EOF,则返回eof(一个负数) |
它们常用于抛弃掉缓冲区的换行符,但一次调用只能丢弃一个字符,cin提供ignore()方法来一次性抛弃多个字符:
1 | cin.ignore(std::streamsize n,delim='\n'); // 抛弃n个字符后或遇到delim并抛弃它后停止 |
operator>>(),cin.getline(),cin.get(...),cin.get(char),cin.ignore()都返回cin的引用,这意味着它们可以链式调用:
1 | cin.getline(a,b).getline(a,b); // 读取两行(或是读取一串较长行) |
如果非要给cin改名,可以选择创建引用istream& in=cin(一般不会直接使用istream或ostream的构造函数,只有在创建文件流对象的时候会间接调用i(o)stream(sb&)、或是想实现多缓冲区输入输出时会直接使用它,在这里不作讨论)
string的输入
上述方法都不支持string类对象,然而显然string类更常用,于是<string>中有一个全局函数:
1 | getline(istream&,string&,char='\n'); |
三个参数,第一个为自定义的输入流,通常是标准输入流cin,第三个参数为指定的结束符(默认为换行符)
有些编译器在使用string类后默认添加<string>,所以就算没有包含它也能运行,但getline()是<string>的函数,应该养成习惯事先包含它,以免出错
stdOutput
cout
与cin的大部分内容类似,cout是ostream类的对象,它与标准输出流相关联,格式化输出如下,operator<<接受引用或常量:
1 | cout << identifier << constant; |
由于string类定义的operator<<()能完整输出整个字符串,所以输出部分不像输入一样有专门的函数
对应putchar(),cout也有类似的方法:
1 | cout.put(char) // 输出单个字符 |
operator<<(),cout.put(char)都返回指向cout的引用,所以cout也可以链式输出:
1 | (cout << str << endl).put(48) |
标准输出缓冲区采用行缓冲,它遇到换行符或eof时将自动刷新缓冲区,此外还提供了两个控制符:
1 | cout << flush << endl; // flush直接刷新缓冲区,endl在缓冲区中插入换行符后再刷新它 |
精细控制输出
C++提供一种不逊于printf()那样精细控制输出位数、输出形式的,更简洁的输出方式,那就是许多控制符
除了刷新缓冲区的flush和endl(在<ostream>中),其它常用的控制符如下:
hex,oct,dec:将整型数据以十六、八、十进制输出,在<bits/basic_ios.h>中setprecision(int):永久设置数据的精度(显示数据的总位数)setfill(char):永久设置字段中填充的字符(默认为空格)setw(int):暂时设置字段的宽度,在输出下一数据后恢复默认字段值(即0),用于对齐画面等
除了第一条,其它三条控制符都需要包含头文件<iomanip>
这些控制符实际上是调用iostream类的祖宗类ios_base的成员函数setf()和部分成员,来实现控制输出的功能,其它控制符详见fmtflags
注:fmtflags不是控制符,而只是部分控制符调用了setf(fmtflags)而已,ios_base::fmtflags和后续将提到的ios_base::iostate,ios_base::openmode都是一串二进制掩码(bitmask),这意味着它们可以通过'|'运算符叠加,在ios_base中提供了它们的一些枚举常量
ios(即basic_ios<char>)类是ios_base类的派生类,能使用它的所有公开和保护性成员,所以直接用ios::badbit等也是正确的,所以在除定义外的说明和实例中都使用ios::
流状态
状态位
所有流对象都有称为流状态的属性,流的打开或关闭由三个状态位决定:
badbit:系统性错误时(如文件不存在、文件不允许访问等)被设置,这种错误一般无法恢复failbit:格式错误时(如输入字符赋给整型对象、get(str)读取到空行等)被设置,这种错误可以恢复eofbit:遇到eof时被设置
ios_base类中有ios_base::badbit,ios_base::failbit,ios_base::eofbit,ios_base::goodbit四种标准状态常量,它们以三位二进制数表示,分别为001,100,010,000
当这三个状态位都被清除(即0)时,goodbit将被清除,表示这个流一切正常,是打开的
检查状态的方法
可通过对应的方法(bad(),fail(),eof(),rdstate())查看这四个位的状态(具体应查看头文件定义,实际上有略微不同)
operator!()和rdstate()效果一样,而good()是与operator!()相反的方法,当goodbit=0时返回true
这些方法都在ios中定义
需要清楚的是,rdstate()的返回值是ios_base::iostate对象,而其它方法都返回布尔类型,这意味着rdstate()方法还可以通过与标准状态常量对比进行错误检测
这些方法都可用于检测错误,例如rdstate()可用于粗略地检查所有错误类型,出错则返回真
这些流并不会在出错时报错,这涉及到ios::exceptions()方法,它默认返回goodbit,只有当f.exceptions() & f.rdstate()不为零时,程序才会抛出ios_base::failure错误
如果需要让流报错,应改变exceptions()的返回值(使用它的重载函数exceptions(iostate)):
1 | // 示例 |
清除状态的方法
ios::clear()和ios::setstate()方法可将流设置成正常,原理有所不同:
clear()将该流的状态强制改成提供的状态,默认提供0,即ios::goodbitsetstate()将该流的状态和提供的状态相叠加,相当于两个二进制数进行了按位或操作,实际上,进行f.setstate(state)相当于进行f.clear(f.rdstate() | state)
1 | // 定义 |
当然,可以用其他流的状态作为参数,只是这没有意义
常用不带参数的clear()方法来强制打开流,而setstate()常被流对象自动调用,来叠加到自身的流状态里
文件I/O
实现文件IO需要包含头文件<fstream>,创建输入流则创建ifstream对象,创建输出流则创建ofstream对象,只不过流的另一端由键盘和显示器变成了文件
因为ifstream,ofstream分别为istream,ostream的派生类,所以可以像cin,cout一样使用方法,而除此之外,文件IO有额外的成员和方法
创建文件流和关闭文件
ifstream和ofstream类中额外定义了默认构造函数和带字符串(有char*类型和string&类型的两个重载)参数的构造函数
以ifstream为例,初始化一个文件流对象如下:
1 | // 定义 |
一个文件流对象除了继承istream/ostream的用于格式化输入输出的流装置外,还额外包含一个私有的filebuf对象,用于与文件进行底层交互;例如文件流对象含有的成员函数open()和close(),它们实质上是调用filebuf类的open()和close(),只是在类里面重写了一遍方便使用罢了
fstream类的对象较为特殊,它继承了两个缓冲区,目的在于用一个对象实现对文件的输入和输出
它们的析构函数是空的,这意味着文件是通过方法close()关闭的,即关闭文件不影响流对象的重复使用,而流的打开与关闭和流状态有关;这也更说明,流不是一个容器,而数据也只会暂时存储在缓冲区中
文件模式
对于一个文件流对象fio,如果要检查它的流是否异常(而不是文件是否打开),一般不用像cin,cout一样的operator!(),rdstate()方法,而是成员函数is_open()(还是调用filebuf类的成员,但这次是间接调用了file类的成员),它能检查出“文件打开方式错误”一类的异常
C++的文件模式设计与C有许多共通之处,但更好理解和记忆:
| ios_base::openmode(以下省略ios::) | C风格 | 意义 | 文件存在 | 文件不存在 |
|---|---|---|---|---|
| in(ifstream默认值) | “r” | 读打开 | 正常 | 异常(failbit+badbit) |
| out(ofstream默认值;默认加上trunc) | “w” | 写打开 | 清空文件 | 创建空文件 |
| ate | fseek(file*,0,SEEK_END) | 跳到eof | 文件必须已经打开 | |
| app(默认加上out) | “a” | 追加写打开 | 不会被清空,并在文件尾开始添加内容 | 创建空文件 |
| trunc | - | 清空文件 | 文件必须已经写打开 | |
| binary | “b” | 二进制模式打开 | 文件必须已经打开 | |
| in | out(fstream默认值) | “r+” | 读写打开 | 不清空文件,并在文件开头开始添加内容 | 异常(failbit+badbit) |
| in | out | trunc | “w+” | 读写打开 | 清空文件 | 创建空文件 |
| other | ate | “other”后,fseek(file*,0,SEEK_END) | 打开文件后跳到eof | 视other而定 | |
| other | binary | “other+b” | 二进制other模式打开 | 视other而定 | |
以下为常用模式:
1 | // 示例 |
读写与异常处理
一般来说,只要打开或关闭文件正常,那么文件流就会保持正常,对于文本内容,像使用cin,cout一样即可;而对于二进制内容,可以用read()(读方法,在istream类中)和write()(写方法,在ostream类中),它们和其它输入输出方法的区别是在读取/输出时不会添加\0,且read()函数不提供默认或指定的终止符(getline()默认遇回车停止):
1 | fin.read(char*,int); // 读取指定个字符到char*中,不支持string类 |
当然在关闭文件后,为了防止读到EOF或其他问题影响这个流对象的再使用,应选择改变exceptions(),或是不管不顾直接clear()强行打开流
附
以阅读头文件的方式回顾所学(Doxygen风格注释),以下是部分内容在头文件内的定义(小标题是头文件名):
各类关联
重命名:


继承关系:

ios_base.h
以下都属于ios_base类:
流状态常量:








文件模式:


basic_ios.h

&setstate().png)




以上是ios类中的成员,下面这个控制符以及oct,dec等是本头文件内的内联函数:

istream&ostream
以istream为例,operator<<与其类似:

get(...)与其类似:


基类初始化器:

控制符flush和endl都在<ostream>里,它们是内联函数:

ifstream&ofstream
以ifstream类为例:


fstream
这些是filebuf类里的:

