Dateioperationen

Willemers Informatik-Ecke

Programme speichern ihre Informationen in Variablen. Leider bleiben diese immer nur bis zum nächsten Stromausfall oder Betriebssystemabsturz erhalten. Und wenn das Programm verlassen wird, ist ihr Inhalt ebenfalls Geschichte. Damit Sie auf Ihre Daten auch morgen noch kraftvoll zugreifen können, empfiehlt es sich, diese in einer Datei abzulegen. Dazu können Sie die Daten als Ausgabestrom in die Datei schreiben. Das Vorgehen entspricht dem bei der Bildschirmausgabe per cout. Diese Form wird sequenziell genannt, weil die Daten nacheinander in der Reihenfolge, wie sie geschrieben wurden, in der Datei landen. Sie können aber auch einen Datenblock an eine beliebige Stelle der Datei schreiben. Später können Sie diesen Datenblock wieder zurückholen, indem Sie den internen Dateizeiger an diese Stelle positionieren und den Datenblock wieder lesen. Diese Vorgehensweise ist typisch für Klassen, insbesondere, wenn sie in irgendeiner Form sortiert abgelegt werden sollen.

fstream

Für die Dateioperationen werden Objekte der Klasse fstream verwendet. Wird in die Datei nur geschrieben, kann stattdessen die Klasse ofstream verwendet werden. Für reine Eingabedateien bietet sich die Klasse ifstream an. Auf die Objekte dieser Klassen können Ein- und Ausgabeoperatoren (>> und <<) angewandt werden. Vor einem Zugriff muss die Datei mit der Elementfunktion open() geöffnet werden. Sie können die Aufgabe des Öffnens auch dem Konstruktor der fstream-Klasse überlassen, indem Sie dem Objekt bei ihrer Definition den Dateinamen als Parameter übergeben. Nach der Bearbeitung der Datei muss sie wieder mit der Elementfunktion close() geschlossen werden. Diese Aufgabe übernimmt aber auch automatisch der Destruktor, so dass Sie close() nur dann aufrufen müssen, wenn Sie eine Datei schließen wollen, bevor das Objekt aufgelöst wird.

[Textausgabe in eine Datei]

#include <fstream>
using namespace std;

int main()
{
    fstream f;
    f.open("test.dat", ios::out);
    f << "Dieser Text geht in die Datei" << endl;
    f.close();
}

Öffnen und Schließen

Konstruktor

Im
Listing wird zunächst ein Objekt der Klasse fstream definiert. Danach wird durch Aufruf der Funktion open() die Datei geöffnet. Die nächste Zeile leitet einen String in die Datei um, und zuletzt schließt der Aufruf von close() die Datei wieder.

Parameter von open()

Die Parameter der Funktion open() sind zuerst der Name der Datei, die geöffnet werden soll. Als zweiter Parameter wird angegeben, in welchem Modus die Datei geöffnet wird. Im Beispiel wird die Datei zur Ausgabe verwendet. Die Parameter von open() können auch gleich bei der Definition des Objekts mitgegeben werden. Damit wird ein Konstruktor aufgerufen, der gleich die Datei öffnet.

fstream f("test.dat", ios::out);

Dateiname

Der Dateiname ist ein C-String, also als Zeiger auf char deklariert. In den meisten Fällen werden Sie Dateinamen aus Dateiauswahldialogen oder Benutzereingaben einfach übernehmen. Wird der Dateiname als Konstante im Programm definiert, ist daran zu denken, dass der Pfadtrenner Backslash zweimal geschrieben werden muss, da er ansonsten in einem String als Sonderzeichen interpretiert wird. Ansonsten empfiehlt sich die Verwendung eines Schrägstrichs als Pfadtrenner. Er wird auch unter MS-Windows als Pfadtrenner akzeptiert. Das ist nicht nur leichter zu lesen, sondern hat seine Vorteile in der Portierbarkeit zu UNIX-Systemen. Dort wird der Schrägstrich und nicht der Backslash als Pfadtrenner verwendet.

Dateimodus

Der zweite Parameter gibt den Modus an, in dem die Datei geöffnet wird. Die Konstante ios::out im Beispielprogramm besagt, dass die Datei zum Schreiben geöffnet wird. Naheliegenderweise verwenden Sie ios::in, wenn Sie eine Datei zum Lesen verwenden möchten. Der Modus ios::trunc bedeutet, dass die Datei beim Öffnen geleert wird. Das ist nur sinnvoll, wenn die Datei geschrieben werden soll.

ios::app

