3000 aa44 0000 T ... C-R&B TEIL 2
Kommen  wir zu einem weiteren Gesichtspunkt.  Zeiger sind ausge-
sprochen hilfreich bei der Verarbeitung von Strukturen.  Sie ma-
chen  es einem leicht,  diese zu verketteten Listen oder  Baeumen 
zusammenzubauen.  Erinnern  wir uns der Struktur namens xyz,  und 
vereinbaren  wir eine weitere von diesem Typ sowie  einen  Zeiger 
darauf:

          struct xyz anothr, *sp;
          sp = &>anothr; /* den Zeiger initialisieren */

Nun  koennen wir jede einzelne Komponente der Struktur,  auf  die 
der Zeiger verweist,  ansprechen.  Auf jede Komponente kann unter 
Verwendung des Operators -> zugegriffen werden.  Ein Beipiel dazu 
ist
          sp->aaa
wobei  sp  der Zeiger und aaa der Name der  Komponente  ist.  Das 
daraus resultierende Objekt hat denselben Typ wie die vereinbarte 
Komponente: sp->aaa bezieht sich auf die int-Komponente von
anothr,  sp->bbb  auf die char- und sp->ccc auf die double-Kompo-
nente. Der Operator -> hat ausserdem einen sehr hohen Vorrang.

Analog  zur Verarbeitung eines ganzzahligen Vektors koennte  auch 
ein  aus Strukturen bestehender Vektor verarbeitet werden,  indem 
man  den Inkrement-Operator verwendet.  In C ist  sichergestellt, 
dass  beim Addieren einer 1 zu einem Zeiger dieser  anschliessend 
auf das naechste Element eines Vektors zeigt, vorausgesetzt, dass 
der  Zeiger als Zeiger auf den ensprechenden Element-Typ  verein-
bart wurde.  C ueberprueft nicht,  ob ein Zeiger auf  irgendetwas 
Sinnvolles zeigt: es liegt bei Ihnen, dies sicherzustellen. Unab-
haengig davon,  wie Sie einen Vektor vereinbaren, duerfen Sie ihn 
in C beliebig indizieren; Zeiger sind ebensowenig eingeschraenkt.

Eine  Verwendung  von Zeigern koennen wir  anhand  einer  einfach 
verketteten  Liste  demonstrieren.  Es  wird ein Vektor  mit  100 
Strukturen  vereinbart,  wobei jede Struktur eine doppelt  genaue 
Gleitkomma-Variable enthaelt und einen Zeiger auf eine  derartige 
Struktur.  Eine  Schleife wird benutzt,  um a) den Zahlenteil auf 
1.0,  2.0 usw.  zu setzen und b) den Zeigerteil auf die  naechste 
Struktur der Liste zeigen zu lassen. Daran anschliessend wird die 
Liste  durchlaufen,  indem man den Zeigern folgt und die in jeder 
einzelnen Struktur enthaltene Zahl ausgibt.

          struct list_ele
          {    double num_part;
               struct list_ele *point_part;
          } list[100];

          main()
          {    int i;
               struct list_ele *lp;

               for (i=0;  i < 100;  i++)    /* Initialisierung */
               {    list[i].num_part = i + 1;
                    list[i].point_part = &>list[i+1];
               }	
               list[99].point_part = 0;     /* Ende markieren */

               for (lp=list; lp!=0; lp = lp->point_part) 
                                        /* Liste durchlaufen */
                    printf ("!f\n",hp)>num_part);	
          y

Lassen Sie uns die schwierigen Teile erklaeren.  Die Initialisie-	
rung ist verhaeltnismaessig einfach,  vorausgesetzt,  Sie erinnern 
sich, dass Vektorelemente von Null aufwaerts gezaehlt werden. Der 
Zeiger  am Ende der Liste wird auf 0 gesetzt;  dies ist  durchaus 
legal und sehr nuetzlich.  C garantiert,  dass ein Zeiger, dessen 
Wert 0 ist,  auf nichts zeigt. Damit ist dieser Wert dazu praede-
stiniert, einen 'Nullzeiger' darzustellen.
Das  Durchlaufen der Liste beginnt,  indem man den Zeiger lp  auf 
das  erste Element zeigen laesst.  Solange lp dann von Null  ver-
schieden ist,  wird der Informationsteil der Struktur (der Gleit-
komma-Wert)  ausgeggeben und lp der Wert des Zeigerteils zugewie-
sen;  dies bewirkt den Uebergang zum naechsten Element der Liste. 
Abgesehen vom ersten Element wird die Liste also positionsabhaen-
gig entlang der Zeiger durchlaufen.
Vergewissern  Sie sich ganz genau,  dass Sie dieses Beispiel ver-
stehen.  Die Zeit,  die Sie hier zum Verstehen investieren, zahlt 
sich aus,  wenn Sie C zum Erstellen wichtiger Programme verwenden 
muessen. Was hat es zur Folge, wenn die Zeile
          
          list[99].point_part = 0;
