C++ IO 指南

C++ IO 指南

1. 文件流 ifstream / ofstream / fstream(核心)

1. 一句话记住三个类

类名作用记忆口诀
ifstream读文件(input)i = input
ofstream写文件(output)o = output
fstream读 + 写全能型

工程建议:读写分离优先,用 ifstream + ofstream,可读性更好。

2. 读文件(最常用)

1. 标准写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <fstream>
#include <iostream>
#include <string>

int main()
{
// 打开输入文件(默认文本模式)
std::ifstream ifs("data.txt");

// 工程里第一件事:检查打开是否成功
if (!ifs)
{
std::cerr << "错误:无法打开 data.txt" << std::endl;
return 1;
}

std::string line;
// 逐行读取:成功才进入循环,失败自动退出
while (std::getline(ifs, line))
{
std::cout << line << std::endl;
}

return 0;
}

2. 为什么必须 while (getline(...))

1
2
3
4
5
6
7
8
9
while (std::getline(ifs, line))
{
// 正确:读取成功才进入循环
}

while (!ifs.eof())
{
// 错误:最后一次读取失败后才会 eof,常导致多处理一次
}

3. 写文件(最常用)

1. 标准写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <fstream>
#include <iostream>

int main()
{
// 追加模式写日志,避免覆盖历史内容
std::ofstream ofs("app.log", std::ios::app);

if (!ofs)
{
std::cerr << "错误:无法打开 app.log" << std::endl;
return 1;
}

// endl 会换行并刷新缓冲区,日志场景可接受
ofs << "[INFO] service started" << std::endl;
ofs << "[INFO] health check passed" << std::endl;

return 0;
}

2. 常用打开模式

模式含义场景
std::ios::out输出模式(默认)写文件
std::ios::app追加模式日志
std::ios::trunc截断模式覆盖写
std::ios::in输入模式读文件
std::ios::binary二进制模式二进制读写

组合模式示例:

1
2
std::ofstream ofs("data.bin", std::ios::app | std::ios::binary);
std::fstream fs("data.txt", std::ios::in | std::ios::out);

4. fstream 读写(了解即可)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <fstream>
#include <iostream>
#include <string>

int main()
{
// in|out:同一个流对象里既读又写
std::fstream fs("test.txt", std::ios::in | std::ios::out);
if (!fs)
{
std::cerr << "文件打开失败" << std::endl;
return 1;
}

std::string firstLine;
std::getline(fs, firstLine);
std::cout << "首行: " << firstLine << std::endl;

// 从读切到写前,清理状态位并移动写指针
fs.clear();
fs.seekp(0, std::ios::end);
fs << "\nappend by fstream";

return 0;
}

工程建议:只有“边读边改同一个文件”才用 fstream,否则分开更稳。

5. 文件流的传参陷阱(工程易错点)

问题:想把文件流传给函数处理,编译直接报错。

1
2
3
4
5
6
// ❌ 编译失败:ifstream 不可拷贝
void processFile(std::ifstream file);

// ❌ 编译失败:vector 里存不了文件流
std::vector<std::ifstream> files;
files.push_back(std::ifstream("a.txt")); // C++11 前报错

原因:文件流继承自 std::basic_ios,内部管理文件句柄,禁止拷贝(防止重复关闭同一个句柄)。

正确做法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <fstream>
#include <iostream>
#include <string>

// 传引用:函数内读取,调用方保持流对象
void processLineByRef(std::ifstream& file)
{
std::string line;
while (std::getline(file, line))
{
// 处理每一行
}
}

// 移动语义:把流所有权转给函数(C++11 起)
void takeOwnership(std::ifstream&& file)
{
std::string line;
std::getline(file, line);
// 函数结束时 file 自动关闭
}

int main()
{
std::ifstream ifs("data.txt");
if (!ifs) return 1;

// 传引用
processLineByRef(ifs);

// 移动语义(临时对象)
takeOwnership(std::ifstream("other.txt"));

return 0;
}

