Crear un archivo de texto en C++ con std::ofstream: escribir, añadir y guardar sin perder datos

Para crear un archivo de texto en C++ se usa std::ofstream y el operador <<, o std::println desde C++23, igual que en la consola. Lo que el ejemplo de siempre no cuenta es que, si el archivo no se puede abrir, todo lo que escribas desaparece sin un solo aviso.

Escribir un archivo con std::ofstream

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

int main()
{
    std::filesystem::create_directories("salida");
    {
        std::ofstream f("salida/notas.txt");
        if (!f) {
            std::println(stderr, "no se pudo crear salida/notas.txt");
            return 1;
        }
        f << "Primera línea\n";
        std::println(f, "Segunda línea, escrita con std::println: {} + {} = {}", 2, 2, 2 + 2);
    }   // aquí f se destruye y el archivo se cierra
    std::println("escrito: {} bytes", std::filesystem::file_size("salida/notas.txt"));
}
salidaescrito: 67 bytes

Tres detalles. Se abre el archivo en el constructor y se comprueba con if (!f). Las llaves que rodean el bloque no son decorativas: al salir de ellas, f se destruye y el archivo se cierra solo, con todo lo pendiente ya escrito en el disco; es la forma idiomática de C++ (se llama RAII) y por eso no hace falta llamar a close(). Y desde C++23 puedes escribir en un archivo con std::println(f, ...), con el mismo formato que en la consola.

Si el archivo ya existía, std::ofstream lo vacía al abrirlo. create_directories crea la carpeta si no existe y no hace nada si ya está.

Cuando el archivo no se puede abrir, nadie avisa

#include <fstream>
#include <print>

int main()
{
    std::ofstream mal("no_existe/x.txt");
    std::println("abrir en una carpeta que no existe: {}", mal ? "bien" : "falló");
    mal << "esto no va a ninguna parte\n";
    std::println("y escribir después tampoco avisa: el programa sigue como si nada");
}
salidaabrir en una carpeta que no existe: falló
y escribir después tampoco avisa: el programa sigue como si nada

Un ofstream que no se abrió no lanza ninguna excepción: entra en estado de error y, a partir de ahí, cada << no hace nada. El programa termina con normalidad y el archivo no existe. La versión anterior de esta página no comprobaba si el archivo se había abierto: con una ruta mal escrita, perdías los datos sin enterarte.

Si prefieres que los fallos se conviertan en excepciones, puedes pedírselo al flujo:

#include <fstream>
#include <print>

int main()
{
    std::ofstream f;
    f.exceptions(std::ios::failbit | std::ios::badbit);
    try {
        f.open("no_existe/x.txt");
        f << "hola\n";
    } catch (const std::ios::failure& e) {
        std::println("no se pudo escribir: {}", e.code().message());
    }
}
salidano se pudo escribir: iostream error

Añadir al final sin borrar lo que había

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

int main()
{
    std::filesystem::create_directories("salida");
    std::ofstream("salida/registro.txt") << "inicio\n";      // crea o vacía el archivo

    for (int i = 1; i <= 3; ++i) {
        std::ofstream f("salida/registro.txt", std::ios::app);
        std::println(f, "evento {}", i);
    }

    std::ifstream f("salida/registro.txt");
    std::string linea;
    while (std::getline(f, linea)) {
        std::println("{}", linea);
    }
}
salidainicio
evento 1
evento 2
evento 3

Con std::ios::app todo lo que se escribe va al final, aunque otro programa haya añadido algo mientras tanto. Es el modo que hay que usar en un archivo de registro.

Guardar sin riesgo de dejar el archivo a medias

Si el programa se cuelga o se va la luz mientras reescribe un archivo importante, puede quedar vacío o cortado. La técnica habitual es escribir en un archivo temporal y, solo cuando todo salió bien, renombrarlo encima del original. En Linux y macOS ese cambio de nombre es atómico: quien lea el archivo verá la versión vieja o la nueva, nunca una mezcla.

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

namespace fs = std::filesystem;

bool guardar(const fs::path& destino, const std::string& contenido)
{
    fs::path temporal = destino;
    temporal += ".tmp";
    {
        std::ofstream f(temporal, std::ios::binary);
        if (!f) return false;
        f << contenido;
        f.close();
        if (!f) return false;            // close() también puede fallar: disco lleno, por ejemplo
    }
    std::error_code ec;
    fs::rename(temporal, destino, ec);   // sustituye al original de una vez
    return !ec;
}

int main()
{
    fs::create_directories("salida");
    bool ok = guardar("salida/config.txt", "tema=oscuro\nidioma=es\n");
    std::println("guardado: {}, tamaño {} bytes, queda el temporal: {}", ok,
                 fs::file_size("salida/config.txt"), fs::exists("salida/config.txt.tmp"));
}
salidaguardado: true, tamaño 22 bytes, queda el temporal: false

Fíjate en el f.close() seguido de if (!f): es el único sitio donde te enteras de que los últimos datos no se pudieron escribir. En este caso sí vale la pena cerrar a mano. Para renombrar con más detalle, mira Renombrar y mover archivos en C++.

El stdafx.h y el using namespace std

La versión anterior de esta página empezaba con #include "stdafx.h". Es un archivo que Visual Studio generaba en sus proyectos para acelerar la compilación, y fuera de ese proyecto no existe:

#include "stdafx.h"
#include <fstream>

int main()
{
    std::ofstream f("salida/x.txt");
}
g++: errorstdafx.cpp:1:10: fatal error: stdafx.h: No such file or directory
    1 | #include "stdafx.h"
      |          ^~~~~~~~~~
compilation terminated.

No forma parte de C++: quítalo y el código compila en cualquier sitio. También sobraba el using namespace std;. Funciona, pero en un programa de verdad acaba provocando choques de nombres, y escribir std:: deja claro de dónde viene cada cosa.

Errores típicos al escribir archivos

  • ErrorQué pasaSolución
  • No comprobar la aperturaLos datos se pierden sin ningún avisoif (!f) o f.exceptions(...)
  • Abrir sin std::ios::appEl archivo se vacía al abrirloUsar std::ios::app para añadir
  • La carpeta no existeofstream no la creastd::filesystem::create_directories antes
  • No mirar el resultado de close()Un disco lleno pasa desapercibidof.close(); if (!f) ...
  • Sobrescribir el original directamenteUn corte deja el archivo a mediasTemporal y luego rename

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.

1 comentario en «Crear un archivo de texto en C++ con std::ofstream: escribir, añadir y guardar sin perder datos»

Deja un comentario