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.