durch
          list[99].point_part = list;
ersetzt wird, und warum?

          



13. Funktionen
--------------
Es wird auch Zeit dazu.

13.1 Funktionen vereinbaren
---------------------------
Eine Funktion vereinbaren Sie,  indem Sie ihren Typ,  ihren Namen 
und ihre Parameter angeben;  daran anschliessend folgen die loka-
len Variablen.  Als naechstes kommt der Anweisungsteil der  Funk-
tion und am Schlusss der Ruecksprung,  der entweder implizit beim 
Erreichen  der  letzten schliessenden geschweiften Klammer  einer 
Funktion  oder durch eine explizite Angabe von  return  entsteht. 
Hier  ist  ein Beispiel einer Funktion,  welche die  Summe  ihrer 
Argumente zurueckgibt.

          int summe (arg1,arg2)
               int arg1, arg2;
          {    int total;

               total = arg1 + arg2;
               return (total);
          }


Die Funktion ist als int summe vereinbart, also als Funktion, die 
einen  ganzzahligen Wert liefert.  Die Typenbezeichnung int  wird 
nicht wirklich benoetigt, da C dies voraussetzt, sofern Sie keine 
Angabe dazu machen.  Die beiden Parameter,  arg1 und arg2, werden 
ebenfalls als ganzzahlige Werte vereinbart.  Auch Parameter brau-
chen nur deklariert zu werden, wenn es sich nicht um int handelt. 
In  unserem  Beispiel existiert auch eine lokale Variable  total; 
als Variable muss sie aber definiert werden - ob int oder nicht.
Die  Funktion berechnet die Summe ihrer Parameter.  Hier ist  sie 
nochmals,  aber in einem vollstaendigen Programm.  Die unnoetigen 
Deklarationen wurden entfernt:

          main()
          {    int i, j;
               for (i=1; i < 100; i++)
                    for (j = 1; j < 100; j++)
                         printf   ("%d+%d =%d\n",i,j,summe(i,j));
          }

          summe(a,b)
          {    int total;
               total = a + b;
               return (total);
          }

Die  Variable  total  waere  ebenfalls  ueberfluessig,   und  der 
Anweisungsteil koennte zu
          return (a + b);
reduziert werden, wenn Sie sich ueberhaupt die Muehe machen.

Die return-Anweisung braucht nicht unbedingt ein Argument. Durch
          return
allein wird ein Ruecksprung aus einer Funktion erreicht, aber mit 
einem  undefinierten Wert.  Dies hat durchaus seine Berechtigung, 
wenn Sie keinen Wert benoetigen,  z.B., weil Sie eine 'procedure' 
in Pascal formulieren wollen.  In C sind die beiden Funktionsauf-
rufe
          summe (x,y);
und
          a = summe (x,y);
zulaessig  - der erste Aufruf von summe liefert einen  Wert,  der 
ignoriert wird.

Es gibt keinerlei Garantie,  dass bei Funktionsbeginn die lokalen 
Variablen bestimmte Werte enthalten,  und genauso sicher behalten 
sie  ihre Werte nicht ueber wiederholte Funktionsaufrufe  hinweg. 
Sie  muessen einer lokalen Variablen immer einen  Wert  zuweisen, 
bevor  Sie sie verwenden.  'Globale Variablen' unterscheiden sich 
darin:  sie stehen waehrend des gesamten Porgrammablaufs zur Ver-
fuegung und sind bei Programmstart auf Null initialisiert, sofern 
sie nicht extra auf andere Werte gesetzt wurden. Im Anhang F wird 
ersichtlich, wie man globale Variablen initialisiert.

Wenn Sie nichts vereinbaren,  setzt C voraus,  dass eine Funktion 
einen  ganzzahligen Wert liefert.  Wird irgendetwas  Anderes  zu-
rueckgeliefert,  geraet  die aufrufende Funktion etwas in Verwir-
rung,  sofern Sie es ihr nicht mitteilen.  Man vermeidet Probleme 
am  einfachsten,  indem  man alle ungewoehnlichen  Funktionen  im 
Programmkopf deklariert.  Wir machen dies hier beim Berechnen der 
Quadratwurzel  von  sin(x)  + sin(x) * cos(x)  fuer  verschiedene 
Werte zwischen 0 und 2*pi:

     #define ZWEIPI (3.141592 * 2)

     extern double sin(), cos(), sqrt();
     double func();

     main()
     {    double i;
          for (i=0; i < ZWEIPI; i += 0.1)
               printf ("i = %f, f = %f\n", i, func(i));
     }

     double func(x)
          double x;
     {    
          return (sqrt(sin(x) + sin(x)*cos(x)));
     }
     
