跳到主要内容

C++ 文件系统(C++17)

简介

在C++17标准中,引入了一个全新的文件系统库std::filesystem,它提供了一套跨平台的文件和目录操作接口。在此之前,C++程序员通常需要依赖操作系统特定的API或第三方库来进行文件系统操作,这使得代码的可移植性变得很差。现在,通过std::filesystem,我们可以用统一的方式来处理文件和目录,而无需担心不同操作系统的差异。

备注

使用std::filesystem需要C++17或更高版本的编译器支持。在编译时,可能还需要链接特定库(通常是-lstdc++fs-lc++fs)。

包含头文件

要使用C++17文件系统库,首先需要包含相应的头文件:

cpp
#include <filesystem>

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

路径操作

std::filesystem::path类是文件系统库的核心,它用于表示文件或目录的路径。

创建和处理路径

cpp
#include <iostream>
#include <filesystem>
namespace fs = std::filesystem;

int main() {
// 创建路径
fs::path myPath = "C:/Users/Documents/file.txt";

// 路径组件
std::cout << "文件名: " << myPath.filename() << std::endl;
std::cout << "扩展名: " << myPath.extension() << std::endl;
std::cout << "不带扩展名的文件名: " << myPath.stem() << std::endl;
std::cout << "父路径: " << myPath.parent_path() << std::endl;

// 路径修改
fs::path newPath = myPath.replace_extension(".cpp");
std::cout << "修改扩展名后: " << newPath << std::endl;

return 0;
}

输出:

文件名: file.txt
扩展名: .txt
不带扩展名的文件名: file
父路径: C:/Users/Documents
修改扩展名后: C:/Users/Documents/file.cpp

路径拼接

可以使用/操作符或append()concat()方法来拼接路径:

cpp
fs::path basePath = "C:/Projects";
fs::path fullPath = basePath / "MyProject" / "src" / "main.cpp";
std::cout << "完整路径: " << fullPath << std::endl;

// 等价于
fs::path samePath = basePath;
samePath.append("MyProject").append("src").append("main.cpp");
std::cout << "相同路径: " << samePath << std::endl;

输出:

完整路径: C:/Projects/MyProject/src/main.cpp
相同路径: C:/Projects/MyProject/src/main.cpp

文件和目录检查

检查文件或目录是否存在

cpp
#include <iostream>
#include <filesystem>
namespace fs = std::filesystem;

int main() {
fs::path p = "example.txt";

if (fs::exists(p)) {
std::cout << p << " 存在" << std::endl;

if (fs::is_regular_file(p))
std::cout << p << " 是一个普通文件" << std::endl;
if (fs::is_directory(p))
std::cout << p << " 是一个目录" << std::endl;
} else {
std::cout << p << " 不存在" << std::endl;
}

return 0;
}

获取文件信息

cpp
if (fs::exists("example.txt")) {
// 获取文件大小
std::uintmax_t size = fs::file_size("example.txt");
std::cout << "文件大小: " << size << " 字节" << std::endl;

// 获取最后修改时间
fs::file_time_type lastModified = fs::last_write_time("example.txt");
std::time_t time = fs::file_time_type::clock::to_time_t(lastModified);
std::cout << "最后修改时间: " << std::ctime(&time);
}

目录操作

创建和删除目录

cpp
#include <iostream>
#include <filesystem>
namespace fs = std::filesystem;

int main() {
// 创建目录
fs::path dirPath = "new_directory";

if (fs::create_directory(dirPath)) {
std::cout << "目录创建成功" << std::endl;
}

// 创建多级目录
fs::path nestedDir = "parent/child/grandchild";
if (fs::create_directories(nestedDir)) {
std::cout << "多级目录创建成功" << std::endl;
}

// 删除目录
if (fs::remove(dirPath)) {
std::cout << "目录删除成功" << std::endl;
}

// 递归删除目录及其内容
std::uintmax_t deletedCount = fs::remove_all("parent");
std::cout << "共删除了 " << deletedCount << " 个文件和目录" << std::endl;

return 0;
}

遍历目录内容

cpp
#include <iostream>
#include <filesystem>
namespace fs = std::filesystem;

