The Real Adok's Way to C
Folge 5

Begleit-Dateien:


+++ Zeigerarrays +++

Typ *name [Anzahl_der_Elemente];

Zeigerarrays  sind  Arrays,  deren  einzelne Elemente  Zeiger  sind  - also im
Prinzip  nichts anderes als zweidimensionale  Arrays, wobei allerdings nur die
Größe  eines  Arrayvektors (die Anzahl der  Zeiger,  die der Array beinhaltet)
konstant  ist  und bei der Definition  angegeben  werden muß. Da jedes Element
eines  Zeigerarrays,  also  jeder Zeiger,  zuerst  mit  malloc() initialisiert
werden  muß, kann die Größe des reservierten Speicherbereichs, auf den gezeigt
wird, pro Zeigerarray-Element verschieden sein.

Kurz  gesagt: Zeigerarrays sind zweidimensionale  Arrays, bei denen ein Vektor
statisch und der andere Vektor dynamisch ist.

Ein  weiteres  Anwendungsgebiet  von  Zeigerarrays  ist  das  Reservieren  von
zweidimensionalen  Arrays,  die  über  die  Stackgröße  hinausgehen.  Wie  ihr
vielleicht  wißt,  werden  lokale Variablen  und  damit  auch Arrays einzelner
Funktionen auf dem Stack (= Stapelspeicher) abgelegt. Die Größe des Stacks ist
jedoch   begrenzt,   und  versucht  man,   einen  Array  anzulegen,  der  mehr
Speicherplatz  einnimmt,  als für den Stack  reserviert ist, kommt es zu einem
Laufzeitfehler  oder  gar zum Absturz. Dagegen  kann  man mit malloc() auf den
gesamten Heap zugreifen.

Als  Beispiel  für  die praktische Anwendung  dient  das  neue Magsystem. Hier
werden  beim Einlesen der aktuellen Textseite eventuelle Links in eine Tabelle
eingetragen. Diese Tabelle ist ein Zeigerarray.

Ähnlich wie die Oberfläche der neunten Ausgabe von Hugi geht auch das folgende
Beispielprogramm  vor.  Zuerst möchte ich  aber noch die Headerdatei adalloc.h
vorstellen.  Sie enthält eine von  mir geschriebene Hüllfunktion für malloc(),
die einem insofern Arbeit abnimmt, als Fehler abgefangen werden und auf Wunsch
der  neu reservierte Speicherbereich mit Nullen  gefüllt wird. Die Idee stammt
aus  dem Buch "Nie wieder Bugs!" von Steve Maguire. Parameter sind ein *Zeiger
auf   den   Zeiger*,  in  welchem   die  Startadresse  des  zu  reservierenden
Speicherbereichs  eingetragen  werden  soll, die  Größe  des zu reservierenden
Speicherbereichs und ein Flag, ob der Speicherbereich nach dem Reservieren mit
Nullen gefüllt werden soll (fill!=0) oder nicht (fill==0).

Diese  Headerdatei wird von den folgenden Beispielprogrammen verwendet und muß
daher   unter   dem  Dateinamen  adalloc.h   im  selben  Verzeichnis  wie  das
Beispielprogramm vorliegen.

//adok's alloc-hüllfunktion

#define __ADALLOC__

void mymalloc(void **pvoid,size_t size,signed char fill)
{
  char **pchar=(char **)pvoid;

  *pchar=(char *)malloc(size);
  if(NULL==*pchar)
  {
    printf("error: NULL pointer returned after trying to allocate memory\n");
    exit(1);
  }
  else if(fill) memset(*pchar,0,size);
}

OK, und hier nun das Beispielprogramm.

//zeigerarray-demoprogramm

#include <stdio.h>
#include <malloc.h>
#include <string.h>
#include <stdlib.h>
#include "adalloc.h"