工程建议:

  1. 优先用引用 std::ifstream&,简单直观。
  2. 需要转移所有权时用移动语义 &&
  3. 别把文件流放进容器(vector<ifstream>),管理麻烦且容易出错。

2. 字符串流 stringstream(核心)

1. 三个类怎么选

类名作用常见用途
istringstream字符串 -> 数据解析配置、协议
ostringstream数据 -> 字符串拼日志、拼 SQL
stringstream读写都可临时处理(工程里少用)

2. ostringstream:拼接字符串

1. 标准写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
#include <sstream>
#include <string>

int main()
{
std::ostringstream oss;

std::string user = "alice";
int code = 200;
double cost = 12.5;

// 流式拼接:自动做类型转换,避免 string + int 的坑
oss << "user=" << user << ", code=" << code << ", cost=" << cost;

std::string line = oss.str();
std::cout << line << std::endl;
return 0;
}

2. 工程例子:日志行构造

1
2
3
4
5
6
7
8
9
#include <sstream>
#include <string>

std::string makeLogLine(const std::string& level, int requestId, const std::string& msg)
{
std::ostringstream oss;
oss << "[" << level << "] req_id=" << requestId << " msg=\"" << msg << "\"";
return oss.str();
}

3. istringstream:解析字符串

1. 标准写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
#include <sstream>
#include <string>

int main()
{
std::string raw = "bob 18 99.5";
std::istringstream iss(raw);

std::string name;
int age = 0;
double score = 0;

if (!(iss >> name >> age >> score))
{
std::cerr << "解析失败" << std::endl;
return 1;
}

std::cout << name << " " << age << " " << score << std::endl;
return 0;
}

2. 工程例子:解析命令

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <sstream>
#include <string>

bool parseCommand(const std::string& data, std::string& cmd, std::string& arg1, std::string& arg2)
{
std::istringstream iss(data);
// 任何一个字段解析失败,直接返回 false
if (!(iss >> cmd >> arg1 >> arg2))
{
return false;
}
return true;
}

4. stringstream(算法题中会常用一点点)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
#include <sstream>
#include <string>

int main()
{
std::stringstream ss;
ss << "Hello " << 42;

std::string word;
int num = 0;
ss >> word >> num;

std::cout << word << " " << num << std::endl;
return 0;
}

3. getline 详解(核心)

快速记忆:getline(字符串流, 变量, 分隔符);

1. 两种常见用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <fstream>
#include <iostream>
#include <string>

int main()
{
std::ifstream ifs("data.txt");
std::string line;

// 文件读取一行
if (std::getline(ifs, line))
{
std::cout << line << std::endl;
}

// 控制台读取一整行(可包含空格)
std::string input;
std::getline(std::cin, input);

return 0;
}

2. cin >>getline 混用大坑

1. 正确模板(直接用)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
#include <limits>
#include <string>

int main()
{
int age = 0;
std::string name;

std::cin >> age;
// 清掉残留换行符,避免下一次 getline 读到空串
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
std::getline(std::cin, name);

std::cout << "age=" << age << ", name=" << name << std::endl;
return 0;
}

口诀:cin >> 后接 getline,中间必须 ignore

2. ignore(max, '\n')ignore() 的联系与区别

联系:两者本质都是“丢弃输入缓冲区中的字符”,避免后续读取被残留内容干扰。

区别:

  • std::cin.ignore(); 只丢弃 1 个字符(默认参数就是 ignore(1))。
  • std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n'); 会持续丢弃,直到遇到换行符 \n(或达到上限)。
1
2
3
4
5
6
7
// 情况 A:确定缓冲区里只剩一个 '\n'
std::cin >> age;
std::cin.ignore(); // 常常够用,但前提比较苛刻

// 情况 B:更通用的工程写法(推荐),具体参数用的时候查一下就行了
std::cin >> age;
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');

工程建议:默认使用带 max(), '\n' 的版本,容错更好。

