============================================== Ein Kurzlehrgang fuer die Programmiersprache C ============================================== Kapitel 4 aus dem Buch: UNIX von M.Banahan / A.Rutter Hanser-Verlag Muenchen, Wien 1984 1. Einfuehrung -------------- Dies ist kein leicht zu verfassendes Kapitel. C in einem UNIX- Buch unberuecksichtigt zu lassen, ist undenkbar, aber die Sprache in einem einzigen Kapitel erschoepfend zu beschreiben ist unmoeg- lich. Aus diesem Grunde koennen wir niemand vollkommen zufrieden- stellen. Da wir das akzeptieren, wollen wir uns auch kein allzu grosses Kopfzerbrechen darueber machen. Dieses Kapitel versucht, in einem minimalen Umfang Lehrstoff zu vermitteln, den wir als nuetzlichen Ueberblick fuer den nicht staendig mit C konfrontierten Benutzer betrachten. Es ist sicher kein Ersatz fuer eine ausfuehrliche Beschreibung (entsprechende Literaturverweise finden Sie am Ende des Buches), noch erhebt es den Anspruch, mehr als die wichtigsten Grundlagen zu erklaeren. Die Reihenfolge der Sprachelemente wurde mit Absicht gewaehlt: wir wollen uns darin von anderen Buechern ueber C unterscheiden, damit jemand, der gleich alles liest, nicht an Langeweile stirbt. 2. Einleitende Bemerkungen -------------------------- Fuer eine grosse Zahl der UNIX-Programmierer sind C und UNIX untrennbar. Wenn sie ueber das eine sprechen, so glauben sie erstaunlicherweise, das andere mit einbezogen zu haben. Das Be- triebssystem ist fast vollstaendig in C geschrieben, und der zur Erreichung der Portabilitaet von UNIX notwendige Arbeitsaufwand zeigt, dass C tatsaechlich eine der portabelsten Sprachen ist, in der Sie Ihre Programme schreiben koennen - insbesondere, wenn Sie dabei UNIX als Zielsystem im Auge haben. Ein auf einem UNIX- System entwickeltes Programm duerfte ohne Aenderungen auch auf einem anderen UNIX-System laufen, sofern Sie nicht tatsaechlich versuchen, es nichtportabel zu machen. Selbst wenn Sie dies tun, so gibt es ein Dienstprogramm (genannt lint(1)) zur Ueberpruefung Iherer Unternehmungen und zum Suchen von Fehlern. Dies soll nicht heissen, dass wir zu der Ansicht neigen, C sei die beste Programmiersprache der Welt, oder zu aehnlichem Unsinn. Solange es Narren gibt, werden sie in unqualifizierte Auseinan- dersetzungen ueber die 'beste' Sprache ausbrechen, und wir haben nicht vor, in dieses Gerangel mit einbezogen zu werden. Dies waere ungefaehr so eintraeglich (und sinnvoll), wie sich ueber Geschmack oder Schoenheit zu streiten. Die Tatsache, dass C von zwei sehr faehigen Personen fuer die Entwicklung von UNIX verwen- det wurde, obwohl diese genuegend andere Sprachen zur Auswahl hatten, und dass so viele qualifizierte Software-Entwickler diese Sprache so sehr bevorzugen, besagt mehr ueber C, als es irgendein anderes Argument jemals koennte. 3. Starthilfen -------------- Lassen Sie uns nicht laenger herumtroedeln - wir haben sowieso kaum Zeit - sondern unverzueglich beginnen. Verwenden Sie den Editor, erzeugen Sie eine Datei mit Namen hallo.c und tragen Sie folgenden Text ein: main() { printf("Hallo Kumpan\n"); } Dann uebersetzen Sie diese mit dem Kommando cc hallo.c Wenn dabei keine Fehler auftraten, so meldet sich sofort wieder die Shell mit dem Prompt. Um das uebersetzte Programm (welches vom Ueberstzer immer a.out genannt wird) auszufuehren, geben Sie einfach seinen Namen an: a.out Die gesamte Ueberstzung und der Lauf wuerde folgendermassen aus- sehen: $ cc hallo.c $ a.out Hallo Kumpan $ Sofern Sie das uebersetzte Programm aufbewahren wollen, muessen Sie es umbenennen, bevor Sie irgendein anderes Programm ueber- setzen. Das Kommando mv dient diesem Zweck. Sie haben gerade ein aus einer Funktion mit einer einzigen Anwei- sung bestehendes Programm uebersetzt. Alle C-Programme enthalten mindestens eine Funktion (ansonsten waeren sie keine Programme). Gleichzeitig muss eine Funktion namens main existieren, da bei ihr das Programm gestartet wird. Ihres macht dabei keine Ausnah- me. In Ihrem Programm ruft die Funktion main eine weitere Funk- tion, printf, mit dem Argument "Hallo Kumpan\n" auf. Im Gegensatz zu writeln in Pascal oder write in Fortran enthaelt C keine Sprachelemente fuer die Ein- und Ausgabe. Die Funktion printf wird im allgemeinen zur Erzeugung einer Ausgabe in C- Programmen benutzt. Aber sie ist lediglich eine Routine, die irgendjemand fuer die Allgemeinheit geschrieben und in eine Bue- cherei mit nuetzlichen Funktionen gesteckt hat. Indem Sie sie in Ihrem Programm erwaehnen, bewirken Sie automatisch, dass printf aus der Buecherei hinzugefuegt wird. C verfuegt grundsaetzlich ueber keine zur Sprache selbst gehoerenden Funktionen. Glueckli- cherweise koennen wir davon ausgehen, dass in einem UNIX-System gewisse Funktionen in Buechereien zur Verfuegung stehen, und printf ist eine von ihnen. 4. Zeichenketten ---------------- Das Argument von prkntf war"ekng Zekcjenkette. Zeichenketten sind beliebig lange Folgen von Zeichen, die mit doppelten Anfuehrungs- zeichen "..." umgeben sind. In C wird eine solche Zeichenkette als ein Zeichenvektor behandelt. Diesem wird ans Ende das Zeichen \0 (das Nullzeichen, dessen Wert 0 ist) angefuegt, um das Ende erkennen zu koennen. Fuer einige schwer zugaengliche Zeichen wird eine spezielle Schreibweise, ein Fluchtsymbol \, gefolgt von einem normalen Zeichen, verwendet. \t Tabulatorzeichen \r Wagenruecklauf \n Zeilentrenner \b backspace \\ Fluchtsymbol \" Doppel-Anfuehrungszeichen ' Apostroph \f Seitenvorschub \0 Nullzeichen C-Programme bestehen ueblicherweise aus einer Ansammlung von Funktionen -wie in Fortran - ohne das Konzept von lokalen Funk- tionen, wie Sie sie in Pascal oder Algol finden koennen. Im Gegensatz zu solchen anderen Sprachen ist die Reihenfolge der Funktionen unwichtig (aber es muss eine namens main vorhanden sein!). Jetzt folgt ein weiteres Programm mit derselben Aufgabe wie das erste. Die Funktion main ruft nun die Funktion speak zur Erledigung der Hauptarbeit auf, und dies tut sie sogar zweimal. main() { speak(); speak(); } speak() { printf'"Hallo Kumpan\n"); } Die Anordnung dieser beiden Funktionen koennte vertauscht werden; es wuerde nichts ausmachen. Probieren Sie es aus, und staunen Sie. 5. Datentypen ------------- Um die Ausfuehrung lohnender Aufgaben ueberhaupt zu ermoeglichen, muessen Sie in der Lage sein, Daten, i.a. Zahlen oder Zeichen, manipulieren zu koennen. C unterstuetzt ein ganzes Sortiment von Datentypen und Mittel zum Strukturieren von Daten. Momentan ver- wenden wir die Typen char, int und double. Sie repraesentieren Zeichen, ganzzahlige Werte und doppelt genaue Gleitkomma-Werte. 6. Variablen und Kommentare --------------------------- Variablen muessen vor ihrer Verwendung vereinbart werden. Sofern sie ausserhalb von Funktionskoerpern vereinbart werden, sind sie global und allen Funktionen zur Verarbeitung zugaenglich. Lokale Variablen werden am Anfang einer Funktion vereinbart und sind nur innerhalb dieser verfuegbar. Dies gestattet die Verwendung des- selben Namens in verschiedenen Funktionen, ohne Konflikte entste- hen zu lassen. Hier folgt ein Programm zur Erstellung einer Tabelle der Zahlen von 1 bis 10 und der zugehoerigen Quadrate. Das Programm enthaelt Kommentare: in C ist alles innerhalb von /* und */ ein Kommentar. Kommentare duerfen ueberall dort eingefuegt werden, wo Leerzei- chen oder Zeilentrenner gestattet sind. /* * Ausgabe ganzer Zahlen und ihrer Quadrate */ main() { int i,isquared; /* Variablen-Vereinbarung */ for (i=1; i<=10; i=i+1) { isquared=i*i; printf( "%d %d\n",i,isquared); } /* Ende der Schleife */ } Dieses Beispiel verdient eine ausgiebigere Erlaeuterung. Direkt am Anfang der Funktion main befindet sich die Vereinbarung der benoetigten Variablen: zwei ganzzahlige Werte, naemlich i und isquared. Mehrere Variablen koennen in einer Definition verein- bart werden, indem man ihre Namen durch Kommas trennt. Namen bestehen aus den Buchstaben a - z, A - Z, den Ziffern 0 - 9 und dem Unterstrich _; dabei muss das erste Zeichen ein Buchstabe oder Unterstrich sein. Die Laenge der von Ihnen verwendeten Namen ist vom jeweiligen System abhaengig; finden Sie es deshalb selbst heraus. Waehlt man einen Namens zu lang, so bedeutet dies, dass nur der Anfang des Namens signifikant ist, der Rest wird igno- riert. Mindestens sechs Zeichen sind wohl immer signifikant. Der naechste Programmteil ist eine durch for kontrollierte Schleife. Sie hat eine dreifache Wirkung. Zuerst wird die Variab- le i auf 1 gesetzt; dann wird, solange wie i kleiner oder gleich 10 ist, der Anweisungsteil der Schleife ausgefuehrt; am Ende jedes Durchganges durch die Schleife wird 1 zu i addiert. Be- achten Sie genau die Positionen der Semikolons in dieser Anwei- sung. Um die Schleife etwas weniger gebraeuchlich zu formulieren, sagen wir, um mit der Schrittweite 5 von 100 herunter bis zu 10 zu gehen, wuerde die Anweisung folgendermassen aussehen: for (i=100; i>=10; i=i-5) Das duerfte wohl kaum eine Ueberraschung sein. Sie werden sich jetzt wohl ueber die geschweiften Klammern wun- dern, die in diesem Prozess auftauchen. Diese werden u.a. be- noetgt, um Anweisungen zusammenzubinden, d.h., um mehrere Anwei- sungen als eine darzustellen; sie bezwecken dasselbe wie begin und end in anderen Sprachen. In unserem Beispiel markieren die erste { und die letzte } den Anfang und das Ende der Funktion main. Das andere Paar fasst die Anweisungen innerhalb der Schlei- fe zusammen. Befaende sich nur eine Anweisung in der Schleife, so waere keine Klammerung erforderlich. In der Tat sind zwei Anweisungen im Inneren der Schleife unnoetig. Die Argumente einer Funktion wie printf duerfen Ausdruecke sein. Die angegebenen Argumente sind eine Zeichenkette "%d %d\n" eine Variable i eine Variable isquared aber isquared kann durch den Ausdruck i*i ersetzt werden. Hier ist das verbesserte Programm: main() { int i; for (i=1; i<=10; i=i+1) printf ( "%d %d\n",i,i*i); } printf ist eine Buechereifunktion und kein Element von C, aber ihre Verwendung ist so weit verbreitet, dass hier ein paar er- klaerende Worte noetig erscheinen.Eine genaue Behandlung findet man in dem Ueberblick der Buechereifunktioenen im Kapitel 9. printf wird eingesetzt, um die unterschiedlichsten Datentypen fuer eine formatierte Ausgabe aufzubereiten und als Standard- Ausgabe Ihres Programms auszugeben (normalerweise zum Terminal, sofern Sie nicht umgelenkt haben). Das erste Argument von printf ist eine Zeichenkette (genau gesagt, ein Zeichenvektor), welche printf Zeichen fuer Zeichen ausgibt. Das Zeichen % ist ein Signal fuer printf, dass eines der anderen Argumente zu formatieren und auszugeben ist. Im obigen Beispiel symbolisiert %d eine Dezimal- zahl und %f stuende fuer eine Gleitkommazahl. Jedes solche For- matelement veranlasst printf, das naechste Argument in der Argu- mentliste zu suchen und auszugeben. print ist aeusserst nuetzlich - wir verwenden hier lediglich einen kleinen Teil dieser Moeg- lichkeiten. 7. Inkrementieren und Dekrementieren ------------------------------------ Nun sieht unser Beispielprogramm fast so aus, wie es ein erfahre- ner C-Programmierer formuliert haette. Die letzte Aenderung bsteht darin, die Zuweisung i=i+1 durch eine Kurzform zu erset- zen. Dabei sollten Sie beachten, dass in C das Symbol fuer eine Zuweisung einfach = ist, nicht etwa :=. Man muss so oft eine Variable um den Wert 1 erhoehen, dass C dafuer eine spezielle Schreibweise besitzt: i++. Analog wird durch i-- erreicht, dass i um den Wert 1 verringert wird. Hier ist die endgueltige Version dieses Programms: main() { int i; for (i=1; i<=10; i++) printf ("%d %d\n",i,i*i); } Testen Sie es. Die soeben vorgstellten Inkrement- und Dekrement-Operatoren koennen auf zwei verschiedene Arten eingesetzt werden. In der Zuweisung a = i++; enthaelt die Variable a den urspruenglichen Wert von i, und i wird anschliessend inkrementiert. Die Alternative dazu ist a = ++i; wobei i zuerst inkrementiert und dann an a der neue Wert von i zugewiesen wird. Die Dekrement-Operation funktioniert nach dem- selben Prinzip. Sofern das Resultat nicht verwendet wird, sind ++i und i++ gleichbedeutend, genauso wie es --i und i-- sind. Der letzte Teil unserer for-Anweisung koennte beide Versionen der Inkrementierung mit genau demselben Effekt enthalten. 8. Kontrollstrukturen --------------------- C bietet viele Mittel und Wege an, um Sie den Ablauf Ihres Pro- gramms kontrollieren zu lassen. Alle derartigen Anweisungen te- sten logische Werte, die entweder gleich oder ungleich Null sind. C interpretiert Null als den logischen Wert 'falsch' und einen von Null verschiedenen Wert als 'wahr'. In den folgenden Abschnitten finden Sie oft den Begriff 'Anwei- sung'. Die exakte Definition einer Anweisung bedarf grosser Sorg- falt; Sie finden diese in der Literatur. Das Hauptproblem, das sich vor allem dem Anfaenger stellt, ist die Verwendung von Semikolons und geschweiften Klammern. Wir moechten Ihnen keinen informativen Ueberblick geben. Ein solcher koennte keinesfalls vollstaendig sein und wuerde nur Verwirrung bei Ihnen zur Folge haben. Stattdessen werden wir Ihnen lauffaehige Programmbeipsiele vorsetzen und Sie Ihre eigenen Schluesse ziehen lassen. Wenn Ihnen dabei irgendein Zweifel kommt, dann besteht die einzig richtige Loesung darin, nachzuschauen, was die Sprachbeschreibung aussagt. Bedauerlicherweise liest sich dagegen James Joyce unge- faehr so leicht wie ein Kinderbuch. 8.1 Die "for"-Anweisung ----------------------- Sie haben sie bereits kennengelernt, aber hier ist sie nochmals: for (Ausdruck_1; Ausdruck_2; Ausdruck_3) Anweisung Als erstes wird Ausdruck_1 (einmal!) bewertet. Dann wird, solange wie Ausdruck_2 ungleich Null ist, der Anweisungsteil, gefolgt von Ausdruck_3, ausgefuehrt. Ausdruck_1 dient meist zum Initialisie- ren des Schleifenzaehlers, Ausdruck_2 als Abbruchkriterium fuer die Schleife und Ausdruck_3 zur Manipulation des Schleifen- zaehlers. Dies haben wir bereits in frueheren Beispielen gesehen. Falls der erste oder dritte Ausdruck, oder auch beide, fehlen, so bedeutet dies, dass an der entprechenden Stelle nichts ausge- fuehrt werden soll. Ein fehlender zweiter Ausdruck wird jedoch als eine stets 'wahre' Bedingung interpretiert, und es entsteht eine Endlos-Schleife, wenn nicht irgendwelche besonderen Schritte zum Ausstieg aus dieser eingeleitet werden. Testen Sie einmal die folgende Endlos-Schleife (aber seien Sie sicher, dass Sie sich mit der 'interrupt'-Taste auskennen): main() { int i; for (i=1; /* kein zweiter Ausdruck */; i++) printf ("i = %d\n",i); } Innerhalb der for-Anweisung sind die Semikolons besonders wichtig. Vergleichen Sie obiges Beispiel mit diesem endlos laufenden Fragment: for (;;) 8.2 Die "while"-Anweisung ------------------------- Die while-Anweisung ist vergleichbar mit einer for-Anweisung, jedoch ohne den ersten und letzten Ausdruck. Ihre Formulierung lautet: while (Ausdruck) Anweisung Der Anweisungsteil wird ausgefuehrt, solange der Ausdrcuk un- gleich Null ist. Eine for-Anweisung kann auf eine while-Anweisung zurueckgefuehrt werden. Die folgenden zwei Programmstuecke haben den gleichen (sinnlosen) Effekt: for (i=1; i<=10; i++) { a=a+i; b=a*a; } i=1; /* dasselbe mit einer while-Schleife */ while (i<=10) { a=a+i; b=a*a; i++; } Die while-Anweisung prueft grundsaetzlich, ob die Schleife ueber- haupt ausgefuehrt werden soll. In einigen wenigen Faellen waere es guenstiger, mindestens einen Schleifendurchlauf auszufuehren - die naechste Anweisung, die wir betrachten, wird Ihnen dies ermoeglichen. 8.3 Die "do"-Anweisung ---------------------- Um wenigstens eine einmalige Ausfuehrung einer Schleife zu bewir- ken, wird die do-Anweisung verwendet: do Anweisung while (Ausdruck): Achten Sie dabei besonders auf die Semikolons. Hier folgt ein weiteres Programmfragment, welches eine do-Anweisung enthaelt. i=5; do { a=a+i; i=i*2; } while (--i); /* Abbruch, wenn i Null erreicht */ Die geschweiften Klammern entfallen, wenn nur eine einfache An- weisung in der Schleife steht. i=7; do a=a+i; while (--i); /+ Abbruch, wenn Null erreicht */ 8.4. Die "continue" und "break"-Anweisungen ------------------------------------------- Um Ihnen besondere Aktionsmoeglichkeiten innerhalb von Schleifen zu gestatten, existieren zwei zusaetzliche Anweisungen. Mit continue springt man zum Ende einer Schleife und umgeht somit alle sonst auszufuehrenden Anweisungen. Durch break verlaesst man eine Schleife unverzueglich. Bevor Sie jedoch etwas ueber die if- Anweisung gelernt haben, sind Ihnen diesen beiden Anweisungen nicht besonders dienlich - ein unbedingtes break oder continue innerhalb einer Schleife bringt niemandem Vorteile. Einige Anwen- dungsbeispiele finden Sie noch weiter hinten im Text. 8.5 Die "if"-Anweisung ---------------------- Diese Anweisung muesste jedem, der moderne Programmiersprachen verwendet, vertraut sein. In C sieht sie folgendermassen aus: if (Ausdruck) Anweisung else Anweisung Wie ueblich ist der else-Teil optional. Verwenden Sie else, so bezieht es sich auf das letzte davorliegende if ohne else-Teil, ebenfalls ein gebraeuchliches Verfahren. Vermutlich sind Beispie- le die besten Lehrmeister. if (a<b) /* wenn a kleiner b dann */ b=0; /* setze b auf Null */ if (a>b) /* wenn a groesser b, dann */ { i++; /* zu i eins addieren */ b=0; /* b auf Null setzen */ } else c++; /* sonst 1 zu c addieren */ if (a+b<c) /* drittes 'if' */ if (a>c) /* viertes 'if' */ b++; else /* gehoert zum vierten 'if' */ c++; else /* gehoert zum dritten 'if' */ { a=c; b=0; } Und jetzt ein wirkliches Programm: es gibt die Zeichenfolge 12345789 (ohne die 6) aus und zeigt die ungeschickte Verwendung einer continue-Anweisung. Im Normalfall wuerde man den Test auf Gleichheit ins Gegenteil umwandeln und auf die continue-Anweisung verzichten. Bedauerlicherweise ist zu diesem Zeitpunkt eine gute Veranschaulichung von continue ziemlich schwierig. main() { int i; for (i=1; i<=10; i++) { if (i==6) /* teste Gleichheit */ continue; printf ("%d",i); } } Die continue-Anweisung wird ausgefuehrt, wenn i gleich 6 ist (dies drueckt == aus) und bewirkt einen Sprung zum Fortsetzungs- punkt der Schleife, wodurch printf uebersprungen wird. Da sich mehr als eine Anweisung innerhalb der Schleife befindet, werden die geschweiften Klammern benoetigt. Weil jedoch die =if-else- Kombination als eine einzige Anweisung betrachtet wird, loest das naechste Beispiel dieselbe Aufgabe ohne die zusaetzlichen ge- schweiften Klammern. main() { int i; for (i=1; i<=10; i++) if (i==6) continue; else printf ("%d",i); } Ein Ersatz von continue durch break haette an dieser Stelle einen Abbruch der Schleife zur Folge, da diese mittels break voellig verlassen wuerde. 8.6. Die "switch"-Anweisung --------------------------- Was in den meisten Programmiersprachen unter der Bezeichnung case-Anweisung laeuft, zieht C vor, mit switch zu bezeichnen (dies ist nicht unvernuenftig - es gibt Unterschiede zur case- Anweisung etwa in Pascal). Dies Anweisung dient dazu, abhaengig vom Wert eines Ausdrucks eine mehrfach verzweigte Auswahl zu treffen. Um beispielsweise herauszufinden, ob ein Ausdruck einen ungeradzahligen Wert zwischen 1 und 10 annimmmt oder einen gerad- zahligen im selben Bereich oder ganz aus diesem Bereich heraus- faellt, koennen Sie so vorgehen: switch (a+b) { case 2: case 4: case 6: case 8: case 10: printf ("gerade\n"); break; case 1: case 3: case 5: case 7: case 9: printf ("ungerade\n"); break; default: printf ("ausserhalb\n"); } Nach switch steht ein Ausdruck in Klammern (die Klammern muessen hier angegeben werden). Mit dem Wert des Ausdrucks wird einer der case-Werte ausgewaehlt. Der Programmablauf wird dann bei der Anweisung fortgesetzt, die dem ausgewaehlten case folgt. Von dort verlaeuft die Ausfuehrung wie gewohnt, und wenn Sie nicht wollen, dass alle danachfolgenden Faelle ebenso zur Ausfuehrung kommen, muessen Sie notwendigerweise die break-Anweisung benutzen; im Beispiel ist das verdeutlicht. Durch die break-Anweisung wird das Programm gezwungen, die switch-Anweisung ganz zu verlassen. Da- nach geht die Ausfuehrung hinter der schliessenden geschweiften Klammer weiter. Wenn das Beispiel keine break-Anweisungen enthielte und der Aus- druck ein ganzzahliges Ergebnis zwischen 2 und 10 haette, so wuerden der Reihe nach die Meldungen gerade, ungerade und ausser- halb ausgegeben. Was wuerden wohl im Bereich liegende ungerade Zahlen bewirken? Die case-Werte innerhalb jeder switch-Anweisung muessen ver- schieden sein. Zweimal case 1: waere im vorigen Beispiel unzulaessig. Der Wert bei case muss ausserdem konstant und ganzzahlig sein. default bedeutet, dass die danach folgenden Anweisungen ausgewaehlt werden sollen, falls der fuer die switch-Anweisung bewertete Ausdruck einen Wert anninmmt, der nicht als case definiert wurde. Wird keiner der definierten Werte erreicht, und ist default nicht vorhanden, so wird keine Anweisung innerhalb der switch-Anweisung ausgefuehrt. 9. Ausdruecke ------------- Wenn wir sie bisher auch praktisch ignorierten, so koennen wir dennoch nicht mehr laenger auf sie verzichten. 9.1. Vergleiche --------------- Als Beispiel sei a < b angefuehrt. Dieser Ausdruck hat wirklich einen Wert (sofern Sie ihn benoetigen): 0 wenn der Vergleich falsch ist, 1 wenn er zutrifft. Die Vergleichsoperatoren sind < kleiner als > groesser als <= kleiner oder gleich >= goesser oder gleich Sie haben Vorrang (werden also frueher bewertet) vor den Operato- ren == gleich != ungleich Wenn Sie Beispiele dafuer brauchen, betrachten Sie nochmals die Kontrollstrukturen im vorigen Abschnitt. 9.2 Bitmanipulationen --------------------- C gestattet Ihnen, ganzzahlige Werte wie Bitmuster zu behandeln. Sie duerfen folgende Operationen auf diese Groessen anwenden: &> und | oder ^ exklusiv oder << nach links verschieben >> nach rechts verschieben ~ (Tilde) Bit-Komplement Anwendungsbeispiele dafuer sind: a = b &> 7; /* letzte 3 Bits auswaehlen */ a = b | 7; /* letzte 3 Bits setzen */ a = b ^ 1; /* letztes Bit komplementieren */ a = ~b; /* Komplement */ a = b << 3: /* um 3 Bits nach links */ a = b >>3; /* bzw. nach rechts schieben */ Seien Sie vorsichtig mit der Verschiebung nach rechts. Was dabei als hoechstwertiges Bits nachgeschoben wird, haengt stark von Ihrem Rechner ab, und Sie muessen eventuell eine 'Und'- oder 'Oder'-Verknuepfung hinzufuegen, um das gewuenschte Resultat zu erhalten. 9.3 Logische Verknuepfungen --------------------------- Um interessante logische Ausdruecke zu bilden, koennen Sie die Verknuepfungen 'Und' (&>&>), 'Oder' (||) und 'Negation' (!) verwen- den. 'Und' liefert als Resultat 1, wenn beide Operatoen von Null verschieden sind, und 0 sonst. 'Oder' verhaelt sich wie 'Und', liefert aber - wie erstaunlich - auch 1, wenn nur ein Operand nicht Null ist. Die Negation liefert 0 fuer einen von 0 verschie- denen Operanden und 1 fuer Null als Operanden. Rechnen Sie mit seltsamens Ergebnissen, wenn Sie den Editor zum Veraendern dieser Ausdruecke verwenden. Das Zeichen &> ist fuer den Editor ein Sonderzeichen. Hier geben wir Ihnen einige Beispiele fuer logische Verknuepfungen: if (a<b &>&> b<c) /* wenn a<b UND b<c */ if (a<b || b<c) /* wenn a<b ODER b<c */ if (!b) /* wenn nicht b (wahr wenn b Null ist) */ Die Negation findet haeufig beim Testen von Flaggen Verwendung; denken Sie etwa an eine Variable namens aus, welche den Wert 'wahr' hat, wenn etwas aus ist, und den Wert 'falsch', wenn das nicht zutrifft. Die Anweisung if (!aus) (in Worten 'if nicht aus') erweist sich in praktischen Anwendun- gen oft als sinnvoll. 10. Zuweisungen --------------- Manch Leute sollen ja Zuwendungen bekommen - fuer einen Rechner sind Zuweisungen fast so gut! 10.1 Zuweisungen als Anweisungen -------------------------------- Die einfachste Form ist eine direkte Zuweisung var = Ausdruck; wobei eine Variable den Wert irgendeines Ausdrucks erhaelt. Der Ausdruck darf aus einer beliebigen Kombination von zulaessigen Werten und Operationen gebildet werden; Vergleiche, Bitperationen und logische Verknuepfungen sind erlaubt, da sie alle numerische Resultate liefern. Wir haben die arithmetischen Operatoren bisher stillschweigend vorausgesetzt, in der Hoffnung, dass Sie deren Bedeutung ohne besondere Erklaerung verstehen wuerden. Falls dies nicht der Fall ist oder Sie sich vergewissern wollen, hier sind sie kurz aufge- listet: + Addition - Subtraktion * Multiplikation / Division % Rest nach Division % ist vielleicht neu fuer Sie. Der Operator wird genau wie die Division verwendet: x % y liefert aber als Resultat den Rest anstelle des Quotienten; % kann nur auf ganzzahlige Argumente angewendet werden. Einige zulaessige Zuweisungen folgen hier nun als Beispiele. a = b; a = b + c; a = (b + c) / d; a = e > f; a = (e > f &>&> c < d) + 1; a = a << 3; Es ist durchaus vernuenftig, das Ergebnis eines Vergleichs einer Variablen zuzuweisen; dieses Ergebnis ist schliesslich auch nur eine Zahl. Das letzte Beispiel ist besonders interessant. Es verschiebt den Wert von a um 3 Stellen (Bits) nach links und weist das Ergebnis wiederum an a zu (numerisch entspricht dies einer Multiplikation mit 8). Die Faelle, in denen das Ziel einer Zuweisung auch im Ausdruck auftaucht, unterstuetzt C durch eine besondere Form der Zuweisungsoperatoren. Wir koennen daher a = a <<3; in Form von a << = 3; umschreiben, was nicht unbedingt als Vorteil erscheinen muss. Lohnend ist dies dann, wenn die linke Seite selbst aus einem komplizierten Ausdruck, etwa einem verschachtelten Vektorzugriff, besteht. Muessen Sie den Ausdruck naemlich zweimal schreiben, so steigen die Chancen fuer Fehler gewaltig. Weitere Gelegenheiten, bei denen diese Schreibweise angenehm ist, bieten sich z.B. bei Anwendungen wie: a + = 2; /* zwei zu 'a' addieren */ Dies liest sich bedeutend besser als die anderen Versionen, die Sie bereits bewundern konnten. Algol-68-Anwender sind mit dieser Idee ohnehin schon vertraut. Hier ist eine Liste aller Zuwei- sungsoperatoren, die dieses Vorgehen unterstuetzen: += -= *= /= %= <<= >>= &>= ^= |= Die Verwendung dieser Zuweisungsoperatorn anstelle der laengeren Form hat zwei wichtige Nebeneffekte. Erstens muss die linke Seite nur einmal bewertet werden; dies ist wichtig, da ja Vektorzugrif- fe oder Zeiger, auf die wiederum Inkrement- oder Dekrement-Opera- toren anwendbar sind, in ihr enthalten sein koennen. So etwas doppelt zu bewerten, wuerde nie das Gewuenschte liefern. Zweitens ist es ein sehr nuetzlicher Fingerzeig fuer den Uebersetzer, der ihm auf die Spruenge hilft, effizienteren Code zu erzeugen. 10.2 Werte von Zuweisungen -------------------------- Zuweisungen liefern immer einen Wert und koennen wiederum in Ausdruecken verwendet werden. Iher Wert ist der zugewiesene Wert. Dies ist in manchen Faellen aeussert nuetzlich, um dem Program- mierer das Erstellen von kurzen und klaren Programmen zu erleich- tern, die einem beim Lesen Freude bereiten. Ebenso leicht ist es jedoch, diese Eigenschaft ins Gegenteil zu verdrehen, so dass hoechst unlesbare Programme entstehen. Wenn wir sie spaeter an- wenden, werden wir versuchen, die Stellen hervorzuheben, die wir dafuer als lohnend erachten. /* * Ein Beipiel fuer Wertzuweisungen. * Diese Stueck Code hat denselben * Effekt wie das naechste */ a = (b = c + d) / (e = f + g); /* naechstes Stueck */ b = c + d; e = f + g; a = b / e; /* wir bevorzugen das zweite Beispiel */ 10.3 Konstanten --------------- Konstanten haben dieselbe Gestalt wie auch in den meisten anderen Programmiersprachen. Ganzzahlige Konstanten werden geschrieben als 1, 2, 3 usw.; Gleitkomma-Konstanten als 1.0, 2.0, 3.0 oder in der wissenschaftlichen Notation als 1e6. Fuer Zeichen gibt es eine besondere Klasse von Konstanten mit der Schreibweise 'a'. Dies entspricht dem ganzzahligen Wert, der intern den Buchstaben a als Zahlenwert repraesentiert. Manchmal werden Sie ueber Oktal- zahlen (Basis 8) stolpern; ausser dass sie mit einer Null begin- nen, sehen sie wie ganz normale ganzzahlige Konstanten aus: 011 entspricht der dezimalen 9. Derselbe Sachverhalt gilt ebenfalls fuer Zeichen: '\011' ist eine andere Repraesentierung (im ASCII- Zeichensatz) fuer das Tabulatorzeichen (schauen Sie nach). Im 'ovp'-Programm im Anhang F koenen Sie die Verwendung von Zeichen als Konstanten sehen. 11. Datenstrukturen ------------------- C bietet eine sehr gute Moeglichkeit zum Strukturieren von Daten, und wenn Sie sich erst einmal an diese gewoehnt haben, werden sie Ihnen unentbehrlich. Wir moechten zwei von ihnen naeher betrach- ten, Vektoren und Strukturen. 11.1 Vektoren ------------- Jeder Programmierer, der sich einer hoeheren Programmiersprache bedient, muss zwangslaeufig ueber Vektoren stolpern. In C verein- bart man mit der Deklaration int xyz[100]; einen Vektor namens xyz mit 100 ganzzahligen Elementen, die mittels xyz[0], xyz[1], bis hin zu xyz[99] zugaenglich sind. Indexwerte reichen also von Null bis zur um eines verminderten Vektorgroesse - dies ist eine der Hauptursachen fuer Fehler bei Neulingen in der C-Programmierung. Die Gruende fuer dieses unge- woehnliche Numerierungsschema werden deutlich, wenn Sie spaeter einmal versuchen, aus der Sicht von C zu durchschauen, was hinter der Bezeichnung Vektor wirklich steckt. Vorlaeufig muessen Sie sich einfach daran gewoehnen. Aber sein Sie aeusserst vorsichtig, denn die derzeit zur Verfuegung stehenden C-Uebersetzer machen sich nicht die Muehe, Ihre Zugriffe auf Vektoren zu ueberpruefen. Mehrdimensionale Vektoren werden als Vektoren von Vektoren behan- delt und folgendermassen deklariert: int xyz[3][4][5]; Dies vereinbart 3 Vektoren, bestehend aus jeweils 4 Vektoren, die ihrerseits je 5 ganzzahlige Werte enthalten. (Diese feine Unter- scheidung ist selten von Bedeutung, ausser Sie versuchen sich an cleveren Tricks mit Zeigern (s.uebernaechster Abschnitt), da der Zugriff auf Vektoren mittels Indizes dies eruebrigt.) Wenn xyz wie oben deklariert wurde, so ist der Ausdruck xyz[1][2][3] += 2; /* addiere zwei zu dem Feldelement */ ebenfalls legal und xyz[1][2][3] = xyz[1][2][3] + 2; vorzuziehen, denn er ist leichter zu schreiben, wahrscheinlich effizienter und aussagekraeftiger. 11.2 Strukturen --------------- Nicht immer sind Vektoren die ideale Loesung fuer ein bestimmtes Problem. Oftmals gibt es reizvollere Wege fuer die Anordnung Ihrer Daten, wie etwa in einem 'record' in Pascal, obgleich die Fortran abtruennig Gewordenen oft gar nicht bemerken, was sie vermissen. C bietet sowohl 'Strukturen', welche wir hier zeigen, als auch 'Varianten' ('unions'). Beide entsprechen weitgehend den 'records' und 'case-variant-records' in Pascal. In diesem Kapitel wollen wir uns jedoch nicht mit Varianten auseinandersetzen. Die Deklaration struct xyz { int aaa; char bbb; double ccc; } abc; vereinbart eine Struktur-Variable namens abc. Die Struktur ent- haelt drei 'Komponenten', einen ganzzahligen Wert namens aaa, ein Zeichen namens bbb und einen Gleitkomma-Wert namens ccc. Auf die Komponenten der Struktur kann mit abc.aaa, abc.bbb und abc.ccc einzeln zugegriffen werden. In der Deklaration wird xyz als der 'Strukturname' bezeichnet. Damit wird der Name der Struktur fuer eine nochmalige spaetere Verwendung vereinbart. Unter der Annahme, dass die vorige Dekla- ration bereits vorliegt, vereinbart struct xyz s1,s2,s3; drei Strukturen (s1,s2,s3) von demselben Typ wie abc. Ausserdem duerfen Sie auch Vektoren aus Strukturen, Strukturen mit Vektoren und aehnliches konstruieren. Wenn Sie ueberhaupt von 'records' oder Strukturen gehoert haben - nennen Sie sie wie Sie wollen - werden Sie zwangslaeufig nach 'Zeigern' fragen. C besitzt selbstverstaendlich Zeiger, sie sind eines der wesentlichsten Sprachelemente. Es wird ziemlich schwer sein, ein C-Programm zu finden, welches keine Zeiger verwendet; sie sind meistens unentbehrlich. Fuehren wir sie uns einmal zu Gemuete! 12. Zeiger ---------- Wenn Sie noch niemals zuvor Zeiger verwendet haben, so steht Ihnen ein hartes Stueck Arbeit in Haus. Zeiger zu erklaeren, ist nicht leicht, und ihre Anwendung zu verdeutlichen, ist ebenfalls schwierig, obwohl das Prinzip ganz einfach ist. Bitte bemuehen Sie sich darum - Zeiger sind sehr verbreitet in C-Programmen, und es spricht nicht fuer einen Programmierer, wenn er nicht imstande ist, sie zu benutzen. Zeiger werden in C vereinbart, indem man angibt, auf was fuer einen Objekt-Typ sie zeigen. int obj, *p; Hier werden zwei Dinge vereinbart - ein ganzzahliger Wert mit Namen obj und ein Zeiger namens p. Der Name des Zeigers ist nicht *p, sondern der * signalisiert, dass es sich um einen Zeiger handelt. Die Bezeichnung int innerhalb der Deklaration besagt, dass p als Zeiger auf ganzzahlige Werte verwendet werden soll. Bevor Sie einen Zeiger ueberhaupt verwerten koennen, muessen Sie dafuer sorgen, dass er auf etwas zeigt. Lassen wir ihn also auf den ganzzahligen Wert namens obj zeigen, indem wir den Operator &> davorsetzen, was soviel wie 'die Adresse von' bedeutet: p = &>obj; Diese Zuweisung uebergibt die Adresse von obj an den Zeiger p; von nun an zeigt p auf obj. Um wiederum das Objekt, auf welches der Zeiger zeigt, anzusprechen, bedient man sich der Schreibweise *p, wobei p ein Zeiger sein muss. Beispielsweise bewirkt p = &>obj; *p = 2; genau dasselbe wie obj = 2; 12.1 Die Verwendung von Zeigern ------------------------------- Jemandem die Verwendung von Zeigern rein verbal beizubringen, ist ebenso erfolgreich, wie jemanden radfahren zu lehren, indem man ihm etwas darueber erzaehlt. Beispiele waeren hilfreich, aber sie wuerden den Rahmen dieses Buches sprengen. Der beste Rat, den wir geben koennen, ist, weitere Literatur und jedes Beispielprogramm, welches Sie in die Finger bekommen, zu studieren. Einige Hinweise koennnten Ihnen zum besseren Verstaendnis von Beispielen hilfreich sein. Zeiger koennen zur Effizienz eines Programmes beitragen, insbesondere bei aufeinanderfolgenden Zu- griffen auf Vektoren oder Strukturen. Das Kopieren eines Vektors ist eine ziemlich weit verbreitete Operation: int a[100], b[100], i; for (i=0; i<100; i++) a[i] = b[i]; Dies kann umgeschrieben werden zu int a[100], b[100], *ap, *bp; ap=a; bp=b; while (ap < &>a[100]) *ap++ = *bp++; Im zweiten Beispiel sind die Zuweisungen ap = a und bp = b interessant. Sie werden vermutlich erwartetet haben zu sehen, wie die Adresse des ersten Vektorelements zugewiesen wird. Tatsaech- lich ist dies auch der Fall. Der Name eines Vektors entspricht naemlich der Adresse seines ersten Elements, und folglich bedeu- ten a und &>a[0] dasselbe. Anschliessend an die Initialisierung der Zeiger wird die Schleife solange ausgefuehrt, bis einer der Zeiger den Vektor verlaesst, auf den er zeigt. (&>a[100] ist kein Element von a mehr). Beide Zeiger werden automatisch erhoeht. Wieso wird durch *ap++ nicht das Objekt, auf welches ap zeigt, veraendert? Weil der Inkremment-Operator einen hoeheren Vorrang als der indirekte Zugriff besitzt. Im Gegensatz dazu inkremen- tiert (*ap)++ das Objekt, auf welches gezeigt wird. In diesem Fall wuerde jedoch die while-Schleife nicht mehr enden, und an (*ap)++ kann man auch nicht zuweisen. Nun sieht zugegebenermassen die Zeigerversion nicht nach einer Verbesserung gegenueber der Vektorversion aus. Dennoch besteht eine gute Chance, das sie effizienter ist, besonders, wenn die Objekte, auf die gezeigt wird, relativ gross sind (wie etwa double-Variablen). Ausserdem waere sie ebenfalls besser, wenn die Objekte, auf die gezeigt wird, grosse Strukturen sind. Manche C-Uebersetzer unterstuetzen allerdings die Zuweisung kompletter Strukturen noch nicht, obgleich dies die Ubersetzer bei Standard- UNIX tun. Wenn Sie einem der aelteren Uebersetzer in die Haende fallen, so muessen Sie die Zuweisung fuer jede Komponente einzeln durchfuehren.