公司动态

现代C++:浅谈文件与目录操作

📅 2026/8/2 12:30:22
现代C++:浅谈文件与目录操作
现代C浅谈文件与目录操作这个仓库已经开源现代化 CC11/14/17/20从基础到进阶的系统教程都在这里力争做一条完备的现代 C 学习路径欢迎各位大佬前来参观喜欢的话点个⭐Github 一键直达: git clone https://github.com/Awesome-Embedded-Learning-Studio/Tutorial_AwesomeModernCPP看看超酷的新网站https://awesome-embedded-learning-studio.github.io/Tutorial_AwesomeModernCPP/上一篇我们学会了用std::filesystem::path处理路径的语法问题——构造、分解、修改、比较全是纯计算不碰磁盘。这一篇我们开始动真格的用filesystem库直接操作文件系统——检查文件是否存在、创建目录、复制文件、删除文件、查询权限和磁盘空间。和上一篇一样我们的环境是 C17GCC 13 / Clang 15 / MSVC 2022。头文件filesystem命名空间namespace fs std::filesystem;。文件状态查询它存在吗它是什么类型文件系统操作的第一步通常是先看看这个路径上到底有什么。filesystem提供了一组查询函数来回答这个问题。exists路径存在吗fs::exists(p)检查给定路径是否在文件系统上存在。它可以接受path对象也可以接受directory_entry我们下一篇会讲。返回bool#includefilesystem#includeiostreamnamespacefsstd::filesystem;intmain(){fs::path p/usr/local/bin/gcc;if(fs::exists(p)){std::coutp 存在\n;}else{std::coutp 不存在\n;}return0;}⚠️exists()在某些情况下会抛出异常比如权限不足导致无法访问父目录。如果你不希望异常传播使用不接受std::error_code的重载版本或者用 try-catch 包裹。更好的做法是使用接受std::error_code的重载std::error_code ec;boolexistsfs::exists(p,ec);if(ec){std::cerr查询失败: ec.message()\n;}is_regular_file / is_directory / is_symlink类型判断知道路径存在之后下一步是判断它的类型。fs::is_regular_file(p)判断是否是普通文件fs::is_directory(p)判断是否是目录fs::is_symlink(p)判断是否是符号链接。还有is_block_file、is_character_file、is_fifo、is_socket、is_other等更细分的类型判断在 Linux 系统编程中偶尔会用到。fs::path p/usr/local/bin;if(fs::is_directory(p)){std::coutp 是一个目录\n;}elseif(fs::is_regular_file(p)){std::coutp 是一个普通文件\n;}elseif(fs::is_symlink(p)){std::coutp 是一个符号链接\n;}⚠️ 如果路径不存在这些函数返回false——不会抛异常。所以你不需要先exists()再判断类型直接判断就行。但要注意如果status()调用本身失败比如权限问题会抛filesystem_error异常。file_size / last_write_time / status元数据查询除了类型我们经常还需要查询文件的大小、最后修改时间和权限状态#includefilesystem#includeiostream#includechrono#includectimenamespacefsstd::filesystem;voidprint_file_info(constfs::pathp){std::error_code ec;// 文件大小字节autosizefs::file_size(p,ec);if(!ec){std::cout大小: size 字节\n;if(size1024*1024){std::cout size/(1024.0*1024.0) MB\n;}elseif(size1024){std::cout size/1024.0 KB\n;}}// 最后修改时间autoftimefs::last_write_time(p,ec);if(!ec){// C20 之前需要转换成 time_t 来显示autosctpstd::chrono::time_point_caststd::chrono::system_clock::duration(ftime-fs::file_time_type::clock::now()std::chrono::system_clock::now());autotime_t_valstd::chrono::system_clock::to_time_t(sctp);std::cout修改时间: std::ctime(time_t_val);}// 文件状态权限等autostatusfs::status(p,ec);if(!ec){std::cout类型: static_castint(status.type())\n;std::cout权限: static_castunsigned(status.permissions())\n;}}intmain(){print_file_info(/usr/local/bin/gcc);return0;}⚠️last_write_time在 C20 之前转换成可读格式有点繁琐如上所示因为file_time_type的时钟不一定是system_clock。C20 提供了更简洁的方式通过std::chrono::clock_cast但 C17 只能用上面的近似方法。在实际项目中用std::ctime做简单显示够用了只是精度可能不完全准确。创建目录fs::create_directory(p)创建一个目录——前提是父目录必须已经存在。如果父目录不存在调用会失败fs::path dir/tmp/myapp_config;if(!fs::exists(dir)){if(fs::create_directory(dir)){std::cout目录创建成功\n;}else{std::cerr目录创建失败\n;}}如果你需要创建一个多级目录比如/tmp/a/b/c而/tmp/a和/tmp/a/b都不存在用fs::create_directories(p)。它会自动创建路径中所有缺失的中间目录类似于mkdir -pfs::path deep_dir/tmp/myapp/data/cache/tmp;fs::create_directories(deep_dir);// 自动创建所有中间目录std::cout创建完成\n;create_directories是笔者用得最多的文件系统操作之一。在程序启动时确保配置目录、日志目录、缓存目录都存在这是一个非常常见的需求。用create_directories一行代码就搞定了不用手动检查每一级是否存在。⚠️create_directory在目录已经存在时返回false但不会报错。create_directories同理——如果所有目录都存在它也返回false。所以你不应该用返回值来判断是否出错而应该用std::error_code版本。复制文件和目录fs::copy(from, to)是一个多功能复制函数。它的行为取决于from的类型和是否指定了copy_options// 默认行为// - 如果 from 是普通文件复制文件到 to// - 如果 from 是目录复制目录结构到 to不递归复制内容// - 如果 from 是符号链接复制链接本身fs::path src/tmp/source.txt;fs::path dst/tmp/dest.txt;std::error_code ec;fs::copy(src,dst,ec);if(ec){std::cerr复制失败: ec.message()\n;}copy_options控制复制行为copy_options是一个 bitmask 类型用来精细控制复制行为。常用的选项包括fs::copy_options::overwrite_existing——如果目标文件已存在覆盖它。默认情况下如果目标已存在copy会失败或跳过取决于具体操作。fs::copy_options::recursive——递归复制目录内容。如果 from 是目录会递归复制目录下所有文件和子目录。fs::copy_options::copy_symlinks——复制符号链接本身而不是跟随链接复制指向的文件。// 递归复制整个目录fs::copy(/tmp/source_dir,/tmp/dest_dir,fs::copy_options::recursive|fs::copy_options::overwrite_existing);fs::copy_file(from, to, options)是专门用于文件复制的函数。它和copy的区别在于copy_file只处理普通文件而且提供了更精细的控制。⚠️ 注意copy_file不提供原子性保证——如果复制过程中失败如磁盘空间不足、断电等目标文件可能处于部分写入状态。如需原子性应使用复制到临时文件 原子重命名模式。(参见临时文件处理部分的safe_write_file函数范例)// 不安全的文件复制无原子性保证fs::path src/data/important_config.yaml;fs::path dst/backup/important_config.yaml;std::error_code ec;fs::copy_file(src,dst,fs::copy_options::overwrite_existing,ec);// 可能性1. 如果 dst 已经存在, 复制过程中内容可能会被逐步覆盖,// 从而导致其他进程看到一个被部分复制的文件// 可能性2. 如果复制中途停电宕机, dst文件可能处于不完整甚至损坏的状态if(ec){std::cerr复制失败: ec.message()\n;}else{std::cout复制成功\n;}删除和重命名fs::remove(p)删除一个文件或空目录。如果路径不存在返回false不报错。如果路径是符号链接删除链接本身而不是目标。如果路径是非空目录删除失败fs::path temp/tmp/temp_file.txt;boolremovedfs::remove(temp);if(removed){std::cout已删除\n;}else{std::cout文件不存在或删除失败\n;}fs::remove_all(p)递归删除一个目录及其所有内容文件、子目录、符号链接。返回删除的文件数量。这是核弹级操作——一定要确认路径正确再调用fs::path temp_dir/tmp/my_temp_dir;autocountfs::remove_all(temp_dir);std::cout删除了 count 个文件/目录\n;⚠️remove_all是不可逆的操作。笔者有一次在调试时不小心把路径写错了少了一层目录差点把整个项目目录清空。幸好当时跑在测试环境里没有造成实际损失。从那以后笔者在调用remove_all之前一定会打印路径并确认。建议你也养成这个习惯。fs::rename(old_path, new_path)重命名或移动文件/目录。在大多数实现中同一文件系统上的重命名是原子操作只修改目录项不移动数据。⚠️ 注意跨文件系统的重命名通常会失败抛出异常或返回错误而不是自动执行复制删除。如需跨文件系统移动应显式使用copyremovestd::error_code ec;fs::rename(/tmp/old_name.txt,/tmp/new_name.txt,ec);if(ec){std::cerr重命名失败: ec.message()\n;}权限与磁盘空间permissions修改文件权限fs::permissions(p, prms)修改文件的权限位类似于chmod。权限用fs::perms枚举表示fs::path script/tmp/my_script.sh;// 设置为 rwxr-xr-x (755)fs::permissions(script,fs::perms::owner_read|fs::perms::owner_write|fs::perms::owner_exec|fs::perms::group_read|fs::perms::group_exec|fs::perms::others_read|fs::perms::others_exec);// 或者用 perm_options 控制修改方式fs::permissions(script,fs::perms::owner_exec,// 只修改 owner_exec 位fs::perm_options::add);// 添加不影响其他位第三个参数perm_options可以是replace替换所有权限默认行为、add添加指定权限位或remove移除指定权限位。这在只需要修改一两个权限位时比替换全部权限更方便。space查询磁盘空间fs::space(p)返回一个space_info结构体包含磁盘的容量、已用空间和可用空间autoinfofs::space(/tmp);if(info.capacity0){std::cout总容量: info.capacity/(1024.0*1024*1024) GB\n;std::cout可用空间: info.available/(1024.0*1024*1024) GB\n;std::cout剩余空间: info.free/(1024.0*1024*1024) GB\n;}注意available和free的区别free是磁盘上的剩余空间包括只有 root 能用的部分available是当前用户实际可用的空间。在 Linux 上这两个值的差异来自 reserved blocksext4 默认保留 5% 给 root。临时文件处理C 没有直接提供创建临时文件的标准 APIC23 的std::filesystem::temp_directory_path()只是告诉你临时目录在哪里。不过在 C17 中我们可以组合使用现有的工具来安全地处理临时文件#includefilesystem#includefstream#includerandom#includestringnamespacefsstd::filesystem;/// brief 创建一个唯一的临时文件路径/// return 临时文件的路径文件尚未创建fs::pathmake_temp_file(){autotemp_dirfs::temp_directory_path();// 生成随机后缀std::random_device rd;std::mt19937gen(rd());std::uniform_int_distributionintdist(0,999999);autosuffixstd::to_string(dist(gen));autotemp_pathtemp_dir/(myapp_temp_suffix.tmp);returntemp_path;}/// brief 安全地将数据写入临时文件然后原子性地重命名为目标文件/// param target 目标文件路径/// param data 要写入的数据/// return 是否成功boolsafe_write_file(constfs::pathtarget,conststd::stringdata){autotempmake_temp_file();// 先写入临时文件{std::ofstreamout(temp);if(!out)returnfalse;outdata;out.close();if(out.fail()){fs::remove(temp);returnfalse;}}// 原子性重命名std::error_code ec;fs::rename(temp,target,ec);if(ec){fs::remove(temp);// 清理临时文件returnfalse;}returntrue;}这个写临时文件 原子重命名的模式在需要保证数据完整性的场景中非常重要——如果写入过程中程序崩溃或断电目标文件要么是旧的完整版本要么是新的完整版本不会出现写了一半的损坏状态。很多数据库、配置文件管理器、包管理器都用这种模式。实战日志轮转工具我们把本篇学到的所有操作组合起来写一个实用的日志轮转log rotation工具。日志轮转的核心逻辑是当日志文件超过一定大小时把它重命名为备份文件加序号然后创建新的空日志文件。同时限制备份数量超过限制的旧备份要删除。#includefilesystem#includeiostream#includefstream#includealgorithm#includevector#includestringnamespacefsstd::filesystem;/// brief 执行日志轮转/// param log_path 日志文件路径/// param max_size 最大文件大小字节/// param max_backups 最大备份数量voidrotate_log(constfs::pathlog_path,std::uintmax_t max_size,intmax_backups){std::error_code ec;// 检查日志文件是否存在且超过大小限制if(!fs::exists(log_path,ec)||ec)return;autosizefs::file_size(log_path,ec);if(ec||sizemax_size)return;autostemlog_path.stem().string();autoextlog_path.extension().string();autoparentlog_path.parent_path();// 收集已有的备份文件std::vectorfs::pathbackups;for(inti1;imax_backups1;i){autobackup_namestem.std::to_string(i)ext;autobackup_pathparent/backup_name;if(fs::exists(backup_path)){backups.push_back(backup_path);}}// 删除超出数量限制的旧备份std::sort(backups.begin(),backups.end());while(static_castint(backups.size())max_backups){fs::remove(backups.back(),ec);backups.pop_back();}// 将现有备份序号 1for(intistatic_castint(backups.size());i1;--i){autoold_namestem.std::to_string(i)ext;autonew_namestem.std::to_string(i1)ext;fs::rename(parent/old_name,parent/new_name,ec);}// 将当前日志重命名为 .1 备份autofirst_backupparent/(stem.1ext);fs::rename(log_path,first_backup,ec);// 创建新的空日志文件std::ofstream(log_path).close();std::cout日志轮转完成: log_path\n;}intmain(){// 示例当 app.log 超过 1MB 时轮转最多保留 5 个备份rotate_log(/tmp/app.log,1024*1024,5);return0;}运行后/tmp/下的文件状态会变成这样app.log ← 新的空日志文件 app.1.log ← 上一次的日志 app.2.log ← 上上次的日志 ... app.5.log ← 最老的备份这个轮转工具使用了exists、file_size、rename、remove等本篇学到的所有核心操作。其中原子重命名保证了轮转过程中不会丢失日志数据——即使程序在重命名过程中崩溃最多也就是某个备份文件没有完成重命名下次轮转会自动处理。错误处理的两种模式贯穿这篇文笔者一直在用两种方式处理错误抛异常和std::error_code。我们来总结一下filesystem中错误处理的最佳实践。大多数fs::xxx()函数都有两个重载版本一个在出错时抛fs::filesystem_error异常另一个接受一个std::error_code参数并在出错时通过它返回错误码。选择哪种取决于你的场景// 模式一抛异常适合不应该失败的操作fs::create_directories(/tmp/myapp/data);// 模式二error_code适合可能失败的操作std::error_code ec;fs::copy(src,dst,ec);if(ec){// 处理错误}笔者个人的偏好是对于程序启动时的初始化操作创建配置目录等用抛异常的版本——因为这些操作失败意味着程序无法正常运行异常可以直接终止启动流程。对于运行时可能正常失败的操作复制文件、删除临时文件等用error_code版本——因为这些失败是可预期的需要优雅地处理。参考资源cppreference: std::filesystemcppreference: copycppreference: create_directorycppreference: removecppreference: permissionsC Stories: 22 Common Filesystem Tasks