刚开始学习C时我们通常会这样输入和输出数据int value0; std::cinvalue; std::coutvalue;后来学习文件操作又会遇到类似的代码std::ifstream input(data.txt); inputvalue;这里的数据来源从键盘变成了文件但读取方式仍然是。这正是CIO流的作用通过一套相近的接口处理不同源和去向的数据。这篇文章从流的基本概念出发介绍标准输入输出、文件流和字符串流并整理使用过程中容易遇到的问题。一、什么是流可以把流理解为程序读写数据的一条通道。判断输入还是输出时要以程序为参照数据来源-输入-程序-输出-数据目标例如从键盘读取一个整数是输入。从文件读取一行文本也是输入。在屏幕上显示结果是输出。将结果保存到文件同样是输出。因此“输入”并不是指键盘输入“输出”也不只指屏幕输出。实际读写时数据还可能经过缓冲区。缓冲区可以暂存数据减少频繁进行底层I/O的开销。不过向流中输入数据不一定意味着数据已经立即显示在屏幕上或写到磁盘中。C标准库提供了不同的流类型操作对象输入输出输入与输出基本流接口istreamostreamiostream文件ifstreamofstreamfstream字符串istringstreamostringstreamstringstream先记住两个方向inputvalue;//从流中提取数据 outputvalue;//向流中插入数据在整数运算中和可以表示移位用于流对象时通过运算符重载它们具有了提取和插入数据的含义。二、标准输入输出不只是cin和cout包含iostream可以使用标准库提供的几个流对象对象用途std::cin标准输入std::cout标准输出std::cerr错误和诊断输出std::clog日志和诊断输出它们是已经创建号的对象不是函数。下面是一个完整的示例#includeiostream int main() { int number 0; std::cout 请输入一个整数; if (std::cin number) { std::cout 你输入的是 number \n; } else { std::cerr 输入的内容无法解析为整数\n; return 1; } return 0; }cin通常从键盘读取cout通常输出到终端但也可以通过重定向连接文件等其他目标。cerr和clog都适合输出诊断信息不过它们的默认刷新行为不同cerr默认启动unitbug输出操作完成后会触发刷新clog默认没有这一设置。使用clog也不会自动生成日志文件。另一个常见区别是std::cout\n; //换行 std::coutstd::endl; //换行并刷新流只需要换行时一般使用\n即可。三、为什么cin读不到带空格的完整句子不同读取方式有不同的规则。对于常见的格式化提取操作默认会跳过前导空白再按照目标类型读取内容。例如int a0,b0; std::cinab;输入10 20或者10 20都能读到两个整数。对这里的读取操作来说空格和换行都可以起分隔作用。但如果目标是std::stringstd::string text; std::cintext;输入hello Ctext只会得到hello后面的内容留待后续读取。原因是向字符串提取数据时遇到后续空白就结束当前字符串的读取。要读取整行应使用std::getline#includeiostream #includestring int main(){ std::string line; if(std::getline(std::cin,line)){ std::coutline\n; } }默认情况下getline保留行内空格。遇到换行时结束当前行。取走换行符但不把它存入字符串。也可能因为输入结束或错误而停止。它还支持指定分隔符例如std::getline(std::cin,line,,);此时逗号成为分隔符/混用和getline时要处理残留换行。假设先读取年龄再读取下一行的姓名std::cin age; std::getline(std::cin, name);输入年龄后留下的换行可能被getline立即读到使name成为空字符串。如果姓名应当从下一行开始可以先丢弃当前行剩余内存#includeiostream #includestring #includelimits int main() { int age 0; std::string name; if (!(std::cin age)) { std::cerr 年龄输入错误\n; return 1; } std::cin.ignore(std::numeric_limitsstd::streamsize::max(), \n); std::getline(std::cin, name); std::cout name , age 岁\n; }这里的ignore会丢弃当前行剩余的所以内容直到取走换行符并非只删除一个字符。四、读写操作可能失败理解流状态输入一个整数看似简单但用户可能输入字母文件可能读到末尾底层设备也可能发生错误。流对象使用状态位记录这些情况状态位含义goodbit没有设置错误状态位eofbit读取过程中输入末尾failbit格式转换或提取操作失败badbit流的底层读写过程发生故障这些状态可以同时存在。最常见的判断方式是直接检查读取表达式int value 0; while (std::cin value) { std::cout value * 2 \n; }这段代码表示每成功读取一个整数就处理一次。之所以成立是因为返回流自身的引用而流在条件判断中会检查是否存在failbait或badbit。不要把它简单理解成“判断有没有到文件末尾”。到达末尾和本次读取失败并不完全相同。例如最后一个生疏可能已被成功读出同时流记录了eofbit。因此读取文件应优先使用while (intput value) { //使用本次成功读到的数据 }而不是提取判断while (!input.eof()) { //此时还不知道下一次读取是否成功 }如果希望在格式错误后重新接受输入通常需要std::cin.clear(); std::cin.ignore(std::numeric_limitsstd::streamsize::max(), \n);//丢弃本行剩余的错误输入 }两者解决的是不同问题clear()重置状态ignore()消费输入内容。五、文件流把数据来源和目标换成文件文件操作需要包含fstream类型主要用途默认打开方式ifstream读取文件inofstream写入文件outfstream读取和写入in | out可以在常见对象时打开文件也可以之后调用open()。例如将两行文本保存到文件#includefstream #includeiostream int main() { std::ofstream output(notes.txt); if (!output) { std::cerr 文件打开失败\n; return 1; } output 学习CIO流\n; output 练习文件读写\n; output.close(); if (!output) { std::cerr 文件写入或关闭失败\n; return 1; } return 0; }文件流对象销毁时会自动关闭文件。这里显示调用close()是为了在程序结束前检查写入和关闭时否成功。读取文件时同样可以逐行处理#includefstream #includeiostream #includestring int main() { std::ifstream input(notes.txt); if (!input) { std::cerr 文件打开失败\n; return 1; } std::string line; while (std::getline(input, line)) { std::cout line \n; } return 0; }可以看到getline的第一个参数从cin换成了文件流其它用法基本不变。打开模式模式绝对如何访问文件。模式作用std::ios::in允许读取std::ios::out允许写入std::ios::app每次写入前定位到末尾std::ios::ate打开后立即定位到末尾std::ios::trunc打开时清空已有内容std::ios::binary使用二进制模式模式之间可以用 | 组合std::ofstream output(notes.txt, std::ios::out | std::ios::app);需要特别区分app每次写入都追加到文件末尾。ate仅在打开后定位末尾之后仍可重新定位。另外默认创建ofstream并打开已有文件会截断原内容想保留内容并追加应指定app。默认的fstream读写模式则通常要求文件已经存在。六、二进制和read、write是什么关系这两个概念容易被混淆打开模式决定访问时是否进行平台相关的文本转换。读写接口决定是否进行数据格式转换。std::ofstream output(number.bin, std::ios::binary); output 123;虽然使用了二进制模式但仍会将整数转换尾文本表示。二进制模式不会自动把123写成一个int的内存表示扩展名.bin也不会改变这一点。常见读写接口可以分为接口功能、格式化提取和插入get()、put()字符读写read()、write()按指定数量读写字符单位tellg()、seekg()获取、调整读取位置tellp()、seekp()获取、调整写入位置对普通char文件流来说read和write常用于字节数据处理。使用read时尤其要注意它不会自动补充字符串结束符\0。因此读取到字符数组后不能直接假定它时合法的C风格字符串。下面演示分块读取并按照实际读取量输出#includefstream #includeiostream int main() { std::ifstream input(notes.txt, std::ios::binary); if (!input) { std::cerr 文件打开失败\n; return 1; } char buffer[1024]; while (input.read(buffer, sizeof buffer) || input.gcount() 0) { std::cout.write(buffer, input.gcount()); } if (input.bad() || (input.fail() !input.eof())) { std::cerr 文件读取失败\n; return 1; } return 0; }最后一次读取可能不足1024字节。gcount()返回实际读取的数量因此这部分数据也能被处理。这里使用cout.write()无需依赖\0判断结束位置。七、字符串流在内存中完成格式化和解析文件流连接文件字符串流连接内存中的字符串。使用字符串流需要包含sstream类型用途ostringstream将数据格式化到字符串istringstream从字符串解析数据stringstream同时支持输入与输出例如拼接一条包含不同类型数据的信息#includeiostream #includesstream #includestring int main() { std::ostringstream output; output 姓名 小林 年龄 20 成绩 92.5; std::string message output.str(); std::cout message endl; return 0; }str()返回字符串流当前保存的完整字符内容。反过来也可以从字符串中解析数据std::istringstream input(20 92.5); int age 0; double score 0.0; if (input age score) { std::cout age score endl; }字符串流仍然遵循流的读取规则默认按格式解析跳过前导空白。向std::string提取时遇到后续空白会停止。格式不匹配扔会失败。成功提取几个字段并不意味着已经消费了字符串中的全部内容。例如从abc中提取整数就会失败。字符串流省去了许多手动管理数组的工作但不能省略结果检查。复用字符串时要分清内容和状态。std::stringstream stream; stream 123; int number 0; stream number; //准备下一次使用 stream.str();//清空底层字符串 stream.clear();//清除状态位 stream 456; stream number;可以用下面的表格记忆操作作用stream.str()获取完整字符串内容stream.str()清空字符串内容stream.clear()重置流状态streamvalue从当前位置解析数据读取到末尾可能设置eofbit提取失败可能设置failbit不能笼统地认为每次转换结束都会设置badbit。八、让自定义类型也能使用流标准库为常见类型提供了流操作重载。对于自己的类型也可以定义相应接口。例如一个二维坐标#includeiostream struct Point { int x 0; int y 0; }; std::ostream operator(std::ostream out, const Point point) { return out point.x point.y; } std::istream operator(std::istream in, Point point) { int x 0; int y 0; if (in x y) { point.x x; point.y y; } return in; } int main() { Point point; if (std::cin point) { std::cout 坐标 point \n; } }这里有三个设计要点输出使用const Point因为输出不需要修改对象。输入使用Point因为读取结果需要保存到对象中/返回流引用以支持连续操作并保留状态供调用者检查。输入时先读入局部变量两个值都读取成功后再更新对象可以避免只更新一半坐标。这些运算符采用非成员函数形式因为左操作数时流对象。如果需要访问类型的私有成员可以通过公共接口实现也可以将函数声明为友元。由于参数类型时istream和ostream这些重载也能用于文件流和字符串流std::ofstream output(point.txt); output point;至此控制台、文件、字符串和自定义类型就通过同一套流接口连接起来了。