Adok's Way to C
Teil 4

Auch  heute  mchte ich euch  zu einer neuen Folge  dieses  Kurses  willkommen
heien!  Diesmal geht es  um das ebenso wichtige  wie komplizierte Kapitel des
Zeigers.  Ich  versuche,  es mglichst  verstndlich  rberzubringen.  Sollten
nachher noch Fragen offen bleiben, knnt 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 fr Programmierer mit gutem  Gedchtnis bringt der indirekte Zugriff
Vorteile.  Beispielsweise  kann  man damit jede  beliebige  Speicherstelle  im
konventionellen RAM ansprechen.  Das ist wichtig fr 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 mtet einer Funktion einen groen Array bergeben,  der
z.B.  einen 600 Zeilen langen Text enthlt  (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).
Wrde man  die 600 Zeilen Text  direkt bergeben, knnte es unter Umstnden zu
einem  Stackberlauf  kommen - mit der Folge,  da das Programm abstrzt.  Mit
Zeigern aber lt 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 frher  mit Zeigern zu tun gehabt.  Erinnert  ihr  euch?  Ich
mchte 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 wrden keine Zeiger,  sondern direkt die
   Variablen,  in denen die Eingaben gespeichert werden sollen,  als Parameter
   bergeben werden. Dazu mte scanf() diese Variablen verndern knnen. Eine
   Funktion  kann aber nie die  Variablen verndern,  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 gelscht werden. Im Gegensatz dazu kann man mit
   Zeigern direkt auf die Originale zugreifen und die Originale verndern.  So
   tut es auch scanf.

2. Arrays:  Ja, auch die Arrays haben etwas mit Zeigern zu tun! Die Definition
   char feld[500];  beispielsweise bedeutet nmlich 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 Fllen tatschlich
   mglich,  den Namen eines Arrays  wie einen Zeiger zu verwenden.  Gegenber
   richtigen Zeigern  knnen Arraynamen  jedoch in ihrem ganzen Leben :) immer
   nur  auf ein und dieselbe  Speicherstelle zeigen - nmlich  auf  das  erste
   Element des dazugehrigen  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 tatschlich! void kann eben nicht
nur  verwendet  werden,   um  bei  Funktionen   zu  signalisieren,   da  kein
Rckgabewert existiert oder sie keine Parameter empfangen knnen, 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 verndert.

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 verndert.

Pat  auf,  da ihr  diese beiden Arten  nicht verwechselt!  Sonst  knnte  es
gefhrlich 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 nchsten Beispielprogramm mchte  ich euch zeigen, wie man das, worber ich
am  Anfang  im  Absatz  'zum anderen' gesprochen habe, realisiert.  Damit  ihr
spter  nicht  immer  sagen  mt  "ich mache  jetzt das,  worber 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 heit 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
enthlt.  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  fr  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  htte man  in den Prototypen und Funktionskpfen
auch   unsigned char string[] schreiben   knnen.   Arrays   werden   bei  der
Parameterbergabe  genauso wie Zeiger  behandelt.  Nach  dem,  was ich  in der
Einleitung zu dieser Kursfolge  ber Arrays gesagt habe,  drfte dies fr euch
verstndlich sein.

Wie ihr wit, kann jede Funktion auch einen Wert zurckgeben. Den Datentyp des
Rckgabewerts  mu am Anfang des Funktionskopfs und des Prototyps stehen.  Der
Kopf  einer Funktion readkey(),  die einen unsigned-char-wert  zurckgibt  und
keine Parameter hat, lautet also unsigned char readkey(void).

Und wie  lt man  diese Funktionen  einen  Wert  zurckgeben?  Dazu  ist  das
Schlsselwort  return  da.  Parameter ist der Rckgabewert bzw.  die Variable,
deren  Inhalt   zurckgegeben  werden  soll.   return  lt   sich  auch   bei
void-Funktionen   einsetzen,   um  die  Ausfhrung  der   Funktion   vorzeitig
abzubrechen.

Hier nun als Beispiel eine mgliche 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 zurckgeben
}

Diese Funktion readkey() dient dazu, um festzustellen, ob und, wenn ja, welche
Taste gerade gedrckt wird.