int main() {
fs::path dirPath = "."; // 当前目录

// 方法1:使用directory_iterator遍历目录内的直接子项
std::cout << "当前目录内容:" << std::endl;
for (const auto& entry : fs::directory_iterator(dirPath)) {
std::cout << " " << entry.path().filename();
if (fs::is_directory(entry))
std::cout << " [目录]";
std::cout << std::endl;
}

// 方法2:使用recursive_directory_iterator递归遍历所有子目录
std::cout << "\n所有子目录和文件:" << std::endl;
for (const auto& entry : fs::recursive_directory_iterator(dirPath)) {
// 计算当前项的深度以进行缩进
int depth = std::distance(fs::recursive_directory_iterator{},
fs::recursive_directory_iterator{entry});

std::string indent(depth * 2, ' ');
std::cout << indent << entry.path().filename();
if (fs::is_directory(entry))
std::cout << " [目录]";
std::cout << std::endl;
}

return 0;
}

文件操作

复制和移动文件

cpp
#include <iostream>
#include <filesystem>
namespace fs = std::filesystem;

int main() {
// 复制文件
try {
fs::path source = "source.txt";
fs::path destination = "destination.txt";

// 创建一个示例文件
std::ofstream(source.c_str()) << "Hello, filesystem!";

// 基本复制
fs::copy_file(source, destination);
std::cout << "文件复制成功" << std::endl;

// 带选项的复制 (如果目标已存在则覆盖)
fs::copy_file(source, destination, fs::copy_options::overwrite_existing);

// 移动文件
fs::path newLocation = "moved.txt";
fs::rename(destination, newLocation);
std::cout << "文件移动成功" << std::endl;

// 删除文件
fs::remove(source);
fs::remove(newLocation);
std::cout << "文件删除成功" << std::endl;
}
catch (const fs::filesystem_error& e) {
std::cerr << "文件系统错误: " << e.what() << std::endl;
}

return 0;
}

复制整个目录树

cpp
try {
fs::path sourceDir = "source_dir";
fs::path destDir = "dest_dir";

// 创建示例目录
fs::create_directory(sourceDir);
std::ofstream(sourceDir / "file1.txt") << "File 1";
fs::create_directory(sourceDir / "subdir");
std::ofstream(sourceDir / "subdir" / "file2.txt") << "File 2";

// 递归复制整个目录
fs::copy(sourceDir, destDir, fs::copy_options::recursive);
std::cout << "目录树复制成功" << std::endl;
}
catch (const fs::filesystem_error& e) {
std::cerr << "文件系统错误: " << e.what() << std::endl;
}

错误处理

std::filesystem中的大多数函数都有两个版本:一个会抛出异常,另一个接受一个error_code参数来报告错误。

cpp
#include <iostream>
#include <filesystem>
namespace fs = std::filesystem;

int main() {
// 方法1:使用try-catch处理异常
try {
fs::path p = "nonexistent_directory";
for (const auto& entry : fs::directory_iterator(p)) {
// 这段代码不会执行,因为目录不存在
std::cout << entry.path() << std::endl;
}
}
catch (const fs::filesystem_error& e) {
std::cerr << "捕获到文件系统异常: " << e.what() << std::endl;
std::cerr << "错误码: " << e.code().value() << std::endl;
std::cerr << "路径1: " << e.path1() << std::endl;
std::cerr << "路径2: " << e.path2() << std::endl;
}

// 方法2:使用error_code
fs::path p = "another_nonexistent_dir";
std::error_code ec;
fs::is_directory(p, ec);

if (ec) {
std::cerr << "发生错误: " << ec.message() << std::endl;
std::cerr << "错误码: " << ec.value() << std::endl;
}

return 0;
}

实际应用案例

案例1:批量重命名文件

以下是一个实用的例子,展示如何使用std::filesystem批量重命名目录中的所有文本文件,给它们添加日期前缀:

cpp
#include <iostream>
#include <filesystem>
#include <string>
#include <ctime>
#include <iomanip>
#include <sstream>

namespace fs = std::filesystem;

