Dateien

  • Was sind Dateien aus Sicht eines C-Programms?
  • Die Größe einer Datei herausfinden
  • Eine Datei in den Speicher kopieren
  • Daten auf die Festplatte schreiben
  • Text- und Binärdateien
  • Formatierte Textausgaben

Was sind Dateien aus Sicht eines C-Programms?

Dateien sind aus Sicht des C-Programms in erster Linie schonmal nicht erreichbar, denn sie liegen auf der Festplatte, während ein Programm Daten nur aus dem Speicher verarbeiten kann. Wir müssen also irgendwie den Inhalt von Dateien in den Speicher kopieren, bzw. später den Inhalt des Speichers auf die Festplatte schreiben.

Wir kopieren also Inhalte in den Speicher, dafür müssen wir erstmal überhaupt Speicher haben. Wie wir an Speicher kommen, haben wir ja bereits zuvor gelernt. Sobald die Daten im Speicher sind, können wir darauf wie auf ein Array zugreifen.

Trotzdem gibt es bei Dateien noch etwas besonderes zu beachten: Dateien können sehr klein sein, aber auch sehr groß. Eventuell sogar größer als den Arbeitsspeicher, den wir zur Verfügung haben. Eventuell können wir eine Datei also nicht am Stück einlesen, sondern müssen uns damit begnügen, nur eine Teil der Datei zu lesen, diesen dann zu bearbeiten und anschließend wieder zu vergessen, während wir den nächsten Part laden.

Gehen wir zunächst davon aus, dass die Datei, die wir bearbeiten wollen so klein ist, dass sie in den Speicher passen wird. Also müssen wir zunächst herausfinden, wie groß die Datei ist.

Die Größe einer Datei herausfinden

Hier hat wohl jedes Betriebsystem eine schicke Funktion für, die das folgende zusammenfasst. Da wir uns hier mit C beschäftigen, beschäftigen wir uns mit dem, was auf allen Computern - egal ob Linux, Mac oder Windows - funktioniert.

Zunächst müssen wir den Pfad zu der Datei wissen, die wir testen wollen. Der Einfachheit halber empfehle ich euch einen Texteditor zu nehmen „Hallo proggen.org“ zu schreiben und die Datei unter datei.txt zu speichern. Die Datei datei.txt muss im gleichen Verzeichnis liegen wie das kompilierte Programm, das wir uns gleich ansehen werden.

Wir können einen beliebigen Pfad angeben. Unter Unix (Linux/Mac) ist das kein Problem. Windows-Benutzer müssen hier aufpassen, denn hier gibt es eine typische Stolperfalle:

char Pfad[]="C:\datei.txt";

