C++ IO流详解:标准IO、文件IO与字符串IO实战(2025 最新版,C++20/23 视角)
C++ 的 IO 流是面向对象 + 模板 + 运算符重载的经典设计,所有流都继承自 std::ios_base → std::basic_ios → std::basic_istream / std::basic_ostream。
std::istream ← std::cin / ifstream / istringstream
std::ostream ← std::cout / ofstream / ostringstream
std::iostream ← fstream / stringstream
三大场景(标准 / 文件 / 字符串)底层完全复用同一套机制,掌握一个就能通吃三个。
一、标准IO()—— 最常用
1. 核心对象
| 对象 | 类型 | 用途 | 缓冲? | 刷新时机 |
|---|---|---|---|---|
cin | istream | 标准输入 | 是 | 遇到 \n 或手动 |
cout | ostream | 标准输出 | 是 | 满或 std::endl |
cerr | ostream | 标准错误(无缓冲) | 否 | 立即输出 |
clog | ostream | 标准日志(有缓冲) | 是 | 满或手动 |
2. 格式化输出(最实用技巧)
#include <iostream>
#include <iomanip> // setw / setprecision / fixed 等
int main() {
double pi = 3.1415926535;
std::cout << std::fixed << std::setprecision(4) << pi << '\n'; // 3.1416
std::cout << std::setw(10) << std::left << "Hello" << "|\n"; // Hello |
std::cout << std::boolalpha << true << '\n'; // true
}
C++20 推荐:用 std::format(比 << 更清晰):
#include <format>
std::cout << std::format("π = {:.4f}, 姓名: {:<10}\n", pi, "张三");
3. 输入实战(避免最常见 bug)
std::string name;
int age;
std::cin >> name >> age; // 遇到空格就结束
std::cin.ignore(1000, '\n'); // 清空缓冲区!!!
std::getline(std::cin, name); // 读整行(推荐)
混合输入神器:
while (std::getline(std::cin, line)) {
std::istringstream iss(line); // 后面会讲
int a, b;
if (iss >> a >> b) { ... }
}
二、文件IO()—— 生产级必备
1. 打开模式(ios:: 组合)
| 模式组合 | 含义 | 常用场景 |
|---|---|---|
ios::in | 只读 | 读取配置文件 |
ios::out | 只写(会清空) | 新建日志 |
ios::app | 追加写入 | 日志文件 |
ios::binary | 二进制模式 | 图片/视频/对象 |
ios::trunc | 清空(out 默认) | 重写文件 |
ios::ate | 打开后定位到末尾 | 追加前先定位 |
std::ofstream ofs("log.txt", std::ios::out | std::ios::app);
if (!ofs) { /* 打开失败 */ }
2. 文本文件读写完整示例(推荐现代写法)
#include <fstream>
#include <vector>
#include <string>
bool write_csv(const std::string& filename, const std::vector<std::vector<std::string>>& data) {
std::ofstream ofs(filename, std::ios::out | std::ios::trunc);
if (!ofs.is_open()) return false;
for (const auto& row : data) {
for (size_t i = 0; i < row.size(); ++i) {
ofs << row[i];
if (i + 1 < row.size()) ofs << ',';
}
ofs << '\n';
}
return true;
}
std::vector<std::string> read_lines(const std::string& filename) {
std::ifstream ifs(filename);
if (!ifs.is_open()) return {};
std::vector<std::string> lines;
std::string line;
while (std::getline(ifs, line)) {
lines.push_back(std::move(line));
}
return lines;
}
3. 二进制文件读写(对象序列化)
struct Player {
int id;
char name[32];
float score;
};
void save_player(const Player& p, const std::string& file) {
std::ofstream ofs(file, std::ios::binary);
ofs.write(reinterpret_cast<const char*>(&p), sizeof(Player));
}
Player load_player(const std::string& file) {
Player p{};
std::ifstream ifs(file, std::ios::binary);
ifs.read(reinterpret_cast<char*>(&p), sizeof(Player));
return p;
}
4. 错误处理全家桶
if (fs.fail()) // 逻辑错误(格式不对)
if (fs.eof()) // 到达文件尾
if (fs.bad()) // 严重错误(磁盘坏)
if (!fs.good()) // 任意错误
fs.clear(); // 重置状态
fs.ignore(1000, '\n'); // 跳过一行
三、字符串IO()—— 最被低估的神器
1. 三大类
| 类 | 用途 | 典型场景 |
|---|---|---|
istringstream | 把字符串当作输入流 | 解析 CSV / 命令行 |
ostringstream | 把输出拼成字符串 | 拼接日志、JSON、SQL |
stringstream | 可读可写 | 格式转换、临时缓冲 |
2. 实战1:字符串 → 数字(比 stoi 更灵活)
std::string s = "123 45.67 hello 999";
std::istringstream iss(s);
int a; double b; std::string c; int d;
iss >> a >> b >> c >> d; // a=123, b=45.67, c="hello", d=999
3. 实战2:数字 → 字符串 + 格式控制(C++20 前最优雅方式)
double price = 1234.56789;
std::ostringstream oss;
oss << std::fixed << std::setprecision(2) << price;
std::string result = oss.str(); // "1234.57"
4. 实战3:CSV 解析器(一行搞定)
std::vector<std::string> split_csv(const std::string& line) {
std::vector<std::string> fields;
std::istringstream iss(line);
std::string field;
while (std::getline(iss, field, ',')) {
fields.push_back(field);
}
return fields;
}
四、现代C++ IO最佳实践(2025 推荐)
- RAII + 异常:文件流构造函数失败时默认不抛异常,用
exceptions()开启
std::ofstream ofs;
ofs.exceptions(std::ios::failbit | std::ios::badbit);
ofs.open("data.txt");
- C++17
std::filesystem+ 流
namespace fs = std::filesystem;
if (fs::exists("config.json")) { ... }
- C++20
std::format+ stringstream 组合拳最强 - 避免全局
using namespace std; - 大文件读写:用
std::ifstream::read()+std::string::reserve()预分配
五、常见坑 & 一句话解决
| 问题 | 原因 | 一句话解决 |
|---|---|---|
cin >> 后 getline 空 | 缓冲区残留 \n | std::cin.ignore(1000, '\n') |
| 文件没写进去 | 没调用 close() 或没刷新 | 用 std::endl 或 ofs.flush() |
| 二进制文件乱码 | 用了文本模式 | 加上 std::ios::binary |
| 性能差(GB 级文件) | 一字节一字节读 | 用 read() + 大缓冲 |
一句话总结:
- 标准IO → 日常交互
- 文件IO → 持久化
- 字符串IO → 数据转换与解析
掌握这三者,你就真正掌握了 C++ 的“输入输出灵魂”。
需要下面任意一个完整可编译项目模板,直接告诉我:
- 日志系统(多线程安全 + 分文件)
- CSV 读写库(带表头、类型转换)
- 二进制对象持久化(支持 vector/map)
- 配置解析器(ini / json 轻量版)
- 大文件拷贝 + 进度条
我直接给你带 CMakeLists.txt 的完整代码。