ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

【C++】《C++ 的 IO 流》---详解

【C++】《C++ 的 IO 流》---详解 学完 C 语言的 scanf / printf再来看 C 的 cin / cout你可能觉得不就是换了个写法吗。其实 C 把输入输出抽象成了流还提供了文件流、字符串流等一整套工具。本文从流是什么讲起一路讲到 OJ 输入技巧、文件读写、stringstream所有代码都在 g (C17) 上实际运行验证过照着抄就能跑。目录先回顾C 语言的输入与输出流到底是什么C IO 流类家族标准 IO 流cin / cout / cerr / clogOJ 刷题输入输出专题为什么 while(cin x) 能当循环条件文件 IO 流stringstream把字符串当成流来用全文总结1. 先回顾C 语言的输入与输出C 语言最常用的两个函数scanf()从键盘读取数据存入变量printf()把数据格式化后输出到屏幕重点掌握宽度控制和精度控制比如%5d、%.2f。不管是输入还是输出C 语言在程序和硬件之间都加了一层缓冲区键盘 ──► 输入缓冲区 ──► 程序 ──► 输出缓冲区 ──► 显示器缓冲区有两个好处用大白话讲屏蔽底层差异数据从哪来、到哪去键盘、文件、网卡……底层操作完全不同。缓冲区把这些脏活累活统一包起来同一套scanf/printf代码换个平台也能跑可移植性好攒一批再处理效率高直接操作硬件很慢缓冲区先把数据攒起来一次读/写一批。另外计算机本身没有行的概念有了缓冲区我们才能定义读到换行符就算一行这样的规则。C 的流继承了缓冲思想所以理解缓冲区对后面很重要。2. 流到底是什么流stream顾名思义就是流动——数据从一个地方流向另一个地方。它是对有序、连续、带方向的数据的一种抽象。数据从键盘流向内存变量叫输入数据从内存流向显示器/文件叫输出。可以把流想象成一根水管数据是水是把水从管里抽进变量是把变量里的水灌进管子。C 提供了一整套 I/O 标准类库里面的每个类都叫流类分别负责控制台、文件、字符串等不同场景。3. C IO 流类家族C 实现了一个庞大的流类库继承关系简化后是这样ios_base格式、状态等最基础功能 └── ios持有一个 streambufstreambuf 才是真正存数据的缓冲区 ├── istream输入流负责读cin 就是它的对象 ├── ostream输出流负责写cout/cerr/clog 是它的对象 └── iostream多重继承 istream ostream既能读又能写按用途分常用的有三大类头文件场景常用类iostream控制台输入输出cin/cout/cerr/clogfstream文件读写ifstream读文件、ofstream写文件、fstream读写sstream把字符串当流用istringstream、ostringstream、stringstream一句话总结istream管读ostream管写streambuf管缓冲区所有流都是同一套家族里的成员所以它们的用法惊人地一致——都是和。4. 标准 IO 流cin / cout / cerr / clogC 标准库内置了四个全局流对象程序一启动就能用对象类型作用是否缓冲cinistream标准输入默认从键盘读缓冲coutostream标准输出输出到显示器缓冲cerrostream标准错误输出错误信息不缓冲立刻显示clogostream标准错误输出日志信息缓冲小知识cerr和clog都输出到标准错误stderr区别是 cerr 不缓冲——程序崩之前错误信息也一定能刷到屏幕上clog 带缓冲适合量大的日志。使用它们要包含iostream并引入命名空间后面的代码统一using namespace std;。关于 cin 的 5 个注意点① cin 是缓冲流。键盘敲的内容先进缓冲区cin 直接从缓冲区取一次输入太多用不完的数据会留在缓冲区里给下次用。② 类型不匹配不会让程序崩溃但流会坏掉。比如变量是 int你却输入字母流会进入错误状态failbit之后所有输入语句全部跳过。需要手动用cin.clear()恢复状态、cin.ignore()清掉脏数据这属于进阶内容初学先有个印象。③ 默认以空格、Tab、换行作为分隔符。所以cin s读字符串时遇到空格就停读不了带空格的一整行读整行要用getline后面讲。④ cin/cout 能直接处理 int、double、char 等内置类型是因为标准库已经为这些类型重载好了和运算符。⑤ 自定义类型不能直接 cin/cout。想写cout 对象就得自己重载想写cin 对象就得自己重载。#include iostream #include string using namespace std; struct Student { int id; string name; }; // 重载 左边必须是 ostream所以不能写成 Student 的成员函数只能写在全局 ostream operator(ostream os, const Student s) { os s.id s.name; return os; // 返回流对象才能支持 cout a b 链式调用 } // 重载 参数是非 const 引用因为要往里写数据 istream operator(istream is, Student s) { is s.id s.name; return is; } int main() { Student st; cin st; // 可以直接输入整个对象 cout st \n; return 0; }运行示例补充一个暖心细节为什么cout 请输入后面不写endl提示语也能立刻显示因为cin 和 cout 默认绑在一起cin 读数据前会自动把 cout 缓冲区刷出去不用我们操心。5. OJ 刷题输入输出专题刷算法题时逻辑全对但输入输出格式写错导致 WA答案错误是最冤的。这一节把几种固定模板讲清楚。5.1 最重要的概念EOFOJ 会把测试数据一次性喂给程序数据读完就到了文件末尾即EOFEnd Of File。while (cin x)读到 EOF 时循环条件为假自动结束——不用我们自己写结束标记。在Windows / VS本地调试输入完数据后按Ctrl Z再回车手动模拟 EOF在Linux / Mac包括云端虚拟机按Ctrl D在线提交什么都不用做系统自动处理。另外输出格式必须和题目描述逐字符一致该空格的地方空格该换行的地方换行多一个少一个都可能 WA。5.2 模板一循环读单个数据int a; while (cin a) { // 处理 a }原理cin a自动跳过空格、Tab、换行只抓有效数据读到数据就继续循环读到 EOF 就停止。适用场景多组测试数据不告诉你有多少组每组一个数或一个不带空格的字符串。5.3 模板二每组读多个数据int a, b, c; while (cin a b c) { // 一次读一组的三个数 }数据之间用空格还是换行隔开都可以cin 会自动忽略空白。注意区分另一种题型题目第一行先给一个 n表示后面恰好有 n 组数据。这时不能用 EOF 循环要用 for 循环固定读 n 次int n; cin n; for (int i 0; i n; i) { int a, b; cin a b; // 处理 }两种写法千万别写混。比如题目第一行给组数2后面是两组数据你却用while (cin a b)读 EOF实测会把组数2也当成数据读成(2,10)、(20,30)最后一个数40直接丢失答案全部错位 WA本地交互式调试时还可能表现为程序一直等输入。5.4 模板三读取一整行极易踩坑cin 碰到空格就停。要保留空格、读取完整一行用getlinestring line; while (getline(cin, line)) { // line 里就是完整的一行不含行末的换行符本身 }经典大坑cin 和 getline 混用int n; cin n; // 输入 3 后按回车数字 3 被取走换行符 \n 还留在缓冲区 string s; getline(cin, s); // getline 一上来就碰到这个残留的 \n读到一个空字符串解决方法在 getline 前把残留的换行吃掉。int n; cin n; cin.ignore(); // 忽略 1 个字符正好是那个 \n // 更保险的写法一直忽略到换行符为止 // cin.ignore(1024, \n); string s; getline(cin, s); // 这次正常读到一整行5.5 模板怎么选输入特征选择一个个数字/单词空白分隔不需要保留空格while (cin x)必须拿到带空格的一整行while (getline(cin, line))第一行明确给了组数 nfor循环读 n 次5.6 输出的实战细节少用endl多用\n。endl每输出一次都强制刷新缓冲区数据量大时频繁刷盘会超时\n只换行不刷新快得多。题目要求每组答案占一行正常在每组末尾输出一个\n即可个别判题严格的 OJ 不喜欢末尾多余空行拿不准时保持每组恰好一个换行。题目要求两个数字之间用空格分隔时行尾不要多加空格这是高频扣分点。6. 为什么 while(cin x) 能当循环条件cin x调用的是operator它的返回值是istream 流对象本身所以才能cin a b链式调用。一个对象怎么能放进 if / while 的条件里呢因为 istream 提供了一个类型转换运算符explicit operator bool() const; // 把流对象转换成 bool流状态正常 → 转换为true读到 EOF 或类型不匹配流进入 fail 状态→ 转换为false。所以while (cin x)的完整含义是读一个数据然后检查流还好不好好就进循环不好就结束。下面用自定义的 Date 类完整演示#include iostream using namespace std; class Date { friend ostream operator(ostream out, const Date d); friend istream operator(istream in, Date d); public: Date(int year 1, int month 1, int day 1) : _year(year), _month(month), _day(day) {} private: int _year; int _month; int _day; }; istream operator(istream in, Date d) { in d._year d._month d._day; return in; } ostream operator(ostream out, const Date d) { out d._year d._month d._day; return out; } int main() { Date d; while (cin d) // 读到 EOF 自动结束OJ 多组输入首选写法 { cout d \n; } return 0; }7. 文件 IO 流文件按内容形式分为文本文件人能看懂的字符和二进制文件字节原样存储。C 用文件流操作文件固定四步定义文件流对象ifstream ifile;—— 只读ofstream ofile;—— 只写fstream iofile;—— 可读可写。open 打开文件把流对象和磁盘文件关联起来打开后一定要判断是否成功读写文本文件用/二进制文件用read()/write()成员函数close 关闭文件对象析构时也会自动关但建议显式调用代码更清晰。7.1 常用打开模式模式含义ios::in只读打开ios::out只写打开默认会清空截断原文件内容实测ofstream和fstream只要单独用ios::out都会清空想保留旧内容要用fstreamios::in | ios::outios::app追加写入数据加到文件末尾不覆盖旧内容ios::binary二进制方式打开多种模式可以用|组合例如ios::out | ios::binary。7.2 四个易错点默认是文本模式要读写二进制必须显式加ios::binaryofstream默认打开就清空文件想追加内容要用ios::appwhile (ifile x)读到文件末尾自动结束原理和while (cin x)完全一样文件流对象销毁时会自动 close但手动 close 是更好的习惯。7.3 入门 Demo文件不存在就创建存在就读数字#include fstream #include iostream using namespace std; int main() { ifstream ifile(test.txt); if (!ifile) // 打开失败文件不存在或没权限 { cout 文件不存在正在创建...\n; ofstream ofile(test.txt); if (ofile.is_open()) { ofile 10 20 30 40 50\n; ofile.close(); cout 文件已创建请重新运行程序读取数据。\n; } return 0; } int a; while (ifile a) { cout a ; } cout \n; ifile.close(); return 0; }7.4 进阶 Demo配置信息的二进制读写与文本读写#include iostream #include fstream #include string #include cstring using namespace std; class Date { public: int _year, _month, _day; Date() : _year(0), _month(0), _day(0) {} Date(int y, int m, int d) : _year(y), _month(m), _day(d) {} friend ostream operator(ostream out, const Date d); friend istream operator(istream in, Date d); }; ostream operator(ostream out, const Date d) { out d._year - d._month - d._day; return out; } istream operator(istream in, Date d) { // 文本格式固定为 2026-9-5用两个字符变量吃掉分隔符 - char ch1, ch2; in d._year ch1 d._month ch2 d._day; return in; } struct ServerInfo { char _address[32]; int _port; Date _date; ServerInfo() : _port(0) { memset(_address, 0, sizeof(_address)); } ServerInfo(const char* address, int port, const Date date) : _port(port), _date(date) { // strncpy 是跨平台的strcpy_s 是 VS 提供的安全版本g 默认不认识 strncpy(_address, address, sizeof(_address) - 1); } }; class ConfigManager { public: ConfigManager(const char* filename) : _filename(filename) {} void WriteBin(const ServerInfo info) { ofstream ofs(_filename, ios::out | ios::binary); if (!ofs.is_open()) { cerr 二进制文件打开失败写入\n; return; } ofs.write((const char*)info, sizeof(info)); } void ReadBin(ServerInfo info) { ifstream ifs(_filename, ios::in | ios::binary); if (!ifs.is_open()) { cerr 二进制文件打开失败读取\n; return; } ifs.read((char*)info, sizeof(info)); } void WriteText(const ServerInfo info) { ofstream ofs(_filename); if (!ofs.is_open()) { cerr 文本文件打开失败写入\n; return; } ofs info._address info._port info._date; } void ReadText(ServerInfo info) { ifstream ifs(_filename); if (!ifs.is_open()) { cerr 文本文件打开失败读取\n; return; } ifs info._address info._port info._date; } private: string _filename; }; int main() { ServerInfo winfo(192.168.0.1, 80, Date(2026, 9, 5)); ConfigManager cf_bin(test.bin); cf_bin.WriteBin(winfo); ServerInfo rbinfo; cf_bin.ReadBin(rbinfo); cout 二进制读回: rbinfo._address rbinfo._port rbinfo._date \n; ConfigManager cf_text(test.txt); cf_text.WriteText(winfo); ServerInfo rtinfo; cf_text.ReadText(rtinfo); cout 文本读回: rtinfo._address rtinfo._port rtinfo._date \n; return 0; }运行结果本机实测生成的 test.txt 用记事本可以直接看懂192.168.0.1 80 2026-9-5test.bin 则是一串乱码字节。7.5 关于这段代码的 5 个关键理解① 为什么 Date 要重载 和 编译器只认识内置类型。ofs info._date里 Date 是自定义类型你不告诉它年月日按什么格式输出它就不知道怎么办。② 为什么重载函数必须写成全局函数cout d等价于operator(cout, d)其左操作数是 ostream 对象而非 Date。由于成员函数的左操作数必须是所属类自身因此该重载只能写成全局函数若需访问私有成员可借助友元声明。③ 二进制读写的本质是内存原样拷贝ofs.write((const char*)info, sizeof(info)); // 把结构体所占内存一字节不差地写进文件 ifs.read((char*)info, sizeof(info)); // 读回来重新盖到同类型结构体的内存上④ 二进制读写有两个大坑结构体里不能有指针char*、std::string内部都有指针。存指针存的是当前进程的内存地址下次运行地址早就失效了有结构体对齐填充字节而且不同机器/编译器的对齐方式、大小端可能不同所以二进制文件跨平台有隐患。只适合全是值类型int、char 数组、嵌套值类型对象且在同一环境读写的场景。⑤ 二进制 vs 文本怎么选方式核心操作优点缺点二进制write/read 整块内存拷贝速度快、代码简单人看不懂含指针的结构体不能用跨平台有隐患文本/逐字段转换人能直接看懂、方便调试、跨平台好稍慢自定义类型要重载运算符这段代码最大的亮点是统一不管 int、char 数组还是 Date 对象全用同一套/自定义类型用起来和内置类型一样自然——这就是运算符重载的价值。8. stringstream把字符串当成流来用8.1 C 语言是怎么做类型转换的C 语言想把整数转成字符串常用两个函数#include cstdio int main() { int n 123456789; char s[32]; // 方法1itoa —— 不是标准 CWindows/VS 上可用VS 里叫 _itoa_sLinux 下没有 // _itoa_s(n, s, sizeof(s), 10); // 方法2snprintf —— 标准 C 函数跨平台推荐 snprintf(s, sizeof(s), %d, n); printf(%s\n, s); return 0; }函数优点缺点跨平台itoa/_itoa_s整数转字符串很直接不是标准 CLinux 下没有❌snprintf标准函数格式灵活要记%d、%f等格式符缓冲区要给够大小✅两个血泪教训缓冲区给小了会溢出。123456789是 9 个数字加结尾的\0共占 10 个字节——char buf[10]恰好够写少一个字节才会溢出。所以用 snprintf 时第二个参数一定要传sizeof(缓冲区)它超了会截断不会越界格式符必须和类型匹配。%f要配 double配 int 属于未定义行为程序可能打印 0、打印乱码甚至崩溃编译器也会给警告。C 给出的更省心的方案就是stringstream。8.2 stringstream 三兄弟包含头文件sstream标准库提供三个类类用途istringstream从字符串里读数据字符串 → 变量ostringstream往字符串里写数据变量 → 字符串stringstream既能读又能写用法和 cin/cout 一模一样往里写往外读。8.3 用途一数值和字符串互转#include sstream #include string #include iostream using namespace std; int main() { int a 12345678; string sa; stringstream ss; ss a; // 像写 cout 一样写进去 ss sa; // 像读 cin 一样读出来sa 就是 12345678 cout int 转 string: sa \n; // 复用同一个 ss清空两件套 ss.clear(); // ① 清状态标志 ss.str(); // ② 清底层内容 double d 12.34; ss d; cout double 转 string: ss.str() \n; // str() 直接取出整个字符串 return 0; }运行结果两种取出方式的区别ss 123; int n; ss n; // 方式① 提取内容像被 cin 读走一样被消费 ss 123; string s ss.str();// 方式②str() 直接拷贝一份底层字符串不影响 ss 的读取位置8.4 重点中的重点清空两件套复用同一个 stringstream 时必须分清它内部有两样独立的东西状态标志eofbit / failbit 等记录我是不是读完了、有没有出错底层字符串真正存放内容的地方。ss.clear(); // 只清状态标志不动内容 ss.str(); // 只清空内容不动状态标志为什么读过之后必须两件套看实测g C17stringstream ss; string s; ss 123; ss s; // 123 被读完流到达末尾状态被置为 eof ss.str(); // 只清内容不清状态 ss 456; // 写入直接失败标准要求写入前流必须是好的状态 ss.str(); // 结果是空字符串 不是 123456注意这是 C 标准规定的确定行为不是看编译器心情。 还要特别纠正一个常见误解状态是脏的情况下ss 456根本写不进去内容不会变成 123456追加成 123456 只可能发生在状态正常但没清内容时。记不住规则就按这个表来使用情况怎么清只用写从没用读状态不会脏ss.str()清内容即可用过读到过末尾必须clear()str()两件套拿不准 / 懒得记永远两件套多写一行不吃亏只用一次干脆每次新建一个stringstream ss;什么都不用清8.5 用途二字符串拼接#include sstream #include iostream using namespace std; int main() { stringstream ss; ss first string,; ss second string; cout 拼接结果: ss.str() \n; // 拼接结果: first string, second string return 0; }写入默认是追加不是覆盖想从头来过就str()。8.6 用途三结构体的序列化与反序列化序列化把结构体数据按一定格式拼成字符串方便存文件、发网络反序列化收到字符串后再还原回结构体。有了流和运算符重载这两件事和普通输入输出完全一样。#include iostream #include sstream #include string using namespace std; class Date { public: int _year, _month, _day; Date(int y 0, int m 0, int d 0) : _year(y), _month(m), _day(d) {} friend ostream operator(ostream out, const Date d); friend istream operator(istream in, Date d); }; ostream operator(ostream out, const Date d) { out d._year - d._month - d._day; return out; } istream operator(istream in, Date d) { char ch1, ch2; in d._year ch1 d._month ch2 d._day; return in; } struct ChatInfo { string _name; // 昵称 int _id; // ID Date _date; // 时间 string _msg; // 消息 }; int main() { ChatInfo winfo { hjq, 135246, Date(2026, 9, 5), 晚上一起看电影吧 }; // ① 序列化结构体 - 字符串 ostringstream oss; oss winfo._name winfo._id winfo._date winfo._msg; string str oss.str(); cout 序列化后: str \n; // ② 模拟通过网络发送 str对方收到后反序列化字符串 - 结构体 ChatInfo rInfo; istringstream iss(str); iss rInfo._name rInfo._id rInfo._date rInfo._msg; cout 还原姓名: rInfo._name ( rInfo._id )\n; cout 还原消息: rInfo._name rInfo._msg \n; return 0; }运行结果注意这个简单协议的局限字段之间用空格分隔所以消息内容里不能带空格否则晚上 看电影会被切成两个字段。真实项目JSON、Protobuf 等会用更完善的格式解决这个问题学习阶段先理解拼字符串—拆字符串的思想即可。顺便对比一下ChatInfo 里有 string 成员所以它不能用write((char*)info, sizeof(info))做二进制序列化string 内部有指针只能像这样转成文本。8.7 stringstream 的底层原理一句话stringstream 内部维护了一个 string 对象。写入往这个 string 后面追加内容读取在这个 string 里维护一个读取位置从头按类型提取str()返回这个 string 的拷贝string 会自动扩容所以不用像 C 语言那样担心 char 数组缓冲区不够。对比 C 风格写法char buf[6]; int ret snprintf(buf, sizeof(buf), %d, 123456789); // 缓冲区不够snprintf 只会截断成 12345绝不越界换成 sprintf 就溢出了 // 返回值 9 表示本该写 9 个字符ret 缓冲区大小就说明被截断了 // 另外还得自己记住 %d、%f 这些格式符 stringstream ss; ss 123456789; // 底层 string 自动扩容多大都不怕 string s ss.str(); // 自动识别类型没有格式符不匹配的问题8.8 使用建议场景一一次性转换直接新建用完销毁string IntToStr(int n) { stringstream ss; // 局部对象不用操心清空 ss n; return ss.str(); }场景二频繁转换、复用同一个对象老老实实用两件套stringstream ss; ss 123; string s1 ss.str(); ss.clear(); ss.str(); ss 456; string s2 ss.str();补充单纯做数字 → stringC11 以后还可以直接用to_string()string → 数字用stoi/stod。它们更轻量stringstream 的优势在于一次处理多个字段、任意类型混搭比如 8.6 的结构体序列化。9. 全文总结流是对有序、连续、带方向数据的抽象负责输入、负责输出控制台流、文件流、字符串流用法完全一致cin/cout处理控制台输入输出带缓冲cerr不缓冲用于报错clog缓冲用于日志自定义类型想直接用 cin/cout必须重载和且要写成全局函数左操作数是流对象OJ 刷题记住三套模板单个/多个数据用while(cin x)整行用getline给了组数用forcin 和 getline 混用时记得ignore()吃掉残留换行输出少用endl多用\nwhile(cin x)能当条件靠的是 istream 的operator bool流正常为 true读到 EOF / 出错为 false文件流四步定义对象 → open判断成功→ 用 或read/write读写 → closeofstream 默认清空文件二进制要加ios::binary二进制读写是内存原样拷贝快但结构体里不能有指针、跨平台有隐患文本读写人能看懂、好调试stringstream底层就是一个自动扩容的 string能做类型转换、字符串拼接、序列化复用时记住clear()清状态 str()清内容两件套只用一次就直接新建对象。一句话收尾流的本质就是让你用操作键盘/屏幕的同一套方式、去操作文件和字符串——接口统一是 C IO 流最大的设计思想。
返回列表