Die erste Zeile gehoert nicht wirklich zu C.  Sie wird vom  'Pre-
prozeesor' ausgewertet,  den das cc-Kommando vor der Uebersetzung 
eines  Programms  aufruft.  Der Preprozessor erkennt  das  Symbol 
#define  und notiert sich,  was definiert wurde,  in diesem  Fall 
ZWEIPI. Wo immer dieser Nmae dann im Programmtext auftaucht, wird 
er  durch  den Rest der Zeile ersetzt,  in der  ZWEIPI  definiert 
wurde.  Diese  Eigenschaft  wird haeufig ausgenutzt und  verhilft 
Programmen zu einer besseren Lesbarkeit und leichteren Handhabung 
- falls jede Konstante nach diesem Verfahren definiert wird,  ist 
nur eine Aenderung einer Zeile notwendig,  um sie ueberall  anzu-
passen.

Die Zeile
          extern double sin(), cos(), sqrt();
drueckt  aus,  dass  das Programm die drei  Funktionen  sin,  cos 
undsqrt verwenden will,  die alle double-Resultate  liefern.  Das 
Symbol  extern bedeutet,  dass sie nicht Teil der aktuellen Datei 
sind,  sondern von aussen bezogen werden.  In Wirklichkeit stehen 
sie in einer Buecherei. Vergleichen Sie dies mit der Vereinbarung 
vin func,  die in dieser Datei zu finden ist. Die Deklaration von 
Funktionen am Anfang der Datei vermeidet, dass bei der Verwendung 
der  Funktionen ganzzahlige Resultate erwartet werden.  Der  Rest 
des Programms sollte nun klar sein! 


