【STL】文件系统导航


本文介绍标准库中的跨平台的文件导航系统,<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。
    说明:
  1. 标准背景
    • 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 特有的文件类型接口。
  2. is_fifo 知识点
    • fifo:FIFO 命名管道,是 POSIX/Unix-like 系统的特殊文件类型,用于进程间通信。
    • Windows 有自己的命名管道机制,但不属于 POSIX FIFO 文件模型;std::filesystem 不识别 Windows 命名管道,因此 is_fifo() 在 Windows 总是返回 false。
    • Linux/macOS 下,如果路径指向 FIFO 管道,is_fifo() 返回 true。
  3. 跨平台开发注意点
    • std::filesystem 为了兼容 POSIX,提供若干 Unix 专属 API,Windows 下会给出退化结果(如下表):
    • Windows 没有块设备文件、字符设备文件、socket 文件、FIFO 管道 这些 POSIX 文件类型。写跨平台代码时,不能假设这些接口一定能检测到对应对象。
函数 说明(Windows 行为)
is_fifo() 恒返回 false
is_socket() 恒返回 false
is_block_file()、is_character_file() 恒返回 false
  1. 库主要能力(文件系统导航)

    • 遍历目录:directory_iterator、recursive_directory_iterator
    • 路径处理:path 类,路径拼接、分解、获取文件名 / 后缀、根路径
    • 文件操作:创建 / 复制 / 重命名 / 删除文件、目录;读取符号链接;查询文件大小、修改时间、权限等。
  2. 代码提示

    • Windows 下如需使用命名管道 IPC,应当直接调用 Win32 原生 API,不要依赖 std::filesystem 的 is_fifo 接口。
      可以使用 <filesystem> 应用程序接口完成以下任务:
  3. 遍历指定路径下的文件与目录;

  4. 获取文件信息,包括创建时间、文件大小、文件扩展名以及根目录;

  5. 拼接、拆解以及比较路径;

  6. 创建、复制和删除目录;

  7. 复制和删除文件;
    各项任务对应的核心 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

重点解析

  1. Windows 平台编码特性
    • Windows 底层 API 全部使用 Unicode(UTF-16),std::filesystem::path 在 Windows 上 value_type 为 wchar_t;POSIX(Linux/macOS)下为 char。
    • 传入 UTF-8 窄字符串、宽字符串,path 构造函数会自动做编码转换,使用者不必手动处理编码转换。
  2. 路径分隔符的兼容性
    • 正斜杠 / 可跨平台书写:写代码时优先用 / ,path 会在 Windows 内部自动转换成本地的反斜杠 \,Linux 保持 / 不变。
    • Windows 原生反斜杠 \\ 同样被支持;普通字符串字面量必须写双反斜杠做转义。
    • C++11 原始字符串字面量 R"(...)",不需要转义反斜杠,适合写 Windows 路径。
      • ⚠️注:只在 path 类中 / 才会被自动转换;普通 std::string 字符串不会自动转换分隔符。
  3. /、/= vs +、+=(append vs concat)
运算符 对应成员函数 行为说明
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"
  1. 易错点
    • 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);
}

运行结果:


详细解析

  1. 核心原则
    • 路径解析只处理字符串文本,不会访问硬盘。
    • 即使路径完全不存在,root_name()、filename()、stem() 这类函数照样可以返回结果。
    • 如果你需要读取文件大小、修改时间等磁盘实体信息,则需要调用 file_size、last_write_time 等独立函数。
  2. 迭代器遍历规则
    • 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 文件名
  1. 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 扩展名,包含开头的点号 .
  1. 几个容易踩坑的细节
    • 扩展名带小数点 :extension() 返回值包含 . ,不要自己再加小数点;
    • 多重点号文件名 :archive.tar.gz
      • filename() → archive.tar.gz
      • stem() → archive.tar
      • extension() → .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);
}

运行结果:


详细原理

  1. 底层比较原理
    • operator < / > / == / != 执行字典序字符串对比,并不是查询磁盘判断两个路径是否指向同一个物理文件。
      • ⚠️重要区分 :
        • path 对象比较:文本层面字典序对比,不访问磁盘;
        • 判断两个路径是否对应磁盘上同一个文件:使用 fs::equivalent(a,b),该函数会访问文件系统;
  2. 分隔符规范化做了什么
    • 规范化:/ 自动转为 Windows 本地分隔符 \ ;
    • 但是!不会自动追加末尾斜杠;
cpp 复制代码
// 二者字符串并不相等,p0 < p1 结果为 true
p0 = C:\Documents
p1 = C:\Documents\
  1. 运算符清单
    • 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);
}

运行结果:


详细解析:

  1. 隐式转换规则
    • Windows 平台:path 的底层字符类型是 wchar_t,隐式转换优先得到 std::wstring。
    • Linux/macOS平台 :path 底层字符类型是 char,隐式转换优先得到 std::string。
    • 代码示例中使用 wofstream / wifstream(宽字符文件流),接收 path 参数时自动隐式转为 wstring。
  2. 显式获取字符串的成员函数(推荐,避免隐式转换带来歧义)
    • 最佳实践:重要场景尽量用显式调用 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 通用格式宽字符串,分隔符统一为 /
  1. 示例代码逻辑简述
    1. 使用宽字符字符串构造 path 对象;
    2. 通过 /= 拼接文件名 NewFile.txt;
    3. 直接把 filePath 传入 wofstream 构造函数,path 隐式转为 wstring,打开文件;
    4. 写入两行文本,关闭文件;
    5. 使用 wifstream 打开文件,同样传入 path 对象触发隐式转换;
    6. 循环读取每行,输出到控制台;
  2. 注意事项
    • 隐式转换仅转换字符串表达,不会校验文件是否真实存在;
    • 区分 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) 跳过无权限目录;
相关推荐
All for pursuit.1 小时前
【动态规划-21】312.戳气球
数据结构·c++·算法·leetcode·动态规划
栈帧之外1 小时前
【C++】STL之set
开发语言·c++
zhanghaha13142 小时前
C++构造函数的一些其他应用(逐词逐句保姆讲解)
c++
jimy13 小时前
c++的 Type Traits (二、更详细版本)
开发语言·c++
半杯咖啡半行码5 小时前
C++11核心特性详解:从C++11到C++20 的现代C++指南
开发语言·c++
All for pursuit.6 小时前
【位运算-3】136.只出现一次的数字
数据结构·c++·算法·leetcode
C++ 老炮儿的技术栈8 小时前
std::atomic 是什么
linux·c语言·网络·c++·visual studio
All for pursuit.8 小时前
【完结】LeetCodeHot100题 详细解题过程及代码汇总
数据结构·c++·算法·leetcode
重生之小比特9 小时前
【算法拾光】双指针|移动零 + 复写零
数据结构·c++·算法