C++17 里“文件相关操作”最好分成两部分看:

  1. 文件内容读写:<fstream>
  2. 文件/目录本身的管理:C++17 新增的 <filesystem>

fstream 负责“打开文件、读内容、写内容”,filesystem 负责“这个文件在哪、存不存在、叫什么、大小多少、怎么删/改名/遍历目录”。


1. <fstream>:文件读写

C++ 主要有三个文件流:

类型 作用
std::ifstream 读文件
std::ofstream 写文件
std::fstream 可读可写

都在:

1
#include <fstream>

1.1 写文件

最简单的:

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

int main() {
std::ofstream ofs("test.txt");

if (!ofs.is_open()) {
std::cout << "文件打开失败\n";
return 1;
}

ofs << "hello C++17\n";
ofs << 123 << '\n';

ofs.close();
}

其实通常不需要手动 close(),因为 ofstream 析构时会自动关闭:

1
2
3
4
{
std::ofstream ofs("test.txt");
ofs << "hello";
} // 离开作用域,自动关闭

这就是典型的 RAII。


2. 写文件的打开模式

常见模式:

1
2
3
4
5
6
std::ios::in
std::ios::out
std::ios::app
std::ios::trunc
std::ios::binary
std::ios::ate

其中:

模式 含义
in 读取
out 写入
app 追加到文件末尾
trunc 打开时清空原文件
binary 二进制模式
ate 打开后立即定位到文件末尾

例如追加:

1
2
3
std::ofstream ofs("log.txt", std::ios::app);

ofs << "new log\n";

这不会覆盖之前的数据。

而普通:

1
std::ofstream ofs("test.txt");

通常相当于:

1
std::ios::out | std::ios::trunc

原文件内容会被清空。


3. ifstream:读取文件

按单词读取

1
2
3
4
5
6
7
std::ifstream ifs("test.txt");

std::string word;

while (ifs >> word) {
std::cout << word << '\n';
}

假设:

1
2
hello world
C++ filesystem

会得到:

1
2
3
4
hello
world
C++
filesystem

因为 >> 会忽略空白字符。


4. 按行读取

实际项目里更常用:

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

int main() {
std::ifstream ifs("test.txt");

std::string line;

while (std::getline(ifs, line)) {
std::cout << line << '\n';
}
}

这个非常重要。

尤其之前搜索引擎项目里做 MD 文档解析,本质上经常就是:

1
2
3
4
5
6
7
ifstream
↓
getline
↓
逐行解析
↓
分词 / 建索引

5. 一次读取整个文件

项目中也很常见,比如读 JSON、Markdown、配置文件:

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

std::string readFile(const std::string& filename) {
std::ifstream ifs(filename);

std::stringstream ss;
ss << ifs.rdbuf();

return ss.str();
}

调用:

1
std::string content = readFile("README.md");

执行逻辑:

1
2
3
4
5
6
7
8
9
文件
↓
ifstream
↓
streambuf
↓
stringstream
↓
std::string

6. 二进制文件

例如图片、模型、压缩包,不能按文本方式理解。

1
std::ifstream ifs("image.jpg", std::ios::binary);

读取:

1
2
3
4
5
char buffer[1024];

while (ifs.read(buffer, sizeof(buffer))) {
// 处理1024字节
}

最后一次可能不足 1024:

1
ifs.gcount()

获得实际读取的字节数。

例如:

1
2
3
4
5
6
7
8
9
while (ifs) {
ifs.read(buffer, sizeof(buffer));

std::streamsize count = ifs.gcount();

if (count > 0) {
// 处理 count 个字节
}
}

7. read() 和 write()

对于二进制数据:

1
2
ofs.write(data, size);
ifs.read(data, size);

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct User {
int id;
double score;
};

User u{1, 95.5};

std::ofstream ofs("user.bin", std::ios::binary);

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

读取:

1
2
3
4
5
6
7
8
User u;

std::ifstream ifs("user.bin", std::ios::binary);

ifs.read(
reinterpret_cast<char*>(&u),
sizeof(u)
);

但这里面试要注意:

直接 dump struct 只适合非常受控的场景。

因为可能受到:

1
2
3
4
5
6
字节对齐
padding
大小端
编译器差异
ABI
类型版本变化

影响。

正式序列化一般还是 protobuf、JSON、自定义格式等。


8. 文件位置:seekg / seekp

这里两个字母可以这么记:

1
2
g = get  → 读
p = put → 写

因此:

