Zum Inhalt springen
Topos Uranos

Zusammenfassung

Alles, was ein Programm berechnet, verschwindet, wenn es endet, es sei denn, es hinterlässt es schriftlich an einem Ort, der es überdauert. Diese Lektion stellt diesen Ort vor, die Textdatei, und die Idee, die es erlaubt, sie zu benutzen, ohne etwas grundlegend Neues zu lernen: Eine Datei wird wie ein Stream gelesen und geschrieben, mit denselben Operationen wie Tastatur und Bildschirm. Behandelt werden std::ifstream und std::ofstream, die Prüfung, ob eine Datei geöffnet wurde, bevor man sie benutzt, und das Arbeitsverzeichnis, das bestimmt, wo gesucht wird; die korrekte Leseschleife, while (std::getline(in, line)), im Gegensatz zur Schleife mit eof(), die eine Angabe zu viel verarbeitet, und der genaue Grund für diese Angabe; das Schreiben mit std::println, der Anhängemodus und das Schließen beim Verlassen des Blocks. Den Abschluss der Lektion bilden die Zeilenformate: Felder durch Kommas trennen, sie mit std::from_chars validieren und in der Spezifikation entscheiden, was mit fehlerhaft aufgebauten Zeilen geschieht.

Lernziele

  1. Eine Datei zum Lesen oder zum Schreiben öffnen und prüfen, ob sie geöffnet wurde, bevor man sie benutzt, wobei die Datei, die nicht existiert, von der unterschieden wird, die in einem anderen Verzeichnis gesucht wird.
  2. Eine Datei zeilenweise mit while (std::getline(in, line)) lesen und, dem Zustand des Streams folgend, erklären, warum while (!in.eof()) eine Angabe zu viel verarbeitet.
  3. Ergebnisse mit std::println(out, …) in eine Datei schreiben und dabei zwischen dem Ersetzen ihres Inhalts und dem Anhängen am Ende wählen.
  4. Eine Datei mit kommagetrennten Werten in einen Vektor von Datensätzen verarbeiten, jede Zeile validieren und die ungültigen mit ihrer Nummer melden.

Eine Datei ist ein Stream

Die Persistenz

Die Variablen eines Programms leben im Speicher, und der Speicher eines Programms wird freigegeben, wenn es endet: Die nächste Ausführung beginnt, ohne sich an etwas zu erinnern. Damit eine Angabe das Programm überdauert, das sie erzeugt hat, muss sie auf einem dauerhaften Medium bleiben, und das Medium für den allgemeinen Gebrauch ist die Datei: eine benannte Folge von Bytes, auf der Festplatte gespeichert, die das Betriebssystem zwischen den Ausführungen aufbewahrt. Eine Textdatei ist eine Datei, deren Bytes Zeichen sind, in UTF-8, wie in der Lektion Text ist nicht ASCII gesehen, gegliedert in Zeilen, die durch das Zeichen '\n' getrennt sind. Sie ist das bescheidenste und das dauerhafteste Format: Jeder Editor öffnet sie, jede Sprache liest sie, und ein Mensch kann sie mit bloßem Auge prüfen.

Nun behandelt C++ eine Datei nicht als etwas anderes als die Tastatur. Seit der Lektion Eingabe, Ausgabe und Fehler ist std::cin ein Eingabe-Stream: eine Quelle von Zeichen, die der Reihe nach verbraucht werden, mit einem Zustand, der festhält, ob die letzte Operation fehlgeschlagen ist. Eine zum Lesen geöffnete Datei ist ein weiterer Eingabestream, vom Typ std::ifstream (von input file stream); eine zum Schreiben geöffnete Datei ein Ausgabestream, vom Typ std::ofstream. Beide werden im Header <fstream> deklariert. Daraus folgt, dass alles, was in der Lektion Richtig einlesen über std::cin gelernt wurde, der Operator >>, std::getline und der Fehlerzustand, unverändert für eine Datei gilt.

Öffnen und prüfen

