C-Kurs - Teil 2

1 Ein Beispiel zum Aufwärmen

Nachdem wir beim letzten Mal einige Kontrollstrukturen ein wenig „beschnüffelt“ haben, will ich Ihnen in dieser Folge die restlichen zwei auch noch zeigen. Ja, Sie haben richtig gelesen. Es sind nur noch zwei. Das zeigt wieder die strukturelle Einfachheit von C. Das Vokabular ist ziemlich knapp und schnell zu erlernen, aber für die Feinheiten braucht man doch schon eine ganze Weile.

Die beiden Kontrollstrukturen, die noch ausstehen, sind die for-Schleife und die Mehrfachverzweigung (switch). Gleichzeitig werde ich, wie in der letzten Folge versprochen, den bedingten Ausdrücken etwas auf den Leib rücken.

Zum Aufwärmen ein kleines Beispiel, an dem der Stoff der letzten Folge kurz wiederholt werden soll, und das außerdem zeigt, wie man in C bereits mit Dateien arbeiten kann, ohne auch nur irgendetwas über das C File-Handling wissen zu müssen.

Wir stellen uns folgende Aufgabe: Zu schreiben ist ein Programm CAT — in Anlehnung an das gleichnamige UNIX Dienstprogramm -, das beliebige Dateien wahlweise auf den Bildschirm, auf den Drucker zur seriellen Schnittstelle oder auf andere Dateien ausgeben kann. Außerdem soll man damit einfach durch Tippen auf der Tastatur eine Datei mit Text füllen können. Puhhh, werden Sie jetzt sagen, der Autor dieses Kurses ist wohl verrückt geworden, das ist doch erst der zweite Teil und dann so ein Monstrum von Programm. Aber keine Angst, ich bin noch normal und das Ganze ist halb so wild. Werfen Sie doch schon einmal einen Blick auf Beispiel 2.1. Genau sechs Zeilen Programm. Sie werden mir wohl nicht glauben, daß diese sechs Zeilen genau das leisten, was in der Aufgabenstellung verlangt ist.

Um zu erklären, wie das funktioniert, müssen wir uns die Standard Ein/Aus-gabe von C etwas näher betrachten. Das Konzept der Standard Ein/Ausga-be stammt - wie fast alle schönen Features des ATARI TOS - ursprünglich von UNIX. Es erlaubt, das ganze Hantieren mit Dateien und physikalischen Ein/Ausgabegeräten auf das Betriebssystem abzuschieben, dem Programmierer und Anwender bietet sich jedesmal die gleiche Schnittstelle dar, egal wo er Daten holen oder wo er welche hinschicken will.

Das C Laufzeitsystem sorgt dafür, daß vor Eintritt in die Funktion main() drei Dateiverbindungen angelegt werden, die da wären:

Die Standardeingabe liegt normal auf der Tastatur, das heißt, wenn ein Programm von der Standardeingabe liest, erhält es die Zeichen geliefert, die Sie auf der Tastatur eingeben. Die Standardausgabe zielt auf den Bildschirm. Wenn Ihr Programm also auf die Standardausgabe schreibt — wie es unser Beispiel in der letzten Folge mit der Funktion printf() bereits getan hat -kommt das Ergebnis auf den Bildschirm. Auch die Standardfehlerausgabe schreibt auf den Bildschirm. Wozu sie da ist, werden wir gleich noch sehen.

Falls das alles wäre, könnte man vor Langeweile natürlich nur gähnen. Aber es gibt da natürlich einen „Trick“. Sie können nämlich beim Aufruf des Programms die Standarddateiverbindungen „umlenken“. Das Programm selbst merkt davon überhaupt nichts. Es glaubt, immer noch von der Tastatur zu lesen, dabei kommt die Eingabe vielleicht über ein Modem von einem anderen Rechner, der sonstwo stehen kann. Wie man stdin und stdout umlenken kann, sehen Sie in Tabelle 2.1.

  1. cat
  2. cat < datei.txt
  3. cat > datei.txt
  4. cat < infile.txt > outfile.txt
  5. cat < listing.lis > Ist:
  6. cat <aux: > receive.txt

Tabelle 2.1

