C++ `std::filesystem` 深度:跨平台文件系统操作

好的,各位听众,大家好!今天我们来聊聊C++标准库里一个非常实用,但又经常被忽略的家伙——std::filesystem。 别害怕,虽然名字听起来像个复杂的操作系统内核模块,但实际上它只是一个帮你轻松搞定各种文件系统操作的工具箱。

开场白:为什么我们需要std::filesystem

在C++17之前,如果你想在代码里操作文件,比如创建目录、读取文件大小、判断文件是否存在,那你可能需要用到一些平台相关的API,比如Windows下的CreateDirectory和Linux下的mkdir。 这就意味着你的代码必须针对不同的操作系统进行编译和修改,简直是噩梦!

std::filesystem横空出世,就是为了解决这个问题。它提供了一套跨平台的API,让你用一套代码就能在不同的操作系统上执行文件系统操作。 简直是程序员的福音!

std::filesystem 的核心概念

要理解std::filesystem,我们需要先了解几个核心概念:

  • path: 这是std::filesystem里最重要的类,它代表文件系统中的路径。 路径可以是绝对路径(比如/home/user/documents)或者相对路径(比如./data.txt)。
  • file_status: 这个类描述了文件或目录的状态,比如它是存在还是不存在,是普通文件还是目录,等等。
  • directory_entry: 这个类代表目录中的一个条目,可以是文件、目录或者其他类型的文件系统对象。

基本操作:牛刀小试

让我们从一些基本的文件系统操作开始,看看std::filesystem是如何简化这些任务的。

1. 包含头文件

首先,我们需要包含filesystem头文件:

#include <filesystem>
#include <iostream>

namespace fs = std::filesystem; // 为了方便,我们使用命名空间别名

2. 创建目录

创建目录非常简单,只需要调用fs::create_directory()函数:

int main() {
    fs::path dir_path = "my_new_directory"; // 创建一个名为 "my_new_directory" 的目录
    if (!fs::exists(dir_path)) {
        if (fs::create_directory(dir_path)) {
            std::cout << "目录创建成功!" << std::endl;
        } else {
            std::cerr << "目录创建失败!" << std::endl;
        }
    } else {
        std::cout << "目录已经存在!" << std::endl;
    }
    return 0;
}

3. 判断文件/目录是否存在

fs::exists()函数可以判断文件或目录是否存在:

fs::path file_path = "my_file.txt";
if (fs::exists(file_path)) {
    std::cout << "文件存在!" << std::endl;
} else {
    std::cout << "文件不存在!" << std::endl;
}

4. 获取文件大小

fs::file_size()函数可以获取文件的大小(以字节为单位):

fs::path file_path = "my_file.txt";
if (fs::exists(file_path)) {
    std::uintmax_t file_size = fs::file_size(file_path);
    std::cout << "文件大小:" << file_size << " 字节" << std::endl;
} else {
    std::cout << "文件不存在!" << std::endl;
}

5. 删除文件/目录

fs::remove()函数可以删除文件或目录。注意,如果目录不为空,remove()函数会失败。 如果要删除非空目录,需要使用fs::remove_all()函数:

fs::path file_path = "my_file.txt";
if (fs::exists(file_path)) {
    if (fs::remove(file_path)) {
        std::cout << "文件删除成功!" << std::endl;
    } else {
        std::cerr << "文件删除失败!" << std::endl;
    }
} else {
    std::cout << "文件不存在!" << std::endl;
}

fs::path dir_path = "my_new_directory";
if (fs::exists(dir_path)) {
    if (fs::remove_all(dir_path)) {
        std::cout << "目录删除成功!" << std::endl;
    } else {
        std::cerr << "目录删除失败!" << std::endl;
    }
} else {
    std::cout << "目录不存在!" << std::endl;
}

6. 文件复制

fs::copy() 函数可以复制文件,它有多种重载形式,可以控制复制的行为,例如是否覆盖已存在的文件。