Eine Datei wird beim Erzeugen des Streams geöffnet, mit dem Dateinamen in geschweiften Klammern: std::ifstream in{"grades.txt"};. Das Öffnen kann fehlschlagen (die Datei existiert nicht, es fehlt die Leseberechtigung, der Name ist falsch geschrieben), und das Fehlschlagen hält das Programm weder an noch erzeugt es eine Meldung: Es versetzt den Stream in einen Fehlerzustand. Ein Stream im Fehlerzustand ist, als Bedingung verwendet, false; die Prüfung lautet daher if (!in), und sie muss vor dem ersten Lesen erfolgen. Das folgende Programm versucht, eine Datei zu öffnen, die nicht existiert.

C++-Programm
#include <fstream>#include <iostream>#include <print>​int main(){    std::ifstream in{"grades.txt"};    if (!in) {        std::println(std::cerr, "Datei konnte nicht geöffnet werden: grades.txt");        return 1;    }    int first{};    in >> first;    std::println("Erste Note: {}", first);}
Ausgabe
Datei konnte nicht geöffnet werden: grades.txt

Das Programm endete mit dem Code 1.

Drei Einzelheiten verdienen Aufmerksamkeit. Die erste ist std::cerr (aus <iostream>), der Fehlerstream: Er schreibt wie die Standardausgabe auf den Bildschirm, ist aber ein eigener Kanal, sodass sich die Fehlermeldungen nicht mit den Ergebnissen vermischen, wenn die Ausgabe in eine Datei umgeleitet wird; std::println nimmt als erstes Argument den Stream an, in den es schreiben soll. Die zweite ist das return 1;: Ein Beendigungscode ungleich null teilt, wie in der Lektion Anatomie eines Programms gesehen, der Person, die das Programm ausgeführt hat, mit, dass etwas schiefgegangen ist. Die dritte ist das, was ohne die Prüfung geschehen wäre: in >> first auf einem Stream im Fehlerzustand tut nichts, und das Programm hätte in aller Ruhe „Erste Note: 0“ geschrieben. Ein falsches Ergebnis, das wie ein wahres aussieht, ist schlimmer als eine Fehlermeldung; daher die Regel dieser Lektion: Jede Datei wird beim Öffnen geprüft.

Relative Pfade und das Arbeitsverzeichnis

Der Name "grades.txt" sagt nicht, in welchem Verzeichnis die Datei liegt. Ein solcher Name ist ein relativer Pfad, und er wird vom Arbeitsverzeichnis aus aufgelöst: dem Verzeichnis, aus dem das Programm ausgeführt wird, das nicht notwendigerweise das Verzeichnis der ausführbaren Datei oder das des Quellcodes ist. Das folgende Programm, im Verzeichnis project neben einer Datei data.txt gespeichert, öffnet diese Datei und schreibt ihre erste Zeile; es wurde zweimal ausgeführt, zuerst aus project und dann aus dem übergeordneten Verzeichnis.

Terminal
$ cd project$ g++ -std=c++23 -Wall -Wextra -Wpedantic -Wconversion -Wshadow -g -fsanitize=address,undefined -fno-sanitize-recover=undefined main.cpp -o main$ ./mainErste Zeile: Hallo aus der Datei$ cd ..$ ./project/mainDatei konnte nicht geöffnet werden: data.txt

Die ausführbare Datei ist dieselbe, und die Datei liegt noch an ihrem Platz; das Einzige, was sich geändert hat, ist das Arbeitsverzeichnis, und mit ihm der Ort, an dem data.txt gesucht wird. Das ist die häufigste Ursache für das „findet die Datei nicht“. Ein Editor, der das Programm per Knopfdruck ausführt, wählt das Arbeitsverzeichnis selbst (in Visual Studio Code ist es meist der im Editor geöffnete Ordner, auch wenn das von der Konfiguration der Aufgabe abhängt), sodass es sich empfiehlt, aus dem Terminal im Verzeichnis der Daten auszuführen, bis man weiß, was der Editor tut. Die entgegengesetzte Versuchung, einen absoluten Pfad wie "/home/ana/curso/data.txt" oder "C:\\Users\\Ana\\data.txt" in den Code zu schreiben, löst das Problem auf einem einzigen Rechner und schafft es auf allen anderen: Das Programm funktioniert nicht mehr, sobald eine andere Person es ausführt oder das Verzeichnis verschoben wird. Empfehlenswert ist der relative Pfad mit einem bekannten Arbeitsverzeichnis oder, besser, der Dateiname als Angabe, die der Benutzer liefert.