The Real Adok's Way to C
Folge 4
Begleit-Dateien:
Auch heute möchte ich euch zu einer neuen Folge dieses Kurses willkommen
heißen! Diesmal geht es um das ebenso wichtige wie komplizierte Kapitel des
Zeigers. Ich versuche, es möglichst verständlich rüberzubringen. Sollten
nachher noch Fragen offen bleiben, könnt ihr euch ja jederzeit an mich wenden!
Bei Zeigern handelt es sich um nichts anderes als um Variablen, in denen man
Adressen von anderen Variablen oder "normalen" Speicherstellen speichern kann.
Welche Vorteile bringt das?
Zum einen kann man dadurch indirekt auf die Variablen/Speicherstellen
zugreifen und muß nicht die komplizierten Variablennamen im Kopf behalten. ;-)
Aber auch für Programmierer mit gutem Gedächtnis bringt der indirekte Zugriff
Vorteile. Beispielsweise kann man damit jede beliebige Speicherstelle im
konventionellen RAM ansprechen. Das ist wichtig für die Grafikprogrammierung.
Anders kommt man in reinem C nicht an den Video-RAM ran.
Zum anderen kann man mit Hilfe von Zeigern jede Menge Speicherplatz sparen.
Stellt euch vor, ihr müßtet einer Funktion einen großen Array übergeben, der
z.B. einen 600 Zeilen langen Text enthält (wie es etwa bei meinem Textviewer
der Fall ist). Bei jedem Funktionsaufruf werden die übergebenen Variablen auf
den Stack kopiert (wo übrigens auch die lokalen Variablen abgelegt werden).
Würde man die 600 Zeilen Text direkt übergeben, könnte es unter Umständen zu
einem Stacküberlauf kommen - mit der Folge, daß das Programm abstürzt. Mit
Zeigern aber läßt sich dies umgehen! Man muß dann lediglich einen Zeiger auf
den Original-Array an die Funktion übergeben, und die Funktion kann mit Hilfe
dieses Zeigers auf den Original-Array zugreifen.
Wir haben schon früher mit Zeigern zu tun gehabt. Erinnert ihr euch? Ich
möchte zwei Beispiele nennen:
1. scanf(): Diese Funktion erwartet als Parameter Zeiger auf die Variablen, in
denen die Eingabe(n) gespeichert werden soll(en). Warum ist das so? Ganz
einfach: Nehmen wir an, an scanf würden keine Zeiger, sondern direkt die
Variablen, in denen die Eingaben gespeichert werden sollen, als Parameter
übergeben werden. Dazu müßte scanf() diese Variablen verändern können. Eine
Funktion kann aber nie die Variablen verändern, die ihr als Parameter
übergeben werden. Wie bereits gesagt, werden bei einem Funktionsaufruf
Kopien dieser Variablen auf einem getrennten Speicherbereich, dem Stack,
angelegt. Die Funktion selbst greift nur auf diese Kopien zu, die nach dem
Beenden der Funktion wieder gelöscht werden. Im Gegensatz dazu kann man mit
Zeigern direkt auf die Originale zugreifen und die Originale verändern. So
tut es auch scanf.
2. Arrays: Ja, auch die Arrays haben etwas mit Zeigern zu tun! Die Definition
char feld[500]; beispielsweise bedeutet nämlich im Prinzip nichts anderes,
als daß 500 Bytes des Arbeitsspeichers reserviert werden und ein Zeiger
namens feld angelegt wird, der auf den Anfang dieser 500 Bytes (auf das
erste Element des Arrays) zeigt. Es ist in den meisten Fällen tatsächlich
möglich, den Namen eines Arrays wie einen Zeiger zu verwenden. Gegenüber
richtigen Zeigern können Arraynamen jedoch in ihrem ganzen Leben :) immer
nur auf ein und dieselbe Speicherstelle zeigen - nämlich auf das erste
Element des dazugehörigen Arrays. Den richtigen Zeigern hingegen kann man
jederzeit eine neue Adresse zuweisen - eben so, wie man auch normalen
Variablen andere Inhalte zuweisen kann.
Das war eine kurze Einleitung, um euch die Zeiger schmackhaft zu machen. Nun
geht's erst richtig los!
+++ Definition und Zuweisung von Zeigervariablen +++
Zeiger werden eigentlich wie ganz normale Variablen definiert - mit einem
Unterschied: Zwischen dem Typ und dem Namen muß das Multiplikationszeichen (*)
stehen. Bei Zeigern hat der Typ eine andere Bedeutung als bei normalen
Variablen. Der Typ gibt hier an, wie der Inhalt der Speicherstelle behandelt
werden soll, auf die der Zeiger zeigt - also, ob es sich bei der
Speicherstelle um eine Variable des Typs int, char, float,... handelt. Falls
der Typ der Speicherstelle uninteressant sein sollte, kann man auch
void-Zeiger definieren:
void * name_des_zeigers;
Nanu, void als Datentyp? Ja, es funktioniert tatsächlich! void kann eben nicht
nur verwendet werden, um bei Funktionen zu signalisieren, daß kein
Rückgabewert existiert oder sie keine Parameter empfangen können, sondern auch
bei der Definition von Zeigern.
Wie sieht die Zuweisung bei Zeigern aus? Es gibt zwei verschiedene Arten der
Zuweisung, die verschiedene Bedeutungen haben:
1. zeiger=wert;
bewirkt, daß dem Zeiger zeiger die Adresse wert zugewiesen wird. Er zeigt
nun an auf diese Adresse.
=> Der Wert des Zeigers selbst wird verändert.
2. *zeiger=wert;
bewirkt, daß der Speicherstelle, auf die der Zeiger zeiger zeigt ( :-) ),
der Wert wert zugewiesen wird.
=> Der Inhalt der Speicherstelle, auf die der Zeiger zeigt, wird verändert.
Paßt auf, daß ihr diese beiden Arten nicht verwechselt! Sonst könnte es
gefährlich werden. Aber hier nun endlich ein Beispielprogramm!
// C-Kurs 4: Zeiger I
#include <stdio.h>
void main()
{
int zahl,*pointer1,*pointer2;
// zahl direkt ansprechen
zahl=1;
printf("zahl: %d\n",zahl);
// zahl über pointer1 ansprechen
pointer1=&zahl;
*pointer1=2;
printf("zahl: %d\n",*pointer1);
// pointer1 an pointer2 zuweisen, zahl über pointer2 ansprechen
pointer2=pointer1;
*pointer2=3;
printf("zahl: %d\n",*pointer2);
}
Zuerst sprechen wir die Variable zahl direkt an. Danach weisen wir dem Zeiger
pointer1 die Adresse von zahl zu und sprechen über pointer1 zahl indirekt an.
Zum Schluß weisen wir dem Zeiger pointer2 die Adresse zu, auf die pointer1
zeigt, und sprechen zahl ein weiteres Mal indirekt an.
+++ Eine andere Art zu callen +++
Im nächsten Beispielprogramm möchte ich euch zeigen, wie man das, worüber ich
am Anfang im Absatz 'zum anderen' gesprochen habe, realisiert. Damit ihr
später nicht immer sagen müßt "ich mache jetzt das, worüber Adok in der
vierten Folge seines C-Kurses im Absatz 'zum anderen' gesprochen hat",
verrate ich euch, wie man dies mit einem Fachbegriff nennt: call by reference.
Tolles Wort bzw. Wortgruppe, nicht wahr? Es bedeutet, daß die Parameter nicht
direkt, sondern über Zeiger übergeben werden. Das Gegenteil von call by
reference heißt call by value (nicht zu verwechseln mit Callgirl!).
// C-Kurs 4: Call by reference
#include <stdio.h>
// Funktionsprototypen
void eingabe(unsigned char *string);
void ausgabe(unsigned char *string);
// Hauptprogramm
void main()
{
unsigned char zeichenkette[81];
eingabe(zeichenkette);
ausgabe(zeichenkette);
}
// Funktionen
void eingabe(unsigned char *string)
{
printf("Gib eine Zeichenkette ein! ");
gets(string);
}
void ausgabe(unsigned char *string)
{
printf("Du gabst folgende Zeichenkette ein:\n%s\n",string);
}
Dies ist unser erstes Beispielprogramm, das auch andere Funktionen als main
enthält. In der ersten Kursfolge haben wir das Wesen der Funktionen kurz
theoretisch behandelt. Deshalb vielleicht noch ein paar Worte zu diesem Thema
an dieser Stelle.
+++ Mehr über Funktionen +++
Bei den Funktionsprototypen handelt es sich um Deklarationen in der Art, wie
sie für die Standard-Funktionen in den Headerdateien zu finden sind.
Funktionsprototypen sind nur dann zwingend erforderlich, wenn die Funktionen
bereits vor ihrer Definition aufgerufen werden sollen - wie es in obigem
Beispielprogramm der Fall ist. Aus diesem Grund schreibe ich die Funktion
main() in meinen Programmen meistens am Ende des Quelltexts. Aber es ist nicht
schlecht, Prototypen anzulegen. Erstens sieht es übersichtlicher aus, und
zweitens macht der Compiler einen dann aufmerksam, wenn man Funktionen mit
Parametern falschen Datentyps aufruft.
Die Prototypen sehen genauso aus, wie der Kopf der Funktion selbst. Nur folgt
anstelle einer geschwungenen Klammer ein Semikolon.
Statt unsigned char *string hätte man in den Prototypen und Funktionsköpfen
auch unsigned char string[] schreiben können. Arrays werden bei der
Parameterübergabe genauso wie Zeiger behandelt. Nach dem, was ich in der
Einleitung zu dieser Kursfolge über Arrays gesagt habe, dürfte dies für euch
verständlich sein.
Wie ihr wißt, kann jede Funktion auch einen Wert zurückgeben. Den Datentyp des
Rückgabewerts muß am Anfang des Funktionskopfs und des Prototyps stehen. Der
Kopf einer Funktion readkey(), die einen unsigned-char-wert zurückgibt und
keine Parameter hat, lautet also unsigned char readkey(void).
Und wie läßt man diese Funktionen einen Wert zurückgeben? Dazu ist das
Schlüsselwort return da. Parameter ist der Rückgabewert bzw. die Variable,
deren Inhalt zurückgegeben werden soll. return läßt sich auch bei
void-Funktionen einsetzen, um die Ausführung der Funktion vorzeitig
abzubrechen.
Hier nun als Beispiel eine mögliche Implementation von readkey():
unsigned char readkey(void)
{
unsigned char taste;
taste=inp(0x60);
if( (taste&128)==128 ) return(0); // Wenn Break-Code (keine Taste) -> 0
return(taste); // Ansonsten Scan-Code zurückgeben
}
Diese Funktion readkey() dient dazu, um festzustellen, ob und, wenn ja, welche
Taste gerade gedrückt wird.
inp() ist in conio.h deklariert und dient zum Auslesen eines Ports. Über
Port 60h (hex, in C also 0x60) läßt sich der Scan-Code der gerade gedrückten
bzw. zuletzt gedrückten Taste auslesen. Ist Bit 7 gesetzt ((taste&128)==128),
so wird gerade keine Taste gedrückt. readkey() gibt dann 0 zurück.
Andernfalls wird der Scancode der gedrückten Taste zurückgegeben.
Mit der Funktion outp(), welche ebenfalls in conio.h deklariert ist, lassen
sich Werte auf Ports schreiben. Parameter sind die Nummer des Ports und der
Wert. Aber das nur am Rande. Nähere Informationen zu Ports findet ihr im
Assembler-Kurs. Nun zurück zu den Zeigern.
+++ Dynamische Arrays +++
Es gibt eine weitere wichtige Einsatzmöglichkeit von Zeigern. Einige von euch
werden vielleicht aus Quick Basic die dynamischen Arrays kennen. Diese sind in
ihrer Größe nicht statisch und können jederzeit vergrößert, verkleinert oder
verschoben werden. Auch in C lassen sich solche dynamischen Arrays erzeugen,
auch wenn sie hier nicht Arrays heißen. Es handelt sich um Zeiger, denen man
mit Funktionen wie malloc() und realloc() Speicher zuweisen kann.
+++ malloc() +++
Deklariert in: stdlib.h, alloc.h
Prototyp: void * malloc(size_t size);
malloc() ist, wie seine Schwesternfunktionen, sowohl in stdlib.h als auch in
alloc.h deklariert. Es reicht, eine der beiden Headerdateien einzubinden. Als
Parameter muß malloc() die Anzahl der Bytes, die im Speicher reserviert werden
soll, übergeben werden. Bei dem Datentyp size_t handelt es sich um eine Art
Aliasnamen von int. Solche Aliasnamen kann man mit typedef erzeugen. Ich werde
darauf später in diesem Kurs noch zu sprechen kommen.
Der Rückgabewert von malloc() ist ein Zeiger vom Typ void. Diesen Zeiger kann
man mit Hilfe von Typecasting jedem anderen Zeiger, egal welchen Typs,
zuweisen. Sollte der Versuch der Speicherreservierung fehlschlagen, weil auf
der Speichermüllhalde, dem Heap, nicht genug Speicher frei ist, liefert
malloc() den Nullzeiger (NULL) zurück. NULL ist in stdio.h definiert.
+++ free() +++
Deklariert in: stdlib.h, alloc.h
Prototyp: void free(void * pointer);
free() ist der Gegenspieler von malloc(). Wenn man einen reservierten
Speicherblock nicht mehr benötigt, muß man ihn spätestens am Programmende
unbedingt von free() freigeben lassen. Tut man das nicht, hinterläßt das
Programm Rückstände. Die Daten des Programms nehmen dann immer noch
Speicherplatz ein, obwohl das Programm selbst schon längst beendet ist.
Darüber "freut" sich der User, der dann weniger freien RAM zur Verfügung hat
als vor dem Starten des unseligen Programms.
Eine andere Gefahr liegt darin, versehentlich auf einen bereits freigegebenen
Speicherblock erneut zuzugreifen. Das darf auf keinen Fall passieren. Wer
weiß, was DOS mit dem gerade freigegebenen Block vorhat? Womöglich
überschreibt man durch solche Unachtsamkeiten einen Teil des residenten Teils
des Betriebssystems und muß neu booten.
Der Parameter pointer ist ein Zeiger auf den freizugebenden Speicherblock.
+++ realloc() +++
Deklariert in: stdlib.h, alloc.h
Prototyp: void * realloc(void * pointer,size_t size);
realloc() dient zum Verändern der Größe des durch pointer und size
beschriebenen Speicherblocks. Es kann passieren, daß realloc() beim Vergrößern
den Speicherblock verschieben muß, weil im aktuellen Speicherbereich kein
Platz mehr zur Expansion ist. Deshalb sollte man den Rückgabewert von
realloc() - den neuen oder, falls es keine Veränderungen gab, den alten Zeiger
bzw. im Fehlerfall NULL - beachten.
Es folgt ein Beispielprogramm zu malloc() und free().
// C-Kurs 4: Dynamische "Arrays"
#include <stdio.h>
#include <stdlib.h>
// Funktionsprototypen
void eingabe(unsigned char *string);
void ausgabe(unsigned char *string);
// Hauptprogramm
void main()
{
char *string; // Zeiger auf dynamischen Array
size_t anzahl_bytes_reservieren; // Platzhalter für die Größe des Arrays
printf("Wieviel Speicherplatz (in Byte) soll für den String reserviert "
"werden? ");
scanf("%d",&anzahl_bytes_reservieren);
fflush(stdin);
// Wenn zu wenig Heap frei, Programmausführung abbrechen
if( (string=(char *)malloc(anzahl_bytes_reservieren)) == NULL )
{
printf("\nFehler! Breche Programmausführung ab!");
exit(1);
}
eingabe(string);
ausgabe(string);
free(string); // Speicher freigeben
}
// Funktionen
void eingabe(unsigned char *string)
{
printf("Gib eine Zeichenkette ein! ");
gets(string);
}
void ausgabe(unsigned char *string)
{
printf("Du gabst folgende Zeichenkette ein:\n%s\n",string);
}
Ah, da ist ja noch eine neue Funktion. :) Dann lasset sie mich euch erklären:
+++ exit() +++
Deklariert in: stdlib.h
Prototyp: void exit(int errorlevel);
Damit läßt sich ein Programm vorzeitig beenden und ein Wert zurückgeben, den
man in BAT-Dateien mit Errorlevel abfragen kann. Gleichzeitig werden alle
reservierten Speicherblöcke freigegeben, geöffnete Dateien geschlossen - kurz
gesagt, Aufräumarbeiten erledigt.
+++ far-Zeiger +++
Folgende Ausführungen gelten nur für den Real Mode von MS-DOS.
Normale Zeiger funktionieren in DOS nur innerhalb eines Segments. Wer
beliebige Speicherstellen ansprechen will, benötigt far-Zeiger. Diese sind
statt 16 Bit ganze 32 Bit groß. Die höherwertigen 16 Bit enthalten das
Segment, die niederwertigen den Offset. Definiert werden far-Zeiger, indem man
unmittelbar vor dem Multiplikationszeichen das Wort far schreibt.
Das Beispielprogramm zeigt, wie man in C eine Pixelsetzroutine für den
Bildschirmmodus 13h schreiben kann.
// C-Kurs 4: Far-Pointer
#include <conio.h>
// Funktionsprototypen
void screen(int nr);
void pset13h(int x,int y,unsigned char col);
// Hauptprogramm
void main()
{
screen(0x13);
pset13h(159,99,14);
getch();
screen(3);
}
// Funktionen
void screen(int nr)
{
asm {
mov ax,nr
int 0x10
}
}
void pset13h(int x,int y,unsigned char col)
{
unsigned char far *adresse;
adresse=((long)0xa000<<16); // Segment berechnen
adresse+=320*y+x; // Offset dazuaddieren
*adresse=col; // Wert schreiben
}
Bei screen() handelt es sich um eine Routine im Inline-Assembler von Borland
C++. In Quick C muß vor asm ein Unterstrich geschrieben werden. In Watcom C++
sieht der Inline-Assembler überhaupt wieder ganz anders aus.
Wer einen C-Dialekt benutzt, der keinen Inline-Assembler erhält, muß entweder
auf dieses Beispielprogramm verzichten oder das Programm umschreiben.
Interrupts lassen sich natürlich auch in C aufrufen. Auf dem PC geschieht dies
mit...
+++ int86() +++
Definiert in: dos.h
Prototyp: int int86(int intno,union REGS far * inregs,
union REGS far * outregs);
Die Register für Eingabe und Ausgabe müssen so definiert werden:
union REGS name_des_registersatzes;
Was es mit unions auf sich hat, werden wir in Teil 5 genau besprechen.
Jedenfalls handelt es sich bei REGS um einen selbstdefinierten Datentyp. Wir
können einzelne Elemente mit Hilfe des Punkt-Operators ansprechen. Haben wir
eine Variable regs des Datentypen REGS definiert, können wir bspw. das
AX-Register über regs.x.ax ansprechen. Wir müßten die Funktion screen()
unseres Beispielprogramms also durch diese Version ersetzen:
void screen(int nr)
{
union REGS regs;
regs.x.ax=nr;
int86(0x10,®s,®s);
}
Natürlich darf nicht vergessen werden, dos.h einzubinden. Schluß mit diesem
Exkurs.
+++ Doppelt-Zeiger usw. +++
Doppelt-Zeiger, Dreifach-Zeiger usw. gibt es auch. Es handelt sich dann um
einen Zeiger auf einen Zeiger ... auf eine Speicherstelle. Definiert werden
diese Poly-Zeiger genauso wie normale Zeiger - nur muß man die entsprechende
Anzahl von Multiplikationszeichen schreiben: void ******** achtfachzeiger;
etc.