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文件系统库提供了一套强大且跨平台的工具,用于处理文件和目录操作。主要功能包括:
- 路径管理和操作(
std::filesystem::path
) - 文件和目录的检查和信息获取
- 目录的创建、删除和遍历
- 文件的复制、移动和删除
- 错误处理和报告
通过使用文件系统库,你可以编写跨平台的代码,避免了使用特定平台API的麻烦,提高了程序的可移植性和可维护性。
最佳实践
- 始终检查文件操作的结果(是否成功)
- 使用适当的错误处理机制(异常或错误代码)
- 对于性能敏感的应用,注意缓存文件系统操作的结果
- 在处理大型目录结构时,考虑使用递归算法的效率和堆栈限制
练习
- 编写一个程序,列出指定目录中的所有文件和子目录,包括它们的大小和最后修改时间。
- 创建一个文件备份工具,将一个目录中的所有文件复制到另一个目录,并在目标目录中已存在同名文件时添加序号后缀。
- 实现一个简单的文件查找功能,根据文件名或扩展名在目录树中查找匹配的文件。
- 编写一个程序,统计指定目录中每种文件类型(根据文件扩展名)的数量和总大小。
- 创建一个目录监控工具,检测指定目录的变化并报告新增、删除或修改的文件。
参考资源
- C++ 参考手册:
<filesystem>
- Microsoft C++ 标准库文件系统参考
- Boost.Filesystem 库 (C++17前的替代方案,与std::filesystem非常相似)