Ähnlich verhält es sich mit dem Modus ios::app. Damit werden alle Ausgaben an die Datei angehängt. Auch dieser Modus wird sinnvollerweise in Kombination mit ios::out verwendet. Dieser Schreibmodus hat vor allem beim Schreiben von Protokolldateien eine besondere Bedeutung. So ist gewährleistet, dass die Daten immer hinten an die Datei angehängt werden. Anstatt zunächst die Größe der Datei und daraus die Schreibposition zu ermitteln, reicht ein einfacher Schreibaufruf aus. Der Modus ios::ate bewirkt, dass der Positionszeiger beim Öffnen der Datei zunächst auf das Ende der Datei gesetzt wird. Um mehrere Modi zu kombinieren, werden sie bitweise mit ODER verknüpft. Sie setzen also einen einfachen, senkrechten Strich zwischen die Moduskonstanten. [Dateimodi]
Konstante Bedeutung
ios::in Zum Lesen
ios::out Zum Schreiben
ios::trunc Datei wird beim Öffnen geleert
ios::app Geschriebene Daten ans Ende anhängen
ios::ate Positionszeiger ans Ende setzen

ofstream und ifstream

Es gibt zwei spezielle Stream-Klassen. Die Klasse ifstream impliziert als Modus beim Öffnen der Datei ios::in. Die Klasse ist also auf das Lesen von Dateien spezialisiert. Das Gegenstück ist ofstream, die für Dateien zuständig ist, die nur geschrieben werden. Hier braucht beim Öffnen der Datei ios::out nicht explizit genannt werden.

binary

Standardmäßig geht C++ davon aus, dass eine Datei eine Textdatei ist. Das führt dazu, dass Zeilenvorschübe je nach Plattform unterschiedlich behandelt werden. In einer UNIX-Umgebung wird ein Zeilenvorschub mit dem ASCII-Zeichen 10 (Line Feed) codiert. Ein Macintosh (Classic) verwendet 13 für Carriage Return, und die Betriebssysteme aus dem Hause Microsoft benutzen eine Kombination aus beiden. Beim Lesen und Schreiben von Dateien werden dabei gegebenenfalls Anpassungen durchgeführt, die aber fatal sind, wenn es sich gar nicht um Texte, sondern um binäre Daten handelt. Um dies zu verhindern, wird beim Öffnen reiner Datendateien die Option ios::binary verwendet.

Schließen

Eine Datei wird automatisch durch den Destruktor geschlossen. Insofern ist das Aufrufen der Funktion close() eigentlich überflüssig. Das explizite Schließen hätte sogar den Nachteil, dass es ein Stream-Objekt gibt, das keinen Zugriff mehr auf eine Datei hat. Insofern ist ein Öffnen durch den Konstruktor und ein Schließen durch den Destruktor etwas eleganter. Auf der anderen Seite wird ein kluger Programmierer eine zum Schreiben geöffnete Datei gern wieder schnellstmöglich schließen wollen. Eine zum Schreiben geöffnete Datei ist verletzbar, solange sie nicht wieder geschlossen ist. Stürzt das Programm vor dem Schließen der Datei ab, kann deren Inhalt defekt sein.

Lesen und Schreiben

Der Sinn und Zweck einer Datei ist es, Daten aufzunehmen. Dabei unterscheiden sich Textdateien von Datendateien. Textdateien lassen sich durch die Ein- und Ausgabeoperatoren bearbeiten. Sie können solche Dateien mit einem beliebigen Editor lesen und bearbeiten. Datendateien werden dagegen typischerweise mit Datenblöcken fester Länge beschrieben und gelesen.

Datenstrom

Die Stream-Objekte für Dateien lassen sich für die Ein- und Ausgabe genauso behandeln, wie Sie es von cin und cout her kennen. Das folgende Beispiel zeigt noch einmal die Ausgabe in eine Datei mit Hilfe des Ausgabeoperators:

fstream Datei(..., ios::out);
...
Datei << "Dies geht in eine Datei" << Variable << endl;

Natürlich können für die Dateiausgabe auch Manipulatoren zur Formatierung der Ausgabe verwendet werden. Das Lesen aus Dateien kann auf analoge Weise über den Eingabeoperator erfolgen.

Datei auslesen

Der Eingabeoperator >> arbeitet mit fstream-Objekten in der gleichen Weise wie mit cin. Die Datei wird so gelesen, als käme ihr Inhalt von der Tastatur. Dazu gehört, dass Leerzeichen, Tabulatoren und Zeilenvorschübe als Eingabetrenner interpretiert werden. Das gilt auch, wenn der Datenstrom in Zeichenketten-Variablen fließt.

Textzeile lesen

Um dennoch aus den Dateien Textzeilen mit Leerzeichen auslesen zu können, wird auch hier die Elementfunktion getline() verwendet. Als erster Parameter wird ein Zeiger auf char übergeben. Die Funktion arbeitet also mit den klassischen C-Strings. Der zweite Parameter ist die maximale Anzahl von Zeichen, die in den Puffer passt.

[Auslesen einer Datei]

#include <fstream>
#include <iostream>
using namespace std;