fs::path source_file = "source.txt";
fs::path destination_file = "destination.txt";

try {
    fs::copy(source_file, destination_file, fs::copy_options::overwrite_existing);
    std::cout << "文件复制成功!" << std::endl;
} catch (const fs::filesystem_error& e) {
    std::cerr << "文件复制失败:" << e.what() << std::endl;
}

进阶操作:探索文件系统

除了基本操作,std::filesystem还提供了一些更高级的功能,比如遍历目录、获取文件状态等。

1. 遍历目录

fs::directory_iterator 可以用来遍历目录中的所有条目:

fs::path dir_path = "."; // 当前目录
for (const auto& entry : fs::directory_iterator(dir_path)) {
    std::cout << entry.path() << std::endl;
}

fs::recursive_directory_iterator 可以递归地遍历目录及其子目录:

fs::path dir_path = "."; // 当前目录
for (const auto& entry : fs::recursive_directory_iterator(dir_path)) {
    std::cout << entry.path() << std::endl;
}

2. 获取文件状态

fs::status() 函数可以获取文件或目录的状态:

fs::path file_path = "my_file.txt";
fs::file_status status = fs::status(file_path);

if (fs::exists(status)) {
    if (fs::is_regular_file(status)) {
        std::cout << "这是一个普通文件" << std::endl;
    } else if (fs::is_directory(status)) {
        std::cout << "这是一个目录" << std::endl;
    } else {
        std::cout << "这是一个其他类型的文件系统对象" << std::endl;
    }
} else {
    std::cout << "文件不存在" << std::endl;
}

3. 获取路径信息

path 类提供了一些方法来获取路径的各个部分:

  • filename(): 获取文件名(不包含路径)
  • parent_path(): 获取父目录路径
  • extension(): 获取文件扩展名
  • stem(): 获取文件名(不包含扩展名)
  • is_absolute(): 判断是否是绝对路径
fs::path file_path = "/home/user/documents/my_file.txt";

std::cout << "文件名: " << file_path.filename() << std::endl;
std::cout << "父目录: " << file_path.parent_path() << std::endl;
std::cout << "扩展名: " << file_path.extension() << std::endl;
std::cout << "文件名 (不含扩展名): " << file_path.stem() << std::endl;
std::cout << "是否是绝对路径: " << file_path.is_absolute() << std::endl;

4. 创建符号链接

fs::create_symlink() 函数可以创建符号链接。

fs::path target_file = "existing_file.txt";
fs::path symlink = "link_to_file.txt";

try {
    fs::create_symlink(target_file, symlink);
    std::cout << "符号链接创建成功!" << std::endl;
} catch (const fs::filesystem_error& e) {
    std::cerr << "符号链接创建失败:" << e.what() << std::endl;
}

5. 获取最后修改时间

fs::last_write_time() 函数可以获取文件的最后修改时间。

fs::path file_path = "my_file.txt";

try {
    auto last_write_time = fs::last_write_time(file_path);
    std::time_t cftime = std::chrono::system_clock::to_time_t(last_write_time);
    std::cout << "最后修改时间:" << std::ctime(&cftime) << std::endl;
} catch (const fs::filesystem_error& e) {
    std::cerr << "获取最后修改时间失败:" << e.what() << std::endl;
}

异常处理:小心驶得万年船

文件系统操作可能会失败,比如文件不存在、权限不足等等。 std::filesystem 使用异常来报告错误。 因此,在使用std::filesystem时,一定要进行异常处理,否则程序可能会崩溃。

try {
    fs::path file_path = "non_existent_file.txt";
    fs::file_size(file_path); // 可能会抛出异常
} catch (const fs::filesystem_error& e) {
    std::cerr << "文件系统错误:" << e.what() << std::endl;
}

路径操作:让路径更灵活

