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); } }