3. 指定分隔符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
#include <sstream>
#include <string>

int main()
{
std::istringstream iss("apple,banana,cherry");
std::string item;

while (std::getline(iss, item, ','))
{
std::cout << item << std::endl;
}

return 0;
}

4. 二进制读写(重要)

1. 必须知道的点

  • 文本模式下,Windows 可能做换行转换。
  • 二进制必须加 std::ios::binary
  • 只直接写 POD/平凡类型,不写指针,不直接写含 std::string 的结构体。

2. 写二进制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <fstream>
#include <iostream>

int main()
{
std::ofstream ofs("data.bin", std::ios::binary);
if (!ofs)
{
std::cerr << "open failed" << std::endl;
return 1;
}

int x = 100;
double y = 3.14159;

// write 需要 const char*(字节视图)
// reinterpret_cast <const char*>:把对象地址按“原始字节序列”解释给 IO 接口
// 这里只是重解释指针类型,不会复制对象
ofs.write(reinterpret_cast<const char*>(&x), sizeof(x));
ofs.write(reinterpret_cast<const char*>(&y), sizeof(y));

return 0;
}

3. 读二进制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <fstream>
#include <iostream>

int main()
{
std::ifstream ifs("data.bin", std::ios::binary);
if (!ifs)
{
std::cerr << "open failed" << std::endl;
return 1;
}

int x = 0;
double y = 0;

// read 需要可写入的 char* 缓冲区
// reinterpret_cast <char*>(&x):把 x 的内存当作字节缓冲区接收数据
// 每次 read 后都检查,避免读半包或损坏数据
if (!ifs.read(reinterpret_cast<char*>(&x), sizeof(x)))
{
std::cerr << "read x failed" << std::endl;
return 1;
}

if (!ifs.read(reinterpret_cast<char*>(&y), sizeof(y)))
{
std::cerr << "read y failed" << std::endl;
return 1;
}

std::cout << "x=" << x << ", y=" << y << std::endl;
return 0;
}

4. 工程增强:带文件头的二进制格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <cstdint>
#include <fstream>
#include <iostream>

struct FileHeader
{
std::uint32_t magic;
std::uint16_t version;
std::uint16_t reserved;
};

int main()
{
// magic 用于快速识别文件类型,version 用于向后兼容
const FileHeader h{0x43494F31, 1, 0}; // "CIO1"

std::ofstream ofs("records.bin", std::ios::binary);
if (!ofs)
{
return 1;
}

ofs.write(reinterpret_cast<const char*>(&h), sizeof(h));

return 0;
}

收益:后续升级格式时,可先读 magic/version,快速做兼容判断。


5. 常见问题和工程建议

1. 路径问题(Windows)

1
2
3
4
5
6
7
8
9
10
#include <fstream>

int main()
{
// 普通字符串里反斜杠要转义;原始字符串不用
std::ifstream a("C:\\Users\\name\\file.txt");
std::ifstream b(R"(C:\Users\name\file.txt)");
std::ifstream c("./data/file.txt");
return 0;
}

