C++学习笔记(IIX)之 IO库
C++语言不能直接处理输入输出,而是通过一族定义在标准库中的类型来处理IO的。
1.IO类
为了支持不同的IO处理操作,在istream和ostream之外,标准库还定义了其他一些IO类型,他们分别被定义在三个独立的头文件中:iostream定义了用于读写流的基本类型,fstream定义了读写命名文件的类型,sstream定义了读写内存对象的类型。以下列出了表格
头文件 | 类型 |
iostream |
istream,wistream 从流读取数据 |
fstream |
ifstream, wifstream 从文件读取数据 |
sstream |
istringstream, wistringstream 从string读取数据 |
为了支持使用宽字符的语言,标准库定义了一组类型和对象来操纵wchar_t类型的数据。宽字符版本的类型和函数的名字以一个w开始。例如,wcin、wcout和wcerr。
下图是他们类间关系:
IO对象无拷贝或赋值
由于不能拷贝IO对象,因此我们也不能将形参或返回类型设置为流类型,进行IO操作的函数通常以引用方式传递和返回流。读写一个IO对象会改变其状态,因此,传递和返回的引用不能是const的。
ofstream out1, out2; out1 = out2;//错误:不能对流对象赋值 ofstream print(ofstream);//错误:不能初始化ofstream参数 out2 = print(out2);//错误:不能拷贝流对象
条件状态
状态位 | 说明 |
strm::iostate | strm是一种IO类型,在上表中已经列出。iostate是一种机器相关的类型,提供了表达条件状态的完整功能 |
strm::badbit | strm::badbit用来指出流已崩溃 |
strm::failbit | strm::failbit用来指出一个IO操作失败 |
strm::eofbit | strm::eofbit用来指出流到达了文件结束 |
strm::goodbit | strm::goodbit用来指出流未处理错误状态。 |
s.eof() | 若流s的eofbit置位,则返回true |
s.fail() | 若流s的failbit或badbit置位,则返回true |
s.bad() | 若流s的badbit置位,则返回true |
s.good() | 若流s处于有效状态,则返回true |
s.clear() | 将流s中所有条件状态复位,将流的状态设置为有效。返回void |
s.clear(flags) | 根据给定的flags标志位,将流s中对应条件状态位置位,flags的类型为strm::iostate.返回void |
s.setstate(flags) | 根据给定的flags标志位,将流s中对应条件状态位置位,flags的类型为strm::iostate.返回void |
s.rdstate() | 返回流s的当前条件状态,返回值类型为strm::iostate |
一个流一旦发生错误,其后续的IO操作都会失败。只有当一个流处于无错状态时,我们才可以对其读写数据。因此前通常应该在使用一个流之前检查它是否处于良好状态。一般是将它当作一个条件来使用。如:
#include <iostream> using namespace std; int main() { int nVal; while(cin >> nVal) { //读操作成功 } return 0; }
goodbit的值为0,表示流未发生错误。
badbit表示系统级错误,通常情况下,一旦badbit被置位,流就无法再使用了。
failbit在发生可恢复错误后被置位。如期望读取数据却读出一个字符等错误。
eofbit指到达文件结束位置,此时eofbit和failbit都会被置位。
如果badbit、failbit、eofbit中任何一个被置位,则检测状态的条件会失败。下面是流状态及函数对照表:
iostate value (member constant) | indicates | functions to check state flags | ||||
---|---|---|---|---|---|---|
good() | eof() | fail() | bad() | rdstate() | ||
goodbit | No errors (zero value iostate) | true |
false |
false |
false |
goodbit |
eofbit | End-of-File reached on input operation | false |
true |
false |
false |
eofbit |
failbit | Logical error on i/o operation | false |
false |
true |
false |
failbit |
badbit | Read/writing error on i/o operation | false |
false |
true |
true |
badbit |
由上表知,在badbit被置位时,fail()也会返回true。使用good或fail是确定流的总体状态的正确方法。实际上,我们将流当作条件使用的代码就等价于!fail()。而eof和bad操作只能表示特定的错误。
输出缓冲
每个输出流都管理一个缓冲区,用来保存程序读写的数据。
导致缓冲刷新(即,数据真正写到输出设备或文件)的原因有很多:
程序正常结束,作为main函数的return操作的一部分
缓冲区满时,需要刷新缓冲,而后新的数据才能继续写入缓冲区
使用操纵符如endl来显式刷新缓冲区
在每个输出操作之后,可以用操纵符unitbuf设置流的内部状态,来清空缓冲区。默认情况下,对cerr是设置unitbuf的,写到cerr的内容都是立即刷新的
一个输出流可能被关联到另一个流。此时,当读写被关联的流时,关联到的流的缓冲区也会被刷新。如默认情况下,cin和cerr都关联到cout。
操纵符
操纵符 | 含义 |
endl | 完成换行并刷新缓冲区 |
ends | 向缓冲区插入一个空字符并刷新缓冲区 |
flush | 不输出任何额外字符只刷新缓冲区 |
unitbuf | 接下来每次写操作后进行一次flush操作 |
nounitbuf | 重置流,恢复正常缓冲区刷新机制 |
Eg:
#include <iostream> using namespace std; int main() { cout<<"carpenter's"<<ends;//输出carpenter's和一个空字符,然后刷新缓冲区 cout<<"ink"<<flush;//输出ink然后刷新缓冲区 cout<<"marker"<<endl;//输出marker和一个换行符,然后刷新缓冲区 cout<<unitbuf;//所有输出操作后都会立即刷新缓冲区 //任何输出都立即刷新,无缓冲 cout<<nounitbuf;//回到正常的缓冲机制 return 0; }
效果图:
Note:如果程序崩溃,输出缓冲区不会被刷新。
关联输入和输出流
用来关联输入与输出流的函数是tie,此函数有两个重载版本:
版本一,不带参数,返回指向输出流的指针。若关联返回指向关联输出流,否则返回空指针。
版本二,接受一个指向ostream的指针,将自己关联到此ostream。即,x.tie(&o)将流x关联到输出流o。x既可是istream也可是ostream。
cin.tie(&cout);//仅用来展示:标准库将cin和cout关联在一起 //old_tie指向当前关联到cin的流(如果有的话) ostream* old_tie = cin.tie(nullptr);//cin不再与其他流关联 //将cin与cerr关联:这不一个好主意,因为cin应该关联到cout cin.tie(&cerr);//读取cin会刷新cerr而不是cout cin.tie(old_tie);//重建cin和cout间的正常关联
每个流同时最多关联到一个流,但多个流可以同时关联到同一个ostream。
2.文件输入输出
除了继承自iostream类型的行为之外,fstream中定义的类型还增加了一些新的成员来管理与流关联的文件。如下表:
fstream特有的操作 | |
函数 | 说明 |
fstream fstrm; | 创建一个未绑定的文件流。fstream是头文件fstream中定义的一个类型 |
fstream fstrm(s); | 创建一个fstream,并打开名为s的文件。s可以是string也可是C风格字符串的指针。默认的文件模式mode依赖于fstream的类型 |
fstream fstrm(s, mode); | 与前一个构造函数类似,但按指定mode打开文件 |
fstrm.open(s) | 打开名为s的文件,并将文件与fstrm绑定。默认的文件模式mode依赖于fstream的类型 |
fstrm.close() | 关闭与fstrm绑定的文件。返回void |
fstrm.is_open() | 返回一个bool值,指出与fstrm关联的文件是否成功打开且尚未关闭 |
使用文件流对象
当我们想要读写一个文件时,可以定义一个文件流对象,并将对象与文件关联起来。每个文件流类都定义了一个名为open的成员函数,它完成一些系统相关的操作,来定位给定的文件,并视情况打开为读或写模式。
创建文件对象时,可以提供文件名(可选的)。如果提供了一个文件名,则open会自动被调用:
ifstream ifs("D:\\test.txt");//构造一个ifstream并打开给定的文件 ifstream ifs;//输出文件流未关联到任何文件 ifs.open("D:\\test.txt");//打开给定的文件
用fstream代替iostream&
要求使用基类型对象的地方,可以用继承类型的对象来替代。这意味着,接受一个iostream类型引用(或指针)参数的函数,可以用一个对应的fstream(或sstream)类型来调用。
成员函数open与close
如果定义一个空文件流对象,可以随后调用open来将它与文件关联起来:
如果调用open失败,failbit会被置位。因为调用open可能失败,进行open是否成功的检测通常是一个好习惯
if(out)//检查open是否成功 //open成功,可以使用文件
一旦一个文件流已经打开,它就保持与对应文件的关联。为了将文件流关联到另外一个文件,必须入选关闭已经关联的文件。一旦文件成功关闭,才可以打开新的文件:
ifs.close(); //关闭文件 ifs.open(ifile +"6"); //打开另一个文件
如果open成功,则open会设置流的状态,使得good()为true。
Note:当一个fstream对象被销毁时,close会自动被调用。
#include <fstream> #include <iostream> int main() { std::ifstream ifs; ifs.open("D:\\test.txt",std::ifstream::in);//以读的方式打开test.txt文件 char ch = ifs.get();//从ifs流读取一个字符 while (ifs.good()) { std::cout << ch; ch = ifs.get(); } ifs.close(); return 0; }
自动构造和析构
//对每个传递给程序的文件执行循环操作 for (auto p = argv + 1; p != argv + argc; ++p) { ifstream input(*p);//创建输入流并打开文件 if(input)//如果文件打开成功,“处理”此文件 { process(input); } else { cerr<<"couldn't open:" + string(*p); } }//每个循环步input都会离开作用域,因此会被销毁
因为input是for循环的局部变量,它在每个循环步中都要创建和销毁一次。当一个fstream对象其作用域时,与之关联的文件会自动关闭。
Note:当一个fstream对象被销毁时,close会自动被调用。
2.2 文件模式
每个流都有一个关联的文件模式(file mode),用来指出如何使用文件。下表列出文件模式和它们的含义。
文件模式表 | |
文件模式 | 含义 |
in | 以读方式打开 |
out | 以写方式打开 |
app | 每次写操作前均定位到文件末尾 |
ate | 打开文件后立即定位到文件末尾 |
trunc | 截断文件 |
binary | 以二进制方式进行IO |
无论用哪种方式打开文件,我们都可以指定文件模式,
有如下限制:
- 只可以对ofstream或fstream对象设定out模式
- 只可以对ifstream或fstream对象设定in模式
- 只有当out也被设定时才可设定trunc模式
- 只要trunc没被设定,就可以设定app模式。在app模式下,即使没有显式指定out模式,文件也总是以输出方式被打开
- 默认情况下,即使我们没有指定trunc,以out模式打开的文件也会被截断。为了保留以out模式打开的文件的内容,我们必须同时指定app模式,这样只会将数据追加写到文件末尾;或者同时指定in模式,即打开文件同时进行读写操作。
- ate和binary模式可用于任何类型的文件流对象,且可以与其他任何文件模式组合使用。
每个文件流类型都定义了一个默认的文件模式,ifstream关联的文件默认以in模式打开;ofstream关联的文件默认以out模式打开;与fstream关联的文件默认以in和out模式打开。
以out模式打开文件会丢弃已有数据
默认情况下,当打开一个ofstream时,文件的内容会被丢弃。阻止一个ofstream清空给定文件内容的方法是同时指定app模式:
//在这几条语句中,file1都被截断 ofstream out("file1");//隐含以输出模式打开文件并截断文件 ofstream out2("file1", ofstream::out);//隐含地截断文件 ofstream out3("file1", ofstream::out | ofstream::trunc); //为了保留文件内容,我们必须显式指定app模式 ofstream app("file2", ofstream::app);//隐含为输出模式 ofstream app2("file2", ofstream::out | ofstream::app);
Note:保留ofstream打开的文件中已有数据的唯一方法是显式指定app或in模式。
每次调用open时都会确定文件模式
对于一个给定流,每当打开文件时,都可以改变其文件模式。
ofstream out;//未指定文件打开模式 out.open("scratchpad");//模式隐含设置为输出和截断 out.close();//关闭out,以便将其用于其他文件 out.open("precious", ofstream::app);//模式为输出和追加 out.close();
Note:在每次打开文件时,都要设置文件模式,可能是显示地设置,也可能是隐式地设置。当程序未指定模式时,就使用默认值。
3.string流
sstream头文件定义了三个类型来支持内存IO,这些类型可以向string写入数据,从string读取数据,就像stirng是一个IO流一样。
istringstream 从string读取数据,ostringstream向string写入数据,而stringstream既可从string读数据也可向string写数据。下表是stringstream对象所特有的操作。
stringstream特有的操作 | |
操作 | 说明 |
sstream strm; | strm是一个未绑定的stringstream对象。sstream是头文件sstream中定义的一个类型 |
sstream strm(s); | strm是一个sstream对象,保存string s的一个拷贝。此构造函数是explicit的 |
strm.str() | 返回strm所保存的string的拷贝 |
strm.str(s) | 将string s拷贝到strm中。返回void |
使用istringstream
当我们某些工作是对整行文本进行处理,而其他一些工作是处理行内的单个单词时,通过可以使用istringstream。下面举一个粟子:
假定,一个文件中存放的是通讯录,每条记录以一个人名开始,后面跟随一个或多个电话号码,如下:
William 0558-1234567 15300000000
view 0571-1234567
Sun 6095550132 2015550175 8005550000
将其读取到类对象中。
#include <string> #include <vector> #include <fstream>//文件流 #include <sstream>//字符串流 using namespace std; struct PersonInfo //存放用户电话信息的类 { string name; vector<string> phones; }; int main() { std::ifstream ifs; ifs.open("D:\\test.txt", std::ifstream::in);//以读的方式打开test.txt文件 string line, word;//分别保存来自输入的一行和单词 vector<PersonInfo> people;//保存来自输入的所有记录 //逐行从输入读取数据,直至ifs遇到文件尾(或其他错误) while (getline(ifs,line)) { PersonInfo info;//创建一个保存此记录数据的对象 istringstream record(line);//将记录绑定到刚读入的行 record >> info.name;//读取名字 while (record >> word)//读取电话号码 { info.phones.push_back(word);//保持它们 } people.push_back(info);//将此记录追加到people末尾 } return 0; }
使用ostringstream
当我们逐步构造输出,希望最后一起打印时,ostringstream是便很有用。
#include <string> #include <vector> #include <fstream>//文件流 #include <sstream>//字符串流 #include <iostream> using namespace std; struct PersonInfo { string name; vector<string> phones; }; //验证电话号码的有效性,并不完善 bool validTel(const string& strTel) { if (!strTel.length()) return false; if ('0' == strTel.at(0))//固话 { if (-1 == strTel.find('-')) return false; return true; } else if ('1' == strTel.at(0) && 11 == strTel.length() )//手机 { return true; } return false; } int main() { std::ifstream ifs; ifs.open("D:\\test.txt", std::ifstream::in);//以读的方式打开test.txt文件 string line, word;//分别保存来自输入的一行和单词 vector<PersonInfo> people;//保存来自输入的所有记录 //逐行从输入读取数据,直至ifs遇到文件尾(或其他错误) while (getline(ifs,line)) { PersonInfo info;//创建一个保存此记录数据的对象 istringstream record(line);//将记录绑定到刚读入的行 record >> info.name;//读取名字 while (record >> word)//读取电话号码 { info.phones.push_back(word);//保持它们 } people.push_back(info);//将此记录追加到people末尾 } for (const auto &entry : people)//对people中每一项 { ostringstream formatted, badNums;//每个循环步创建一个对象 for (const auto &nums : entry.phones)//对每个数 { if (!validTel(nums)) { badNums << "\t" << nums;//将数的字符串形式存入badNums } else//将格式化的字符串"写入"formatted { formatted << "\t" << (nums); } } if (badNums.str().empty())//没有错误的数 { cout << entry.name << "\t"//打印名字 << formatted.str() << endl;//与格式化的数 } else//否则,打印名字和错误的数 { cerr << "input error: " << entry.name << " invalid number(s) " << badNums.str() << endl; } } return 0; }
下面是测试数据的运行结果:
小结:
C++使用标准库类来处理面向流的输入和输出:
iostream处理控制台IO
fstream处理命名文件IO
stringstream完成内存string的IO