ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

C++IO流:从控制台输入输出到文件与字符串处理

C++IO流:从控制台输入输出到文件与字符串处理

刚开始学习C++时,我们通常会这样输入和输出数据:

int value=0; std::cin>>value; std::cout<<value;

后来学习文件操作,又会遇到类似的代码:

std::ifstream input("data.txt"); input>>value;

这里的数据来源从键盘变成了文件,但读取方式仍然是>>。这正是C++IO流的作用:通过一套相近的接口,处理不同源和去向的数据。

这篇文章从流的基本概念出发,介绍标准输入输出、文件流和字符串流,并整理使用过程中容易遇到的问题。

一、什么是流?

可以把流理解为程序读写数据的一条通道。判断输入还是输出时,要以程序为参照:

数据来源->输入->程序->输出->数据目标

例如:

  • 从键盘读取一个整数,是输入。
  • 从文件读取一行文本,也是输入。
  • 在屏幕上显示结果,是输出。
  • 将结果保存到文件,同样是输出。

因此,“输入”并不是指键盘输入,“输出”也不只指屏幕输出。

实际读写时,数据还可能经过缓冲区。缓冲区可以暂存数据,减少频繁进行底层I/O的开销。不过,向流中输入数据,不一定意味着数据已经立即显示在屏幕上或写到磁盘中。

C++标准库提供了不同的流类型:

操作对象输入输出输入与输出
基本流接口istreamostreamiostream
文件ifstreamofstreamfstream
字符串istringstreamostringstreamstringstream

先记住两个方向:

input>>value;//从流中提取数据 output<<value;//向流中插入数据

在整数运算中,>>和<<可以表示移位;用于流对象时,通过运算符重载,它们具有了提取和插入数据的含义。

二、标准输入输出:不只是cin和cout

包含<iostream>,可以使用标准库提供的几个流对象:

对象用途
std::cin标准输入
std::cout标准输出
std::cerr错误和诊断输出
std::clog日志和诊断输出

它们是已经创建号的对象,不是函数。

下面是一个完整的示例:

#include<iostream> 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::cout<<std::endl; //换行,并刷新流

只需要换行时,一般使用'\n'即可。

三、为什么cin>>读不到带空格的完整句子?

不同读取方式有不同的规则。

对于常见的格式化提取操作,>>默认会跳过前导空白,再按照目标类型读取内容。例如:

int a=0,b=0; std::cin>>a>>b;

输入:

10 20

或者:

10 20

都能读到两个整数。对这里的读取操作来说,空格和换行都可以起分隔作用。

但如果目标是std::string:

std::string text; std::cin>>text;

输入:

hello C++

text只会得到"hello",后面的内容留待后续读取。

原因是:>>向字符串提取数据时,遇到后续空白就结束当前字符串的读取。

要读取整行,应使用std::getline:

#include<iostream> #include<string> int main(){ std::string line; if(std::getline(std::cin,line)){ std::cout<<line<<'\n'; } }

默认情况下,getline:

  • 保留行内空格。
  • 遇到换行时结束当前行。
  • 取走换行符,但不把它存入字符串。
  • 也可能因为输入结束或错误而停止。

它还支持指定分隔符,例如:

std::getline(std::cin,line,',');

此时逗号成为分隔符/

混用>>和getline时,要处理残留换行。

假设先读取年龄,再读取下一行的姓名:

std::cin >> age; std::getline(std::cin, name);

输入年龄后留下的换行,可能被getline立即读到,使name成为空字符串。

如果姓名应当从下一行开始,可以先丢弃当前行剩余内存:

#include<iostream> #include<string> #include<limits> int main() { int age = 0; std::string name; if (!(std::cin >> age)) { std::cerr << "年龄输入错误\n"; return 1; } std::cin.ignore(std::numeric_limits<std::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_limits<std::streamsize>::max(), '\n');//丢弃本行剩余的错误输入 }

两者解决的是不同问题:clear()重置状态,ignore()消费输入内容。

五、文件流:把数据来源和目标换成文件

文件操作需要包含<fstream>:

类型主要用途默认打开方式
ifstream读取文件in
ofstream写入文件out
fstream读取和写入in | out

可以在常见对象时打开文件,也可以之后调用open()。

例如,将两行文本保存到文件:

#include<fstream> #include<iostream> int main() { std::ofstream output("notes.txt"); if (!output) { std::cerr << "文件打开失败\n"; return 1; } output << "学习C++IO流\n"; output << "练习文件读写\n"; output.close(); if (!output) { std::cerr << "文件写入或关闭失败\n"; return 1; } return 0; }

文件流对象销毁时会自动关闭文件。这里显示调用close(),是为了在程序结束前检查写入和关闭时否成功。

读取文件时,同样可以逐行处理:

#include<fstream> #include<iostream> #include<string> 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风格字符串。

下面演示分块读取,并按照实际读取量输出:

#include<fstream> #include<iostream> 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同时支持输入与输出

例如,拼接一条包含不同类型数据的信息:

#include<iostream> #include<sstream> #include<string> 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()重置流状态
stream>>value从当前位置解析数据

读取到末尾可能设置eofbit,提取失败可能设置failbit,不能笼统地认为每次转换结束都会设置badbit。

八、让自定义类型也能使用流

标准库为常见类型提供了流操作重载。对于自己的类型,也可以定义相应接口。

例如,一个二维坐标:

#include<iostream> 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'; } }

这里有三个设计要点:

  1. 输出使用const Point&,因为输出不需要修改对象。
  2. 输入使用Point&,因为读取结果需要保存到对象中/
  3. 返回流引用,以支持连续操作,并保留状态供调用者检查。

输入时先读入局部变量,两个值都读取成功后再更新对象,可以避免只更新一半坐标。

这些运算符采用非成员函数形式,因为左操作数时流对象。如果需要访问类型的私有成员,可以通过公共接口实现,也可以将函数声明为友元。

由于参数类型时istream&和ostream&,这些重载也能用于文件流和字符串流:

std::ofstream output("point.txt"); output << point;

至此,控制台、文件、字符串和自定义类型就通过同一套流接口连接起来了。

返回列表