2. 文件定位(seekg/tellg

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <fstream>
#include <iostream>

int main()
{
std::ifstream ifs("data.txt", std::ios::binary);
if (!ifs)
{
return 1;
}

std::streampos pos = ifs.tellg();
std::cout << "pos=" << pos << std::endl;

ifs.seekg(0, std::ios::end);
std::streampos size = ifs.tellg();
std::cout << "size=" << size << std::endl;

return 0;
}

3. 缓冲区刷新

1
2
3
4
5
6
7
8
9
10
#include <fstream>

int main()
{
std::ofstream ofs("log.txt", std::ios::app);
// 关键日志可主动 flush,降低进程崩溃时丢日志风险
ofs << "important line";
ofs.flush();
return 0;
}

4. 性能优化(只在高频 IO 场景启用)

sync_with_stdio(false) 到底是啥?

简单说:C++ 默认会和 C 的 stdio 同步,保证 printf 和 cout 不乱序,关掉之后:

  • cin/cout 不再和 stdio 共享缓冲
  • 性能会明显提高

本质就是 减少锁 + 减少同步开销,刷题常开,工程基本不用。

1
2
3
4
5
6
7
8
#include <iostream>

int main()
{
std::ios::sync_with_stdio(false); // 关闭 C++/C 流同步,核心提升 cin/cout 速度,代价是不能混用 C/C++ 输入输出
std::cin.tie(nullptr); // 解除 cin 和 cout 的绑定,避免 cin 触发 cout 自动刷新,进一步优化性能
return 0;
}

注意:启用后不要混用 printf/scanfcin/cout

5. 工程实战建议(强烈推荐)

  1. 日志写入优先 append,避免误覆盖历史日志。
  2. 关键数据落盘用“临时文件 + rename”做原子替换,避免写一半崩溃导致坏文件。
  3. 读取外部输入(文件/网络)时,每一步都校验返回值,不要假设一定成功。
  4. 跨平台二进制格式务必定义字节序和版本号。
  5. 文本协议优先 UTF-8,减少编码问题。

6. std::cerrstd::clog 的使用建议

  • std::cerr:偏“错误输出”,适合立即关注的错误信息。
  • std::clog:偏“常规日志输出”,适合运行过程中的信息日志。
1
2
3
4
5
6
7
8
#include <iostream>

int main()
{
std::clog << "[INFO] server started" << std::endl; // 普通日志
std::cerr << "[ERROR] config load failed" << std::endl; // 错误日志
return 0;
}

工程建议:业务日志用 clog,错误路径用 cerr,便于后续日志分流。顺便简单提一嘴(需要一些其他知识才能理解):

fd(文件描述符)缓冲用途
cout1正常输出
cerr(常搭配 strerror 把错误码变成人能看懂的错误信息)2错误输出
clog(少用,通常会选择第三方日志库 spdlog、glog)2日志

cerr 不缓冲的原因:程序崩溃时,你至少能看到错误信息。如果用 cout,可能还在缓冲区里,来不及刷就挂了。


6. 速查表

1. 头文件

1
2
3
4
5
#include <fstream>   // 文件流:std:: ifstream / std:: ofstream / std:: fstream
#include <sstream> // 字符串流:std:: istringstream / std:: ostringstream / std:: stringstream
#include <iostream> // 标准输入输出:std:: cin / std:: cout / std:: cerr / std:: clog / std:: endl
#include <string> // 字符串与行读取:std:: string / std:: getline
#include <limits> // 数值边界:std:: numeric_limits <std::streamsize>:: max()

2. 最常用模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 读文件
std::ifstream ifs("file.txt");
if (!ifs)
{
// 文件不存在/无权限/路径错误都会走这里
return 1;
}
std::string line;
while (std::getline(ifs, line))
{
// 处理每一行
}

// 写文件(追加)
std::ofstream ofs("file.txt", std::ios::app);
if (!ofs)
{
return 1;
}
// 追加写,不会清空旧内容
ofs << "content" << std::endl;

// cin >> + getline
int x = 0;
std::string s;
std::cin >> x;
// 忽略输入缓冲区残留换行
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
std::getline(std::cin, s);

7. 小结

90% 工程场景只要熟练掌握这 5 条:

  1. ifstream + getline 逐行读取。
  2. ofstream + ios::app 追加日志。
  3. ostringstream 安全拼接字符串。
  4. istringstream 解析结构化文本。
  5. cin >> 后接 getline 必加 ignore

这 5 条能覆盖绝大多数日常开发中的 IO 问题。简单提一嘴,知道名字就行,用到再查:

  • seekg / seekp → 文件定位
  • tellg / tellp → 获取位置
  • flush() → 刷缓冲
  • std::hex / std::dec → 进制
  • setw / setfill → 格式控制
  • sync_with_stdio(false) → 提速