In Fall 1) kommt alles, was Sie tippen, auf dem Bildschirm an. Fall 2) gibt die Datei datei.txt auf dem Bildschirm aus. Bei 3) wird alles, was Sie tippen, auf die Datei datei.txt geschrieben. Fall 4) kopiert die Datei infile.txt auf die Datei outfile.txt. Der Aufruf 5) druckt li-sting.lis auf dem Drucker aus, und schließlich 6) empfängt eine Datei über die serielle Schnittstelle und legt sie in receive.txt ab. Zwischen den Umlenkungszeichen (>,<) und der nachfolgenden Dateispezifikation darf kein Zwischenraum stehen.

Jetzt wird es aber Zeit, daß wir uns das Ganze einmal von der Seite des Programmierers ansehen. (Beispiel 2.1) Vielleicht ist Ihnen aufgefallen, daß es von Beispiel 2.1 zwei Versionen gibt. Das hat seinen Grund. Version 1 ist die instruktivere, an der man zeigen kann, was für elegante Formulierungen in C möglich sind. Sie hat bloß einen kleinen Schönheitsfehler: Sie funktioniert auf dem Atari nicht. Das liegt daran, daß die Funktion getchar() beim ATARI keinen EOF (End Of File) Status liefert und man sich deshalb mit der Funktion feof() behelfen muß. Es könnte aber durchaus sein, daß bei Ihrem C die erste Version funktioniert. Probieren Sie es am besten aus -oder professioneller: Lesen Sie sich die Dokumentation zur Funktion getchar() durch.

Zuerst wird ein Headerfile namens stdio.h eingelesen. In ihm sind alle Definitionen und Vereinbarungen enthalten, die zum Arbeiten mit der Standard Ein/Ausgabe notwendig sind. Wenn Sie mal aus Neugierde in die Datei reinschauen, werden Sie wieder einmal feststellen, daß der C Preprozessor eine äußerst nützliche Sache ist.

Er verbirgt nämlich die relativ komplizierten Definitionen vor dem Programmierer. Das ganze Programm besteht nur aus der Funktion main(), die ihrerseits nur eine einfache while-Schleife enthält. Am Interessantesten ist sicherlich die Bedingung der Schleife. Das Einlesen des nächsten Zeichens von Standardeingabe - genau das tut nämlich getchar() - ist in die Schleifenbedingung verlagert worden. Das ist sehr typisch für C Programme. Daß die Konstruktion so funktioniert, liegt natürlich daran, daß - wie wir beim letzten Teil schon gesehen haben - eine Zuweisung immer den Wert, der an die linke zugewiesen wird, als Ergebnis liefert. So wird also an die Variable c das nächste Zeichen in der Eingabe zugewiesen und gleichzeitig das Ergebnis dieser Zuweisung — eben das nächste Zeichen — mit dem EOF-Wert verglichen (EOF ist im übrigen eine Konstante, die normalerweise in stdio.h vereinbart wird). Die Klammern um die Zuweisung sind unbedingt notwendig, da der != Operator (ungleich) eine höhere Priorität hat als die Zuweisung. Sie können sich ja als Übung überlegen, was c sonst für einen Wert erhält, wenn man die Klammern vergißt. (Dazu muß man wissen, daß in C Wahrheitswerte (boolsche Werte) mit False=0 und True! = 0 (meist 1) codiert werden). Falls der Vergleich ergibt, daß das Dateiende noch nicht erreicht wurde, wird das gelesene Zeichen durch die Funktion putchar() an die Standardausgabe weitergereicht. Möglicherweise ist Ihnen aufgefallen, daß die Variable c als int-Wert definiert ist, da wir aber doch nur Zeichen aus dem ASCII Zeichensatz lesen wollen (Codes 0-255), müßte es doch reichen, eine char-Variable zu vereinbaren. Der Grund liegt bei der EOF Abfrage. EOF hat normalerweise den Wert -1. Da durch die einzulesenden Zeichen alle verfügbaren char-Codes belegt werden, muß man c als int vereinbaren, um diesen zusätzlichen Wert darstellen zu können. Bei der ATARI Version des Programms hingegen kann c ohne Probleme auch als char definiert werden, da die EOF Abfrage von der feof Funktion ohne Betrachtung des gelesenen Zeichens erledigt wird. In Tabelle 2.2 habe ich nochmal die Funktionen, die mit der Standard Ein/Ausgabeschnittstelle arbeiten, zusammengefaßt.