int main(int argc, char *argv[])
{
    fstream f;
    char cstring[256];
    f.open(argv[1], ios::in);
    while (!f.eof())
    {
        f.getline(cstring, sizeof(cstring));
        cout << cstring << endl;
    }
    f.close();
}

Daneben gibt es noch eine globale Funktion namens getline(), die ein Objekt vom Typ ifstream als ersten Parameter hat. Diese globale Funktion arbeitet auch mit der Standardklasse string und akzeptiert ein Objekt dieser Klasse als zweiten Parameter. Das Beispiel oben würde dann so aussehen:

[Auslesen einer Datei (getline.cpp)]

#include <fstream>
#include <iostream>
#include <string>
using namespace std;

int main(int argc, char *argv[])
{
    ifstream f;  // Datei-Handle
    string s;
    f.open(argv[1], ios::in); // Öffne Datei aus Parameter
    while (!f.eof())          // Solange noch Daten vorliegen
    {
        getline(f, s);        // Lese eine Zeile
        cout << s << endl;    // Zeige sie auf dem Bildschirm

    }
    f.close();                // Datei wieder schließen
}

Datendateien

Zum Schreiben von binären Daten eignet sich das Verfahren der Datenumleitung allerdings weniger. Hier werden die Funktionen read() und write() verwendet. Damit lassen sich feste Datenblöcke lesen und schreiben. Für die Datenblöcke werden meistens Objekte als Hauptspeicherpuffer verwendet.

tDaten Daten;
fstream f(..., ios::out|ios::binary|ios::in);
f.write(&Daten, sizeof(Daten));
...
f.read(&Daten, sizeof(Daten));

Mit dem Aufruf von fwrite() wird das Objekt Daten in eine Datei ab der aktuellen Position geschrieben. Mit dem späteren Aufruf von read() wird aus der Datei in ein Objekt Daten gelesen.

Datenpuffer

Eine Klasse, die als Datenpuffer verwendet wird, sollte natürlich auch alle Daten enthalten. Sobald die Klasse Zeiger enthält, werden Hauptspeicheradressen auf die Daten und nicht die Daten selbst gesichert. Solche Zeiger sind nicht immer offen zu sehen, sondern verbergen sich manchmal hinter Datentypen. So enthält beispielsweise ein Objekt der Klasse string Verweise auf die Zeichenkette, aber nicht die Zeichenkette selbst. Dementsprechend eignet sich für eine Dateipuffer eher ein klassischer C-String, also ein festes Array von char, als ein string oder ein Zeiger auf einen char.

Beispiel

Das folgende Testprogramm sichert den ersten Parameter, mit dem das Programm aufgerufen wurde, in der Datei testdatei. Wird das Programm ohne Parameter aufgerufen, wird der zuletzt abgelegte Name wieder aus der Datei gelesen und auf dem Bildschirm ausgegeben. Das Programm liegt in zwei Versionen vor. In der Version, in der ein C-String zur Aufnahme der Daten in tDaten verwendet wird, funktioniert es einwandfrei. Entfernen Sie dagegen die Kommentarzeichen der Variablendefinition für Daten und kommentieren die bisherige Variablendefinition aus, werden die Daten in einem string-Objekt abgelegt, weil nun die Klasse tStrDaten verwendet wird. Das Programm scheitert, weil die Zeichenkette nicht mehr innerhalb der Grenzen von tStrDaten liegt und damit nicht in die Datei geschrieben wird.

[Test: Datenstruktur sichern (testfstr.cpp)]

#include <fstream>
#include <string>
using namespace std;
#include <string.h>

// Diese Klasse nimmt die Zeichenkette im klassischen
// char-Array auf und enthält damit die Daten.
class tDaten
{
public:
   void Set(char *para)
   {
        strncpy(data, para, sizeof(data));
        data[sizeof(data)-1] = 0;
   }
   void Show()
   {
       cout << data << endl;
   }
private:
    char data[255];
};

// Hier werden die C++-Strings verwendet. Allerdings enthält
// ein Objekt vom Typ string nicht die eigentlichen Daten.
class tStrDaten
{
public:
   void Set(char *para)
   {
       data = para;
   }
   void Show()
   {
       cout << data << endl;
   }
private:
    string data;
};

int main(int argc, char**argv)
{
  // tStrDaten Daten; // Damit funktioniert es nicht!
  tDaten Daten;       // Damit funktioniert es!

  // Eine Datein namens testdatei wird geöffnet
  fstream f("testdatei", ios::out|ios::binary|ios::in);

  // Wurde ein Parameter beim Aufruf übergeben?
  if (argc>=2)
  {
      Daten.Set(argv[1]); // In Daten ablegen
      // Objekt in Datei speichern
      f.write((char *)&Daten, sizeof(Daten));
  }
  // Kein Argument? Dann Datei auslesen!
  if (argc==1)
  {
      // Dateiinhalt in Objekt lesen
      f.read((char *)&Daten, sizeof(Daten));
      Daten.Show(); // ... und anzeigen
  }
}