1
2
seekg()
tellg()

操作读指针。

1
2
seekp()
tellp()

操作写指针。

例如获取文件大小:

1
2
3
4
5
6
7
std::ifstream ifs("test.bin", std::ios::binary);

ifs.seekg(0, std::ios::end);

std::streamsize size = ifs.tellg();

ifs.seekg(0, std::ios::beg);

逻辑:

1
2
3
4
5
6
7
8
9
文件头
│
├────────────── 文件内容 ────────────┤
│ ↑
│ seekg(end)
│ │
│ tellg()
│ │
│ 得到文件大小

9. C++17 <filesystem>

这个是 C++17 文件操作里最重要的新东西:

1
2
3
#include <filesystem>

namespace fs = std::filesystem;

以前很多操作需要:

1
2
3
Windows API
POSIX API
boost::filesystem

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
2
3
4
fs::path dir = "data";
fs::path file = "test.txt";

fs::path fullPath = dir / file;

结果:

1
data/test.txt

好处是跨平台。

Windows:

1
\

Linux:

1
/

让 filesystem 自己处理。


11. 判断文件是否存在

1
2
3
if (fs::exists("test.txt")) {
std::cout << "存在\n";
}

这个是非常高频 API。


12. 判断是文件还是目录

1
2
fs::is_regular_file(path);
fs::is_directory(path);

例如:

1
2
3
4
5
fs::path p = "data";

if (fs::is_directory(p)) {
std::cout << "这是目录\n";
}

13. 创建目录

1
fs::create_directory("data");

但是:

1
data/images/train

如果父目录不存在,create_directory() 不会帮你全部创建。

这时候:

1
fs::create_directories("data/images/train");

会递归创建:

1
2
3
data
└── images
└── train

项目里一般更常用:

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
2
3
4
fs::rename(
"data/a.txt",
"backup/a.txt"
);

16. 复制文件

1
2
3
4
fs::copy_file(
"a.txt",
"b.txt"
);

如果目标已经存在,可以:

1
2
3
4
5
fs::copy_file(
"a.txt",
"b.txt",
fs::copy_options::overwrite_existing
);

复制目录:

1
2
3
4
5
fs::copy(
"src",
"dst",
fs::copy_options::recursive
);

17. 获取文件大小

1
2
3
auto size = fs::file_size("test.txt");

std::cout << size << '\n';

返回单位:

1
byte

18. 获取当前工作目录

1
2
3
fs::path cwd = fs::current_path();

std::cout << cwd << '\n';

这在实际开发里非常重要,因为经常会遇到:

我明明有这个文件,为什么程序说找不到?

比如:

1
ifstream ifs("config.json");

这里不是找:

1
源码目录/config.json

而是:

1
当前工作目录/config.json

所以 Debug 时经常先:

1
std::cout << fs::current_path() << '\n';

19. 遍历目录

特别常用。

1
2
3
4
5
for (const auto& entry :
fs::directory_iterator("data")) {

std::cout << entry.path() << '\n';
}

例如目录:

1
2
3
4
data/
├── a.txt
├── b.txt
└── image.jpg

就会依次遍历这些文件。


20. 递归遍历目录

1
2
3
4
5
for (const auto& entry :
fs::recursive_directory_iterator("data")) {

std::cout << entry.path() << '\n';
}

例如:

1
2
3
4
5
6
7
data
├── a.txt
├── docs
│ ├── a.md
│ └── b.md
└── images
└── 1.jpg

全部都会遍历。

这其实就很适合你的搜索引擎项目:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
文档目录
↓
recursive_directory_iterator
↓
筛选 .md
↓
ifstream
↓
读取 Markdown
↓
解析
↓
分词
↓
构建索引

21. 获取扩展名、文件名

假设:

1
2
fs::path p =
"/home/wq/blog/test.md";

那么:

  • p.filename();得到:test.md

  • p.stem();得到:test

  • p.extension();得到:.md

  • p.parent_path();得到:/home/wq/blog

例如筛选 Markdown:

1
2
3
4
5
6
7
for (const auto& entry :
fs::recursive_directory_iterator("docs")) {

if (entry.path().extension() == ".md") {
std::cout << entry.path() << '\n';
}
}

22. 相对路径与绝对路径

fs::absolute("test.txt")

1
2
path absolute(const path& p);
path absolute(const path& p, const path& base);