std::filesystem::path 类不仅仅是一个简单的字符串,它还提供了一些方法来操作路径:

  • append()/=: 追加路径
  • remove_filename(): 移除文件名
  • replace_extension(): 替换扩展名
  • make_preferred(): 转换为当前操作系统偏好的路径格式
fs::path base_path = "/home/user";
base_path /= "documents"; // 追加路径
base_path /= "my_file.txt";

std::cout << "完整路径: " << base_path << std::endl;

base_path.remove_filename(); // 移除文件名

std::cout << "移除文件名后的路径: " << base_path << std::endl;

base_path /= "another_file.txt";
base_path.replace_extension(".cpp"); // 替换扩展名

std::cout << "替换扩展名后的路径: " << base_path << std::endl;

#ifdef _WIN32
    base_path.make_preferred(); // 在Windows上将 / 转换为 
    std::cout << "转换为Windows偏好格式后的路径: " << base_path << std::endl;
#endif

权限管理:(C++20新增)

C++20 引入了权限管理,通过 fs::permissions() 函数可以设置和查询文件权限。

#if __cpp_lib_file_permissions >= 201707L // 检查编译器是否支持文件权限

fs::path file_path = "my_file.txt";

try {
    // 设置文件权限为所有者读写,同组用户只读,其他用户无权限
    fs::permissions(file_path,
                    fs::perms::owner_read | fs::perms::owner_write | fs::perms::group_read,
                    fs::perm_options::replace);

    // 获取文件权限
    fs::perms current_permissions = fs::status(file_path).permissions();

    std::cout << "文件权限: " << std::hex << static_cast<int>(current_permissions) << std::endl;
} catch (const fs::filesystem_error& e) {
    std::cerr << "权限设置失败:" << e.what() << std::endl;
}

#else
    std::cout << "当前编译器不支持文件权限管理。" << std::endl;
#endif

总结:std::filesystem 的优势

  • 跨平台: 一套代码,到处运行。
  • 易于使用: API简洁明了,容易上手。
  • 安全: 使用异常处理,避免程序崩溃。
  • 功能强大: 提供丰富的文件系统操作功能。

注意事项

  • 权限: 文件系统操作需要相应的权限。
  • 路径分隔符: 不同操作系统使用不同的路径分隔符(Windows: ,Linux/macOS: /)。 std::filesystem 会自动处理这些差异。
  • 符号链接: 处理符号链接时要小心,避免循环引用。
  • 并发: 多个线程同时操作文件系统可能会导致问题,需要进行同步。

实战案例:日志文件管理

让我们用一个实际的例子来展示std::filesystem的威力。 假设我们需要编写一个日志文件管理程序,它可以:

  1. 每天创建一个新的日志文件。
  2. 如果日志文件超过一定大小,就进行归档。
  3. 定期删除过期的日志文件。
#include <iostream>
#include <fstream>
#include <filesystem>
#include <chrono>
#include <ctime>

namespace fs = std::filesystem;

// 配置参数
const std::string log_directory = "logs";
const std::uintmax_t max_log_file_size = 1024 * 1024; // 1MB
const int log_file_retention_days = 30;

// 获取当前日期字符串 (YYYY-MM-DD)
std::string get_current_date_string() {
    auto now = std::chrono::system_clock::now();
    std::time_t now_c = std::chrono::system_clock::to_time_t(now);
    std::tm now_tm;
    localtime_r(&now_c, &now_tm); // 使用线程安全的 localtime_r

    char date_str[11]; // YYYY-MM-DD
    strftime(date_str, sizeof(date_str), "%Y-%m-%d", &now_tm);
    return std::string(date_str);
}

// 获取当前日志文件路径
fs::path get_current_log_file_path() {
    return fs::path(log_directory) / (get_current_date_string() + ".log");
}