void main()
{
  unsigned char *string_pointer[3],
                i;

  //jeden der drei strings mit 50 bytes initialisieren
  for(i=0;i<3;i++)
    mymalloc(&string_pointer[i],50,0);

  //mit werten belegen
  strcpy(string_pointer[0],"hi, dies ist der erste string.");
  strcpy(string_pointer[1],"so sieht der inhalt des zweiten aus.");
  strcpy(string_pointer[2],"das ist der dritte.");

  //ausgabe
  for(i=0;i<3;i++)
    printf("string %d: %s\n",i,string_pointer[i]);

  //freigabe
  for(i=0;i<3;i++)
    free(string_pointer[i]);
}

Da  in diesem Beispielprogramm a) *jeder*  der drei Speicherplätze, auf welche
die drei Zeiger hinweisen, 50 Byte groß ist und b) insgesamt nur 3*50=150 Byte
reserviert  werden,  wäre  es hier  auch  möglich, mit einem zweidimensionalen
Array  zu arbeiten (außer man hat  die Stackgröße in den Compilereinstellungen
auf  einen  kleineren Wert als 150 Byte  gesetzt). Doch das Programm sollte ja
nur  demonstrieren,  wie Zeigerarrays funktionieren,  und nicht unbedingt eine
Situation darstellen, in der Zeigerarrays wirklich benötigt werden.

Kommen wir nun zur anderen Art des harmonischen Zusammenspiels von Zeigern und
Arrays.

+++ Arrayzeiger +++

Typ (*name) [Anzahl_der_Elemente];

Arrayzeiger  sind  eine Weiterentwicklung  des folgenden Konzepts: Angenommen,
wir haben einen Array und einen Zeiger definiert...

char array[10],
     *pointer;

...dann  kann man über den Zeiger  einzelne Elemente des Arrays auf indirektem
Wege ansprechen. Zuerst müssen wir den Zeiger auf den Array weisen lassen:

pointer=array;

Wenn   wir  nun  *pointer  auslesen,  erhalten   wir  den  Inhalt  des  ersten
Arrayelements.  Erhöhen  wir  pointer stufenweise um  1,  so  erhalten wir per
*pointer die nächsten Arrayelemente.

Wichtig  ist  in diesem Fall, daß der  Zeiger  und der Array von demselben Typ
sind. Auf diese Weise kann man durch einen eindimensionalen Array navigieren.

(Nebenbei: Was würde bei folgender Definition passieren?

char      array[10];
short int *pointer;

Richtig  - wir könnten die Elemente des char-Arrays - zur Erinnerung: ein char
belegt  1  Byte - als short ints -  ein short int belegt 2 Bytes - ansprechen.
Dies  bedeutet, daß *pointer gleich  zwei Elemente des char-Arrays zurückgibt.
Angenommen,  pointer zeige auf array[0], array[0] sei 10 und array[1] 3. Wegen
des Speicheraufbaus von Intel-Prozessoren würde *pointer dann

(3<<8)+10

ergeben,  was soviel wie 778 ist, denn  array[1] wäre das Hi-Byte des ints und
array[0]  das  Lo-Byte. Kommt dies manchen  vielleicht  bekannt vor? Richtig -
genauso  verhält es sich bei den allgemeinen  Registern, die 16 Bit groß sind,
aber in zwei 8-Bit-Subregister gesplittet werden können. Dieses Konzept machen
sich auch die unions zunutze. Wir werden darauf noch zu sprechen kommen. Siehe
auch zeiger05.c.)

Wie  kann man aber nun durch zweidimensionale Arrays navigieren? Ganz einfach:
mit den Arrayzeigern. Wandeln wir das vorige Beispiel leicht ab:

char array[10][5],
     (*pointer)[5];

So,  das reicht schon, um durch  bloßes Inkrementieren bzw. Dekrementieren von
pointer durch den zweidimensionalen Array zu wandern. Schreiben wir...

pointer=array[0];