ist nämlich nicht der gewünschte Pfad, wie er scheint. Wir erinnern uns, dass der Backslash ('\') ein besonderes Zeichen ist, dass das nachfolgende Zeichen anders interpretiert - \0 für das Nullbyte oder \n für ein New-Line-Zeichen. Um genau einen Backslash zu erhalten, müssen wir zwei Backslashs schreiben. Der richtige Pfad im C-Programm lautet also:

char Pfad[]="C:\\datei.txt";

Das gilt auch für die Linux- und Mac-Benutzer, aber sie haben den Vorteil, dass der Backslash hier nicht zum Trennen von Verzeichnissen verwendet wird, also das Problem hier nicht so dringend zu erwarten ist.

Zunächst müssen wir dem Betriebsystem sagen, dass wir Zugriff auf eine Datei wünschen. Dies geht mit der Funktion fopen() (File Open), auch hierfür wird die Headerdatei stdio.h benötigt. Wie beim Arbeitsspeicher müssen wir eine geöffnete Datei wieder freigeben, wenn wir sie nicht länger benötigen. Hierfür wird die Funktion fclose() (File Close) verwendet. fopen() gibt ein Handle zurück, mit dem man sich auf die geöffnete Datei beziehen kann - ähnlich des Zeigers auf den Speicher bei malloc(). Und wie bei malloc() gilt, dass wenn das Handle NULL ist, dass etwas schief gegangen ist.

Wenn Du wissen willst, was passiert ist, so findet sich das in der Fehlerbehandlung der Standard-C-Library. Dort findet sich auch ein Beispielprogramm, dass den Zugriff auf eine nicht existierende Datei beschreibt.

Schauen wir uns das ganze als Quelltext an, wie er als Beispiel in fopen() aufgeführt wird.

#include <stdio.h>
#include <stdlib.h>
 
int main (void)
{
  FILE *file = fopen("testfile.txt", "r");
  // (...)
  fclose(file);
  return EXIT_SUCCESS;
}

fopen() benötigt den Dateinamen und einen zweiten String, der beschreibt, wie die Datei geöffnet werden soll. Für Details schau Dir die Tabelle in der Beschreibung der fopen()-Funktion an. Wenn wir als Modus „r“ oder „w“ gewählt haben, befinden wir uns am Anfang der Datei. Mit „a“ befinden wir uns am Ende der Datei.

Wo wir genau sind, können wir mit der Funktion ftell() erfragen.

Um die Größe einer Datei herauszufinden werden wir nun einen kleinen Trick verwenden. Im Arbeitsspeicher können wir uns frei Bewegen, wir können mal am Anfang eines Arrays zugreifen und mal am Ende. Das geht bei einer Datei mit Hilfe der Funktion fseek() ebenfalls, aber man sollte damit vorsichtiger umgehen, denn Festplatten müssen sonst eventuell häufig hin- und herspringen und wenn die Festplatte „rödelt“ hilft das nicht, um das Programm schnell ablaufen zu lassen.

#include <stdio.h>
 
int main (void)
{
  FILE *file = fopen( "datei.txt", "a" );
 
  if( file )
  {
    long int size = ftell( file );
    printf( "Wir befinden uns an Position %ld, die Datei ist %ld Byte groß.\n", size, size );
 
    fclose(file);
  }
  else
    printf( "Datei konnte nicht geöffnet werden.\n" );
 
  return 0;
}

Der Modus „a“ stellt uns hinter das letzte Byte, denn „a“ ist ja eigentlich dafür da, hinter der Datei anzufügen (a=append auf Deutsch: anfügen). Damit man etwas anfügen kann, muss die Datei allerdings existieren, das bedeutet, wenn sie nicht existiert, wird sie von fopen() angelegt. Wenn wir eine nicht existierende Datei nicht anlegen wollen, so müssen wir sie mit dem Modus „r“ öffnen - wir befinden uns dann allerdings am Anfang der Datei. Hier kommt fseek() ins Spiel.

Und jetzt kommen wir zu einer kleinen Gemeinheit: Unter Linux funktioniert das Programm erwartungsgemäß, aber unter Windows liefert die Funktion ftell() den Wert 0 zurück. Es muss zu erst geschrieben werden, bevor ftell() ein richtiges Ergebnis liefert! Versuchen wir etwas, womit auch Windows zuverlässig zurecht kommt.

Mit der Funktion fseek() kann man sich entweder an eine gewünschte Stelle positionieren, ähnlich einem Arrayzugriff, weil man vom Beginn der Datei guckt, man kann sich relativ bewegen - also wenn man bereits 100 Bytes gelesen hat, kann man sich 10 Bytes vor oder zurück bewegen - oder man kann sich abhängig vom Ende der Datei positionieren. Wie das genau geht ist auf der Seite für fseek() beschrieben.

Die Kombination von ftell() und fseek() lässt uns zum Ende der Datei springen und dort fragen, wo wir gerade sind. Wir können so zum Beispiel eine Datei mit dem Modus „r“ per fopen() zum Lesen öffnen, uns dann mit fseek() an das Ende der Datei setzen lassen und dann mit ftell() fragen, wo wir eigentlich sind. Anschließend - um die Datei lesen zu können, setzen wir uns mit fseek() wieder zurück an den Anfang:

#include <stdio.h>
 
int main (void)
{
  FILE *file = fopen( "datei.txt", "r" );
 
  if( file )
  {
    fseek( file, 0, SEEK_END );
    long int size = ftell( file );
    printf( "Wir befinden uns an Position %ld, die Datei ist %ld Byte groß.\n", size, size );
 
    fclose(file);
  }
  else
    printf( "Datei konnte nicht geöffnet werden.\n" );
 
  return 0;
}

Eine Datei in den Speicher kopieren

Wir wissen nun, wie man eine Datei öffnet und dass wir sie in den Speicher kopieren können. Die Frage ist nun ganz einfach: welchen Befehl brauchen wir hierfür? Die Antwort ist einfach: fread().

Mit fread fordern wir das Betriebsystem auf, eine Datei zu laden und in den Speicher zu kopieren. Die Funktion benötigt den Zeiger, wohin die Daten geschrieben werden sollen und natürlich das Handle der geöffneten Datei. Zusätzlich werden zwei Parameter benötigt, die die Größe der zu lesenden Daten beschreiben. Wir haben hier zunächst die Größe eines Blocks - wenn wir einen Text laden, dann ist ein Buchstabe 1 Byte groß (bzw. sizeof( char )) und können dann angeben, wieviele 1 Byte große Blöcke wir laden wollen.

Laden wir also einmal die komplette Datei:

#include <stdio.h>
#include <stdlib.h>
 
int main (void)
{
  FILE *file = fopen("datei.txt", "r");
 
  if( file )
  {
    fseek( file, 0, SEEK_END );
    long int size = ftell( file );
    printf( "Wir befinden uns an Position %ld, die Datei ist also bisher %ld Byte groß.\n", size, size-1 );
    fseek( file, 0, SEEK_SET );
 
    char * buffer = (char *) malloc( size );
    if( buffer )
    {
      long int read = fread( buffer, sizeof( char ), size-1, file );
 
      if( read != size - 1 )
        printf( "%ld sollten gelesen werden, es wurden aber nur %ld Bytes gelesen.\n", size-1, read );
 
      buffer[ read ] = '\0';
 
      printf( "Text gelesen:\n%s\n", buffer );
    } 
    else
      printf( "Speicher konnte nicht angefordert werden.\n" );
 
    fclose(file);
  }
  else
    printf( "Datei konnte nicht geöffnet werden.\n" );
 
  return EXIT_SUCCESS;
}

fread() gibt als Rückgabewert, wieviele Blöcke (die hier sizeof( char ), also jeweils 1 Byte, groß sind) transportiert wurden

Wenn wir die Daten nur kurz brauchen können wir auch die Datei auslesen, verarbeiten und auch gleich wieder vergessen. So können wir beispielsweise einen Text auf dem Bildschirm ausgeben, der größer als unser Hauptspeicher ist, denn wir müssen die Datei nicht vollständig in den Speicher laden. Unsere Datei mit dem Inhalt „Hello proggen.org“ dürfte aber problemlos in den Speicher passen. Geizen wir aber einfach mal mit Speicher und nehmen uns nur 2 Bytes Speicher, um die Datei auszugeben:

#include <stdio.h>
#include <stdlib.h>
 
int main (void)
{
  FILE *file = fopen("datei.txt", "r");
 
  if( file )
  {
    long int read;
    char buffer[2];
 
    while(( read = fread( buffer, sizeof( char ), 2, file ) ))
      printf( "%*s", read, buffer );
 
    printf( "\n" );
 
    fclose(file);
  }
  else
    printf( "Datei konnte nicht geöffnet werden.\n" );
 
  return EXIT_SUCCESS;
}

Das Programm ist schonmal deutlich kürzer. Wir brauchen keinen Speicher zu alloziieren, denn wir nehmen ja nur die 2 Byte als lokale Variable. Das printf() sieht merkwürdig aus, der formatstring sieht ungewöhnlich aus, genau das Beispiel habe ich unter formatstring beim Kapitel Feldbreite beschrieben. Hier wird nicht nur ein Text ausgegeben, sondern er wird in ein Feld ausgegeben, das deutet der '*' bei printf an. Vor dem String gibt man die Breite eines Feldes an. Wenn wir keine Daten bekommen haben, kommen wir am printf() nicht mehr an, weil wir die Schleife verlassen haben. read muss also den Wert 1 oder 2 haben, entsprechend werden die gelesenen Zeichen in ein Feld von ein oder zwei Zeichen Breite gesetzt. Das ist wichtig, da wir hier kein Nullbyte am Ende der zwei Bytes haben. Das ganze können wir auch mit einem zusätzlichen Byte regeln, so dass wir abschließend immer ein Nullbyte haben:

    char buffer[3];              // <- drei statt nur zwei Byte
    while(( read = fread( buffer, 1, 2, file ) ))
    {
      buffer[ read ] = '\0';
      printf( "%s", buffer );
    }

Datenträger werden in der Regel in Blöcke unterteilt, besonders häufig sind dabei Blockgrößen von 512 Bytes und 4096 Bytes. Mit 2 Bytes muss der Computer genau die 2 Byte aus einem Block frickeln, die wir in diesem Moment haben wollen. Das Betriebsystem wird das gut optimiert haben, dennoch ist es schneller, wenn wir Daten möglichst am Stück lesen. Daher empfiehlt es sich für das Vorgehen Dateien stückweise einzulesen, wenn man die Größe eines Blocks (512 Bytes bzw. 4096 Bytes) oder ein ganzzahliges Vielfaches der Blockgröße am Stück einliest. Meine Empfehlung ist daher bei Texten 4096 Bytes am Stück zu laden. Wenn die Datei kleiner ist, liefert fread() entsprechend die Anzahl der Blöcke zurück, die gelesen werden konnten.

Ab jetzt können wir Dateien lesen, am Stück oder auch in Teilen. :-)

