35. Dateien II

Binärdateien

Textdateien enthalten Text, d.h. “friedliche” Zeichen, die durch einen Zeilentrenner in relativ kurze Zeilen strukturiert wird  (das ist LF unter Unix, CRLF unter DOS, CR unter MacOS). Zahlen können so nur ineffizient gespeichert werden. Abhilfe: Binärdateien.

Anzumerken ist, dass binär geschriebene Daten im Allgemeinen nicht “portabel” sind, d.h. auf Intel-Maschinen geschriebene Daten können auf SPARC-Maschinen nicht ohne weiteres gelesen werden. Mit etwas Pech (und wenn man nicht aufpasst) können selbst verschiedene Compiler auf ein und derselben Maschine unverträgliche Binärdateien schreiben. Aber natürlich gibt es dafür Standards, und man kann Programme so schreiben, dass sie überall lesbare Binärdateien erzeugen – Dateien wie Bilder, Kompressate, Musik- oder Filmdateien sind aus Gründen der Platzersparnis in aller Regel solche Binärdateien, die, wenn sie ordentlich gemacht sind, auch nicht fragen, ob sie auf einem Atari ST oder einem PC unter BeOS geschrieben oder gelesen werden.

Lesen und Schreiben von Binärdateien:

size_t fread(void *ptr, size_t size,
  size_t nmemb, FILE *stream);
size_t fwrite(void *ptr, size_t size,
  size_t nmemb, FILE *stream);

Beide Funktionen geben zurück, wie viele Objekte sie wirklich geschrieben oder gelesen haben und nehmen eine Zeiger auf das erste Objekt, die Größe eines Objekts, die Zahl der Objekte die verarbeitet werden sollen, und die Datei, in der die Daten landen sollen. Beispiel: Eine Funktion, die ein Array von longs schreibt:

int save_longs(char *fname, long *data,
  size_t n_items)
{
  FILE *targ=fopen(fname, "wb");
  int rtval;

  if (!targ)
    return -1;
  rtval = fwrite(data, sizeof(long), n_items,
    targ)!=n_items;
  fclose(targ);
  return rtval;
}

Wir sehen hier zum ersten Mal den sizeof-Operator. Er berechnet die Größe seines Arguments in chars – das sind in der Regel Bytes. Als Argument kommen sowohl Typen als auch Variablen in Betracht. Außer bei chars sollte man niemals Annahmen über die Größe von Typen machen und diese Entscheidung immer dem Compiler (also sizeof) überlassen.

Der sizeof-Operator liefert ein Ergebnis vom Typ size_t. Das ist immer eine vorzeichenlose ganze Zahl – ihr Größe hängt allerdings von Compiler und Maschine ab. Dieser Typ wird von der Standardbibliothek immer verwendet, wenn irgendwelche Differenzen von Adressen (oder: Größen im Speicher) gebraucht werden, und deshalb gibt z.B. auch strlen einen size_t zurück. Ihr solltet size_ts wie unsigned ints verwenden, aber die Typen nicht mischen, weil ihr nicht wissen könnt, welcher int wohl groß genug ist, um die Daten zu halten.

Das ist bei printf ein gewisses Problem – wir müssen, wenn wir size_ts ausgeben wollen, ja einen Formatcode angeben, und in dem Formatcode steht dann schon drin, welchen Integer printf zu erwarten hat. Häufig ist das aufgrund der Promotion von Argumenten kein Problem, aber z.B. auf 64-bit-Maschinen mit 32-bit ints kann das ins Auge gehen. Deshalb definiert ANSI den Längencode z, der mit den restlichen Integer-Formatcodes (d, i, o, u, x und Freunde) kombiniert werden darf. Die korrekte Art, einen size_t auszugeben, ist also printf("%zu", sizeof(int)).

Material zum Spielen:

#include <stdio.h>

int main(void)
{ int arr[7], *ptr=arr;
  struct { double foo; char bar;} baz;

  printf("char: %zu, short: %zu, int: %zu, long: %zu, float: "
    "%zu, double: %zu\n", sizeof(char), sizeof(short),
    sizeof(int), sizeof(long), sizeof(float), sizeof(double));
  printf("7-arr of int: %zu, FILE: %zu\n", sizeof(arr), sizeof(FILE));
  printf("struct of double and char: %zu\n", sizeof(baz));
  printf("pointer1: %zu, pointer2: %zu\n", sizeof(ptr), sizeof(void*));
  return 0;
}

gibt am gcc/Linux i386/glibc 2.2 aus:

char: 1, short: 2, int: 4, long: 4, float: 4, double: 8
7-arr of int: 28, FILE: 148
struct of double and char: 12
pointer1: 4, pointer2: 4

Interessant vor allem die Größe des Structs – Grund für die 3 zusätzlichen Bytes ist das padding, mit dem der Compiler dafür sorgt, dass Variablen wenn möglich auf durch vier teilbaren Adressen liegen – Intel-CPUs mögen das, andere bestehen darauf.

Dass diese Größen variieren, zeigt schon der Vergleich mit gcc/MacOS X. Dort gibt das Programm

char: 1, short: 2, int: 4, long: 4, float: 4, double: 8
7-arr of int: 28, FILE: 88
struct of double and char: 16
pointer1: 4, pointer2: 4

aus. Auf einer 64-bit-Maschine (IBM 260, z.B. aixterm8 im URZ) mit 64-bit-Compiler (und IBM-C-Bibliothek) ergibt sich hingegen

char: 1, short: 2, int: 4, long: 8, float: 4, double: 8
7-arr of int: 28, FILE: 88
struct of double and char: 16
pointer1: 8, pointer2: 8

Andere Dateioperationen

Einer Datei ist ein Zeiger zugeordnet, der immer auf das nächste Byte zeigt, das gelesen oder geschrieben wird. long ftell(FILE *stream) gibt diesen Zeiger zurück,

int fseek(FILE *stream, long offset, int whence);

setzt ihn. whence ist dabei entweder SEEK_SET, SEEK_CUR oder SEEK_END, die den offset als relativ zum Dateianfang, zum augenblicklichen Zeiger, oder zum Dateiende definieren. Das Verstecken von “magischen” Zahlen hinter mehr oder minder mnemonischen Makros ist gute Programmierpraxis. Leuten, die bei solchen Gelegenheiten enum murmeln, sollte man (oft) nicht zuhören. Moderner sind die Funktionen fgetpos und fsetpos, die in mancher Hinsicht portabler sind. Näheres vgl. man-page.


Markus Demleitner

Copyright Notice