inp()  ist in conio.h  deklariert  und  dient  zum Auslesen eines Ports.  ber
Port 60h  (hex, in C also 0x60)  lt sich der Scan-Code der gerade gedrckten
bzw. zuletzt gedrckten Taste auslesen.  Ist Bit 7 gesetzt ((taste&128)==128),
so  wird  gerade  keine  Taste  gedrckt.   readkey()   gibt  dann  0  zurck.
Andernfalls wird der Scancode der gedrckten Taste zurckgegeben.

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.  Nhere  Informationen  zu Ports  findet ihr  im
Assembler-Kurs. Nun zurck zu den Zeigern.

+++ Dynamische Arrays +++

Es gibt eine weitere wichtige Einsatzmglichkeit von Zeigern.  Einige von euch
werden vielleicht aus Quick Basic die dynamischen Arrays kennen. Diese sind in
ihrer Gre  nicht statisch und knnen jederzeit vergrert,  verkleinert oder
verschoben werden.  Auch in C lassen sich solche dynamischen Arrays  erzeugen,
auch wenn sie hier nicht Arrays heien.  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 spter in diesem Kurs noch zu sprechen kommen.

Der Rckgabewert 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  Speichermllhalde,  dem  Heap,  nicht  genug  Speicher frei ist,  liefert
malloc() den Nullzeiger (NULL) zurck. 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 bentigt,  mu man ihn  sptestens  am  Programmende
unbedingt  von  free()  freigeben lassen.  Tut man das nicht,  hinterlt  das
Programm  Rckstnde.   Die  Daten  des  Programms   nehmen  dann  immer  noch
Speicherplatz  ein,  obwohl  das Programm selbst  schon  lngst  beendet  ist.
Darber "freut" sich der User,  der dann weniger freien RAM  zur Verfgung 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?   Womglich
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  Verndern   der  Gre   des  durch  pointer  und  size
beschriebenen Speicherblocks. Es kann passieren, da realloc() beim Vergrern
den  Speicherblock  verschieben mu,  weil im aktuellen  Speicherbereich  kein
Platz  mehr  zur  Expansion  ist.  Deshalb sollte  man  den  Rckgabewert  von
realloc() - den neuen oder, falls es keine Vernderungen 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 fr die Gre des Arrays

  printf("Wieviel Speicherplatz (in Byte) soll fr den String reserviert "
         "werden? ");
  scanf("%d",&anzahl_bytes_reservieren);
  fflush(stdin);

  // Wenn zu wenig Heap frei, Programmausfhrung abbrechen
  if( (string=(char *)malloc(anzahl_bytes_reservieren)) == NULL )
  {
    printf("\nFehler! Breche Programmausfhrung 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 erklren:

+++ exit() +++

Deklariert in: stdlib.h
Prototyp:      void exit(int errorlevel);

Damit lt sich ein Programm  vorzeitig beenden und ein Wert zurckgeben,  den
man in BAT-Dateien  mit Errorlevel  abfragen kann.  Gleichzeitig  werden  alle
reservierten Speicherblcke freigegeben,  geffnete Dateien geschlossen - kurz
gesagt, Aufrumarbeiten erledigt.

+++ far-Zeiger +++

Folgende Ausfhrungen gelten nur fr den Real Mode von MS-DOS.

Normale  Zeiger  funktionieren  in  DOS  nur  innerhalb  eines  Segments.  Wer
beliebige  Speicherstellen  ansprechen will, bentigt far-Zeiger.  Diese  sind
statt  16 Bit  ganze  32 Bit gro.  Die  hherwertigen  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  fr  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 erhlt, mu entweder
auf  dieses  Beispielprogramm   verzichten   oder  das  Programm  umschreiben.
Interrupts lassen sich natrlich 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 fr Eingabe und Ausgabe mssen 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
knnen  einzelne Elemente mit Hilfe  des Punkt-Operators ansprechen. Haben wir
eine  Variable  regs  des  Datentypen REGS  definiert,  knnen  wir  bspw. das
AX-Register  ber  regs.x.ax  ansprechen.  Wir  mten  die  Funktion screen()
unseres Beispielprogramms also durch diese Version ersetzen:

void screen(int nr)
{
  union REGS regs;

  regs.x.ax=nr;
  int86(0x10,&regs,&regs);
}

Natrlich 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.