Daten auf die Festplatte schreiben

Nun können wir Dateien lesen, jetzt wollen wir uns ansehen, wie man Daten auf die Festplatte bekommt. Das ganze funktioniert genauso wie beim Lesen, nur dass diesmal der Speicher auf die Festplatte kopiert wird.

Die Funktion heißt entsprechend fwrite().

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
 
int main (void)
{
  FILE *file = fopen("datei2.txt", "w");
  char const * buffer="Hallo proggen.org\n";
 
  if( file )
  {
    long int written;
    long int toWrite = strlen( buffer );
 
    written = fwrite( buffer, 1, toWrite, file );
 
    if( written != toWrite )
      printf( "Schreibvorgang ist fehlgeschlagen\n" );
 
    fclose(file);
  }
  else
    printf( "Datei konnte nicht geöffnet werden.\n" );
 
  return EXIT_SUCCESS;
}

Nachdem das Programm ausgeführt wurde, findet sich auf der Festplatte eine zweite Datei: datei2.txt, die den Inhalt „Hallo proggen.org“ enthält, sowie ein New-Line-Zeichen. Wie bei fread() werden bei fwrite() zuerst der Buffer, der nun ausgelesen wird, die Größe eines Datensatzes (1 Byte, entspricht sizeof( char )) und die Anzahl der Datensätze angegeben. Abschließend wird das Filehandle übergeben.