...so  zeigt pointer auf den Beginn des ersten "Verzeichnisses" im Array - auf
array[0][0].  Mit pointer++ gelangt man  auf array[1][0], dann auf array[2][0]
etc.  Und wer will, kann ja  mit einem zweiten Pointer, einem Standardpointer,
durch den zweiten Vektor des Arrays navigieren.

Das Ganze läßt sich verwenden, falls man mehrere gleich aufgebaute Arrays hat,
man sich aber nicht festlegen will bzw. muß, welcher Array in einem bestimmten
Programmteil angesprochen werden soll.

Hier nochmal das Ganze am Stück:

//arrayzeiger-demoprogramm

#include <stdio.h>
#include <malloc.h>
#include <string.h>
#include <stdlib.h>
#include "adalloc.h"

void main()
{
  unsigned char string_pointer[3][50],
                (*arraypointer)[50],
                i;

  //mit werten belegen
  strcpy(string_pointer[0],"hi, dies ist der erste string.");
  strcpy(string_pointer[1],"so sieht der inhalt des zweiten aus.");
  strcpy(string_pointer[2],"das ist der dritte.");

  //arraypointer auf die erste "zeile" von string_pointer hinweisen lassen
  arraypointer=string_pointer[0];

  //ausgabe über arraypointer
  for(i=0;i<3;i++)
  {
    printf("string %d: %s\n",i,*arraypointer);
    arraypointer++;
  }
}

Fertig mit diesem Kapitel.

+++ Benutzerdefinierte Datentypen +++

Häufig  benötigt  man bestimmte Informationen viele  Male,  z.B., wenn man mit
Virtual Screens arbeiten sollte. Pro Virtual Screen werden Infos über Größe in
X-  und in Y-Richtung, Farbtiefe und daraus resultierendem Speicherplatzbedarf
sowie  natürlich die Adresse, wo sich dieser Virtual Screen im Arbeitsspeicher
befindet,  benötigt.  Damit  man nicht für  jeden  Virtual Screen einen Haufen
Variablen  zum  Beherbergen  dieser Informationen  definieren  muß  - und noch
schlimmer: diesen Haufen an Variablen nicht manuell an jede Funktion übergeben
muß,  wodurch  man schnell das eine oder  andere  zu übergeben vergißt - haben
sich  die  Erfinder  der  strukturierten  Programmierung  die  Möglichkeit der
Definition  eigener  Datentypen  einfallen lassen.  Natürlich  sind dies nicht
wirklich   neue   Datentypen   wie  int   und   char,  sondern  vielmehr  eine
Zusammenfassung  von  mehreren  Variablen verschiedenen  Typs.  Unter C werden
benutzerdefinierte  Datentypen  mit  struct deklariert.  Wenn  wir bei unserem
Beispiel  bleiben,  den Virtual Screens,  so  könnten wir folgendermaßen einen
Virtual-Screen-Datentypen erzeugen:

struct VIRTUAL_SCREEN
{
  long   x_groesse;
  long   y_groesse;
  short  farbtiefe;
  size_t speicherplatzbedarf;
  void * adresse;
};

Schreiben wir anstelle der letzten Zeile bspw.

} meine_virtual_screen_variable;

...so  wird  gleich eine Variable  des soeben definierten Datentyps definiert.
Dies  läßt sich natürlich aber auch erst nachträglich erledigen. Wir schreiben
dazu an der entsprechenden Programmstelle:

struct VIRTUAL_SCREEN meine_spaeter_definierte_virtual_screen_variable;

Ebenso  lassen  sich natürlich Zeiger  auf benutzerdefinierte Datentypen sowie
Arrays  definieren - haargenau so, wie es auch bei den Standard-Datentypen der
Fall ist.

Doch wie sprechen wir die benutzerdefinierten Datentypen an, weisen ihnen bzw.
ihren  Elementen  Werte zu? Dazu dient  der  Punkt-Operator. Will ich etwa die
Farbtiefe von meine_virtual_screen_variable auf 8 stellen, so schreibe ich:

meine_virtual_screen_variable.farbtiefe=8;

Und  will  ich  mittels mymalloc() einen  10.000  Byte  kleinen Virtual Screen
reservieren und mit Nullen füllen, schreibe ich:

mymalloc(&(meine_virtual_screen_variable.adresse),10000,1);

Mehrere  benutzerdefinierte  struct-Datentypen lassen  sich  wiederum in einem
"großen" struct-Datentyp zusammenfassen:

struct a_struct
{
  int  x;
  char y;
};
                                    ...und...
struct b_struct
{
  float *x;
  int    y;
};
                                 ...werden zu...
struct big_struct
{
  struct a_struct var1;
  struct b_struct var2;
  long            var3;
} test;

...und  wir  können dann bspw. auf  die  y-Variable von b_struct innerhalb der
big_struct-Variablen per test.var1.y zugreifen.

+++ unions +++

Doch es gibt noch eine weitere Möglichkeit, structs zusammenzufassen, und zwar
mit  union. unions werden genauso wie  structs definiert (jedoch natürlich mit
dem  Schlüsselwort union statt struct). Doch  bei der Verwendung gibt es einen
gewaltigen  Unterschied!  Und  zwar geht es um  jene  Sache, die ich vorhin im
Exkurs bei den Arrayzeigern beschrieben habe.

In  unions  sind die einzelnen  Elemente/structs nicht voneinander unabhängig,
sondern überlagern einander. Beispiel: Wir definieren eine union, die aus zwei
structs besteht.

struct bytes
{
  char byte[4];
};

struct words
{
  short word[2];
};

union sample
{
  struct bytes b;
  struct words w;
} sample_var;

Jetzt  können  wir  per sample.b.byte[n] Byte  Nummer  n ansprechen, wobei nur
dieses   eine  Byte  ausgelesen/verändert  wird.  Wir  können  aber  auch  per
sample.w.word[n]  Word  Nummer  n  ansprechen,  wobei  klarerweise  zwei Bytes
verändert werden.

sample_var.b.byte[0]=3;
sample_var.b.byte[1]=10;

...ergibt dasselbe wie:

sample_var.w.word[1]=(3<<8)+10;

Jetzt wissen wir auch, wie die union REGS funktioniert. Seht euch doch mal die
Headerdatei an, in der REGS definiert ist (normalerweise dos.h, bei Watcom C++
i86.h)!  Ich  glaube, auf ein Beispielprogramm  verzichten  zu können - die im
Lieferumfang des Compilers enthaltene Headerdatei erklärt alles.

+++ typedef +++

Mittels  typedef  lassen sich beliebigen  Datentypen neue Namen geben. Syntax:
typedef alter_name neuer_name.

+++ enum +++

enum ist das Schlüsselwort, welches für das Erzeugen von Aufzählungsdatentypen
gebraucht wird. Schreiben wir etwa...

enum BOOL { TRUE, FALSE };

...so haben wir einen neuen Datentypen namens BOOL erstellt, der nur die Werte
TRUE  oder FALSE annehmen kann. Intern werden enums jedoch wie ints behandelt.
Dem  ersten  Wert wird 0 zugewiesen, dem zweiten  1 etc. Es lassen sich in der
Definition von BOOLs jedoch auch explizit Werte angeben:

enum BOOL { TRUE=-1, FALSE };

Schon haben wir die richtige Definition eines boole'schen Datentyps - TRUE ist
-1, und FALSE ist jene Integerzahl, die nach -1 folgt, also 0.

+++ Dateizugriff +++

Eine   Anwendungsmöglichkeit  für  benutzerdefinierte   Datentypen  und  deren
Übergabe  an  Funktionen ist der Dateizugriff.  Hier  muß man allerdings nicht
selbst  einen  neuen Datentypen definieren, denn  den gibt's schon: FILE heißt
der  Datentyp,  welcher  Infos über die  korrespondierende  Datei wie etwa ihr
Handle, unter welchem es zur Zeit in DOS ansprechbar ist, enthält.

