C++17 里“文件相关操作”最好分成两部分看:
- 文件内容读写:
<fstream> - 文件/目录本身的管理:C++17 新增的
<filesystem>
fstream负责“打开文件、读内容、写内容”,filesystem负责“这个文件在哪、存不存在、叫什么、大小多少、怎么删/改名/遍历目录”。
1. <fstream>:文件读写
C++ 主要有三个文件流:
| 类型 | 作用 |
|---|---|
std::ifstream |
读文件 |
std::ofstream |
写文件 |
std::fstream |
可读可写 |
都在:
1 | #include <fstream> |
1.1 写文件
最简单的:
1 | #include <fstream> |
其实通常不需要手动 close(),因为 ofstream 析构时会自动关闭:
1 | { |
这就是典型的 RAII。
2. 写文件的打开模式
常见模式:
1 | std::ios::in |
其中:
| 模式 | 含义 |
|---|---|
in |
读取 |
out |
写入 |
app |
追加到文件末尾 |
trunc |
打开时清空原文件 |
binary |
二进制模式 |
ate |
打开后立即定位到文件末尾 |
例如追加:
1 | std::ofstream ofs("log.txt", std::ios::app); |
这不会覆盖之前的数据。
而普通:
1 | std::ofstream ofs("test.txt"); |
通常相当于:
1 | std::ios::out | std::ios::trunc |
原文件内容会被清空。
3. ifstream:读取文件
按单词读取
1 | std::ifstream ifs("test.txt"); |
假设:
1 | hello world |
会得到:
1 | hello |
因为 >> 会忽略空白字符。
4. 按行读取
实际项目里更常用:
1 | #include <fstream> |
这个非常重要。
尤其之前搜索引擎项目里做 MD 文档解析,本质上经常就是:
1 | ifstream |
5. 一次读取整个文件
项目中也很常见,比如读 JSON、Markdown、配置文件:
1 | #include <fstream> |
调用:
1 | std::string content = readFile("README.md"); |
执行逻辑:
1 | 文件 |
6. 二进制文件
例如图片、模型、压缩包,不能按文本方式理解。
1 | std::ifstream ifs("image.jpg", std::ios::binary); |
读取:
1 | char buffer[1024]; |
最后一次可能不足 1024:
1 | ifs.gcount() |
获得实际读取的字节数。
例如:
1 | while (ifs) { |
7. read() 和 write()
对于二进制数据:
1 | ofs.write(data, size); |
例如:
1 | struct User { |
读取:
1 | User u; |
但这里面试要注意:
直接 dump struct 只适合非常受控的场景。
因为可能受到:
1 | 字节对齐 |
影响。
正式序列化一般还是 protobuf、JSON、自定义格式等。
8. 文件位置:seekg / seekp
这里两个字母可以这么记:
1 | g = get → 读 |
因此:
1 | seekg() |
操作读指针。
1 | seekp() |
操作写指针。
例如获取文件大小:
1 | std::ifstream ifs("test.bin", std::ios::binary); |
逻辑:
1 | 文件头 |
9. C++17 <filesystem>
这个是 C++17 文件操作里最重要的新东西:
1 | #include <filesystem> |
以前很多操作需要:
1 | Windows API |
C++17 以后直接标准库解决。
10. std::filesystem::path
filesystem 的核心不是 string,而是:
1 | std::filesystem::path |
例如:
1 | fs::path p = "data/test.txt"; |
推荐不要自己拼:
1 | std::string path = dir + "/" + file; |
而是:
1 | fs::path path = fs::path(dir) / file; |
例如:
1 | fs::path dir = "data"; |
结果:
1 | data/test.txt |
好处是跨平台。
Windows:
1 | \ |
Linux:
1 | / |
让 filesystem 自己处理。
11. 判断文件是否存在
1 | if (fs::exists("test.txt")) { |
这个是非常高频 API。
12. 判断是文件还是目录
1 | fs::is_regular_file(path); |
例如:
1 | fs::path p = "data"; |
13. 创建目录
1 | fs::create_directory("data"); |
但是:
1 | data/images/train |
如果父目录不存在,create_directory() 不会帮你全部创建。
这时候:
1 | fs::create_directories("data/images/train"); |
会递归创建:
1 | data |
项目里一般更常用:
1 | create_directories() |
14. 删除文件
1 | fs::remove("test.txt"); |
也可以删除空目录。
如果想把整个目录树删掉:
1 | fs::remove_all("data"); |
注意:
1 | remove_all() |
危险性比较高,因为会递归删除。
15. 重命名 / 移动文件
1 | fs::rename("old.txt", "new.txt"); |
目录也可以:
1 | fs::rename("old_dir", "new_dir"); |
很多情况下移动文件也是:
1 | fs::rename( |
16. 复制文件
1 | fs::copy_file( |
如果目标已经存在,可以:
1 | fs::copy_file( |
复制目录:
1 | fs::copy( |
17. 获取文件大小
1 | auto size = fs::file_size("test.txt"); |
返回单位:
1 | byte |
18. 获取当前工作目录
1 | fs::path cwd = fs::current_path(); |
这在实际开发里非常重要,因为经常会遇到:
我明明有这个文件,为什么程序说找不到?
比如:
1 | ifstream ifs("config.json"); |
这里不是找:
1 | 源码目录/config.json |
而是:
1 | 当前工作目录/config.json |
所以 Debug 时经常先:
1 | std::cout << fs::current_path() << '\n'; |
19. 遍历目录
特别常用。
1 | for (const auto& entry : |
例如目录:
1 | data/ |
就会依次遍历这些文件。
20. 递归遍历目录
1 | for (const auto& entry : |
例如:
1 | data |
全部都会遍历。
这其实就很适合你的搜索引擎项目:
1 | 文档目录 |
21. 获取扩展名、文件名
假设:
1 | fs::path p = |
那么:
p.filename();得到:test.mdp.stem();得到:testp.extension();得到:.mdp.parent_path();得到:/home/wq/blog
例如筛选 Markdown:
1 | for (const auto& entry : |
22. 相对路径与绝对路径
fs::absolute("test.txt")
1 | path absolute(const path& p); |
作用:得到绝对路径(完整路径)
- 不带第二个参数:以**当前进程工作目录(cwd)**作为基准,把传入路径补成全路径。
- 自动解析
...,但不会做文件存在性检查,文件不存在也照样生成绝对路径字符串。
示例:
1 | // 当前工作目录 /home/weizhao/project |
区分:
absolute:只补齐成绝对形式,不简化多余的./、../;fs::canonical:不仅绝对,还化简,要求文件必须真实存在。
2. fs::relative(path)
1 | path relative(const path& p); |
作用:从基准路径 base 出发,得到 p 的相对路径
- 无第二个参数:默认基准 = 当前工作目录
- 返回:从 base 走到 p 的相对路径字符串
- 同样不要求文件存在,纯字符串路径运算
示例:
1 | // cwd = /home/weizhao/project |
指定base版本:
1 | fs::path p = "/a/b/c/file.txt"; |
关键注意点
- 路径必须同根 Windows:
C:/a和D:/b不同盘,relative会抛异常; Linux:不同挂载点一般不会抛,但跨根无法得到相对路径,会返回原绝对路径。 - 异常 默认会抛
filesystem_error;可以用重载带std::error_code不抛异常版本:
1 | fs::relative(p, ec); |
- 和
fs::weakly_canonical搭配场景relative不会自动化简../,如果路径里有冗余节点,建议先化简:
1 | auto abs_p = fs::weakly_canonical(fs::absolute(p)); |
对比
fs::absolute:相对 → 绝对,补全完整路径fs::relative:绝对 → 相对,算出从基准目录过去的简写路径
23. 一个实际项目级例子
假设需求:
遍历某个目录下所有
.md文件,把内容读取出来。
可以这样写:
1 | #include <filesystem> |
这里其实把 C++17 文件操作的两大体系串起来了:
1 | std::filesystem |
24. 异常处理
filesystem 的很多操作失败时可能抛:
1 | std::filesystem::filesystem_error |
例如:
1 | try { |
但 filesystem 还有一个很好用的机制:
1 | std::error_code |
例如:
1 | std::error_code ec; |
这时候不会通过异常报告失败。
服务器、基础库代码里经常更喜欢这种写法,因为错误流更加可控。
C++17 文件操作有哪些?
C++ 文件操作主要分两部分。文件内容读写主要使用
<fstream>,包括ifstream、ofstream和fstream,支持文本和二进制读写,以及seekg/seekp进行随机访问。C++17 又正式引入了<filesystem>,提供跨平台的路径和文件系统操作,比如path、exists、create_directories、remove、rename、copy、file_size,以及directory_iterator和recursive_directory_iterator进行目录遍历。实际项目中通常用filesystem定位和管理文件,再用fstream读取具体内容。
可以把整个体系记成这一张图:
1 | C++17 文件操作 |