Versionen

Denken Sie auch daran, dass Dateistrukturen im Allgemeinen langlebiger als Programmversionen sind. Sobald die Struktur des Dateipuffers geändert wird, sind die Dateien, die von der vorherigen Version des Programms geschrieben wurden, nicht mehr lesbar. Das wird dann kritisch, wenn die vorherige Version an den Kunden gegangen ist. Der Anwender wird erwarten, dass nach einem Update die alte Datenstruktur mindestens gelesen werden kann. Jeder Prozess [1] verwaltet für jede geöffnete Datei einen Positionszeiger. Dieser Zeiger steht nach dem Öffnen standardmäßig am Anfang der Datei und bewegt sich durch Lesen oder Schreiben immer weiter in Richtung Dateiende. Um die Position zu ändern, bietet die Klasse ifstream die Funktion seekg() an. Die Klasse ofstream verwendet dazu die Funktion seekp(). Damit kann an jeder beliebigen Stelle der Datei ein Datenblock gelesen oder geschrieben werden. Dazu wird zunächst ein Seek-Aufruf abgesetzt und anschließend gelesen oder geschrieben. Der erste Parameter der Seek-Funktionen ist die Position, die der Dateizeiger bekommen soll. Optional kann ein zweiter Parameter angegeben werden. Dieser bestimmt, aus welcher Richtung die neue Position berechnet werden soll. Dort können die folgenden Konstanten stehen: [Positionsrichtung]
Wert Bedeutung
ios::beg Vom Dateianfang aus gesehen
ios::cur Von der aktuellen Dateiposition aus gesehen
ios::end Vom Dateiende aus gesehen

Positionsbestimmung

Mit den parameterlosen Elementfunktionen tellg() und tellp() kann ermittelt werden, an welcher Position der Dateizeiger derzeit steht.

Puffer leeren

Dateizugriffe werden deutlich schneller, wenn das Betriebssystem nicht sofort alle Daten auf die Festplatte schreibt, sondern sie so lange im Hauptspeicher puffert, bis mit einem Durchgang mehrere Blöcke auf einmal geschrieben werden können. Dieses Verhalten ist heutzutage auf allen Systemen Standard. Manchmal soll aber der Datensatz sofort auf die Platte geschrieben werden. Dies wird durch Aufruf der Elementfunktion flush() der Klasse fstream erzwungen.

Zustandsbeobachtung

Bei der Arbeit mit Dateien können diverse Probleme entstehen, auf die das Programm vorbereitet sein sollte. So könnte eine Datei, die das Programm lesen will, gar nicht existieren oder kürzer sein als erwartet. Beim Schreiben könnte das Schreibrecht fehlen oder die Platte voll sein.

good()

Das Stream-Objekt kann jederzeit mit der Elementfunktion good() gefragt werden, ob bei der letzten Aktion Fehler festgestellt worden sind. Die Funktion good() hat keine Parameter und liefert einen booleschen Wert zurück. Alternativ kann auch das Stream-Objekt selbst abgefragt werden. Enthält sie eine 0, entspricht das dem Rückgabewert false der Funktion good().