Die Funktionen zum Dateizugriff befinden sich allesamt in stdio.h.

+++ fopen() +++

Prototyp: FILE *fopen(char *path,char *mode);

Bevor  man  mit einer Datei überhaupt  etwas  anfangen kann, muß sie natürlich
geöffnet  werden. Diese Aufgabe übernimmt  fopen(). Parameter sind ein String,
der  eventuell den Pfad, aber auf jeden  Fall den Namen der zu öffnenden Datei
enthält,  sowie ein Modusstring, welcher den Modus enthält, in dem diese Datei
geöffnet werden soll, hehe. :)

Da  in  C  nur sequentieller Dateizugriff erlaubt  ist,  gibt es im großen und
ganzen auch nur drei Zugriffsmodi: "r" wie read, "w" wie write (überschreiben)
und  "a"  wie  append (anhängen bzw.,  falls  die  angegebene Datei noch nicht
existiert,  neu erzeugen). Hängt man noch ein b daran, schreibt also "rb" bzw.
"wb",  wird  die Datei im Binärmodus geöffnet,  und  hängt man ein t daran, im
Textmodus.  Läßt  man  b und t weg,  wird  die  in stdio.h definierte Variable
_fmode  ausgewertet  (standardsmäßig  auf Textmodus  eingestellt)  und je nach
ihrem  Inhalt verfahren. In den Konstanten  O_TEXT und O_BINARY sind die Werte
definiert, die _fmode haben muß, um den jeweiligen Modus anzuzeigen.

Und dann gibt es auch noch das Pluszeichen: Sowohl "r+" als auch "w+" und "a+"
dienen  dazu,  gleichzeitig Lese- und  Schreibzugriffe  tätigen zu können. Der
kleine,  feine, aber verheerende Unterschied: "w+" legt im Gegensatz zu seinen
Konsorten  die zu öffnende Datei auf jeden Fall neu an. Existiert sie bereits,
wird  ihr Inhalt gelöscht. "a+" fängt  mit den Schreibeoperationen am Ende der
Datei  an,  hängt  also das Geschriebene an  die  Datei an, und "r+" setzt den
Schreibezeiger  an  den Beginn der Datei,  wo sich anfangs auch der Lesezeiger
befindet.

Rückgabe  von  fopen() ist ein Zeiger  auf  eine FILE-struct-Variable, den man
einem  eigenen  FILE-Zeiger einverleiben sollte.  Falls  der Rückgabewert NULL
ist, so konnte die Datei nicht geöffnet werden.

+++ fclose() +++

Prototyp: int fclose(FILE *file);

fclose()  schließt die Datei, auf die der als Parameter übergebene FILE-Zeiger
zeigt. Diese Funktion müßt ihr dann aufrufen, wenn ihr eine geöffnete Datei in
eurem  Programm  nicht  mehr braucht. Der  Rückgabewert  gibt an, ob die Datei
fehlerfrei  geschlossen  werden  konnte, was  eigentlich  immer  der Fall sein
sollte,  außer,  euer  Computer  hat 'nen  Dachschaden  -  oder  ihr habt 'nen
Dachschaden,  weil  ihr  an fclose() ein  Dateihandle  übergeben habt, das gar
nicht geöffnet ist.

Nun  zu  einigen  der Funktionen, mit derer  Hilfe  ihr  etwas mit den Dateien
anfangen könnt. Eigentlich kennt ihr die meisten ja schon...

+++ Lesefunktionen +++

int fgetc(FILE *file);

...gibt  das Byte an der aktuellen Position des Dateizeigers bzw. den Wert der
int-Konstanten EOF, wenn das Dateiende erreicht wurde, zurück. Der Dateizeiger
wird, wie auch bei den anderen Lesefunktionen, entsprechend erhöht.

char *fgets(char *s,int nr,FILE *file);

