Vai al contenuto
Topos Uranos

Riepilogo

Tutto ciò che un programma calcola scompare quando termina, a meno che non lo lasci scritto in qualche luogo che gli sopravviva. Questa lezione presenta quel luogo, il file di testo, e l’idea che permette di usarlo senza imparare nulla di radicalmente nuovo: un file si legge e si scrive come uno stream, con le stesse operazioni della tastiera e dello schermo. Si studiano std::ifstream e std::ofstream, la verifica che un file sia stato aperto prima di usarlo e la cartella di lavoro, che decide dove lo si cerca; il ciclo di lettura corretto, while (std::getline(in, line)), a confronto con il ciclo con eof(), che elabora un dato di troppo, e la ragione esatta di quel dato; la scrittura con std::println, la modalità di aggiunta e la chiusura all’uscita dal blocco. Chiudono la lezione i formati di riga: separare campi con virgole, validarli con std::from_chars e decidere, nella specifica, che cosa fare delle righe malformate.

Obiettivi di apprendimento

  1. Aprire un file in lettura o in scrittura e verificare che sia stato aperto prima di usarlo, distinguendo il file che non esiste da quello che si cerca in un’altra cartella.
  2. Leggere un file riga per riga con while (std::getline(in, line)) e spiegare, seguendo lo stato dello stream, perché while (!in.eof()) elabora un dato di troppo.
  3. Scrivere risultati in un file con std::println(out, …), scegliendo tra sostituirne il contenuto e aggiungere in fondo.
  4. Elaborare un file di valori separati da virgole in un vettore di record, validando ogni riga e segnalando quelle non valide con il loro numero.

Un file è uno stream

La persistenza

Le variabili di un programma vivono nella memoria, e la memoria di un programma viene liberata quando termina: l’esecuzione successiva comincia senza ricordare nulla. Perché un dato sopravviva al programma che lo ha prodotto, deve restare su un supporto permanente, e il supporto d’uso generale è il file: una sequenza di byte con un nome, salvata sul disco, che il sistema operativo conserva tra un’esecuzione e l’altra. Un file di testo è un file i cui byte sono caratteri, in UTF-8 come si è visto nella lezione Il testo non è ASCII, organizzati in righe separate dal carattere '\n'. È il formato più modesto e il più duraturo: qualsiasi editor lo apre, qualsiasi linguaggio lo legge, e una persona può controllarlo a colpo d’occhio.

Ora, C++ non tratta il file come una cosa diversa dalla tastiera. Dalla lezione Input, output ed errori, std::cin è uno stream di input: una sorgente di caratteri che si consumano in ordine, con uno stato che registra se l’ultima operazione è fallita. Un file aperto in lettura è un altro stream di input, di tipo std::ifstream (da input file stream); un file aperto in scrittura, uno stream di output, di tipo std::ofstream. Entrambi sono dichiarati nell’header <fstream>. Ne segue che tutto ciò che si è imparato su std::cin nella lezione Leggere bene, l’operatore >>, std::getline e lo stato di errore, vale senza cambiamenti per un file.

Aprire e verificare

Un file si apre alla creazione dello stream, con il nome del file tra parentesi graffe: std::ifstream in{"grades.txt"};. L’apertura può fallire (il file non esiste, manca il permesso di leggerlo, il nome è scritto male), e il fallimento non ferma il programma né produce alcun messaggio: lascia lo stream in stato di errore. Uno stream in errore, usato come condizione, vale false; pertanto la verifica è if (!in), e va fatta prima della prima lettura. Il programma seguente tenta di aprire un file che non esiste.

Programma in C++
#include <fstream>#include <iostream>#include <print>​int main(){    std::ifstream in{"grades.txt"};    if (!in) {        std::println(std::cerr, "Impossibile aprire grades.txt");        return 1;    }    int first{};    in >> first;    std::println("Primo voto: {}", first);}
Output
Impossibile aprire grades.txt

Il programma è terminato con il codice 1.

Tre dettagli meritano attenzione. Il primo è std::cerr (da <iostream>), il flusso degli errori: scrive sullo schermo come lo standard output, ma è un canale distinto, in modo che i messaggi di errore non si mescolino con i risultati quando l’output viene reindirizzato su un file; std::println accetta come primo argomento lo stream su cui deve scrivere. Il secondo è il return 1;: un codice di terminazione diverso da zero, come si è visto nella lezione Anatomia di un programma, avvisa chi ha eseguito il programma che qualcosa è andato storto. Il terzo è ciò che sarebbe accaduto senza la verifica: in >> first su uno stream in errore non fa nulla, e il programma avrebbe scritto «Primo voto: 0» in tutta tranquillità. Un risultato falso con l’aspetto di uno vero è peggiore di un messaggio di errore; donde la regola di questa lezione: ogni file si verifica all’apertura.

Percorsi relativi e cartella di lavoro

Il nome "grades.txt" non dice in quale cartella si trova il file. Un nome così è un percorso relativo, e si risolve a partire dalla cartella di lavoro: la cartella da cui si esegue il programma, che non è necessariamente la cartella dell’eseguibile né quella del codice sorgente. Il programma seguente, salvato nella cartella project accanto a un file data.txt, apre quel file e ne scrive la prima riga; è stato eseguito due volte, prima da project e poi dalla cartella superiore.

Terminale
$ cd project$ g++ -std=c++23 -Wall -Wextra -Wpedantic -Wconversion -Wshadow -g -fsanitize=address,undefined -fno-sanitize-recover=undefined main.cpp -o main$ ./mainPrima riga: Ciao dal file$ cd ..$ ./project/mainImpossibile aprire data.txt

L’eseguibile è lo stesso, e il file è ancora al suo posto; l’unica cosa che è cambiata è la cartella di lavoro, e con essa il luogo in cui si cerca data.txt. Questa è la causa più frequente del «non trova il file». Un editor che esegue il programma con un pulsante sceglie per conto suo la cartella di lavoro (in Visual Studio Code di solito è la cartella aperta nell’editor, anche se dipende dalla configurazione dell’attività), per cui conviene eseguire dal terminale, nella cartella dei dati, finché non si sa che cosa fa l’editor. La tentazione opposta, scrivere nel codice un percorso assoluto come "/home/ana/curso/data.txt" o "C:\\Users\\Ana\\data.txt", risolve il problema su una sola macchina e lo crea su tutte le altre: il programma smette di funzionare non appena lo esegue un’altra persona o si sposta la cartella. Ciò che si raccomanda è il percorso relativo con una cartella di lavoro nota o, meglio, il nome del file come dato fornito dall’utente.