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.