...liest  nr-1 Bytes aus *file ein,  hört jedoch beim Erreichen des Zeilenende
(ASCII  13  und anschließend ASCII 10) auf.  Das  Eingelesene wird im String s
gespeichert, dessen Adresse auch der Rückgabewert dieser Funktion ist.

size_t fread(void *ptr,size_t size,size_t n,FILE *file);

...dient  dazu, auch andere Werte als  bloße Bytes einzulesen. Bei ptr handelt
es  sich um einen Zeiger auf  die Variable bzw. auf den Array/Speicherbereich,
in  den  n Elemente mit der Größe  size aus der Datei *file eingelesen werden.
Rückgabe ist die tatsächliche Anzahl der gelesenen Elemente.

Da  ich  euch  bis jetzt noch  nicht  den  sizeof()-Operator vorgestellt habe,
möchte ich es an dieser Stelle nachholen. Mittels sizeof() ist es möglich, die
Größe eines Datentyps - auch eines benutzerdefinierten - zu ermitteln. Syntax:
sizeof(Typ).  Ihr  seht,  der  sizeof()-Operator  wird  wie  eine ganz normale
Funktion verwendet.

Wollen  wir  also z.B. eine long-Variable  aus einer Datei einlesen, schreiben
wir:

long longvar;
FILE *file;
...
fread(&longvar,sizeof(long),1,file);

+++ Schreibefunktionen +++

int fputc(int c,FILE *file);

...ist  die Funktion zum Schreiben eines Bytes in eine Datei. Der Rückgabewert
sagt über den Erfolg der Funktion aus.

int fputs(char *s,FILE *file);

...ist  das  Analogon  zu  fgets. Man kann  einen  ganzen  String  in ein File
schreiben. Der Rückgabewert erfüllt dieselbe Aufgabe wie bei fputc().

size_t fwrite(void *ptr,size_t size,size_t n,FILE *file);

...tut  haargenau dasselbe wie fread(), nur  halt in der umgekehrten Richtung.
Parameter etc. haben dieselbe Bedeutung.

+++ Dateizeiger-Positionsfunktionen +++

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

...ermöglicht  es, den Dateizeiger auf eine beliebige Stelle zu setzen. whence
kann  einer  von drei Werten  sein: SEEK_SET (Dateianfang), SEEK_CUR (aktuelle
Position)  oder  SEEK_END (Dateiende). offset ist  relativ  zu whence und kann
natürlich auch eine negative Zahl sein.

long ftell(FILE *file);

...gibt den aktuellen Dateizeiger in file zurück.

So, nun noch ein Beispielprogramm, und dann lassen wir es gut sein.

//dateizugriff-demoprogramm

#include <stdio.h>

void main()
{
  char string[]="hallo!",
       string1[10];
  int  i     =1000,
       j;
  FILE *file;

  //schreiben
  file=fopen("test.txt","wb");
  if(file==NULL)
    printf("error: couldn't create file\n");
  else
  {
    fputs(string,file);
    fwrite(&i,sizeof(int),1,file);
    fclose(file);
  }

  //lesen
  file=fopen("test.txt","rb");
  if(file==NULL)
    printf("error: couldn't open file\n");
  else
  {
    fgets(string1,7,file);
    fread(&j,sizeof(int),1,file);
    fclose(file);

    //ausgeben
    printf("string: %s\nint: %d\n",string1,j);
  }
}

Seid stolz auf euch! Wenn ihr nämlich bis hierhin gekommen seid und auch alles
versteht,  habt  ihr  den  Kurs  absolviert  und  solltet  in  der  Lage sein,
C-Programme lesen und schreiben zu können. Wenn ihr noch nicht alles versteht,
beginnt  noch  einmal von vorne, bis ihr's  könnt.  Keine Bange, ihr werdet es
schon schaffen, sofern eure Intelligenz überdurchschnittlich ist. Immerhin ist
C eine sehr einfache Programmiersprache.