Tabelle 2.2

Noch zwei Anmerkungen zum letzten Beispiel: Wenn Sie eine Datei über die Tastatur eingeben, müssen Sie dem Programm mitteilen, wenn das Dateiende erreicht ist. Dies bewirkt die Tastenkombination CTRL-Z. Außerdem sendet die Returntaste nur ein Carriage-Return; eine Zeile muß aber mit CR-LF abgeschlossen werden. Wenn Sie also eine neue Zeile anfangen wollen, müssen Sie nach RETURN noch CTRL-J eingeben. - Das gilt natürlich nur für Eingabe über die Tastatur.

Programme der obigen Art heißen im UNIX Jargon „Filter“, da sie einen Strom von Zeichen auf irgendeine Art und Weise verarbeiten und das Ergebnis an die Ausgabe weitergeben. Unter UNIX gibt es zur Standard Ein/Ausgabe noch zusätzlich ein „Pipe“-Konzert. Durch Pipes kann man Filterprogramme „zusammenstecken“ wie Röhren, so daß das, was aus einem Rohr kommt, gleich ins nächste fließt. Übertragen bedeutet das einfach, daß die Ausgabe eines Programms gleichzeitig Eingabe des nächsten — im Idealfall parallel laufenden — Programms ist. CAT ist ein sehr einfacher Filter, es gibt aber auch sehr viel kompliziertere Filterprogramme. Wer sich dafür interessiert und vielleicht ein paar Tools (Werkzeuge) für den ATARI schreiben will, der sollte mal mit Hilfe eines UNIX Buches auf Ideensuche gehen.

Vielleicht ist Ihnen jetzt auch klar geworden, wofür man eine Standardfehlerausgabe braucht. Wenn ein Programm Fehlermeldungen erzeugt und diese auf die Standardausgabe geben würde, würden die Fehler irgendwo in den Pipes oder in einer Datei versickern, und der Benutzer bekäme sie nie zu Gesicht. Deshalb braucht man eine weitere Dateiverbindung, die auf dem Bildschirm gerichtet ist.

2 Die for-Schleife

Nach diesem kleinen Abstecher in die Standard Ein/Ausgabe wieder zurück zu den noch ausstehenden Kontrollstrukturen. Zuerst sehen wir uns einmal die for-Schleife an. PASCAL und BASIC Programmierer kennen den Begriff bereits. Das for Konzept von C geht aber ein wenig über das der eben genannten Programmiersprachen hinaus. Die allgemeine Form der for-Schleife ist wie folgt:

for (Ausdruck 1; Ausdruck2; Ausdruck3) Anweisung(sblock)

Ausdruck1 wird ausgeführt (berechnet), bevor die Schleife betreten wird. An dieser Stelle kann man also eine Initialisierung unterbringen. Meistens wird hier die Schleifenvariable initialisiert. Ausdruck2 wird vor jedem Schleifendurchlauf ausgewertet — auch vor dem ersten wenn er ein Ergebnis != 0 (also TRUE) liefert, wird die Schleife ausgeführt, wenn nicht, geht die Ausführung des Programms nach der Schleife weiter. An Stelle von Ausdruck2 wird also meist ein Abbruchkriterium für die Schleife stehen. Ausdruck3 schließlich wird immer nach einem Schleifendurchlauf ausgewertet.

An seiner Stelle wird man normalerweise eine Laufvariable hoch- oder runterzählen. Alle drei Ausdrücke können leer sein, die Semikolons aber müssen geschrieben werden. Wenn Ausdruck1 fehlt, wird nichts initialisiert, fehlt Ausdruck2, wird er immer als TRUE angenommen - das ergibt also eine Endlosschleife, die von innen mit einer break oder goto-Anweisung verlassen werden muß - fehlt der dritte Ausdruck, wird nach einem Schleifendurchlauf eben nichts getan. Im Folgenden ein paar einzeilige Beispiele, die nur verdeutlichen sollen, wie eine for-Schleife aussehen kann.

