Kapitel 3 Ausdrücke, Anweisungen, Kontrollstrukturen Wir behandeln nun übersichtsartig die wichtigsten Sprachelemente in (höheren) Programmiersprachen für die Formulierung von Algorithmen. 3.1 Variablen und Objekte Ausdrücke beinhalten üblicherweise Variable, auf deren Wert bei der Auswertung zugegriffen wird. In Java gibt es zwei unterschiedliche Arten von Variablen (bzw. Datentypen) nämlich Standardtypen und Referenztypen. Die Standardtypen sind die “eingebauten” Typen1 boolean char byte short int long float double 1 Bit 16 Bit ganze Zahl ohne Vorzeichen entspricht dem Zahlenwert eines Unicode-Zeichen 8 Bit ganze Zahl mit Vorzeichen 16 Bit ganze Zahl mit Vorzeichen 32 Bit ganze Zahl mit Vorzeichen 64 Bit ganze Zahl mit Vorzeichen 32 Bit Fließkommazahl 64 Bit Fließkommazahl Bei diesen Typen wird unter dem Namen der Variablen stets der Wert angesprochen. In der Sequenz int a = 1; 1 Auf die rechnerinterne Darstellung der auftretenden Zahlen wird genauer in Kapitel 12 eingegangen. Version vom 2. November 2004 31 32 KAPITEL 3. AUSDRÜCKE, ANWEISUNGEN, KONTROLLSTRUKTUREN int b = 2; a = a + b; wird also in a + b auf die Werte 1 und 2 zugegriffen. Alle anderen Typen in Java sind Referenztypen. Hierzu gehören • Klassentypen • Schnittstellentypen • Arraytypen Wir behandeln zunächst nur die Klassentypen. Jede Klasse stellt einen Datentyp dar. Die Werte von Variablen eines Klassentyps heißen in Java Objekte. In TextField input, output; werden zwei Variablen der Klasse TextField deklariert. Bei Referenztypen wird unter dem Namen der Variablen nicht der Wert (also das Objekt), sondern die Adresse (Referenz) angesprochen, an der das Objekt im Speicher abgelegt ist. Man braucht daher Methoden, um • die Objekte im Speicher zu erzeugen, • die Werte/Daten einzugeben, • auf die Werte/Daten zuzugreifen. Die Erzeugung der Objekte erfolgt (bis auf wenige Ausnahmen) mit der new-Anweisung und dem Aufruf eines Konstruktors. In output = new TextField (10); ist TextField(10) ein Konstruktor (von mehreren) der Klasse TextField, der ein Textfeld mit 10 Zeichen konstruiert und im Speicher einrichtet. Ein anderer Konstruktor ist z. B. output = new TextField("Coma I", 10); der das Textfeld gleich mit dem String Coma I belegt und eine Länge von 10 Zeichen vereinbart. Jede Klasse verfügt üblicherweise über mehrere Konstruktoren für Objekte, darunter einen DefaultKonstruktor ohne Argumente. Die Eingabe von Daten erfolgt dann z. B. über die Methode setText() wie in output.setText("Hallo!"); 3.1. VARIABLEN UND OBJEKTE 33 Der Zugriff auf Daten entsprechend über die Methode getText() output.getText(); Ausnahmen gibt es bei der Klasse String (als Zugeständnis an C-Programmierer). Hier ist neben der Java-konformen Konstruktion String str = new String("Hallo"); auch die direkte Zuweisung möglich String str = "Hallo"; Ferner wird unter dem Namen einer Variablen vom Typ String in Ausdrücken stets der Wert (also der String selbst) angesprochen. Für alle Standardtypen gibt es in Java korrespondierende Klassen (sogenannte Wrapper-Classes), etwa Integer für int, Double für double usw. Die Sequenz int a = 64; Integer A = new Integer(a); erzeugt ein Integer Objekt mit Wert von a (also 64). Durch int b = A.intValue(); wird der int-Variablen b der Wert des Objektes A zugewiesen, auf den mit der Methode intValue() zugegriffen wird. Völlig falsch und nicht Typ-verträglich wäre hier die Zuweisung int b = A; da unter A die Adresse von A, aber nicht der Wert angesprochen wird. Zur Illustration des Unterschiedes zwischen Adresse (Referenz) und Wert betrachte man folgende Deklaration: TextField input, output; String str = new String("Hallo!"); Hierfür werden im Speicher Plätze für drei Referenzen angelegt. Die ersten beiden werden mit null initialisiert, während die dritte eine Referenz auf den String Hallo enthält. Dabei steht die Konstante null für die “leere” Adresse. Im allgemeinen nehmen Referenzen natürlich weniger Speicherplatz als die eigentlichen Daten ein. Dies wird durch die unterschiedliche Größe der Speicherplätze angedeutet. Die Referenz von str auf die eigentlichen Daten (den Wert) wird durch den Pfeil dargestellt. 34 KAPITEL 3. AUSDRÜCKE, ANWEISUNGEN, KONTROLLSTRUKTUREN input null output null str r - Hallo! Durch input = new TextField(str, 10); output = new TextField("Wie geht’s?", 10); str = "Gut!"; werden zwei neue TextField-Objekte mit Adressen input und output erzeugt, die die Werte Hallo! bzw. Wie geht’s? haben. Ferner wird der Variablen str der neue Wert Gut! zugewiesen. Es ergibt sich folgendes Speicherbild: input r - Hallo! output r - Wie geht’s? str r - Gut! Durch die Zuweisung input = output wird input die Adresse von output (nicht der Wert!) zugeordnet. Danach werden unter input und output dasselbe TextField-Objekt mit Wert Wie geht’s? angesprochen. Das Objekt mit Wert Hallo! kann jedoch nicht mehr (mit input) angesprochen werden. Es entsteht folgendes Speicherbild: input rP PP Hallo! output r PP q P - str r - Wie geht’s? Gut! Um in input denselben Wert wie in output abzuspeichern, muss man die Methoden getText() und setText() verwenden: input.setText(output.getText()); Den Unterschied zwischen Referenz und Wert zeigt auch folgendes Codefragment. Integer intA = new Integer(10); Integer intB = new Integer(10); String compare = (intA == intB) + " " + intA.equals(intB); Zunächst werden zwei verschiedene Integer Objekte intA und intB mit demselben Wert 10 erzeugt. Der Boolesche Ausdruck (intA == intB) vergleicht die Referenzen von intA und intB, während 35 3.2. AUSDRÜCKE intA.equals(intB) mittels der Methode equals() der Klasse Integer die Werte von intA und intB vergleicht. Der String compare bekommt also den Wert true false zugewiesen. 2 3.2 Ausdrücke Ein Ausdruck ist grob gesprochen eine Formel oder Rechenregel, die stets einen Wert (Resultat) spezifiziert. Der Ausdruck besteht aus Operatoren und Operanden. Beispiele für Operanden sind Konstanten, Variablen, Funktionen; Beispiele für Operatoren sind die arithmetischen Operatoren + - * / und die logischen Operatoren ! && || (nicht, und, oder). Operatoren sind immer in Zusammenhang mit zugehörigen Wertebereichen (im Programmiersprachen Jargon: Datentypen oder Typen) zu sehen, z. B. ganze Zahlen oder Gleitkommazahlen. Sie werden daher in Zusammenhang mit Datentypen in Kapitel 5 noch eingehender diskutiert. Beispiele für Ausdrücke in Java sind: a > b a * b / c != c + d * e (a + b) * 3 / 2 logischer Ausdruck logischer Ausdruck arithmetischer Ausdruck Haben a,. . .,e die Werte 1, 2, . . . , 6, so liefern die Ausdrücke die Werte false, true, bzw. 4. Der Vorrang von Operatoren ist im Zweifelsfall durch Klammern zu regeln. Bei gleichwertigen Operatoren erfolgt die Auswertung von links nach rechts. 3.2.1 Ausdrücke, genaue Erklärung In Java unterscheidet man wie in C lvalues und rvalues. Beide sind Ausdrücke, jedoch können lvalues in Zuweisungen nur links vom Zuweisungszeichen = stehen. Genauer: lvalues bezeichnen alles, was Inhalt einer Speicheradresse ist (also insbesondere Variablen, während rvalues allgemein den Wert eines Ausdrucks bezeichnet. Ein Ausdruck hat ganz allgemein 3 Eigenschaften: • Einen Wert oder Rückgabewert, der sich durch vollständige Auswertung des Ausdrucks ergibt. • Einen Typ, nämlich den Typ seines Wertes. Bei Funktionsaufrufen ist dies der Typ des Rückgabewertes (Rückgabetyp). • Einen Effekt. Darunter versteht man einen Effekt auf Speicherinhalte. Ergibt sich dieser nicht aus der Zuweisung eines Wertes an einen lvalue, so nennt man dies einen Seiteneffekt.3 Seiteneffekte entstehen vor allem bei Funktionsaufrufen (Änderung von Parametern oder globalen Variablen), aber auch bei vielen Operatoren. 2 Eine einzige Ausnahme von der Unterscheidung zwischen Adresse und Wert gibt es bei Strings, vgl. Abschnitt 5.4. wird auch die Zuweisung an einen lvalue als Seiteneffekt bezeichnet. 3 Manchmal 36 KAPITEL 3. AUSDRÜCKE, ANWEISUNGEN, KONTROLLSTRUKTUREN 3.2.2 Ausdrücke und einfache Anweisungen Eine wichtige Regel in der C-Programmierung (und damit auch in Java) ist: Ein Ausdruck wird zu einer (einfachen) Anweisung durch Anfügen eines Semikolons. In diesem Fall wird der Rückgabewert unterdrückt, und Zuweisungen an lvalues und ggf. Seiteneffekte sind die einzigen Effekte. Weitere einfache Anweisungen sind: Aufruf einer void-Funktion oder eine Definition, jeweils abgeschlossen durch ein Semikolon. Die leere Anweisung besteht nur aus einem Semikolon. 3.2.3 Beispiele 1. a = b Dies ist ein Zuweisungsausdruck mit dem Zuweisungsoperator =. Dabei wird dem lvalue a (der einen Speicherplatz bezeichnet) der Wert des rvalue b zugewiesen. Es wird also ein Speicherinhalt verändert; dies zählt aber nicht als Seiteneffekt, da es eine Zuweisung an einen lvalue ist. Der Wert des Ausdrucks ist der Wert, der der linken Seite zugewiesen wird. Der Typ ist der Typ der linken Seite. Die einfache Anweisung a = b; weist also a den Wert von b zu. Der Wert des Ausdrucks a = b, also b, wird unterdrückt. 2. c += b = a += ist ein Zuweisungsoperator mit folgender Bedeutung: x += y ist äquivalent zu x = x + y. In dem Ausdruck c += b = a ist c der lvalue und b = a der rvalue. Die Auswertung des rvalue weist b (als Seiteneffekt!) den Wert von a zu. Der Wert des rvalues ist der Wert von a. Zu diesem wird der Wert von c addiert. (wegen des += Operators) und die Summe dem lvalue c zugewiesen. Ein Beispiel mit konkreten Werten: a) vorher a b c 1 5 2 b) nachher a b c 1 1 3 ← Seiteneffekt! Solche Seiteneffekte sind zwar möglich, aber sollten tunlichst vermieden werden, da sie zu unlesbaren Programmen führen. Die Zuweisungen b = a; c += b; leisten dasselbe und sind klarer. 3. Zuweisungen in logischen Ausdrücken Ein deutliches Beispiel für die Eigenschaften von Ausdrücken ist 3.3. DEFINITIONEN UND DEKLARATIONEN 37 if ((a = b) > c) c = a; für int Variablen a,b,c. Die Zuweisung a = b liefert einen Wert (nämlich den von b). Ist dieser größer als der Wert von c, so wird c = a ausgeführt. Durch die Auswertung des Ausdrucks a = b wird als Seiteneffekt a der Wert von b zugewiesen. 4. Der Inkrementoperator ++ (entsprechend --) Er kann als Präfix (++a) oder Postfix (a++) auf einen arithmetischen Ausdruck a angewendet werden. Ist a ein lvalue, so wird der Wert von a um 1 erhöht. Der Wert von a++ ist der Wert von a; der Wert von ++a ist der Wert von a plus 1. Also: a++; ++a; bedeutet: erst benutzen, dann erhöhen bedeutet: erst erhöhen, dann benutzen Ein Beispiel mit Werten a == 2 und b == 4: a = b++; a = ++b; a++; ++a; 3.3 weist a den Wert 4 zu und erhöht als Seiteneffekt den Wert von b auf 5. weist a den Wert 5 zu und erhöht als Seiteneffekt den Wert von b auf 5. weist a den Wert von 3 zu, dies ist kein Seiteneffekt, da a ein lvalue ist. weist a den Wert von 3 zu, dies ist kein Seiteneffekt, da a ein lvalue ist. Definitionen und Deklarationen In Java unterscheidet man zwischen Deklaration und Definition. Deklarationen führen Identifier ein und assoziieren mit ihnen Typen, so dass der Compiler aufgrund dieser Deklaration die Typverträglichkeit überprüfen kann (type checking). In Java erfolgt dabei automatisch eine Default-Initialisierung. Definitionen sind Deklarationen, die zugleich (bei Variablen und Konstanten) den Identifiern Speicherplatz zuordnen oder (bei Funktionen) den Rumpf der Funktion aufführen. Jeder Identifier muss deklariert sein, bevor er benutzt werden kann. Beispiele sind: int a; double x = 3.14; int Square(int x) {return x*x;} int Square(int x); deklariert die Integer Variable a. definiert die Gleitkomma Variable x und initialisiert sie zu 3.14. definiert die Funktion f (x) = x2 . In den {...} Klammern steht der Funktionsrumpf. deklariert eine Funktion Square. Dem Compiler sind dadurch Name, Rückgabetyp und Argumenttyp bekannt. Weitere Beispiele werden in Kapitel 7.1.3 diskutiert. 38 KAPITEL 3. AUSDRÜCKE, ANWEISUNGEN, KONTROLLSTRUKTUREN 3.4 Strukturierte Anweisungen Neben den einfachen Anweisungen gibt es die strukturierten Anweisungen: • zusammengesetzte Anweisung (Verbundanweisung) • bedingte Anweisung • wiederholende Anweisung und daraus abgeleitete Anweisungen (z. B. die selektive Anweisung). Sie werden mit den nachfolgend beschriebenen Kontrollstrukturen gebildet, die in allen höheren Programmiersprachen existieren. Sie heißen Kontrollstrukturen, da mit ihnen kontrolliert“ wird, wie Programme intern in ” Abhängigkeit von Bedingungen gesteuert werden, d. h., je nach Bedingung unterschiedliche Anweisungen ausführen, teilweise auch wiederholt. Man sagt auch, die Kontrollstrukturen legen den Programmfluss“ fest. Neben den Kontrollstrukturen bilden Methoden und Klassen weitere wichtige ” Bausteine zum Steuern und Strukturieren von Programmen. AUf sie wird ausführlich in Kapitel 7 eingegengen. Für die Kontrollstrukturen werden wir neben einer umgangsprachlichen Beschreibung und einem Beispiel drei programmiersprachenunabhängige Darstellungen angeben, und zwar als: • Pseudocode, • Struktogramm, • Flussdiagramm. Danach wird auf die entsprechende Realisierung der Kontrollstrukturen in Java eingegangen. Struktogramme (auch Nassi-Shneiderman-flowcharts genannt) und Flussdiagramme sind graphische Beschreibungen von Algorithmen unter Verwendung der genannten Kontrollstrukturen. Pseudocode leistet das gleiche in einer an Pascal orientierten, mit normalem Text durchsetzten Beschreibung. Die folgende Tabelle stellt die Pseudocode Äquivalente bereits eingeführter Java Konstrukte zusammen. Pseudocode := = 6 = not and or {. . .} Java = == != ! && || /*...*/ bzw. //... Bedeutung Zuweisung Test auf Gleichheit Test auf Ungleichheit logisches nicht logisches und logisches oder Kommentare 39 3.4. STRUKTURIERTE ANWEISUNGEN 3.4.1 Zusammengesetzte Anweisung, Verkettung Die zusammengesetzte Anweisung (compound statement) besteht in der Hintereinanderschaltung (Verkettung) von Anweisungen (einfachen oder strukturierten) M1 , . . . , Mt , die in der gegebenen Reihenfolge sequentiell abgearbeitet werden. Beispiel 3.1 (Hypothekberechnung) Es ist der monatliche Betrag a zu berechnen, der zur Ablösung einer Annuitätshypothek in Höhe von b EURO bei einer Laufzeit von n Jahren und jährlicher Verzinsung zum Zinssatz von z % anfällt. Dazu stellen wir folgende Überlegung an. Der Betrag b wächst in n Jahren bei der angegebenen Verzinsung auf b · (1 + z)n , falls keine Rückzahlungen erfolgen. Die monatlichen Zahlungen von a müssen, wenn sie zum selben Zinssatz verzinst werden, nach n Jahren auf den selben Betrag von b · (1 + z)n führen. Durch Gleichsetzung beider Beträge lässt sich a berechnen. Sehen wir uns an, wie sich die monatlichen Zahlungen verzinsen. Im ersten Jahr wird 12a “angespart”. Dieser Betrag wird allerdings erst ab dem zweiten Jahr verzinst.4 Er wächst also nach n Jahren auf 12a(1 + z)n−1 EURO. Entsprechend ergeben die Zahlungen im zweiten Jahr am Ende 12a(1 + z)n−2 EURO, usw. Insgesamt ist der Wert aller Zahlungen nach n Jahren auf 12a · (1 + z)n−1 + 12a · (1 + z)n−2 + . . . + 12a angewachsen. Die Gleichsetzung der Beträge ergibt dann: b · (1 + z)n = 12a · (1 + z)n−1 + 12a · (1 + z)n−2 + . . . + 12a = 12a[(1 + z)n−1 + . . . + (1 + z)1 + (1 + z)0 ] (1 + z)n − 1 = 12a (1 + z) − 1 (1 + z)n − 1 = 12a z Hieraus folgt a= b (1 + z)n · z · 12 (1 + z)n − 1 Dies resultiert in folgenden Algorithmus zur Berechnung von a. 4 Dies ist eine gerichtlich umstrittene Praxis vieler Banken. An sich müssten die Zahlungen bereits—wie bei Sparguthaben—für Teile eines Jahres verzinst werden. Die entsprechende Rechnung bleibt als Übung überlassen. 40 KAPITEL 3. AUSDRÜCKE, ANWEISUNGEN, KONTROLLSTRUKTUREN Algorithmus 3.1 (Hypothek Abtrag) M1 Lese b ein (in EURO) M2 Lese z ein (in %) M3 Lese n ein (in Jahren) M4 Berechne r := 1 + z M5 Berechne R := rn M6 Berechne a := (b/12)(R · z)/(R − 1) M7 Gebe a aus Die Verkettung erlaubt keine Verzweigung. Sie hat daher (ohne Verwendung weiterer Kontrollstrukturen) nur einen beschränkten Anwendungsbereich. Viele sogenannte “programmierbare” Taschenrechner der ersten Generation waren nur so programmierbar. Der Pseudocode für die Verkettung lautet: begin M1 ; M2 ; .. . Mt ; end Das Struktogramm für die Verkettung ist in Abbildung 3.1 angegeben, das Flussdiagramm in Abbildung 3.2. M1 M2 .. . Mt Abbildung 3.1: Struktogramm der Verkettung. −→ M1 −→ M2 −→ . . .−→ Mt −→ Abbildung 3.2: Flussdiagramm der Verkettung. In Java werden zusammengesetzte Anweisungen durch geschweifte Klammern dargestellt: { M1 ; M2 ; .. . 41 3.4. STRUKTURIERTE ANWEISUNGEN Mt ; } Ein Beispiel für ein Java Programm mit nur einer zusammengesetzten Anweisung ist Programm 2.1 (Temperatur.java). Hier folgt ein weiteres für die Hypothekberechnung. Programm 3.1 Hypothek.java // Hypothek.java // // calculates monthly mortgage rate import java.awt.*; import java.applet.Applet; import java.awt.event.ActionListener; import java.awt.event.ActionEvent; public class Hypothek extends Applet { double amount, ratePerMonth, interestRate, interestFactor, power; period; int Label TextField // // // // // // total amount monthly rate interest rate in % 1 + interest rate interestFactor^period number of years amountPrompt, interestRatePrompt, periodPrompt, startPrompt; // declare Labels amountField, interestRateField, periodField; // declare textfields for input // setup the graphical user interface components // and initialize labels and text fields public void init() { setLayout(new FlowLayout(FlowLayout.LEFT)); setFont(new Font("Times", Font.PLAIN, 14)); amountPrompt = new Label("Geben sie den Gesamtbetrag " + "des Darlehens in EURO an:"); amountField = new TextField(10); amountField.addActionListener(new ActionListener(){ public void actionPerformed(ActionEvent e){ calculateMortgage(); 42 KAPITEL 3. AUSDRÜCKE, ANWEISUNGEN, KONTROLLSTRUKTUREN } }); interestRatePrompt = new Label("Geben Sie den Zinssatz " + "in % an:"); interestRateField = new TextField(10); interestRateField.addActionListener(new ActionListener(){ public void actionPerformed(ActionEvent e){ calculateMortgage(); } }); periodPrompt = new Label("Geben Sie die Laufzeit " + "in Jahren an:"); periodField = new TextField(10); periodField.addActionListener(new ActionListener(){ public void actionPerformed(ActionEvent e){ calculateMortgage(); } }); startPrompt = new Label("Drücken Sie Return " + "zum Start der Berechnung."); add(amountPrompt); add(amountField); add(interestRatePrompt); add(interestRateField); add(periodPrompt); add(periodField); add(startPrompt); } // display the result as graphics public void paint(Graphics g) { g.drawString("Ihre monatliche Rate betraegt EURO " + ratePerMonth, 15, 150); g.drawString("für EURO " + amount + " bei " + period + " Jahren und " + interestRate + " % Zinsen.", 15, 170); } // process user’s action on the input text fields 3.4. STRUKTURIERTE ANWEISUNGEN 43 public void calculateMortgage() { // get input numbers amount = Double.parseDouble(amountField.getText()); interestRate = Double.parseDouble( interestRateField.getText()); period = Integer.parseInt(periodField.getText()); interestFactor = (100 + interestRate) / 100; power = Math.pow(interestFactor, period); ratePerMonth = (amount / 12) * power * (interestFactor - 1)/(power - 1); // round to Cent ratePerMonth = (Math.round(ratePerMonth * 100)) / 100.0; repaint(); // calls method paint for the whole applet } } Hier wird die Funktion pow() aus der Klasse Math für die Potenzierung verwendet. pow(a,b) berechnet den Wert ab und gibt ihn als Rückgabewert zurück. Um die Berechnung in jedem Eingabefeld auslösen zu können, wird an jedes Eingabefeld ein ActionListener hinzugefügt, der in allen Fällen dieselbe Methode calculateMortgage() aufruft. Ferner nutzen wir die Graphikmöglichkeiten von Java zur Ausgabe. Die Anweisung repaint() veranlasst das Applet, sich selbst als Graphikobjekt zu sehen und die Methode paint() für sich selbst auf zu rufen. In ihr wird die Methode drawString(str, x, y) verwendet, die den String str an der Position (x,y) zeichnet. Dabei sind x,y int-Variablen, die die Anzahl der Pixel von der linken oberen Ecke der Appletfläche angeben (nach rechts bzgl. x und nach unten bzgl. y. Das fertige Applet ist in Abbildung 3.3 dargestellt. Abbildung 3.3: Das Hypothek Applet. 44 3.4.2 KAPITEL 3. AUSDRÜCKE, ANWEISUNGEN, KONTROLLSTRUKTUREN Selektion, Bedingte Anweisung Die Selektion ermöglicht das Ansteuern einer Alternative in Abhängigkeit von Daten. Der Prototyp der Selektion ist die if-Anweisung. Die if-Anweisung Der Pseudocode für die if-Anweisung lautet: if B then S1 else S2 Dabei ist B ein Boolescher Ausdruck und sind S1 , S2 beliebige Anweisungen (insb. wieder strukturierte Anweisungen). Der Prozessor berechnet den Wahrheitswert (true bzw. false) von B und steuert in Abhängigkeit davon S1 bzw. S2 an. Der else-Teil darf fehlen. Beispiel 3.2 (Betrag) Der Betrag |a − b| der Differenz a − b zweier reeller Zahlen a, b ist zu berechnen. Eine Lösung in Pseudocode lautet: if a > b then berechne a − b else berechne b − a Das Struktogramm der if-Anweisung ist in Abbildung 3.4 dargestellt, das Flussdiagramm in Abbildung 3.5. HH H true HH false B XXX X true XXX X X H S1 S2 B XXX S1 Abbildung 3.4: Struktogramm der if-Anweisung. In Java wird die if-Anweisung folgendermaßen gebildet: if (B) B1 else B2 Auch hier darf der else-Teil fehlen. Die Codekonvention schlägt vor, if und else Teile immer mit {...} zu klammern und dabei folgendes Einrückmuster zu verwenden: 45 3.4. STRUKTURIERTE ANWEISUNGEN + H ? HH − HH B H ? ? S1 S2 ? ? H HH − HH B H + ? S1 ? Abbildung 3.5: Flussdiagramm der if-Anweisung. if (B1 ) { Anweisungen } else if (B2 ) { Anweisungen } else (B1 ) { Anweisungen } Die selektive Anweisung Sollen mehr als die zwei Fälle true und false unterschieden werden, so ist dies mit der selektiven Anweisung oder Selektion möglich. Diese lässt sich als “geschachtelte” Variante der if-Anweisung auffassen. Sind etwa c1 , . . . cn die Werte, die der Ausdruck B annehmen kann, und soll beim Wert ci die Anweisung Si ausgeführt werden, so lässt sich die entsprechende Selektion wie folgt in Pseudocode realisieren: if B = c1 then S1 else if B = c2 then S2 else if . . . .. . else if B = cn then Sn Hierfür existiert in Pseudocode die folgende Kurzform: case B of c1 : S1 ; c2 : S2 ; 46 KAPITEL 3. AUSDRÜCKE, ANWEISUNGEN, KONTROLLSTRUKTUREN .. . cn : Sn ; end Das zugehörige Struktogramm ist in Abbildung 3.6 dargestellt, das Flussdiagramm in Abbildung 3.7. aa c1 aaa aa a c2 aaa aa B aa a aa a aa a aa a S1 S2 cn aaa a ... Sn Abbildung 3.6: Struktogramm der Selektion. B c1 ? S1 PP PP PP P PP c2 ? S2 cn ? ... Sn r ? Abbildung 3.7: Flussdiagramm der Selektion. In Java existiert dafür die switch-Anweisung, siehe Übung. Bei ineinandergeschachtelten if-Anweisungen kann es wegen fehlender else Teile zu Unklarheiten bei der Zuordnung der else zu den if kommen. Falls dies nicht durch Klammerung mit {...} geregelt wird, bezieht sich ein “hängendes” else immer auf das letzte if, auf das eine Zuordnung möglich ist. Bei if (Bedingung1 ) Anweisung1 47 3.4. STRUKTURIERTE ANWEISUNGEN if (Bedingung2 ) Anweisung2 else Anweisung3 bezieht sich das else also auf das zweite if. 3.4.3 Wiederholung Die Wiederholung ermöglicht die wiederholte Durchführung einer Anweisung (meist mit veränderten Werten von Variablen). Die Häufigkeit der Wiederholung wird dabei durch eine Boolesche Bedingung kontrolliert. Der Prototyp der Selektion ist die while-Anweisung. Die while-Anweisung Der Pseudocode für die while-Anweisung lautet: while B do S Dabei ist B ein Boolescher Ausdruck und ist S eine beliebige Anweisung (insb. wieder eine strukturierte Anweisung). Der Prozessor berechnet den Wahrheitswert (true bzw. false) von B vor jeder Ausführung von S. Ist B = true so wird S ausgeführt, sonst nicht. Falls der Wahrheitswert von B immer true bleibt, so wird S stets wieder ausgeführt. Man gerät dann in eine sog. Endlosschleife. Sie stellt bei Anfängern einen häufig gemachten Programmierfehler dar. Das Struktogramm der while-Anweisung ist in Abbildung 3.8 dargestellt, das Flussdiagramm in Abbildung 3.9. B S Abbildung 3.8: Struktogramm der while-Anweisung. In Java wird die while-Anweisung wie folgt gebildet: while ( B ) S Die Codekonvention schlägt folgende einrückende Schreibweise vor: while (B) { Anweisungen S } 48 KAPITEL 3. AUSDRÜCKE, ANWEISUNGEN, KONTROLLSTRUKTUREN ? − HH H B H H H + ? ? S Abbildung 3.9: Flussdiagramm der while-Anweisung. Beispiel 3.3 (Größter gemeinsamer Teiler) Für zwei positive natürliche Zahlen x, y ist der größte gemeinsame Teiler ggT (x, y) zu berechnen. Der größte gemeinsame Teiler von zwei positiven natürlichen Zahlen x und y ist die größte natürliche Zahl, die x und y teilt. So ist ggT (12, 16) = 4 und ggT (12, 17) = 1. Zur Berechnung des ggT nutzen wir folgende mathematische Aussage: Lemma 3.1 Sind a, b ∈ N und ist a > b > 0, so ist ggT (a, b) = ggT (a − b, b). Beweis: Zeige: 1. Ist c ein Teiler von a und b, so auch von a − b und b. 2. Ist c ein Teiler von a − b und b, so auch von a und b. Aus 1. und 2. folgt: a, b und a − b, b haben dieselben Teiler, und damit auch denselben größten gemeinsamen Teiler. zu 1: Sei c ein Teiler von a und b. Dann existieren k1 , k2 ∈ N mit k1 · c = a und k2 · c = b. Hieraus folgt: a − b = k1 · c − k2 · c = (k1 − k2 ) · c Da a > b, ist k1 − k2 > 0. Also ist a − b ein Vielfaches von c und somit c ein Teiler von a − b. Da c nach Annahme auch ein Teiler von b ist, ist c ein Teiler von a − b und b. Also gilt 1. zu 2: Sei c ein Teiler von a − b und b. Dann existieren l1 , l2 ∈ N mit l1 · c = a − b und l2 · c = b. Hieraus folgt: a = (a − b) + b = l1 · c + l2 · c = (l1 + l2 ) · c Also ist c ein Teiler von a. Da c nach Annahme auch ein Teiler von b ist, ist c ein Teiler von a und b. Also gilt 2. Dies führt zu folgendem Algorithmus zur Berechnung von ggT (x, y) (in Pseudocode). 3.4. STRUKTURIERTE ANWEISUNGEN 49 Algorithmus 3.2 (Größter gemeinsamer Teiler) a := x; b := y; while a 6= b do if a > b then a := a − b else b := b − a; {ggT (a, b) = ggT (x, y)} {ggT (x, y) = a = b} ggT (x, y) := a; Wir überlegen uns zunächst, dass dieser Algorithmus korrekt arbeitet. Satz 3.1 Algorithmus 3.2 berechnet für zwei beliebige positive natürliche Zahlen x, y ihren größten gemeinsamen Teiler. Beweis: Lemma 3.1 garantiert, dass am Ende der if-Anweisung bei jedem Durchlauf der whileSchleife die Bedingung ggT (a, b) = ggT (x, y) gilt. Da a oder b in jedem Durchlauf der while-Schleife um mindestens 1 kleiner wird und a und b positiv bleiben, kann die while-Schleife höchstens max{x, y} mal durchlaufen werden. Also terminiert die while-Schleife. Beim Austritt aus der while-Schleife gilt a = b, also ggT (a, b) = a = b. Zusammen mit der oben gezeigten Gleichheit ggT (a, b) = ggT (x, y) folgt die Behauptung. Ein Zahlenbeispiel: Für x = 28, y = 12 ergeben sich folgende Werte für a und b. a: 28 → 16 → 4 → 4 → 4 b: 12 → 12 → 12→ 8 → 4 Nach dem 4. Schleifendurchlauf ist a = b = 4, und der Algorithmus terminiert mit ggT (28, 12) = 4. Ein entsprechendes Java Programm lautet: Programm 3.2 GGT.java // GGT.java // // calculates gratest common divisor gcd(x,y) // of natural numbers x and y import java.awt.*; import java.applet.Applet; import java.awt.event.ActionListener; import java.awt.event.ActionEvent; public class GGT extends Applet { 50 KAPITEL 3. AUSDRÜCKE, ANWEISUNGEN, KONTROLLSTRUKTUREN int x, y, a, b, gcd; Label xPrompt, yPrompt, startPrompt; message; xField, yField; String TextField // natural numbers // declare Labels // declare String for Result // declare textfields for input // setup the graphical user interface components // and initialize labels and text fields public void init() { setLayout(new FlowLayout(FlowLayout.LEFT)); setFont(new Font("Times", Font.PLAIN, 14)); setSize(380, 150); xPrompt = new Label("Geben sie eine natürliche Zahl " + "x > 0 ein:"); xField = new TextField("24", 10); xField.addActionListener(new ActionListener(){ public void actionPerformed(ActionEvent e){ compute_gcd(); } }); yPrompt = new Label("Geben Sie eine natürliche " + "Zahl y > 0 ein:"); yField = new TextField("36", 10); yField.addActionListener(new ActionListener(){ public void actionPerformed(ActionEvent e){ compute_gcd(); } }); startPrompt = new Label("Drücken Sie Return zum " + "Start der Berechnung."); add(xPrompt); add(xField); add(yPrompt); add(yField); add(startPrompt); 3.4. STRUKTURIERTE ANWEISUNGEN 51 message = "Der ggT von 24 und 36 ist 12."; } // display the result as graphics public void paint(Graphics g) { g.drawString(message, 15, 130); } // process user’s action on the input text fields public void compute_gcd() { // get input numbers x = Integer.valueOf(xField.getText()).intValue(); y = Integer.valueOf(yField.getText()).intValue(); if (x <= 0 || y <= 0) { message = "Bitte nur positive ganze Zahlen " + "eingeben!"; } else { a = x; b = y; while (a != b) { if (a > b) { a = a - b; } else { b = b - a; } // gcd(x,y) == a message = "Der ggT von " + x + " und " + y + " ist " + a + "."; } } repaint(); } } Man beachte, dass unzulässige Eingaben von ganzen Zahlen (0 oder negative Zahlen) durch den Benutzer im Programm abgefangen werden. Dies ist notwendig, da das Programm sonst in eine Endlosschleife gerät. Das fertige Applet ist in Abbildung 3.10 dargestellt. 52 KAPITEL 3. AUSDRÜCKE, ANWEISUNGEN, KONTROLLSTRUKTUREN Abbildung 3.10: Das GGT Applet. Varianten der while-Anweisung Bei der while-Anweisung wird S nie ausgeführt, wenn B bereits zu Anfang den Wert false hat. Oft möchte man jedoch die Anweisung S mindestens einmal (unabhängig von B) ausführen. Dies ist mit der while-Anweisung wie folgt möglich: S; while B do S Da dies häufig vorkommt, gibt es hierfür die repeat-Anweisung als eigene Kontrollstruktur. repeat S while B oder, äquivalent dazu, repeat S until not B Der Prozessor führt erst S aus, dann wird B geprüft (post checking im Gegensatz zu pre checking bei der while-Anweisung). Falls B noch erfüllt ist, so wird S erneut ausgeführt. Das Struktogramm der repeat-Anweisung ist in Abbildung 3.11 dargestellt, das Flussdiagramm in Abbildung 3.12. In Java gibt es hierfür die do-while-Anweisung: do S while ( B ); Laut Codekonvention soll sie wie folgt geschrieben werden: do { Anweisungen S } while (B); 53 3.4. STRUKTURIERTE ANWEISUNGEN S not B Abbildung 3.11: Struktogramm der repeat-Anweisung. ? S ? + HH H H B H ? H − Abbildung 3.12: Flussdiagramm der repeat-Anweisung Manchmal muss man eine feste Anzahl von Wiederholungen (Iterationen) von S machen (z. B. n mal). Dies kann realisiert werden durch die Mitführung einer Kontrollvariablen z, die nicht in S vorkommt. Diese wird üblicherweise als Zähler bezeichnet. z := n; while z > 0 do z := z − 1; S Kurzform: repeat S n times Das Struktogramm hierfür ist in Abbildung 3.13 dargestellt. In Java gibt es dafür das for-statement als eigene Kontrollstruktur, siehe Übung. 3.4.4 Mächtigkeit von Kontrollstrukturen Mit den eingeführten Kontrollstrukturen Verkettung, Selektion und Wiederholung kann alles berechnet werden, was im intuitiven Sinne berechenbar ist. Der Nachweis hiervon wird in der Theorie der Berechenbarkeit geführt. 54 KAPITEL 3. AUSDRÜCKE, ANWEISUNGEN, KONTROLLSTRUKTUREN S n times Abbildung 3.13: Struktogramm der n-maligen Wiederholung. Tatsächlich lässt sich dies bereits mit weniger Kontrollstrukturen erreichen, z. B. reichen die Verkettung und die while-Schleife bereits aus. (Übung: hierdurch ist die if-Anweisung simulierbar). Verkettung und Iteration (n-malige Wiederholung) reichen jedoch nicht aus. Man braucht die Möglichkeit, beliebige Boolesche Ausdrücke B als Abbruchbedingung zu wählen. Die gleiche Mächtigkeit erreicht man alternativ auch mit der Verkettung, der Selektion und der gotoAnweisung. Die goto-Anweisung ermöglicht es, zu beliebigen Stellen im Programm zu “springen”. In älteren Programmiersprachen (Basic, Fortran) und in Assembler gehören goto-Anweisungen zum Standard, in moderneren Sprachen sind sie verpönt, da die Programme durch sie meist unstrukturiert werden und schwer zu verstehen und zu überprüfen sind. In Java gibt es kein goto Statement, jedoch sind Sprünge partiell mit den abgeschwächten“ goto ” Anweisungen break und continue erlaubt, bei denen auch Sprungstellen definiert werden können. Wir erläutern hier nur zwei spezielle Situationen um aus geschachtelten if- oder while-Anweisungen heraus zu springen. Hierbei sind keine selbst definierten Sprungstellen erforderlich. Zum Verlassen von Schleifen “in der Mitte” gibt es die Anweisungen break und continue. Die while Anweisung wird dann zu while (true) { ... if (!Fortsetzungsbedingung ) break; ... } bzw. zu while ( Fortsetzungsbedingung ){ ... if (!Bedingung ) continue; weitere Anweisungen } Im ersten Fall wird die while-Schleife ganz verlassen, im zweiten Fall werden die “weiteren Anweisungen” übersprungen und erfolgt der nächste Durchlauf der while-Schleife. 55 3.5. LITERATURHINWEISE break kann auch in der if- bzw. switch-Anweisung auftreten, vgl. Übung. Zum Abschluss geben wir in den Abbildungen 3.14 und 3.15 die verbesserte Version des Algorithmus zur Berechnung der nächst größeren Primzahl zu einer gegebenen Zahl (vgl. Kapitel 2.3) in Form von Struktogrammen an. Lese Zahl n ein Durchlaufe alle ungeraden Zahlen z > n der Reihe nach k kein Teiler von z und k · k ≤ z Teste alle ungeraden Zahlen k ab k = 3 bis k · k ≤ z ob sie Teiler von z sind kein Teiler von z gefunden Gebe z aus Abbildung 3.14: Struktogramm des Primzahl-Algorithmus (Grobversion). Lese Zahl n ein ` `` ``` n gerade ``` true ``` false ``` `` z := n − 1 z := n z := z + 2 k := 3 divisorFound := false (divisorFound = false) and (k · k ≤ z) XXX X true z modulo k = 0 XXX XXX divisorFound := true X XXX false k := k + 2 divisorFound = false Gebe z aus Abbildung 3.15: Struktogramm des Primzahl-Algorithmus (Feinversion). 3.5 Literaturhinweise Kontrollstrukturen werden in nahezu allen Büchern über Programmiersprachen behandelt. Geschichtliche Hinweise zur Entstehung der Kontrollstrukturen und Vergleiche zwischen verschiedenen Programmierstilen und Sprachen finden sich in [Hor84]. 56 KAPITEL 3. AUSDRÜCKE, ANWEISUNGEN, KONTROLLSTRUKTUREN Bzgl. Java verweisen wir auf [CW98, DD01, Küh99]. Die Behandlung von Kontrollstrukturen, Ausdrücken, Typechecking usw. durch Compiler ist ausführlich in [ASU86] dargestellt. Eine Einführung in die Theorie der Formalen Sprachen ist in [Wii87] enthalten. Tiefer geht [HU79].