Es ist wichtig, dass das Filehandle die Erlaubnis zum Schreiben der Datei hat. Eine Datei, die nur mit „r“ geöffnet wird, kann nicht beschrieben werden. Hier brauchen wir „r+“, „w“ oder „a“, wie die einzelnen Modi in fopen() beschrieben sind.

Formatierte Textausgaben

Wir können nun Texte lesen und schreiben, aber nur als Blöcke: Wir müssen also zuerst einen Text-Block zusammenstellen, zum Beispiel mit sprintf(). Nehmen wir an, file wäre das gültige File-Handle.

char buffer[512];
 
int wert = 4711;
char const * varname = "Wert";
 
sprintf( buffer, "%s=%d", varname, wert );
 
fwrite( buffer, sizeof( char ), strlen( buffer ), file );

Oder man schreibt eben viele Blöcke:

int wert = 4711;
char const * varname = "Wert";
char const * wertAsString = "4711";
 
fwrite( varname, sizeof( char ), strlen( varname ), file );
fwrite( "=", sizeof( char ), 1, file );
fwrite( wertAsString, sizeof( char ), strlen( wertAsString ), file );

Schön ist das alles nicht. Aber die Standard-C-Library hat dafür auch eine Funktion, die mit der Funktion fprintf() beides kombiniert, die ähnlich wie printf() und sprintf() funktioniert, aber nicht auf den Bildschirm oder in den Speicher, sondern diesmal direkt in die Datei schreibt.

