Gestión de archivos con streams y el principio RAII

Cuando necesitas persistencia de datos, no te peleas con los descriptores de archivo de C ni gestionas manualmente el cierre de recursos. En C++ usamos streams (flujos), que son abstracciones de alto nivel para la entrada y salida de datos. Específicamente, tenemos std::ifstream para lectura (input), std::ofstream para escritura (output) y std::fstream cuando necesitas realizar ambas operaciones sobre el mismo archivo.

Estos objetos se rigen por el principio RAII (Resource Acquisition Is Initialization): el archivo se abre en el constructor (o mediante .open()) y se cierra automáticamente cuando el objeto sale de su ámbito (scope) debido a su destructor. Esta es la razón por la que es seguro trabajar con ellos incluso si se lanza una excepción. Para operar con el archivo, definimos modos de apertura como std::ios::binary para datos no textuales, std::ios::app para añadir al final sin borrar el contenido, o std::ios::trunc para vaciar el archivo al abrirlo.

Para navegar por el archivo, distinguimos entre el puntero de lectura y el de escritura. Usamos seekg/tellg (seek get) para la posición de lectura y seekp/tellp (seek put) para la posición de escritura. Es crucial verificar siempre el estado del flujo mediante if (!f) o f.is_open() tras la apertura; de lo contrario, el programa intentará operar sobre un flujo inválido, lo que resultará en fallos silenciosos o errores de lógica.

#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <iterator>
#include <filesystem>

// Estructura para demostrar escritura binaria
struct ConfigPacket {
    int version;
    float threshold;
};

void ejecutar_demo() {
    const std::string path_text = "config.txt";
    const std::string path_bin = "data.dat";

    // 1. Escritura de texto con ofstream
    {
        // std::ios::trunc es el modo por defecto si no se especifica nada
        std::ofstream ofs(path_text, std::ios::out | std::ios::trunc);
        if (!ofs) {
            std::cerr << "Error al crear el archivo de texto.\n";
            return;
        }
        ofs << "Parametro_A: 100\n";
        ofs << "Parametro_B: 200.5\n";
        ofs << "Linea_Final\n";
        // Al salir de este bloque, ofs se cierra automáticamente (RAII)
    }

    // 2. Lectura de texto línea a línea con ifstream
    {
        std::ifstream ifs(path_text);
        if (!ifs.is_open()) return;

        std::string linea;
        while (std::getline(ifs, linea)) {
            std::cout << "Leido: " << linea << "\n";
        }
    }

    // 3. Lectura completa de un archivo en un std::string
    {
        std::ifstream ifs(path_text);
        // Usamos iteradores de buffer para copiar todo el contenido de golpe
        std::string contenido((std::istreambuf_iterator<char>(ifs)),
                               std::istreambuf_iterator<char>());
        std::cout << "Contenido total: [" << contenido << "]\n";
    }

    // 4. Operaciones binarias con fstream (lectura/escritura)
    {
        std::fstream fs(path_bin, std::ios::in | std::ios::out | std::ios::binary | std::ios::trunc);
        if (!fs) return;

        ConfigPacket p1{1, 0.75f};
        ConfigPacket p2{2, 0.99f};

        // Escritura binaria: necesitamos un puntero a char para la API de C
        fs.write(reinterpret_cast<const char*>(&p1), sizeof(ConfigPacket));
        fs.write(reinterpret_cast<const char*>(&p2), sizeof(ConfigPacket));

        // Navegación: Volver al principio para leer
        fs.seekg(0, std::ios::beg);

        ConfigPacket lectura;
        fs.read(reinterpret_cast<char*>(&lectura), sizeof(ConfigPacket));
        std::cout << "Binario - Version: " << lectura.version << "\n";

        // Peek: Ver el siguiente caracter sin consumirlo
        fs.seekg(sizeof(ConfigPacket) * 2); // Ir al final de los datos escritos
        if (fs.peek() == std::ios::eof) {
            std::cout << "Estamos al final del archivo binario.\n";
        }
    }

    // Limpieza de archivos de prueba
    std::filesystem::remove(path_text);
    std::filesystem::remove(path_bin);
}

int main() {
    ejecutar_demo();
    return 0;
}

Análisis del código

En el primer bloque, usamos std::ofstream con el modo std::ios::trunc para asegurar que empezamos con un archivo limpio. El objeto ofs gestiona la apertura y, gracias a RAII, el archivo se cierra en cuanto el bloque de llaves termina, sin necesidad de llamar a close().

Para la lectura completa en un std::string, empleamos el constructor que acepta dos iteradores: std::istreambuf_iterator<char>. Esto es más eficiente que leer caracter por caracter con get(), ya que el iterador accede directamente al búfer del stream.

En la parte de archivos binarios, la clave es std::fstream con el flag std::ios::binary. Al usar fs.write, realizamos un reinterpret_cast<const char*> de la estructura ConfigPacket. Esto es necesario porque la función write espera un puntero a bytes (char*) para copiar la representación exacta de la memoria al disco, evitando que el sistema intente interpretar caracteres especiales como saltos de línea.

Para el posicionamiento, fs.seekg(0, std::ios::beg) mueve el puntero de lectura al inicio del archivo basándose en la posición beg (begin). El método fs.peek() nos permite inspeccionar el siguiente byte en el flujo sin avanzar el puntero de lectura, lo cual es vital en parsers donde necesitas decidir qué hacer basándote en el próximo carácter sin consumirlo.

El error frecuente

Un error clásico ocurre cuando intentas reutilizar un objeto std::ifstream después de haber llegado al final del archivo.

std::ifstream ifs("archivo.txt");
std::string linea;
while (std::getline(ifs, linea)) { /* ... */ } 
// Aquí ifs ha alcanzado el EOF (End of File) y ha seteado la flag 'eofbit'

// Intentar volver a leer o incluso hacer un seekg() fallará
ifs.seekg(0); // ¡Esto fallará silenciosamente!

Cuando un stream detecta el fin del archivo o un error de lectura, activa flags de estado internos (eofbit, failbit o badbit). Una vez que estas banderas están activas, cualquier operación posterior (incluso un seekg para volver al principio) será ignorada por el objeto. La solución es llamar a ifs.clear() para resetear el estado de las banderas antes de intentar posicionar el puntero o leer de nuevo.

81

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio