El sistema de archivos en C++ con std::filesystem

std::filesystem es la parte de C++ para trabajar con archivos y carpetas sin depender del sistema operativo. Aquí la usamos para recorrer un árbol de carpetas, sumar tamaños, separar las partes de una ruta, copiar, mover y fechar archivos, cambiar permisos y tratar los errores con excepciones o con std::error_code.

Recorrer una carpeta y sus subcarpetas

std::filesystem (C++17) trabaja con rutas, carpetas y archivos sin depender del sistema operativo: el mismo código funciona en Linux, macOS y Windows. Todos los ejemplos de esta página crean su propia carpeta de pruebas en el directorio temporal y la borran al terminar. El primero recorre el árbol con recursive_directory_iterator y suma los tamaños:

#include <algorithm>
#include <filesystem>
#include <fstream>
#include <print>
#include <string>
#include <vector>

namespace fs = std::filesystem;

void crear(const fs::path& p, std::size_t bytes)
{
    std::ofstream(p) << std::string(bytes, 'x');
}

int main()
{
    const fs::path base = fs::temp_directory_path() / "decodigo-fs";
    fs::remove_all(base);                                   // por si quedó de otra ejecución
    fs::create_directories(base / "docs" / "2026");         // crea también las intermedias
    fs::create_directories(base / "img");
    crear(base / "leeme.txt", 120);
    crear(base / "docs" / "informe.txt", 2'048);
    crear(base / "docs" / "2026" / "octubre.csv", 5'300);
    crear(base / "img" / "logo.png", 18'000);
    crear(base / "img" / "foto.JPG", 250'000);

    std::vector<fs::path> archivos;
    for (const fs::directory_entry& e : fs::recursive_directory_iterator(base))
        if (e.is_regular_file()) archivos.push_back(e.path());
    std::ranges::sort(archivos);              // el orden del recorrido no está garantizado

    std::uintmax_t total = 0;
    for (const auto& p : archivos) {
        auto bytes = fs::file_size(p);
        total += bytes;
        std::println("{:>9}  {}", bytes, fs::relative(p, base).string());
    }
    std::println("{} archivos, {:.1f} KB en total", archivos.size(), total / 1024.0);

    std::println("borrados: {}", fs::remove_all(base));   // devuelve cuántos elementos borró
}
salida     5300  docs/2026/octubre.csv
     2048  docs/informe.txt
   250000  img/foto.JPG
    18000  img/logo.png
      120  leeme.txt
5 archivos, 269.0 KB en total
borrados: 9

Tres detalles que conviene recordar. create_directories crea todas las carpetas intermedias que falten y no se queja si ya existen. El orden en que el iterador devuelve las entradas depende del sistema de archivos, así que si lo vas a mostrar, ordénalo. Y remove_all borra el árbol entero y devuelve cuántos archivos y carpetas eliminó: 9 aquí, 5 archivos y 4 carpetas, contando la carpeta base.

Las partes de una ruta

Una fs::path sabe separarse en partes, y el operador / une rutas con el separador correcto de cada sistema:

#include <algorithm>
#include <cctype>
#include <filesystem>
#include <print>
#include <string>

namespace fs = std::filesystem;

std::string minusculas(std::string s)
{
    std::ranges::transform(s, s.begin(), [](unsigned char c) { return std::tolower(c); });
    return s;
}

int main()
{
    fs::path p = fs::path("/home/ana") / "fotos" / "verano.2026.JPG";

    std::println("ruta completa:  {}", p.string());
    std::println("carpeta:        {}", p.parent_path().string());
    std::println("nombre:         {}", p.filename().string());
    std::println("sin extensión:  {}", p.stem().string());
    std::println("extensión:      {}", p.extension().string());
    std::println("¿es absoluta?:  {}", p.is_absolute());

    // Comparar extensiones sin distinguir mayúsculas
    std::println("¿es una foto?:  {}", minusculas(p.extension().string()) == ".jpg");

    p.replace_extension(".webp");
    std::println("convertida:     {}", p.string());

    // .. y . se resuelven sin tocar el disco
    fs::path rara = "/home/ana/fotos/../docs/./cv.pdf";
    std::println("normalizada:    {}", rara.lexically_normal().string());
}
salidaruta completa:  /home/ana/fotos/verano.2026.JPG
carpeta:        /home/ana/fotos
nombre:         verano.2026.JPG
sin extensión:  verano.2026
extensión:      .JPG
¿es absoluta?:  true
¿es una foto?:  true
convertida:     /home/ana/fotos/verano.2026.webp
normalizada:    /home/ana/docs/cv.pdf

La extensión es solo lo que va tras el último punto: en verano.2026.JPG es .JPG, con el punto incluido y en mayúsculas tal como está escrita. Por eso, para filtrar por tipo, hay que comparar en minúsculas. Y nunca te fíes de la extensión para decidir si un archivo subido por un usuario es seguro: cualquiera puede llamar foto.jpg a un programa.

Copiar, mover y la fecha de modificación

#include <chrono>
#include <filesystem>
#include <format>
#include <fstream>
#include <print>

namespace fs = std::filesystem;
using namespace std::chrono;

int main()
{
    const fs::path base = fs::temp_directory_path() / "decodigo-copiar";
    fs::remove_all(base);
    fs::create_directory(base);
    std::ofstream(base / "notas.txt") << "versión 1\n";

    // Copiar: falla si el destino existe, salvo que se diga qué hacer
    fs::copy_file(base / "notas.txt", base / "copia.txt");
    fs::copy_file(base / "notas.txt", base / "copia.txt", fs::copy_options::overwrite_existing);

    // Mover o renombrar
    fs::create_directory(base / "archivo");
    fs::rename(base / "copia.txt", base / "archivo" / "notas-2026.txt");
    std::println("¿sigue copia.txt? {}", fs::exists(base / "copia.txt"));
    std::println("¿está en archivo/? {}", fs::exists(base / "archivo" / "notas-2026.txt"));

    // Fecha de modificación: se fija una para que el ejemplo dé siempre lo mismo
    auto cuando = sys_days{2026y / October / 30} + 9h + 15min;
    fs::last_write_time(base / "notas.txt", clock_cast<file_clock>(cuando));
    auto leida = clock_cast<system_clock>(fs::last_write_time(base / "notas.txt"));
    std::println("modificado: {:%Y-%m-%d %H:%M} UTC", floor<minutes>(leida));

    fs::remove_all(base);
}
salida¿sigue copia.txt? false
¿está en archivo/? true
modificado: 2026-10-30 09:15 UTC

Para renombrar un archivo hay un ejemplo dedicado, con todos sus casos de error, en Renombrar un archivo en C++. Ojo con fs::rename entre discos distintos: no puede mover el archivo y falla; en ese caso hay que copiar y después borrar.

Errores: excepciones o error_code

Casi todas las funciones de std::filesystem existen en dos versiones. La normal lanza fs::filesystem_error si algo falla; la que recibe un std::error_code al final no lanza nada y deja el error en esa variable. La segunda es mejor cuando el fallo es algo esperado, como que un archivo no exista:

#include <filesystem>
#include <fstream>
#include <print>
#include <system_error>

namespace fs = std::filesystem;

int main()
{
    const fs::path no_existe = "/tmp/decodigo-no-existe.txt";

    // 1. Con excepciones
    try {
        auto bytes = fs::file_size(no_existe);
        std::println("mide {} bytes", bytes);
    } catch (const fs::filesystem_error& e) {
        std::println("excepción: {}", e.what());
        std::println("  código: {}", e.code().message());
    }

    // 2. Con error_code: no lanza
    std::error_code ec;
    auto bytes = fs::file_size(no_existe, ec);
    if (ec)
        std::println("error_code: {} (y bytes vale {})", ec.message(), bytes == static_cast<std::uintmax_t>(-1) ? "-1" : "otro valor");

    // 3. Algunas funciones avisan devolviendo false
    std::println("¿borró algo remove()? {}", fs::remove(no_existe));

    // 4. Crear una carpeta donde ya hay un archivo
    std::ofstream("/tmp/decodigo-archivo") << "x";
    fs::create_directory("/tmp/decodigo-archivo/sub", ec);
    std::println("crear carpeta dentro de un archivo: {}", ec.message());
    fs::remove("/tmp/decodigo-archivo");
}
salidaexcepción: filesystem error: cannot get file size: No such file or directory [/tmp/decodigo-no-existe.txt]
  código: No such file or directory
error_code: No such file or directory (y bytes vale -1)
¿borró algo remove()? false
crear carpeta dentro de un archivo: Not a directory

Los mensajes vienen del sistema operativo, por eso salen en inglés en Linux. No hay que comparar el texto, sino el código: ec == std::errc::no_such_file_or_directory funciona en cualquier sistema.

Permisos

fs::permissions cambia los permisos de un archivo y fs::status(p).permissions() los lee. Aquí se quita el permiso de escritura y se comprueba que ya no se puede abrir para escribir:

#include <filesystem>
#include <fstream>
#include <print>
#include <string>

namespace fs = std::filesystem;

std::string rwx(fs::perms p)
{
    std::string s;
    const fs::perms bits[] = {fs::perms::owner_read, fs::perms::owner_write, fs::perms::owner_exec,
                              fs::perms::group_read, fs::perms::group_write, fs::perms::group_exec,
                              fs::perms::others_read, fs::perms::others_write, fs::perms::others_exec};
    for (int i = 0; i < 9; ++i)
        s += (p & bits[i]) != fs::perms::none ? "rwx"[i % 3] : '-';
    return s;
}

int main()
{
    const fs::path f = fs::temp_directory_path() / "decodigo-permisos.txt";
    std::ofstream(f) << "datos\n";

    fs::permissions(f, fs::perms::owner_read | fs::perms::owner_write | fs::perms::group_read);
    std::println("permisos: {}", rwx(fs::status(f).permissions()));

    fs::permissions(f, fs::perms::owner_write, fs::perm_options::remove);   // quitar solo la escritura
    std::println("sin escritura: {}", rwx(fs::status(f).permissions()));

    std::ofstream salida(f);
    std::println("¿se puede abrir para escribir? {}", salida.is_open());

    fs::permissions(f, fs::perms::owner_write, fs::perm_options::add);       // devolverla
    fs::remove(f);
}
salidapermisos: rw-r-----
sin escritura: r--r-----
¿se puede abrir para escribir? false
Comprobar y luego usar es una carrera. Mirar si un archivo existe o si tiene permisos y, en la línea siguiente, abrirlo, deja un hueco en el que otro proceso puede cambiarlo. En programas que trabajan con rutas que puede controlar otra persona (una carpeta compartida, /tmp), lo seguro es intentar la operación directamente y tratar el error, en lugar de comprobar antes. Más en Los fallos de seguridad típicos de C++.
  • ParaFunción
  • Saber si existe y qué esexists, is_regular_file, is_directory, is_symlink
  • Tamaño de un archivofile_size
  • Listar una carpetadirectory_iterator (un nivel) y recursive_directory_iterator (todo el árbol)
  • Crear carpetascreate_directory (una) y create_directories (con las intermedias)
  • Copiar, mover, borrarcopy_file, copy, rename, remove, remove_all
  • Ruta absoluta o relativaabsolute, canonical, relative, lexically_normal
  • Espacio libre en el discospace
  • Carpeta temporal y carpeta actualtemp_directory_path y current_path

Todo el código de esta página se compiló con GCC 15.3 (C++23, -std=c++23 -Wall -Wextra, sin avisos) y se ejecutó en Linux, en un contenedor limpio sin acceso a la red, antes de publicarla; las salidas están copiadas de esa ejecución.