int wert = 4711;
char const * varname = "Wert";
 
fprintf( file, "%s=%d\n", varname, wert );

Wie es scanf() gibt, um vom Bildschirm einzulesen, gibt es auch eine fscanf()-Funktion. Allerdings rate ich dazu, solche Aufgaben durch selbstgeschriebene Parser zu erledigen, da die scanf()-Funktionen nur dann zuverlässig arbeiten, wenn die Eingaben absolut korrekt sind. Als Entwickler kann man allerdings nicht garantieren, dass der User falsche Eingaben macht oder eine Datei manipuliert hat. Dies zu überprüfen erfordert einen selbst geschriebenen Parser. Hier kann die Funktion strtok() hilfreich sein. Allgemein rate ich dazu, sich mal einen Überblick über die Standard-C-Library zu verschaffen.

Text- und Binärdateien

Manche Betriebssysteme unterscheiden zwischen Text- und Binärdateien. Eine Textdatei besteht aus mehreren Zeilen und enthält keine Nullbytes. Für Textdateien eignen sich Funktionen wie fputs() und fgets() um Textzeilen zu schreiben und zu lesen, entsprechend durch NewLine-Zeichen getrennt. Beispiele für Textdateien sind - oh Wunder - .txt-Dateien, INI-Dateien oder HTML-Dateien. Bei einer Binärdatei hingegen kann das NewLine-Zeichen jede beliebige Bedeutung haben und es können jederzeit Nullbytes auftreten. Wie eine Binärdatei interpretiert wird, entscheidet alleine das Programm, dass die Datei einliest.

Binärdateien sind beispielsweise Grafiken, Musikdateien oder auch ausführbare Programme. Ausführbare Programme werden geladen und vom Prozessor interpretiert. Eine Grafik- oder Musikdatei wird geladen, dekodiert und anschließend an die Grafikkarte bzw. Soundkarte übergeben.

Um Binärdateien zu unterscheiden, finden sich häufig am Anfang der Datei einige besondere Kennzeichner. Ich habe hier mal zufällig zwei JPEG-Bilder ausgewählt und die ersten 16 Bytes mit dem Linux-Befehl hd (HexDump) ausgegeben:

xin@trinity:~$ hd -n 16 example.jpg 
00000000  ff d8 ff e0 00 10 4a 46  49 46 00 01 01 00 00 01  |......JFIF......|
00000010
xin@trinity:~$ hd -n 16 example2.jpg 
00000000  ff d8 ff e0 00 10 4a 46  49 46 00 01 01 01 00 48  |......JFIF.....H|
00000010

Die erste lange Zahl gibt an, wo sich die nachfolgenden Daten in der Datei befinden - nämlich am Anfang (00000000). Es folgen 16 hexadezimale Zahlen, die 16 Bytes entsprechen und dahinter zwischen den bei beiden vertikalen Strichen die Darstellung der lesbaren Zeichen. Alle nicht lesbaren Zeichen werden durch einen Punkt dargestellt.

Hier zeigt sich schon, dass die ersten 15 Bytes bei beiden Dateien identisch sind.