作用:得到绝对路径(完整路径)

  • 不带第二个参数:以**当前进程工作目录(cwd)**作为基准,把传入路径补成全路径。
  • 自动解析 . ..,但不会做文件存在性检查,文件不存在也照样生成绝对路径字符串。

示例:

1
2
3
// 当前工作目录 /home/weizhao/project
fs::path p = fs::absolute("test.txt");
// p => "/home/weizhao/project/test.txt"

区分:

  • absolute:只补齐成绝对形式,不简化多余的 ./、../;
  • fs::canonical:不仅绝对,还化简,要求文件必须真实存在。

2. fs::relative(path)

1
2
path relative(const path& p);
path relative(const path& p, const path& base);

作用:从基准路径 base 出发,得到 p 的相对路径

  • 无第二个参数:默认基准 = 当前工作目录
  • 返回:从 base 走到 p 的相对路径字符串
  • 同样不要求文件存在,纯字符串路径运算

示例:

1
2
3
4
// cwd = /home/weizhao/project
fs::path full = "/home/weizhao/project/src/main.cpp";
fs::path rel = fs::relative(full);
// rel => "src/main.cpp"

指定base版本:

1
2
3
fs::path p = "/a/b/c/file.txt";
fs::path base = "/a";
auto r = fs::relative(p, base); // "b/c/file.txt"

关键注意点

  1. 路径必须同根 Windows:C:/a 和 D:/b 不同盘,relative 会抛异常; Linux:不同挂载点一般不会抛,但跨根无法得到相对路径,会返回原绝对路径。
  2. 异常 默认会抛 filesystem_error;可以用重载带 std::error_code 不抛异常版本:
1
2
fs::relative(p, ec);
fs::absolute(p, ec);
  1. 和 fs::weakly_canonical 搭配场景 relative 不会自动化简 ../,如果路径里有冗余节点,建议先化简:
1
auto abs_p = fs::weakly_canonical(fs::absolute(p));

对比

  • fs::absolute:相对 → 绝对,补全完整路径
  • fs::relative:绝对 → 相对,算出从基准目录过去的简写路径

23. 一个实际项目级例子

假设需求:

遍历某个目录下所有 .md 文件,把内容读取出来。

可以这样写:

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#include <filesystem>
#include <fstream>
#include <iostream>
#include <sstream>
#include <string>

namespace fs = std::filesystem;

std::string readFile(const fs::path& path) {
std::ifstream ifs(path);

if (!ifs.is_open()) {
return {};
}

std::stringstream ss;
ss << ifs.rdbuf();

return ss.str();
}

int main() {
fs::path root = "./docs";

if (!fs::exists(root)) {
std::cout << "目录不存在\n";
return 1;
}

for (const auto& entry :
fs::recursive_directory_iterator(root)) {

if (!entry.is_regular_file()) {
continue;
}

if (entry.path().extension() != ".md") {
continue;
}

std::string content =
readFile(entry.path());

std::cout
<< "文件: "
<< entry.path()
<< '\n';

std::cout
<< "大小: "
<< content.size()
<< '\n';
}
}

这里其实把 C++17 文件操作的两大体系串起来了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
std::filesystem
│
├── 找到文件
│
├── 判断类型
│
├── 获取路径
│
▼
std::ifstream
│
├── 打开
│
├── 读取
│
▼
std::string

24. 异常处理

filesystem 的很多操作失败时可能抛:

1
std::filesystem::filesystem_error

例如:

1
2
3
4
5
6
try {
fs::remove_all("/xxx");
}
catch (const fs::filesystem_error& e) {
std::cout << e.what() << '\n';
}

但 filesystem 还有一个很好用的机制:

1
std::error_code

例如:

1
2
3
4
5
6
7
std::error_code ec;

fs::remove("test.txt", ec);

if (ec) {
std::cout << ec.message() << '\n';
}

这时候不会通过异常报告失败。

服务器、基础库代码里经常更喜欢这种写法,因为错误流更加可控。


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
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
             C++17 文件操作

┌───────────────┐
│ 文件系统 │
│ <filesystem> │
└───────┬───────┘
│
┌───────────┼─────────────┐
▼ ▼ ▼
path 文件管理 目录遍历
│ exists/remove │
│ rename/copy │
│ file_size │
│ │
└─────────────┬─────────────┘
▼
找到文件
│
▼
┌─────────────┐
│ <fstream> │
└──────┬──────┘
│
┌─────────┼─────────┐
▼ ▼ ▼
ifstream ofstream fstream
读 写 读写
│
▼
getline / >> / read
│
▼
程序中的数据