13.2 Argumente von C-Programmen
-------------------------------
Einem Programm,  das mit Hlfe der 'Shell' gestartet wird, koennen 
Argumente uebergeben werden. Sie haben dies bereits bei Kommandos 
wie  'ls' und vielleicht auch 'cc'  angewendet.  Diese  Argumente 
erscheinen  als  Parameter  von main und  sind  innerhalb  dieser 
Routine verfuegbar. Die exakte Deklaration fuer sie ist:

          main (argc,argv)
               int argc;
               char ** argv;
          {

Dies  drueckt aus,  dass argc ein ganzzahliger Wert und argv  ein 
Zeiger  auf eine Reihe von Zeichenketten ist.  Wenn ein  Programm 
zur Ausfuehrung kommt, so enthaelt argc die Anzahl der uebergebe-
nen  Argumente,  und  argv zeigt auf den Anfang einer  Liste  von 
Adressen,  ueber die die eigentlichen Argumente erreichbar  sind. 
Als  letzte Adresse,  also als argv[argc],  steht Null in  dieser 
Liste. Hier folgt ein Programm, das seine Argumente wieder ausgibt:

          main (argc,argv)
               int argc;
               char ** argv;
          {
               while  (*argv)        /* wenn noch ein Argument */
                   printf ("%s\n",*argv++);   /* dann ausgeben */
          }

Mit %s wird printf signalisiert, dass es eine Zeichenkette erwar-
ten  soll.  Ueberlegen Sie genau,  warum diese Programm  funktio-
niert,  und  probieren  Sie  es dann aus.  Sie haben  soeben  das       
Dienstprogramm echo des Syestem nachgebaut.

Gut,  es ist beinahe echo, mit dem kleinen Unterschied, dass Sie, 
wenn Sie es testen, ein wenig ueberrascht werden. Als erstes gibt 
es  seinen Namen aus (a.out,  sofern Sie ihn nicht geaendert  ha-
ben),  weil  gemaess einer Vereinbarung das erste an ein Programm 
uebergebene Argument immer sein Name ist. Einige Programme nehmen 
auf diese Tatsache Bezug und steuern in Abhaengigkeit davon  ihre 
Aktionen - dies sind eine Art verborgener Flaggen.


13.3 Was steckt hinter einem Argument?
--------------------------------------
Wenn eine Funktion aufgerufen wird,  so stehen ihre Parameter wie 
initialisierte  lokale Variablen zur Verfuegung.  Veraendert  die 
Funktion  die  Werte  von Parametern,  so ist  die  entsprechende 
Veraenderung  in  dem Programmteil,  der die  Argumente  uebergab 
(beim Aufrufer),  nicht sichtbar. Dies ruehrt davon her, dass der 
Uebersetzer  von allen Funktionsargumenten eine Kopie  macht  und 
diese  anstelle der Originale uebergibt.  Wie baut man dann  aber 
Funktionen, die Dinge beim Aufrufer veraendern? Machbar ist dies, 
indem man Zeiger anstelle der Ojekte selbst uebergibt. Testen Sie 
folgendes und erkennen Sie, warum es klappt:

     main()
     {    int i;
          i=10;
          zero(&>i);      /* uebergib die Adresse von i,  */
                         /* d.h. einen Zeiger auf i, und */
          printf ("i=%d\n",i);     /* gib Ergebnis aus   */
     }

     zero(arg)
          int *arg;      /* dorthin, worauf arg zeigt    */
     {
          *arg=0;                 /* eine Null schreiben */
     }

Der  Funktion  zero wird ein Zeiger auf einen  ganzzahligen  Wert 
uebergeben, und sie setzt das Objekt auf Null, auf das der Zeiger 
verweist.


14. Die Standard-Buecherei
--------------------------
Es  wurden ziemlich grosse Anstrengungen unternommen,  um  C-Pro-
gramme portabel zu machen.  Man hat sich vor allem darum bemueht, 
eine  einheitliche und portable Ein- und Ausgabe-Schnittstelle  - 
die  'Standard-E/A-Buecherei'  - zur Verfuegung  zu  stellen.  Um 
unsere Finger zu schonen (wir tippen selber),  wollen wir sie von 
nun an mit 'SIO' abkuerzen. Um SIO zu verwenden, muss ziemlich am 
Anfang  Ihres Programmes folgende Kontrollzeile fuer den  Prepro-
zessor stehen:

               #include <stdio.h>

wobei # in der ersten Spalte steht.  Die Kontrollzeile veranlasst 
den Preprozessor,  eine Datei, die alle notwendigen Deklarationen 
und Definitionen fuer SIO enthaelt,  zu lesen;  vom  Preprozessor 
gehen  die  Definitionen an den Uebersetzer weiter.  Sie  muessen 
sich also nicht darum kuemmern, was darin steht.
Die  ganze Buecherei ist viel zu umfangreich,  um sie  in  diesem 
Kapitel  zu  beschreiben.  Kernighan und Ritchie [6]  haben  dies 
schon  ausgezeichnet getan,  und ein Ueberblick wird im Kapitel 9 
gegeben;  hier koennen wir hoechstens einige kurze Beipiele  zei-
gen.


14.1 Lese- und Schreiboperationen
---------------------------------
Im  Normalfall verfuegt Ihr Programm ueber drei offene  Dateiver-
bindungen;  sie  werden  von der Shell uebergeben  und  sind  als 
Standard-Eingabe  (standard  input),  Standard-Ausgabe  (standard 
output) und Diagnose-Ausgabe (standard error) bekannt.  Um diese, 
wie genau jede andere Datei, per SIO anzusprechen, benoetigen Sie 
sogenannte 'Filepointer'.  Es handelt sich dabei wirklich um Zei-
ger,  die  von SIO-Routinen zur Verwaltung von Strukturen benutzt 
werden, deren Form uns gleichgueltig sein kann.
SIO vereinbart drei dieser Zeiger fuer uns; sie werden als stdin, 
stdout und stderr bezeichnet,  was bei Ihnen keine Zweifel  ueber 
ihre Verwendung hinterlassen sollte.  Wir betrachten die Ein- und 
Ausgabe  zuerst  anhand dieser vordefinierten  Dateiverbindungen, 
bevor wir untersuchen, wie man einen Filepointer fuer eine belie-
bige Datei vereinbart. Zum Lesen eines Zeichens verwenden Sie die 
Funktion getc:

          c = getc(stdin)

liest  ein Zeichen von der Standard-Eingabe und weist es  an  die 
Variable c zu.  Dieselbe Funktion darf mit jedem beliebigen File-
pointer verwendet werden,  den Sie erhalten haben.  Beim Versuch, 
ueber das Dateiende hinaus zu lesen,  wird der spezielle Wert EOF 
geliefert;  EOF  ist als Teil von SIO vordefiniert.  Die von getc 
gelieferten Werte sind int und nicht char, da neben allen zulaes-
sigen  Werten  fuer Zeichen auch der Wert  EOF  geliefert  werden 
kann. In eine Datei zu schreiben, ist genauso einfach:

          putc (c,stdout)

schreibt ein Zeichen c in die Standard-Ausgabe.  Wiederum duerfen 
Sie  jeden  beliebigen  Filepointer verwenden  - wie  waer's  mit 
stderr?  Seien  Sie vorsichtig bei der Verwendung von Zeigern auf 
Zeichen im Zusammenhang mit putc.  Mit grosser Wahrscheinlichkeit 
handelt  es  sich bei putc um etwas mit der  Bezeichnung  'Makro' 
(Makros  sind eine weitere Eigenschaft des  Preprozessors).  Wenn 
ein Makro durch den Preprozessor verarbeitet wird,  koennen  meh-
rere Zugriffe auf c entstehen. Dies stellt kein Problem dar, wenn 
c  eine  einfache Variable ist.  Handelt es sich dabei jedoch  um 
einen Zeiger mit einem hinzugefuegten Inkrement-Operator, koennte 
der Zeiger mehrmals inkrementiert werden.  Wir muessen daher emp-
fehlen,  die  Verwendung von Inkrement- und  Dekrement-Operatoren 
innerhalb von Aufrufen von SIO-Funktionen zu vermeiden.

Ein- und  Ausgabe  von  Zeichen wird in dem  folgenden  einfachen 
Beispiel demonstriert; hier wird die Standard-Eingabe zeichenwei-
se in die Standard-Ausgabe kopiert.  Gleichzeitig wird dabei  die 
Anzahl  der  kopierten Zeichen bestimmt und als  Diagnose-Ausgabe 
angezeigt.

     #include <stdio.h>

     main()
     {    int c;
          int count=0;
                         /* Zeichen bis zum Dateiende lesen */
          while ((c=getc(stdin)) != EOF)     /* solange das */
          {    count++;        /* Dateiende nicht  erreicht */
               putc(c,stdout);      /* zaehlen und ausgeben */
          }
                   /* Anzahl der gelesenen Zeichen ausgeben */
          fprintf(stderr, "%d Zeichen gelesen\n",count);
     }

Beachten Sie, wie while das Ergebnis einer Zuweisung ueberprueft. 
Dies  ist ein weit verbreiteter Einsatz einer Zuweisung  - einge-
baut  in  eine  bedingte Anweisung mit  gleichzeitigem  Test  des 
Ergebnisses - Sie tun gut daran,  sich dies einzupraegen. Ausser-
dem finden Sie hier einen Aufruf von fprintf,  welcher fast iden-
tisch zu dem von printf ist, den wir bereits kennengelernt haben. 
Der einzige Unterschied besteht darin,  dass fprintf einen  File-
pointer  als  erstes  Argument erwartet und auf  die  zugehoerige 
Datei ausgibt. printf gibt nur auf die Standard-Ausgabe aus.

Uebersetzen Sie das Programm und starten Sie es,  wobei als  Ein-
gabe sein eigener Quelltext verwendet wird.  Hier sehen Sie,  wie 
wir es machten - die Datei mit dem Quelltext heisst copy.c.

     $ cc copy.c
     $ a.out <copy.c
               ( es gibt seinen eigenen Quelltext aus)
     339 Zeichen gelesen
     $


14.2 Dateiverbindungen
----------------------
Es  ist  ganz nett,  mit den Standard-Ein- und Ausgabedateien  zu 
spielen,  aber was wir damit anfangen koennen, ist limitiert. Die 
groesseren  Maedchen und Jungen moechten unbedingt  ihre  eigenen 
Dateiverbindungen  einrichten.  Zuerst benoetigen wir dazu einige 
Filepointer.
SIO stellt dazu einen Typ namens FILE zur Verfuegung, so dass Sie 
Ihre  eigenen  Filepointer vereinbaren koennen und  sicher  sind, 
dass  diese  den richtigen Typ besitzen.  Haben Sie  erst  einmal 
einen  Filepointer vereinbart,  muessen Sie ihn mit  einer  Datei 
verbinden.  Gluecklicherweise  gibt es eine Funktion,  die  diese 
Aufgabe uebernimmt.  Um eine Dateiverbindung zu eroeffnen,  rufen 
Sie  die  Funktion  fopen auf und uebergeben ihr  den  Namen  der 
gewuenschten  Datei  und die Zugriffsart,  entweder "r","w"  oder 
"a";  dies  bedeutet lesen,  schreiben oder anfuegen.  Als  Namen 
geben  Sie  den UNIX-Pfadnamen der  gewuenschten  Datei  an.  Als 
Ergebnis  liefert  fopen entweder einen  zulaessigen  Filepointer 
oder  den vordefinierten Wert NULL;  Sie sollten immer durch eine 
Ueberpruefung sicherstellen, dass die Dateiverbindung erfolgreich 
eroeffnet  wurde.  Hier  folgt ein Programm,  welches  die  Datei 
namens fred in eine Datei namens bill kopiert:

     #include <stdio.h>

     main()
     {    FILE *fp, *bp;                /* zwei Filepointer */
          int c;
                               /* Datei zum Lesen eroeffnen */
          if ((fp=fopen("fred","r")) == NULL)
          {    fprintf(stderr,"Kann fred nicht eroeffnen\n");
               exit(1);
          }
                              /* Datei zum Schreiben eroeffnen */
          if ((bp=fopen("bill","w")) == NULL)
          {    fprintf(stderr,"Kann bill nicht eroeffnen\n");
               exit(1);
          }

          while ((c=getc(fp)) != EOF)
               putc(c,bp);
     }

Die Funktion exit bricht ein Programm immer unverzueglich ab; wir 
garantieren, dass das Programm von diesem Funktionsaufruf niemals 
zurueckkehrt.  Das an exit uebergebene Argument wird manchmal von 
dem Programm ausgewertet, von dem das aktuelle Programm gestartet 
wurde. Sie sollten immer einen Wert zurueckgeben, der anzeigt, ob 
das  Programm korrekt abgelaufen ist oder  nicht.  Im  Normalfall 
bedeutet  Null Erfolg,  und jeder andere Wert signalisiert,  dass 
ein Problem aufgetaucht ist; deshalb haben wir eine 1 gewaehlt.

Die Argumente bei fopen,  die Lesen,  Schreiben und Anfuegen ver-
langen,  verstehen  sich weitgehend von selbst.  Lesen ist offen-
sichtlich - das Programm wird mit einer existenten Datei  verbun-
den  und  liest daraus.  Beim Schreiben wird die  Datei  erzeugt, 
falls sie nicht bereits existierte;  falls sie bereits existiert, 
so  wird der gesamte vorige Inhalt vernichtet.  Anfuegen funktio-
niert wie schreiben,  aber der vorherige Inhalt der Datei  bleibt 
erhalten.


15. Das Ende!
-------------
Gut,  bringen  wir die Sache zu Ende.  Wir haben kaum ein Drittel 
dessen  erreicht,  was wir beabsichtigten,  aber wir  haben  eben 
nicht  genuegend  Platz dafuer.  Viele Leser werden  von  unserer 
Auswahl enttaeuscht sein,  d.h.  von dem, was wir gezeigt und was 
wir ausgelassen haben.  Diese fordern wir auf, 'Schreiben Sie Ihr 
eigenes Buch', und wir wuenschen ihnen dazu alles Gute.

Sie haben nun einen grossen Teil der Sprache kennengelernt,  aber 
Sie  brauchen  noch viel Uebung,  um sie  gruendlich  zu  lernen. 
Experimentieren  Sie,  verschlingen Sie die Protokolle von  moeg-
lichst  vielen realen Programmen,  lesen Sie andere Buecher,  und 
geniessen Sie das. Als abschliessendes Beispiel praesentieren wir 
ein kur
zes und etwas haessliches Dienstprogramm, welches an einem 
lang
weiligen  zweiten Weihnachtsfeiertag von Banahan  geschrieben 
wur
de.  Es heisst 'ovp';  es liest entweder die  Standard-Eingabe 
oder  von  angegebenen  Dateien und schreibt  auf  die  Standard-
Ausgabe.  Es schreibt, was es liest und verwandelt dabei Zeichen, 
die  unter
strichen sind,  in Zeichen,  die mehrfach uebereinander 
geschrieben sind.  In Bradford war es einige Zeit im Einsatz, und 
es  enthaelt  keine uns bekannten Fehler.  Den  Programmtext  von 
'ovp' finden Sie im Anhang F. 

'ovp' enthaelt Register-Variablen;  wir haben diese frueher nicht 
erwaehnt.  Prinzipiell  unterscheiden sie sich nicht von  anderen 
Variablen,  aber  sie besitzen keine Adresse;  Sie koennen  daher 
keinen  Zeiger  auf sie zeigen  lassen.  Die  Vereinbarung  einer 
Register-Variablen  ist  ein  Hinweis (nur ein Hinweis!)  an  den 
Uebersetzer,  dass sie haeufig benutzt werden soll,  und dass  es 
eine  gute Idee waere,  ein Hardware-Register an ihrer Stelle  zu 
verwenden.  Register-Variablen dort zu vereinbaren,  wo sie nicht 
notwendig sind,  kann das Gegenteil Iherer Absicht bewirken:  Sie 
koennten den Uebersetzer in die Irre fuehren, so dass er speziel-
le  Variablen  in Registern haelt,  obwohl die Register  anderswo 
effizienter genutzt werden koennten.


´äČ
Anhang F: "ovp" - ein Dienstprogramm in C
-----------------------------------------
'ovp'  soll Text ausgeben und dabei unterstrichene Zeichen fett, 
d.h.  mehrfach, drucken. Ein solches Programm ist z.B. nuetzlich, 
wenn  man  eine  kursive Ausgabe von  'nroff'  (die  traditionell 
unterstrichen dargestellt wird) durch eile Art vmn Fettdruck  auf 
einem  konventionellen Drucker darstellen will.  Ein Zeichen gilt 
als  unterstrichen,  wenn mit Hilfe von 'backspace'  schliesslich 
ein  Unterstrich _ in die gleiche Spalte am  Drucker  praktiziert 
wird.

Im allgemeinen sind C-Programme nicht gerade ausfuehrlich kommen-
tiert.  Ein  kleines Programm wie 'ovp' kann man auch  ohne  viel 
Kommentar  verstehen.  Ein C-Programm besteht ueblicherweise  aus 
relativ  kleinen  Funktionen,  die jeweils eine  einzige  Aufgabe 
erfuellen. Wenn Sie die Aufgabe jeder Funktion verstehen, koennen 
Sie  normalerweise das Programm im Grossen verstehen,  bevor  Sie 
sich klar machen, was in jeder Funktion im Detail geschieht.

Wir haben absichtlich vermieden, den Text von 'ovp' mit Kommenta-
ren  zu  ueberladen.  Das Programm gleicht daher den  meisten  C-
Programmen,  denen  wir begegnet sind.  Es schien uns nicht sinn-
voll,  ein atypisches Beispiel vorzustellen,  und wir haben daher 
die  Erklaerungen in diesen Anhang,  aber nicht in  das  Programm 
aufgenommen. 

Die Strategie des Programms ist recht einfach, in main werten wir 
zunaechst  die  Optionen aus.  Anschliessend bearbeiten  wir  der 
Reihe  nach  die  Dateien,  deren Namen als  Argumente  angegeben 
wurden.  Wurden keine Dateinamen uebergeben,  so bearbeitet 'ovp' 
seine  Standard-Eingabe,  verhaelt  sich also wie  ein  typischer 
Filter.

handle  bearbeitet  jeweils eine Datei,  die in  main  mit  stdin 
verbunden  wurde.  Zeichen werden einzeln gelesen und untersucht. 
Tabulatorzeichen  verwandeln wir hier in eine Folge von  Leerzei-
chen,  Zeilentrenner  \n oder Seitenvorschub \f  beenden  jeweils 
eine  Textzeile,  und EOF bedeutet genau dieses,  naemlich Datei-
ende.  Die meisten Zeichen gehen direkt weiter an save und werden 
dort in chars notiert. save verfolgt ausserdem, in welcher Einga-
bespalte posn wir uns befinden.

Ist  in  den Speicherflaechen noch Platz vorhanden,  so wird  ein 
normales  Zeichen in chars abgelegt.  Fuer ein _Zeichen  notieren 
wir im hoechsten Bit von chars (das ja fuer den ASCII-Zeichensatz 
nicht  benoetigt wird),  dass das entsprechende Zeichen in  chars 
ueberdruckt werden muss.  Bei \b gehen wir eine Spalte nach links 
zurueck.

Ist kein Platz vorhanden, speichern wir die Zeichen nicht weiter, 
zaehlen aber die Spaltenposition trotzdem hoch, damit beim Expan-
dieren  von  Tabulatorzeichen keine  endlose  Schleife  entstehen 
kann.

Erreichen  wir  in handle schliesslich das Ende einer  Zeile,  so 
uebergeben wir die einzelnen Zeichen aus dem Zwischenspeicher  an 
out  zur Ausgabe.  Gleichzeitig loeschen wir den Zwischenspeicher 
wieder.  Den Zeilentrenner uebergeben wir direkt;  damit  koennen 
auf keinen Fall Lawinen von Leerzeilen durch Ueberdrucken entste-
hen.

out  merkt  sich intern ueber alle  Aufrufe  hinweg,  in  welcher 
Ausgabespalte  wir  uns befinden;  die dazu  verwendete  Variable 
outcol ist in der Speicherklasse static definiert, damit ihr Wert 
ueber Aufrufe hinweg erhalten bleibt. Ein Leerzeichen wird norma-
lerweise  nicht  sofort ausgegeben,  sondern ebefalls intern  no-
tiert;  damit koennen Folgen von Leerzeichen durch wenige Tabula-
torzeichen dargestellt werden. 

Ein isoliertes Zeichen wird erkannt und umcodiert,  anschliessend 
werden  vor jedem Zeichen eventuell noch gespeicherte Leerzeichen 
ausgegeben. Soll das Zeichen ueberdruckt werden, so geschieht das 
ganz am Schluss.

Am  schwierigsten  fuer den Anfaenger sind natuerlich die  ebenso 
trickreichen wie allgemein verwendeten Anweisungen in main ,  mit 
denen die Optionen ausgewertet werden. Sie sollen diese Formulie-
rungen,  vermutlich anhand einer Skizze der verschiedenen Zeiger, 
genau ansehen,  und sie dann in Ihren eigenen Programmen entspre-
chend verwenden.


/*
 *   ovp - statt unterstreichen mehrfach drucken
 *
 *  TAB-Positionen sind in Spalte 1, 9, 17
 *  NUNDER gibt an,  wie oft ein unterstrichnenes Zeichen  hoech-
 *  stens  gedruckt werden soll - je nach Drucker gibt's ab 4
 *  Aerger.
 *  -on  es soll n-mal (statt NUNDER) ueberdruckt werden
 *  -t   TAB durch Leerzeichen ersetzen
 */

#include <stdio.h>       /* Standard-Buecherei */

#define   LINESIZ   512  /* max. Anzahl Zeichen pro Zeile */

#define   NUNDER    4    /* max. Anzahl fuer Ueberdruck */

char chars[LINESIZ];
int posn;                /* Spalte in der Eingabe */
int nover = NUNDER;      /* kontrolliert ueberdrucken */
int tflag = 0;           /* !=0: TAB ersetzen */

main (argc,argv)
     int argc;                     /* Anzahl Argumente */
     char ** argv;                 /* Texte der Argumente */
{
     while (--argc &>&> **++argv == '-')  
          switch (*++*argv) {
          case 'o':
               nover = atoi(++*argv);
               if (nover < 0 || nover > NUNDER)
               {    fprintf(stderr,"ovp: zu oft -o%s\n", *argv);
                    nover = NUNDER;
               }
               break;
          case 't':
               ++tflag
               break;
          default:
               fprintf(stderr,"ovp: unbekanntes Argument -%s\n",
                       *argv);
          }
          
          if(argc == 0)       /* kein Dateiname als Argument */
               handle();
          else          /* Dateien der Reihe nach bearbeiten */
               do   
                    if (freopen (*argv,"r",stdin))
                         handle();
                    else
                         perror (*argv);
               while (*++argv);
}

handle()                                /* stdin bearbeiten */
{    register int i, c;

     for(;;)                            /* jede Zeile */
     {    for(;;)                       /* eine Zeile */
          {    switch (c = getchar()) {
               case EOF:
                    return;
               case '\t':
                    do
                         save(' ');
                    while (posn &> 07);
                    continue;
               default:
                    save(c);
                    continue;
               case '\f':
               case '\n':
                    ;
               }
               for (i=0; i < posn &>&> i < LINESIZ; ++i)
               {    out (chars[i]);
                    chars[i] = 0;
               }
               out ('\n',0), posn = 0;
               break;                   /* naechste Zeile */
          }
     }
}

save(c)                       /* Zeichen zwischenspeichern */
     char c;
{
     switch (c) {
     case '\b':               /* wird nicht gespeichert */
          if (posn)
               --posn;
          return;
     case '_':                /* im hoechsten Bit notiert */
          if (posn < LINESIZ)
               chars[posn] |= 0200;
          break;
     default:       /* in den niedrigen Bits gespeichert */
          if (posn < LINESIZ)
               chars[posn] = c &> ~0200;
     }
     ++posn;
}

out(c)                             /* Zeichen ausgeben */
     char c;
{    static int spaces = 0;        /* TAB einfuegen */
     static int outcol = 0;        /* Spalte in der Ausgabe */
     register int target;
     register int ov;

     ov = (c &> ~0200) ? nover: 0;
     switch (c &>= ~0200) {
     case '\n':                    /* Zeilentrenner */
          putchar(c), outcol = spaces = 0;
          return;
     case 0:                       /* isolierter _ */
          c = '_', ov=0;
          break;
     case ' ':                     /* optimieren ? */
          if (tflag == 0)
          {    ++spaces;
               return;
          }
     }
               /* vor anderen Zeichen: Leerzeichen ausgeben */
     target = outcol + spaces;
     switch (spaces) {
     default:            /* TAB nur fuer viele Leerzeichen */
          while ((outcol+8 &> ~07) <= target)
               putchar ('\t'), outcol = outcol+8 &> ~07;
     case 1:             /* wenige Leerzeichen bleiben */
          while (outcol < target)
               putchar(' '), ++outcol;
          spaces = 0;
     case 0:
          ;
     }

                         /* Zeichen ausgeben und ueberdrucken */
     putchar(c), ++outcol;
     while (ov--)
     {    putchar ('\b');
          putchar (c);
     }
}