Schauen wir uns das ganze mal mit MP3-Dateien an:

xin@trinity:~$ hd -n 16 example.mp3 
00000000  49 44 33 04 00 00 00 00  08 20 54 49 54 32 00 00  |ID3...... TIT2..|
00000010
xin@trinity:~$ hd -n 16 example2.mp3 
00000000  49 44 33 03 00 00 00 00  1f 76 54 49 54 32 00 00  |ID3......vTIT2..|
00000010

Und hier haben wir noch zwei ausführbare Dateien unter Linux.

xin@trinity:~$ hd -n 16 /bin/bash
00000000  7f 45 4c 46 02 01 01 00  00 00 00 00 00 00 00 00  |.ELF............|
00000010
xin@trinity:~$ hd -n 16 /bin/cat
00000000  7f 45 4c 46 02 01 01 00  00 00 00 00 00 00 00 00  |.ELF............|
00000010

Auch hier lassen sich schon leicht Ähnlichkeiten zwischen den beiden MP3-Dateien aufzeigen. Diese helfen dem Betriebssystem die Dateien einzuordnen. So wird ein JPEG-Bild oder eine MP3-Datei vom Betriebsystem niemals als ausführbares Programm interpretiert. Bei allen Dateien finden sich reichlich Nullbytes, die in einer Textdatei so nicht vorkommen würden. Es sind Binärdateien, die im Gegensatz zu Textdateien eben nicht menschenlesbar sind.

Möchte man mit Binärdateien arbeiten sollte man bei fopen() „b“ an den Modus anzufügen, zum Beispiel „rb“ oder „r+b“:

FILE * file;
 
file = fopen( "datei.bin", "r+b" );

Gelesen und geschrieben werden binäre Dateien genauso mit fread() und fwrite(). Hier zeigt sich jetzt auch endlich, wieso fread() und fwrite() Blockgrößen und eine Blockanzahl als Parameter erwarten.

Schauen wir uns eine übliche Binärdatei an. Wir wollen die Koordinaten von Linien oder Dreiecken speichern. Und wir brauchen einen Header, der das Format kennzeichnet und uns darüber informiert, wieviele Objekte zu lesen sind.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
 
struct Header
{
  char id[20];
 
  unsigned short objects;
};
 
struct Segment
{
  unsigned short xStart;
  unsigned short yStart;
  unsigned short xStop;
  unsigned short yStop;
};
 
struct Triangle
{
  unsigned short x1;
  unsigned short y1;
  unsigned short x2;
  unsigned short y2;
  unsigned short x3;
  unsigned short y3;
};
 
void writeSegment( FILE * file, struct Segment * segment, unsigned int count )
{
  unsigned short type = 1;                                    /* Segmentidentifikation */
 
  fwrite( &type, sizeof( unsigned short ), 1, file );         /* Typinformation */
  fwrite( segment, sizeof( struct Segment ), count, file );   /* Daten */
}
 
void writeTriangle( FILE * file, struct Triangle * triangle, unsigned int count )
{
  unsigned short type = 2;                                    /* Dreieckeidentifikation */
 
  fwrite( &type, sizeof( unsigned short ), 1, file );         /* Typinformation */
  fwrite( triangle, sizeof( struct Triangle ), count, file ); /* Daten */
}
 
int main( void )
{
  FILE * file = fopen( "datei.bin", "w+b" );
 
  if( file )
  {
    struct Header header;
 
    /* zu schreibende Daten vorbereiten */
 
    struct Segment  segment1   =   {  0,  0, 10,  0 };
    struct Segment  segments[] = { { 10,  0, 10, 10 },
                                   { 10, 10,  0,  0 } };
 
    struct Triangle triangle   =   { 20,  0, 30, 0, 30, 10 };
 
    /* Header vorbereiten */
 
    strcpy( header.id, "proggen.org binfile" /* 17 Bytes + 1 Nullbyte */ );
    header.objects = 4;
 
    /* Header schreiben */
 
    fwrite( &header, sizeof( struct Header ), 1, file );
 
    /* Daten schreiben */
 
    writeSegment ( file, &segment1, 1 );  /* einzelnes Segment */
    writeTriangle( file, &triangle, 1 );  /* einzelnes Dreieck */
    writeSegment ( file, segments, 2 );  /* Datenarray schreiben */
 
    /* Datei schließen */
 
    fclose( file );
  }
 
  return EXIT_SUCCESS;
}

