本文介绍标准库中的跨平台的文件导航系统,
<filesystem>头文件实现了 C++文件系统技术规范 IOS / IEC TS 18822:2015(最终草案文档 N4100:https://wg21.link/n4100)。该头文件提供一系列类型与函数,允许开发者编写`平台无关`的代码来**遍历、操作文件系统**。
目录
- [1 概述](#1 概述)
- [2 路径](#2 路径)
-
- [2.1 路径的构造与拼接](#2.1 路径的构造与拼接)
- [2.2 路径解析](#2.2 路径解析)
- [2.3 路径比较](#2.3 路径比较)
- [2.4 path 与字符串类型之间的转换](#2.4 path 与字符串类型之间的转换)
- [2.5 遍历目录与文件](#2.5 遍历目录与文件)
1 概述
由于该库是面向跨平台设计 ,其中包含部分对 Windows 系统没有实际意义的 API。
- 例如:is_fifo(const path&) 函数在 Windows 平台调用时,永远返回 false。
说明:
- 标准背景
- N4100 是 C++17 之前的实验性技术规范(Filesystem TS),最初命名为 <experimental/filesystem>,命名空间 std::experimental::filesystem::v1。
- C++17 正式接纳该规范,头文件改为
<filesystem>,命名空间为 std::filesystem,原始 TS 文档 ISO/IEC TS 18822:2015 现已撤回作废。- 规范的设计参考 POSIX 接口,所以里面有大量 Unix/Linux 特有的文件类型接口。
- is_fifo 知识点
- fifo:FIFO 命名管道,是 POSIX/Unix-like 系统的特殊文件类型,用于进程间通信。
- Windows 有自己的命名管道机制,但不属于 POSIX FIFO 文件模型;std::filesystem 不识别 Windows 命名管道,因此 is_fifo() 在 Windows 总是返回 false。
- Linux/macOS 下,如果路径指向 FIFO 管道,is_fifo() 返回 true。
- 跨平台开发注意点
- std::filesystem 为了兼容 POSIX,提供若干 Unix 专属 API,Windows 下会给出退化结果(如下表):
- Windows 没有块设备文件、字符设备文件、socket 文件、FIFO 管道 这些 POSIX 文件类型。
写跨平台代码时,不能假设这些接口一定能检测到对应对象。
| 函数 | 说明(Windows 行为) |
|---|---|
| is_fifo() | 恒返回 false |
| is_socket() | 恒返回 false |
| is_block_file()、is_character_file() | 恒返回 false |
库主要能力(文件系统导航)
- 遍历目录:directory_iterator、recursive_directory_iterator
- 路径处理:path 类,路径拼接、分解、获取文件名 / 后缀、根路径
- 文件操作:创建 / 复制 / 重命名 / 删除文件、目录;读取符号链接;查询文件大小、修改时间、权限等。
代码提示
- Windows 下如需使用命名管道 IPC,应当直接调用
Win32 原生 API,不要依赖 std::filesystem 的 is_fifo 接口。
可以使用 <filesystem> 应用程序接口完成以下任务:遍历指定路径下的文件与目录;
获取文件信息,包括创建时间、文件大小、文件扩展名以及根目录;
拼接、拆解以及比较路径;
创建、复制和删除目录;
复制和删除文件;
各项任务对应的核心 API:
- 注:std::filesystem 负责文件系统元数据、目录、路径管理;文件内容读写(读 / 写字节、文本)不由 filesystem 完成,文本内容读写需要 iostream / fstream 系列库。
<filesystem>:管"在哪里":路径、目录结构、文件属性、删除复制,不读写文件内部字节。<iostream> / <fstream>:管"里面是什么":打开文件、读取、写入文件内容。
| 任务 | 主要接口 / 类 |
|---|---|
| 遍历文件、目录 | directory_iterator(单层遍历)、recursive_directory_iterator(递归遍历) |
| 获取文件元信息 | file_size()、last_write_time()、path::extension()、path::root_directory() |
| 路径拼接 / 拆解 / 比较 | path 类;operator/= 拼接;filename()/stem()/extension() 拆解;compare()、==、< 比较路径 |
| 目录增删复制 | create_directory() / create_directories()、copy()、remove() / remove_all() |
| 文件复制删除 | copy_file()、remove() |
2 路径
2.1 路径的构造与拼接
Windows(自 XP 版本起)内部以 Unicode 格式原生存储路径 。
path 类自动完成全部必要的字符串编码转换。它可以接收宽字符、窄字符数组,以及 UTF-8 或 UTF-16 编码的 std::string、std::wstring 作为入参。path 类还会自动规范化路径分隔符。你可以在构造函数参数中直接使用单正斜杠 / 充当目录分隔符 ;使用这种分隔符,同一份字符串可以同时在
Windows和UNIX环境中表示路径:
cpp
path pathToDisplay(L"/FileSystemTest/SubDir3"); //合法
path pathToDisplay2(L"\\FileSystemTest\\SubDir3"); //一如既往,同样合法
path pathToDisplay3(LR"(\FileSystemTest\SubDir3)"); //原始字符串字面量也可使用
如需拼接两条路径,可以使用重载的
/与/=运算符,其作用类似于 std::string 的 + 与 +=。path 对象会在需要时自动补上路径分隔符:
cpp
path myRoot("C:/FileSystemTest"); //末尾不带分隔符,完全没问题
myRoot /= path("SubDirRoot"); //结果:C:/FileSystemTest/SubDirRoot
重点解析
- Windows 平台编码特性
- Windows 底层 API 全部使用 Unicode(UTF-16),std::filesystem::path 在 Windows 上 value_type 为 wchar_t;POSIX(Linux/macOS)下为 char。
传入 UTF-8 窄字符串、宽字符串,path 构造函数会自动做编码转换,使用者不必手动处理编码转换。- 路径分隔符的兼容性
正斜杠 / 可跨平台书写:写代码时优先用 / ,path 会在 Windows 内部自动转换成本地的反斜杠 \,Linux 保持 / 不变。- Windows 原生反斜杠
\\同样被支持;普通字符串字面量必须写双反斜杠做转义。- C++11 原始字符串字面量 R"(...)",不需要转义反斜杠,适合写 Windows 路径。
- ⚠️注:只在 path 类中 / 才会被自动转换;普通 std::string 字符串不会自动转换分隔符。
/、/=vs+、+=(appendvsconcat)
| 运算符 | 对应成员函数 | 行为说明 |
|---|---|---|
| path1 / path2 | append() | 返回新 path,自动按需插入系统首选目录分隔符,用于拼接目录层级 |
| path1 /= path2 | append() | 原地修改对象,自动加分隔符 |
| path += "xxx" | concat() | 纯字符串直接拼接,不会自动添加分隔符,适合追加扩展名 |
cpp
namespace fs = std::filesystem;
fs::path base{"C:/a"};
base /= "b"; //append,自动加分隔符 → "C:/a/b"
fs::path p{"file"};
p += ".txt"; //concat,直接拼接,不加斜杠 → "file.txt"
- 易错点
operator /=智能判断什么时候不加分隔符:
- 如果右边开头已经带分隔符、或者右边是空路径,则不会重复追加分隔符,避免出现 a//b。
- path 只是语法层面处理路径字符串 ,构造对象不会访问磁盘 ;
路径对应的文件 / 目录可以根本不存在。
可编译示例(需要使用 C++17 及以上标准库):
cpp
#include <filesystem>
#include <iostream>
namespace fs = std::filesystem;
int main()
{
fs::path p1(L"/FileSystemTest/SubDir3");
fs::path p2(L"\\FileSystemTest\\SubDir3");
fs::path p3(LR"(\FileSystemTest\SubDir3)");
fs::path myRoot("C:/FileSystemTest");
myRoot /= "SubDirRoot";
std::cout << "native: " << myRoot.string() << "\n";
std::cout << "generic: " << myRoot.generic_string() << "\n";
return 0;
}
运行结果:
- 没有对应路径也可以编译;
.string():返回原生表示(保留原始分隔符).generic_string():统一转为 /
2.2 路径解析
path 类提供若干成员方法,用于获取路径字符串各个组成部分的信息。这些信息仅仅解析路径文本本身,和磁盘上是否真实存在该文件 / 目录无关 。你可以获取根路径、相对路径、文件名、文件扩展名等内容。还可以对 path 对象进行迭代,逐层遍历路径层级里的每一段目录。
下面示例演示如何迭代遍历路径对象,以及提取路径各组成部分信息:
cpp
// filesystem_path_example.cpp
// 编译命令: /EHsc /W4 /permissive- /std:c++17 (或更高标准)
#include <string>
#include <iostream>
#include <sstream>
#include <filesystem>
using namespace std;
using namespace std::filesystem;
wstring DisplayPathInfo()
{
// 该路径指向的文件不一定真实存在;
// 我们只是解析路径字符串本身,并不会访问磁盘上的文件对象。
path pathToDisplay(L"C:/FileSystemTest/SubDir3/SubDirLevel2/File2.txt ");
wostringstream wos;
int i = 0;
wos << L"Displaying path info for: " << pathToDisplay << endl;
// 迭代遍历路径的每一个片段
for (path::iterator itr = pathToDisplay.begin(); itr != pathToDisplay.end(); ++itr)
{
wos << L"path part: " << i++ << L" = " << *itr << endl;
}
wos << L"root_name() = " << pathToDisplay.root_name() << endl
<< L"root_path() = " << pathToDisplay.root_path() << endl
<< L"relative_path() = " << pathToDisplay.relative_path() << endl
<< L"parent_path() = " << pathToDisplay.parent_path() << endl
<< L"filename() = " << pathToDisplay.filename() << endl
<< L"stem() = " << pathToDisplay.stem() << endl
<< L"extension() = " << pathToDisplay.extension() << endl;
return wos.str();
}
int main()
{
wcout << DisplayPathInfo() << endl;
// wcout << ComparePaths() << endl; // 参见后续示例
wcout << endl << L"Press Enter to exit" << endl;
wstring input;
getline(wcin, input);
}
运行结果:
详细解析
- 核心原则
- 路径解析只处理字符串文本,不会访问硬盘。
即使路径完全不存在,root_name()、filename()、stem() 这类函数照样可以返回结果。- 如果你需要
读取文件大小、修改时间等磁盘实体信息,则需要调用 file_size、last_write_time 等独立函数。- 迭代器遍历规则
- path 的迭代器会把路径拆分成一个个片段,Windows 示例:C:/FileSystemTest/SubDir3/SubDirLevel2/File2.txt 拆分结果:
- Linux/Unix 路径 /home/user/test.txt 迭代起始片段是
/
| 索引 | 片段 | 说明 |
|---|---|---|
| 0 | C: | 驱动器名(根名称 root-name) |
| 1 | \ | 根分隔符 |
| 2 | FileSystemTest | 一级目录 |
| 3 | SubDir3 | 二级目录 |
| 4 | SubDirLevel2 | 三级目录 |
| 5 | File2.txt | 文件名 |
- 7个成员函数释义
| 成员函数 | 返回结果 | 含义 |
|---|---|---|
| root_name() | C: | 驱动器盘符名称,Windows 特有;Linux 为空; |
| root_path() | C:\ | 根路径(盘符+根分隔符) |
| relative_path() | FileSystemTest\SubDir3\SubDirLevel2\File2.txt | 去掉根路径之后剩余的相对部分 |
| parent_path() | C:\FileSystemTest\SubDir3\SubDirLevel2 | 父目录路径,去掉最末尾那一段 |
| filename() | File2.txt | 路径最末端的文件名(带扩展名) |
| stem() | File2 | 不带扩展名的文件名主干 |
| extension() | .txt | 扩展名,包含开头的点号 . |
- 几个容易踩坑的细节
- 扩展名带小数点 :
extension()返回值包含.,不要自己再加小数点;- 多重点号文件名 :archive.tar.gz
filename()→ archive.tar.gzstem()→ archive.tarextension()→ .gz
- stem() 只剥离最后一个扩展名
- 路径末尾带斜杠
C:/a/b/:filename() 返回空路径,parent_path() 返回C:/a/b;- Linux 和 Windows 返回片段不一样,盘符相关接口在 Linux 环境返回空。
2.3 路径比较
path 类重载了与 std::string、std::wstring 完全一致的比较运算符。当比较两个路径时,会先将路径分隔符规范化,之后再执行字符串比较 。
如果一条路径末尾缺少斜杠(正斜杠或反斜杠),系统不会自动补全末尾分隔符 ,而末尾斜杠的有无会直接影响比较结果。
下面示例演示路径对象之间的比较规则:
cpp
// filesystem_path_example.cpp
// 编译命令: /EHsc /W4 /permissive- /std:c++17 (或更高标准)
#include <string>
#include <iostream>
#include <sstream>
#include <filesystem>
using namespace std;
using namespace std::filesystem;
wstring ComparePaths() {
path p0(L"C:/Documents"); // 末尾无分隔符
path p1(L"C:/Documents/"); // p0 < p1
path p2(L"C:/Documents/2013/"); // p1 < p2
path p3(L"C:/Documents/2013/Reports/"); // p2 < p3
path p4(L"C:/Documents/2014/"); // p3 < p4
path p5(L"D:/Documents/2013/Reports/"); // p4 < p5
wostringstream wos;
wos << boolalpha <<
p0.wstring() << L" < " << p1.wstring() << L": " << (p0 < p1) << endl <<
p1.wstring() << L" < " << p2.wstring() << L": " << (p1 < p2) << endl <<
p2.wstring() << L" < " << p3.wstring() << L": " << (p2 < p3) << endl <<
p3.wstring() << L" < " << p4.wstring() << L": " << (p3 < p4) << endl <<
p4.wstring() << L" < " << p5.wstring() << L": " << (p4 < p5) << endl;
return wos.str();
}
int main()
{
wcout << ComparePaths() << endl;
// wcout << ComparePaths() << endl; // 参见后续示例
wcout << endl << L"Press Enter to exit" << endl;
wstring input;
getline(wcin, input);
}
运行结果:
详细原理
- 底层比较原理
- operator < / > / == / != 执行字典序字符串对比,并不是查询磁盘判断两个路径是否指向同一个物理文件。
- ⚠️重要区分 :
- path 对象比较:文本层面字典序对比,不访问磁盘;
- 判断两个路径是否对应磁盘上同一个文件:使用
fs::equivalent(a,b),该函数会访问文件系统;- 分隔符规范化做了什么
- 规范化:/ 自动转为 Windows 本地分隔符 \ ;
- 但是!不会自动追加末尾斜杠;
cpp// 二者字符串并不相等,p0 < p1 结果为 true p0 = C:\Documents p1 = C:\Documents\
- 运算符清单
std::filesystem::path支持全套比较运算符:== 、!= 、< 、<= 、> 、>=
2.4 path 与字符串类型之间的转换
path对象可以隐式转换 为std::wstring或者std::string。这意味着你可以直接把 path 对象传给像basic_ofstream::open这类函数,示例如下:
cpp
// filesystem_path_conversion.cpp
// compile by using: /EHsc /W4 /permissive- /std:c++17 (or later)
#include <string>
#include <iostream>
#include <fstream>
#include <filesystem>
using namespace std;
using namespace std::filesystem;
int main()
{
const wchar_t* p{ L"C:/Users/Public/Documents" };
path filePath(p);
filePath /= L"NewFile.txt";
// Open, write to, and close the file.
wofstream writeFile(filePath, ios::out); // implicit conversion
writeFile << L"Lorem ipsum\nDolor sit amet";
writeFile.close();
// Open, read, and close the file.
wifstream readFile;
wstring line;
readFile.open(filePath); // implicit conversions
wcout << L"File " << filePath << L" contains:" << endl;
while (readFile.good())
{
getline(readFile, line);
wcout << line << endl;
}
readFile.close();
wcout << endl << L"Press Enter to exit" << endl;
wstring input;
getline(wcin, input);
}
运行结果:
详细解析:
- 隐式转换规则
Windows 平台:path 的底层字符类型是wchar_t,隐式转换优先得到std::wstring。Linux/macOS平台:path 底层字符类型是char,隐式转换优先得到std::string。- 代码示例中使用 wofstream / wifstream(宽字符文件流),接收 path 参数时自动隐式转为 wstring。
- 显式获取字符串的成员函数(推荐,避免隐式转换带来歧义)
- 最佳实践:重要场景尽量用显式调用 native() / wstring(),不要依赖隐式转换,防止不同平台下类型推断意外变化。
| 方法 | 返回值 | 说明 |
|---|---|---|
| path::native() | basic_string<value_type> | 平台原生编码字符串(Windows:wstring;Linux:string) |
| path::string() | std::string | 转为 UTF-8 窄字符串 |
| path::wstring() | std::wstring | 转为 UTF-16 宽字符串 |
| path::generic_string() | std::string | 通用格式,分隔符统一为 / |
| path::generic_wstring() | std::wstring | 通用格式宽字符串,分隔符统一为 / |
- 示例代码逻辑简述
- 使用宽字符字符串构造 path 对象;
- 通过 /= 拼接文件名 NewFile.txt;
- 直接把 filePath 传入 wofstream 构造函数,path 隐式转为 wstring,打开文件;
- 写入两行文本,关闭文件;
- 使用 wifstream 打开文件,同样传入 path 对象触发隐式转换;
- 循环读取每行,输出到控制台;
- 注意事项
- 隐式转换仅转换字符串表达,
不会校验文件是否真实存在;区分 native(系统原生分隔符,Windows 是 \)与 generic(通用 / 分隔符);- 如果
混用窄字符流 std::ifstream 和 Windows 的 path,直接传 path 会隐式转 string,涉及编码转换,容易出现中文乱码问题。
2.5 遍历目录与文件
<filesystem>头文件提供directory_iterator类型用于遍历单层目录 ;
recursive_directory_iterator类用于递归遍历目录及其所有子目录 。向迭代器构造函数传入一个 path 对象完成构造后,迭代器会指向该路径下第一个 directory_entry。调用默认构造函数即可得到尾后迭代器(end 迭代器)。遍历目录时,会遇到多种类型的条目:目录、普通文件、符号链接、套接字文件等。
directory_iterator迭代返回的元素类型是directory_entry。
核心概念:
- directory_iterator:单层遍历,不进入子文件夹;
- recursive_directory_iterator:深度递归,自动遍历所有子目录;
- directory_entry :目录条目包装对象,封装单条文件/目录信息;
- .path():获取对应的 std::filesystem::path;
- .is_directory() / .is_regular_file() / .is_symlink() 判断类型;
- .file_size() 获取文件大小(普通文件)
如下示例代码使用 directory_iterator() 进行单层遍历:
cpp
#include <filesystem>
#include <iostream>
namespace fs = std::filesystem;
int main()
{
fs::path dir = R"(C:\Users\Public)";
// 单层遍历
for (const auto& entry : fs::directory_iterator(dir))
{
std::cout << entry.path() << '\n';
}
// 递归遍历所有子目录
/*
for (const auto& entry : fs::recursive_directory_iterator(dir))
{
std::cout << entry.path() << '\n';
}
*/
return 0;
}
运行结果:
补充注意点(MSVC 文档重点)
- 迭代器会自动跳过
.和..这两个特殊目录项;- 访问条目属性(is_directory 等)时,底层会做一次文件系统 stat 调用;大批量遍历有性能开销;
- 遇到权限不足、目录被删除等异常,默认会抛出 filesystem_error;可使用
directory_iterator(path, fs::directory_options::skip_permission_denied)跳过无权限目录;




