文章目录
- 前言
- IO头文件
- iostream
- fstream
- sstream
- 流的使用
- 不能拷贝或对 IO对象 赋值
- 条件状态与 iostate 类型
- 输出缓冲区
- 文件流
- fstream类型
- 文件模式
- 文件光标函数
- tellg() / tellp()
- seekg() / seekp()
- 向文件存储内容/读取文件内容
- string流
- istringstream
- ostringstream
前言
我们在使用 C++
的过程中,总避免不了 IO操作
,比如经常用到的一些 IO库设施
:
istream
:(输入流)类型,提供输入操作。ostream
:(输出流)类型,提供输出操作。cin
:一个istream
对象,从标准输入读取数据。cout
:一个ostream
对象,向标准输出写入数据。cerr
:一个ostream
对象,通常用于输出程序错误消息,写入到标准错误。>>运算符
:用来从一个istream
对象读取输入数据。<<运算符
:用来向一个ostream
对象写入输出数据。getline函数
:从一个给定的istream
读取一行数据,存入一个给定的string
对象中。
但实际上可能仅仅是懵懵懂懂在使用,如果不深入了解的话,这样的使用是浅薄的。
IO头文件
iostream
定义了用于读写流的基本类型。
- istream,wistream 从流读取数据
- ostream,wostream 向流写入数据
- iostream,wiostream 读写流
fstream
定义了读写命名文件的类型。
- ifstream,wifstream 从文件读取数据
- ofstream,wofstream 向文件写入数据
- fstream,wfstream 读写文件
sstream
定义了读写内存string对象的类型。
- istringstream,wistringstream 从 string 读取数据
- ostringstream,wostringstream 向 string 写入数据
- stringstream,wstringstream 读写 string
流的使用
标准库通过继承使我们忽略不同类型流之间的差异。举例来说,类型 ifstream
和 istringstream
都继承自 istream
。因此,我们如何使用 cin
,就可以同样地使用这些类型的对象。
不能拷贝或对 IO对象 赋值
ofstream out1, out2;
out1 = out2; // error:不能对流对象赋值
ofstream printf(ofstream); // error: 不能初始化ofstream参数
out2 = printf(out2); // error: 不能拷贝流对象
- 由于不能拷贝IO对象,因此我们也不能将形参或返回类型设置为流类型。
- 进行IO操作的函数通常以引用方式传递和返回流。读写一个IO对象会改变其状态,因此传递和返回的引用不能是const的。
条件状态与 iostate 类型
IO操作使用不当的话会发生错误,而如果是发生在系统深处的错误,那么就超出了应用程序可以修正的范围。但也有一些错误是可以恢复的,IO类也提供了一些函数和标志来访问、操纵流的条件状态。
下面对表中的四个条件位作进一步介绍。
iostate 类型
IO库定义了一个与机器无关的 iostate
类型,它提供了表达流状态功能。
IO库定义了 4个 iostate类型
的 constexpr
值(常量表达式),表示特定的位模式:
- badbit: 表示系统级错误,如不可恢复的读写错误。通常情况下,一旦
badbit
被置位,流就无法再使用了。 - failbit: 在发生可恢复错误后被置位,如期望读取数值却读出一个字符等错误。这种问题通常是可以修正的,流还可以继续使用。
- eofbit: 如果到达文件结束位置,连同
failbit
一起被置位。 - goodbit: 值为
0
时,表示流未发生错误。
对他们进行一个简单的使用:
auto old_state = cin.rdstate(); // 返回流cin的当前状态,返回值类型为 strm::iostate
cin.clear(); // 将cin所有条件位复位,换言之,使cin有效
// clear重载版本允许有参数,接受一个iostate值,表示流的新状态
cin.clear(cin.rdstate() & ~cin.failbit & ~cin.badbit);
// 先用rdstate读出当前条件状态,再将failbit和badbit复位生成新状态。
process_input(cin); // 使用cin
cin.setstate(old_state); // 将cin置为原有状态
输出缓冲区
以前对于输出缓冲区是没什么概念的……直到在做美团往年笔试题的时候,有道编程题如果用 endl
作为换行会报超时,原因是 endl
频繁刷新输出缓冲区,因此需要用 '\n'
。
操作系统的 IO操作
是很耗时的,缓冲机制使操作系统将程序的多个输出操作组合成单一的系统级写操作(写到显示设备上),对性能的提升是巨大的。
导致缓冲刷新(数据真正写到输出设备或文件)的原因有很多:
- 程序正常结束,作为
main函数
的return操作
的一部分,缓冲刷新被执行。 - 缓冲区满时,需要刷新缓冲,而后新的数据才能继续写入缓冲区。
- 我们可以使用操纵符(如
endl
) 来显式刷新缓冲区。 - 在每个输出操作之后,我们可以用
操纵符unitbuf
设置流的内部状态,来清空缓冲区。默认情况下,对cerr
是设置unitbuf
的,因此写到cerr
的内容都是立即刷新的。 - 一个输出流可能被关联到另一个流。在这种情况下,当读写被关联的流时,关联到的流的缓冲区会被刷新。例如,默认情况下,
cin
和cerr
都关联到cout
。因此,读cin
或写cerr
都会导致cout
的缓冲区被刷新。
关于第三点,共有三种操作符可用来刷新缓冲:
- endl: 换行并刷新缓冲区
- flush: 仅刷新缓冲区,但不输出任何额外字符
- ends: 向缓冲区插入一个空字符然后刷新缓冲区
关于第四点:
- unitbuf操纵符: 每次写操作之后都进行一次 flush 操作
- nounitbuf操纵符: 重置流,使其恢复默认的缓冲区刷新机制
值得一提的是,如果程序崩溃,输出缓冲区不会被刷新。
关于第五点,C++提供了 tie函数
来查看当前对象关联的输入输出流,tie
有两个重载版本:
- 无参数版本: 返回指向输出流的指针。当前对象若关联了一个输出流,则返回指向该流的指针;若未关联流,则返回空指针。
- 参数为一个指向 ostream 的指针: 将当前对象关联到此
ostream
。
每个流同时最多关联到一个流,但多个流可以同时关联到同一个 ostream
。
示例:
cin.tie(&cout); // 标准库默认将 cin 和 cout 关联在一起
cin.tie() = cin.tie(nullptr); // 通过传递空指针,让 cin 不再与其他流关联
文件流
fstream类型
除了继承自 iostream类型
的行为外,fstream类型
还增加了一些新的成员来管理与流关联的文件。
open函数
fstrm(s)
之所以能在调用时打开文件s,是因为自动调用了 open函数
,等价于:
ifstream in; // 输入文件流未与任何文件关联
in.open(ifile); // 打开指定文件,并与in绑定
对一个已经打开的文件流调用 open
会失败,此时 failbit
会被置位,随后使用文件流的操作都会失败。因此,调用 open
后检测是否成功是个好习惯:
if(in) // 成功
else // 不成功
如果想要将文件流关联到另一个文件,必须先关闭已关联的文件:
in.cloes();
in.open(ifile);
open
成功调用会将 good()
设为 true
。
close函数
当一个 fstream对象
被销毁时,close
会自动被调用。
文件模式
每个文件流类型都定义了一个默认的文件模式,当我们未指定文件模式时,就使用此默认模式。
- 与
ifstream关联
的文件默认以in模式
打开; - 与
ofstream关联
的文件默认以out模式
打开; - 与
fstream关联
的文件默认以in和out模式
打开。
虽然不论是调用 open
打开文件,还是 fstrm(s)
这样隐式打开文件,都可以指定文件模式,但指定文件模式有如下限制:
- 只可以对
ofstream
、fstream
对象设定out
模式。 - 只可以对
ifstream
、fstream
对象设定in
模式。 - 只有
out
也被设定时才可设定trunc
模式。 - 只要
trunc
没被设定,就可以设定app
模式。在app
模式下,即使没有显式指定out
模式,文件也总是以输出方式被打开。 - 默认情况下,以
out
模式打开的文件同时使用trunc
模式,即会被截断(内容被丢弃)。- 为了保留以
out
模式打开的文件的内容,我们必须同时指定app
模式,这样只会将数据追加写到文件末尾; - 或者同时指定
in
模式,即打开文件同时进行读写操作。
- 为了保留以
ate
和binary
模式可用于任何类型的文件流对象,且可以与其他任何文件模式组合使用。
关于第五点,举例详细讲一下:
/*截断*/
ofstream out1("file1"); // 隐含以out模式打开文件并截断文件
ofstream out2("file1", ofstream::out); // 隐含地截断文件
ofstream out3("file1", ofstream::out | ofstream::trunc);
// 显式实现out模式打开文件并截断/*app模式保留文件内容*/
ofstream app1("file2", ofstream:app); // 隐含out模式
ofstream app2("file2", ofstream:out | ofstream:app);
文件光标函数
tellg() / tellp()
该函数没有参数,返回 pos_type
类型的值,就是一个整数,代表当前 读取光标【tellg】 / 写入光标【tellp】 的位置距文件首的字节数。
seekg() / seekp()
g
表示get
,指示函数在输入流上工作。该函数的作用移动读操作光标。p
是put
缩写,指示函数在输出流上工作。seekp
用于移动写操作光标。
// 一个参数
basic_istream<Elem, Tr>& seekg( pos_type pos
);// 两个参数
basic_istream<Elem, Tr>& seekg( off_type off, ios_base::seekdir way
);// seekp 函数原型及参数信息同 seekg
pos
:移动读取指针的绝对位置。要求传入的参数类型与函数tellg
(见下文)的返回值类型相同。off
:偏移量,单位字节(B
)。正数表示向右偏移,负数表示向左偏移。way
:基地址,off
根据该地址进行偏移。有下面三个取值:
描述 | 模式标志 |
---|---|
文件首 | std::ios::beg |
文件尾 | std::ios::end |
当前光标位置 | std::ios::cur |
注意,如果目前已经在文件末尾,则在调用seekg
之前,必须清除文件末尾的标志:
fstream ioFile("文件路径");ioFile.get(ch); // 先将字符读入流
while (!ioFile.fail())
{cout.put(ch); // 再将流中内容输出到屏幕ioFile.get(ch); //
}
// 假设此时已经读取到文件流对象的末尾
/* 缺少调用 clear() */
文件流对象.seekg(0L, ios::beg); // 移动到文件开头
文件流对象.tellg(); // 返回-1,说明上一步并为真正移动到文件开头// 正确做法
文件流对象.clear();
文件流对象.seekg(0L, ios::beg); // 移动到文件开头
向文件存储内容/读取文件内容
// url 文件路径
void write(std::string &url){std::ofstream fwrite(url, std::ios::binary);if (!fwrite.is_open()) {std::cout << "open url fail" << std::endl;}/* 写入数据 */// 内置类型int i = 1024;fwrite.write((const char *) &i, sizeof(i));// 数组std::vector<std::string> stringVec = {"a", "b", "c"};int64_t vecTotalSize = sizeof(std::string) * stringVec.size();fwrite.write((const char *) stringVec.data(), vecTotalSize);// 写入数组数据时需要记录总数据的长度fwrite.write((const char *) &vecTotalSize, sizeof(vecTotalSize));// 自定义结构People people;people.name = "lihua";people.age = 21;fwrite.write((const char *) &people, sizeof(people));// 关闭流fwrite.close();
}
void read(std::string &url){std::ifstream fread;fread.open(url, std::ofstream::binary);if (!fread.is_open()) {std::cout << "open url fail" << std::endl;}/* 读取数据,顺序和写入顺序相反 */// 自定义结构People people;int peopleLen = sizeof(people);fread.seekg(-peopleLen, std::ios::end);fread.read((char *) &people, peopleLen);// 数组int64_t vecSize; // 先读取数组数据的长度const int vecSizeLen = sizeof(vecSize);// std::ios::cur 在 People 数据的结尾处// 读取 vecSize 需要将指针左移到 vecSize 数据的开头// 这就需要经过 peopleLen 和 vecSizeLen 两个长度fread.seekg(-peopleLen - vecSizeLen, std::ios::cur);fread.read((char *) &vecSize, vecSizeLen); //8 bytesint64_t stringVecSize = vecSize / sizeof(std::string); // 根据数组数据的长度算出数组的大小std::vector<std::string> stringVec;stringVec.resize(stringVecSize);fread.seekg(-vecSizeLen - vecSize, std::ios::cur);auto pos = fread.tellg();fread.read((char *) stringVec.data(), vecSize);pos = fread.tellg();// 内置类型int j;int len = sizeof(j);fread.seekg(-vecSize - len, std::ios::cur);fread.read((char*) &j, len);// 关闭流fread.close();
}
string流
同样的,除了继承自 iostream
的操作,sstream
也增加了独有的操作。
istringstream
我们经常会碰到处理整行字符串的问题,比如:比较版本号
用双指针截取字符串当然是一种方法,但是使用 istringstream
这个标准库提供的利器会更加方便。当然,两种方法的时间、空间复杂度是一样的。
下面通过分析 istringstream
的使用来进一步理解如何用:
class Solution {
public:int compareVersion(string version1, string version2) {istringstream in1(version1); // 将文本version1与输入流in1绑定istringstream in2(version2);int a, b;char c;while(in1.good() || in2.good()){in1 >> a; // 从in1中读取int数据到a中,遇到空白符or非int数据停下in2 >> b;if(a > b) return 1;if(a < b) return -1;a = b = 0;in1 >> c; // 从in1中读取char类型数据到c中,遇到空白符or非char数据停下in2 >> c;}return 0;}
};
再比如,有这样的输入,人名和他们的常用密码,一个人可能有多种常用密码:
cmy 12345 22345
lx 6644
lhy 6633 1221 5665
那么我们可以这样处理:
struct per_pw{string name;vector<string> pw;
}
string s, word; // s暂存来自输入的一行文本
vector<per_pw> people;
while(getline(cin, s)){ // 处理一行文本,也就是一个人的信息per_pw pp;istringstream in(s); // 将in绑定到刚读取的sin >> pp.name; // 读取名字while(in >> word) // 读取密码pp.pw.push_back(word); // 密码存入pp的pw数组中people.push_back(pp); // 将这个人的信息保存在people数组中
}
ostringstream
当我们希望将多个输出最后一起打印时,ostringstream
是很有用的。举个简单的例子:
ostringstream out; // 创建一个未绑定的输出流
vector<string> vs = {"cmy", "lx", "lhy"};
for (string s : vs) {out << s << " ";
}
cout << out.str() << endl;
// str():返回out保存的string的拷贝,也就是将out转换为string类型。
我们使用标准的输出运算符<<向 out
写入数据,有趣的是,这些写入操作实际上转换为 string
操作,向 out
中的 string
对象添加字符。