Schauen wir uns nun die Datei an:

xin@trinity:~/workspace/proggen.org/c/tutorial/file$ gcc binwrite.c 
xin@trinity:~/workspace/proggen.org/c/tutorial/file$ ./a.out 
xin@trinity:~/workspace/proggen.org/c/tutorial/file$ ls -l datei.bin 
-rw-r--r-- 1 xin xin 66 25. Mai 23:22 datei.bin
xin@trinity:~/workspace/proggen.org/c/tutorial/file$ hd datei.bin 
00000000  70 72 6f 67 67 65 6e 2e  6f 72 67 20 62 69 6e 66  |proggen.org binf|
00000010  69 6c 65 00 04 00 01 00  00 00 00 00 0a 00 00 00  |ile.............|
00000020  02 00 14 00 00 00 1e 00  00 00 1e 00 0a 00 01 00  |................|
00000030  0a 00 00 00 0a 00 0a 00  01 00 0a 00 0a 00 00 00  |................|
00000040  00 00                                             |..|
00000042

Wir erkennen den Header oben. Die ersten 20 Byte enthalten „proggen.org binfile“, In der zweiten Zeile erkennen wir in der Bytedarstellung „ile“ und Nullbyte: 69 6c 65 00.

Es folgt die 2 Byte große unsigned-Short-Zahl „04 00“: 4. Anschließend der Typ „01 00“ - 1: ein Segment. Es folgen die Koordinaten: 00 00/00 00 nach 00 00/0a 00. 0a ist eine hexadezimale Zahl und hat den Wert 10. Auf Intel-Systemen (und kompatiblen, wie z.B. AMD-Prozessoren) werden Zahlen im BigEndian-Format gespeichert.

Beginnend mit der dritten Zeile (00000020) kommt „02 00“: ein Dreick mit Punkt 1 („14 00 00 00“ = 20/0), Punkt 2 („1e 00 00 00“ ⇒ 30/0) und Punkt 3 („1e 00 0a 00“ ⇒ 30/10). Die letzten beiden Zeichen der dritten Zeile („01 00“) sagen aus, dass wieder ein Segment kommt, die Koordinaten folgen in der ersten Hälfte der 4. Zeile (00000030): „0a 00 00 00 0a 00 0a 00“: 10/0 nach 10/10. Die zweite Hälfte der dritten Zeile beginnt wieder mit dem Typen („01 00“ ⇒ 1, ein Segment) und gefolgt von den Koordinaten: „0a 00 0a 00 00 00“ und „00 00“ in der letzten Datenzeile (00000040). Das entspricht 10/10 nach 0/0.

Hiermit haben wir ein binäres Datenformat erfunden, das Koordinaten von Segmenten und Dreiecken speichern kann. Menschenlesbar ist es aber nur, wenn man verstanden hat, wie der Computer die Daten liest.

Erklären wir es dem Computer:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
 
struct Header
{
  char id[20];
 
  unsigned short objects;
};
 
struct Segment
{
  unsigned short xStart;
  unsigned short yStart;
  unsigned short xStop;
  unsigned short yStop;
};
 
struct Triangle
{
  unsigned short x1;
  unsigned short y1;
  unsigned short x2;
  unsigned short y2;
  unsigned short x3;
  unsigned short y3;
};
 
