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.