【C++第二十九章】IO流

张开发
2026/4/11 22:29:36 15 分钟阅读

分享文章

【C++第二十九章】IO流
前言 IO流这一章表面上看知识点很多cin、cout、ifstream、ofstream、stringstream、文本读写、二进制读写、流插入、流提取、序列化、反序列化……如果把这些内容拆开去背很容易感觉像是在记一堆零散接口但如果抓住主线就会发现它们其实都围绕同一个核心问题展开程序里的数据究竟怎样从外部进入内存又怎样从内存输出到外部。从这个角度看IO流并不是单纯的“输入输出语法”而是一套更抽象的模型把数据看成有方向、连续流动的信息再通过统一的流类体系把键盘、屏幕、文件、字符串这些不同来源或去向用相似的操作方式组织起来。也正因为如此这一章真正重要的并不是死记某个成员函数而是理解什么叫流为什么C要把输入输出抽象成流类为什么自定义类型也能支持和为什么文本和二进制各有优缺点以及为什么stringstream能把“对象 - 字符串”这条链路连起来。一. 从C到C输入输出为什么会从函数走向流 在C语言里最常见的输入输出方式通常是scanf / printffgetc / fputcfread / fwritefprintf / fscanf它们的核心思路是通过一组函数完成输入输出行为再配合缓冲区提升效率。程序并不直接和设备一点一点交互而是先经过输入缓冲区和输出缓冲区再由代码去读取或写入。1.1 为什么缓冲区这么重要因为设备读写速度和程序执行速度并不一致。若每读一个字符、每写一个字节都立刻直接和设备打交道开销会非常大。有了缓冲区之后输入时设备先把数据放进输入缓冲区程序再从缓冲区读取输出时程序先把数据写进输出缓冲区再由系统统一刷到设备1.2C为什么还要再抽象一层因为单纯函数接口虽然够用但不够统一也不够适合自定义类型和泛型编程。C想做的是把输入输出过程抽象成“流”再用类体系去组织各种输入输出行为。这样之后不同来源和目标就能在统一风格下处理扩展性也更强。二. 什么是流为什么它强调“有序连续 有方向” “流”最本质的含义不是某个具体类而是一种抽象模型数据像水流一样从一端流向另一端。2.1 流模型里最关键的两个特征有序连续数据不是乱序跳跃出现的而是按顺序一个接一个流动。具有方向性要么是输入到程序内部要么是从程序内部输出到外部。2.2 为什么这个抽象很有价值因为一旦把键盘输入、屏幕输出、文件读写、字符串拼接都看成“流”那接口设计就可以统一很多读都是“从某个流里取数据”写都是“往某个流里送数据”于是cin、ifstream、istringstream这类对象虽然数据来源不同但都能共享“提取数据”的行为cout、ofstream、ostringstream虽然目标不同但也都能共享“插入数据”的行为。三.C流类体系为什么会出现输入流、输出流、文件流、字符串流 流既然是一种统一抽象那C就需要一组标准类去承载它。3.1 最常见的几类流标准输入输出流cincoutcerrclog文件流ifstream读文件ofstream写文件fstream既能读也能写字符串流istringstreamostringstreamstringstream3.2 为什么文件流名字这么直观ifstream input file streamofstream output file stream所以最直接的记法就是ifstream负责读ofstream负责写3.3 类体系为什么会带来继承设计因为这些流虽然来源不同但很多能力是共享的。标准库通过继承把“共同部分”提炼出来再把“特化能力”分到不同派生类中。3.4 为什么ostream相关实现里会出现虚继承流类体系存在公共基类抽象如果某些分支同时继承到同一个基础流能力就可能形成菱形结构。为避免公共基类重复出现需要使用虚继承。这也是流类设计里一个比较典型的面向对象细节。四. 为什么while(cin s)能成立流对象为什么能参与条件判断 这是IO流里一个非常常见、也非常值得讲清楚的点。string s;while(cins){coutsendl;}4.1 表面现象看起来像是cin s负责读取读取结果还能直接放进while条件里判断真假4.2 本质原因因为输入流对象支持一种“转成布尔语义”的能力。也就是说流对象会在读取成功、状态正常时表现为真若读取失败、到达文件末尾或状态异常就表现为假。4.3 这背后反映了什么设计思想这其实不是“语法魔法”而是把流状态也当成对象状态的一部分。于是读取行为结束后调用者可以非常自然地继续用对象状态控制流程。4.4 为什么这比老式返回值判断更统一因为数据提取和状态检查被连成了一条操作链。写代码时不必显式再多拆一层“先读再判错”表达会更紧凑也更符合流式处理的风格。 避坑指南while(cin s)不是因为返回了一个普通布尔值而是因为流对象本身具备参与条件判断的语义。五. 流插入和流提取为什么和能成为自定义类型的统一入口 ⚠️C里最经典的流操作符就是流插入流提取5.1 对内置类型来说它们很自然intx;cinx;coutxendl;5.2 更重要的是它们还能扩展到自定义类型这就是C流体系特别强的地方之一。只要为自定义类型重载这两个操作符就能让对象像内置类型一样参与输入输出。例如一个日期类ostreamoperator(ostreamout,constDated){outd._year d._month d._day;returnout;}istreamoperator(istreamin,Dated){ind._yeard._monthd._day;returnin;}5.3 为什么这是非常统一的扩展点因为一旦约定好往流里写对象用从流里读对象用那自定义类型就自动接入了整套流生态可以输出到控制台可以写入文件可以写入字符串流也可以从文件、字符串中读回5.4 这也是“对象 - 文本”转换的基础很多后续更高级的封装本质上都建立在这一步之上只要对象会流插入和流提取它就更容易和文本世界打通。六. 类型转换和流为什么无关类型也可能“连起来” 这部分内容表面是在讲类型转换实际上和流设计也很有关系因为流体系大量依赖了自定义类型和内置类型之间的转换能力。6.1 内置类型之间的转换最容易理解例如doubled1.1;intxd;这是普通数值类型转换。6.2 自定义类型为什么也能和其他类型建立关系因为类可以主动定义两类入口构造函数允许“其他类型 - 自定义类型”类型转换运算符允许“自定义类型 - 其他类型”6.3 一个典型例子classC{public:C(intx){}};这意味着int - C是可以成立的。classE{public:operatorint(){return0;}};这意味着E - int也可以成立。6.4 为什么这一块和流相关因为流对象也依赖类似机制去提供“状态可判断”“对象可读写”这些能力。本质上流设计和类型转换设计在这里是相通的通过运算符重载和转换接口把对象行为自然融进语言表达式里。七.iostream和cstdio为什么能混用又为什么会提到同步 ️很多人学C流时都会问一个问题既然有了cin/cout那printf/scanf还能不能一起用7.1 可以混用的原因因为标准库在设计上保留了与C标准输入输出库的兼容性所以二者可以同时存在。7.2 为什么会提到sync_with_stdio因为为了兼容默认情况下iostream往往会和stdio做同步。这样做的优点是两套输出体系更容易保持顺序一致代价则是会多一些同步开销。7.3 关闭同步意味着什么ios::sync_with_stdio(false);这通常意味着取消与stdio的同步提升iostream性能但混用printf/scanf和cin/cout时顺序表现更需要谨慎7.4 正确理解这一点它不是“一个一定要关一个一定不能关”的问题而是若你主要用iostream可考虑关闭同步提速若你频繁混用两套接口就需要更清楚自己在做什么八. 文件流文本读写和二进制读写为什么是两套完全不同的思路 8.1 文本读写的核心特点文本方式的最大优点是人能直接看懂调试方便可读性好跨平台兼容性通常更强但缺点是需要做格式化和解析写入和读取时要做字符串转换存储体积可能更大8.2 二进制读写的核心特点二进制方式的特点则相反读写速度快不需要做额外文本格式化“有多大写多大”很直接但缺点也非常明显人看不懂调试不直观和对象内部布局强相关一旦对象里含有指针、动态资源、容器风险会骤增8.3 为什么“容器中大多数存的是指针所以二进制需要慎重”因为许多对象内部并不是“值全都直接嵌在对象本体里”而是会通过指针指向额外堆空间。若你只是把对象当前这块内存原样写出去那么写到文件里的往往只是地址值而不是地址所指向的数据内容。九. 为什么复杂对象不能简单做原样二进制读写 这一块是IO流里非常容易踩坑、也非常值得重点吃透的内容。9.1 原样二进制写入适合什么对象它只适合内存布局稳定、没有外部资源依赖、没有指针成员、没有复杂管理语义的简单对象。9.2 为什么带指针或容器的对象会出问题假设对象里有string、容器或其他动态资源管理成员那么对象本体通常只保存指针长度容量一些控制信息真正的数据在堆区。9.3 同一进程中“读回来还能看到值”也不代表是正确的有时在同一进程里刚写完马上读表面上似乎还能读出内容这只是因为原堆区内存暂时还没被覆盖。可本质上你只是把地址值又读回来了而不是把字符串内容真正序列化进文件。9.4 为什么会出现浅拷贝和二次析构风险因为一旦读进来的对象和原对象都指向同一块内部资源最终析构时就可能重复释放同一块内存。9.5 为什么跨进程更明显会坏掉因为地址只在当前进程地址空间里有意义。换一个进程去读那些地址值基本等于野指针根本不可能继续正确访问。 避坑指南二进制文件里若只是写入了对象中的指针值那保存下来的不是“数据”而只是“当前进程里的地址幻觉”。十. 文本读写为什么虽然麻烦一点却更适合复杂对象 10.1 文本写入的核心做法例如ofswinfo._addressendl;ofswinfo._xendl;ofswinfo._dateendl;10.2 文本读取的核心做法ifsrinfo._address;ifsrinfo._x;ifsrinfo._date;10.3 它为什么更安全因为文本保存的是值语义结果不是对象内部某一时刻的内存布局。只要约定好输出格式和读取规则就能比较稳定地跨进程、跨运行阶段甚至跨平台使用。10.4 代价是什么需要自己定义格式需要自定义类型支持/读取时还要做解析但这些代价通常比“把地址当数据写出去”的风险小得多。十一.stringstream为什么它能成为对象和字符串之间的桥梁 ⚠️如果说文件流是在“对象 - 文件”之间搭桥那么字符串流就是在“对象 - 字符串”之间搭桥。11.1 它的本质把字符串当成一个可读可写的流来处理。11.2 为什么它很适合做拼接例如Dated(2024,3,10);ostringstream oss;ossd;string sqlselect * from t_score where name ;sqloss.str();sql;这里ostringstream就像一个中转缓冲区先把对象按流插入规则写进去再整体变成字符串。11.3 为什么它比手写字符串拼接更自然格式控制更统一可直接复用重载对自定义类型更友好适合逐步构造复杂文本11.4 它也能反过来解析字符串string strss.str();istringstreamiss(str);issrinfo._name;issrinfo._id;issrinfo._date;issrinfo._msg;于是一个字符串就又能被当成输入流解析回对象字段。十二. 序列化与反序列化为什么本质上就是“对象 - 可传输形式”的转换 12.1 什么是序列化把各种信息转换成字符串或其他可存储、可传输形式的过程。12.2 什么是反序列化把字符串或其他外部表示形式恢复成程序内部对象信息的过程。12.3 为什么stringstream特别适合教学场景下理解这两个概念因为它非常直观到字符串流像是在把对象“压平”成文本从字符串流读回像是在把文本“拆解”回字段12.4 为什么“字符串切割只能用于简单情况”因为一旦字段本身可能带空格、换行、特殊分隔符单纯靠空格切割、换行切割就很容易失效。所以简单用stringstream拆字段适合教学和简单场景但更复杂的序列化场景通常还需要更严格的协议格式。 避坑指南序列化不是“把对象内存原样写出去”而是“把对象信息转成可恢复的外部表示”。这也是为什么文本序列化和二进制内存拷贝根本不是一回事。十三. 这一章最该建立起来的整体框架 如果把整章内容压缩成一条主线可以这样理解输入输出本质上是数据在“外部世界 - 程序内存”之间流动C用“流”统一抽象了这种过程流强调有序连续、具有方向性标准输入输出流、文件流、字符串流只是不同数据源/目的地上的同一种模型和让内置类型与自定义类型都能自然接入流体系流对象还能携带状态因此while(cin s)这种写法才成立文本读写适合人类可读、跨进程稳定的值语义表达二进制读写适合简单对象的高效写入但对带指针、带动态资源的对象必须格外谨慎stringstream把“对象 - 字符串”的桥梁打通也自然引出了序列化与反序列化总结 IO流这一章最重要的不是背出几个类名或成员函数而是建立一个统一理解输入输出并不是零散的设备操作而是一种被抽象成“流”的数据传递模型。沿着这条主线再回头看整章内容很多知识点就会自然连起来cin/cout是标准设备上的流ifstream/ofstream是文件上的流stringstream是字符串上的流/是流体系对数据读写的统一操作入口自定义类型通过重载这些运算符进入整个流生态文本读写强调可读性和可恢复性二进制读写强调效率但必须尊重对象真实语义序列化和反序列化本质上就是让对象能够离开内存、再回到内存所以这一章最终可以压缩成一句话流的价值不只是“能输入输出”而是“把不同数据来源和目标统一进同一种抽象模型里再让对象以一致方式参与其中”。当这条认识真正建立起来之后后面继续看日志系统、配置文件读写、网络消息封装、对象序列化协议时就会自然落到同一套“数据如何从对象走向外部表示又如何从外部表示恢复成对象”的主线上。

更多文章