ARTICLE DETAIL

资讯详情

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

C++ IO流详解之标准IO、文件IO与字符串IO实战

C++ IO流详解之标准IO、文件IO与字符串IO实战 前言C 的输入输出是一套基于流stream的抽象。相比 C 的printf/scanf它的优势是类型安全和可扩展性 —— 你可以为自己的类型定义operator让它像内置类型一样被输出。但代价是流的内部状态是一台小状态机很多人不会管理它于是踩出经典的一串 bugcin n; getline(cin, s);为什么s是空的为什么读文件循环多做了一次为什么输入一个字母后后面所有cin 全失败为什么读二进制文件总是差几个字节答案都指向同一处流状态位stream state flags。本文按标准 IO → 文件 IO → 字符串 IO的顺序讲清楚。一、流体系总览所有流都继承自同一套模板基类ios_base ← 格式标志、精度、区域设置 └── basic_iosCharT ← 状态位、缓冲区、绑定流 ├── basic_istream ← 输入 │ ├── basic_ifstream (文件输入) │ └── basic_istringstream (字符串输入) └── basic_ostream ← 输出 ├── basic_ofstream (文件输出) └── basic_ostringstream (字符串输出)头文件提供用途iostreamcin/cout/cerr/clog标准 IOfstreamifstream/ofstream/fstream文件 IOsstreamistringstream/ostringstream/stringstream字符串 IOiomanipsetw/setprecision/setfill/hex格式化关键洞察三者用的是同一套操作符和方法。你在cin上学到的、getline、状态位在ifstream和istringstream上完全一样。这就是设计上的复用。二、流状态位一切坑的根源2.1 四个状态位basic_ios里有四个位iostate状态位含义触发条件goodbit一切正常值为 0默认eofbit到达输入末尾读到文件末尾failbit操作失败可恢复格式不匹配如把abc读进intbadbit流已损坏不可恢复磁盘错误、缓冲区失效重要细节eofbit置位时failbit也会同时置位读取没能成功取到数据一旦failbit或badbit置位后续所有读写都被直接忽略直到调用clear()good()等价于rdstate() 0#include iostream int main() { int n; std::cout 请输入一个整数: ; std::cin n; // 输入 abc std::cout std::boolalpha; std::cout good() std::cin.good() \n; // false std::cout fail() std::cin.fail() \n; // true std::cout eof() std::cin.eof() \n; // false int m; std::cin m; // 流已卡死直接失败 std::cout 第二次失败: std::cin.fail() \n; // true }状态转换--------- 读到末尾 --------- 继续读 --------- | goodbit | --------- | eofbit | -------- | failbit | --------- --------- --------- | ^ | 格式不匹配 / 打开失败 | --------------------------------------------2.2 正确的输入校验模式❌ 错误写法int n; std::cin n; // 用户输入 abcn 被置 0静默错误 std::cout n;✅ 正确写法把读取表达式放进条件里返回流对象流能隐式转换为bool等价于!fail()。#include iostream #include limits int main() { int n; while (true) { std::cout 请输入整数: ; if (std::cin n) break; // 读取成功 std::cout 输入无效请重试\n; std::cin.clear(); // 1. 清状态位 std::cin.ignore(std::numeric_limitsstd::streamsize::max(), \n); } // 2. 丢残渣 std::cout 你输入了 n \n; }这两步是固定搭配。只clear()不ignore()那个导致失败的abc还留在缓冲区里下一轮又失败形成死循环。三、标准 IO 实战3.1 格式化输出#include iostream #include iomanip int main() { double pi 3.14159265358979; std::cout std::setprecision(3) pi \n; // 3.14 std::cout std::fixed std::setprecision(2) pi \n; // 3.14 std::cout std::scientific pi \n; // 3.14e00 std::cout std::setfill(0) std::setw(8) 42 \n; // 00000042 std::cout std::setw(8) 43 \n; // 00000043 std::cout std::hex 255 \n; // ff std::cout std::showbase std::hex 255 \n; // 0xff std::cout std::dec std::boolalpha true \n; // true }核心记忆点setw是一次性的只影响紧接着的一次输出而setprecision、setfill、hex、fixed是持久的一直保持到下次被改。3.2 读取整行与分割#include iostream #include sstream #include string int main() { std::string line; std::getline(std::cin, line); // 读整行不含换行符 std::istringstream iss(line); int x, sum 0; while (iss x) sum x; std::cout 总和 sum \n; }这是整行读取 流解析的标准组合拳getline一次拿到一行避免残留换行符问题istringstream按空白切分。处理配置文件和 CSV 时极其常用。3.3cout/cerr/clog流缓冲用途cout有缓冲区可重定向正常输出cerr无缓冲unitbuf错误信息立即可见clog有缓冲日志调试时如果程序崩溃、cout最后几行没打出来那是缓冲区没刷—— 换成cerr就能看到。这是排查崩溃位置的经典技巧。四、文件 IO 实战4.1 打开、读写、关闭#include fstream #include string #include iostream int main() { { // ---------- 写 ---------- std::ofstream ofs(data.txt); // 默认 trunc截断 if (!ofs) { std::cerr 无法打开文件写\n; return 1; } ofs Tom 18\n Jerry 20\n; } // 析构自动关闭 { // ---------- 读 ---------- std::ifstream ifs(data.txt); if (!ifs) { std::cerr 无法打开文件读\n; return 1; } std::string name; int age; while (ifs name age) // ✅ 把读取写进条件 std::cout name 今年 age 岁\n; } }while (ifs name age)为什么对返回流引用流在fail()时转换为false。读到文件尾时失败并置位循环自动结束不会多执行一次循环体。4.2 打开模式模式含义ios::in读ifstream默认ios::out写ofstream默认隐含truncios::app追加每次写都到末尾ios::ate打开后定位到末尾但可任意写ios::trunc打开时清空文件ios::binary二进制模式Windows 上不做\n↔\r\n转换std::ofstream ofs(log.txt, std::ios::app); // 追加日志 std::ofstream bin(data.bin, std::ios::binary); // 二进制写 std::fstream rw(data.txt, std::ios::in | std::ios::out); // 读写app与ate的区别app每次写之前强制定位到末尾ate只在打开时定位到末尾之后可以seekp到任意位置。4.3 二进制读写#include fstream #include vector #include iostream struct Record { int id; char name[16]; double score; }; int main() { std::vectorRecord recs {{1, Alice, 95.5}, {2, Bob, 88.0}}; { // 写字节数 元素数 * 元素大小 std::ofstream ofs(recs.bin, std::ios::binary); ofs.write(reinterpret_castconst char*(recs.data()), static_caststd::streamsize(recs.size() * sizeof(Record))); } { // 读 std::ifstream ifs(recs.bin, std::ios::binary | std::ios::ate); std::streamsize bytes ifs.tellg(); // 文件总字节数 ifs.seekg(0); // 回到开头 std::vectorRecord in(static_castsize_t(bytes) / sizeof(Record)); ifs.read(reinterpret_castchar*(in.data()), bytes); for (const auto r : in) std::cout r.id r.name r.score \n; } }注意reinterpret_castchar*—— 二进制 IO 把对象当字节序列处理。这也意味着这种写法不可移植结构体对齐padding、字节序endianness在不同平台不同。4.4 定位seekg/tellg#include fstream #include iostream int main() { std::ifstream ifs(data.txt); if (!ifs) return 1; ifs.seekg(0, std::ios::end); // 跳到末尾 std::streamsize size ifs.tellg(); // 取位置 文件大小 ifs.seekg(0, std::ios::beg); // 回到开头 std::cout 文件大小 size 字节\n; ifs.seekg(5, std::ios::beg); // 跳过前 5 字节 std::string rest; std::getline(ifs, rest); std::cout rest \n; }定位基准说明ios::beg从开头算ios::cur从当前位置算ios::end从末尾算seekg用于输入流g getseekp用于输出流p put。五、字符串 IO 实战5.1ostringstream拼接字符串❌ 低效写法std::string s id std::to_string(id) , name name , score std::to_string(score); // 多次临时 string 构造✅ 更清晰的写法#include sstream #include iomanip #include iostream int main() { int id 7; std::string name Tom; double score 92.5; std::ostringstream oss; oss id id , name name , score std::fixed std::setprecision(1) score; std::string s oss.str(); std::cout s \n; // id7, nameTom, score92.5 }好处一次分配且能直接用的所有重载包括setprecision这类格式化器。5.2istringstream解析字符串#include sstream #include string #include iostream int main() { // 用自定义分隔符切分 std::istringstream iss(1,Tom,95.5); std::string field; while (std::getline(iss, field, ,)) std::cout [ field ]; std::cout \n; // [1][Tom][95.5] // 格式化解析 std::istringstream iss2(2026 09 28); int y, m, d; if (iss2 y m d) std::cout y - m - d \n; }std::getline(iss, field, delim)的第三个参数是自定义分隔符这是解析 CSV / 日志的利器。5.3 类型转换#include sstream #include string #include iostream template class T bool from_string(const std::string s, T out) { std::istringstream iss(s); // 吃掉尾部空白后必须恰好到末尾才能拒绝 12abc return static_castbool(iss out) (iss std::ws).eof(); } int main() { int n; if (from_string(123, n)) std::cout 解析成功: n \n; if (!from_string(12abc, n)) std::cout 解析失败有残留字符\n; }C17 起有更简单、无分配、不受 locale 影响的std::from_chars#include charconv int n; auto [ptr, ec] std::from_chars(s.data(), s.data() s.size(), n);六、完整实战一个可运行的读文件统计器#include fstream #include sstream #include string #include iostream // 去掉首尾空白 static std::string trim(const std::string s) { const auto b s.find_first_not_of( \t\r\n); if (b std::string::npos) return ; const auto e s.find_last_not_of( \t\r\n); return s.substr(b, e - b 1); } int main() { { // 准备一个数据文件 std::ofstream ofs(words.txt); ofs apple 3\nbanana 5\ncherry 7\n; } std::ifstream ifs(words.txt); if (!ifs) { std::cerr 打开失败\n; return 1; } std::string line; int lineno 0, total 0; while (std::getline(ifs, line)) { // 整行读 lineno; line trim(line); if (line.empty() || line[0] #) continue; // 跳过空行和注释 std::istringstream ls(line); std::string word; int n; if (ls word n) total n; else std::cerr 第 lineno 行格式错误: line \n; } std::cout 共 lineno 行合计 total \n; // 15 }g -stdc17 -Wall -Wextra stat.cpp -o stat ./stat常见坑点坑 1cin 与getline混用getline读到空串❌ 错误写法int age; std::string name; std::cin age; // 用户输入 18\n\n 留在缓冲区 std::getline(std::cin, name); // 立刻读到那个 \nname 为空✅ 正确写法二选一// 方案 A读完后丢掉换行符 std::cin age; std::cin.ignore(std::numeric_limitsstd::streamsize::max(), \n); std::getline(std::cin, name); // 方案 B全程用 getline再解析更稳 std::string line; std::getline(std::cin, line); age std::stoi(line); std::getline(std::cin, name);方案 B 更稳因为它不依赖缓冲区里恰好剩一个换行符这个假设。坑 2while (!ifs.eof())多读一次❌ 错误写法while (!ifs.eof()) { ifs name age; std::cout name age \n; // 最后一次会重复输出 }原因eof()是在读取失败之后才置位的。读到最后一行后eof()仍是false循环再执行一次这次读取失败name/age保持上次的值被重复输出。✅ 正确写法while (ifs name age) { // 读取失败时流为 false std::cout name age \n; }黄金法则不要在循环条件里检查eof()要把读取本身作为条件。坑 3failbit粘住后续读取全部失败int n; std::cin n; // 输入 abc → failbit 置位 std::cin n; // 直接失败流已卡死 std::cin n; // 还是失败✅ 恢复需要两步std::cin.clear(); // 1. 清状态位 std::cin.ignore(std::numeric_limitsstd::streamsize::max(), \n); // 2. 丢残渣坑 4stringstream复用前忘记clear()❌ 错误写法std::stringstream ss; ss 123; int a; ss a; // 读到 eofeofbit 置位 ss 456; ss a; // ❌ 失败状态位还留着✅ 正确写法ss.str(); // 清内容 ss.clear(); // 清状态位 —— 两步不能少 ss 456; ss a;str()和clear()是两个不同操作前者清内容后者清状态。漏掉任何一个都会出问题。坑 5printf与cout混用导致输出顺序错乱❌ 错误写法std::cout A; printf(B); // C 的 stdout 缓冲和 C 的 cout 缓冲独立 std::cout C; // 可能输出 BAC 或 ACB✅ 正确写法统一用一种必须混用时手动刷std::cout A std::flush; printf(B); fflush(stdout);默认情况下cin/cout与stdin/stdout是同步的混用安全但有性能损失。一旦调用std::ios::sync_with_stdio(false)提速就绝不能再混用printf。坑 6读二进制文件被 Windows 换行转换破坏❌std::ifstream ifs(image.png);—— 文本模式会把0x0D0A转成一个0x0A。✅std::ifstream ifs(image.png, std::ios::binary);Windows 文本模式还会把0x1A当 EOF。只要不是纯文本一律加ios::binary。坑 7write/read用错长度ofs.write(reinterpret_castconst char*(v.data()), v.size() * sizeof(int)); // ✅ 字节数 元素数 * 元素大小 ofs.write(reinterpret_castconst char*(v.data()), v.size()); // ❌ 只写了 size() 字节坑 8ofstream忘记检查是否打开成功std::ofstream ofs(/不存在的目录/x.txt); ofs hello; // 静默失败什么都没写✅ 正确写法if (!ofs) { std::cerr 打开失败\n; return 1; }流对象不会因为失败而抛异常除非设了ofs.exceptions()它只会安静地置位failbit。坑 9std::endl在循环里造成性能灾难for (int i 0; i 1000000; i) ofs i std::endl; // ❌ 每次 flush慢十倍以上 for (int i 0; i 1000000; i) ofs i \n; // ✅ 只在缓冲区满时真正写盘std::endl等价于\nflush()。总结内容关键结论状态位eof会连带failfail/bad置位后所有操作被忽略恢复clear()清标志 ignore()清缓冲两步缺一不可输入循环把读取写进条件while (cin x)不要判eof()与getline混用会残留换行符用ignore或全程getline文件模式二进制必加ios::binary追加用ios::app字符串 IOistringstreamgetline(iss, f, ,)是解析利器stringstream复用str()和clear()都要调格式化setw一次性setprecision/hex持久性能用\n不用std::endl不要混用printf一句话收束C 流编程的核心不是记住操作符而是管理好状态位和缓冲区。把读取写进条件失败后clearignore文本用文本模式、字节用二进制模式这三条养成习惯绝大多数 IO bug 都不会再出现。
返回列表