Input e output su file con fstream in C++

Input e output su file con fstream in C++

Quasi ogni programma reale deve, prima o poi, leggere dati da un file o salvarne il risultato su disco. Il C++ affronta la gestione dei file attraverso gli stream, lo stesso modello concettuale usato per l'input e l'output su console. L'intestazione fstream mette a disposizione le classi necessarie per aprire, leggere e scrivere file di testo, con un'interfaccia coerente e sicura che integra naturalmente il principio RAII.

Le classi degli stream su file

Sono tre le classi principali. La classe std::ofstream serve a scrivere su un file, la classe std::ifstream a leggere da un file, mentre std::fstream consente sia la lettura sia la scrittura. Tutte funzionano in modo analogo agli oggetti std::cout e std::cin già incontrati, usando gli stessi operatori di inserimento ed estrazione.

Scrivere su un file

Per scrivere, si crea un oggetto std::ofstream passando il nome del file. Se il file non esiste viene creato; se esiste, per impostazione predefinita il suo contenuto viene sovrascritto. Si scrive poi con l'operatore di inserimento, esattamente come si farebbe con la console.

#include <fstream>
#include <iostream>

int main() {
    std::ofstream output("data.txt");

    // Verifica sempre che il file sia stato aperto
    if (!output.is_open()) {
        std::cerr << "Impossibile aprire il file" << std::endl;
        return 1;
    }

    output << "Prima riga" << '\n';
    output << "Seconda riga" << '\n';

    return 0;   // Il file viene chiuso automaticamente
}

L'oggetto std::cerr è lo stream di errore standard, usato per i messaggi diagnostici. È buona pratica verificare sempre, tramite il metodo is_open, che l'apertura sia riuscita, poiché numerose cause, dai permessi mancanti a un percorso errato, possono farla fallire.

La chiusura automatica e RAII

Un aspetto elegante degli stream su file è che il loro distruttore chiude automaticamente il file quando l'oggetto esce dall'ambito. Non è quindi strettamente necessario invocare manualmente il metodo close, sebbene sia possibile farlo per rilasciare la risorsa prima del termine dell'ambito. Questa aderenza al principio RAII garantisce che i file non restino inavvertitamente aperti, anche in presenza di eccezioni.

Leggere un file riga per riga

La lettura più comune di un file di testo avviene riga per riga. La funzione std::getline estrae dallo stream una riga completa fino al carattere di ritorno a capo e la memorizza in una stringa. Inserendo la chiamata nella condizione di un ciclo while, si sfrutta il fatto che lo stream, al termine del file, assume uno stato che interrompe il ciclo.

#include <fstream>
#include <string>
#include <iostream>

int main() {
    std::ifstream input("data.txt");
    if (!input.is_open()) {
        std::cerr << "File non trovato" << std::endl;
        return 1;
    }

    std::string line;
    // Legge finche restano righe disponibili
    while (std::getline(input, line)) {
        std::cout << line << std::endl;
    }
    return 0;
}

Leggere valori formattati

Quando un file contiene dati strutturati, come numeri separati da spazi, si può usare l'operatore di estrazione per leggerli direttamente nei tipi appropriati, delegando allo stream la conversione. Questo approccio è comodo per formati semplici e regolari.

std::ifstream input("numbers.txt");
int number;
int total = 0;

// Legge un intero alla volta finche ce ne sono
while (input >> number) {
    total += number;
}

Modalità di apertura

All'apertura di un file è possibile specificare una modalità che ne modifica il comportamento. Tra le più utili vi è la modalità di aggiunta, che posiziona la scrittura in coda al contenuto esistente anziché sovrascriverlo, e la modalità binaria, indispensabile quando si lavora con dati non testuali. Le modalità si combinano con l'operatore bit a bit di disgiunzione.

#include <fstream>

// Apre in aggiunta: le nuove righe si accodano alle esistenti
std::ofstream log("app.log", std::ios::app);
log << "Nuovo evento registrato" << '\n';

Gli stream su file offrono un modo uniforme, sicuro e intuitivo di persistere e recuperare dati. Grazie alla coerenza con gli stream di console e all'integrazione con RAII, la gestione dei file in C++ risulta robusta e priva di molte insidie tipiche della programmazione di basso livello. Per formati complessi si costruiscono poi livelli di serializzazione e parsing sopra queste primitive, ma i fondamenti restano sempre questi.