void readSegment( FILE * file )
{
  struct Segment segment;
 
  if( 1 == fread( &segment, sizeof( struct Segment ), 1, file ) )
  {
    printf( "Segment : %u/%u -> %u/%u\n", segment.xStart, segment.yStart, segment.xStop, segment.yStop );
  }
  else printf( "Fehler beim Auslesen eines Segments\n" );
}
 
void readTriangle( FILE * file )
{
  struct Triangle triangle;
 
  if( 1 == fread( &triangle, sizeof( struct Triangle ), 1, file ) )
  {
    printf( "Triangle: %u/%u -> %u/%u -> %u/%u\n", triangle.x1, triangle.y1
                                                , triangle.x2, triangle.y2
                                                , triangle.x3, triangle.y3 );
  }
  else printf( "Fehler beim Auslesen eines Segments\n" );
}
 
int main( void )
{
  FILE * file = fopen( "datei.bin", "rb" );
 
  if( file )
  {
    struct Header header;
 
    /* einen (1) Header lesen */
 
    if( 1 == fread( &header, sizeof( struct Header ), 1, file ) )
    {
      if( 0 == strcmp( "proggen.org binfile", header.id ) )
      {
        printf( "%d Elemente in der Datei\n", header.objects );
 
        unsigned int toRead = header.objects;
        unsigned short type;
 
        while( 1 == fread( &type, sizeof( unsigned short ), 1, file ) )
        {
          if( type == 1 ) readSegment( file );
          else if( type == 2 ) readTriangle( file );
        }
 
        printf( "Fertig.\n" );
      }
      else printf( "Datei ist vom falschen Format\n" );
    }
    else printf( "Header konnte nicht gelesen werden.\n" );
 
    fclose( file );
  }
 
  return EXIT_SUCCESS;
}

Schauen wir uns nun an, was dabei rumkommt:

xin@trinity:~/workspace/proggen.org/c/tutorial/file$ gcc binread.c 
xin@trinity:~/workspace/proggen.org/c/tutorial/file$ ./a.out 
4 Elemente in der Datei
Segment : 0/0 -> 10/0
Triangle: 20/0 -> 30/0 -> 30/10
Segment : 10/0 -> 10/10
Fertig.

Fertig. Wir können in einem binären Format also Datenstrukturen sehr einfach auf die Festplatte bringen und wieder laden. Das ist ein sehr großer Vorteil. Es kann auch von Vorteil sein, dass man sie gar nicht lesen kann oder selbst dann nur schwer, wenn man weiß, was die Daten bedeuten. Das kann allerdings auch ein Nachteil sein: Eine Konfigurationsdatei, die durch einen Fehler im Programm kaputt gegangen ist, kann man nur schwer reparieren - das geht mit einer menschenlesbaren Textdatei einfacher. Und wer Daten in einer Versionsverwaltung speichert, ist mit Textdateien ebenfalls besser beraten.

Ziel dieser Lektion

Eine lange Lektion, doch wir haben gesehen, wie man Text- und Binärdateien schreiben und lesen kann. Bisher haben wir die notwendigen Daten immer im Programm vorliegen gehabt, nun können wir ein Programm eine Datei laden lassen und entsprechend der Datei zu steuern. Wir haben weitere Funktionen der Standard-C-Library kennengelernt und eigentlich ist mir in dieser Lektion wichtiger, wenn Du Dich in die Lage versetzt fühlst, auch mal in die Standard-C-Library zu schauen und auszuprobieren, dann ist das mehr wert, als dass Du nach nur einer Lektion Experte im Laden und Schreiben von Dateien bist.

Dateien zu verarbeiten wird sich immer wieder ergeben. Die sichere Verwendung der Funktionen wird sich mit wachsender Erfahrung ergeben und ist nach einer vergleichsweise kurzen Lektion noch nicht zu erwarten. Daher schau Dir die Lektion aufmerksam an und probiere eigene kleine Programme aus, um Deine Erfahrung wachsen zu lassen.

In der kommenden Lektion über Programmierstil werden wir uns ein paar Details zu üblicher C-Programmierung ansehen.