int main() {
// 获取当前日期
auto now = std::chrono::system_clock::now();
auto time_now = std::chrono::system_clock::to_time_t(now);
std::tm tm = *std::localtime(&time_now);

std::ostringstream oss;
oss << std::put_time(&tm, "%Y%m%d_");
std::string datePrefix = oss.str();

fs::path dirPath = "."; // 当前目录
int count = 0;

for (const auto& entry : fs::directory_iterator(dirPath)) {
if (entry.path().extension() == ".txt") {
fs::path oldPath = entry.path();
fs::path newPath = oldPath.parent_path() / (datePrefix + oldPath.filename().string());

if (!fs::exists(newPath)) {
fs::rename(oldPath, newPath);
std::cout << "重命名: " << oldPath << " -> " << newPath << std::endl;
count++;
}
}
}

std::cout << "完成重命名 " << count << " 个文件" << std::endl;
return 0;
}

案例2:递归计算目录大小

这个例子展示如何递归计算目录的总大小:

cpp
#include <iostream>
#include <filesystem>
#include <string>

namespace fs = std::filesystem;

// 格式化文件大小为可读形式
std::string formatFileSize(uintmax_t size) {
const char* units[] = {"B", "KB", "MB", "GB", "TB"};
int unitIndex = 0;
double fileSize = static_cast<double>(size);

while (fileSize >= 1024 && unitIndex < 4) {
fileSize /= 1024;
unitIndex++;
}

char buffer[64];
if (unitIndex == 0) {
std::snprintf(buffer, sizeof(buffer), "%llu %s", static_cast<unsigned long long>(size), units[unitIndex]);
} else {
std::snprintf(buffer, sizeof(buffer), "%.2f %s", fileSize, units[unitIndex]);
}
return buffer;
}

// 递归计算目录大小
uintmax_t calculateDirectorySize(const fs::path& dirPath) {
uintmax_t totalSize = 0;

if (!fs::exists(dirPath) || !fs::is_directory(dirPath)) {
return 0;
}

for (const auto& entry : fs::recursive_directory_iterator(dirPath)) {
if (fs::is_regular_file(entry)) {
std::error_code ec;
uintmax_t fileSize = fs::file_size(entry, ec);
if (!ec) {
totalSize += fileSize;
}
}
}

return totalSize;
}

int main(int argc, char* argv[]) {
fs::path targetDir = argc > 1 ? argv[1] : ".";

try {
if (!fs::is_directory(targetDir)) {
std::cerr << targetDir << " 不是一个有效的目录" << std::endl;
return 1;
}

std::cout << "计算目录 " << fs::absolute(targetDir) << " 的大小..." << std::endl;
uintmax_t size = calculateDirectorySize(targetDir);
std::cout << "总大小: " << formatFileSize(size) << std::endl;

// 输出子目录大小
std::cout << "\n各子目录大小:" << std::endl;
for (const auto& entry : fs::directory_iterator(targetDir)) {
if (fs::is_directory(entry)) {
uintmax_t dirSize = calculateDirectorySize(entry);
std::cout << entry.path().filename() << ": " << formatFileSize(dirSize) << std::endl;
}
}
}
catch (const fs::filesystem_error& e) {
std::cerr << "文件系统错误: " << e.what() << std::endl;
return 1;
}
catch (const std::exception& e) {
std::cerr << "错误: " << e.what() << std::endl;
return 1;
}

return 0;
}

总结

C++17文件系统库提供了一套强大且跨平台的工具,用于处理文件和目录操作。主要功能包括:

  1. 路径管理和操作(std::filesystem::path
  2. 文件和目录的检查和信息获取
  3. 目录的创建、删除和遍历
  4. 文件的复制、移动和删除
  5. 错误处理和报告

通过使用文件系统库,你可以编写跨平台的代码,避免了使用特定平台API的麻烦,提高了程序的可移植性和可维护性。

最佳实践
  • 始终检查文件操作的结果(是否成功)
  • 使用适当的错误处理机制(异常或错误代码)
  • 对于性能敏感的应用,注意缓存文件系统操作的结果
  • 在处理大型目录结构时,考虑使用递归算法的效率和堆栈限制

练习

  1. 编写一个程序,列出指定目录中的所有文件和子目录,包括它们的大小和最后修改时间。
  2. 创建一个文件备份工具,将一个目录中的所有文件复制到另一个目录,并在目标目录中已存在同名文件时添加序号后缀。
  3. 实现一个简单的文件查找功能,根据文件名或扩展名在目录树中查找匹配的文件。
  4. 编写一个程序,统计指定目录中每种文件类型(根据文件扩展名)的数量和总大小。
  5. 创建一个目录监控工具,检测指定目录的变化并报告新增、删除或修改的文件。

参考资源