Das erste Beispiel ist direkt äquivalent zur for-Schleife in PASCAL. Eine Laufvariable i wird auf den Wert 0 initialisiert und in jedem Durchlauf um eins erhöht (auf die Opertoren + + und / = etc. gehe ich später noch ein), bis der Wert 100 erreicht ist. Durch die printf-Funktion wird dann die Variable i und ihr Quadrat gedruckt. Im zweiten Beispiel wird die Laufvariable nicht erhöht, sondern jedesmal durch 2 geteilt, bis irgendwann der Wert 5 erreicht ist. Das dritte Beispiel zeigt eine Endlosschleife, die durch break verlassen werden muß. Das vierte Beispiel greift schon etwas vor. Es zeigt, wie man ein mit ’ 0’ abgeschlossenes char-Array in ein anderes kopiert. Zu bemerken ist dabei, daß die ’ 0’ auch mitkopiert wird, da die Zuweisung v >r dem Vergleich ausgeführt wird. Im Laufe des Kurses werden wir noch einige Male mit for-Schleifen zu tun haben, ich werde Sie dann gegebenenfalls nochmals auf Besonderheiten aufmerksam machen.

3 Die switch Anweisung

Die switch-Anweisung ähnelt dem case-Statement in PASCAL. Sie erlaubt es, in Abhängigkeit vom Ergebnis eines Ausdrucks, mehrfach zu verzweigen.

Die genaue Syntax sieht so aus:

switch ( Ausdruck ) \
case Konstanter Ausdruck 1: ...
case Konstanter Ausdruck 2: ...
case Konstanter Ausdruck n: ...
[ default: ...]

Der nach dem Schlüsselwort switch stehende Ausdruck wird ausgewertet. Das Ergebnis wird nun nacheinander mit den konstanten Ausdrücken 1 .. n verglichen. Bei Übereinstimmung werden die hinter dem Doppelpunkt stehende Anweisung(sfolge) und alle danach folgenden Fälle ausgeführt.

Will man nur die Anweisungen für den zutreffenden Fall ausführen, muß man als letzte Anweisung des Blocks ein break schreiben. Diese auf den ersten Blick etwas seltsam anmutende Verhaltensweise erklärt sich aus der Implementierung der Anweisung. Die Anweisungsfolgen zu allen cases stehen direkt hintereinander, und die Konstanten bilden eine Sprungleiste, über die an die richtige Stelle in dem sich ergebenden Codestück verzweigt wird. Der (optionale) default wird ausgeführt, wenn keine Übereinstimmung gefunden werden konnte. Falls der default fehlt, wird im Programm nach dem switch weitergemacht. Um das Ganze etwas anschaulicher zu machen, eine Beispielfunktion error(). Sie gibt zu einer Fehlernummer, die von gemdos, bios oder xbios geliefert wird eine Fehlermeldung im Klartext aus (siehe Beispiel 2.2). Sie läßt sich vorteilhaft in Programme einbauen, die direkt auf Betriebssystemebene arbeiten, z. B. um das Directory einer Diskette auszugeben usw. Funktionen dieser Bauart werden wir im nächsten Teil noch kennenlernen, wenn wir das nötige Handwerkszeug dazu erarbeitet haben. Sie sehen, daß jede Fehlermeldung durch ein break abgeschlossen ist. Der default Fall wird dazu verwendet, unbekannte Fehler anzuzeigen. Beispiel 2.3 ist ein Ausschnitt aus einem Taschenrechner Accessory, das ich für den ATARI geschrieben habe. Es zeigt den Teil, in dem die Tastendrücke ausgewertet werden. Die Konstanten EINS, ZWEI .., NEUN etc. werden geliefert, wenn der Benutzer mit dem Mauszeiger ein „Taste“ des Rechners „drückt“. Die ganze Auswertung der Tasten ist ein riesiger switch, den ich hier natürlich nur auszugsweise angeben kann. Was ich eigentlich daran zeigen will, ist, wie man mehrere Fälle behandelt, die alle auf den gleichen Anweisungsblock führen.