fstream Datei;
...
if (Datei.good())
{
    // Alles super
...
if (Datei)
{
    // Auch gut

Das folgende Programm öffnet eine Datei, schreibt einen Datensatz hinein, geht wieder an den Anfang der Datei zurück und liest dann den zuvor geschriebenen Satz. Dabei prüft es jeden Schritt direkt nach seiner Ausführung.

fstream f("test.dat", ios::out|ios::binary|ios::in);
if (!f.good())
{
  cerr << "Fehler beim Öffnen von test.dat" << endl;
}
f.write(&Daten, sizeof(Daten));
if (!f.good())
{
  cerr <<"Fehler beim Schreiben von test.dat"<< endl;
}
f.seekg(0, ios::beg);
f.read(&Daten, sizeof(Daten));
if (!f.good())
{
  cerr << "Fehler beim Lesen von test.dat" << endl;
}

Dateiende

Wird aus dem Beispiel die Zeile mit dem Seek-Befehl herausgenommen, so würde der anschließende Lesebefehl einen Fehler verursachen, wenn die Datei zu Anfang leer war. Durch den Schreibbefehl steht der Dateipositionszeiger am Ende der Datei. Der folgende Lesebefehl kann nicht mehr genug Daten aus der Datei lesen, um die Puffervariable Daten zu füllen. Dieser Zustand wird als EOF (End Of File) bezeichnet. Das Erreichen des Dateiendes ist keine Katastrophe. Es ist durchaus üblich, eine Datei Satz für Satz auszulesen, bis das Ende der Datei erreicht wird. Um das Dateiende von anderen Fehlern zu unterscheiden, gibt es eine eigene Funktion namens eof(). Sie liefert so lange den Rückgabewert false, bis das Ende der Datei überschritten wurde. Die folgende Schleife liest also alle Daten einer Datei:

while (!f.eof())
{
    f.read(&Daten, sizeof(Daten));
}
f.clear();

Fehlerbits löschen

Nachdem ein Fehler aufgetreten ist, liefert die Funktion good() diesen Fehler so lange zurück, bis durch den Aufruf von clear() der Fehlerzustand des Stream-Objekts zurückgesetzt wurde. Im obigen Beispiel wurde dies nach dem Ende der Schleife durchgeführt. Die Schleife wurde ja durch einen EOF verlassen.

fail()

Neben dem Dateiende gibt es noch die Möglichkeit, dass das Schreiben oder Lesen fehlschlägt, weil der Aufruf im gewünschten Umfang nicht abgewickelt werden konnte. In einem solchen Fall liefert die Elementfunktion fail() von fstream den Wert true zurück.

bad()

Ein schwerwiegender Fehler kann durch Abfrage der Elementfunktion bad() von fstream festgestellt werden. Nach dem Auftreten eines solchen Fehlers werden weitere Operationen scheitern.

exceptions()

Durch den Aufruf der Elementfunktion exceptions() von fstream können Sie festlegen, welche Fehlerzustände eine Ausnahmebehandlung auslösen sollen. Als Parameter können die Konstanten ios::failbit, ios::badbit und ios::eofbit, mit binärem Oder verknüpft, übergeben werden. Das folgende Beispiel führt zu einer Ausnahmebehandlung, wenn das Fail-Bit oder das Bad-Bit gesetzt wird:

f.exceptions(ios::failbit|ios::badbit);
Zum Thema Ausnahmebehandlungen finden Sie
an anderer Stelle ausführlichere Informationen. Dabei wird der Umgang mit den Ausnahmen von fstream in einem eigenen Abschnitt behandelt.

Dateizugriffe nach ANSI-C

Dateien gibt es ja nun nicht erst, seit es C++ gibt, und so ist es nahe liegend, dass bereits C Funktionen zur Verfügung stellt, um Dateien zu bearbeiten. Viele C++-Programmierer haben mit C angefangen, und so findet man auch in C++-Programmen häufig diese Art der Dateibehandlung. Die Kenntnis dieser Dateizugriffe ist wichtig, wenn Sie ein älteres Programm erweitern oder korrigieren sollen. Neuere Programme sollten mit den fstream-Klassen geschrieben werden. Die Vorteile der neuen Klassen sind:
  • Die Operationen werden auf Typsicherheit überprüft.
  • Sie können eine Ausnahmebehandlung verwenden.
  • Wenn Sie ostream und istream verwenden, kann bereits beim Kompilieren sichergestellt werden, dass nicht versehentlich in Dateien geschrieben wird, die nur zum Lesen geöffnet wurden.

Handles

Da C keine Klassen kennt, erfolgen die Zugriffe auf Dateien über eine Sammlung von Funktionen. Beim Öffnen der Datei bekommen Sie ein so genanntes Handle (engl. Handgriff) zurück. Diesen Wert müssen Sie bei jedem Zugriff auf die Datei übergeben, damit das Betriebssystem weiß, mit welcher Datei Sie arbeiten. Dieses Handle ist bei den ANSI-C-Funktionen ein Zeiger auf den Typ FILE. Das folgende Beispiel zeigt, wie ein Datensatz in eine Datei geschrieben und anschließend wieder ausgelesen wird.

[Dateibehandlung]

#include <stdio.h>

int main()
{
FILE *f;

    f = fopen("test.dat", "rwb");
    if (f)
    {
        fwrite(puffer, sizeof(puffer), 1, f);
        fseek(f, 0, SEEK_SET);
        fread(puffer, sizeof(puffer), 1, f);
        fclose(f);
    }
}

Öffnen und schließen

Die Datei wird durch den Aufruf von fopen() geöffnet und durch den Aufruf von fclose() geschlossen. Die Funktionen haben die folgenden Prototypen:

FILE *fopen(const char* DateiName, const char* Modus);
int fclose(FILE *DateiHandle);

Der erste Parameter von fopen() ist der Dateiname. Der zweite Parameter ist der Öffnungsmodus, der als Zeichenkette eines oder mehrerer Buchstaben übergeben wird. Tabelle (tabfilemodus) zeigt eine Übersicht. [Öffnungsmodi bei fopen()]
Zeichen Bedeutung
r Zum Lesen öffnen
w Datei leeren und zum Schreiben öffnen
a Daten werden angehängt
r+ Neben dem Lesen auch das Schreiben zulassen
w+ Datei leeren und zum Schreiben und Lesen öffnen
b Binärdatei (keine Konvertierung der Zeilenendezeichen)

Lesen und schreiben

Die Funktion fwrite() schreibt einen Datenblock in die Datei. Die Funktion fread() liest einen Datenblock aus der Datei. Die Funktionen haben folgende Prototypen:

size_t fread(void *Puffer, size_t Groesse, size_t n, FILE *f);
size_t fwrite(void *Puffer, size_t Groesse, size_t n, FILE *f);

Parameter

Der erste Parameter ist ein Zeiger auf den Puffer, in dem die Daten im Hauptspeicher liegen. Da der Parametertyp ein Zeiger auf void ist, kann die Adresse einer beliebigen Speicherstruktur übergeben werden. Die nächsten beiden Parameter beschreiben die Größe des Puffers. Zunächst wird die Größe jedes Blocks angegeben, dann die Anzahl der Blöcke. Werden Datendateien verwendet, deren Puffer eine Datenstruktur ist, wird der Parameter Groesse typischerweise durch ein sizeof() der Klasse bestimmt und die Anzahl n auf 1 gesetzt. Wird dagegen eine Textdatei verarbeitet, ist es sinnvoll, Groesse auf 1 zu setzen und die Anzahl n auf die gewünschte Puffergröße zu setzen. Der Rückgabewert beider Funktionen ist die Anzahl der Blöcke, die tatsächlich verarbeitet werden konnten. Stimmt n also mit dem Rückgabewert nicht überein, ist etwas schief gegangen. Der letzte Parameter ist das Datei-Handle.

Textdateien

Wenn Sie mit Textdateien arbeiten, gibt es zwei spezielle Funktionen, die für diesen Zweck etwas einfacher in der Handhabung sind: fgets() und fputs(). Die Funktion fgets() liest eine Textzeile in einen Puffer. Die Länge wird durch einen Parameter begrenzt, um einen Pufferüberlauf zu vermeiden. Die Funktion fputs() schreibt eine Zeichenkette in die Datei. Dabei wird die Länge durch eine 0 begrenzt, so wie das bei C-Strings üblich ist:

char *fgets(char *Puffer, int Groesse, FILE *f);
char *fputs(char *Puffer, FILE *f);

Mit der Funktion fprintf() ist es möglich, die Daten formatiert in eine Datei zu schreiben. Abgesehen von dem Datei-Handle entsprechen die Parameter exakt der Funktion printf().

char *fprintf(FILE *f, char *Format, ...);

Positionieren

Mit der Funktion fseek() kann der Dateipositionszeiger verschoben werden. Bei den C-Dateien gibt es nur einen gemeinsamen Positionszeiger für das Lesen und Schreiben.

int fseek(FILE *f, long Offset, int RelPos);

Der erste Parameter ist das Datei-Handle, der zweite ist der Abstand, und der dritte ist eine Konstante, die beschreibt, worauf sich der Abstand bezieht.

[fseek(): relative Position]
Konstante Bedeutung
SEEK_SET Ab dem Anfang der Datei
SEEK_CUR Ab der aktuellen Position
SEEK_END Ab dem Ende der Datei

End Of File

Die Funktion feof() ermittelt, ob das Ende der Datei erreicht wurde. Als einzigen Parameter hat die Funktion das Datei-Handle. Der Rückgabewert ist 0, wenn das Ende der Datei nicht erreicht wurde.

int feof(FILE *f);

Dateisystemkommandos

Die folgenden Aufrufe gehören zum ANSI-Standard für C und C++ und ermöglichen dem Programm den grundlegenden Umgang mit dem Dateisystem.

Datei löschen: remove

Die Funktion remove() löscht die Datei, deren Name ihr als Parameter übergeben wird.

#include <stdio.h>
int remove(const char *dateiname);

Die globale Variable errno

Die Funktion gibt 0 bei Erfolg und --1 bei einem Fehler zurück. Die Fehlerursache steht in der Variablen errno. Die meisten Systemaufrufe liefern einen Rückgabewert kleiner als 0, wenn etwas schief gelaufen ist. Ist der Rückgabewert nicht aussagekräftig genug, wird die Fehlerursache in der globalen Variable errno kodiert. Die Konstanten, die errno annehmen kann, stehen in der Header-Datei errno.h zur Verfügung. So kann beim Aufruf von remove() unter anderem eine der folgenden Konstanten in der Variable errno zu finden sein: [Fehlerkonstanten]
Konstante Bedeutung
EACCES Fehlende Schreibberechtigung
ENOENT Diese Datei gibt es nicht.
EROFS Datei befindet sich auf einem schreibgeschützten Medium.

Umbenennen: rename

Die Funktion zum Umbenennen von Dateien heißt rename(). Unter UNIX ist es mit dieser Funktion auch möglich, Dateien innerhalb des gleichen Dateisystems zu verschieben.

#include <stdio.h>
int rename(const char *dateiname, const char *neuname);

Die Funktion gibt 0 bei Erfolg und --1 bei einem Fehler zurück. Die Fehlerursache steht in der Variablen errno.

Weitere Funktionen

Die Kommandos zum Umgang mit Verzeichnissen gehören leider nicht zum ANSI-Standard. Unter UNIX sind sie in jedem Fall verfügbar und nach POSIX [2] standardisiert. Auch unter Windows werden sie von vielen Compilern unterstützt. Aus diesem Grund sind hier die Stichwörter aufgezählt, die Sie benötigen, um in der Hilfe Ihres Compilers nachzuschlagen, ob die Funktionen unterstützt werden. Unter UNIX geben Sie das Kommando man, gefolgt von der Funktion, an und erhalten den Prototyp und die Beschreibung der Funktion. [Verzeichnisfunktionen]
Funktion Wirkung
mkdir Erzeugt ein Verzeichnis
chdir Wechselt das Verzeichnis
rmdir Löscht ein leeres Verzeichnis
opendir Öffnet ein Verzeichnis zur Suche nach Dateien
readdir Liest den nächsten Eintrag im Verzeichnis
closedir Schließt das Verzeichnis wieder

Verzeichnis

Das Auslesen eines Verzeichnisses ist zwar durch den ANSI-Standard nicht abgedeckt, aber immerhin durch POSIX. Dieser Standard für UNIX wird auch von einigen C-Compilern implementiert. Da das Auslesen von Verzeichnissen immer wieder benötigt wird, soll es dennoch zumindest kurz behandelt werden. Hier folgt ein kleines Programm, das alle Dateinamen eines Verzeichnisses auf dem Bildschirm ausgibt.

[Auslesen eines Verzeichnisses (dir.cpp)]

#include <dirent.h>
#include <iostream>
using namespace std;

int main()
{
    DIR *hdir;
    struct dirent *entry;

    hdir = opendir(".");
    do
    {
        entry = readdir(hdir);
        if (entry)
        {
            cout << entry->d_name << endl;
        }
    } while (entry);
    closedir(hdir);
}

Der Zeiger auf DIR ist das benötigte Handle auf das Verzeichnis. Die Funktion opendir() erhält als Parameter den Verzeichnisnamen. Bei manchen Compilern gab es Schwierigkeiten, andere Verzeichnisse als das aktuelle Verzeichnis auszulesen. Das sollten Sie bei Ihrem Compiler testen. Nach dem Eröffnen des Verzeichnisses liefert die Funktion readdir() entweder einen Zeiger auf eine Variable der Verzeichniseintragsstruktur dirent oder eine 0, wenn kein weiterer Eintrag im Verzeichnis existiert. Zu guter Letzt wird das Verzeichnis wieder geschlossen. Das einzige im POSIX-Standard garantierte Element von dirent ist der Dateiname unter d_name. Das ist allerdings auch ausreichend, denn über den Namen lassen sich beispielsweise mit der Funktion stat() weitere Informationen ermitteln.

Datei-Eigenschaften ermitteln

Der UNIX-Systemaufruf stat() ist mit der C-Bibliothek auch auf andere Systeme portiert worden. Allerdings gehört diese Funktion nicht zum ANSI-Standard. Mit diesem Aufruf können Informationen über Dateien erlangt werden. Der Funktionsaufruf ist unter UNIX auf jeden Fall verfügbar. Unter Windows verstehen ihn die Borland-Compiler und natürlich die GNU-Compiler. Visual C++ bietet den Aufruf unter dem Namen _stat() an.

stat()

Mit den Funktionen stat() und fstat() können Sie Informationen über eine Datei ermitteln. Die Ergebnisse werden in einer Struktur vom Typ stat
[3] abgelegt. Sie müssen eine Variable dieses Typs anlegen und deren Adresse der Funktion stat() übergeben. Die Funktion hat folgenden Prototyp:

#include <sys/types.h>
#include <sys/stat.h>
int  stat(char *dateiname,   struct stat *puffer);

Die Ergebnisse stehen in der Variablen von Typ stat, auf die der Parameter puffer zeigt. Die Definition der Struktur beinhaltet die folgenden Elemente:

struct stat {
  dev_t  st_dev     /* (P) Device, auf dem die Datei liegt */
  ushort st_ino     /* (P) i-node-Nummer */
  ushort st_mode    /* (P) Dateityp  */
  short  st_nlink   /* (P) Anzahl der Links der Datei  */
  ushort st_uid     /* (P) Eigentuemer-User-ID (uid)  */
  ushort st_gid     /* (P) Gruppen-ID (gid)  */
  dev_t  st_rdev    /* Major- und Minornumber, falls Device */
  off_t  st_size    /* (P) Größe in Byte  */
  time_t st_atime   /* (P) Zeitpunkt letzter Zugriffs  */
  time_t st_mtime   /* (P) Zeitpunkt letzte Änderung  */
  time_t st_ctime   /* (P) Zeitpunkt letzte Statusänderung */
};

Die Bestandteile dieser Struktur können sich je nach System unterscheiden. Die mit (P) gekennzeichneten Elemente sind aber unter UNIX zwingend von POSIX vorgeschrieben.

st_dev und st_ino
Unter UNIX beschreiben st_dev und st_ino eindeutig den Ort einer Datei. st_dev ist das Device, bei Festplatten also die Partition. st_ino bezeichnet den i-node, eine unter UNIX eindeutige Kennnummer für Dateien einer Partition. Unter Windows wird in st_dev die Nummer des Laufwerks hinterlegt. st_ino ist immer 0.
st_mode
Die rechten zwölf Bits von st_mode beschreiben, ob das Programm die Datei schreiben, lesen oder ausführen kann. Unter UNIX können mit diesen Informationen die Rechte für Benutzer, Gruppen und den Rest der Welt unterschieden werden. Tabelle beschreibt, wie die rechten neun Bits von st_mode die Rechte kodieren. [4] [Rechte für eine Datei]
Benutzer Gruppe Welt
Lesen 4 4 4
Schreiben 2 2 2
Ausführen 1 1 1
Unter Windows können die Benutzerrechte ausgewertet werden. Sie geben Auskunft, ob die Datei geschrieben und gelesen werden kann. Über die Dateierweiterung ermittelt Windows, ob die Datei ausführbar ist, und stellt diese Information in st_mode zur Verfügung. In den nächsten vier Bits wird codiert, welchen Typ die Datei hat. Um beides zu trennen, gibt es die Konstante S_IFMT. Mit ihr können Sie eine Maske über diese Bits setzen. Anschließend können Sie den Wert mit folgenden Konstanten vergleichen: [Konstanten für den Dateityp]
Konstante Dateityp
S_IFSOCK Sockets
S_IFLNK Symbolische Links
S_IFREG Reguläre Dateien
S_IFBLK Block-Devices
S_IFDIR Verzeichnisse
S_IFCHR Char-Devices
S_IFIFO FIFOs
Das folgende Beispielprogramm ermittelt für die als ersten Parameter übergebene Datei die Rechte und stellt anschließend fest, ob es sich um eine Datei, einen symbolischen Link (Symbolische Links sind eine Besonderheit der Dateisysteme von UNIX, die es unter MS-Windows nicht gibt.) oder ein Verzeichnis handelt.

[Dateityp und Rechte bestimmen]

#include <sys/types.h>
#include <sys/stat.h>

int main(int argc, char **argv)
{
   struct stat Status;
   int Dateityp;
   stat(argv[1], &Status);
   printf("Dateirechte: %o \n", Status.st_mode & ~S_IFMT);
   Dateityp = Status.st_mode & S_IFMT;
   switch (Dateityp) {
     case S_IFREG: puts("Datei"); break;
     case S_IFLNK: puts("Symbolischer Link"); break;
     case S_IFDIR: puts("Verzeichnis"); break;
     default: puts("Sonstiges");
   }
}

Die Konstanten S_IFREG (reguläre Dateien) und S_IFDIR (Verzeichnisse) werden auch unter Windows unterstützt. Damit können also Dateien und Verzeichnisse unterschieden werden. Die anderen Dateitypen sind unter Windows unbekannt.

st_nlink
In st_nlink steht, wie viele harte Links auf die Datei zeigen. Da Windows keine Links anlegen kann, ist dieser Wert dort immer 1.
st_uid und st_gid
Mit st_uid und st_gid werden der Besitzer und die Besitzergruppe ermittelt. Der Wert ist eine Zahl, nämlich die, die in der Datei /etc/passwd bzw. in /etc/group festgelegt wird. Unter Windows steht hier immer 0.
st_rdev
In st_rdev ist die Major- und Minornummer codiert, sofern es sich bei der Datei um ein Device handelt. Unter Windows steht hier die Laufwerksnummer.
st_size
Sofern es sich bei der Datei um eine reguläre Datei handelt, finden Sie in st_size die Größe in Bytes.
st_atime, st_mtime und st_ctime
Jeder lesende oder schreibende Zugriff auf die Datei aktualisiert den Wert st_atime. Jede Veränderung des Dateiinhalts wird in st_mtime notiert. Der Zeitpunkt der Änderungen des Benutzers, der Rechte, der Linkzahl oder von Ähnlichem (also von allem, was nicht den Inhalt betrifft) wird in st_ctime festgehalten.

[1]
Ein Prozess ist ein gestartetes Programm. Wird ein Programm mehrfach gestartet, laufen mehrere Prozesse desselben Programms.
[2]
POSIX (Portable Operating System Interface) ist eine Familie von Standards für die UNIX-Schnittstellen, die vom IEEE (Institute for Electrical und Electronic Engineers) verbindlich festgelegt wurden.
[3]
Unter Visual C++ heißt auch die Struktur _stat.
[4]
Die fehlenden Bits sind extrem UNIX-spezifisch.


Informatik-Ecke Einstieg in C++ (C) Copyright 2005 Arnold Willemer