// 归档日志文件
void archive_log_file(const fs::path& log_file_path) {
    fs::path archive_path = fs::path(log_directory) / "archive";
    fs::create_directory(archive_path); // 创建 archive 目录

    fs::path archived_file_path = archive_path / (log_file_path.filename().string() + ".gz");

    // 使用系统命令进行压缩 (需要系统支持 gzip)
    std::string command = "gzip "" + log_file_path.string() + "" -c > "" + archived_file_path.string() + """;
    int result = system(command.c_str()); // 执行系统命令

    if (result == 0) {
        fs::remove(log_file_path); // 删除原始日志文件
        std::cout << "日志文件 " << log_file_path << " 归档成功." << std::endl;
    } else {
        std::cerr << "日志文件 " << log_file_path << " 归档失败." << std::endl;
    }
}

// 删除过期的日志文件
void delete_old_log_files() {
    auto now = std::chrono::system_clock::now();
    auto retention_duration = std::chrono::hours(24 * log_file_retention_days);
    auto cutoff_time = now - retention_duration;

    for (const auto& entry : fs::directory_iterator(log_directory)) {
        if (fs::is_regular_file(entry.status()) && entry.path().extension() == ".log") {
            try {
                auto last_write_time = fs::last_write_time(entry.path());
                if (last_write_time <= cutoff_time) {
                    fs::remove(entry.path());
                    std::cout << "删除过期日志文件: " << entry.path() << std::endl;
                }
            } catch (const fs::filesystem_error& e) {
                std::cerr << "删除日志文件 " << entry.path() << " 失败: " << e.what() << std::endl;
            }
        }
    }

    // 删除 archive 目录下的过期归档文件
    fs::path archive_path = fs::path(log_directory) / "archive";
    if (fs::exists(archive_path) && fs::is_directory(archive_path)) {
        for (const auto& entry : fs::directory_iterator(archive_path)) {
            if (fs::is_regular_file(entry.status()) && entry.path().extension() == ".gz") {
                try {
                    auto last_write_time = fs::last_write_time(entry.path());
                    if (last_write_time <= cutoff_time) {
                        fs::remove(entry.path());
                        std::cout << "删除过期归档文件: " << entry.path() << std::endl;
                    }
                } catch (const fs::filesystem_error& e) {
                    std::cerr << "删除归档文件 " << entry.path() << " 失败: " << e.what() << std::endl;
                }
            }
        }
    }
}

int main() {
    // 创建日志目录
    fs::create_directory(log_directory);

    // 获取当前日志文件路径
    fs::path current_log_file_path = get_current_log_file_path();

    // 检查日志文件大小,如果超过限制则进行归档
    if (fs::exists(current_log_file_path) && fs::file_size(current_log_file_path) > max_log_file_size) {
        archive_log_file(current_log_file_path);
    }

    // 写入日志信息
    std::ofstream log_file(current_log_file_path, std::ios::app);
    if (log_file.is_open()) {
        auto now = std::chrono::system_clock::now();
        std::time_t now_c = std::chrono::system_clock::to_time_t(now);
        std::tm now_tm;
        localtime_r(&now_c, &now_tm);
        char timestamp[26]; // YYYY-MM-DD HH:MM:SS.milliseconds
        strftime(timestamp, sizeof(timestamp), "%Y-%m-%d %H:%M:%S", &now_tm);

        log_file << "[" << timestamp << "] Log message: This is a test log message." << std::endl;
        log_file.close();
        std::cout << "写入日志信息到 " << current_log_file_path << std::endl;
    } else {
        std::cerr << "无法打开日志文件 " << current_log_file_path << std::endl;
    }

    // 删除过期的日志文件
    delete_old_log_files();

    return 0;
}

这个例子展示了如何使用std::filesystem来管理日志文件,包括创建目录、获取文件大小、删除文件等等。

总结

std::filesystem 是一个非常强大的工具,它可以让你轻松地进行跨平台的文件系统操作。 掌握它,你的C++代码将会更加健壮、易于维护,并且更具有可移植性。 希望今天的讲解对你有所帮助! 谢谢大家!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注