Ich werde jetzt noch kurz auf die Operatoren, die C zu bieten hat, eingehen. In den vorausgegangenen Beispielen haben wir schon einige kennengelernt. Zuerst sind da natürlich, wie in jeder Programmiersprache, die Grundrechenarten ( + , -, ★, /, %), deren Bedeutung wohl jedem klar ist. Der %-Operator steht für die Moduloperation (Restberechnung) und entspricht dem div von PASCAL. Man kann sich als Faustregel merken: das Ergebnis hat immer das gleiche Vorzeichen wie der erste Operand. Also 11%2 = 1 aber -11%2 = -1 und 11% - 2 = 1 zuletzt noch -11% - 2 = — 1.

Zur Gruppe der Operatoren, die auf Bitebene arbeiten gehören:

< < - a < < b verschiebt a um b Bits nach links
> > - a > > b verschiebt a um b Bits nach rechts
&, I, e - bitweises UND, ODER und Exclusiv Oder.
---Nicht (1er Komplement)

Der zweite Operand bei den shift Operationen darf nie negativ sein.

Dann gibt es die logischen Operatoren, die zum Großteil denen anderer Programmiersprachen gleichen:

<, < =, >, > = - wie üblich kleiner, kleiner gleich, größer und größer gleich.
= = - Gleichheit (Eine häufige Fehlerquelle ist es nur ein = zu schreiben)
!= - den hatten wir schon. Der ungleich-Operator.
&& - logisches UND
|| - logisches ODER

Die logischen Operatoren liefern den int-Wert 1 bzw 0, je nachdem, ob der Vergleich „wahr“ bzw. „falsch“ ist.

Man sollte bei Anwendung der logischen Operatoren immer Teilausdrücke so klammern, wie man sie intendiert hat, da die Prioritäten der Operatoren manchmal etwas anders sind als man erwarten würde.

Jetzt fehlen uns nur noch die Kurzoperatoren von C. Da wären zuerst + + und ~. Jeder, der zum ersten Mal ein C Programm sieht, wird sie gleich bemerken. Sie bilden eine Kurzschreibweise für Zuweisungen der Form x = x + —1; haben aber noch einen Nebeneffekt, je nachdem, ob sie vor oder nach einer Variablen stehen. x+ + allein ist identisch zu + +x, aber im Zusammenhang mit einer Zuweisung ergeben sich Unterschiede. Im Programmstück jx = 5; a = x++;i hat a nach Ausführung den Wert 5, aber bei [x = 5; a = + +x;i den Wert 6. Einmal wird also nach Ausführung der Zuweisung inkrementiert, beim anderen Mal vorher. Die Inkrement und Dekrementoper atoren dürfen nur auf Variable vom Typ char, int und auf Pointer angewendet werden.

Dann sind da noch die Operatoren der Form var op = expr. Sie sind von der Bedeutung her identisch mit dem Ausdruck der Form var = var op (expr). Beachten Sie die Klammern um expr. Der Operator wird immer auf die ganze rechte Seite angewendet; im Programmstück

!a = 5; a★ = 2 + 3;i

hat also a nach Ausführung den Wert 25, nicht etwa 13. Sie sollten diese Schreibweise unbedingt verwenden. Sie macht Ausdrücke im Allgemeinen leichter lesbar und erlaubt es dem Compiler, einen wesentlich effizienteren Code zu erzeugen.

Den noch ausstehenden ?: Operator werde ich erst im nächsten Teil besprechen.

Zum Abschluß des zweiten Teils ein Überblick über die Grunddatentypen, die C zur Verfügung stellt, und ihre maschinenabhängige Länge, insbesondere im Bezug auf den ATARI.

In manchen C’s gibt es noch die Typen unsigned short und unsigned long. Das ist aber beim C des Entwicklungspakets nicht der Fall.

Im nächsten Teil des Kurses werde ich die zusammengesetzten Datentypen wie structs und arrays und die zentralen Datentypen in C, die Pointer, behandeln, außerdem werde ich eine vollständige Übersicht über die vorhandenen Operatoren geben. Sie wissen dann schon soviel, um auch ein paar interessantere Beispiele zu verstehen. Zum Schluß noch eine kleine Denksportaufgabe:

Wie kann man die Werte zweier Variablen vertauschen, ohne eine dritte Variable als Hilfsvariable zu verwenden?

Die Lösung verrate ich Ihnen in der nächsten Folge.



Aus: ST-Computer 06 / 1986, Seite 54

Links

Copyright-Bestimmungen: siehe Über diese Seite