223 71 15MB
German Pages 251 [264] Year 1995
Die PASCAL-Fibel Strukturierte Programmierung mit Pascal Lehr- und Arbeitsbuch für Anfänger (Für alle gängigen Pascal-Systeme)
Von
Dr. Peter Witschital und
Dr. Thomas Kühme überarbeitet und erweitert von
Dipl.-Inform. Bettina Meiners
Fünfte, überarbeitete und erweiterte Auflage
R. Oldenbourg Verlag München Wien
Dr. Peter Witschital Am Treiberweg 26 D-85630 Grasbrunn Dr. Thomas Kiihme 127 Lake Meadow Drive Johnson City, TN 37615 U.S.A. Dipl.-Inform. Bettina Meiners, geb. Benzel Institut für Programmiersprachen und Informationssysteme Abteilung Programmiersprachen Technische Universität Braunschweig Gaußstr. 11 D-38106 Braunschweig
Die Deutsche Bibliothek - CIP-Einheitsaufnahme Witschital, Peter Die Pascal-Fibel: strukturierte Programmierung mit Pascal; Lehr- und Arbeitsbuch für Anfänger ; (für alle gängigen PascalSysteme) / von Peter Witschital und Thomas Kiihme. - 5., Überarb. und erw. Aufl. / Überarb. und erw. von Bettina Meiners. - München ; Wien : Oldenbourg, 1996 ISBN 3-486-23262-2 NE: Kühme, Thomas:; Meiners, Bettina [Bearb.]
© 1996 R. Oldenbourg Verlag GmbH, München Das Werk einschließlich aller Abbildungen ist urheberrechtlich geschützt. Jede Verwertimg außerhalb der Grenzen des Urheberrechtsgesetzes ist ohne Zustimmung des Verlages unzulässig und strafbar. Das gilt insbesondere für Vervielfältigungen, Übersetzungen, Mikroverfilmungen und die Einspeicherung und Bearbeitung in elektronischen Systemen. Gesamtherstellung: R. Oldenbourg Graphische Betriebe GmbH, München
ISBN 3-486-23262-2
Vorwort Eine Fibel ist ein Lesebuch, ein Elementarlehrbuch, ein Lernbuch zur elementaren Einführung in ein Sachgebiet — so heißt es bei Duden und Brockhaus. Die Pascal-Fibel ist einerseits ein Lehrbuch für Programmieranfänger und soll den Leser am Beispiel der weit verbreiteten Programmiersprache Pascal in die Grundzüge des Programmierens einführen. Nicht die Darstellung des vollen Pascal-Sprachumfangs steht im Vordergrund, sondern die leicht verständliche und ausführliche Erläuterung der wichtigsten Konzepte der Programmiersprache Pascal. Dabei stehen diejenigen Konzepte im Vordergrund, die man auch in anderen Programmiersprachen wiederfindet. Vorkenntnisse aus Informatik, Mathematik oder anderen Bereichen sind nicht erforderlich. Die Pascal-Fibel ist aber auch ein Lesebuch. Neue Sprachelemente werden möglichst erst dann vorgestellt, wenn eine Veranlassung dazu gegeben ist. Der Leser wird nicht zu früh mit Dingen belastet, deren Sinn und Zweck er noch nicht verstehen kann. Die Pascal-Fibel kann daher gut Seite für Seite durchgelesen werden. Das ist wichtig, denn die Fibel ist für das Selbststudium gedacht. Der Kritik, die Fibel sei daher ein weniger gutes Nachschlagewerk, begegnen wir mit einem ausführlichen Stichwortverzeichnis. Die Einführung in das Programmieren erfolgt anhand vieler ausführlicher Beispiele. Im Anschluß an die Lektionen und Unterlektionen werden Kontrollaufgaben gestellt, deren Lösungen man im Anhang findet. Schon nach kurzer Zeit ist der Leser in der Lage, kleine, aber deshalb nicht weniger anspruchsvolle Übungsaufgaben zu lösen und seinen Lernerfolg daran zu messen. Die Pascal-Fibel sowie die gleichzeitig erscheinende F O R T R A N - F i b e l entstanden als Unterlagen zu einem Programmierkurs für Studenten aller Fachrichtungen und Semester an der Technischen Universität Braunschweig. Der Programmierkurs wird auf Arbeitsplatzrechnern mit dem Pascal-System PolyPascal durchgeführt, das dem weit verbreiteten Turbo-Pascal ähnelt. In der Pascal-Fibel wird sowohl Standard-Pascal als auch PolyPascal beschrieben. A u f Unterschiede wird jeweils im Text hingewiesen. An einigen Stellen, insbesondere bei der Dateibearbeitung, geht die Pascal-Fibel intensiver auf die Besonderheiten von PolyPascal ein. Die Hinweise für PolyPascal gelten im allgemeinen auch für Turbo-Pascal.
Bei allen Kursteilnehmern u n d -betreuern des Programmierkurses möchten wir uns herzlich für die vielen wertvollen Hinweise u n d Anregungen bedanken. Unser Dank gilt auch und vor allem Herrn Prof. Dr. G ü n t h e r Stiege, der den Anstoß zur E n t s t e h u n g der Fibeln gab u n d uns bei diesem Vorhaben umfassende U n t e r s t ü t z u n g gewährte. Besonderen Dank möchten wir Herrn Karsten Luck und Herrn Arnd Gerns für die Erstellung der Druckvorlage unter Verwendung des P r o g r a m m s TßX aussprechen. Nicht zuletzt danken wir unseren Angehörigen f ü r das uns entgegengebrachte Verständnis u n d die Hilfe beim Korrekturlesen. Beschließen möchten wir die Danksagungen mit einem herzlichen Dankeschön an den Oldenbourg Verlag, der bereitwillig auf unsere W ü n s c h e bei der Gestaltung der Fibel einging. Den Lesern der Pascal-Fibel wünschen wir viel Erfolg beim P r o g r a m m i e r e n ! Braunschweig, im F r ü h j a h r 1987
Peter Witschital Thomas Kühme
Vorwort zur 3. A u f l a g e Die Pascal-Fibel geht nun, zwei Jahre nach Erscheinen der Erstauflage, bereits in die dritte Auflage. Das "Fibel-Konzept" hat sich erfolgreich bewährt u n d bedarf daher noch keiner Überarbeitung. In die vorliegende Neuauflage wurden einige Korrekturen u n d Ergänzungen aufgenommen. Die Übungsaufgaben im A n h a n g C wurden teilweise durch neue, noch interessantere Aufgaben ersetzt. Mit d e m neuen Kurzindex z u m Herausklappen wurde eine oft geäußerte Anregung verwirklicht. In Verbindung mit der bewährten Spiralbindung kann die Fibel über ihren Lehrbuch-Charakter hinaus - nun noch besser als Arbeitsbuch direkt a m Rechner verwendet werden. Wir danken allen, die durch ihre Kritik, Anregungen u n d Mitwirkung zum guten Gelingen dieser Auflage beigetragen haben. Braunschweig, im F r ü h j a h r 1989
Peter Witschital Thomas Kühme
Vorwort zur 4. Auflage Wir freuen uns, h i e r m i t eine vierte, sorgfältig durchgesehene A u f l a g e der Pascal-Fibel präsentieren zu können. D i e d u r c h w e g positive Resonanz, die in den Briefen unserer Leser zum Ausdruck kommt, belegt, daß mit dem Fibel-Konzept der E i n s t i e g in das Prog r a m m i e r e n m i t Pascal ganz wesentlich erleichtert wird. Wir freuen u n s über d i e s e n Erfolg und wünschen allen künftigen Lesern ein gutes G e l i n g e n ! Peter Witschital Thomas Kühme
Vorwort zur 5. Auflage Eine Fibel ist ein Lesebuch, ein Elementarlehrbuch, ein Lernbuch zur elementaren Einführung in ein Sachgebiet - so heißt es bei Duden und Brockhaus. Die Pascal-Fibel ist einerseits ein Lehrbuch für Programmieranfänger und soll den Leser am Beispiel der weit verbreiteten Programmiersprache Pascal in die Grundzüge des Programmierens einführen. Nicht die Darstellung des vollen Pascal-Sprachumfangs steht im Vordergrund, sondern die leicht verständliche und ausführliche Erläuterung der wichtigsten Konzepte der Programmiersprache Pascal. Dabei stehen diejenigen Konzepte im Vordergrund, die man auch in anderen Programmiersprachen wiederfindet. Vorkenntnisse aus Informatik, Mathematik oder anderen Bereichen sind nicht erforderlich. Die Pascal-Fibel ist aber auch ein Lesebuch. Neue Sprachelemente werden möglichst erst dann vorgestellt, wenn eine Veranlassung dazu gegeben ist. Der Leser wird nicht zu früh mit Dingen belastet, deren Sinn und Zweck er noch nicht verstehen kann. Die Pascal-Fibel kann daher gut Seite für Seite durchgelesen werden. Das ist wichtig, denn die Fibel ist für das Selbststudium gedacht. Der Kritik, die Fibel sei daher ein weniger gutes Nachschlagewerk, begegnen wir mit einem ausführlichen Stichwortverzeichnis und einem Kurzindex zum schnellen Auffinden aller wichtigen Stellen. Die Einführung in das Programmieren erfolgt anhand vieler ausführlicher Beispiele. Im Anschluß an die Lektionen und Unterlektionen werden Kontrollaufgaben gestellt, deren Lösungen man im Anhang findet. Schon nach kurzer Zeit ist der Leser in der Lage, kleine, aber deshalb nicht weniger anspruchsvolle Übungsaufgaben zu lösen und seinen Lernerfolg daran zu messen. Die Pascal-Fibel sowie die gleichzeitig erschienene FORTRAN-Fibel entstanden 1987 als Unterlagen zu einem Programmierkurs für Studenten aller Fachrichtungen und Semester an der Technischen Universität Braunschweig. In den ersten vier Auflagen dieser Fibel wurde neben Standard-Pascal auch der Pascal-Dialekt PolyPascal beschrieben, der damals in den praktischen Übungen zum Programmierkurs verwendet wurde. In nunmehr acht Jahren hat sich die Fibel nicht nur im Unterricht mit mehr als 3.000 Studenten bewährt. Der Aufbau der Fibel und der lockere Stil sind sehr gut angekommen und ermöglichen nach wie vor einen leichten Einstieg in
das Programmieren. Inzwischen haben sich die Voraussetzungen dieses Programmierkurses verändert. Es wird nicht mehr mit PolyPascal, sondern zur Zeit mit XL Pascal gearbeitet. Immer mehr Studenten arbeiten am eigenen Rechner zu Hause mit Turbo-Pascal. Um die Fibel noch universeller und unabhängiger von bestimmten Programmiersystemen zu gestalten, wurde sie komplett überarbeitet. In der Fibel wird jetzt grundsätzlich Standard-Pascal beschrieben. An den Stellen, an denen Standard-Pascal nicht als ausreichende Grundlage genügt, wird auf diese Tatsache ausdrücklich hingewiesen und Turbo-Pascal zugrunde gelegt. In diesem Zusammenhang beschreibt jetzt die Lektion 'Dateibearbeitung' die Handhabung von Dateien mit Direktzugriff unter Turbo-Pascal. Die Fibel wurde außerdem um einen 'Anhang D' erweitert, der Abweichungen der Pascal-Versionen Turbo-Pascal, PolyPascal und XL Pascal von dem beschriebenen Standard-Pascal erläutert. Er soll dem Leser helfen, die in der Fibel erlernte Programmiersprache auch mit einem anderen Pascal-System anwenden zu können. Uberarbeitet und erneuert wurde weiterhin die Lektion 'Dynamische Variablen und Zeigertypen'. Es wird jetzt ausführlicher auf die rechnerinterne Darstellung und Verwaltung von dynamischen Variablen eingegangen. Bei allen Kursteilnehmern und -betreuern des Programmierkurses möchten wir uns herzlich für die vielen wertvollen Hinweise und Anregungen bedanken. Unser Dank gilt auch und vor allem Herrn Prof. Dr. Günther Stiege, der den Anstoß zur Entstehung der Fibeln gab und uns bei diesem Vorhaben umfassende Unterstützung gewährte. Besonderen Dank möchten wir Herrn Karsten Luck und Herrn Arnd Geras für die Erstellung der Druckvorlage unter Verwendung des Programms TgX aussprechen. Im Zusammenhang mit der Überarbeitung der Fibel für die fünfte Auflage danken wir Herrn Martin Neitzel für seine Unterstützung in technischen Fragen und seine wertvollen Anregungen zur Neugestaltung von Lektion 14.2. Nicht zuletzt danken wir unseren Angehörigen für das uns entgegengebrachte Verständnis und die Hilfe beim Korrekturlesen. Beschließen möchten wir die Danksagungen mit einem herzlichen Dankeschön an den Oldenbourg Verlag, der bereitwillig auf unsere Wünsche bei der Gestaltung der Fibel einging. Den Lesern der Pascal-Fibel wünschen wir viel Erfolg beim Programmieren! Braunschweig, im Sommer 1995
Peter Witschital Thomas Kühme Bettina Meiners
Inhaltsverzeichnis Lektionen: 1 Hinweise z u m Lesen der Fibel
1
2 Grundbegriffe der P r o g r a m m i e r u n g
3
2.1
Daten, Programme, Programmiersprachen
3
2.2
Vom Problem zum Programm
7
3 Programmierumgebung
11
4 Elementare Bestandteile eines P r o g r a m m s
15
5 A u f b a u eines P r o g r a m m s 6 Ü b e r s e t z e n und A u s f ü h r e n eines P r o g r a m m s
20 22
7 Variablen, Zuweisungen und elementare Ein- / A u s g a b eanweisungen
26
8 Elementare Operationen mit Zahlen
33
9 Programmstrukturen
38
9.1 9.1.1 9.1.2
Auswahl Einfache Auswahl Mehrfache Auswahl
38 38 49
9.2 9.2.1 9.2.2 9.2.3
Wiederholung Wiederholung mit nachfolgender Bedingung Wiederholung mit vorausgehender Bedingung Wiederholung mit vorgegebener Anzahl
56 56 60 63
10 K o m m e n t i e r u n g und Gestaltung des Programmtextes
71
11 Rechnerarithmetik
75
12 Datenstrukturen I 12.1 12.2 12.3
84
Selbstdefinierte Datentypen Eindimensionale Felder Mehrdimensionale Felder
85 91 108
13 Prozeduren und Funktionen
116
13.1 13.2
Prozeduren Funktionen
14 Datenstrukturen II 14.1 14.2 14.3
Verbundtypen Dynamische Variablen und Zeigertypen Verkettete Listen
117 133
140 140 153 160
15 Dateibearbeitung 16 Rekursion
173 189
Anhänge: A Struktogramme B Lösungen zu den Kontrollaufgaben C Übungsaufgaben D Pascal-Versionen E Literaturhinweise
209 211 232 240 245
Stichwortverzeichnis
246
1
Lektion 1
Hinweise zum Lesen der Fibel Die Pascal-Fibel ist für das Selbststudium konzipiert. Sie ist in Lektionen aufgeteilt, die aufeinander aufbauen und die Sie daher in der vorgegebenen Reihenfolge durchgehen sollten. Die Aufteilung des Lehrstoffs auf die Lektionen erfolgte nach thematischen Gesichtspunkten. Die einzelnen Lektionen haben daher recht unterschiedlichen Umfang. Insofern ist der Begriff "Lektion" in der Pascal-Fibel nicht synonym zu "Lerneinheit". Die umfangreichen Lektionen sind deshalb weiter untergliedert und so in mundgerechte (hirngerechte) Happen aufgeteilt. Bevor Sie sich das erste Mal an den Rechner setzen, sollten Sie Lektion 2 lesen. In Lektion 3 werden Sie aufgefordert, sich mit dem Ihnen zur Verfügung stehenden Rechnersystem vertraut zu machen. Während Sie im Laufe der weiteren Lektionen zum (zur) Pascal-Experten(in) werden, sollte der Rechner Ihr ständiger Begleiter bleiben. In den ersten Lektionen werden Sie dazu angehalten, die Beispielprogramme in den Rechner einzugeben, auszuprobieren und mit ihnen zu experimentieren. Später sollten Sie mit selbst ausgewählten Beispielen entsprechend verfahren. Uberspringen Sie nichts und versuchen Sie, alle Kontrollaufgaben zu lösen, die im Anschluß an die Lektionen bzw. Unterlektionen gestellt werden. Für fast alle Kontrollaufgaben sind Lösungen im Anhang angegeben. Sehen Sie dort aber erst nach, wenn Sie sich ernsthaft mit einer Aufgabe befaßt haben. Hin und wieder werden Sie im Anschluß an eine Lektion aufgefordert, eine Übungsaufgabe zu bearbeiten. Die Übungsaufgaben sind Aufgaben ohne angegebene Lösungen. Sie geben Ihnen Gelegenheit, die Anwendung des Wissens, das Sie bis dahin erworben haben, an einem praktischen Beispiel zu üben. Wenn Sie alle Übungsaufgaben erfolgreich bearbeitet haben, können Sie zu Recht von sich behaupten, Pascal programmieren zu können. Sie finden die Übungsaufgaben im Anhang C der Pascal-Fibel.
1 Hinweise zum Lesen der Fibel
Programmtexte sowie Ein- und Ausgaben von Programmen werden in der Fibel durch einen stilisierten Bildschirm hervorgehoben und zusätzlich durch einen s p e z i e l l e n S c h r i f t t y p , auch im fortlaufenden Text, gekennzeichnet:
Dies i s t e i n Text auf dem B i l d s c h i r m .
Zur allgemeinen Darstellung eines Programmschemas wird in der Fibel die folgende Notation verwendet: i f t h e n e i s e Die sind als Platzhalter zu verstehen, die noch durch einen geeigneten Programmtext ersetzt werden müssen. Die Sprachgrundlage der Beschreibungen in der Pascal-Fibel bildet StandardPascal nach ISO-Norm (ISO 7185). In einigen Fällen reicht diese Norm als Grundlage nicht aus. Die tatsächlichen Anwendungen sind hier sehr verschieden. Es wird deshalb an diesen Stellen die verbreitete Version von Turbo-Pascal als Spezialfall beschrieben. Die wichtigsten Abweichungen einiger Pascal-Versionen von dieser Norm sind im Anhang D zusammengefasst. Doch nun genug der Vorrede, los geht's!
3
Lektion 2
G r u n d b e g r i f f e der P r o g r a m m i e r u n g 2.1
Daten, Programme, Programmiersprachen
Dieser Abschnitt richtet sich an alle diejenigen, die sich erstmalig mit der Prog r a m m i e r u n g eines Rechners beschäftigen. Die wichtigsten Begriffe dieses Problemkreises werden kurz erläutert. Der Begriff R e c h n e r (engl.: Computer) ist eigentlich zu speziell gefaßt. Denn ein Rechner kann nicht nur mit Zahlen rechnen, sondern auch D a t e n ganz anderer Art verarbeiten, zum Beispiel Texte. Die Verarbeitung von Daten findet immer in drei Schritten s t a t t . Der erste Schritt ist die D a t e n e i n g a b e , die zum Beispiel über eine T a s t a t u r erfolgen kann. Als zweites folgt die eigentliche V e r a r b e i t u n g der Daten im Rechner u n d der dritte Schritt ist die A u s g a b e der Ergebnisse, zum Beispiel auf einen Bildschirm oder einen Drucker.
Wenn wir mit dem Rechner etwas anfangen wollen, müssen wir i h n irgendwie dazu bringen, das zu t u n , was wir von ihm wollen. Dazu müssen wir i h m ein P r o g r a m m geben. Ein P r o g r a m m enthält eine Reihe von A n w e i s u n g e n an den Rechner, die jeden Verarbeitungsschritt genau festlegen. Erst, wenn dem Rechner ein P r o g r a m m als Ganzes vorliegt, kann er es auf Wunsch beliebig oft und m i t verschiedenen Daten ausführen. N e h m e n wir an, wir wollen den Rechner zur Addition zweier beliebiger ganzer Zahlen einsetzen. Die Zahlen sollen ü b e r die T a s t a t u r eingegeben werden, und das Ergebnis soll auf dem Bildschirm erscheinen.
4
2 Grundbegriffe der Programmierung
"Beliebige Zahlen" bedeutet dabei nicht, daß wir uns zwei Zahlen aussuchen können, sondern daß wir beim Schreiben des Programms noch nicht wissen, welche beiden Zahlen später mit dem Rechner addiert werden sollen. Bevor wir ein Programm erstellen können, müssen wir uns überlegen, welche Daten wir dem Rechner geben wollen, was er damit machen soll und was wir als Ergebnis erhalten wollen. Die Eingabedaten für unser Beispiel sind zwei Zahlen, das einzige Ausgabedatum deren Summe. Die eigentliche Verarbeitung der Daten besteht lediglich aus der Addition der beiden Zahlen. Wir wollen nun ein Programm aufschreiben, das die notwendigen Schritte enthält und zunächst in unserer Umgangssprache formuliert ist. Additionsprogramm: 1. Lies die beiden zu addierenden Zahlen von der Tastatur ein ! 2. Addiere die beiden Zahlen! 3. Gib die Summe auf dem Bildschirm aus ! Leider ist der Rechner ziemlich dumm. Wenn wir unser Programm in dieser Form in den Rechner eingeben, wird er sich beharrlich weigern, auch nur im entferntesten zu verstehen, was wir von ihm wollen. Der Rechner versteht nämlich kein Deutsch, sondern nur seine spezielle M a s c h i n e n s p r a c h e . Man könnte nun darangehen, die umgangssprachlichen Anweisungen in die sogenannten M a s c h i n e n b e f e h l e zu übertragen, diese in den Rechner einzugeben und ihn dann ausführen zu lassen (im Bild links). Aber die Maschinensprache kennt nur zwei "Buchstaben" (0 und 1) und läßt sich sehr schlecht aussprechen. Außerdem braucht man für eine Anweisung der Umgangssprache unter Umständen sehr viele Wörter in der Maschinensprache (wegen der wenigen Buchstaben), und schließlich hat fast jeder Rechner seine eigene Maschinensprache, die nur er und kein anderer Rechner versteht. Maschinensprache ist also für den Menschen sehr schwer zu lernen. Mensch M"
R R_
M" M M M M M M_ R" R R R R R R_ Rechner
~M M M M M M _M -R R R R R R _R
2.1 D a t e n , P r o g r a m m e , Programmiersprachen
5
U m die Verständigung zwischen Mensch u n d Rechner zu erleichtern, wurden h ö h e r e P r o g r a m m i e r s p r a c h e n entwickelt. Diese basieren auf der grundsätzlichen Funktionsweise von Rechnern, sind aber nicht auf einen speziellen Rechnertyp abgestimmt. Ihre Anweisungen orientieren sich mehr an der Umgangssprache u n d sind somit leichter zu verstehen u n d zu erlernen. Gegenüber den Anweisungen der Umgangssprache h a b e n die Anweisungen von höheren Programmiersprachen den Vorteil, daß sie nach Form und Bedeutung einer strengen Festlegung genügen, u m ganz präzise formulieren zu können, was m a n vom Rechner will. Bei komplizierteren Problemstellungen ist dies in der Umgangssprache nicht i m m e r möglich. Allerdings sind nun zwei Umsetzungen des P r o g r a m m t e x t e s notwendig (im Bild rechts). Die eine, von der Umgangssprache in die höhere Programmiersprache, f ü h r t der Programmierer aus. Diese Umsetzung wird C o d i e r u n g eines Prog r a m m s genannt, u n d das Ergebnis ist das Q u e l l p r o g r a m m in der jeweiligen P rogrammiersprache. Die noch unvollständig codierte Fassung unseres Beispielprogramms würde in einigen bekannten höheren Programmiersprachen in etwa so aussehen: BASIC:
10 20 30 40
INPUT ZAHL1, ZAHL2 SUMME = ZAHL1 + ZAHL2 PRINT SUMME END
FORTRAN 77:
READ ZAHLl, ZAHL2 SUMME = ZAHL1 + ZAHL2 PRINT *, SUMME END
PASCAL:
BEGIN READ (ZAHL1, ZAHL2); SUMME := ZAHL1 + ZAHL2; WRITELN (SUMME) END.
Die andere Umsetzung, von der höheren Programmiersprache in die Maschinensprache, kann der Rechner übernehmen, wenn ihm ein U b e r s e t z e r (das ist auch ein spezielles P r o g r a m m ) zur Verfügung steht, der die höhere Programmiersprache in seine Maschinensprache übersetzen kann. Bei so einer Übersetzung wird jede Anweisung des P r o g r a m m s in der höheren Programmiersprache i m allgemeinen in mehrere Maschinenbefehle übersetzt. Das entstehende Maschinenprog r a m m wird als Ergebnis des Ubersetzungsvorgangs O b j e k t p r o g r a m m genannt. Das O b j e k t p r o g r a m m kann vom Rechner direkt ausgeführt werden.
6
2 Grundbegriffe der Programmierung
Sehen wir uns den Ablauf der Programmierung noch einmal zusammenfassend an. Ausgangspunkt ist die Problemstellung. Es folgen dann: • Formulierung eines Programmtextes in der Umgangssprache • Codierung des Programms in der höheren Programmiersprache • Eingabe des Quellprogramms in den Rechner • Ubersetzung des Quellprogramms in ein Maschinenprogramm durch den Rechner (genauer: durch das Ubersetzerprogramm im Rechner) • Ausführung des Objektprogramms durch den Rechner (beliebig oft) Wie läuft nun die Ausführung eines Programms ab ? Wir nehmen an, wir hätten die vorstehenden Schritte für unser Beispiel, die Addition zweier Zahlen, durchgeführt und hätten den Rechner bereits dazu veranlaßt, die Ausführung unseres Programms zu starten. Der Rechner führt die erste Anweisung aus, die ihm sagt, daß er zwei Zahlen von der Tastatur einlesen soll. Er wartet so lange, bis wir tatsächlich zwei Zahlen über die Tastatur eingeben, zum Beispiel 3 und 5, getrennt durch einen Zwischenraum. Erst, wenn der Rechner unsere Eingabe erhalten hat, fährt er mit der Ausführung des Programms fort und addiert die beiden Zahlen gemäß der nächsten Anweisung. Mit der dritten Anweisung erfolgt die Ausgabe der Summe auf dem Bildschirm, in diesem Fall 8. Danach wird das Programm beendet. Wir können das Programm noch mehrfach anstarten, d.h., die Ausführung des Objektprogramms veranlassen. Jedesmal können andere Daten eingegeben werden. In einem zweiten Programmlauf würde zum Beispiel die Eingabe der Zahlen 789 und 310 zu dem Ergebnis 1099 auf dem Bildschirm führen.
Kontrollaufgaben K.2.1
Zwei Zahlen sollen addiert werden, und die Summe soll mit einer dritten multipliziert werden. Die Zahlen sollen über die Tastatur eingegeben werden, und das Ergebnis soll auf dem Bildschirm erscheinen. Schreiben Sie ein Programm in der Umgangssprache !
K.2.2
Wie unterscheiden sich Maschinensprachen von höheren Programmiersprachen? Wo liegen die Vorteile der höheren Programmiersprachen?
K.2.3
Nennen Sie ein paar höhere Programmiersprachen!
K.2.4
Was versteht man unter der Codierung eines Programms ?
K.2.5
Was ist ein Quellprogramm und was ein Objektprogramm ?
K.2.6
Wer übersetzt ein Quellprogramm in ein Objektprogramm ?
7
2.2 Vom Problem zum Programm
2.2
Vom Problem zum Programm
Im vorhergehenden Abschnitt wurde der Ablauf der Programmierung in einer höheren Programmiersprache schematisch beschrieben. Dabei war für das einfache Beispiel "Additionsprogramm" die Formulierung eines Programmtextes in der Umgangssprache anschaulich klar und ausgehend von der Problemstellung ohne Zwischenschritte möglich. Im allgemeinen ist dieser Schritt, nämlich der eigentliche Programmentwurf, jedoch wesentlich aufwendiger und kann als Kernstück jeder Programmentwicklung angesehen werden. Die Vorgehensweise beim Programmentwurf ist Gegenstand dieses Abschnitts. Graphische Darstellungen der Programmabläufe und -strukturen sind nützliche Hilfsmittel und werden kurz erläutert. Bevor wir auf den Programmentwurf eingehen, wollen wir diesen Vorgang nochmals in den Gesamtablauf der Programmierung einordnen: Problemstellung Programmentwurf Programmtext in der Umgangssprache (evtl. mit graphischer Darstellung) Codierung Quellprogramm in der höheren Programmiersprache Eine Problemstellung der Datenverarbeitung ist gegeben durch die Beschreibung der in das Problem eingehenden Daten und der gewünschten Ergebnisse. Der Programmtext muß für das jeweilige Problem einen Lösungsweg darstellen, der eindeutig festlegt, wie die Verarbeitung zu erfolgen hat, um aus den eingehenden Daten die gewünschten Ergebnisse zu erhalten. Ein solcher Lösungsweg wird A l g o r i t h m u s genannt. Der Lösungsweg für das Problem, aus zwei Zahlen die Summe zu ermitteln, besteht, wie wir gesehen haben, einfach darin, die beiden Zahlen zu addieren. Als Beispiel für einen etwas umfangreicheren Lösungsweg wollen wir folgendes Problem betrachten. Es seien drei Zahlen gegeben, und es soll der größte Abstand zwischen j e zwei dieser Zahlen bestimmt werden. Ein- und Ausgabe sollen über das Bildschirmgerät erfolgen.
8
2 Grundbegriffe der Programmierung
Der folgende Programmtext stellt einen möglichen Algorithmus zur Lösung dieser Aufgabe mit dem Rechner dar: 1. Lies die drei Zahlen über die Tastatur ein ! 2. Bestimme die größte der drei Zahlen ! 3. Bestimme die kleinste der drei Zahlen ! 4. E r m i t t l e den gesuchten Abstand durch Subtraktion der kleinsten von der größten Zahl ! 5. Gib den Abstand als Ergebnis aus ! Jede der Anweisungen 1. bis 5. stellt ein Teilproblem dar, das unter Umständen wieder durch einen Algorithmus gelöst werden muß. Die Anweisungen müssen so lange "verfeinert" werden, bis sie in der gewählten Programmiersprache codiert werden können. Für die Programmiersprache F O R T R A N wäre der Programmentwurf für das Beispiel bereits abgeschlossen, denn in F O R T R A N kann man Anweisungen formulieren, die das Maximum bzw. Minimum von drei Zahlen bestimmen. Das Programm ließe sich in F O R T R A N direkt codieren. Die Anweisungen 2. und 3. müßten hingegen für eine Programmierung in Pascal weiter zerlegt werden, denn es gibt in dieser Sprache keine Anweisungen mit entsprechender Bedeutung wie in F O R T R A N . Die größte und die kleinste Zahl könnten stattdessen zum Beispiel folgendermaßen gefunden werden: 2a. Bestimme die größere von der ersten und der zweiten Zahl ! 2b. Bestimme die größte Zahl als Maximum der größeren der beiden und der dritten Zahl ! 3a. Bestimme die kleinere von der ersten und der zweiten Zahl ! 3b. Bestimme die kleinste Zahl als Minimum der kleineren der beiden und der dritten Zahl ! Für diese Anweisungen gibt es entsprechende in Pascal, so daß der Programmentwurf hiermit fertig ist und das Programm codiert werden kann. Im allgemeinen empfiehlt es sich, den Lösungsweg zunächst möglichst grob aufzuschreiben und eine s c h r i t t w e i s e V e r f e i n e r u n g so lange wie beschrieben durchzuführen, bis jedes Teilproblem einer Anweisung oder einer kurzen Anweisungsfolge in der zu verwendenden höheren Programmiersprache entspricht. Neben der Herleitung eines eindeutigen Lösungsweges muß beim Programmentwurf festgelegt werden, welche Daten verarbeitet werden sollen. Dazu gehören nicht nur die Ein- und Ausgabedaten, sondern auch jegliche Zwischenergebnisse.
2.2 Vom Problem z u m P r o g r a m m
9
Für das obige Beispiel ergibt sich als Aufstellung der zu verarbeitenden Daten folgendes: • • • •
drei Zahlen die größte Zahl die kleinste Zahl der Abstand
(Eingabe) (Zwischenergebnis) (Zwischenergebnis) (Endergebnis bzw. Ausgabe)
Je nach verwendeter Programmiersprache kann oder muß diese Aufstellung ebenfalls codiert und zum Lösungsweg hinzugefügt werden. Mit Hilfe graphischer Darstellungen kann man Programme übersichtlicher darstellen und sich dadurch die Entwurfsarbeit erleichtern. Gebräuchliche Darstellungsformen sind P r o g r a m m a b l a u f p l ä n e (Flußdiagramme) und S t r u k t o g r a m m e (Nassi-Shneiderman-Diagramme). Struktogramme sind Blockdiagramme, die die Struktur der Programme hervorheben, aber auch den Programmablauf erkennen lassen. Mögliche Strukturen sind neben der rein sequentiellen Abarbeitung der Anweisungen eines Programms zum Beispiel die mehrfache Wiederholung von Anweisungen oder eine Verzweigung des Programmablaufs unter einer bestimmten Bedingung. Auf solche Programmstrukturen werden wir in Lektion 9 näher eingehen. Mit Programmablaufplänen lassen sich alle denkbaren Strukturen darstellen, mit Struktogrammen hingegen nur einige bestimmte. Um die Übersichtlichkeit, Anderbarkeit und Zuverlässigkeit von Programmen zu verbessern, sollte man nicht beliebige Programmstrukturen verwenden, sondern nur bestimmte, nämlich genau die durch Struktogramme darstellbaren Strukturen. Durch die Beschränkung auf diese Strukturen befolgen wir die Regeln der S t r u k t u r i e r t e n Programmierung. Die Bedeutung der verschiedenen Sinnbilder bei Struktogrammen werden Sie im Laufe der Fibel kennenlernen. Sie kann auch im Anhang A nachgeschlagen werden. Die äußere Form jeden Sinnbildes ist ein Rechteck. Die obere Kante des Rechtecks bedeutet den Beginn der Verarbeitung, die untere Kante das Ende der Verarbeitung. Das einfachste Sinnbild sieht demnach so aus: Verarbeitung
Die Sinnbilder können ineinander verschachtelt werden. Schachtelt man beispielsweise mehrere Sinnbilder des Typs "Verarbeitung" in ein großes Sinnbild dieses Typs, so erhält man eine (Verarbeitungs-) "Folge". Als einfaches Beispiel für eine Folge von Verarbeitungsschritten wollen wir uns hier das Struktogramm zu unserem Beispielalgorithmus ansehen:
10
2 Grundbegriffe der Programmierung
maximaler Abstand drei Zahlen einlesen die größte Zahl bestimmen die kleinste Zahl bestimmen den Abstand durch Differenzbildung bestimmen den Abstand ausgeben Eine Verfeinerung kann nun einfach vorgenommen werden, indem man ein neues Struktogramm zeichnet und es mit einem entsprechenden Titel versieht: die größte Zahl bestimmen die größere von den ersten beiden Zahlen bestimmen die größere von der soeben gewählten und der dritten Zahl bestimmen Entsprechend müßte das Struktogramm für die Bestimmung der kleinsten Zahl aussehen. Man kann die Verfeinerung natürlich auch gleich in den Grobentwurf des Algorithmus eintragen, sofern dort noch ausreichend Platz ist.
Kontrollaufgaben K.2.7
Welche Forderung muß ein Lösungsweg für eine Problemstellung erfüllen ?
K.2.8
Wie sollte man bei der Herleitung eines Lösungsweges, der programmiert werden soll, verfahren ?
K.2.9
Was gehört neben der Herleitung des Lösungsweges noch zum Programmentwurf ?
K.2.10
Zeichnen Sie ein einfaches Struktogramm, das beschreibt, was Sie tun, wenn Sie mit Ihrem Auto fahren wollen. Es soll beginnen mit "Autotür aufschließen" und enden mit "Motor starten". Bitte vergessen Sie nicht, den Sicherheitsgurt anzulegen!
11
Lektion 3
Programmierumgebung Unter einer Programmierumgebung ist im weitesten Sinne alles zu verstehen, was man benötigt, u m eigene Programme entwickeln und testen zu können. Natürlich gehört dazu vor allem ein Rechner mit bestimmten Gerätekomponenten: die sogenannte H a r d w a r e . Dazu gehören aber auch etliche Programme, die zur G r u n d s o f t w a r e des Rechners zählen und die eine Kommunikation mit dem Rechner überhaupt erst möglich machen.
Hardware Als wesentlich für die Programmierumgebung sind die abgebildeten Teile eines Rechners anzusehen: Drucker
Bildschirm und Tastatur
Zentraleinheit
Arbeitsspeicher
Die Abbildung gibt nur den funktionalen Zusammenhang zwischen den betreffenden Komponenten wieder und stellt nicht unbedingt ihre räumliche Anordnung dar. So sind beispielsweise bei einem Arbeitsplatzrechner (engl.: Personal Computer, P C ) oft Zentraleinheit mit Arbeitsspeicher und externem Speicher in einem Gehäuse der Größe eines Aktenkoffers integriert, während bei einer Großrechenanlage die entsprechenden Komponenten auch heute noch mehrere Schränke füllen.
12
3 Programmierumgebung
Bildschirm, Tastatur und Drucker sind Ein-/Ausgabegeräte, über die wir mit dem Rechner kommunizieren können. Die Zentraleinheit (engl.: central processing unit, CPU) ist der eigentliche Kern des Rechners. Hier werden Maschinenprogramme interpretiert und ausgeführt. Weil der Arbeitsspeicher flüchtig ist, d.h., alle eingegebenen Daten, insbesondere Programmtexte, nach dem Abschalten des Rechners verloren gehen, benötigt man externe Speicher, die Informationen auf Dauer speichern können. Externe Speicher sind zum Beispiel Festplatten (engl.: hard disk) oder Disketten (engl.: floppy disk).
Grundsoftware Zu den Programmen, die mit dem Rechner bereits mitgeliefert werden und die für den Betrieb des Rechners unerläßlich sind, gehört das Betriebssystem. Dieses Programm übernimmt "Verwaltungsaufgaben", die beim Betrieb des Rechners intern anfallen. Dazu gehört insbesondere die Koordinierung und Durchführung sämtlicher Ein-/Ausgabevorgänge. Nach dem Einschalten des Rechners bzw. nach der Anmeldung zu einer Bildschirmsitzung, wie es bei Rechnern mit Mehrbenutzerbetrieb heißt, befinden wir uns normalerweise in einem Kommandomodus. Damit kann ebenso ein zeilenweise geführter Dialog gemeint sein wie auch eine graphische Benutzeroberfläche mit Fenstertechnik, Menüs und Maus. Die auf dieser Ebene zur Verfügung stehenden Kommandos zur Ein-/Ausgabe, Verwaltung, Ubersetzung und Ausführung von eigenen Programmen können als Programmierumgebung im engeren Sinne aufgefaßt werden. Aus Gründen der Übersichtlichkeit sind die Daten auf den externen Speichern zu Dateien zusammengefaßt. Eine Datei kann zum Beispiel enthalten: • • • •
einen Programmtext in einer höheren Programmiersprache einen beliebigen anderen Text Zahlen ein Maschinenprogramm
Dateien tragen einen Namen, unter dem wir sie in Kommandos ansprechen können. Das Betriebssystem führt auf dem externen Speicher ein Dateiverzeichnis, in das alle Dateien namentlich eingetragen werden. Hinter allen Kommandos stecken Maschinenprogramme, die zur Ausführung gebracht werden. Herausragende Bedeutung haben zwei, nämlich der Editor zur Eingabe und Änderung von Programmtexten und der Ubersetzer zum Erzeugen von Maschinenprogrammen aus Quellprogrammen. Da sich die Namen der Kommandos und ihre Verwendung von Rechner zu Rechner sehr stark unterscheiden, können wir hier nur die allgemeine Bedeutung der Kommandos erläutern. Ziehen Sie daher parallel dazu die Unterlagen des Ihnen zur Verfügung stehenden Rechners heran.
13
Kommandos zur Dateiverwaltung Die hier aufgeführten Kommandos sind die wichtigsten Kommandos zur Dateiverwaltung. Sie erleichtern Ihnen die Arbeit und helfen, den Uberblick über Dateien und Programme zu bewahren. • • • • • •
Ausgabe des Dateiverzeichnisses Ausgabe einer Datei auf den Bildschirm Ausgabe einer Datei auf den Drucker Kopieren einer Datei Löschen einer Datei Umbenennen einer Datei
Editor Mit dem (Text-)Editor können wir einen beliebigen Text (z.B. einen Programmtext) in den Rechner eingeben und dafür sorgen, daß dieser Text in einer (Text-) Datei abgelegt wird. Der Inhalt einer vorhandenen Datei kann mit dem Editor verändert werden. Man spricht auch von der Tätigkeit des "Edierens".
Pascal-Übersetzer Der Ubersetzer erzeugt aus einem Pascal-Quellprogramm ein ablauffähiges Maschinenprogramm. Er muß dazu intern den Quellprogrammtext in mehreren Schritten verarbeiten. Auf den Vorgang des Ubersetzens kommen wir in Lektion 6 zurück.
Ausführung eigener Programme Ein mit dem Übersetzer erzeugtes Maschinenprogramm können Sie ausführen lassen. Als Kommando reicht oft der Programmname aus; bei manchen Systemen gibt es auch ein spezielles Kommando zum Starten eines eigenen Programms.
Pascal-Systeme Normalerweise geben Sie alle Kommandos zur Dat ei Verwaltung, zum Edieren, zum Ubersetzen und zum Ausführen eines Programms auf der Kommandoebene des Betriebssystems. Für Pascal gibt es aber auch sogenannte Pascal-Systeme, wie zum Beispiel Turbo-Pascal oder PolyPascal. Dabei handelt es sich um Programmentwicklungssysteme, in die ein Editor, ein Übersetzer, ein sogenanntes Laufzeitsystem zum Ausführen der Programme und einige Kommandos zur Dateiverwaltung integriert sind. Wenn Sie mit einem solchen System arbeiten, dann rufen Sie auf der Kommandoebene des Betriebssystems das Pascal-System auf. Weitere Kommandos geben Sie dann auf der Kommandoebene des PascalSystems! Mit einem bestimmten Kommando können Sie dann auf die Ebene des Betriebssystems zurückkehren.
14
3 Progiammierumgebung
Kontrollaufgabeii K.3.1
Besorgen Sie sich Informationen über Ihren Rechner, Ihr Betriebssystem und eventuell Ihr Pascal-System! Machen Sie sich mit den wichtigsten Kommandos zur Dateiverwaltung und dem Editor vertraut! (Mit dem Ubersetzen und Ausführen von Programmen befassen wir uns erst in Lektion 6.)
K.3.2
Erzeugen Sie eine Datei mit dem Namen h a l l o , die einen kurzen Text enthält!
K.3.3
Geben Sie die Datei h a l l o auf den Drucker aus!
K.3.4
Kopieren Sie die Datei h a l l o auf eine Datei mit dem Namen huhu!
K.3.5
Kontrollieren Sie, ob die Dateien h a l l o und huhu existieren, indem Sie sich das Dateiverzeichnis ansehen!
K.3.6
Benennen Sie die Datei h a l l o um in h o l l a !
K.3.7
Löschen Sie die Datei huhu!
Übungsaufgabe:
Bitte bearbeiten Sie jetzt die Übungsaufgabe 1!
15
Lektion 4
Elementare Bestandteile eines P r o g r a m m s In dieser Lektion werden Sie die Grundelemente kennenlernen, aus denen PascalProgramme zusammengesetzt werden. Sehen wir uns zunächst ein Beispiel an: program s t d p r o j a h r begin vrite(365*24,' end.
(output); Stunden hat e i n J a h r ' )
Dieses Programm präsentiert uns auf dem Bildschirm die hochinteressante Information: 8760 Stunden hat e i n Jahr. Es ist zwar ein sehr kurzes Programm, aber dennoch lassen sich bereits einige grundsätzliche Eigenschaften von PascalProgrammen erkennen. Ein Programm besteht aus einer Folge von S y m b o l e n . Die wichtigsten PascalSymbole sind: -
Zeichenketten, Zahlen, Spezialsymbole, Bezeichnen
Zeichenketten Zeichenketten sind Folgen von Zeichen, die links und rechts von Apostrophen ' ' eingeschlossen sind. Innerhalb von Zeichenketten können alle auf einem Rechner verfügbaren Zeichen vorkommen. Sollte ein Apostroph so übermütig sein und selbst Teil einer Zeichenkette sein wollen, so muß es sich zweimal hintereinander in die Kette einreihen. Beispiele:
'Dies i s t eine
Zeichenkette.'
' 1 . 0 0 0 . 0 0 0 . — $' '0123456789' 'That''s i t ! '
16
4 Elementare Bestandteile eines Programms
Zahlen Bei den Zahlen müssen wir die ganzen Zahlen und die "reellen" Zahlen unterscheiden. Die ganzen Zahlen werden in der üblichen Weise mit oder ohne Vorzeichen dargestellt: 45
+777
-28375
(auch 007 ist zulässig)
Der Wertebereich der ganzen Zahlen ist in Abhängigkeit vom verwendeten Rechner und Ubersetzer unterschiedlich. Die üblichsten Wertebereiche sind -32768 ... +32767 und -2147483648 ... +2147483647. Die reellen Zahlen können entweder in der Festpunktschreibweise 3.14159
0.00125
-2345678.0
+17.5
oder in der Gleitpunktschreibweise le-12
123.0E+9
-45.678e7
+21E6
dargestellt werden, l e - 1 2 bedeutet dann also "1 mal 10 hoch -12". Wichtig ist auch, daß vor und nach dem Dezimalpunkt (kein Komma!) mindestens eine Ziffer stehen muß, gegebenenfalls eine Null. Der zulässige Wertebereich ist auch für die reellen Zahlen rechner- bzw. übersetzerabhängig (vgl. Lektion 11).
Spezialsymbole Die in Pascal zulässigen Spezialsymbole sind:
(
)
[
]
*
Ebenfalls zu den Spezialsymbolen gehören die sogenannten Wortsymbole. In Pascal gibt es die folgenden 35 Wortsymbole (auch r e s e r v i e r t e W ö r t e r genannt) : and array begin case const div do
downto else end file for function goto
if in label mod nil not of
or packed procedure program record repeat set
then to type until var while with
Neben diesen reservierten Wörtern aus Standard-Pascal sind in den verschiedenen Pascal-Versionen noch ein paar weitere Wörter reserviert. In Ihrem Handbuch finden Sie eine Liste der reservierten Wörter. Die Bedeutung der einzelnen Spezialsymbole wird im weiteren Verlauf der Fibel bei passender Gelegenheit erläutert werden.
17
Bezeichner Bezeichner werden verwendet, um bestimmten Objekten Namen zu geben. Solche Namen können Sie frei Ihrer Phantasie entspringen lassen, solange sie nur aus Buchstaben (a-z, A-Z) und Ziffern (0-9) bestehen und mit einem Buchstaben beginnen. Bezeichner dürfen beliebig lang sein. Unser Beispielprogramm hat den Namen stdprojahr bekommen. Es hätte auch spinat heißen können, aber erstens ist Spinat nicht jedermanns Sache und zweitens sollten Namen so gewählt werden, daß sie etwas über die Bedeutung des benannten Objekts aussagen. Natürlich dürfen Sie keine Wortsymbole als Bezeichner verwenden. Sollten Sie versehentlich doch eines dieser reservierten Wörter als Bezeichner mißbrauchen wollen, wird Sie ihr Pascal-Übersetzer nicht besonders höflich, aber direkt auf Ihren Irrtum hinweisen. Weiterhin gibt es eine Reihe von vordefinierten Bezeichnern (Standardbezeichner), denen von vornherein eine bestimmte Bedeutung zugewiesen ist. Beispielsweise kann unter dem Namen maxint der größte darstellbare ganzzahlige Wert angesprochen werden. Im Gegensatz zu den reservierten Wörtern können Sie derartige Namen in Ihrem Programm mit einer neuen Bedeutung versehen, wobei die vordefinierte Bedeutung natürlich verlorengeht. Eine solche Umdefinition scheint nicht sonderlich sinnvoll, es sei denn, Sie möchten sich und andere bei einem späteren Lesen des Programms ein wenig in Verwirrung stürzen. Einige Beispiele für Pascal-Bezeichner: gültige:
Summe VERSI0N123 xl kubikwurzel
ungültige: Std pro Jahr Ping-Pong lOOstel file
(enthält Leerzeichen) (enthält Spezialzeichen) (beginnt nicht mit Buchstaben) (reserviertes Wort)
Groß- und Kleinbuchstaben werden in Pascal nicht unterschieden. ChopSuey und chopsuey sind also derselbe Name.
T r e n n u n g der e l e m e n t a r e n B e s t a n d t e i l e Schließlich ist da noch etwas, was uns an unserem Beispielprogramm hätte auffallen können. Es ist die Z e l l e n s t r u k t u r des Programms. Diese Zeilenstruktur hat auf Pascal-Programme prinzipiell keinen Einfluß. Man könnte das ganze Programm auch in eine Zeile schreiben (vorausgesetzt sie ist lang genug) oder es irgendwie anders auf mehrere Zeilen verteilen. Man sollte die Darstellung eines Programms aber immer so wählen, daß die Lesbarkeit und Übersichtlichkeit des Programms erhöht wird. Wir werden darauf später noch eingehen.
18
4 Elementare Bestandteile eines Programms
Im übrigen kann man sowieso nicht an jeder beliebigen Stelle im Programmtext von einer Zeile in die nächste übergehen. Zeilenenden haben nämlich in Pascal die Bedeutung von Trennzeichen. Weitere Trennzeichen sind Leerstellen und K o m m e n t a r e . Ein Kommentar ist eine beliebige Folge von Zeichen, eingeschlossen in { }•, und dient zur Erläuterung des Programmtextes. Kommentare können sich über mehrere Zeilen erstrecken. Die Bedeutung des Programms wird durch die Kommentare nicht beeinflußt. { D i e s i s t e i n K o m m e n t a r , der d a z u d i e n t , z u e r l a e u t e r n , w a s e i n K o m m e n t a r ist! }
Trennzeichen dürfen niemals innerhalb eines Pascal-Symbols erscheinen, abgesehen von einer Ausnahme, die die Regel bestätigt: in Zeichenketten darf bis auf Zeilenenden sonst alles vorkommen. Zwischen zwei aufeinanderfolgenden PascalSymbolen können beliebig viele Trennzeichen stehen. Pascal-Symbole können auch ohne Trennzeichen direkt hintereinander geschrieben werden, solange sich ihre Bedeutung dadurch nicht verändert, weil sie zu einem Symbol verschmelzen, wie zum Beispiel die beiden Wortsymbole begin und write zum Bezeichner beginvrite.
K o nt r o llaufgab e n K.4.1
Was ist eine Zeichenkette? Welche Zeichen kann eine Zeichenkette enthalten? Wie kann ein Apostroph in eine Zeichenkette aufgenommen werden?
K.4.2
Welche Regeln gelten für die Schreibweise von ganzen Zahlen in Pascal?
K.4.3
Welche Möglichkeiten gibt es für die Schreibweise von reellen Zahlen in Pascal?
K.4.4
Geben Sie an, ob es sich um gültige Zahlen oder Zeichenketten handelt: 'Paris' 12858 -3.1783465e-ll -927.
.5 270.23E7
2.4E0.5 '45,8 7,' 'Wie g e h t ' s '
4,5
37'/. -83.3A8 J Ì > 1
2.75 e 3
K.4.5
Was sind reservierte Wörter (oder Wortsymbole)?
K.4.6
Was ist ein Bezeichner? Wie muß er aufgebaut sein?
K.4.7
Was sind Standardbezeichner? Wie unterscheiden sie sich von den reservierten Wörtern?
K.4.8
Wie kann der größte im Rechner darstellbare ganzzahlige Wert in Pascal angesprochen werden?
19
K.4.9
Entscheiden Sie, ob es sich um gültige Bezeichner handelt: zahll 1877 xx-3 type 3f ach.
K.4.10
HALLO I H O C H DREI azumquadrat %satz H1P2
Welche Bedeutung haben Zeilenenden, Leerzeichen und Kommentare in Pascal-Programmen?
20
Lektion 5
A u f b a u eines P r o g r a m m s Das folgende Beispiel ist eine vollständige, kommentierte Fassung des Additionsprogramms aus Lektion 2:
r
Programm zur Addition zweier ganzer Zahlen > { ===== =============================== program addition
(input.output);
-( *** Vereinbarungsteil #******************* ]• var zahll, zahl2, summe: integer;
V
{ *** Anweisungsteil *********************** }• begin readln (zahll, zahl2); { einlesen } summe := zahll + zahl2; { addieren } writeln (summe) i ausgeben }• end.
Jedes Pascal-Programm besteht aus Programmkopf und Programmblock. Programmkopf und Programmblock werden durch ein Semikolon voneinander getrennt. Am Ende des Programms muß ein Punkt stehen. Der P r o g r a m m k o p f besteht aus dem Wortsymbol program, gefolgt von einem frei wählbaren Bezeichner, der dem Programm einen Namen gibt. Zusätzlich werden die Parameter genannt, über die das Programm mit der "Außenwelt" kommuniziert. Darauf werden wir später genauer eingehen. Der P r o g r a m m b l o c k ist unterteilt in Vereinbarungsteil und Anweisungsteil. Im Vereinbarungsteil können Namen für verschiedene Objekte vereinbart werden, zum Beispiel für die Daten, die mit dem Programm verarbeitet werden sollen. Der A n w e i s u n g s t e i l enthält die Schritte des Algorithmus, der zur Lösung eines Problems entwickelt wurde. Diese Algorithmusschritte werden A n w e i s u n g e n genannt. Der Anweisungsteil beginnt mit dem Wortsymbol begin und endet mit dem Wortsymbol end. Durch ein Semikolon wird eine Anweisung von der nächsten getrennt. Hinter der letzten Anweisung braucht kein Semikolon zu stehen, da keine weitere Anweisung folgt.
21
Kontrollaufgaben K.5.1
Aus welchen beiden Hauptteilen besteht ein Pascal-Programm?
K.5.2
In welche beiden Teile läßt sich der Programmblock unterteilen? Wozu dient jeder dieser Teile?
K.5.3
Welche Funktion hat das Semikolon in Pascal?
K.5.4
Welches Zeichen muß am Ende eines Pascal-Programms stehen?
22
Lektion 6
Ubersetzen und Ausführen eines Programms Die Bestandteile u n d den A u f b a u eines Pascal-Programms haben Sie kennengelernt. Ehe wir auf die Einzelheiten der Programmierung eingehen, wollen wir ein einfaches P r o g r a m m übersetzen und ausführen lassen. Dazu eignet sich wieder unser Additionsprogramm, u n d zwar in der in Lektion 5 dargestellten Form. Lesen Sie die folgenden Abschnitte A bis E in der jeweils angegebenen Reihenfolge. F ü h r e n Sie möglichst parallel dazu die beschriebenen Aktionen an Ihrem Rechner durch.
A . Eingabe des Programmtextes: Geben Sie den P r o g r a m m t e x t des Additionsprogramms aus Lektion 5 mit Hilfe Ihres Text-Editors in den Rechner ein. Sichern Sie das Quellprogramm, z u m Beispiel, indem Sie es auf die Festplatte in ihr Dateiverzeichnis schreiben. Der Dateiname, unter dem Sie den Programmtext abspeichern, braucht nicht mit d e m i m Programmkopf angegebenen P r o g r a m m n a m e n übereinzustimmen, kann es aber. Uberprüfen Sie zum Schluß noch einmal, ob Sie sich auch nicht vertippt haben.
B. Übersetzen des Programms: Damit der Rechner einen Programmtext übersetzen kann, gibt es ein P r o g r a m m , das d e m Rechner die dafür notwendigen Anweisungen gibt. Dieses P r o g r a m m heißt Ubersetzer (engl.: Compiler). Es verwendet als Eingabedaten den Prog r a m m t e x t und erzeugt als Ausgabe ein ablauffähiges Maschinenprogramm. Informieren Sie sich, wie Sie Ihren Ubersetzer aufrufen können. Normalerweise muß m a n dem Übersetzer beim Aufruf sagen, welches P r o g r a m m er übersetzen soll. Bei sogenannten P a s c a l - S y s t e m e n hingegen, wie z. B. Turbo-Pascal oder PolyPascal, übersetzt der Übersetzer das Programm, das sich gerade i m Arbeitsspeicher befindet. Wenn Sie mit einem solchen S y s t e m arbeiten und das P r o g r a m m , das Sie übersetzen wollen, nicht mehr im Arbeitsspeicher ist, müssen Sie es neu laden. Nun können Sie den Ubersetzer starten. Auf dem Bildschirm erscheinen nun irgendwelche Meldungen des Übersetzers. Fehlermeldungen erkennt m a n d a r a n , daß darin die Worte "Fehler" oder "error" vorkommen. Sollten Fehlermeldungen nicht auftreten, können Sie mit der Ausführung des P r o g r a m m s wie unter D beschrieben fortfahren. Andernfalls müssen Sie eine Korrektur des P r o g r a m m t e x t e s vornehmen, wie in Abschnitt C beschrieben.
23
C. Fehler beim Übersetzen: Werden bei der Übersetzung eines Programms Fehler gemeldet, gibt es dafür zwei mögliche Gründe: 1. Der Programmtext ist falsch, d.h., die Regeln der Programmiersprache wurden vom Programmierer nicht befolgt. Der Ubersetzer bemerkt diese Fehler und gibt eine entsprechende Meldung aus. 2. Der Programmtext ist richtig, aber der Übersetzer arbeitet fehlerhaft. Diese Möglichkeit ist nicht ganz auszuschließen, sollte aber nur zuallerletzt in Betracht gezogen werden. Versuchen Sie, die ausgegebenen Fehlermeldungen zu deuten, den Fehler im Programmtext zu finden und ihn zu korrigieren. Lesen Sie die Meldungen dazu durch, und machen Sie sich nötigenfalls ein paar Notizen. Rufen Sie dann wieder den Editor auf, u m den Programmtext zu bearbeiten. Systeme wie Turbo-Pascal oder PolyPascal bieten hier den Komfort, daß man bei Aufruf des Editors automatisch gleich an die Fehlerstelle gelangt. Das ist nicht allgemein üblich. Sind Sie der Meinung, den Fehler behoben zu haben, müssen Sie das Programm erneut übersetzen lassen. Lesen und arbeiten Sie also bei B weiter !
D. Ausführung des Programms: Wenn der Ubersetzer keine Fehler im Programmtext bemerkt, erzeugt er ein ablauffähiges Maschinenprogramm, das dem Pascal-Programmtext entspricht. Starten Sie das Programm. Dies geschieht normalerweise, indem man den Namen der Datei, die das ablauffähige Programm enthält, als Kommando benutzt. Bei den Pascal-Systemen startet man Programme, die sich im Arbeitsspeicher befinden, im allgemeinen einfach durch das Kommando RUN. Im Falle des Additionsprogramms wird nun nichts weiter passieren, als daß die Schreibmarke an den Anfang der nächsten Zeile geht. Der Rechner wartet auf die Eingabe der beiden zu addierenden Zahlen. Geben Sie zwei ganze Zahlen getrennt durch eine Leerstelle ein und schließen Sie die Eingabe mit der Eingabetaste ab. Der Rechner fährt dann mit der Abarbeitung des Programms fort, indem er die Summe berechnet und auf dem Bildschirm ausgibt. Nach dem Programmende erscheint wieder das Aufforderungszeichen für die Eingabe des nächsten Kommandos. Sie können das Programm nun erneut starten. Sollte es während der Ausführung des Programms zu Fehlern kommen, müssen Sie Abschnitt E lesen.
E. Fehler bei der Ausführung: Auch während der Ausführung des Programms kann es zu Fehlern kommen, die entweder zu falschen Ergebnissen, d.h. Ausgaben des Programms, führen oder die vom Rechner entdeckt werden und entsprechende Meldungen auf dem Bildschirm zur Folge haben. Ursachen können sein:
24
6 Übersetzen und Ausführen eines Programms
1. Das Programm enthält einen Fehler, den der Übersetzer nicht bemerken konnte. Der Rechner führt die fehlerhaften Anweisungen aus. Bemerkt er dabei den Fehler, so gibt er eine Fehlermeldung aus. In manchen Fällen hört er auch ganz auf zu arbeiten. Man sagt dann, er sei "abgestürzt". 2. Das Programm ist richtig, aber es ist vom Ubersetzer fehlerhaft bearbeitet worden. Die Reaktion des Rechners ist die gleiche wie bei 1. 3. Das Programm ist richtig und ist auch fehlerfrei übersetzt worden, aber der Rechner arbeitet fehlerhaft. Dies ist auf Fehler im Betriebssystem oder in der Hardware des Rechners zurückzuführen. Fehler, die unter Punkt 1 fallen, sind vom Programmierer zu untersuchen und im Programmtext zu korrigieren. Unter Umständen muß sogar der gesamte Programmentwurf revidiert werden. In jedem Fall muß anschließend der Programmtext wieder übersetzt werden (Abschnitt B ) . Fehler im Ubersetzer (2.) und im Rechner (3.) sollten nicht vorkommen bzw. sind sehr selten. Gibt es solche Fehler bei dem verwendeten System dennoch, werden Sie (hoffentlich) durch systemspezifische Unterlagen darüber informiert, wie sie zu beseitigen oder zu umgehen sind. Falls die Übersetzung des Beispielprogramms auf Anhieb fehlerfrei verlief, bauen Sie bitte absichtlich einen Fehler in Ihr Programm ein, und durchlaufen Sie die Punkte ab Abschnitt B nochmals. Experimentieren Sie mit der Zeilenstruktur, den Leerstellen und den Kommentaren. Zu Fehlern während der Ausführung des Programms kann es trotz korrektem Programm und fehlerfreier Ubersetzung auch kommen, wenn der Rechner auf unerlaubte Operationen stößt, wie zum Beispiel die Division durch Null oder die Bildung einer Quadratwurzel aus einer negativen Zahl. Schreiben Sie zum Kennenlernen der Reaktion des Rechners in solchen Fällen unser Additionsprogramm um in ein Divisionsprogramm. Dazu brauchen Sie lediglich das Spezialsymbol " + " , das in der Anweisung summe = z a h l l + z a h l 2 für die Addition steht, durch das Wortsymbol " d i v " zu ersetzen (div führt eine ganzzahlige Division durch, siehe Lektion 8). Lassen Sie das Programm übersetzen und ausführen, und geben Sie die beiden Zahlen 3 und 0 ein. Die erscheinende Fehlermeldung bedeutet, daß eine Division durch Null nicht möglich ist und daß das Programm daher abgebrochen wird.
Syntax und Semantik Die Regeln, die besagen, wie ein Programm rein formal auszusehen hat, nennt man S y n t a x der Programmiersprache. Geht es um den Inhalt, die Bedeutung des Programms, spricht man von der S e m a n t i k des Programms. Zur Verdeutlichung ein Beispiel aus der Umgangssprache: Der Satz "Farblose grüne Ideen schlafen wild." stammt im englischen Original von Noam Chomsky und ist ein typisches Beispiel für einen zwar syntaktisch korrekten Satz, der aber semantisch keinen Sinn ergibt.
25
Ein ähnliches Beispiel ist die folgende Anweisung: Schreibe den dreißigsten Buchstaben des Alphabets! Nicht immer sind semantische Unstimmigkeiten so klar zu erkennen: Denke Dir eine Zahl von 1 bis 30 aus, bezeichne diese Zahl mit n, schreibe den n-ten Buchstaben des Alphabets! Ob die Unstimmigkeit überhaupt auftritt, hängt hier vom Ergebnis eines vorausgehenden Algorithmusschritts ab. Beim Ubersetzen und Ausführen eines Programms muß der Rechner drei Schritte durchlaufen. Er muß 1. die Symbole, die für die Formulierung des Algorithmus verwendet wurden, verstehen, 2. jeden Algorithmusschritt entsprechend seiner Bedeutung in ausführbare Operationen überführen 3. und schließlich die entsprechenden Operationen ausführen. Syntaktische Fehler können im ersten Schritt, bestimmte semantische Fehler im zweiten Schritt und andere semantische Fehler erst im dritten Schritt festgestellt werden. Die ersten beiden Schritte werden vom Ubersetzer vorgenommen. Der Ubersetzer kann also die syntaktischen Fehler und einige semantische Fehler aufdecken, aber nicht alle semantischen Fehler. Schließlich gibt es in Programmen noch logische Fehler. Die Anweisung, nach der das Gesamtgewicht von Boris Beckers mit Tennisbällen prall gefüllter Tennistasche berechnet werden soll, ist syntaktisch und semantisch korrekt: gesamtgewicht
:= t a s c h e n l e e r g e w i c h t + (taschenvolumen / ballvolumen)
* ballgewicht
Sie liefert aber das falsche Ergebnis, weil nicht berücksichtigt wird, daß das Taschenvolumen mit runden Bällen nicht ausgefüllt werden kann, ohne daß Zwischenräume zwischen den Bällen bleiben.
Kontrollaufgaben K.6.1
Führen Sie die unter A bis E beschriebenen Schritte durch.
K.6.2
Welche Ursachen haben Fehler, die a) beim Ubersetzen und b) beim Ausführen von Programmen auftreten?
K.6.3
Überlegen Sie sich Beispiele für syntaktische, semantische und logische Fehler!
26
Lektion 7
Variablen, Zuweisungen und e l e m e n t a r e
Variablen I m P r o g r a m m zur Addition zweier ganzer Zahlen haben wir zwei Zahlen von der T a s t a t u r eingelesen. Wo sind diese beiden Zahlen geblieben? Sie wurden in zwei Speicherplätzen abgelegt, denen wir die Namen z a h l l und z a h l 2 gegeben haben. Solche Speicherplätze nennt man in Programmiersprachen V a r i a b l e n . Im Gegensatz zu konstanten, unveränderlichen Werten, die wir direkt ins P r o g r a m m schreiben, können Variablen während des Programmlaufs unterschiedliche Werte a n n e h m e n , zum Beispiel Eingabedaten, Zwischenergebnisse usw. D a m i t wir die Variablen im P r o g r a m m ansprechen können, braucht jede Variable einen Namen (Bezeichner). Im Vereinbarungsteil des P r o g r a m m s wird für jede im P r o g r a m m benötigte Variable ein Name vereinbart. Man sagt auch, die Variable wird d e k l a r i e r t . I m Vereinbarungsteil muß bei der V a r i a b l e n d e k l a r a t i o n auch genau angegeben werden, von welchem D a t e n t y p jede Variable sein soll. Die Unterscheidung nach D a t e n t y p e n wird vorgenommen, weil Operationen zur Verknüpfung von Daten im allgemeinen nur innerhalb eines Datentyps sinnvoll definierbar sind. Für jeden D a t e n t y p sind die möglichen Werte, die die Daten dieses T y p s annehmen können, festgelegt.
D i e D a t e n t y p e n integer und real Es gibt viele verschiedene Datentypen, insbesondere aber vier G r u n d d a t e n t y p e n , von denen wir zunächst erst einmal zwei genauer betrachten wollen: Den D a t e n t y p den D a t e n t y p
integer real
für ganze Zahlen für reelle Zahlen.
und
W a r u m unterscheidet m a n so genau zwischen ganzen Zahlen und reellen Zahlen? Oft verwendet m a n Zahlen zum Zählen von irgendwelchen Objekten. Dabei sind nur ganzzahlige Werte sinnvoll. Gäbe es nur reelle Werte, so m ü ß t e jedesmal ü b e r p r ü f t werden, ob es sich im vorliegenden Fall u m einen ganzzahligen Wert handelt. Dieser Test entfällt, wenn es einen speziellen D a t e n t y p für ganze Zahlen gibt. Zweitens wird in der Regel für einen reellen Wert mehr Speicherplatz verbraucht als f ü r einen ganzzahligen W e r t , und drittens sind Rechenoperationen auf ganzen Zahlen schneller als auf reellen Zahlen. Aus diesen Gründen sollten also reelle Werte nur verwendet werden, wenn m a n sie tatsächlich braucht.
27
S y n t a x der Variablendeklaration Die Variablendeklarationen beginnen mit dem Wortsymbol var. Darauf folgt für jede zu deklarierende Variable ihr Name entsprechend den Bildungsregeln für Bezeichner und, nach einem Doppelpunkt, ihr Datentyp. Jede Variablendeklaration wird durch ein Semikolon abgeschlossen. Beispiel: zahll zahl2 summe
integer; integer; integer;
Um den Schreibaufwand zu vermindern, können vor dem Doppelpunkt auch mehrere Variablennamen durch Kommas getrennt angegeben werden, wie in dem Beispiel in Lektion 5. Diese Variablen haben dann alle den gleichen, hinter dem Doppelpunkt angegebenen Typ.
D a t e n e i n g a b e über die Tastatur Für das Einlesen von Daten steht uns die sogenannte Standardprozedur read zur Verfügung. Was eine Prozedur ist, werden Sie in Lektion 13 erfahren. Vorerst nehmen wir diesen Begriff einfach nur zur Kenntnis. Eine Prozedur bringt man zur Ausführung, indem man sie aufruft. Einen Aufruf der Prozedur read wollen wir im folgenden aber vereinfachend read-Anweisung nennen. Die Prozedur read liest Daten von der Standardeingabedatei. Die Standardeingabe" datei" ist für gewöhnlich die Tastatur. Zwischendurch noch eine kurze Erläuterung: Die Standardeingabedatei hat programmintern den Namen input. Da alle Dateien, die an der Ein-/Ausgabe beteiligt sind, im Programmkopf aufgeführt werden müssen, taucht dort auch der Name input auf. In einigen Pascal-Versionen kann " (input,Output)" im Programmkopf auch weggelassen werden. Es wird dann automatisch vom Ubersetzer ergänzt. Wie sieht eine read-Anweisung aus? Hinter dem Bezeichner read folgt in Klammern eine sogenannte Parameter liste. In Lektion 13 werden Sie mehr darüber erfahren, was eine Parameterliste ist. Die Parameterliste enthält einen oder mehrere, durch Kommas getrennte Variablennamen, die angeben, für welche Variablen mit dieser read-Anweisung Werte eingelesen werden sollen. | r e a d ( z a h l l ) ; read(zah!2)
J
oder read(zahll,zahl2) Wie verhält sich die Prozedur read? Die Ausführung des Programms wird bei der read-Anweisung angehalten. Jetzt müssen die Daten, die eingelesen werden
28
7 Variablen, Zuweisungen und elementare Ein-/Ausgabeanweisungen
sollen, über die Tastatur eingegeben werden. Die eingegebenen Daten müssen vom Typ her zu den in der Parameterliste der read-Anweisung angegebenen Variablen passen, sonst gibt es eine Fehlermeldung. Die Prozedur read verhält sich in Standard-Pascal beim Lesen von der Tastatur als Eingabedatei genauso wie beim Lesen von einer richtigen Datei. Dieses Verhalten wird in Lektion 12.2 und Lektion 15 ausführlich beschrieben. An dieser Stelle folgt für die erste Anwendung dieser Prozedur nur eine kurze Erläuterung der Eigenschaften. In vielen Pascal-Versionen legt die Prozedur read beim Lesen von der Tastatur ein besonderes Verhalten an den Tag. Jede read-Anweisung ist beendet, wenn ein Zeichen folgt, das nicht mehr zum Format der einzulesenden Variablen paßt. Häufig wird dieses Zeichen durch das Betätigen der Eingabetaste erzeugt. Soll jedoch eine Variable vom Typ Char eingelesen werden, so wird nur genau ein Zeichen gelesen und die Anweisung beendet. Das bedeutet, eine Betätigung der Eingabetaste am Ende der Eingabe erzeugt ein Zeilenende-Zeichen, das einerseits nicht mehr zu den vorherigen gehört, aber andererseits von der nächsten read-Anweisung als erstes Zeichen eingelesen wird. Werden mit einer read-Anweisung mehrere Werte eingelesen, so können vor, zwischen und nach den Werten beliebig viele Leerzeichen eingegeben werden, und die eingegebenen Werte müssen durch mindestens ein Leerzeichen voneinander getrennt werden. Wurden vor dem Drücken der Eingabetaste mehr Werte eingegeben als die Parameterliste Variablen enthält, so liest die read-Anweisung nur die zuerst eingegeben Werte. Die restlichen, überzähligen Werte werden ignoriert. Wurden zuwenig Werte eingegeben, so behalten die Variablen, die keine Werte mehr abbekommen haben, einen Undefinierten Wert, d.h. sie können irgendeinen beliebigen, zufälligen Inhalt haben. Vor Undefinierten Variablen sollte man sich in acht nehmen. Sie können oftmals große Verwirrung stiften. Verwendet man statt read die Prozedur readln, so wird nach Betätigen der Eingabetaste auf dem Bildschirm eine neue Zeile begonnen. Die Verwendung von readln statt read für die Tastatureingabe ist meist sehr sinnvoll, da dann das Zeilenende-Zeichen nicht als erstes Zeichen von der nächsten readAnweisung eingelesen wird. Außerdem ergibt sich dadurch auf dem Bildschirm ein übersichtlicheres Bild. (Beachten Sie die Kontrollaufgabe 7.12.)
Datenausgabe auf den Bildschirm Für das Ausgeben von Daten steht uns die Standardprozedur write zur Verfügung. Die Prozedur write schreibt auf die Standardausgabedatei, welche programmintern den Namen Output hat. Die Standardausgabe"datei" ist im Normalfall der Bildschirm. Einen Aufruf der Prozedur write wollen wir im folgenden vereinfachend writeAnweisung nennen. Hinter dem Bezeichner write folgt in Klammern wiederum eine Parameterliste. Wie bei der read-Anweisung kann die Parameterliste der write-Anweisung einen oder auch mehrere Parameter enthalten. Die Parameter
29
können Variablen sein, deren Werte ausgegeben werden sollen, oder auch konstante Werte, wie Zahlen oder Zeichenketten. Die Prozedur write sieht in Abhängigkeit vom Datentyp der auszugebenden Variablen eine bestimmte Breite für die Ausgabe vor, in die der Wert rechtsbündig eingetragen wird. Diese Breite ist bei den verschiedenen Ubersetzern unterschiedlich. Häufig werden immer genau so viele Zeichen ausgegeben, wie für die Darstellung des auszugebenden Werts erforderlich sind. Diese Standardform der Ausgabe können Sie aber auch nach Ihren Wünschen beeinflussen. Für jeden auszugebenden Wert können Sie die gewünschte Breite angeben, in die der Wert dann rechtsbündig eingetragen wird, und zwar durch Angabe der Breite hinter dem Parameter in der Parameterliste. Die Breitenangabe wird vom Parameter durch einen Doppelpunkt getrennt: wert:n Beispiel: write(zahll:6,
zahl2:7)
Ist die angegebene Breite zu gering, so werden so viele Zeichen ausgegeben, wie zur Darstellung des Werts erforderlich sind. Stimmt die Standard-Ausgabebreite genau mit der Zeichenanzahl des auszugebenden Werts überein, so werden beispielsweise die durch die write-Anweisung ^ w r i t e ( z a h l l , zah!2)
J
ausgegebenen Zahlen auf dem Bildschirm nicht durch ein Leerzeichen getrennt. Die explizite Angabe der Ausgabebreite schafft hier Abhilfe. Reelle Zahlen werden normalerweise in der Gleitpunktschreibweise ausgegeben. Sie können die Ausgabe in Festpunktschreibweise veranlassen, indem Sie schreiben: wert:n:m Dabei gibt n die gesamte Ausgabebreite an, und m gibt an, wieviele Stellen hinter dem Dezimalpunkt ausgegeben werden sollen. Diese Schreibweise ist nur zulässig, wenn wert vom Datentyp r e a l ist. ^write(rzahl:10:4)
J
könnte zum Beispiel folgende Ausgabe bewirken (u bedeutet Leerzeichen): ^uu713.2568
'
J
Bei der Ausgabe wird nach Ausführung einer write-Anweisung keine neue Zeile begonnen. Wenn Sie eine neue Zeile beginnen möchten, müssen Sie statt der Prozedur write die Prozedur writeln verwenden. Dabei wird dann nach Ausgabe des letzten in dieser Anweisung aufgeführten Wertes auf eine neue Zeile übergegangen.
30
7 Variablen, Zuweisungen und elementare Ein-/Ausgabeanweisungen
Die Prozedur writeln können Sie auch ganz ohne Parameterliste aufrufen. Sie erhalten dann eine Leerzeile in der Ausgabe. Wenn Sie die Möglichkeit, mit der write-Anweisung auch Zeichenketten auszugeben, intensiv nutzen, können Sie die Form der Ausgabe erheblich verbessern. Im Beispiel aus Lektion 5 wurde als Ergebnis nur eine Zahl ausgegeben. Besser wäre gewesen: writeln('Die Summe ist:
' .summe)
Die Ausgabe hätte dann zum Beispiel so ausgesehen: Die Summe ist:
27
Oder noch komfortabler: writeC'Die Summe von ',zahll,' und ',zahl2,' ist '.summe)
ergibt beispielsweise folgende Ausgabe: Die Summe von 13 und 14 ist 27
Sehr sinnvoll ist es auch, vor jeder read-Anweisung eine Aufforderung zur Eingabe der Daten auf den Bildschirm auszugeben, z. B.: write('Bitte geben Sie zwei ganze Zahlen ein:
')
j
Zuweisungen Sie haben nun schon eine Methode kennengelernt, wie man Variablen Werte zuweisen kann: die read-Anweisung. Eine andere Methode ist die Zuweisungs-Anweisung. Für die Zuweisung wird in Pascal das Spezialsymbol : = verwendet. ^summe := z a h l l + z a h ! 2
J
Diese Anweisung muß folgendermaßen verstanden werden: Der Wert der Variablen z a h l l und der Wert der Variablen zahl2 werden addiert. Das Ergebnis dieser Addition wird der Variablen summe zugewiesen. Man sagt: summe "ergibt sich aus" z a h l l plus zahl2.
31
Auf der linken Seite einer Zuweisungs-Anweisung steht eine Variable und auf der rechten Seite ein A u s d r u c k . Im obigen Fall ein arithmetischer Ausdruck. Arithmetische Ausdrücke werden wir in der nächsten Lektion behandeln. (Ein Ausdruck kann auch aus einer einzelnen Variablen bestehen.) Es wird immer zuerst der Wert des Ausdrucks bestimmt und anschließend der Variablen zugewiesen. Deshalb kann die Variable, der ein neuer Wert zugewiesen werden soll, auch auf der rechten Seite vorkommen: summe : = 2 * summe Bei der Abarbeitung dieser Anweisung wird zuerst der alte Wert von summe mit zwei multipliziert und anschließend der dabei entstandene neue Wert der Variablen summe zugewiesen. Hat summe beispielsweise vor Ausführung der Anweisung den Wert 22 gehabt, so hat diese Variable hinterher den Wert 44. Alle Variablen, die in einem Ausdruck vorkommen, müssen bereits einen definierten Wert haben, d. h. es muß ihnen an einer früheren Stelle im Programm bereits ein Wert zugewiesen worden sein. Dies kann allerdings auch durch eine read-Anweisung geschehen sein. Wird bei der Berechnung eines Ausdrucks eine Undefinierte Variable angetroffen, so führt dies (leider nicht immer) zu einer Fehlermeldung. Sehr wichtig ist auch, daß der Wert des Ausdrucks vom Typ her zum Typ der Variablen auf der linken Seite paßt. Zum Beispiel kann ein reeller Wert keiner ganzzahligen Variablen zugewiesen werden. Dazu mehr in Lektion 8.
Kontrollaufgaben K.7.1
Geben Sie an, welcher Datentyp (integer oder real) jeweils für die folgenden Daten der geeignete ist: Matrikelnummer Preis für ein Mensaessen (in DM) Klausurnote Anzahl Kursteilnehmer Körpergröße (in m) Körpergewicht (in kg) Telefonnummer
K.7.2
Warum unterscheidet man die Datentypen i n t e g e r und r e a l ? Wann sollte welcher Datentyp benutzt werden?
K.7.3
Was steht bei der read-Anweisung in der Parameterliste?
K.7.4
Wodurch unterscheiden sich r e a d und r e a d l n ?
K.7.5
Was kann bei der write-Anweisung in der Parameterliste stehen?
32
7 Variablen, Zuweisungen und elementare Ein-/Ausgabeanweisungen
K.7.6
Wodurch unterscheiden sich w r i t e und w r i t e l n ? writeln-Anweisung ohne Parameterliste?
K.7.7
Wie kann die Form der Ausgabe beeinflußt werden?
K.7.8
Wodurch kann die Form der Ausgabe noch weiter verbessert werden?
K.7.9
Was geschieht, wenn die angegebene Ausgabebreite breiter als der auszugebende Wert ist? Was geschieht, wenn sie zu klein ist?
K.7.10
Wie werden normalerweise Werte des Typs r e a l ausgegeben? Wie kann m a n die Ausgabe in Festpunktschreibweise veranlassen?
K.7.11
Was sollte man tun, bevor man Daten von der Tastatur einliest?
K.7.12
Experimentieren Sie mit den Prozeduren r e a d , r e a d l n , w r i t e und w r i t e l n anhand des Beispielprogramms aus Lektion 5. Lesen Sie die Zahlen auch einmal mit zwei Leseanweisungen ein. Modifizieren Sie die Gestaltung der Ausgabe!
K.7.13
Wozu dient die Zuweisungs-Anweisung? Was muß man beachten, damit der Ausdruck auf der rechten Seite der Variablen auf der linken Seite zugewiesen werden kann?
K.7.14
Welche Fehler enthält das folgende Pascal-Programm?
V
program f e h l e r h a f t ; v a r summe: integer, z a h l l , zahl2: integer; record: real; vehlernummer: integer; beginn readln(zahll,zahl2); summe = Z a h l l + z a h l 2 ; writeln(summe) end
Was bewirkt eine
33
Lektion 8
Elementare Operationen mit Zahlen Sie wissen bereits, daß in Pascal Zahlen im Programm vorkommen können, und zwar ganze Zahlen und reelle Zahlen. Die Zahlen können entweder direkt im Programmtext stehen oder sich in Variablen vom Typ i n t e g e r oder vom Typ r e a l verbergen. Sicher können Sie es nun kaum erwarten zu erfahren, welche arithmetischen Operationen mit diesen Zahlen in Pascal erlaubt sind. Hier nun zuerst die mit ganzen Zahlen erlaubten Operationen:
Arithmetische Operationen mit ganzen Zahlen Symbol
Operation
Beispiel
Ergebnis des Beispiels
+
Addition
27 + 14
41
-
Subtraktion
19-7
12
*
Multiplikation
13 * 11
143
div
ganzzahlige Division ohne Rest
17 div 5 -17 div 5
3 -3
mod
Modulo (Divisionsrest)
17 mod 5 - 1 7 mod 5
2 3
Zu diesen Operationen sind drei Dinge zu beachten: 1. Das Ergebnis der ganzzahligen Division ist immer ein ganzzahliger Wert. Der Wert gibt an, "wie oft der rechte Operand in den linken Operanden ganz reingeht". Eine Null als rechter Operand ist unzulässig, wie Sie vielleicht bereits in Lektion 6 bemerkt haben. Das Ergebnis ist positiv, wenn beide Operanden dasselbe Vorzeichen haben, und negativ, wenn sie verschiedene Vorzeichen haben. 2. Das Ergebnis der Modulo-Operation ist genau definiert als i mod j = i - (k * j ) für ein ganzzahliges k, so daß gilt: 0 < = (i mod j) < j . Für positive linke Operanden entspricht dies genau dem Rest, der bei der ganzzahligen Division übrigbleibt. Als rechter Operand sind nur Zahlen zugelassen, die größer als Null sind. 3. Das Ergebnis von Operationen mit ganzen Zahlen muß im Wertebereich der ganzen Zahlen liegen. Sonst gibt es eine Fehlermeldung (vgl. Lektion 11).
34
8 Elementare Operationen mit Zahlen
Und nun die mit reellen Zahlen erlaubten Operationen:
Arithmetische Operationen mit reellen Zahlen Symbol
Operation
Beispiel
Ergebnis des Beispiels
+
Addition
13.7 + 8 . 0
21.7
-
Subtraktion
4.21 -
4.207
*
Multiplikation
2.1 * 0.1
0.21
/
Division
18.4 /
4.0
0.003
4.6
W i r stellen fest, daß es die Operatoren +, - und * mit ganzen Zahlen und mit reellen Zahlen gibt, die Operatoren div und mod aber nur mit ganzen Zahlen und den Operator / nur mit reellen Zahlen. Jede ganze Zahl ist nach der mathematischen Definition auch eine reelle Zahl, aber nicht umgekehrt. Dies ist auch in Pascal so: Wo eine reelle Zahl (vom Typ r e a l ) gefordert ist, kann auch eine ganze Zahl (vom Typ i n t e g e r ) stehen, sie wird dann intern in eine reelle Zahl umgewandelt. 2 1 / 3 ist also erlaubt (tatsächlich wird dabei die Division mit reellen Zahlen 2 1 . 0 3 . 0 ausgeführt), nicht erlaubt ist aber z.B.:
/
1 6 . 6 div 3 Beachten Sie, daß das Ergebnis einer Operation mit dem /-Operator immer reell ist. 2 1 / 3 ergibt also 7 . 0 , nicht 7! Entsprechend wird bei den Operatoren +, - und * eine Operation mit reellen Zahlen durchgeführt, sobald mindestens einer der Operanden eine reelle Zahl ist. Nehmen wir an, Sie haben folgende Variablen deklariert: var
i n t l , int2 reell
: integer; :real;
Sie möchten nun gern folgende Anweisung schreiben: ^intl
:= r e e l l div i n t 2
J
Diese Anweisung ist unzulässig, weil der Operator div nur mit ganzen Zahlen definiert ist. Sie schreiben also: f intl
:= r e e l l
/
i
n
t
2
J
35
Diese Anweisung ist immer noch unzulässig, weil das Ergebnis des Ausdrucks auf der rechten Seite reell ist und folglich nicht einer ganzzahligen Variablen zugewiesen werden kann. Sie können sich aber eventuell behelfen, indem sie eine der folgenden S t a n d a r d f u n k t i o n e n verwenden:
Beispiele:
trunc(x)
liefert den ganzzahligen Teil von x
round(x)
liefert den x nächstliegenden ganzzahligen Wert (übliche Rundung)
t r u n c ( 2 . 5 ) liefert 2 r o u n d ( 2 . 5) liefert 3
t r u n c ( - 2 . 5 ) liefert -2 r o u n d ( - 2 . 5 ) liefert -3
Beide Funktionen haben als Argument einen reellen Ausdruck und als Resultat einen ganzzahligen Wert. Ihre Anweisung könnte also zum Beispiel lauten: ^intl
:= t r u n c ( r e e l l / i n t 2 )
J
:= t r u n c ( r e e l l )
J
oder j^intl
div int2
Die Zuweisung eines ganzzahligen Werts an eine reelle Variable ist unproblematisch, da j a in diesem Fall, wie oben erwähnt, automatisch eine Typanpassung vorgenommen wird: ^reell
:= i n t l * i n t 2
J
Mit den arithmetischen Operatoren, die Sie kennengelernt haben, können Sie nun auch längere arithmetische Ausdrücke bilden, z.B.: 3 + 5 * 7
d i v 4 - 1 4 mod 5
Welches Ergebnis liefert dieser Ausdruck? In Pascal gilt die Regel, daß Multiplikationsoperatoren ( * , / , div, mod) Vorrang (Präzedenz) vor den Summationsoperatoren (+, - ) haben. Operationen von gleicher Präzedenz werden von links nach rechts ausgeführt. Der obige Ausdruck würde also ausgewertet werden, als sei er folgendermaßen geklammert: (3 + ( ( 5 * 7) d i v 4 ) ) - (14 mod 5) Das Ergebnis ist also 7. Wenn Sie eine andere Reihenfolge bei der Ausführung der Operationen haben wollen, können Sie auch in Pascal einfach entsprechend Klammern setzen. Sie können natürlich auch vorsichtshalber klammern, wenn Sie sich beim Vorrang der Operatoren nicht ganz sicher sind. Oft helfen Klammern auch, einen Ausdruck übersichtlicher zu gestalten.
36
8 Elementare Operationen mit Zahlen
Die Vorzeichenoperatoren + und - , die man von den Summationsoperatoren + und - streng unterscheiden muß, dürfen nur zu Beginn eines Ausdrucks oder eines Unterausdrucks vorkommen. Ein Unterausdruck beginnt nach jeder öffnenden Klammer. Der Ausdruck -22 * - ( 4 + -7) ist also falsch und müßte richtig so aussehen: -22 * ( - ( 4 +
(-7)))
Sie haben eben bereits zwei Standardfunktionen, t r u n c und round, kennengelernt. Es gibt in Pascal natürlich noch mehr solche vordefinierte Standardfunktionen. Die folgende Tabelle enthält die sogenannten arithmetischen Funktionen. Weitere Standardfunktionen werden Sie im weiteren Verlauf der Fibel kennenlernen.
Arithmetische Standardfunktionen Funktion
Typ des Arguments
T y p des Resultats
Funktionsbeschreibung
abs(x)
integer real
integer real
Absolutwert von x
sqr(x)
integer real
integer real
Quadrat von x (square)
sqrt(x)
integer real
real real
Quadratwurzel aus x (square root) (x >= 0)
sin(x)
integer real
real real
Sinus von x, x im Bogenmaß
cos(x)
integer real
real real
Cosinus von x, x im Bogenmaß
arctan(x)
integer real
real real
Arcus Tangens von x, Resultat im Bogenmaß
exp(x)
integer real
real real
e (2.71828...) hoch x
ln(x)
integer real
real real
natürlicher Logarithmus (Basis e) von x ( x > 0 )
In dieser Tabelle steht x für einen beliebigen Ausdruck, der der Funktion als Argument übergeben wird.
37
Einige Beispiele: sin(winkel) / cos(winkel)
Tangens von winkel
exp(y * l n ( x ) )
x hoch y (x>0)
- p / 2 + s q r t ( s q r ( p ) / 4 - q) - p / 2 - s q r t ( s q r ( p ) / 4 - q)
Lösungen der quadratischen Gleichung sqr(x) + p*x + q
Kontrollaufgaben K.8.1
Es ist gefährlich, die Lösungen einer quadratischen Gleichung so zu programmieren wie in obigem Beispiel. Warum? Wodurch kann dabei ein Fehler hervorgerufen werden?
K.8.2
Was sind arithmetische Operatoren? Welche arithmetischen Operatoren können mit ganzen Zahlen, welche mit reellen Zahlen benutzt werden?
K.8.3
Worin unterscheiden sich die Divisionsoperatoren / und d i v , wenn man sie mit Operanden vom Typ i n t e g e r benutzt?
K.8.4
Welcher Art ist das Ergebnis, wenn ein arithmetischer Operator einen Operanden vom Typ i n t e g e r mit einem Operanden vom T y p r e a l verknüpft?
K.8.5
Bestimmen Sie Typ und Wert der folgenden arithmetischen Ausdrücke: s q r ( 4 ) div 3 abs(round(-5.7)/2) sqrt(9) * 5 3 # (18 mod 4) (18 mod 4 ) * 2 . 0 (18 mod 4) / 2 trunc(sqrt(2)) sqr(round(l.5)) trunc(-13.8) div 4 3 * 4 - 10 + 6 / 3
K.8.6
Schreiben Sie ein kurzes Programm, das eine Temperaturangabe in Grad Fahrenheit einliest, diese mit der Formel C — (5/9) * (F - 32) in Grad Celsius umrechnet und das Ergebnis ausgibt.
Übungsaufgabe:
Bearbeiten Sie jetzt bitte Übungsaufgabe 2!
38
Lektion 9
Programmstrukturen 9.1
Auswahl
9.1.1
Einfache Auswahl
Unsere bisherigen Programmbeispiele bestanden nur aus einfachen Folgen von Verarbeitungsschritten. Wenn der Rechner so ein P r o g r a m m ausführt, beginnt er mit der ersten Anweisung des Anweisungsteils und arbeitet die im P r o g r a m m t e x t folgenden Anweisungen nacheinander ab, bis er das P r o g r a m m e n d e erreicht h a t . Jede Anweisung wird also genau einmal ausgeführt. F ü r viele Problemstellungen ist die Folge als einzige P r o g r a m m s t r u k t u r jedoch unzureichend. Es sei z u m Beispiel ein P r o g r a m m zu entwerfen, das einen Buchs t a b e n vom Bildschirm einliest und ausgibt, ob der Buchstabe ein Vokal ist oder nicht. Die grobe P r o g r a m m s t r u k t u r gibt das folgende S t r u k t o g r a m m an: Lies einen B u c h s t a b e n ein! Untersuche, ob der B u c h s t a b e ein Vokal ist! Gib einen entsprechenden Ergebnistext aus! Auf dieser Verfeinerungsstufe ist die P r o g r a m m s t r u k t u r eine Folge. Bei der Codierung des Programms würden wir spätestens im dritten Verarbeitungsschritt auf ein P r o b l e m stoßen. Es heißt dort: Gib einen entsprechenden Ergebnistext aus! Das heißt, es soll ein Text • ausgegeben werden, der dem Ergebnis der Untersuchung im zweiten Verarbeitungsschritt entspricht. Mögliche Ergebnistexte sind also: - Der eingegebene Buchstabe ist ein Vokal. - Der eingegebene Buchstabe ist kein Vokal.
39
9.1 Auswahl
W ä h r e n d der Ausführung des P r o g r a m m s m u ß zwischen diesen beiden Texten ausgewählt werden. Der dritte Schritt müßte also genauer lauten: Falls der Buchstabe ein Vokal ist, dann Gib aus: Der eingegebene Buchstabe ist ein Vokal. sonst Gib aus: Der eingegebene Buchstabe ist kein Vokal. Damit sind wir von der P r o g r a m m s t r u k t u r Folge abgewichen und haben eine neue S t r u k t u r , die A u s w a h l oder Verzweigung genannt wird. In Abhängigkeit von einer B e d i n g u n g wird entweder die Anweisung im dannZweig oder die Anweisung im sonst-Zweig ausgeführt, die andere wird nicht abgearbeitet. Als S t r u k t o g r a m m kann man dies so darstellen:
Falls
der Buchstabe ein Vokal ist
dann
sonst Gib aus: Der eingegebene Buchstabe ist kein Vokal
Gib aus: Der eingegebene Buchstabe ist ein Vokal
Beispielprogramm vokal Nehmen wir die Codierung dieses Programms doch einmal in Angriff. F ü r den Programmkopf benötigen wir einen treffenden Namen für unser P r o g r a m m . Wie wär's m i t : v o k a l ? ^program
v
o
k
a
l
;
^
Der D a t e n t y p char Es soll ein Buchstabe eingelesen werden. Wir brauchen also eine Variable, der wir den einzulesenden Buchstaben mit einer read-Anweisung zuweisen können. Welchen D a t e n t y p wählen wir f ü r die Variable? Bisher kennen wir nur die Datentypen i n t e g e r und r e a l . Beide D a t e n t y p e n sind f ü r die A u f n a h m e von Zahlen geeignet, nicht aber f ü r Buchstaben. Doch W i r t h sei Dank (Prof. Nikiaus W i r t h ist der "Erfinder" der Programmiersprache Pascal), gibt es in Pascal den D a t e n t y p c h a r . Eine Variable vom T y p c h a r (Abkürzung f ü r character, d. h. Zeichen) kann genau ein Zeichen aus dem Zeichensatz des Rechners aufnehmen.
40
9 Programmstrukturen
Also schreiben wir im Vereinbarungsteil unseres Programms var buchstabe:
char;
und im Anweisungsteil readln
(buchstabe)
Die auf einem Rechner verfügbaren Zeichen unterliegen einer bestimmten Ordnung. Die Zeichen aus dem ASCII- Zeichensatz, der in den meisten Arbeitsplatzrechnern (PCs) verwendet wird, sind folgendermaßen angeordnet:
T a b e l l e der A S C I I - Z e i c h e n 0
Steuerzeichen
31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
Steuerzeichen Leerzeichen I n
# $
% & »
( ) *
+ > —
/
0 l 2 3 4
53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78
5 6 7 8 9
< =
> ?
@ bzw. § A B C D E F G H I J K L M N
79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104
O P Q R s T U V W X Y Z [ bzw. A \ bzw. Ö ]A bzw. Ü
_
i
a b c d e f g h
105 i 106 j 107 k 108 1 109 m 110 n 111 o 112 P 113 q 114 r 115 s 116 t 117 u 118 V 119 w 120 X 121 y 122 z 123 { bzw. ä 124 | bzw. 6 125 } bzw. ü 126 bzw. ß 127 Steuerzeichen
41
9.1 Auswahl
Nun sollen wir untersuchen, ob der Buchstabe ein Vokal ist. Dazu müssen wir die Bedingung formulieren, unter der ein Buchstabe ein Vokal ist: Ein Buchstabe ist ein Vokal, wenn es -
der der der der der
Buchstabe Buchstabe Buchstabe Buchstabe Buchstabe
a e i o u
oder oder oder oder
ist.
Wir müssen also den eingelesenen Buchstaben nacheinander mit den Buchstaben a, e, i, o und u v e r g l e i c h e n . Dazu gibt es in Pascal die V e r g l e i c h s o p e r a t o r e n :
Vergleichsoperatoren Symbol
Operation
Beispiel
Ergebnis
=
gleich
' a ' = 'a> 5 = 7
wahr falsch
ungleich
6 19 ' e i n s '
>
größer
10 > 3 . 5 'oma' > 'omi'
wahr falsch
=
größer gleich
6 >= 6 3 . 9 9 7 >= 4 . 0
wahr falsch
= 0 do begin anzahl := anzahl + 1 ; summe := summe + zahl; readln(zahl) end;
•[ erste Zahl einlesen }
{ Anzahl erhoehen } { Summenbildung } { naechste Zahl einlesen 3"
{ Mittelwert berechnen und ausgeben bzw. Fehlermeldung ausgeben, falls gar keine positiven Zahlen eingegeben wurden } if anzahl > 0 then writeln('Mittelwert: ',summe/anzahl:8:2) eise writeln('Es wurden keine positiven ganzen Zahlen ', 'eingegeben!') end.
63
9.2 W i e d e r h o l u n g
9.2.3
Wiederholung mit vorgegebener Anzahl
Bevor Sie nach der repeat-Schleife u n d der while-Schleife noch einen dritten Schleifentyp kennenlernen, wollen wir uns mit ein paar Standardfunktionen beschäftigen, die f ü r das bessere Verständnis dieses Schleifentyps hilfreich sein können.
Ordinalfunktionen Sicher erinnern Sie sich, in Lektion 9.1.1 etwas davon gehört zu h a b e n , daß die auf einem Rechner verfügbaren Zeichen einer Ordnung unterliegen. Diese O r d n u n g wird durch den Zeichensatz definiert, der auf dem Rechner verfügbar ist, zum Beispiel durch den ASCII-Zeichensatz. Eine solche Ordnung gibt es aber nicht nur f ü r Zeichen, also für die Werte des Typs c h a r , sondern f ü r alle abzählbaren Datentypen. Nochmal zur Erinnerung: abzählbare D a t e n t y p e n sind diejenigen, bei denen zwischen zwei beliebigen Werten immer endlich viele, also auch abzählbar viele andere Werte liegen. Sie kennen bisher nur drei abzählbare Datentypen, u n d zwar i n t e g e r , c h a r u n d b o o l e a n . In der Lektion 12 werden Sie weitere abzählbare Datentypen kennenlernen. Abzählbare D a t e n t y p e n werden, eben wegen der Ordnung, auch O r d i n a l t y p e n genannt. Die erste Standaxdfunktion, die wir nun betrachten wollen, ist die Funktion o r d . Sie liefert zu allen Werten eines Ordinaltyps die entsprechende O r d i n a l z a h l als integer-Wert: Die Ordinalzahl eines i n t e g e r - W e r t s ist der Wert selbst. Die Ordinalzahl eines c h a r - W e r t s wird durch den Zeichensatz des Rechners festgelegt. Die Ordinalzahlen der Werte f a l s e u n d t r u e sind 0 und 1. Beim ASCII-Zeichensatz gilt zum Beispiel ord('A') = 65. Wenn Sie's nicht glauben, d a n n sehen Sie doch selbst in der Tabelle der ASCII-Zeichen nach! Das A r g u m e n t der Funktion ord muß ein Ausdruck sein, der einen Wert liefert, dessen T y p ein Ordinaltyp ist. Es folgen nun noch ein paar weitere Beispiele (unter Verwendung des ASCII-Zeichensatzes): Funktionsaufruf
Resultat
ord(27) ord('+') ord(true) o r d ( 1 2 3 - 321) ord(>7>) o r d ( l > maxint)
27 43 1 -198 55 0
64
9
Programmstrukturen
Nur f ü r den D a t e n t y p c h a r gibt es zur Funktion o r d die Umkehrfunktion e h r . Die S t a n d a r d f u n k t i o n e h r benötigt als Argument einen Ausdruck vom T y p i n t e g e r und liefert als Resultat den Wert vom Typ c h a r mit der entsprechenden Ordinalzahl. Es f ü h r t zu einem Fehler, wenn es kein Zeichen mit dieser Ordinalzahl gibt. F ü r jeden Wert c des Datentyps c h a r gilt also: chr(ord(c))
= c
Beispiele (unter Verwendung des ASCII-Zeichensatzes): Funktionsaufruf chr(lOO) chr(ord('A') chr(ord('F')
Resultat + 1) + ord('a')
- ord('A'))
'd' 'B> 't'
Da die Werte eines Ordinaltyps geordnet sind, existiert für jeden Wert, abgesehen vom ersten u n d letzten, genau ein Vorgänger und genau ein Nachfolger in dieser Ordnung. Wenn Sie sich f ü r den Vorgänger interessieren, d a n n hilft Ihnen die S t a n d a r d f u n k t i o n p r e d . Geht es u m einen Nachfolger, d a n n sollten Sie sich vertrauensvoll an die Standardfunktion s u c c wenden. Die Funktionen s u c c und p r e d benötigen als Argument einen Ausdruck, der einen Wert eines Ordinaltyps liefert. Die Funktion s u c c liefert dann als Ergebnis den Wert desselben Typs, dessen Ordinalzahl gegenüber der des Arguments u m 1 erhöht ist. Entsprechend liefert die Funktion p r e d als Ergebnis den Wert desselben Typs, dessen Ordinalzahl gegenüber der des Arguments u m 1 erniedrigt ist. Es f ü h r t zu einem Fehler, wenn die betreffenden Werte nicht existieren. Auch dazu a m besten ein paar Beispiele: Funktionsaufruf
Resultat
succ(6) succ('A') succ(false) pred(512) pred('z') pred(true)
7 'B' true 511 'y' false
So, nun sind wir gut gewappnet, um uns auf die nächste Schleifenstruktur zu stürzen! Bei den T y p e n von Programmschleifen, die Sie bisher kennengelernt haben, war die Anzahl der Wiederholungen zu Beginn der Schleife nicht genau bekannt, sie war abhängig von einer Bedingung, die erfüllt werden mußte. Neben diesen b e d i n g t e n Wiederholungen gibt es in Pascal noch einen weiteren Schleifentyp, bei dem die Anzahl der Schleifendurchläufe von vornherein feststeht. Er wird realisiert durch die f o r - A n w e i s u n g .
65
9.2 Wiederholung
Die for-Anweisung Die f o r - A n w e i s u n g h a t folgende formale Form: to f o r : =
< E n d w e r t > do < A n w e i s u n g > downto
G r o b gesagt passiert bei der f o r - A n w e i s u n g folgendes: Der L a u f v a r i a b l e n wird der A n f a n g s w e r t zugewiesen. Die Laufvariable wird d a n n schrittweise erhöht bzw. e r n i e d r i g t , bis der E n d w e r t erreicht ist. F ü r j e d e n W e r t , den die Laufvariable a n n i m m t , wird die A n w e i s u n g nach d e m do einmal a u s g e f ü h r t . N u n wollen wir den ganzen Ablauf nochmal genauer b e t r a c h t e n : Vor d e m ersten Schleifendurchlauf werden die Ausdrücke f ü r Anfangs- u n d E n d wert d e r Laufvariablen ausgewertet. Anfangswert u n d E n d w e r t können beliebige A u s d r ü c k e sein. Sie m ü s s e n einen Wert liefern, der v o m gleichen T y p ist wie die Laufvariable, f ü r die n a t ü r l i c h nur a b z ä h l b a r e D a t e n t y p e n zugelassen sind. Die W o r t s y m b o l e t o bzw. downto geben an, ob die L a u f v a r i a b l e " h o c h g e z ä h l t " oder " h e r u n t e r g e z ä h l t " werden soll. Ist der A n f a n g s w e r t größer als der E n d w e r t , so ist bei V e r w e n d u n g von t o die f o r Anweisung bereits b e e n d e t . Der Schleifenrumpf wird also g a r nicht a u s g e f ü h r t . E n t s p r e c h e n d e s gilt f ü r downto, wenn der Anfangswert kleiner als der E n d w e r t ist. W i r d festgestellt, daß die Schleife m i n d e s t e n s einmal d u r c h l a u f e n werden m u ß , so wird der A n f a n g s w e r t der Laufvariablen zugewiesen. Die auf das W o r t s y m b o l do folgende Anweisung, der Schleifenrumpf, wird ausg e f ü h r t . Der Schleifenrumpf kann natürlich auch eine V e r b u n d a n w e i s u n g sein. I m Anschluß a n die A u s f ü h r u n g des Schleifenrumpfs wird der Wert des Nachfolgers (bei t o ) bzw. Vorgängers (bei downto) der Laufvariablen b e s t i m m t u n d dieser zugewiesen. Es w i r d g e p r ü f t , ob der Wert der Laufvariablen n u n größer (bei t o ) bzw. kleiner (bei downto) als der E n d w e r t ist. Falls j a , wird die f o r - A n w e i s u n g b e e n d e t , andernfalls wird e r n e u t der Schleifenrumpf a u s g e f ü h r t . Bei V e r w e n d u n g einer f o r - A n w e i s u n g ist zu b e a c h t e n , d a ß der Wert der Laufvariablen i n n e r h a l b der Schleife zwar v e r w e n d e t , aber nicht v e r ä n d e r t werden darf. Zuweisungen a n die Laufvariable im Schleifenrumpf sind also nicht zulässig. Nach B e e n d i g u n g der f or-Schleife ist der Wert der Laufvariablen Undefiniert. Die Laufvariable kann d a n n den E n d w e r t h a b e n , a b e r auch einen beliebigen a n d e r e n Wert. U m die Vorgänge b e i m Ablauf der f o r - A n w e i s u n g n o c h m a l s zu verdeutlichen, wollen wir eine f o r - A n w e i s u n g einmal d e m e n t s p r e c h e n d e n P r o g r a m m s t ü c k m i t einer w h i l e - A n w e i s u n g gegenüberstellen. D a b e i sind h i l f 1 u n d h i l f 2 geeignete Hilfsvariablen, die an keiner a n d e r e n Stelle i m P r o g r a m m v o r k o m m e n :
66
9
Programmstrukturell
Die Anweisung f o r lv
:= av t o ew do rümpf
ist äquivalent zu begin h i l f 1 := aw; h i l f 2 := ew; i f h i l f 1 z» do . . . k := n+100 downto n do . . . z a e h l e r := 5 downto - 5 do . . . l a u f v a r := n t o n do . . . count := anz t o anz-1 do . . .
10 26 101 11 1 0
Als Beispiel wollen wir auch hier wieder unser Programm zur Berechnung des M i t telwerts hereinziehen. Damit sich für die Lösung der Aufgabe die f o r - A n w e i s u n g anbietet, muß die Anzahl der einzulesenden Zahlen vorgegeben sein. Die modifizierte Aufgabenstellung soll daher lauten:
Beispielprogramm mittelwert3 Es sollen 20 positive ganze Zahlen eingelesen werden. Anschließend soll der M i t telwert der eingelesenen Zahlen berechnet werden. D a die Anzahl der einzulesenden Zahlen nun feststeht, müssen sie in der Schleife nicht mehr gezählt werden. Die for-Anweisung stellt sich im Struktogramm dann folgendermaßen dar:
Wiederhole
für jede,ganze Zahl von 1 bis 20
Zahl einlesen
Summe um eingelesene Zahl erhöhen
67
9.2 Wiederholung
Und so sieht der fertige Programmtext aus:
program mittelwert3; { Dieses Programm liest 20 positive ganze Zahlen ein und berechnet deren Mittelwert. } -(
Vereinbarungsteil ******************** J-
var zahl, summe: i:
integer;
integer;
{ Laufvariable }
•{ ********************* Anweisungsteil ********************** begin summe := 0;
{ Initialisierung
}
}
writeln('Bitte geben Sie 20 positive ganze Zahlen ein:'); for i := 1 to 20 do begin readln(zahl) ; summe := summe + zahl end;
•{ Zahl einlesen } { Summenbildung }
{ Mittelwert berechnen und ausgeben }• writein('Mittelwert: ',summe/20 :8:2) end.
Beispielprogramm reihensumme Nun wollen wir noch ein Beispiel betrachten, bei dem der Wert der Laufvariablen innerhalb der Schleife verwendet wird. Wir wollen die Summe einer numerischen Reihe berechnen:
Diese Formel beschreibt eine sogenannte alternierende Reihe und soll bedeuten, daß die Summe , 1 1 1 ,1 als Ergebnis genau den natürlichen Logarithmus von 2 (geschrieben In 2) liefert, wenn man die Summation bis ins Unendliche fortführt.
68
9 Programmstrukturen
D a wir dazu nicht genügend Zeit haben, müssen wir uns in unserem P r o g r a m m darauf beschränken, die Summe bis zu irgendeinem festen Wert von n zu bilden. Dieser Wert, der die Anzahl der zu summierenden Reihenglieder angibt, soll von der T a s t a t u r eingelesen werden. Dann soll die Summe berechnet werden. Dabei muß berücksichtigt werden, daß die Reihenglieder für ungerade Werte von n addiert u n d f ü r gerade Werte von n subtrahiert werden müssen. Nachdem die S u m m e berechnet wurde, soll das Ergebnis mit dem Wert verglichen werden, den die S t a n d a r d f u n k t i o n I n für das Argument 2 liefert. Bevor wir das P r o g r a m m schreiben, zeichnen wir lieber erst ein Struktogramm: Summe mit d e m Wert 0 initialisieren Aufforderung zur Eingabe der Anzahl Reihenglieder ausgeben Anzahl der Reihenglieder einlesen
W i e d e r h o l e für alle ganzen Zahlen von 1 bis "Anzahl der Reihenglieder" Falls die Laufvariable ungerade ist sonst
dann Addiere 1/Laufvariable zur Summe
Subtrahiere 1/Laufvariable von der Summe
Die berechnete S u m m e und die Anzahl Reihenglieder ausgeben Z u m Vergleich den Wert der Standardfunktion ausgeben
Wie stellen wir a m besten fest, ob der Wert der Laufvariablen ungerade ist? Da k o m m t uns eine weitere Standardfunktion sehr gelegen: Die S t a n d a r d f u n k t i o n o d d liefert uns den Booleschen Wert t r u e , wenn der i n t e g e r - W e r t , den wir ihr als Argument gegeben haben, ungerade ist, und den Wert f a l s e , wenn der Wert gerade ist. Zwei Beispiele: Funktionsaufruf
Resultat
odd(-25) odd(32000)
true false
Na, wenn das so ist, d a n n steht der Codierung des P r o g r a m m s j a nichts mehr im Wege:
9.2 Wiederholung
69
program reihensumme; { Dieses Programm berechnet eine Reihensumme. Es wird eingelesen, wieviele Reihenglieder summiert werden sollen. Neben der berechneten Summe wird der Wert der Standardfunktion (Summe der unendlichen Reihe) zum Vergleich ausgegeben. } •( ******************** Vereinbarungsteil ******************** } var nmax: integer; n: integer; summe: real;
{ Laufvariable }
{ ********************* Anweisungsteil ********************** } begin summe := 0;
{ Initialisierung }
write('Wieviele Reihenglieder sollen summiert werden ?
');
readln(nmax); { 1Berechnung der- Summe - 1/2 + 1/3 1/4 + usw. bis 1/nmax } for n := 1 to nmax do if odd(n) then summe := summe + 1/n eise summe := summe - 1/n; { berechnete Summe ausgeben } writeln('Bei ',nmax,' Reihengliedern ist die Summe: summe:7:5); { Vergleichswert ausgeben } writeln('Die Standardfunktion liefert fuer ln(2): ln(2):7:5) end.
Denken Sie immer daran, daß der Wert einer Laufvariablen innerhalb einer Schleife zwar verwendet, aber niemals verändert werden darf!
70
9 Programmstrukturen
Kontrollaufgaben K.9.21
Wodurch unterscheidet sich die while-Anweisung von der r e p e a t Anweisung? Wann wird jeweils die Bedingung ausgewertet? Wie oft wird der Schleifenrumpf jeweils mindestens ausgeführt?
K.9.22
Wie unterscheidet sich die for-Anweisung von der r e p e a t - und der while-Anweisung? Wie oft wird der Schleifenrumpf einer f o r Anweisung ausgeführt?
K.9.23
Können Programmschleifen ineinander verschachtelt werden?
K.9.24
Wozu dient die Standardfunktion ord? Welcher Art muß das Argument sein? Welcher Art ist das Resultat?
K.9.25
Wozu dient die Standardfunktion ehr? Welcher Art muß das Argument sein? Welcher Art ist das Resultat?
K.9.26
Wozu dienen die Standardfunktionen succ und pred? Welcher Art müssen die Argumente sein? Welcher Art sind die Resultate?
K.9.27
Wozu dient die Standardfunktion odd? Welcher Art muß das Argument sein? Welcher Art ist das Resultat?
K.9.28
Finden Sie heraus, was das folgende Programmstück leistet. Stellen Sie dazu eine Tabelle auf, die die Werte enthält, die die Variablen während des Programmlaufs annehmen. Nehmen Sie an, für die Variablen d i v i dend und d i v i s o r seien die Werte 17 und 5 eingelesen worden. Welche Ausgabe erzeugt das Programm für diese Eingabewerte? Tabelle der Variablenwerte: Zeitpunkt vor dem 1. Schleifendurchlauf nach dem 1. Schleifendurchlauf nach dem 2. usw.
dividend 17
w r i t e ( d i v i d e n d , ' div d i v i s o r , ' = '); q u o t i e n t := 0 ; w h i l e d i v i d e n d > = d i v i s o r do begin d i v i d e n d := d i v i d e n d - d i v i s o r ; q u o t i e n t := q u o t i e n t + 1 end; w r i t e l n ( q u o t i e n t , ' Rest = '.dividend)
divisor 5
quotient 0
71
Lektion 10
K o m m e n t i e r u n g und G e s t a l t u n g des P r o g r a m m t e x t e s Nachdem Sie nun eine ganze Reihe von P r o g r a m m s t r u k t u r e n kennengelernt hab e n , mit denen Sie schon ganz schön komplizierte P r o g r a m m e schreiben können, wird es höchste Zeit, daß wir uns einmal über die Kommentierung und Gestaltung des P r o g r a m m t e x t e s unterhalten. Vielleicht finden Sie es eher lästig, wenn Sie sich beim Schreiben Ihrer P r o g r a m m e auch noch Gedanken über die Gestaltung des P r o g r a m m t e x t e s machen sollen. Schließlich kostet es ganz schön viel Zeit, sich treffende K o m m e n t a r e auszudenken, und dem Pascal Ubersetzer ist es vollkommen egal, wie die Anweisungen auf die Programmzeilen verteilt werden. Er kümmert sich weder um Einrückung noch um Kommentar. Nach der Lektüre dieser Lektion werden Sie aber (hoffentlich!) besser verstehen, warum gute Kommentierung und gute Gestaltung des P r o g r a m m t e x t e s so e x t r e m wichtig sind. Sie werden sehen, daß Sie dadurch sogar Zeit s p a r e n können. Sehen Sie sich zunächst die beiden folgenden Programmbeispiele an und versuchen Sie herauszufinden, was die Programme tun. Nehmen Sie sich das einfachere der beiden P r o g r a m m e zuerst vor, vielleicht das kürzere?
Beispiel 1: p r o g r a m p; var i,j:
integer;
begin repeat read(i); if i > 0 then begin for j:=2 to i do while i mod j = 0 do b e g i n write(j,' '); i := i div j end; writeln end until i=0 end.
72
10 Koinmentierung und Gestaltung des Programmtextes
Beispiel 2:
C
\
program primfaktoren; i Dieses Programm zerlegt eine natuerliche Zahl in ihre Primfaktoren. Es wird versucht,die Zahl sooft wie moeglich durch 2 zu teilen, dann durch 3, 4, usw. Das Programm wird solange wiederholt, bis eine Null eingegeben wird. Eingabedaten: Ausgabedaten:
die zu zerlegende Zahl die Primfaktoren der eingegebenen Zahl
Verfasser: August Achter Version vom: 8. August 1986 } -[ «««Hl************ Vereinbarungsteil ********************** } var zahl , faktor:
integer
-[ ***************** Anweisungsteil ************************ }
V
begin repeat Einlesen der zu zerlegenden Zahl } { write ('Bitte die zu zerlegende nat. Zahl eingeben: '); readln (zahl); { Zerlegung in Primfaktoren ]if zahl > 0 then begin { Zerlegung nur fuer natuerl. Zahlen } write ('Primfaktoren: '); for faktor := 2 to zahl do begin while (zahl mod faktor) = 0 do { Primfaktor gefunden } begin write (faktor:1,' '); zahl := zahl div faktor end end; writeln end until zahl=0 end.
J
73
Na, h a b e n Sie herausgefunden, was die P r o g r a m m e tun? Bei welchem P r o g r a m m h a b e n Sie es schneller herausgefunden? Die Antwort erübrigt sich wohl! Falls Sie es noch nicht gemerkt haben sollten: Beide P r o g r a m m e t u n dasselbe. Das P r o g r a m m p liefert genauso die richtigen Ergebnisse wie das P r o g r a m m p r i m f a k t o r e n , aber das P r o g r a m m primf a k t o r e n ist sehr viel besser. So wie es Ihnen eben erging, geht es täglich vielen Programmierern, die sehr viel kostbare Zeit damit vergeuden (müssen), die P r o g r a m m e ihrer Vorgänger zu verstehen. Insbesondere, um das P r o g r a m m f ü r andere Personen durchsichtig zu machen, sind eine gute Gestaltung und eine ausführliche Kommentierung extrem wichtig. "Ich habe gar nicht die Absicht, mein P r o g r a m m an andere weiterzugeben. Hauptsache ich selbst weiß, was mein P r o g r a m m t u t . " , werden Sie sagen. Nun, das wissen Sie im Moment und vielleicht noch nächste Woche. Aber kramen Sie doch in einem halben J a h r einmal Ihr P r o g r a m m xyz hervor und erklären Sie j e m a n d e m , wozu Sie das P r o g r a m m geschrieben haben und wie es funktioniert. Sie werden sehen, es wird Ihnen vorkommen, als h ä t t e n Sie das P r o g r a m m noch nie gesehen, es sei denn, Sie haben das P r o g r a m m gut strukturiert und mit viel K o m m e n t a r versehen. Der K o m m e n t a r wird umso wichtiger, je umfangreicher die Programme werden. Einen Fünfzeiler kann man vielleicht auch ohne K o m m e n t a r noch leicht durchschauen. Bei hundert, zweihundert oder gar tausend Programmzeilen wird's da schon schwieriger. Übrigens tragen auch ausführliche w r i t e - A n w e i s u n g e n , durch die der Benutzer zum Beispiel zur Eingabe von Daten aufgefordert wird, oder die die ausgegebenen Werte näher erläutern, zur besseren Verständlichkeit eines Programms bei. Und nicht zuletzt hilft Ihnen gute G e s t a l t u n g und Kommentierung bereits in der P h a s e der Programmentwicklung bei der Suche nach Fehlern im P r o g r a m m . Schon da werden Sie viel Zeit sparen, wenn Sie im voraus etwas mehr Zeit investieren! Alle Ihre P r o g r a m m e sollten f o l g e n d e K r i t e r i e n erfüllen: - Jedes P r o g r a m m sollte im Programmkopf folgende Informationen enthalten: • • • • •
Kurzbeschreibung des P r o g r a m m s welche Eingabedaten werden benötigt? welche Ausgabedaten erhält m a n ? Name des Verfassers Erstellungsdatum
- Vereinbarungsteil u n d Anweisungsteil sollten deutlich voneinander abgegrenzt werden. - Durch die Wahl geeigneter Bezeichner soll der Verwendungszweck von Variablen usw. verdeutlicht werden.
74
10 Kommentierung und Gestaltung des Programmtextes
- Durch Einrücken von Blöcken und Einfügen von Leerzeilen soll die Struktur des Programms besser erkennbar werden. - Zusammengehörige begin-end oder i f - t h e n - e l s e sollten untereinander stehen. - Durch Kommentare sollen die nicht unmittelbar verständlichen Anweisungen erläutert werden. Bei den Beispielprogrammen in dieser Fibel wurden die Kommentare, insbesondere im Programmkopf, aus Platzgründen etwas knapp gehalten. Auf die Angabe von Verfasser und Erstellungsdatum wurde ganz verzichtet, denn die Verfasser der Fibel sind auch die Verfasser der Programme. Die Beschreibung der Einund Ausgabedaten wurde meist in die (recht kurze) Kurzbeschreibung des Programms integriert. In einem Lehrbuch ist dies vertretbar, weil die Programme j a meist noch ausführlich im Text außerhalb des Programmtextes erläutert werden. Ohne die Fibel müßten die Kommentare in einem Programm aber unbedingt ausführlicher sein. Machen Sie es bei Ihren Programmen besser als wir! Soviel zunächst zur Kommentierung und zur Gestaltung des Programmtextes. Darüberhinaus sollten gute Programme so geschrieben sein, daß u n e r w ü n s c h t e E i n g a b e n nicht zu einem unkontrollierten Abbruch oder zu fehlerhaften Ausgaben führen. Muß beispielsweise eine positive ganze Zahl eingegeben werden, so sollte das Programm sicherstellen, daß der Benutzer keine negative Zahl eingeben kann. Bei Eingabe einer negativen Zahl, sollte das Programm den Benutzer deutlich auf seinen Irrtum hinweisen. Fehler, die nicht (bzw. nur mit extrem hohem Aufwand) abgefangen werden können, wie z. B. die Eingabe einer reellen Zahl, wenn eine ganze Zahl gefordert ist, können vermieden werden, indem der Benutzer bei der Aufforderung zur Eingabe explizit darauf hingewiesen wird, daß jetzt eine ganze Zahl eingegeben werden soll.
Kontrollaufgaben K.10.1
Wie erreicht man eine gute Gestaltung des Programmtextes?
K.10.2
Wozu dienen die Kommentare in einem Programm?
K.10.3
Welche Informationen sollte der Programmkopf enthalten?
Übungsaufgabe:
Bearbeiten Sie jetzt bitte Übungsaufgabe 3! (Und beherzigen Sie, was Sie eben gelernt haben!)
75
Lektion 11
Rechnerarithmetik In dieser Lektion geht es um die verschiedenen Möglichkeiten der Zahlendarstellung im Rechner und die daraus erwachsenden Probleme. Alle diese Probleme hängen mit den begrenzten Möglichkeiten zusammen, die dem Rechner für die Darstellung von Zahlen zur Verfügung stehen. In der Vorstellungswelt der Mathematik existieren beispielsweise unendlich viele ganze Zahlen. Bei den reellen Zahlen ist nicht nur der Wertebereich unbeschränkt, sondern es existieren zwischen je zwei solcher Zahlen wiederum unendlich viele reelle Zahlen. Im Rechner, als einer elektronischen Maschine, müssen die Zahlen letztendlich als unterschiedliche elektronische Zustände dieser Maschine dargestellt und erkannt werden können. Da eine solche real existierende Maschine aber nicht unendlich viele verschiedene Zustände annehmen kann, können somit auch nicht alle theoretisch denkbaren Zahlen verwirklicht werden. Der Zahlenvorrat im Rechner ist also begrenzt. Während wir üblicherweise im Zehnersystem (Dezimalsystem) rechnen, bevorzugt der elektronische Rechner das Zweiersystem (Dualsystem), bei dem es nur die Ziffern 0 und 1 gibt. So wie Zahlen im Zehnersystem Dezimalzahlen genannt werden, heißen Zahlen im Zweiersystem Dualzahlen. Wie Sie wissen, entsprechen den ganzen Zahlen bzw. den reellen Zahlen aus der Mathematik im Rechner die Datentypen i n t e g e r und r e a l . Für beide Zahlentypen wollen wir im folgenden untersuchen, worauf man beim Rechnen mit einem begrenzten Zahlenvorrat zu achten hat. Die interne Darstellung der Zahlen ist rechner- und übersetzerabhängig. Die im folgenden beschriebene Darstellung gilt daher speziell für IBM-kompatible PC's mit dem unter dem Betriebssystem MS-DOS installierten Pascal-System TurboPascal. Die beschriebenen Effekte sind aber auf andere Rechner mit anderen Ubersetzern übertragbar.
76
11 Rechnerarithmetik
Rechnen mit ganzen Zahlen Zahlen vom Typ integer werden im Rechner als Dualzahlen gespeichert, wobei in vielen Pascal-Versionen für jede Zahl 16 Dualstellen zur Verfügung stehen. Genauere Informationen über die Zahlendarstellung im Rechner entnehmen Sie bitte weiterführender Literatur. Für den Anfang genügt es zu wissen, daß mit einer 16-stelligen Dualdarstellung alle Dezimalzahlen im Bereich von -32768 bis +32767 exakt darstellbar sind. Berechnungen mit integer-Zahlen sind daher immer exakt, solange der zulässige Wertebereich nicht überschritten wird. Dieser Nebensatz ist allerdings sehr wichtig, denn eine Fehlermeldung erhält man nur dann, wenn die zu große oder zu kleine Zahl als konstanter Wert im Programm steht oder eingelesen wird. Tritt eine Bereichsüberschreitung aber im Laufe einer Berechnung auf, so wird in der Regel kein Fehler gemeldet. Sie können dies leicht mit Ihrem Additionsprogramm aus Lektion 5 überprüfen: 32767 + 1 ergibt -32768. Manchmal kann es durchaus sinnvoll sein, in einen Algorithmus eine Uberprüfung einzubauen, mit Hilfe derer eine drohende Bereichsüberschreitung erkannt und rechtzeitig abgewendet werden kann. Denken Sie einmal an das Programm m i t t e l w e r t 2 aus Lektion 9.2.2. Dort wurden positive ganze Zahlen eingelesen und aufsummiert, bis eine negative Zahl eingegeben wurde. Da Anzahl und Größe der Zahlen vorher nicht bekannt sind, kann es bei jeder Addition zu einer Bereichsüberschreitung kommen. Wir sollten also vorher jeweils überprüfen, ob die neu entstehende Summe nicht vielleicht zu groß wird. Bei der Formulierung einer entsprechenden Bedingung muß man allerdings aufpassen, daß man dabei nicht schon eine Bereichsüberschreitung erzeugt. So zum Beispiel geht es nicht: if summe + zahl > maxint t h e n . . . { Ueberlauf melden > e i s e summe := summe + zahl Da es keine Zahl des Datentyps i n t e g e r gibt, die größer als maxint ist, wird die Bedingung immer den Wert falsch haben. Die Bereichsüberschreitung tritt nämlich schon im Booleschen Ausdruck selbst auf und ist so nicht zu erkennen. Eine mögliche, korrekte Lösung ergibt sich, wenn Sie auf beiden Seiten der Ungleichung zahl subtrahieren:
C ^
:
if summe > maxint - zahl t h e n . . . { Ueberlauf melden } e i s e summe := summe + zahl
^ /
Auch beim Erhöhen der Variablen anzahl kann theoretisch natürlich eine Bereichsüberschreitung stattfinden. Da aber nicht zu erwarten ist, daß jemand mehr als 32767 Werte eingibt, kann hier auf eine Uberprüfung verzichtet werden.
77
R e c h n e n m i t reellen Zahlen Anders als bei den integer-Zahlen ist es bei den real-Zahlen. Die reellen Zahlen werden im Rechner durch ein Wertepaar abgebildet. Auch hierbei wird natürlich im Rechner die Dualdarstellung verwendet, aber das Prinzip läßt sich ebensogut mit Hilfe der Dezimaldarstellung erläutern. Die reellen Zahlen werden also dargestellt durch eine sogenannte M a n t i s s e und einen E x p o n e n t e n . Der reelle Wert ist dann gleich Mantisse * i 0 E x P o n e n t Der Exponent ist eine ganze Zahl und wird so gewählt, daß sich der Wert der Mantisse immer zwischen 0.1 und 1 bewegt (0.09 kann ja als 0.9* 1 0 - 1 dargestellt werden und 1.1 als 0.11 * 10 1 ). Die Genauigkeit und der Wertebereich der reellen Zahlen ergibt sich dabei durch die Anzahl Stellen in der Mantisse und durch den Wertebereich des ganzzahligen Exponenten. Bei PolyPascal verfügt die Mantisse über eine Genauigkeit von etwa 12 Dezimalstellen (in Wirklichkeit 40 Dualstellen). Sie kann mit Hilfe des Exponenten mit einem Wert zwischen 1 0 - 3 8 und 10 + 3 8 multipliziert werden (tatsächlich 2 - 1 2 7 bis 2 + 1 2 7 ) . Es sind also reelle Zahlen im Bereich von ungefähr - 1 . 7 * 10 38 bis +1.7 * 10 38 darstellbar. Der tatsächliche Bereich geht von - ( 1 - 2~ 4 0 ) * 2 1 2 7 bis + ( 1 - 2 " 4 0 ) * 2 1 2 7 . Wie bei den Zahlen des Datentyps i n t e g e r kann es natürlich auch beim Datentyp r e a l zu Bereichsüberschreitungen kommen. Hier bekommt man in der Regel aber eine Fehlermeldung. Bei den reellen Zahlen stellen Bereichsüberschreitungen wegen des doch recht großen Bereichs auch nicht das Hauptproblem dar. Die Probleme beim Rechnen mit reellen Zahlen erwachsen in der Hauptsache aus der U n g e n a u i g k e i t der Darstellung. Wenn wir an reelle Zahlen denken, so stellen wir uns meist eine Zahlengerade vor, die von minus unendlich bis plus unendlich führt, und auf der unendlich viele reelle Zahlen liegen. Zwischen zwei bestimmten reellen Zahlen liegen wiederum unendlich viele andere reelle Zahlen. Wir wissen bereits, daß im Rechner mit einer endlichen Stellenzahl nicht unendlich viele reelle Zahlen darstellbar sind. Zwischen den im Rechner darstellbaren reellen Zahlen befinden sich also Lücken. Alle reellen Zahlen, die in solch eine Lücke fallen, müssen im Rechner durch die nächstgelegene darstellbare reelle Zahl dargestellt werden. Dadurch entstehen natürlich Ungenauigkeiten. Wie groß können diese Ungenauigkeiten sein? Betrachten wir einmal nur den positiven Bereich der Zahlengeraden. Im Negativen sieht es entsprechend aus. Der Einfachheit halber rechnen wir wieder im Dezimalsystem. Unsere Mantisse habe eine Genauigkeit von 12 Stellen. Um die Größe einer Lücke festzustellen, müssen wir die Differenz zwischen zwei benachbarten Zahlendarstellungen bilden. Nehmen wir zunächst zwei benachbarte darstellbare Zahlen im Bereich um 1 (der Exponent hat hier den Wert 0): 0.999999999999 * 10° - 0.999999999998 * 10° 0.000000000001 * 10°
78
11 Rechneiaiithmetik
Die G r ö ß e der Lücken liegt hier also in der Größenordnung 1 0 - 1 2 . In Richtung auf Null zu werden die Lücken kleiner. D e r kleinste E x p o n e n t h a t den Wert - 3 8 . Die Lücken schrumpfen also bis auf die Größe von etwa 1 0 - 1 2 * 1 0 - 3 8 = 1 0 - 5 0 . Von 1 in R i c h t u n g unendlich werden die Lücken rapide größer. I m Bereich von 1 0 3 8 liegt die Größe der Lücken bei 1 0 " 1 2 * 1 0 3 8 = 1 0 2 6 !!! B e i kleinen Werten ist der a b s o l u t e A b s t a n d zwischen zwei darstellbaren reellen Zahlen also am kleinsten, bei großen Werten a m größten. Der r e l a t i v e A b s t a n d , also der A b s t a n d im Verhältnis zur Größe der Zahlen ist aber überall etwa gleich. Der relative A b s t a n d wird berechnet, indem m a n den absoluten Abs t a n d durch eine der beiden Zahlen teilt, zwischen denen er b e s t i m m t wurde. Der relative A b s t a n d zwischen zwei darstellbaren reellen Zahlen liegt also in unserem Beispiel immer im Bereich 1 0 " 1 1 bis 1 0 ~ 1 2 . Eine A u s n a h m e bildet die Zahl 0 mit den ihr b e n a c h b a r t e n Zahlen. Die Größe des relativen Abstands wird durch die Stellenzahl der Mantisse b e s t i m m t . D e r relative A b s t a n d zweier reeller Zahlen wird auch als r e l a t i v e G e n a u i g k e i t der Zahlendarstellung bezeichnet. Alle R e c h n u n g e n mit Zahlen vom Datentyp r e a l sind also im Sinne einer exakten A r i t h m e t i k fehlerbehaftet. F ü r den Rechner sind diese Ungenauigkeiten natürlich keine " F e h l e r " . W i r können daher auch keine Fehlermeldung v o m Rechner erwarten, in der er uns darauf aufmerksam m a c h t , daß ein Ergebnis im arithmetischen Sinne falsch ist. So p a r a d o x es klingen mag: Wir müssen selbst darauf achten, daß unsere P r o g r a m m e die Ergebnisse liefern, die sie liefern sollen. Dies gilt insbesondere b e i m R e c h n e n m i t reellen Zahlen. Die folgenden Beispiele sollen die Tücken der Rechnerarithmetik illustrieren:
Fehler bei der Auswertung arithmetischer Ausdrücke p r o g r a m bug; { * * * * * * * * * * * * * * * * * * * * Vereinbarungsteil ******************** var a,b,c:
}
real;
{ ********************** Anweisungsteil ********************* begin a := 1.0el3; b := 1.0; c := 1.0el3; writeln('a + b - c = writeln('a - c + b = end.
',a+b-c); ',a-c+b)
}
79
Dieses Programm erzeugt auf einem IBM-kompatiblen P C mit Turbo-Pascal folgende Ausgabe: a + b - c = 0.0000000000E+00 a - c + b = 1.0000000000E+00
Das richtige Ergebnis müßte natürlich in beiden Fällen 1 lauten. Die Ursache des Fehlers ist in diesem einfachen Fall leicht zu finden. Arithmetische Ausdrücke mit Operatoren gleicher Präzedenz werden im allgemeinen von links nach rechts ausgewertet. Bei der Addition a + b ergibt sich theoretisch der Wert 1.000000000001E+13 als Zwischenergebnis. Da die Mantisse aber nur über maximal 12 Dezimalstellen verfügt, kann dieser Wert im Rechner nicht dargestellt werden und wird zum Wert 1.00000000000E+13 gerundet. Wird nun der Wert von c subtrahiert, ergibt sich 0 als Ergebnis. Im anderen Fall wird zunächst die Subtraktion a - c durchgeführt, die als Zwischenergebnis 0 liefert. Wird nun der Wert von b hinzuaddiert, ergibt sich das korrekte Ergebnis 1. Vorsicht ist also geboten bei allen arithmetischen Ausdrücken, in denen, wie in diesem Beispiel, reelle Zahlen stark unterschiedlicher Größenordnung miteinander verknüpft werden. Die Gefahr liegt besonders darin, daß dies bei einem aufwendigeren numerischen Algorithmus oft nicht leicht zu erkennen ist.
Fehler bei der A u s w e r t u n g v o n Vergleichsausdrücken Dieses Beispiel soll zeigen, wie wenig sinnvoll eine Uberprüfung reeller Zahlen auf Gleichheit in vielen Fällen ist. program dreieck; { Dieses Programm prueft, ob ein Dreieck rechtwinklig ist.
}
{ ******************** Vereinbarungsteil ******************** var a,b,c: real;
}
{ ********************** Anweisungsteil ********************* begin writeln('Geben Sie die Laengen der Dreiecksseiten ein!'); write ('(die laengste Seite als letzte!!!): ');
}
readln(a,b,c); if a*a + b*b = c*c { Pythagoras > then writeln('rechtwinklig') eise writeln('nicht rechtwinklig') end.
80
11 Rechnerarithmetik
L a u t e t die Eingabe zu diesem Programm 9 . 0 , 1 2 . 0 und 1 5 . 0 , so erkennt das P r o g r a m m richtigerweise ein rechtwinkliges Dreieck. Gibt man als Seitenlängen hingegen 0 . 9 , 1 . 2 und 1 . 5 an, so wird fälschlicherweise "nicht rechtwinklig" diagnostiziert. Der Grund dafür ist natürlich wieder bei der Zahlendarstellung zu suchen. Sowohl 9 . 0 , 1 2 . 0 und 1 5 . 0 als auch die daraus berechneten Ausdrücke a * a + b * b und c * c sind exakt darstellbare r e a l - Z a h l e n . Die verglichenen Werte sind also gleich. Folglich ergibt sich der Wert wahr. Bei den lediglich um eine Zehnerpotenz kleineren Zahlen 0 . 9 , 1 . 2 und 1 . 5 ist eine exakte Darstellung der Zahlen und der mit ihnen gebildeten Ausdrücke rechnerintern nicht möglich. Durch Rundungsfehler ergeben sich zwei verschiedene Werte für die Ausdrücke a * a + b * b und c * c . Der Vergleich liefert den Wert false. Abhilfe kann man schaffen, wenn man in der Bedingung für die Rechtwinkligkeit eines Dreiecks nur eine annähernde Gleichheit unter Berücksichtigung möglicher Rundungsfehler fordert. Man könnte zum Beispiel fordern, daß die Abweichung kleiner als 1 0 - 4 sein muß. Wichtig ist aber, daß man nicht eine a b s o l u t e A b w e i c h u n g , sondern eine r e l a t i v e A b w e i c h u n g betrachtet. Der Unterschied soll anhand der folgenden Tabelle verdeutlicht werden:
Wert x 1000000.0 0.000001 1000000.0 0.01
Wert y 1000100.0 0.0000010001 1000000.0001 0.0101
absolute Abweichung
relative Abweichung
10 2 -10
IO-4 IO"4 IO"10 IO"2
10
10"4 10"4
Die absolute Abweichung eines Werts y von einem Wert x ist die Differenz der beiden Werte. Dabei ist unwesentlich, ob die Differenz positiv oder negativ ist. Sinnvollerweise bildet man den Betrag. Die relative Abweichung erhält man, indem man die absolute Abweichung durch einen der beiden Werte x oder y teilt und wieder den Betrag bildet. Eine relative Abweichung, die kleiner als 1 0 - n ist, besagt, daß sich die beiden zu vergleichenden Zahlen frühestens in der n + l - t e n signifikanten Dezimalstelle unterscheiden. Signifikante Dezimalstellen sind alle Dezimalstellen ohne führende Nullen. M i t Hilfe der relativen Abweichung kann man sicherstellen, daß ein Ergebnis unabhängig von der Größenordnung auf eine bestimmte Anzahl von Dezimalstellen genau ist. W i r wollen die Bedingung für das Programm d r e i e c k nun so modifizieren, daß alle Dreiecke als rechtwinklig bezeichnet werden, bei denen der Wert für die Summe der Kathetenquadrate (a 2 + b 2 ) in mindestens vier signifikanten Dezimalstellen mit dem Wert für das Hypothenusenquadrat (c 2 ) übereinstimmt.
81
Bei der Formulierung der Bedingung vermeidet man möglichst die Division, da sie leicht zu Problemen führen kann (z.B. Division durch null). Stattdessen multipliziert m a n die andere Seite der Ungleichung: if a b s ( a * a + b*b - c»c) < 1.0e-4 * v t h e n ...
abs(c*c)
Abbruch eines Iterationsverfahrens Als Beispiel für ein Iterationsverfahren wollen wir hier näherungsweise die Quadratwurzel aus einer positiven reellen Zahl x bestimmen. N e w t o n hat dafür folgende Formel angegeben: x Vi-1
+
Vi =
—
^
^
¿=1,2,3,...
Als Startwert der Iteration muß ein geeigneter Wert für yo gewählt werden, y, ist der Näherungswert für die Quadratwurzel aus x nach d e m i'-ten Iterationsschritt. Im ersten Iterationsschritt (t = 1) erhält m a n den ersten Näherungswert t/i = y\ aus d e m Startwert j/ t _i = j/o- Im nächsten Iterationsschritt (t = 2) wird der so errechnete Wert für y , _ ! = yi in die Formel eingesetzt, und es ergibt sich ein neuer Näherungswert yi = y2, usw. Bei diesem Iterationsverfahren von Newton hat sich der Startwert yo = x bewährt. Die Iteration soll abbrechen, wenn eine bestimmte Genauigkeit erreicht ist. In diesem Beispiel soll eine relative Genauigkeit von 1 0 - 7 erreicht werden. Die Iteration kann demnach beendet werden, sobald die relative Abweichung eines Näherungswerts v o m folgenden kleiner als 1 0 - 7 ist. Abbruchkriterium: \y< -
y»-11 < io~ 7 * |t/,|
Und so sieht das zentrale Programmstück aus (alle vorkommenden Variablen müssen v o m D a t e n t y p r e a l sein): read(x);
•[ Zahl einlesen, von der die Quadratwurzel b e s t i m m t werden soll }
yi
{ Startwert
:= x;
setzen
}
repeat yiminusl := yi; yi := (yiminusl + x / y i m i n u s l ) / 2 u n t i l abs(yi - y i m i n u s l ) ] of
;
Der I n d e x t y p gibt an, wieviele Feldelemente angelegt werden sollen. Für jeden möglichen Wert, den der Indextyp annehmen kann, wird ein Feldelement angelegt. Deshalb dürfen als Indextyp nur Ordinaltypen angegeben werden. Mögliche Indextypen sind also alle Aufzählungstypen und Teilbereichstypen und die Typen b o o l e a n , c h a r und i n t e g e r . Vom T y p i n t e g e r können im allgemeinen aber nur Teilbereiche als Indextyp verwendet werden. Der Indextyp kann in der Definition des Feldtyps entweder als Typname oder als implizite Typdefinition angegeben werden. Der K o m p o n e n t e n t y p gibt an, welchen Typ die einzelnen Elemente des Feldes haben sollen. Als Komponententyp kann jeder beliebige Typ verwendet werden.
92
12 D a t e n s t r u k t u r e n I
Ein paar Beispiele: t y p e v e k t o r = a r r a y [ 1 . . 1 0 ] of r e a l ; ampel = a r r a y [ ( r o t , g e l b , g r u e n ) ] of b o o l e a n ; z a e h l z e i c h e n = a r r a y [ c h a r ] of i n t e g e r ; b o o l a r r a y = a r r a y [ b o o l e a n ] of 1 . . 9 9 ; V e r t e i l u n g = a r r a y [ - 5 0 . . 5 0 ] of 0 . . 1 0 0 ; F ü r die Aufgabe, die Buchstabenhäufigkeit zu bestimmen, benötigen wir für jeden B u c h s t a b e n ein Feldelement. Als Indextyp für das Feld würde sich also folgender T y p gut eignen: type buchstabe =
'a'..'z';
Als K o m p o n e n t e n t y p brauchen wir einen Typ, dessen Wertebereich gut zum Zählen geeignet ist, also i n t e g e r . Damit sähe die erforderliche Felddefinition
type zaehlfeld = array
[ b u c h s t a b e ] of
integer;
Die Definition h ä t t e natürlich auch so aussehen können: type zaehlfeld = array
['a'-.'z']
of
integer;
Nachdem wir n u n den Datentyp z a e h l f e l d definiert haben, können wir uns eine Variable dieses T y p s deklarieren, die dann unsere Häufigkeitstabelle a u f n e h m e n soll: var haeuftab:
zaehlfeld;
Die Variable h a e u f t a b enthält das gesamte Feld, das aus 26 ( ' a ' . . ' z ' ) Element e n besteht. Die einzige Operation, die mit ganzen Feldern zulässig ist, ist die Zuweisung. H ä t t e n wir also eine zweite Variable vom T y p z a e h l f e l d , nennen wir sie Zwis c h e n e r g e b n i s , so wäre folgende Zuweisung korrekt: Zwischenergebnis := h a e u f t a b Zuweisungen sind aber nur zwischen Feldern des gleichen Typs erlaubt. Betracht e n Sie die folgenden Deklarationen:
r
var f e l d l , feld2: feld3 :
array array
[ 1 . . 1 0 ] of [ 1 . . 1 0 ] of
integer; integer;
N
93
12.2 Eindimensionale Felder
Die Zuweisung ^feldl
:= f e l d 2
j
ist natürlich erlaubt, denn f e l d l und f eld2 sind vom gleichen Typ. Der Typ der Variablen f e l d 3 hat zwar die gleiche Struktur wie der Typ, von dem die Variablen f e l d l und f eld2 sind, aber dennoch würde die Zuweisung ^ feldl
:= f eld3
^
zum Fehler führen. In Pascal sind zwei Typen nämlich nur dann wirklich gleich, wenn ihnen dieselbe Typdefinition zugrundeliegt. A m einfachsten vermeidet man diese Probleme durch eine explizite Typdefinition: / t y p e i n t f e l d = a r r a y [ 1 . . 1 0 ] of var
f eldl, feld2 feld3
: :
integer;
intfeld; intfeld;
V Nun liegt allen drei Variablen dieselbe Typdefinition zugrunde. Daher können sie einander beliebig zugewiesen werden. Die einzelnen Elemente eines Feldes spricht man über den sogenannten I n d e x an. Der Index wird in eckige Klammern eingeschlossen und an den Variablennamen angehängt. Wollen wir beispielsweise den Inhalt des Feldelements für den Buchstaben 'b' um eins erhöhen, so schreiben wir: ^haeuftab['b']
:= h a e u f t a b [ ' b ' ] + 1
J
Oder allgemein: var z e i c h e n :
buchstabe;
haeuftab[zeichen]
:= haeuftab [ z e i c h e n ] + 1
Der Index kann ein beliebiger Ausdruck sein. Wichtig ist nur, daß der Wert, den der Ausdruck liefert, im Wertebereich des Indextyps liegt! Mit den einzelnen Elementen eines Feldes sind alle Operationen erlaubt, die mit dem Komponententyp1 erlaubt sind !
94
12 Datenstrukturen I
Die folgende Abbildung verdeutlicht noch einmal die Zusammenhänge: h a e u f t a b (Typ: z a e h l f e l d ) h a e u f t a b [ 'n ] 1 a
1 b
(Typ: i n t e g e r )
1 ...
n
(Typ: b u c h s t a b e ) Die Variable h a e u f t a b bezeichnet das ganze Feld und ist vom Typ z a e h l f e l d . Die einzelnen Komponenten des Feldes werden über Indizes angesprochen (z. B . h a e u f t a b [ ' n ' ] ) und sind hier vom Typ i n t e g e r . Die Indizes selbst sind in diesem Fall vom T y p b u c h s t a b e .
Beispielprogramm haeufigkeit Gehen wir nun daran, das Programm zur Bestimmung der relativen Häufigkeiten von Buchstaben in Texten zu entwerfen. Sonderzeichen, Steuerzeichen und Ziffern sollen nicht berücksichtigt werden, aber alle Buchstaben von A bis Z, egal ob groß oder klein geschrieben, sollen mitgezählt werden. Die Zählung soll beendet werden, sobald das Zeichen ' # ' auftritt. Bevor wir mit dem Zählen beginnen, müssen wir sicherstellen, daß alle Feldelemente der Häufigkeitstabelle mit dem Wert 0 initialisiert wurden. Da wir nach Abschluß der Zählung nicht die absoluten, sondern die relativen Häufigkeiten ausgeben wollen, benötigen wir auch die Gesamtanzahl aller gezählten Buchstaben. Sie läßt sich am einfachsten ermitteln, indem wir während des Zählvorgangs in einer zusätzlichen Variablen die Gesamtanzahl mitzählen. Die Großbuchstaben lassen sich für die Zählung leicht in Kleinbuchstaben umwandeln. Die Kleinbuchstaben haben von den Großbuchstaben einen festen Abstand, nämlich o r d ( ' a ' ) - o r d ( ' A ' ) . Dies gilt für viele Zeichensätze, doch kann die Differenz durchaus unterschiedlich groß sein. Im ASCII-Zeichensatz beträgt diese Differenz 32 (vgl. Tabelle in Lektion 9.1.1), in einem anderen weit verbreiteten Zeichensatz beispielsweise 64. Wenn wir unser Programm also möglichst allgemeingültig halten wollen, sollten wir diese Differenz nicht direkt als konstanten Wert ins Programm schreiben, sondern sie wie oben angegeben berechnen.
12.2 Eindimensionale Felder
95
Das S t r u k t o g r a m m z u m P r o g r a m m h a e u f i g k e i t sieht also so aus: Häufigkeitstabelle und Gesamtanzahl mit 0 initialisieren A b s t a n d zwischen Groß- und Kleinbuchstaben bestimmen ein Zeichen einlesen
Falls
Zeichen ein Großbuchstabe ist
in Kleinbuchstaben umwandeln
Falls
Zeichen ein Kleinbuchstabe ist
entsprechendes Feldelement erhöhen u n d Gesamtanzahl erhöhen b i s Zeichen = '#'
Wiederhole
für alle Zeichen von 'a' bis 'z'
Relative Häufigkeit des Buchstabens berechnen u n d ausgeben
Wenn das P r o g r a m m nach diesem S t r u k t o g r a m m codiert wird, d a n n hat es einen großen Nachteil, über den sich der Benutzer sicher bald beklagen wird: Die auszuwertenden Texte müssen jedesmal wieder neu von Hand über die T a s t a t u r eingegeben werden. Besser wäre es, wenn das P r o g r a m m die Texte von einer Datei lesen könnte. Das Arbeiten mit Dateien wird ausführlich in der Lektion 15 behandelt. Hier wollen wir aber vorab auf einen einfachen Spezialfall eingehen: das Arbeiten mit einer T e x t d a t e i .
Lesen von einer Textdatei Eine Textdatei ist eine Datei, die Text enthält, der aus Zeichen besteht, die zum Zeichensatz des Rechners gehören. Sie muß also nicht unbedingt Texte der deutschen Sprache enthalten, sondern kann auch Zahlen (in Zeichendarstellung, nicht in rechnerinterner Darstellung als Dualzahl) oder ein Pascal-Quellprogramm enthalten. Man kann in eine Textdatei also z u m Beispiel mit einem Texteditor die Daten hineinschreiben, die man später mit einem P r o g r a m m verarbeiten möchte.
96
WEIS
12 Datenstrukturen I
muß man tun, wenn ein Programm Daten von einer Textdatei lesen soll?
Zunächst muß man dem Rechner mitteilen, daß man mit einer Datei arbeiten möchte. Das tut man, indem man eine Variable vom Typ t e x t deklariert. (Der Typ text ist ein spezieller vordefinierter Dateityp. Doch dazu mehr in Lektion 15 !) Man schreibt also: var :
text;
Zum Beispiel: var datei:
text;
Nun hat man für die Datei also einen Variablennamen, über den man sie vom Programm aus ansprechen kann. In Standard-Pascal muß dieser Dateiname als zusätzlicher Ein-/Ausgabe-Parameter im Programmkopf genannt werden. Dies ist bei vielen anderen Pascal-Ubersetzern nicht erforderlich. Beispiel: ^ p r o g r a m haeufigkeit
(input, Output,
datei);
Nehmen wir nun an, wir hätten mit dem Texteditor eine Datei mit dem Namen "roman.txt" erstellt und wollen uns nun einen Uberblick über die Buchstabenhäufigkeit in unserem "Roman" verschaffen. Dazu müssen wir dem Programm mitteilen, daß es unter dem Namen d a t e i die Datei "roman.txt" ansprechen soll. Wir müssen also sozusagen der Variablen d a t e i die Datei "roman.txt" zuweisen. Wie dies gemacht wird, ist sehr stark vom Rechner und vom Übersetzer abhängig. Auf keinen Fall geht es aber mit einer einfachen Zuweisung. In Turbo-Pascal muß die entsprechende Anweisung z. B. lauten: assign(datei,'roman.txt')
Da wir aber mit unserem Programm nicht immer nur die Datei "roman.txt" bearbeiten wollen, sondern auch mal andere Dateien, ist es sehr sinnvoll, wenn man sich für den "wirklichen" Dateinamen eine Variable vom Typ s t r i n g deklariert, var datnam:
string[20];
den Dateinamen von der Tastatur einliest und dann schreibt: assign(datei,datnam)
Nun müssen wir die Datei noch zum Lesen öffnen. Dazu schreiben wir: reset(datei)
12.2 Eindimensionale Felder
97
Da wir nun manchmal von der Tastatur lesen und manchmal von der Datei, müssen wir beim Aufruf der read-Prozedur angeben, von wo gelesen werden soll:
^
read(...)
ohne zusätzliche Angaben ist identisch mit T r e a d (input,
...)
und liest von der Standardeingabedatei, also der Tastatur. read(datei,
...)
\
liest von der Datei, die im Programm den Namen d a t e i hat. In Lektion 7 haben Sie erfahren, wie sich die Prozedur read beim Lesen von der Tastatur verhält. Nun wollen wir kurz darauf eingehen, wie sich die Prozedur read beim Lesen von einer Textdatei verhält. Das im folgenden beschriebene Verhalten gilt für Standard-Pascal generell, also auch beim Lesen von der Tastatur. Vorab ist es wichtig zu wissen, daß ein Zeilenende in einer Textdatei einfach durch ein bestimmtes Sonderzeichen aus dem Zeichensatz des Rechners dargestellt wird. Beim Lesen von der Tastatur entspricht jedes Drücken der Eingabetaste einem Zeilenende-Zeichen in der Textdatei. Für eine Variable vom Typ char liest read genau ein Zeichen. Dieses Zeichen kann zum Beispiel auch das Zeilenende-Zeichen sein! Die nächste read-Anweisung beginnt mit dem Zeichen, das auf das soeben gelesene Zeichen folgt. Für eine Variable vom Typ s t r i n g liest read so viele Zeichen wie möglich, bis das Zeilenende bzw. das Dateiende erreicht ist oder die Zeichenkette bis zu ihrer maximalen Länge gefüllt wurde. Die nächste read-Anweisung beginnt mit dem Zeichen, das auf das zuletzt gelesene unmittelbar folgt, also eventuell mit dem Zeilenende-Zeichen oder dem Dateiende-Zeichen. Für eine Variable vom Typ i n t e g e r oder r e a l werden zunächst alle Leerzeichen und Zeilenenden übersprungen. Wenn die dann folgende Zeichenfolge nicht der vorgeschriebenen Darstellung für ganze bzw. reelle Zahlen entspricht, tritt ein Fehler auf. Die Zahl wird eingelesen, bis ein Zeichen gelesen wird, das nicht mehr zur Zahl gehören kann (in vielen Pascal-Dialekten muß dies häufig ein Leerzeichen, ein Zeilenende oder das Dateiende sein). Die nächste read-Anweisung beginnt mit dem Zeichen, das nicht mehr zu der eben gelesenen Zahl gehört.
98
12 Datenstrukturen I
Die r e a d l n - A n w e i s u n g verhält sich ebenso wie die read-Anweisung bis auf die T a t s a c h e , daß die nächste r e a d - bzw. r e a d l n - A n w e i s u n g immer mit dem ersten Zeichen der nächsten Zeile beginnt. Nach der A b a r b e i t u n g der P a r a m e t e r l i s t e werden also alle restlichen Zeichen der aktuellen Zeile einschließlich des Zeilenendes übersprungen. Dazu ein Beispiel: Nehmen wir an, Sie wollen in S t a n d a r d - P a s c a l von der T a s t a t u r eine Zahl einlesen. Sie verwenden dazu die read-Anweisung, geben die gewünschte Zahl ein und schließen die Eingabe mit der E i n g a b e t a s t e ab. M i t der nächsten read-Anweisung möchten Sie drei Zeichen vom T y p c h a r einlesen. Sie geben a b c ein und drücken wieder auf die E i n g a b e t a s t e . Leider werden Sie nicht das gewünschte Ergebnis erhalten. Die drei Zeichen, die Sie eingelesen h a b e n , sind nämlich das Zeilenende-Zeichen, ein a und ein b. Die nächste r e a d Anweisung beginnt m i t dem Zeichen c . Sie h ä t t e n die Zahl also unbedingt m i t einer r e a d l n - A n w e i s u n g einlesen müssen! Nun reicht's aber. K e h r e n wir lieber zu unserer T e x t d a t e i und unserem Beispielprogramm h a e u f i g k e i t zurück: Wenn wir die zu bearbeitenden Daten von einer Datei einlesen, brauchen wir nicht mehr unsere Endekennung '#', sondern wir können solange lesen bis wir das Dateiende erreicht h a b e n . Dazu gibt es die Standardfunktion e o f (end of file). Diese Funktion liefert einen Wert vom T y p b o o l e a n , und zwar f a l s e , solange das Dateiende noch nicht erreicht ist, und t r u e , sobald das Dateiende erreicht ist. Als P a r a m e t e r muß der D a t e i n a m e angegeben werden, dem die Datei zugewiesen ist, über die man die Information haben möchte: ^eof(datei)
J
Zum Schluß, wenn das Arbeiten mit der Datei beendet ist, muß die Datei wieder geschlossen werden. Dies geschieht durch: ^close(datei) D a m i t ergibt sich nun endlich folgendes P r o g r a m m : \ program haeufigkeit; { Dieses P r o g r a m m liest zunaechst d e n Neimen einer Datei ein. Dann liest es Z e i c h e n fuer Z e i c h e n die genannte Datei und bestimmt fuer alle Buchstaben von 'a' bis 'z' deren relative Haeufigkeit, b e z o g e n auf die Gesamtzahl aller vorkommenden Buchstaben. Gross-/Kleinschreibung w i r d nicht unterschieden. Z u m Schluss w e r d e n die ermittelten Werte ausgegeben. } -[ ******************** Vereinbarungsteil ******************** type buchstabe =
'a'..'z';
}
99
12.2 Eindimensionale Felder
v a r a n z : a r r a y [ b u c h s t a b e ] of summe, abstand : integer; zeichen : char; datnam : string[20]; datei : text; { *********************
integer;
Anweisungsteil **********************
}
begin { * H a e u f i g k e i t s t a b e l l e u n d S u m m e auf 0 i n i t i a l i s i e r e n * 3* f o r z e i c h e n := 'a' t o 'z' d o a n z [ z e i c h e n ] := 0; s u m m e := 0; { Abstand zwischen Gross - und Kleinbuchstaben bestimmen a b s t a n d := o r d ( ' a ' ) - o r d ( ' A ' ) ;
}
{ » » * Dateineune e i n l e s e n u n d D a t e i z u m L e s e n o e f f n e n *** w r i t e ( ' V o n welcher Datei soll gelesen w e r d e n ? '); readln(datnam); assign(datei,datnam); reset(datei);
}
repeat { **** E i n l e s e n des naechsten Zeichens **** } read(datei,zeichen); { **** U m w a n d e l n v o n G r o s s - in K l e i n b u c h s t a b e n **** } if ( z e i c h e n >= 'A') a n d ( z e i c h e n if ( z e i c h e n >= 'a') a n d ( z e i c h e n close(datei);
V
{ **** A u s d r u c k e n der relativen Haeufigkeiten **** } f o r z e i c h e n := 'a' t o 'z' d o begin w r i t e ( z e i c h e n , a n z [ z e i c h e n ] / s u m m e * 1 0 0 : 5 : 1 ' / , '); { **** alle 5 Buchstaben neue Zeile beginnen **** } if ( ( o r d ( z e i c h e n ) - o r d ( ' a ' ) + 1) m o d 5) = 0 then writeln end; writeln end.
J
100
12 D a t e n s t r u k t u r e n I
B e i s p i e l p r o g r a m m skalarprodukt Sehr häufig werden Felder dazu verwendet, um in mathematischen Programmen V e k t o r e n zu repräsentieren. Wir wollen nun ein solches Anwendungsbeispiel betrachten, bei dem das Skalarprodukt zweier Vektoren berechnet wird. Auf die mathematische Bedeutung eines Vektors wollen wir hier nicht näher eingehen. Es soll uns genügen zu wissen, daß ein Vektor der Länge n aus n Komponenten (Einzelwerten) besteht und daß das Skalarprodukt zweier Vektoren der Länge n definiert ist als die Summe der Produkte der einzelnen Komponenten:
a2
ibi\ =
V>>> Prozedur zum Ausgeben des Feldinhalts
< < < < < }•
{ ***** Vereinbarungsteil Prozedur "ausgeben" v a r i: integer; { Laufvariable fuer Schleife
***** > }
{ ***** begin
*****
Anweisungsteil
Prozedur
"ausgeben"
}
f o r i := 1 t o n d o w r i t e (liste [i] : 6) ; writeln end; procedure
sortieren;
i >>>>> Prozedur zum Sortieren des Feldes B i s h e r n o c h n i c h t r e a l i s i e r t !!! >> Prozedur zum Sortieren des Feldes nach dem Verfahren "Sortieren durch Austauschen". Weitere Erlaeuterung siehe Struktogramm.
V
begin a := 0 ; b := 0 ; p (a,b); writeln (a,b) end.
{ Aufruf d e r P r o z e d u r p )• y1
Das P r o g r a m m erzeugt die folgende Ausgabe:
Der Formalparameter x der Prozedur p ist ein Wertparameter. Beim Aufruf der Prozedur p wird also der Aktualparameter, der aus der Variablen a besteht, a u s g e w e r t e t und der resultierende Wert 0 an den Wertparameter x übergeben. Der Formalparameter y der Prozedur p ist ein Variablenparameter. Innerhalb der Prozedur p bekommt die als Aktualparameter übergebene Variable b also den Namen y. Innerhalb der Prozedur p wird nun zu x und zu y jeweils 1 addiert. Danach hat die "lokale Variable" x den Wert 1. Ebenso hat die Variable b, die innerhalb der Prozedur auf den Namen y hört, nun den Wert 1. x und y werden ausgegeben:
Die Prozedur p wird beendet, und wir kehren wieder ins Hauptprogramm zurück. Die Variable a ist nun nach wie vor unverändert. Sie hat immer noch den Wert 0. Die Variable b hingegen wurde im Unterprogramm verändert. Sie h a t nun den Wert 1. Daher die Ausgabe:
13.1 P r o z e d u r e n
131
N u n h a b e n Sie also gelernt, wie m a n selbst Prozeduren schreiben kann. Vielleicht ist Ihnen aufgefallen, daß wir für unsere selbstgeschriebenen Prozeduren den Dat e n t y p der P a r a m e t e r immer genau festlegen mußten. Standardprozeduren wie z. B. r e a d o d e r w r i t e sind da sehr viel flexibler. Solche Privilegien bleiben dem normalen Pascal-Programmierer jedoch verwehrt. Die N a m e n der Standardprozeduren sind aber ganz normale Namen, keine Wortsymbole. D a h e r können Sie z. B. den Namen r e a d auch für eine Ihrer selbstgeschriebenen Prozeduren verwenden. Dies wird aber im allgemeinen nicht sinnvoll sein, denn d e m Blockkonzept von Pascal folgend, würde damit die s t a n d a r d m ä ß i g vordefinierte Prozedur r e a d unansprechbar werden.
Kontrollaufgaben K.13.1
Aus welchen Teilen besteht der Vereinbarungsteil eines PascalP r o g r a m m s ? In welcher Reihenfolge müssen diese Teile in S t a n d a r d Pascal stehen?
K.13.2
In welchem Teil eines Pascal-Programms (besser: eines Pascal-Blocks) k ö n n e n Prozeduren deklariert werden?
K.13.3
Was versteht man unter einem globalen Bezeichner im Gegensatz zu einem lokalen Bezeichner? Was ist der Gültigkeitsbereich eines Bezeichnen?
K.13.4
Welche Eigenschaften h a b e n Wertparameter im Gegensatz zu Varia b l e n p a r a m e t e r n ? Wie werden sie in der Formalparameterliste unterschieden?
K.13.5
W a n n verwendet m a n W e r t p a r a m e t e r und wann Variablenparameter?
K.13.6
Inwieweit muß die Aktualparameterliste b e i m Aufruf einer Prozed u r mit der Formalparameterliste der Prozedurdeklaration übereinstimmen?
K.13.7
Welcher Wert wird von folgendem P r o g r a m m ausgedruckt?
program g l o b a l ; var i :
integer;
procedure lokal; var i : integer; begin i := 1 end; begin i := 0 ; lokal; write(i) end.
132
13 Prozeduren und Funktionen
K.13.8
Welche Werte werden von folgendem Programm ausgedruckt?
program
Schachtel;
var zahl:
integer;
p r o c e d u r e box; var zahl: integer; procedure k i s t e ; var zahl: integer; begin { k i s t e } z a h l := 3; vrite(zahl) end; { k i s t e } b e g i n { box } z a h l := 2; kiste; vrite(zahl) end; { box ]• begin { Schachtel } z a h l := 1; box; write(zahl) end. { Schachtel }
V
K.13.9
J
Was geschieht, wenn man im Programm s c h a c h t e l den Anweisungsteil des Hauptprogramms folgendermaßen verändert:
b e g i n { Schachtel } zahl := 1; kiste; write(zahl) end. { Schachtel }
13.2 Funktionen
13.2
133
Funktionen
Neben den Standardprozeduren gibt es j a auch noch Standardfunktionen. Nun sagen Sie bloß, Sie wollen auch noch wissen, wie Sie s e l b s t Funktionen schreiben können? Also gut: Der wichtigste Unterschied zwischen Prozeduren und Funktionen ist, daß Funktionen im Gegensatz zu Prozeduren innerhalb von Ausdrücken aufgerufen werden können, weil sie an die Stelle ihres Aufrufes einen Wert zurückliefern. Zum Beispiel:
^ y : = x » sin(alpha) Standardmäßig vordefiniert, gibt es die Funktionen s i n und cos für die Berechnung von Sinus und Cosinus. Schreiben wir uns also eine eigene Funktion für die Berechnung des Tangens:
function tan (winkel: real): real; begin tan := sin(winkel)/cos(vinkel) end; Funktionsdeklarationen beginnen mit dem Wortsymbol f u n c t i o n , gefolgt von dem F u n k t i o n s n a m e n . Dann folgt die F o r m a l p a r a m e t e r l i s t e , für die die gleichen Regeln gelten wie für Prozeduren. Im Anschluß an die Formalparameterliste folgt nach einem Doppelpunkt der R e s u l t a t t y p der Funktion. Der Funktionskopf w i r d durch ein Semikolon abgeschlossen. Dann folgt der V e r e i n b a r u n g s t e i l der Funktion (im obigen kurzen Beispiel noch leer). Der A n w e i s u n g s t e i l einer Funktion besteht wie bei den Prozeduren aus einer Verbundanweisung. Die gesamte Funktionsdeklaration wird durch ein Semikolon abgeschlossen. Prozedur- und Funktionsdeklarationen können am Ende jedes Vereinbarungsteils in beliebiger Reihenfolge stehen, also auch durcheinander. Alles, was wir über Prozeduren gelernt haben, gilt entsprechend auch für Funktionen. Insbesondere was die Gültigkeitsbereiche von Namen und die Arten der Parameterübergabe angeht. Die wichtigsten Unterschiede werden im folgenden erläutert: Funktionen werden in Ausdrücken aufgerufen. Unsere Tangens-Funktion könnte beispielsweise so aufgerufen werden: ^z
:= 1 - tan(gamma)
J
Der Resultattyp einer selbstgeschriebenen Funktion muß im Gegensatz zu manchen Standardfunktionen eindeutig festgelegt werden. Außer den strukturierten Datentypen sind alle Pascal-Datentypen als Resultattyp einer Funktion erlaubt. Als Resultattyp muß ein T y p n a m e angegeben werden. Bevor die Funktion verlassen wird, muß das F u n k t i o n s r e s u l t a t d e m F u n k t i o n s n a m e n z u g e w i e s e n worden sein !!!
134
13 Prozeduren und Funktionen
Sehen wir uns doch a m besten noch ein p a a r Beispiele an: f u n c t i o n fak (n: i n t e g e r )
: integer;
{ D i e s e F u n k t i o n b e r e c h n e t d i e F a k u l t a e t e i n e r ganzen Zahl n . } { ******************** V e r e i n b a r u n g s t e i l ******************** } var f ,
i : integer;
-[ ********************** A n w e i s u n g s t e i l ********************* )• begin f := 1; if n < 0 t h e n w r i t e l n ( ' F a k u l t a e t von n e g a t i v e r Zahl eise for i : = l t o n d o f : = f • i;
!')
f a k := f end; Die Fakultät einer ganzen Zahl, geschrieben n ! , ist definiert als das Produkt aller ganzen Zahlen kleiner gleich n: n! = l * 2 * 3 * - - - * ( n — 1) * n Zum Beispiel: 5! = 1 * 2 * 3 * 4 * 5 = 120 0! ist per definitionem gleich 1. Die Anweisung produkt
:= f a k ( 3 )
weist der Variablen p r o d u k t den Wert 6 zu. Die Verwendung der lokalen Variablen f in der obigen Funktion ist notwendig, weil der Funktionsname f a k nicht wie eine normale Variable Teil eines Ausdrucks sein darf. f a k := f a k » i würde zu einer Fehlermeldung des Ubersetzers führen. Nur als F u n k t i o n s a u f r u f darf f a k in einem Ausdruck auftreten: p := f a k ( n ) / ( f a k ( k ) • f a k ( n - k ) )
J
135
13.2 Funktionen
Im nächsten Beispiel wollen wir den Binomialkoeffizienten " n über k" berechnen. Man schreibt ihn auch:
Die Binomialkoeffizienten lassen sich anschaulich a m besten über das sogenannte " P a s c a l s c h e Dreieck" darstellen, das nach d e m Mathematiker Blaise Pascal benannt ist, der auch der Programmiersprache, die Sie gerade lernen, seinen N a m e n geliehen hat. D a s Pascalsche Dreieck sieht so aus: Zeile N r . 0 1 2 3 4 5 6 7 8 9
1 1
1 8
4 5
6
7
1 1
1
1 3
6 10
15 21
28
1
2
3
1 1
1
20 35
56
1 4
10
1 5
15 35
70
6 21
56
28
. u. s. w. J e d e Zahl in diesem Dreieck ist die S u m m e der beiden Zahlen, die eine Zeile weiter oben direkt links und rechts über der Zahl stehen. (Beispiel: Zeile 6: 20 = 10 + 10) Der Binomialkoeffizient " n über k" entspricht in diesem Dreieck dem ( k + l ) - t e n Element in Zeile n. " 5 über 2" ist zum Beispiel das 3. Element in Zeile 5, also 10. Es muß gelten: 0
function fak (n: integer) : integer; var f, i :
integer;
begin { fak > f := 1; for i := 1 to n do f := f * i; fak := f end; { fak > { ********************** Anweisungsteil ********************* } begin { bin } if (n
integer;
Deklaration der Funktion quadraht mit SEITENEFFEKT ! ! ! « «
function quadraht (x: integer): integer; { berechnet das Quadrat von x > begin zenzi := zenzi - x; { Seiteneffekt } quadraht := sqr(x) end; { ********************** Anweisungsteil ********************* } begin zenzi := 10; anton := quadraht(zenzi); writeln (anton, zenzi); zenzi := 10; anton := quadraht(lO) * quadraht(zenzi); writeln (anton, zenzi); zenzi := 10; anton := quadraht(zenzi) * quadraht(10); writeln (anton, zenzi) end. Dieses P r o g r a m m liefert die Ausgabe:
Überlegen Sie selbst, warum! Zum Abschluß der Lektion über Prozeduren und Funktionen sollen ein paar Besonderheiten nicht unerwähnt bleiben.
138
13 Prozeduren und Punktionen
So wie man Prozeduren ohne Parameter schreiben kann, kann man natürlich auch parameterlose Funktionen schreiben. Im Gegensatz zu parameterlosen Prozeduren, die oft verwendet werden, gibt es aber nur sehr wenige sinnvolle Anwendungen für parameterlose Funktionen. In Standard-Pascal ist vorgesehen, daß an Prozeduren und Funktionen nicht nur Wert- und Variablenparameter übergeben werden können, sondern auch Prozedur- und Funktionsparameter. Man kann also zum Beispiel eine Funktion schreiben, die Werte eines Polynoms berechnet, und dann den Namen dieser Funktion über einen Parameter an eine Prozedur übergeben, die dann die von dieser Funktion gelieferten Funktionswerte des Polynoms graphisch auf dem Bildschirm darstellt. In Pascal-Versionen, die auf kleinen Rechnern, wie z. B. Arbeitsplatzrechnern, laufen, ist die Ubergabe von Prozedur- oder Funktionsparametern meist nicht möglich. Manche Pascal-Versionen bieten ferner die Möglichkeit, variabel lange Felder oder Zeichenketten (strings) variabler Länge an Prozeduren oder Funktionen als Aktualparameter zu übergeben. Wenn Sie genau wissen wollen, welche Arten von Parametern Ihr Übersetzer akzeptiert, dann befragen Sie bitte Ihr UbersetzerHandbuch. Noch eine Anmerkung: Es kann zu Fehlern führen, wenn Sie eine Funktion, in der read, r e a d l n , w r i t e oder w r i t e l n aufgerufen wird, in der Ausgabeliste einer w r i t e - oder writeln-Anweisung aufrufen!
Kontrollaufgaben Die folgenden Kontrollaufgaben beziehen sich auf die gesamte Lektion 13. K.13.10
Geben Sie an, welche der folgenden Prozedur- bzw. Funktionsparameter Wertparameter und welche Variablenparameter sind: \
procedure liesliste (var liste:feld); procedure inhalt (radius:real; var v:real; dim:integer); procedure xyz (var a, b, c:real; i, j:integer); function cot (winkel: V
real);
function max (var kettel, kette2:
feld);
K.13.11
Worin unterscheidet sich eine Funktion von einer Prozedur? Welchen Einfluß hat dies auf die Form des Aufrufs?
K.13.12
Wann sollte man eine Funktion verwenden, wann eine Prozedur?
K.13.13
Was versteht man unter Seiteneffekten?
K.13.14
Schreiben Sie eine Funktion "hoch", die als Argumente eine reelle Zahl x und eine positive ganze Zahl y erhält, und als Resultat x^ liefert.
139
13.2 Funktionen
K.13.15
Was wird von folgendem Programm ausgedruckt?
program obacht; var a,b:
integer;
procedure p (m: i n t e g e r ; var n: begin m := m + n; n := 2 * m; writeln('p',m,n) end;
integer);
procedure q ( v a r m: i n t e g e r ; n: begin m := n + m; n := m * 2; writeln('q',m,n); p(m,n); w r i t e i n ( ' q',m,n) end;
integer);
begin "C hauptprogramm } a := 2; b := 3; q(a,b); writeln('haupt',a,b); p(a,b); writeIn('haupt',a,b) end. V
Übungsaufgabe:
Bitte bearbeiten Sie jetzt die Übungsaufgabe 5.
140
Lektion 14
Datenstrukturen II 14.1
Verbundtypen
Die einzelnen Komponenten eines Feldes müssen alle vom gleichen Typ sein. Im Gegensatz dazu bietet der Verbundtyp (Satztyp, r e c o r d - T y p ) die Möglichkeit, K o m p o n e n t e n unterschiedlichen T y p s in einer Datenstruktur zusammenzufassen. Betrachten wir folgendes Beispiel: Der Weinhändler Rebstöckl möchte sein Angebot über seinen Arbeitsplatzrechner verwalten. Er möchte für jede Weinsorte aus seinem Angebot den Jahrgang, die Rebsorte, das Anbaugebiet, die Qualität und den Preis speichern. Da er die Pascal-Fibel bisher nur bis einschließlich Lektion 13 durchgearbeitet hat, entschließt er sich, für seine maximal 500 Weine die folgende Datenstruktur anzulegen: var name jahrgang rebsorte
: array [1..500] of string[30]; : array [1..500] of 1900..2000; : array [1..500] of (riesling, silvaner, kerner, muellerthurgau, rulaender, burgunder); anbaugebiet: array [1..500] of (rheinpfalz, rheinhessen, rheingau, mosel, nahe, ahr, baden, franken, elsass , wuerttemberg); qualitaet : array [1..500] of (tafelwein, qualitaetswein, kabinett, spaetlese, auslese, beerenauslese, trockenbeerenauslese, eiswein); preis : array [1..500] of real;
Hin und wieder kommt es natürlich vor, daß alle Vorräte einer Weinsorte verbraucht sind. Wenn die Sorte nicht mehr zu beschaffen ist, müssen alle folgenden Einträge im Feld um eine Position aufrücken.
14.1 Verbundtypen
141
Das programmiert der Weinhändler so: { delnr ist der Index der zu loeschenden weinsorte > { maxnr ist der Index des letzten belegten Feldelements }
V
for i := delnr to maxnr-1 do begin name[i] := name[i+l]; jahrgang[i] := jahrgang[i+1]; rebsorteti] := rebsorte[i+1]; anbaugebiet[i] := anbaugebiet[i+1]; qualitaet[i] := qualitaet[i+1]; preis[i] := preis[i+l] end;
Es sind also für jede Weinsorte sechs einzelne Zuweisungen notwendig. "Das ist ja ziemlich umständlich!", sagt sich der Weinhändler. Von seinem Freund, dem Weinhändler Zucker, der im Studium der Pascal-Fibel schon weiter fortgeschritten ist, bekommt Rebstöckl den Tip, Lektion 14 zu lesen, denn dort erfahre man etwas über die Datenstruktur Verbund. Wie bereits erwähnt, ist der Verbund eine Datenstruktur, deren Komponenten unterschiedliche Datentypen haben können. Die einzelnen Komponenten eines Verbunds werden nicht wie bei Feldern über Indexwerte angesprochen, sondern über Namen. Einen Verbund definiert man, indem man alle seine Komponenten explizit aufzählt. Die Aufzählung wird durch die Wortsymbole record und end eingeschlossen. Für jede Komponente muß ein Name und der Datentyp angegeben werden. Die Namen müssen innerhalb eines Verbunds eindeutig sein. Die Reihenfolge, in der die Komponenten aufgezählt werden, ist unerheblich. Die syntaktischen Regeln entsprechen den bei Variablendeklarationen geltenden Regeln. Komponenten gleichen Typs können demnach auch hier, durch Kommas getrennt, einer einzigen Typangabe vorangestellt werden. Beispiel: type beispiel
record namel name2 ) name3,name4 name5,name6 name7 end;
: : : :
typl; typ2; typ3; typ4
Nach diesen Erkenntnissen entschließt sich der Weinhändler Rebstöckl, seine Datenstruktur anders aufzubauen. Er definiert sich zunächst einen Verbund, der alle Daten für eine Weinsorte aufnehmen kann:
142
14 Datenstrukturen II
type s o r t e = record name Jahrgang rebsorte
: s t r i n g [30] ; : 1900..2000; : ( r i e s l i n g , s i l v a n e r , muellerthurgau, k e r n e r , r u l a e n d e r , burgunder); anbaugebiet: ( r h e i n p f a l z , rheinhessen, r h e i n g a u , mosel, nahe, a h r , baden, franken, e l s a s s , vuerttemberg); qualitaet : (tafelwein, qualitaetswein, kabinett, spaetlese, auslese, beerenauslese, trockenbeerenauslese, eiswein); preis : real end;
Anschließend deklariert er ein Feld, das aus 500 solcher Datensätze besteht: i v a r veinliste:
array
[1..500]
of
sorte;
So wie man ganze Felder, wenn sie denselben Feldtyp haben, einander zuweisen kann, kann man auch g a n z e V e r b ü n d e , wenn sie denselben Verbundtyp haben, einander z u w e i s e n . Wie bei den Feldern ist die Zuweisung die einzige mit ganzen Verbunden erlaubte Operation. Das kurze Programmstück zur Korrektur der Weinliste vereinfacht sich nun erheblich: f o r i : = delnr t o maxnr-1 do w e i n l i s t e [ i ]
:=
weinliste[i+1];
Durch die Anweisung im Schleifenrumpf wird der gesamte Verbund, der das ( i + l ) - t e Element des Feldes w e i n l i s t e bildet, in das i-te Element des Feldes übertragen. Nochmal zur Erinnerung: Die Zuweisung ist die einzige Operation, die für ganze Verbünde zugelassen ist. Ganze Verbünde können also nicht miteinander verglichen werden. Ein V e r g l e i c h muß k o m p o n e n t e n w e i s e durchgeführt werden. Ebenso muß die E i n - / A u s g a b e von Verbunden immer k o m p o n e n t e n w e i s e vorgenommen werden. Angenommen, wir hätten den Verbundtyp type Spieler = record vorname, nachname: alter: 1..99 end;
string [20];
143
14.1 V e r b u n d t y p e n
und folgende Variablen deklariert: var mannschaft: a r r a y [ 1 . . 2 0 ] of kapitaen: Spieler;
Spieler;
_J
Dann kann man nicht schreiben ^writeln('Kapitaen:',
kapitaen)
sondern muß stattdessen jede Komponente des Verbunds einzeln ausgeben. Wie spricht man eine Komponente eines Verbunds an? Da jede Komponente einen Namen hat, spricht man sie natürlich mit ihrem Namen an. Der Komponentenname allein genügt aber nicht, er muß dem Verbundnamen, getrennt durch einen Punkt, beigefügt werden. Die Ausgabe der Daten des Mannschaftskapitäns sieht dann z. B. so aus: writeln('Kapitaen: '.kapitaen.vorname,' ' , kapitaen.nachname, k a p i t a e n . a l t e r : 3 ) Machen wir uns nochmal die Zusammenhänge klar: Die Variable mannschaft ist ein Feld, dessen Elemente den Verbundtyp s p i e l e r haben, mannschaft [5] ist das fünfte Element des Feldes mannschaft, also ein ganzer Verbund vom Typ s p i e l e r . Durch mannschaft [5] .vorname wird eine Komponente des Verbunds mannschaft[5] (also eine Komponente des fünften Elements des Feldes manns c h a f t ) bezeichnet. Die Mannschaft könnte man also so ausgeben:
r
f o r i := 1 t o 20 do w r i t e l n ( m a n n s c h a f t [ i ] . v o r n a m e , ' mannschaft[i].nachname, mannschaft [ i ] . a l t e r : 3)
Die Ausgabeanweisung ^ w r i t ein (vorname) ist unzulässig. Wessen Vorname soll gemeint sein? Es gibt schließlich zwanzig Spieler und einen Kapitän! Ein Komponentenname allein ist ebenso unsinnig wie ein Index ohne den Namen des anzusprechenden Feldes. Es kann aber in verschiedenen V e r b u n d e n d e r gleiche K o m p o n e n t e n n a m e verwendet werden, und außerdem kann der Komponentenname noch als normaler Variablenname auftauchen.
144
14 D a t e n s t r u k t u i e n II
Wenn wir zusätzlich zu unserer Mannschaft noch die Variablen v a r whisky: r e c o r d name: string[20]; s o r t e : ( s c o t c h , bourbon, c a n a d i a n , a l t e r : 1..20 end; alter: integer;
malt);
deklarieren, dann sind mannschaft[2].alter whisky.alter alter gültige Bezeichner. Der Sohn eines Spielers aus der Altherren-Mannschaft könnte gar auf die Idee kommen, für seinen Alten eine Variable var a l t e r :
Spieler;
zu deklarieren. Dann wäre auch alter.alter ein gültiger Bezeichner. Solche Scherze sollten aber zugunsten der Übersichtlichkeit des Programms vermieden werden! Als Komponenten von Verbunden sind natürlich auch wieder Verbünde zugelassen. Als Beispiel diene der leicht modifizierte Verbundtyp spieler: type spieler = record vorname, nachname: s t r i n g [ 2 0 ] ; geburtsdatum: record tag: 1..31; monat: 1. . 12; jähr: 1900..2000 end end; Ausgehend von dieser Definition des Verbundtyps s p i e l e r bezeichnet kapitaen
einen ganzen Verbund vom Typ s p i e l e r ,
kapitaen.geburtsdatum
eine Komponente des Verbunds k a p i t a e n , die den Verbundtyp record t a g : 1. .31; monat: 1. .12; jähr: 1900..2000 end hat und
145
14.1 Verbundtypen
k a p i t a e n . g e b u r t sdatum.jahr die Komponente j ä h r des Verbunds g e b u r t s datum, der eine Komponente des Verbunds k a p i t a e n ist. Alle Komponenten von Verbunden haben genau die Eigenschaften, die auch eine ganz normale Variable vom gleichen Typ hat. kapitaen.geburtsdatum. j ä h r kann also im Programm genauso verwendet werden wie jede andere Variable, die den Teilbereichstyp 1900. .2000 hat.
Komplexe Zahlen Ein klassisches Beispiel für die Verwendung eines Verbunds bieten die K o m p l e x e n Z a h l e n . Die Komplexen Zahlen stellen eine Erweiterung der Reellen Zahlen dar, die zum Beispiel durch die Tatsache motiviert wird, daß die Gleichung
Eine Komplexe Zahl besteht dann aus einem R e a l t e i l und einem I m a g i n ä r t e i l . Und es gilt : z = x + ty Re(z) = x Im(z) = y
mit: z komplex x,y reell
Die quadratische Gleichung x2 - 2x + 5 hat dann nach der Lösungsformel für quadratische Gleichungen (vgl. Lektion 8) die Lösungen:
2 = - § - y/'r-q
x
p = —2 und 9 = 5 eingesetzt: x i = 1 + \/l — 5 = 1+ ^=4 = 1 + 2i x 2 = 1 - 2t
146
14 Datenstiukturen II
W ä h r e n d es in manchen anderen Programmiersprachen f ü r Komplexe Zahlen einen speziellen D a t e n t y p gibt, muß m a n sich in Pascal mit einem Verbund behelfen: t y p e complex = r e c o r d r e , im : end; v a r x l , x2 :
real
complex;
Eine Zuweisung von 1 - 2 i an die Variable x2 müßte d a n n so aussehen: x 2 . r e := 1; x 2 . i m := - 2 ; Selbstverständlich kann m a n in Pascal nicht mit dem V e r b u n d t y p complex rechnen, sondern muß alles auf Operationen zwischen reellen Zahlen zurückführen. Die Zuweisung := ( - p / 2 ) + s q r t ( s q r ( p ) / 4 - q)
J
ist z w a r nicht schlecht, aber leider v ö l l i g f a l s c h !
Die with-Anweisung Beim Arbeiten mit Verbunden kommt es oft vor, daß in einem begrenzten Programmteil K o m p o n e n t e n eines bestimmten Verbundes häufig angesprochen werden. Betrachten wir dazu folgendes Beispiel: k a p i t a e n . v o r n a m e := ' w e r n e r ' ; k a p i t a e n . n a c h n a m e := ' b r o e s e l ' ; k a p i t a e n . g e b u r t s d a t u m . t a g := 11; k a p i t a e n . g e b u r t s d a t u m . m o n a t : = 11; k a p i t a e n . g e b u r t s d a t u m . j ä h r : = 55; Es ist lästig, daß m a n immer wieder den kompletten, aus dem V e r b u n d n a m e n und d e m K o m p o n e n t e n n a m e n bestehenden, Variablennamen angeben muß. Erleichterung schafft die w i t h - A n w e i s u n g . Sie hat die Form: w i t h < V e r b u n d v a r i a b l e > do Alle K o m p o n e n t e n der Verbundvariablen, die zwischen den Wortsymbolen w i t h und do aufgeführt ist, können dann innerhalb der auf das Wortsymbol do folgenden Anweisung allein durch ihren Komponentennamen angesprochen werden.
147
14.1 Verbundtypen
Beispiele: w i t h k a p i t a e n do b e g i n v o r n a m e := 'werner'; n a c h n a m e := 'broesel' end;
V w i t h k a p i t a e n . g e b u r t s d a t u m do b e g i n tag := 11; m o n a t := 11; jähr := 55 end;
w i t h k a p i t a e n do b e g i n v o r n a m e := 'charlie' n a c h n a m e := 'brown'; w i t h g e b u r t s d a t u m do
begin tag := 1; monat := 4 ; jähr := 69 end
end; Wie Sie sehen, wird die auf das Wortsymbol do folgende Anweisung in aller Regel eine Verbundanweisung sein (Die Bezeichnung Verbundanweisung h a t nichts m i t dem D a t e n t y p Verbund zu tun!!!). Weiter sehen Sie, daß m a n with-Anweisungen auch ineinander verschachteln kann. Erinnern Sie sich an die drei gültigen Bezeichner mannschaft[2].alter whisky.alter alter und betrachten Sie folgende Schachtelung von with-Anweisungen: a l t e r := 66; w i t h w h i s k y do b e g i n a l t e r := 12; with mannschaft[2] do begin alter sorte end; end
:= 29; := malt;
148
14 Datenstrukturen II
Was passiert? Außerhalb der vith-Anweisungen wird der Variablen a l t e r der Wert 66 zugewiesen. Innerhalb der äußeren v i t h - Anweisung wird der Variablen w h i s k y . a l t e r der Wert 12 zugewiesen. Die Variable a l t e r ist hier nun nicht mehr ansprechbar. Innerhalb der inneren with-Anweisung wird der Variablen mannschaft [2] . a l t e r der Wert 29 zugewiesen. Es wird also immer von innen nach außen vorgegangen. Man kann sich das etwa so vorstellen: Für die Zuweisung s o r t e := malt betrachten wir zunächst den engsten Gültigkeitsbereich. Gibt es eine Variable mannschaft[2] . s o r t e ? Nein. Also betrachten wir jetzt den nächstgrößeren Gültigkeitsbereich. Gibt es eine Variable whisky. s o r t e ? J a . Also weisen wir ihr den Wert malt zu. Statt einer Verschachtelung der Art
r
with whisky do with mannschaft[2] do
kann man auch kürzer schreiben T w i t h whisky, mannschaft[2] do . Man kann also mehrere Verbundvariablen, durch Kommas getrennt, angeben. Dabei kann eine Verbundvariable auch eine Komponente einer zuvor aufgeführten Verbundvariablen sein. Statt
V
with kapitaen do with geburtsdatum do
kann man also auch schreiben ^ w i t h k a p i t a e n , geburtsdatum do . . (Anmerkung zur Schreibweise: Lassen Sie hinter dem Komma eine Leerstelle! kapitaen,geburtsdatum kann sonst zu leicht mit kapitaen.geburtsdatum verwechselt werden!) Die R e i h e n f o l g e der aufgeführten Verbundvariablen ist bei der with-Anweisung also durchaus von B e d e u t u n g ! Bei der Verwendung der with-Anweisung mit einer Verbundvariablen, die Element eines Feldes ist, ergibt sich nicht nur eine erhebliche Schreiberleichterung, sondern auch eine Effizienzsteigerung. Da die Indexwerte erst während der Laufzeit des Programms berechnet werden, findet diese Berechnung bei Verwendung einer with-Anweisung wie ^ w i t h mannschaft [ i ] do
...
nur einmal statt, ohne with-Anweisung hingegen mehrfach, was die Laufzeit des Programms verlängert.
149
14.1 V e r b u n d t y p e n
D a die Berechnung des Indexwerts nur einmal zu Beginn der v i t h - A n w e i s u n g durchgeführt wird, darf der Wert innerhalb der Anweisung nicht mehr verändert werden.
C
\
v i t h m a n n s c h a f t [ i ] do b e g i n i
:= i + 1 ;
end v
y
wäre u n z u l ä s s i g ! Ebenfalls unzulässig ist innerhalb der v i t h - A n w e i s u n g eine Zuweisung an die Verbundvariable als Ganzes!
Verbund mit Variantteil Nun wollen wir noch kurz eine Erweiterung der Verbunddefinition besprechen, u n d zwar den V e r b u n d m i t v a r i a n t e m Teil. Dazu betrachten wir folgendes Beispiel: F ü r eine Bibliothek sollen Bücher und Zeitschriften erfaßt werden. Gespeichert werden soll die Signatur, der Titel, das Erscheinungsjahr u n d der Verlag. Bei Büchern sollen bis zu drei Autoren erfaßt werden, bei Zeitschriften hingegen die N u m m e r des Heftes. M a n könnte natürlich einen Verbund definieren, der sowohl Platz für drei Autoren als auch für die H e f t n u m m e r bietet, und die nicht b e n u t z t e n K o m p o n e n t e n durch einen besonderen Eintrag als " u n b e n u t z t " kennzeichnen. Es gibt aber auch die Möglichkeit, einen Verbund mit variantem Teil zu verwenden. Ein varianter Teil h a t syntaktisch die Form: c a s e < V a r i a n t e n s e l e k t o r > of :(); :();
< Selektorkonstanten-Liste>: ( < Komponenten-Liste > ) Eine V a r i a n t e besteht dabei aus einer Selektorkonstanten-Liste mit der zugehörigen Komponenten-Liste. Der V a r i a n t e n s e l e k t o r ist selbst eine Komponente des Verbundes und wird demnach ganz normal in der Form < K o m p o n e n t e n n a m e > : < T y p > angegeben. F ü r unser Beispiel bietet sich d a n n folgender Verbund an:
14 Datenstrukturen II
150
t y p e litart = (buch,Zeitschrift); eintrag = record Signatur: array [1..9] of char; jähr: 1000..9999; titel,verlag: string[50]; case art: litart of buch: (autorl,autor2,autor3: string[20]); Zeitschrift: (heftnr: integer) end; var kartei:
array
[1..9999] of
eintrag;
V
Ein Eintrag könnte dann so aussehen: w i t h kartei[247] do begin Signatur := '2365-8549'; jähr := 1985; titel := 'Wie sag ich''s meinem Computer?'; verlag := 'Algorithmic Press, Old York'; art := buch; autorl := 'Pascal, Blaise'; autor2 := 'Fortran, Frederic'; autor3 := '' end; V
J
oder so: v i t h kartei[248] do begin Signatur := '5367-2239'; jähr := 1986; titel := 'Gourmand - Das Magazin fuer Mensaesser'; verlag := 'Verlag McDagobert, Cheeseburg'; art := Zeitschrift; heftnr := 1 end;
Der Wert der Komponente a r t gibt an, welchen Aufbau der Rest des Verbunds hat. a r t dient als V a r i a n t e n s e l e k t o r . Die Variantenselektor-Komponente ist allen Varianten gemeinsam. Weitere gemeinsame Komponenten (falls es welche gibt) müssen vor dem Varianten Teil definiert werden. Der V a r i a n t t e i l befindet sich also immer a m S c h l u ß eines Verbunds. Die Komponenten-Liste jeder Variante wird durch ein K l a m m e r n p a a r ( ) eingeschlossen. Die Klammern müssen immer vorhanden sein, auch dann, wenn eine Variante gar keine Komponenten enthält.
14.1 Verbundtypen
151
Das Wortsymbol e n d beendet sowohl den durch das Wortsymbol case eingeleiteten Variantteil als auch die gesamte, mit dem Wortsymbol record beginnende Verbunddefinition. Alle K o m p o n e n t e n n a m e n in einem Verbund müssen eindeutig sein, auch wenn sie in unterschiedlichen Varianten stehen. In der Selektorkonstanten-Liste können mehrere Konstanten einer Variante zugeordnet werden. Nicht alle möglichen Werte des Variantenselektors müssen in den Selektorkonstanten-Listen auftauchen. Varianten können ineinander verschachtelt werden. In der KomponentenListe einer Varianten kann also wiederum ein varianter Teil vorhanden sein. Als Variantenselektor sind alle abzählbaren Typen zulässig. Beim Arbeiten mit Varianten dürfen immer nur die Komponenten der sogenannten aktiven Variante angesprochen werden. Die aktive Variante ist die, die durch den aktuellen Wert des Variantenselektors ausgewählt ist. Seien Sie sehr vorsichtig beim Arbeiten mit Varianten, denn in aller Regel merkt das Pascal-System nicht, wenn Sie auch Elemente einer nicht aktiven Variante ansprechen! Alle Komponenten, denen noch nichts zugewiesen wurde, haben Undefinierte Inhalte. Verbünde mit Variantteil bieten dem Programmierer noch weitergehende Möglichkeiten, die aber nur von geübten Programmierern genutzt werden sollten, da sie sehr leicht zu Fehlern führen können. Sie sollen deshalb hier nicht besprochen werden. Vorsicht ist auch geboten bei der Zuweisung von Verbunden mit Variantteil als Ganzes! Kontrollaufgab en K.14.1
Welche Probleme gibt es beim Ausdrucken der zu einer Weinsorte gespeicherten Daten des Weinhändlers Rebstöckl?
K.14.2
Welcher grundsätzliche Unterschied besteht zwischen den Datentypen a r r a y und record?
K.14.3
Welche Operation ist die einzige, die mit ganzen Verbunden erlaubt ist? Was muß man dabei beachten?
K.14.4
Kann in verschiedenen Verbunden der gleiche Komponentenname verwendet werden?
K.14.5
In dieser Lektion wurde erklärt, was genau bei der erweiterten Definition des Verbundtyps Spieler durch kapitaen.geburtsdatum.tag bezeichnet wird. Beschreiben Sie dementsprechend was durch mannschaft[11].geburtsdatum.tag bezeichnet wird.
152
K.14.6
14 Datenstruktuien II
Welchen Datentyp haben die folgenden Variablen? (Vorausgesetzt werden die erweiterte Definition des Verbundtyps s p i e l e r und die Deklarationen von k a p i t a e n und mannschaft.) kapitaen kapitaen.nachname k a p i t a e n . g e b u r t sdatum.monat mannschaft mannschaft[7] mannschaft[7].vorname mannschaft[7].gehurtsdatum mannschaft[7].geburtsdatum.tag
K.14.7
Schreiben Sie ein kurzes Programm, das zwei komplexe Zahlen einliest und ihr Produkt ausgibt. Die Multiplikation von komplexen Zahlen ist folgendermaßen definiert:
(i! + ix2)(yi + u/2) = ((zm - x2y2) + t'(zij/2 + Z2yi)) K.14.8
Gegeben sei eine vith-Anweisung für mehrere, durch Kommas getrennte Verbundvariablen. Ein bestimmter Komponentenname komme in mehreren der genannten Verbünde vor. Welcher Verbund wird angesprochen, wenn dieser Komponentenname in der with-Anweisung vorkommt?
K.14.9
Wo muß bei Verbunden mit Variantteil der Variantteil stehen?
K.14.10
Wozu dient der Variantenselektor? Welche Datentypen sind für Variantenselektoren zugelassen?
14.2 Dynamische Variablen und Zeigertypen
14.2
153
D y n a m i s c h e Variablen und Zeigertypen
Alle Variablen, die wir bisher deklariert haben, waren sogenannte statische Variablen. Wir mußten uns bereits beim Schreiben des Programms überlegen, wieviel Speicherplatz wir benötigen. Probleme bereitete dieses Vorgehen zum Beispiel bei Listen, wenn man noch nicht weiß, wieviele Elemente die Liste enthalten wird. Wenn wir die Liste in einem Feld I var liste: array [1..maxanz] of datensatz; abspeichern, müssen wir einige Nachteile in Kauf nehmen: - Wir müssen uns entscheiden, wieviele Elemente die Liste maximal aufnehmen soll, weil der Wert von maxanz bereits beim Schreiben des Programms festgelegt werden muß. - Wenn wir maxanz zu klein wählen, erreichen wir beim Arbeiten mit dem Programm womöglich zu häufig diese Obergrenze. Wählen wir maxanz zu groß, verschenken wir Speicherplatz, weil stets der Platz für das gesamte Feld belegt wird, auch wenn es nur teilweise gefüllt ist. - Wenn wir an irgendeiner bestimmten Stelle in die Liste ein Element einfügen wollen, müssen wir alle noch folgenden Elemente um eine Position weiterschieben. - Ebenso führt das Löschen eines Elements aus der Liste zu einem „Loch", das nur durch Verschieben aller folgenden Elemente beseitigt werden kann. Diese Nachteile können wir umgehen, wenn wir mit einer aus dynamischen Variablen aufgebauten dynamischen Datenstruktur arbeiten. Der tatsächlich benötigte Speicherplatz wird bei diesem Verfahren erst bei der Entstehung von dynamischen Variablen während des Programmlaufs belegt. Dadurch können Datenstrukturen beliebiger Größe aufgebaut werden. Um mit dynamischen Variablen arbeiten zu können, benötigen wir einen neuen Datentyp, den Zeigertyp. Ein Zeigertyp wird definiert durch das Spezialsymbol ~ (es soll die Spitze eines Pfeiles darstellen) gefolgt von einem Typnamen. Variablen dieses Typs werden als Zeiger oder Zeigervariablen (engl, pointer) bezeichnet. Eine Zeigervariable kann als Werte Bezeichner anderer Objekte (in unserem Fall von dynamischen Variablen) annehmen. Man sagt auch, dieser Zeiger zeigt auf ein Objekt. (Wir betrachten diesen Sachverhalt später noch etwas genauer.) Zunächst ein kleines Beispiel. Eine Zeigervariable, die als Werte Bezeichner von ganzzahligen dynamischen Variablen annehmen kann (die also auf Variablen vom Typ i n t e g e r zeigen kann), könnte z. B. folgendermaßen deklariert werden: var zeigvar : "integer;
154
14 Datenstrukturen II
Diese Deklaration erzeugt eine Zeigervariable, die noch Undefiniert und in Abbildung a) dargestellt ist (das ' ? ' steht für Undefiniert): a)
b) zeigvar
zeigvar
nil
Soll dieser Zeiger keinen Bezeichner als Wert besitzen (also nirgends hinzeigen), aber trotzdem wohldefiniert sein, so erhält er, wie in Abbildung b) zu sehen, den konstanten Wert nil: zeigvar
:= n i l ;
Zum Erzeugen einer dynamischen Variablen, auf die dieser Zeiger zeigen soll, rufen wir die Standardprozedur n e w auf: inew(zeigvar); Dies erzeugt eine dynamische Variable, mit einem Bezeichner, der nach außen nicht bekannt gegeben aber der Variablen z e i g v a r als Wert zugewiesen wird. Zur Veranschaulichung dient die folgende Abbildung:
zeigvar Die Zeigervariable z e i g v a r zeigt auf eine dynamische Variable, die wiederum Werte vom Typ i n t e g e r annehmen kann, aber zu diesem Zeitpunkt noch Undefiniert ist. Wie können wir nun der dynamischen Variablen, auf die der Zeiger z e i g v a r zeigt, etwas zuweisen? z e i g v a r als Zeigervariable ist sozusagen eine Referenz auf eine integer-Variable. Um auf die dynamische integer-Variable zugreifen zu können, müssen wir den Zeiger dereferenzieren. Dies geschieht, indem man dem Namen des Zeigers das Symbol " hinzufügt: zeigvar"
:= 4711;
z e i g v a r " bezeichnet die dynamische Variable vom Typ i n t e g e r , auf die der Zeiger z e i g v a r zeigt. Es ergibt sich nach dieser Zuweisung nun folgende Situation:
zeigvar
4711
Wir können jetzt Zeigervariablen und dynamische Variablen erzeugen und ihnen Werte zuweisen. Wie aber haben wir uns die Erzeugung und Verwaltung dieser Objekte im Speicher vorzustellen?
14.2 Dynamische Variablen und Zeigertypen
155
Der Übersetzer reserviert zunächst nur eine Speicherzelle für die Zeigervariable zeigvar und zwar dort, wo auch andere Variablen abgelegt werden, im sogenannten festen Datenbereich. Wird zur Laufzeit dem Zeiger zeigvar der Wert nil zugewiesen, so wird dies an der reservierten Stelle eingetragen. Wir können uns das etwa folgendermaßen vorstellen:
zeigvar
nil
Wird zur Laufzeit mit new(zeigvar) eine dynamische Variable erzeugt, so müssen zwei Schritte ablaufen: 1. Im sogenannten variablen Datenbereich muß ein freier Speicherplatz für eine dynamische integer-Variable gefunden und reserviert werden. 2. Die Adresse dieser Speicherzelle (in der Abbildung a) 1024) wird nun an der zuvor für den Zeiger zeigvar reservierten Stelle eingetragen. Der Programmierer erfährt von der tatsächlichen Adresse jedoch nichts.
zeigvar
1094
a)
zeigvar
1024
1024
4711
b) 1024
?
Findet nun zur Laufzeit eine Zuweisung der Form zeigvar" := 4711 statt, so wird genau der umgekehrte Weg verfolgt: 1. Es wird in der Speicherzelle von zeigvar nachgeschaut, an welcher Adresse im variablen Datenbereich die integer-Variable zu finden ist. Gefundene Referenz: 1024. 2. Der Speicherplatz an der Adresse 1024 wird mit dem Wert 4711 belegt (Abb. b). Zum Kennenlernen einiger weiterer Möglichkeiten, die uns Zeiger und dynamische Variablen bieten, betrachten wir das folgende etwas größere Beispiel. Gegeben seien die Deklarationen: type artikel = record artbe: string[20]; artnr: integer end; artzeiger = "artikel; var
neuerArtikel, kleinerArtikel, blauerArtikel : artzeiger;
14 Datenstrukturen II
156
Nach Ausführung der Anweisungen new( neuerArtikel ); new( kleinerArtikel ); V
liegt die folgende Situation vor:
neuerArtikel
kleinerArtikel
artbe
?
artnr
?
artbe
?
artnr
?
blauerArtikel Die Zeiger neuerArtikel und kleinerArtikel zeigen nun jeweils auf eine dynamische Verbundvariable vom Typ Artikel. Wie eine dynamische Variable vom Typ integer mit einem Wert belegt wird, haben wir bereits zu Beginn dieses Kapitels gelernt. Nun stellt sich die Frage, wie die soeben erzeugten dynamischen Verbünde mit Werten belegt werden. Dazu müsssen wir zunächst natürlich wieder den Zeiger dereferenzieren (mittels "). Danach muß noch die zu belegende Komponente des Verbundes ausgewählt werden (durch .). Als Programmtext sieht das Ganze dann folgendermaßen aus: neuerArtikel".artbe := 'ottifant' neuerArtikel".artnr := 1111; kleinerArtikel".artbe := 'ente'; kleinerArtikel".artnr := 2222; Die Komponenten der dynamischen Variablen sind nun mit den vorgegebenen Werten belegt: artbe ottifant neuerArtikel
kleinerArtikel
blauerArtikel
artnr
1111
artbe
ente
artnr
2222
14.2 Dynamische Variablen und Zeigertypen
157
Die Zeigervariable b l a u e r A r t i k e l ist bisher immer noch Undefiniert. Da ein ottifant möglicherweise blau ist, wollen wir nun diesem Zeiger die bereits vorhandene dynamische Variable, die den Artikel 'ottifant' beschreibt, zuweisen. Dies geschieht, indem wir folgendes schreiben: ^blauerArtikel
:= n e u e r A r t i k e l ;
J
Durch diese Anweisung wird dem Zeiger b l a u e r A r t i k e l genau dieselbe Adresse zugeordnet, die auch schon dem Zeiger n e u e r A r t i k e l zugeordnet wurde. Beide Zeiger zeigen jetzt auf ein und dieselbe Stelle im dynamischen Datenbereich des Speichers:
neuerArtikel
kleinerArtikel
artbe
ottifant
artnr
1111
artbe
ente
artnr
2222
blauerArtikel Solche Zeigerzuweisungen sind nur unter Zeigern gleichen Typs erlaubt. V e r b o t e n wäre also etwa b l a u e r A r t i k e l := z e i g v a r , da der eine ein Zeiger auf Verbünde vom T y p a r t i k e l ist und der andere auf integer-Variablen zeigen kann. Neben der Zuweisung von Zeigern desselben Typs sind als einzige weitere Operationen der Vergleich auf Gleichheit und Ungleichheit sowie der Test auf n i l erlaubt. if if
blauerArtikel = neuerArtikel n e u e r A r t i k e l n i l then . . .
then :
Sie haben gelernt, wie man mit der Prozedur new für eine neue dynamische Variable Speicherplatz vom System anfordert. Wenn man die dynamische Variable nicht mehr benötigt, kann man diesen Speicherplatz dem System zurückgeben. In Standard-Pascal geschieht dies, indem man die Prozedur dispose aufruft und ihr als Parameter einen Zeiger auf die nicht mehr benötigte Variable mitgibt. Haben wir zum Beispiel mit new(zeigvar) Speicherplatz für die dynamische Variable z e i g v a r " angefordert, so können wir diesen Speicherplatz mit dispose(zeigvar)
158
14 Datenstrukturen II
dem System zurückgeben, wobei der Wert des Zeigers zeigvar anschließend Undefiniert ist. Es ist also ratsam, nach jedem dispose (x) ein x := n i l auszuführen. Bei der Arbeit mit Zeigern und insbesondere beim Zurückgeben von Speicherplatz können leider auch viele unerwünschte Effekte auftreten. Wenn wir im Beispiel von Seite 157 durch Ausführung von dispose(neuerArtikel); n e u e r A r t i k e l := n i l ; Speicherplatz zurückgeben, so ergibt sich folgende Situation:
neuerArtikel
kleinerArtikel
blauerArtikel Die Stelle im dynamischen Speicherbereich, auf die die Zeiger n e u e r A r t i k e l und b l a u e r A r t i k e l zeigen, wird durch das d i s p o s e dem System zurückgegeben. Diese Speicherstelle ist nicht mehr reserviert, hier können also vom System wieder neue Daten eingetragen werden. Der Zeiger n e u e r A r t i k e l wurde ordnungsgemäß auf n i l gesetzt. Der Zeiger b l a u e r A r t i k e l jedoch zeigt weiterhin auf die ehemals reservierte Speicherstelle. Da der Inhalt dieser Stelle nun aber Undefiniert ist, hängt der Zeiger sozusagen in der Luft. Er ist zum sogenannten hängenden Zeiger (engl, dangling pointer) geworden. Ein weiteres Problem, das bei der Arbeit mit dynamischen Strukturen auftreten kann, sind die unerreichbaren Objekte. Man bezeichnet damit dynamische Variablen, auf die keine Zugriffsmöglichkeit mehr besteht. Solche unerreichbaren Objekte können einerseits durch unvorsichtige Zeigerzuweisungen und andererseits durch mehrfaches Erzeugen mit new entstehen. Zur Verdeutlichung noch zwei Beispiele: Wenn im Beispiel von Seite 157 die Zuweisung kleinerArtikel
nil;
ausgeführt wird, so wird der Artikel 'ente' zum unerreichbaren Objekt. Es existiert kein Zeiger mehr, der einen Zugriff auf diese dynamische Variable ermöglicht. Der Speicherplatz wird aber dennoch belegt:
14.2 Dynamische Variablen und Zeigertypen
kleinerArtikel
nil
159
artbe
ente
artnr
2222
Für das Beispiel der mehrfachen Zuweisung wollen wir nun der Zeigervariablen z e i g v a r wieder eine dynamische Variable zuordnen. Wir führen die folgenden Anweisungen aus und erhalten die abgebildete Situation:
v
new(zeigvar); z e i g v a r * := 17;
zeigvar
17
zeigvar 1234
1234.
1Z
Anschließend führen wir die folgenden Anweisungen aus und betrachten die danach entstandene Situation: new(zeigvar); z e i g v a r " := 25; V
1 93S -LZ. 2h.
Die zuerst erzeugte dynamische Variable, der der Wert 17 zugeordnet wurde, ist jetzt zum unerreichbaren Objekt geworden, weil dem Zeiger z e i g v a r eine neue Speicherstelle zugewiesen wurde.
Kontrollaufgaben K.14.11
Welche Werte sind gültige Werte für eine Zeigervariable? Welche Möglichkeiten gibt es, einer Zeigervariablen einen solchen Wert zuzuweisen? Was bewirkt die eine dieser Möglichkeiten neben der Zuweisung noch?
K.14.12
Wie spricht man die dynamische Variable an, auf die der Zeiger j e n e d o r t zeigt?
K.14.13
Welche Operationen sind mit Zeigern zugelassen? Was ist dabei zu beachten? Wie kann man einen Zeiger wohldefiniert auf nichts zeigen lassen?
K.14.14
160
14.3
14 D a t e n s t r u k t u r e n II
Verkettete Listen
Nun wollen wir ein Anwendungsbeispiel betrachten, das zeigt, wie man mit dynamischen Variablen neue Datenstrukturen bilden kann. Der Schlüssel zu neuen Datenstrukturen liegt in der Tatsache, daß dynamische Variablen selbst Zeiger enthalten können, die wiederum auf dynamische Variablen zeigen.
Beispielprogramm lager Wir wollen ein Programm schreiben, das eine Bestandsliste für ein Lager verwaltet. Die Liste soll nach Artikelnummern sortiert sein. Für jeden Artikel soll gespeichert werden, wieviele Einheiten davon noch auf Lager sind. Neben Artikelnummer und Bestand enthält jedes Listenelement einen Zeiger, der auf das nächste Listenelement zeigt:
artnr best. anker
next
Der sogenannte Anker ist eine Zeigervariable, die auf das erste Listenelement zeigt, in unserem Fall also auf den Artikel mit der niedrigsten Artikelnummer. Die eigentlichen Listenelemente sind dynamische Variablen. Hat der Zeiger, der auf das folgende Listenelement zeigt, den Wert n i l , so ist der betrachtete Artikel der letzte in der Liste. Die Artikelnummern werden in diesem Beispiel nicht einfach aufsteigend vergeben, sondern stellen einen Schlüssel dar, der etwas über den Artikel aussagt. Kommt ein neuer Artikel ins Sortiment, muß er also an der richtigen Stelle in die Liste einsortiert werden. Wird ein Artikel aus dem Sortiment gestrichen, muß er aus der Liste entfernt werden. Am häufigsten sind jedoch die normalen Lagerbewegungen, also Veränderungen des Lagerbestands. Hin und wieder wird auch eine komplette Ubersicht über den gesamten Lagerbestand benötigt. Der mit diesem Programm arbeitende Lagerverwalter soll eine Liste der möglichen Bearbeitungsarten, ein sogenanntes Menü, angezeigt bekommen. Aus diesem Menü kann er sich dann die gewünschte Bearbeitungsart auswählen. Die verschiedenen Bearbeitungsarten werden wir im Zuge der Programmverfeinerung als Prozeduren realisieren. Für das Hauptprogramm entwickeln wir zunächst folgendes Struktogramm:
161
14.3 Verkettete Listen
Initialisierung des Ankers mit d e m Wert
nil
M e n ü anzeigen
gewählte B e a r b e i t u n g s a r t einlesen
Falls —
gewählte Bearbeitungsart
1
Lagerbestand aktuali-
bis
2
3
Neuen Artikel aufneh-
Artikel löschen
gewählte Bearbeitungsart =
=
sonst —
4 Bestandsliste ausgeben
Keine Aktion
Fehlermeldung "Ungültige Wahl" ausgeben
0
D a r a u s ergibt sich das folgende H a u p t p r o g r a m m , bei d e m die P r o z e d u r d e k l a r a tionen natürlich noch fehlen: f
\ program
lager;
i Dieses Programm verwaltet eine Bestandsliste eines Lagers. Jedes Listenelement enthaelt eine Artikelnummer, den von diesem Artikel noch vorhandenen Bestand und einen Zeiger auf das Folgeelement in der Liste. Die Liste soll a u f s t e i g e n d n a c h A r t i k e l n u m m e r n sortiert sein. } { ******************** Vereinbarungsteil
********************
}
type artzeiger = ~artikel; artikel = record artnr, bestand: integer; next: artzeiger end; var anker: artzeiger; wähl: integer; {
» » » »
HIER FEHLEN NOCH DIE PROZEDURDEKLARATIONEN
{ ********************** begin anker
:= n i l ;
« « «
gerade betrachtete Artikelnr.
dann Hilfszeiger auf das Element setzen, auf das gerade der Suchzeiger zeigt Suchzeiger um ein Listenelement weitersetzen
Falls
sonst gesuchte Artikelnummer = gerade betrachtete Artikelnummer
- dann Fehlermeldung ausgeben
sonst Zeiger a r t z auf das Listenelement setzen, auf das der Suchzeiger zeigt
Suchzeiger auf n i l setzen damit die Schleife abbricht
166
14 Datenstrukturen II
Die fertig codierte Prozedur a r t i k e l n e u sieht d a n n folgendermaßen aus:
(
-N procedure
artikelneu;
{ M i t d i e s e r P r o z e d u r k a n n e i n n e u e r A r t i k e l in die Liste aufgenommen werden } { ************** Vereinbarungsteil var nummer: integer; a r t z , s u c h z , h i l f s z , neuart: einfuegen: boolean;
"artikelneu" * * * * * * * * * * * * * * }
artzeiger;
-( * * * * * * * * * * * * * * * * A n w e i s u n g s t e i l "artikelneu" * * * * * * * * * * * * * * *
}
begin writeln; w r i t e l n ( ' W e l c h e A r t i k e l n u m m e r soll d e r n e u e A r t i k e l 'bekommen?'); write ( ' A r t i k e l n u m m e r : '); r e a d l n (nummer);
',
{ E i n f u e g e s t e l l e in der L i s t e s u c h e n } artz := n i l ; e i n f u e g e n := t r u e ; { I n i t i a l i s i e r u n g e n Jh i l f s z := nil; s u c h z := anker; w h i l e suchz n i l do if n u m m e r >
suchz*.artnr
then begin h i l f s z := suchz; suchz := suchz".next end else
i Zeiger > { w e i t e r s e t z e n 3-
begin if n u m m e r = s u c h z " . a r t n r then begin writeln; w r i t e l n ( ' E s e x i s t i e r t b e r e i t s ein A r t i k e l ' , ' m i t dieser A r t i k e l n u m m e r ! ! ! ' ) ; e i n f u e g e n : = false end' else a r t z := suchz; suchz end;
:= nil
167
14.3 V e r k e t t e t e L i s t e n
if
einfuegen
then begin new(neuart); n e u a r t " . a r t n r := nummer; n e u a r t * . b e s t a n d := 0; n e u a r t " . n e x t := a r t z ;
V
end
end;
if hilfsz = nil then anker := neuart { Einfuegen am Anfang } e i s e h i l f s z ' . n e x t := n e u a r t ; -( E i n f . i n der Mitte > { oder am Ende } writeln; w r i t e l n ( ' D e r A r t i k e l mit der Artikelnummer '.nummer, ' wurde e i n g e f u e g t . ' ) ; writeln('Anfangsbestand: 0 ' ) J
Eine wichtige Rolle spielt bei dieser Prozedur der Hilfszeiger h i l f s z , der dem Suchzeiger suchz immer um ein Listenelement hinterherhinkt. Er wird beim Einfügen dringend gebraucht, wie die beiden folgenden Bilder zeigen. Machen Sie oft Gebrauch von derartigen Zeichnungen, um sich die Vorgänge beim Umsetzen von Zeigern klarzumachen: neuart".next
:= a r t z
hilfsz
artz
artnr [663] best. anker
next
\ /
artnr [945]
best.
ö
next
m
168
14 Datenstrukturen II
hilfsz".next
:= n e u a r t
hilfsz
a r t n r 279] b e s t . ~25~1 anker
next
czl
/
artnr |663|
artnr [945]
best. [47]
best.
58 |
next
next
|nil|
/
neuaxt
Der Zeiger a r t z (bzw. der Suchzeiger suchz) zeigt auf den Artikel, vor den der neue Artikel in die Liste eingefügt werden soll, während der Hilfszeiger h i l f sz auf den Artikel zeigt, hinter den der neue Artikel eingefügt werden soll. Der Hilfszeiger wird benötigt, damit der Zeiger next des Artikels, der zum Vorgänger des neuen Artikels in der Liste werden soll, auf den neuen Artikel umgesetzt werden kann. Es ist jedoch ein Sonderfall zu beachten: Wenn der Hilfszeiger noch den Wert n i l hat, wird am Anfang der Liste eingefügt. Das neue Element wird dann erstes Element der Liste. In diesem Fall muß nicht der next-Zeiger eines Listenelements, sondern der Anker auf das neue Element umgesetzt werden. Wenn man eine Prozedur zum Einfügen eines Elements in eine verkettete Liste schreibt, muß man darauf achten, daß folgende fünf Fälle korrekt bearbeitet werden: a) b) c) d) e)
Einfügen am Anfang der Liste Einfügen mitten in die Liste Einfügen am Ende der Liste Einfügen in eine leere Liste Kein Einfügen, weil Element schon vorhanden
Beachten Sie hierzu bitte auch die Kontrollaufgabe 14.15! Der nächste Menüpunkt ist das Löschen eines Artikels aus der Liste. Dazu gehört folgender Entwurf:
169
14.3 Verkettete Listen
artikelloesch Artikelnummer des zu löschenden Artikels einlesen Artikel in der Liste suchen F a l l s der Artikel nicht gefunden wurde
- dann — Fehlermeldung ausgeben
sonst Zur Sicherheit nochmal nachfragen, ob der Artikel wirklich gelöscht werden soll F a l l s Antwort positiv
dann
sonst -
F a l l s Löschen am Anfang der Liste
dann Anker auf Nachfolger des zu löschenden Artikels setzen
sonst Zeiger next des Vorgängers auf den Nachfolger des zu löschenden Artikels setzen
Speicherplatz des gelöschten Artikels freigeben Bestätigen, daß der Artikel gelöscht wurde Diese Prozedur kann folgendermaßen codiert werden: procedure artikelloesch; { Mit dieser Prozedur kann ein Artikel aus dem Sortiment entfernt werden } { ************ Vereinbarungsteil "artikelloesch" ************ > var nummer: integer; artz, suchz, hilfsz: artzeiger; bestaetigung: char; { ************** Anweisungsteil "artikelloesch" ************* } begin writeln; writeln('Welcher Artikel soll aus dem Sortiment entfernt 'werden?'); write ('Artikelnummer: '); readln (nummer); { Artikel in der Liste suchen > artz := nil; hilfsz := nil; { Initialisierungen suchz := anker;
}
170
14 Datenstrukturen II
while suchz nil do if nummer = suchz".artnr then begin > artz := suchz; { Artikel gefunden suchz := nil end else begin hilfsz := suchz; { Zeiger weitersetzen > suchz := suchz".next end; if artz = nil then begin writeln; writeln('Es existiert kein Artikel mit dieser ', 'Artikelnummer!') end else begin writeln; write ('Soll der Artikel mit der Artikelnummer artz".artnr, ' wirklich geloescht werden? (j/n) : '); readln(bestaetigung); if bestaetigung = 'j' then begin if hilfsz = nil then anker := artz".next
V
end;
end
end
{ Loesehen } { am Anfang > else hilfsz".next := artz".next; -[ Loeschen } { in der Mitte oder am Ende > dispose(artz); { Speicherplatz freigeben > writeln; writeln('Der Artikel mit der Artikelnummer nummer, ' wurde aus dem Sortiment entfernt!')
)
N u n fehlt uns nur noch die Prozedur z u m Drucken der Bestandsliste. folgendes S t r u k t o g r a m m :
Sie hat
druckeliste Suchzeiger auf erstes Listenelement setzen S o l a n g e Suchzeiger n i l D a t e n des Elements, auf das der Suchzeiger zeigt, ausgeben Suchzeiger u m ein Element weitersetzen
14.3 Verkettete Listen
171
Die Codierung dieser kleinen Prozedur sollte Ihnen nun wirklich nicht mehr schwerfallen. Sie soll daher einer Kontrollaufgabe vorbehalten bleiben. Wir haben in diesem Beispiel eine recht einfache Datenstruktur mit dynamischen Variablen kennengelernt: die e i n f a c h v e r k e t t e t e L i s t e . Mit dynamischen Variablen lassen sich noch viele weitere interessante Datenstrukturen aufbauen, z. B . doppelt verkettete Listen, Bäume usw. Eine genauere Beschreibung dieser Strukturen würde den Rahmen dieser Fibel aber bei weitem sprengen. Deshalb muß hier auf weiterführende Literatur verwiesen werden. Nun noch ein paar abschließende Hinweise zur Arbeit mit Zeigern im allgemeinen: Die Dereferenzierung eines Zeigers kann auch über mehrere Stufen stattfinden: anker*.next ~.next~.artnr bezeichnet die Artikelnummer des dritten Artikels in der Liste. Aber Vorsicht! Wenn nicht mindestens drei Elemente in der Liste vorhanden sind, führt diese Konstruktion zu einem Fehler. Man kann auch dynamische Variablen für Verbünde mit variantem Teil erzeugen. Ruft man die Prozedur new ohne zusätzliche Angaben auf, so wird für die dynamische Variable Speicherplatz bereitgestellt, der für die umfangreichste Variante ausreicht. Es gibt aber auch eine besondere Art des Aufrufs von new, bei der man den Variantenselektor mit angibt. In diesem Fall wird nur soviel Speicherplatz bereitgestellt, wie für die ausgewählte Variante benötigt wird. Die vorliegende Fibel ist für Programmieranfänger gedacht, deshalb soll auf diese Besonderheiten nicht eingegangen werden. Pascal stellt Ihnen für Ihre Programme eine Vielzahl von Datenstrukturen zur Verfügung. Es bleibt dem Programmierer überlassen, diejenige Datenstruktur auszuwählen, die für sein spezielles Problem am besten geeignet ist. Beispielsweise bieten verkettete Listen Vorteile beim Einfügen und Löschen von Elementen. Felder sind dafür weniger geeignet, weil dabei oft große Teile des Feldes verschoben werden müssen. Andererseits kann man auf ein bestimmtes Element eines Feldes jederzeit leicht zugreifen, während man eine verkettete Liste erst vom Anfang bis zur gewünschten Stelle durchlaufen muß. Die Wahl der geeigneten Datenstrukturen hat also, wie Sie sehen, großen Einfluß auf die Effizienz eines Programms.
172
14 Datenstrukturen II
Kontrollaufgaben K.14.15
Spielen Sie für die fünf Fälle, die beim Einfügen in eine verkettete Liste korrekt bearbeitet werden müssen, den Programmablauf der Prozedur a r t i k e l n e u durch. Könnte man die Uberprüfung, ob die neu einzufügende Artikelnummer schon in der Liste vorhanden ist (nummer = suchz" . a r t n r ) , auch aus der Suchschleife herausziehen?
K.14.16
Codieren Sie die Prozedur d r u c k e l i s t e !
K.14.17
Welche Vor- bzw. Nachteile hat eine verkettete Liste gegenüber einem Feld?
173
Lektion 15
Dateibearbeitung In dieser Lektion wollen wir uns mit der Bearbeitung von Dateien beschäftigen. Sie sollen lernen, wie man Programme schreibt, die Daten von Dateien lesen und auf Dateien schreiben können. Sie haben ja bereits in der Lektion 12.2 ein wenig Kontakt mit Textdateien geknüpft. Dabei haben Sie sicherlich auch festgestellt, wie hilfreich es sein kann, wenn man nicht immer wieder alle Daten, die ein Programm verarbeiten soll, von Hand über die Tastatur eingeben muß. Besonders bei der Verarbeitung sehr großer Datenmengen kommt man praktisch ohne Dateien nicht mehr aus. Stellen Sie sich vor, das Programm P I erzeugt als Ausgabe etwa 10.000 reelle Zahlen, die Sie mit dem Programm P 2 weiterverarbeiten wollen. Ohne Dateien müßten Sie alle Zahlen vom Bildschirm abschreiben, um sie für das Programm P2 wieder eintippen zu können. Wenn Sie diese Mühe nicht scheuen, lesen Sie bitte weiter bei Lektion 16. Sie haben sich also entschlossen, hier weiterzulesen. Na schön, dann wollen wir das Thema mal in Angriff nehmen. Dateien werden in Pascal behandelt wie Datentypen. Eine Datei (engl, file) ist ein Datenobjekt, das aus einer Folge von typengleichen Elementen besteht. Diese Untergliederung in einzelne Elemente bedeutet, daß eine Datei ein strukturierter Datentyp ist. Die Definition eines Datei-Datentyps, im folgenden kurz Dateityp genannt, muß folgende Form haben: type = f i l e of ; Den Typbezeichner dürfen Sie sich wie üblich selbst aussuchen. Als Komponententyp darf jeder beliebige Datentyp außer Dateitypen selbst verwendet werden. Typdefinitionen der Art " f i l e of f i l e . . . " sind also nicht möglich. Ein paar Beispiele: type zahlendatei person
personendatei
file of real; record vorname, nachname: alter: 0..99 end; file of person;
string [30] ;
174
15 Dateibearbeitung
Nun haben wir zwar zwei Dateitypen definiert, aber eine Typdefinition allein erzeugt noch kein Datenobjekt. Deshalb sind nach diesen Typdefinitionen noch keine Dateien vorhanden. Diese werden erst durch entsprechende Variablendeklarationen geschaffen: var zdat: zahlendatei; pdat: per s onendat e i ; i d a t : f i l e of i n t e g e r ; V
Wie Sie sehen, kann, wie üblich, die Typdefinition auch implizit bei der Variablendeklaration vorgenommen werden. Im Gegensatz zu Feldern oder Verbunden wird bei Dateien die Anzahl der in ihnen enthaltenen Elemente bei der Typdefinition nicht festgelegt. Diese Anzahl der E l e m e n t e steht zu diesem Zeitpunkt noch nicht fest und kann variieren. Eine neue Datei enthält vorerst überhaupt keine Elemente. Die Elemente von Dateien sollen im folgenden Sätze oder D a t e n s ä t z e genannt werden. Diese Bezeichnung rührt daher, weil es in der Praxis sehr viele Dateien von der Art der Personendatei pdat gibt. Für Elemente solcher Dateien paßt eben die Bezeichnung Datensatz besser als die Bezeichnung Element. Für ganze Dateien sind keinerlei Operationen, z. B. Zuweisungen oder Vergleiche, erlaubt. Eine Anweisung wie ^ neuedatei := a l t e d a t e i
^
wäre also, auch wenn beide Dateien vom gleichen Typ sind, falsch. Grundsätzlich werden zwei Arten der Dateibearbeitung unterschieden: Sequentielle B e a r b e i t u n g und B e a r b e i t u n g mit Direktzugriff. In jedem Fall haben die Datensätze einer Datei eine bestimmte Reihenfolge. Sequentielle B e a r b e i t u n g bedeutet, daß man die Sätze der Datei nur Satz für Satz, einen nach dem anderen und nur von vorn nach hinten bearbeiten kann. Will man zum Beispiel nur den 98. Datensatz bearbeiten, so muß man am Anfang der Datei beginnen und den ersten Datensatz "ansehen". Dann muß man den zweiten Datensatz holen und "ansehen", dann den dritten und so weiter, bis man endlich den interessierenden 98. Datensatz "in den Händen hält". Wenn man dann anschließend den 97. Datensatz bearbeiten will, so hat man arges Pech, denn es geht immer nur vorwärts in der Datei. Man muß also wieder beim ersten Datensatz beginnen... Diese Technik der sequentiellen Bearbeitung von Dateien ist gut geeignet für Dateien, die auf Magnetbändern gespeichert sind. Aus technischen Gründen kann man Magnetbänder nicht ständig hin- und herspulen, sondern muß sie in einer Richtung weiterlaufen lassen. Standard-Pascal läßt ausschließlich sequentielle Dateibearbeitung zu.
175
Für die heute weit verbreiteten Speichermedien Diskette und Festplatte bieten sich für die Dateibearbeitung eher Techniken mit Direktzugriff an, bei denen man jeden Datensatz direkt adressieren kann. Dies ist in Standard-Pascal aber nicht möglich. Bevor wir uns näher mit den verschiedenen Arten der Dateibearbeitung befassen, müssen wir festlegen, über welche Pascal-Version wir reden wollen. In Standard-Pascal wird z. B. nicht festgelegt, wie eine externe Datei mit einem internen Dateinamen zu verknüpfen ist. Außerdem läßt Standard-Pascal, wie bereits erwähnt, nur die sequentielle Dateibearbeitung zu, viele Pascal-Versionen hingegen erlauben auch die durchaus nützliche Bearbeitung mit Direktzugriff. Wir wollen hier beide Arten der Dateibearbeitung besprechen. Abweichungen von Standard-Pascal werden in der Form beschrieben, wie sie in Turbo-Pascal realisiert sind (für andere Pascal-Versionen siehe Anhang D).
Sequentielle Dateibearbeitung Wie Sie es schon von den Textdateien her kennen, kann man Dateien mit der Standardprozedur reset zum Lesen und mit der Standardprozedur rewrite zum Schreiben öffnen. Man kann eine Datei mehrmals zum Lesen, aber nur einmal zum Schreiben öffnen, wenn man die in ihr enthaltenen Daten nicht verlieren will. Da beim Aufruf von r e w r i t e alle vorhandenen Daten gelöscht werden, kann man in einer bestehenden Datei weder Datensätze löschen, noch einfügen, noch verändern. Wenn man also in einer bestehenden Datei Datensätze hinzufügen, löschen oder ändern möchte, so muß man sich behelfen, indem man alle Datensätze in eine andere Datei "umkopiert" und bei diesem Kopiervorgang die notwendigen Änderungen vornimmt. Kopieren bedeutet, daß man die bestehende Datei zum Lesen und eine neue Datei zum Schreiben öffnet. Die nicht zu verändernden Datensätze werden einfach aus der alten Datei gelesen und in die neue übernommen (geschrieben). Während dieses "Umkopierens" können die zu löschenden Datensätze weggeworfen, die neu einzufügenden Datensätze eingefügt und die zu ändernden Datensätze geändert werden. Zum Schluß kann die neue Datei, falls erforderlich, komplett auf die alte Datei zurückkopiert werden. Auch dieses Zurückkopieren muß wieder Satz für Satz vorgenommen werden. Alles in allem ein sehr aufwendiger Vorgang! Beim Arbeiten mit Dateien werden in Standard-Pascal für das Lesen bzw. Schreiben neben den Standardprozeduren read und w r i t e die Standardprozeduren get und put verwendet. Diese Prozeduren gibt es in anderen PascalVersionen häufig nicht. Zunächst wollen wir ein Beispiel für sequentielle Dateibearbeitung betrachten. Unser Programm zur Verwaltung der Lagerliste aus Lektion 14.3 hat in der praktischen Anwendung einen schwerwiegenden Nachteil. Sobald unser Lagerverwalter die Bearbeitung der Liste beendet, geht die komplette Liste mit allen in ihr gespeicherten Daten verloren. Beim erneuten Anstarten des Programms stehen wir wieder vor einer leeren Liste.
176
15 Dateibearbeitung
Es wäre also äußerst sinnvoll, vor Beendigung des Programms die Daten in einer Datei abzuspeichern. Beim erneuten Anstarten des Programms können die Daten dann von dieser Datei wieder eingelesen werden, und die Bearbeitung kann fortgeführt werden. Welchen Komponententyp wählen wir für unsere Lagerdatei? Der Typ a r t i k e l ist nicht geeignet, denn es ist nicht sinnvoll, die Zeiger der verketteten Liste mit auf die Datei zu schreiben: Wie Sie wissen, enthalten Zeiger Speicheradressen von dynamischen Variablen. Beim Wiedereinlesen der Daten ist aber keineswegs gewährleistet, daß die verkettete Liste wieder genau so im Speicher abgelegt wird wie beim vorhergehenden Programmlauf. Deshalb stimmen die Speicheradressen nicht mehr, und die Zeiger zeigen "irgendwo in die Botanik". Wir könnten als Komponententyp den Typ i n t e g e r wählen, aber im Hinblick darauf, daß in der Praxis die Datensätze meist nicht nur aus zwei ganzzahligen Werten bestehen, wollen wir eine andere Lösung wählen. Dazu ändern wird den Vereinbarungsteil des Programms l a g e r folgendermaßen ab: type artzeiger = " a r t i k e l ; satz = record a r t n r , bestand: integer end; artikel = record daten: satz; next: artzeiger end; var anker: a r t z e i g e r ; wähl: i n t e g e r ; l a g e r d a t e i : f i l e of
satz;
Wir haben nun also einen Datentyp s a t z definiert, der zwar alle Daten eines Artikels enthält, nicht aber den Zeiger auf den nächsten Artikel. Nun können wir eine Datei vom Typ f i l e of s a t z deklarieren. Im Programm l a g e r müssen alle Bezeichner der Art . . . " . a r t n r bzw. . . . " . b e s t a n d unserer neuen Datenstruktur angepaßt werden. An diesen Stellen muß es nun . . . " . d a t e n . a r t n r bzw. . . . " . d a t e n . b e s t a n d heißen. Im Anweisungsteil des Programms l a g e r rufen wir nun - hinter der r e p e a t Schleife - vor dem Programmende eine Prozedur auf, die die Daten aus der verketteten Liste in die Lagerdatei abspeichert. In dieser Prozedur wird die Liste einmal von Anfang bis Ende durchlaufen. Dabei werden die Daten in die Datei geschrieben. Die Struktur dieser Prozedur ist ähnlich wie bei der Prozedur druckeliste:
177
abspeichern Lagerdatei zum Schreiben öffnen Suchzeiger auf erstes Listenelement setzen S o l a n g e Suchzeiger n i l Daten des Elements, auf das der Suchzeiger zeigt, auf die Datei schreiben Suchzeiger um ein Element weitersetzen Lagerdatei schließen Zu Beginn der Prozedur muß noch, wie wir es von den Textdateien her kennen, der programmexterne Dateiname dem programminternen Dateinamen zugewiesen werden. Wir wollen den programmexternen Dateinamen "lagerdat" verwenden. Somit ergibt sich in Turbo-Pascal der folgende Code:
(
procedure
\
abspeichern;
{ ************* Vereinbarungsteil var suchz:
"abspeichern"
************* >
artzeiger;
{ *************** Anweisungsteil
"abspeichern" ************** >
begin assign(lagerdatei,'lagerdat'); rewrite(lagerdatei);
{ D a t e i zum S c h r e i b e n o e f f n e n >
suchz := a n k e r ; w h i l e suchz n i l do { L i s t e durchlaufen > begin write(lagerdatei,suchz".daten); suchz := s u c h z " . n e x t end; close(lagerdatei) end; V
{ Datei schliessen > )
Beim erneuten Anstarten des Programms l a g e r müssen die abgespeicherten Daten von der Datei eingelesen werden. Dazu muß die Lagerdatei durch Aufruf von r e s e t zum Lesen geöffnet werden. Beim Einlesen der Daten muß dann eine neue verkettete Liste aufgebaut werden.
15 Dateibearbeitung
178
Doch Vorsicht: Falls das Programm l a g e r zum allerersten Mal aufgerufen wird, existiert unter Umständen noch gar keine Lagerdatei. Wenn es noch keine Datei mit dem Namen "lagerdat" gibt, führt der Aufruf von r e s e t zu einem Fehler! Sie werden in Kürze eine Methode kennenlernen, um diesen Fehler abzufangen. Vorerst wollen wir aber dem Lagerverwalter die Entscheidung überlassen. Daher deklarieren wir eine char-Variable entscheid und fügen in den Anweisungsteil des Programms l a g e r unmittelbar vor die repeat-Schleife folgendes Programmstück ein: w r i t e C D a t e n von der Lagerdatei e i n l e s e n ? read ( e n t s c h e i d ) ; i f e n t s c h e i d = ' j ' then e i n l e s e n ;
(j/n):
');
Die Prozedur e i n l e s e n ist etwas aufwendiger als die Prozedur abspeichern: einlesen Lagerdatei zum Lesen öffnen
Solange
Dateiende der Lagerdatei noch nicht erreicht
Neue dynamische Variable erzeugen
Daten von der Lagerdatei lesen und in die neu geschaffene dynamische Variable eintragen
Zeiger next der dyn. Variable mit n i l initialisieren
Falls
die Liste noch leer ist
Anker auf das neue Element setzen
Zeiger next des bisher letzten Listenelements (auf das der Zeiger a r t z zeigt) auf das neue Element setzen
Zeiger a r t z auf das neue, nun letzte Listenelement setzen Lagerdatei schließen Auf der folgenden Seite finden Sie den zugehörigen Pascal-Code:
179
procedure einlesen; { *************** Vereinbarungsteil "einlesen" *************** } var artz, neuart: artzeiger; { ***************** Anweisungsteil "einlesen" **************** } begin assign(lagerdatei,'lager.dat'); reset(lagerdatei); { Lagerdatei zum Lesen oeffnen )while not eof(lagerdatei) do begin new(neuart); { Neue dyn. Variable erzeugen
}
read(lagerdatei, neuart".daten); neuart".next := nil; if anker = nil { Liste noch leer? > then anker := neuart else artz".next := neuart; artz := neuart end; close(lagerdatei) end;
{ Lagerdatei schliessen >
Die Abläufe bei der Dateibearbeitung lassen sich leichter verstehen, wenn wir uns einen Dateizeiger vorstellen, der anzeigt, an welcher Stelle in der Datei wir uns gerade befinden. Wird eine Datei mit reset zum Lesen geöffnet, so wird dieser Dateizeiger auf den Anfang des ersten Datensatzes der Datei gesetzt. Nachdem der erste Datensatz gelesen wurde, zeigt der Dateizeiger auf den Anfang des zweiten Datensatzes. So wird der Dateizeiger immer weiter durch die Datei bewegt, bis schließlich der letzte Datensatz der Datei gelesen wurde. Danach steht der Dateizeiger am Ende der Datei, also auf der Position hinter dem letzten Datensatz der Datei. Wenn der Dateizeiger an dieser Position steht, liefert die Standardfunktion eof den Wert true. Wird eine Datei mit rewrite zum Schreiben geöffnet, so wird der Dateizeiger in Standard-Pascal an den Anfang der leeren (gelöschten) Datei gesetzt. Wenn ein Datensatz in die Datei geschrieben wird, wird der Dateizeiger entsprechend weitergesetzt, so daß er immer auf die erste freie Position in der Datei zeigt. Beim Schreiben steht der Dateizeiger also immer am Dateiende. (Achtung! Bei Textdateien wird dieses Prinzip auch realisiert. Bei anderen Dateien, die z. B. mit Turbo-Pascal auch im Direktzugriff bearbeitet werden können sieht das anders aus. Mehr dazu im folgenden Abschnitt.) Bei sequentieller Dateibearbeitung kann der Dateizeiger in der Datei immer nur vorwärts bewegt werden.
180
15 Dateibearbeitung
Dateibearbeitung mit Direktzugriff Viele Pascal-Versionen bieten neben der sequentiellen Dateibearbeitung auch die Dateibearbeitung mit Direktzugriff. Standard-Pascal hingegen nicht! Wir wollen hier die Realisierung in Turbo-Pascal beschreiben: Der Dateizeiger kann bei dieser Art der Bearbeitung in einer Datei nicht nur Satz für Satz vorwärtsbewegt werden, sondern auch direkt auf einen bestimmten Datensatz gesetzt werden. Dazu dient die Prozedur ^ seek(datei,n)
J
Durch diesen Aufruf wird der Dateizeiger auf den Datensatz mit der Nummer n gesetzt, wobei der erste Datensatz die Nummer 0 hat. n muß ein Ausdruck sein, der einen integer-Wert oder einen real-Wert liefert. Bei einem real-Wert ist nur der ganzzahlige Anteil signifikant. Damit man die Möglichkeiten, die durch diesen Direktzugriff geschaffen werden, auch richtig ausnutzen kann, wird in Turbo-Pascal eine Datei durch reset nicht nur zum Lesen, sondern zum Lesen und Schreiben geöffnet. Die Datei muß bereits existieren, und ihr Inhalt bleibt natürlich erhalten. Der Dateizeiger wird an den Anfang der Datei gesetzt. Wir wollen kurz zwei Beispielprogramme betrachten, bei denen uns zwei weitere Funktionen sehr hilfreich sind, die in vielen Pascal-Versionen (eventuell unter einem anderen Namen) zur Verfügung stehen: filepos(datei) und ^filesize(datei)
J
Die Funktion f ilepos liefert uns die Nummer des Datensatzes, auf den gerade der Dateizeiger zeigt. Die Funktion f i l e s i z e liefert uns die Länge der Datei, d. h. die Anzahl der in ihr enthaltenen Datensätze. Da der erste Datensatz in einer Datei die Nummer 0 hat, hat der letzte Datensatz die Nummer f i l e s i z e ( d a t e i ) - 1 . Das erste Anwendungsbeispiel ist ein Programm, das Zahlen, die in aufsteigend sortierter Reihenfolge in einer Datei stehen, in umgekehrter Reihenfolge auf eine andere Datei schreibt. Dazu wird in der zu lesenden Datei der Dateizeiger erst auf den letzten, dann auf den vorletzten Datensatz gesetzt usw.
181
Das Programm in Kurzform: program umdrehen; { Dieses Programm liest Zahlen von einer Datei, auf der diese in aufsteigend sortierter Reihenfolge stehen, und schreibt sie in absteigend sortierter Reihenfolge auf eine andere Datei } var datauf, datab: file of integer; zahl, pos: integer; begin reset(datauf); rewrite(datab); for pos := filesize(datauf)-l downto 0 do begin seek(datauf, pos); read(datauf, zahl); write(datab, zahl) end; V
end.
Im zweiten Anwendungsbeispiel wird in einer Personendatei nach einer bestimmten Person gesucht. Wenn sie gefunden wurde, wird der entsprechende Datensatz modifiziert. Hierbei ist wichtig, daß in Turbo-Pascal in einer mit reset geöffneten Datei auch geschrieben werden darf. Auch dieses Programm wird nur in Kurzform angegeben: f
N
program modifiziere; { Dieses Programm sucht einen bestimmten Datensatz und nimmt an ihm Aenderungen vor. } type perstyp = record vorname, nachname: alter: 0..99 end; var persdat: file of perstyp; person: perstyp; vnam, nnam: string[20]; gefunden: boolean;
string[20];
182
15 Dateibearbeitung
begin reset(persdat); writeln('Wessen Alter soll geaendert werden?'); write('Vorname: '); readln(vnam); write('Nachname: '); readln(nnam); gefunden := false; while not eof(persdat) and not gefunden do begin { Datensatz lesen } read(persdat, person); { Namen vergleichen } if (person.vorname = vnam) and (person.nachname = nnam) then begin { Bisheriges Alter ausgeben: } writeln('Bisheriges Alter: '.person.alter); { Neues Alter einlesen } write('Neues Alter: '); readln(person.alter); { Nach dem Lesen des Datensatzes wurde der Dateizeiger bereits auf den naechsten Datensatz weitergesetzt. Deshalb muss vor der Aenderung der Dateizeiger mit der seek-Prozedur um einen Datensatz zurueckgesetzt werden. } seek(persdat, filepos(persdat)-l); write(persdat, person);
end;
gefunden := true end
close(persdat) end. V
Sie sehen also, daß man in Turbo-Pascal Dateien ä n d e r n kann, ohne die Datei neu zu schreiben. Sätze einer bestehenden Datei hinzufügen können Sie nur am Ende der Datei. Damit die in ihr enthaltenen Daten nicht gelöscht werden, müssen Sie die Datei mit reset öffnen, und das Fenster ans Ende der Datei setzen, z. B. mit rseek(datei, filesize(datei))
J
Wenn Sie Sätze in einer Datei löschen wollen, müssen Sie die Datei nach wie vor neu schreiben.
183
Die wichtigsten Prozeduren und Funktionen zur Dateibearbeitung rewrite(datei)
r e w r i t e öffnet eine Datei zum Schreiben und setzt den Dateizeiger an den Anfang der (leeren) Datei. Existiert die Datei noch nicht, so wird sie erzeugt. Existiert sie bereits, so werden alle in ihr enthaltenen Daten gelöscht.
reset(datei)
r e s e t öffnet eine Datei zum Lesen und Schreiben und setzt den Dateizeiger an den Anfang der Datei. Die Datei muß bereits existieren, sonst gibt es einen Fehler.
read(datei,...)
read liest einen oder mehrere Datensätze von der Datei ein. Der Typ der Variablen in der Eingabeliste muß der Komponententyp der Datei sein. (Für den Dateityp t e x t gelten andere Regeln, auf die wir noch zu sprechen kommen.) Der Dateizeiger wird um die Anzahl der gelesenen Datensätze weitergesetzt.
write(datei,...)
write schreibt einen oder mehrere Datensätze auf die Datei. Der Typ der Variablen in der Ausgabeliste muß der Komponententyp der Datei sein. (Für den Dateityp t e x t gelten andere Regeln, auf die wir noch zu sprechen kommen.) Der Dateizeiger wird um die Anzahl der geschriebenen Datensätze weitergesetzt.
seek(datei,n)
seek setzt den Dateizeiger auf den Datensatz mit der Nummer n. n muß ein Ausdruck sein, der einen i n t e g e r - oder einen real-Wert liefert. Bei real-Werten gilt nur der ganzzahlige Anteil. Die Nummer des ersten Datensatzes in einer Datei ist 0.
close(datei)
close schließt die Datei und sichert den neuen Status der Datei, damit keine Daten verlorengehen.
eof(datei)
eof liefert den Wert t r u e , wenn der Dateizeiger am Ende der Datei, also hinter dem letzten Datensatz in der Datei steht. Andernfalls liefert eof den Wert f a l s e .
184
15 Dateibearbeitung
Desweiteren benötigen wir eine Zuordnung der externen Datei zu unserem internen Dateinamen. In Turbo-Pascal ist dafür die Prozedur a s s i g n zuständig. In anderen Pascal-Versionen werden häufig völlig andere Zuordnungsmechanismen verwendet. (Siehe auch Anhang D) assign(datei,datnam)
datnam muß ein Ausdruck sein, dessen Wert vom Typ s t r i n g ist. datnam spezifiziert einen Dateinamen auf Betriebssystemebene, a s s i g n weist dem programminternen Dateinamen d a t e i den externen Dateinamen datnam zu. a s s i g n darf nicht mit einer Datei aufgerufen werden, die bereits geöffnet ist.
Zwei weitere nützliche Funktionen, die Turbo-Pascal zur Verfügung stellt, haben wir bereits kennengelernt: filepos (datei)
f i l e p o s liefert die aktuelle Position des Dateizeigers als integer-Wert, also die Nummer des Datensatzes, auf den gerade der Dateizeiger zeigt.
filesize(datei)
f i l e s i z e liefert die Länge der Datei als i n t e ger-Wert, also die Anzahl von Datensätzen, die in der Datei enthalten sind. Wenn f i l e s i z e den Wert 0 liefert, ist die Datei leer.
In Turbo-Pascal gibt es noch weitere Prozeduren und Funktionen zur Dateibearbeitung. Wir wollen diese aber im Rahmen dieses Einführungskurses nicht behandeln. Wer gern mehr wissen möchte, möge sich anhand seines UbersetzerHandbuchs informieren.
Textdateien Eine besondere Rolle spielen die T e x t d a t e i e n , die Sie bereits in Lektion 12.2 kennengelernt haben. Der Datentyp t e x t ist ein vordefinierter Dateityp, der, wie der Dateityp f i l e of char, den Datentyp char zum Komponententyp hat. Dennoch unterscheiden sich die beiden Dateitypen in ihren Eigenschaften recht deutlich: Beim Arbeiten mit Dateien vom Typ f i l e of char gilt die Regel, daß die Variablen, die in den Ein- bzw. Ausgabelisten von read bzw. w r i t e stehen, den Datentyp char haben müssen. Beim Arbeiten mit Dateien vom Typ t e x t hingegen können mit read und w r i t e auch Variablen anderer Datentypen gelesen bzw. geschrieben werden. Es findet automatisch eine U m w a n d l u n g zwischen der internen Darstellung der Werte eines Datentyps und der zeichenorientierten Darstellung einer Textdatei statt.
185
Die Standardeingabedatei input und die Standardausgabedatei Output sind ebenfalls Textdateien. input ist standardmäßig zum Lesen, Output standardmäßig zum Schreiben geöffnet, reset bzw. r e w r i t e dürfen auf input und output nicht angewendet werden. Im Gegensatz zu Dateien anderen Typs können Textdateien auch in Turbo-Pascal nicht gleichzeitig lesend und schreibend bearbeitet werden. Die Prozedur reset öffnet eine Textdatei nur zum Lesen! Die Prozedur rewrite öffnet wie immer eine Textdatei zum Schreiben. Textdateien können auch in Turbo-Pascal nur sequentiell bearbeitet werden. Die Prozedur seek und die Funktionen f i l e p o s und f i l e s i z e stehen für Textdateien nicht zur Verfügung! Textdateien verfügen über eine Zeilenstruktur. Wir haben bereits in Lektion 12.2 über Zeilenenden in Textdateien gesprochen. Wegen der Zeilenstruktur gibt es für Textdateien eine boolesche Standardfunktion, mit deren Hilfe man ein Zeilenende erkennen kann, die Funktion eoln (end of line). Sie wird, wie eof, entweder mit dem Dateinamen als Parameter, z. B. e o l n ( d a t e i ) , oder ohne Parameter, was e o l n ( i n p u t ) entspricht, aufgerufen, eoln liefert den Wert t r u e , wenn der Dateizeiger auf einem Zeilenende steht, sonst den Wert f a l s e . Zum Erkennen des Dateiendes kann auch bei Textdateien die boolesche Standardfunktion eof verwendet werden. Wird eof ohne zusätzlichen Parameter aufgerufen, so wird eof (input) ausgeführt. Für Textdateien gibt es aber folgende zusätzliche Prozeduren und Funktionen, die auf Dateien anderen Typs nicht anwendbar sind: readln(textdatei,...)
r e a d l n verhält sich genauso wie read, setzt aber anschließend den Dateizeiger an den Anfang der nächsten Zeile.
writeln(textdatei,...)
w r i t e l n verhält sich wie write, schreibt aber anschließend eine Zeilenende-Markierung auf die Datei.
append(textdatei)
append öffnet eine Textdatei zum Schreiben und setzt den Dateizeiger ans Ende der Datei. Die in der Datei enthaltenen Daten bleiben erhalten, und am Ende können weitere Daten angefügt werden.
eoln(textdatei)
Die boolesche Funktion eoln liefert den Wert t r u e , wenn der Dateizeiger auf einem Zeilenende steht. Andernfalls liefert eoln den Wert f a l s e . Wenn eof ( t e x t d a t e i ) den Wert t r u e liefert, liefert e o l n ( t e x t d a t e i ) ebenfalls den Wert t r u e .
186
15 Dateibearbeitung
Abfangen von Ein-/Ausgabefehlern Wir wollen nun zum Abschluß der Lektion über Dateibearbeitung noch einmal auf das Problem zurückkommen, auf das wir beim Einlesen der Lagerliste von der Lagerdatei gestoßen sind: Falls noch keine Lagerdatei existiert, kann beim Aufruf von r e s e t ein Fehler auftreten. Die Möglichkeiten, solche Fehler zu vermeiden, sind stark vom verwendeten Pascal-Übersetzer abhängig. Häufig ist es möglich, die vom Pascal-System vorgenommene Fehlerbehandlung vorübergehend abzuschalten und anschließend selbst zu überprüfen, ob ein Fehler aufgetreten ist. In Turbo-Pascal macht man das folgendermaßen: Durch den sogenannten "Compiler-Schalter" {$I-> wird die interne Fehlerbehandlung abgeschaltet, durch {$I+> wird sie wieder eingeschaltet. Es handelt sich hier nicht um normale Kommentare, die vom Ubersetzer ignoriert werden, sondern in diesem speziellen Fall wird der Ubersetzer durch diese Direktiven veranlaßt, die Fehlerbehandlung ein- bzw. auszuschalten. Die Prozedur r e s e t hinterläßt in der Variablen ioresult einen Wert, der darüber Auskunft gibt, ob r e s e t erfolgreich oder fehlerhaft war. i o r e s u l t ist eine System-Variable und braucht deshalb nicht deklariert zu werden. Ist der Wert von i o r e s u l t null, so war r e s e t erfolgreich, ist er hingegen ungleich null, liegt ein Fehler vor. Wir können also schreiben: {$I-> r e s e t ( l a g e r d a t e i ) if i o r e s u l t = 0 then . . .
{$I+>;
{ Falls reset erfolgreich > { dann . . . }
Um das Programm lager für die Bearbeitung verschiedener Lagerlisten verwenden zu können, wollen wir, wie Sie es bereits aus der Lektion 12 kennen, den wirklichen Dateinamen der Lagerdatei vom Lagerverwalter über die Tastatur eingeben lassen, bevor wir ihn mit der assign-Prozedur dem internen Dateinamen zuweisen. Etwa, indem wir den Dateinamen in die string-Variable d a t nam einlesen: w r i t e C Z u bearbeitende Lagerdatei: readln(datnam); a s s i g n ( l a g e r d a t e i , datnam);
');
Nehmen wir nun einmal an, der Benutzer vertippt sich bei der Eingabe des Dateinamens und gibt den Namen einer Datei ein, die es gar nicht gibt. Dann würde das Programm beim nachfolgenden Aufruf von r e s e t durch einen Fehler abgebrochen.
187
Besser, wenngleich auch etwas aufwendiger, wäre da natürlich die folgende Version (unter Verwendung der booleschen Variablen ok): repeat write('zu bearbeitende Lagerdatei: '); readln(datnam); assign(lagerdatei.datnam); ok := true; -C$1-} r e s e t ( l a g e r d a t e i ) { $ 1 + } ; i f i o r e s u l t 0 then begin writeln('Eine Datei mit dem Namen datnam, ' gibt es nicht! ') ; writelnC'Ueberlegen Sie noch einmal genau:', ' Wie heisst die '); ok := false end until ok; Noch ein weiteres Anwendungsbeispiel: Wie Sie wissen, wird beim Aufruf von r e w r i t e die Datei überschrieben. Sie möchten nun vorher überprüfen, ob die Datei bereits existiert. Falls ja, möchten Sie den Benutzer explizit fragen, ob die Datei überschrieben werden soll oder nicht. Sie können dies durch einen Kunstgriff erreichen. Zunächst versuchen Sie, die Datei mit r e s e t zu öffnen. Sie überprüfen den Wert, den r e s e t in der Variablen i o r e s u l t hinterlassen hat. Ist der Wert ungleich null, hat das Offnen nicht geklappt, die Datei existiert also noch nicht. In diesem Fall können Sie also einfach r e w r i t e aufrufen, um eine neue Datei zu erzeugen. Andernfalls, wenn der Wert von i o r e s u l t null ist, gibt es die Datei bereits, das Offnen der Datei hat geklappt. Sie schließen die Datei wieder und fragen den Benutzer, ob er die Datei überschreiben möchte. Falls ja, rufen Sie r e w r i t e auf. Übrigens sollten Sie immer, wenn Sie den Benutzer etwas fragen, seine Antwort so überprüfen, daß die Reaktion auf die Antwort im Zweifel immer die sicherere ist. D a s heißt, wenn Sie fragen S o l l d i e D a t e i u e b e r s c h r i e b e n werden?
(j/n)
dann darf die Datei nur überschrieben werden, wenn die Antwort gleich ' j ' ist, nicht aber, wenn sie ungleich ' n ' ist! Wenn Sie hingegen fragen S o l l e n d i e Daten i n e i n e r D a t e i g e s i c h e r t werden?
(j/n)
dann sollten Sie die Daten immer sichern, wenn die Antwort ungleich ' n ' ist, nicht nur, wenn die Antwort genau gleich ' j ' ist.
188
15 Dateibearbeitung
Kontrollaufgaben K.15.1
Welche Datentypen sind als Komponenten von Dateien erlaubt?
K.15.2
Welche Operationen sind für ganze Dateien erlaubt?
K.15.3
Welche zwei verschiedenen Arten der Dateibearbeitung gibt es? Welche Art der Dateibearbeitung gibt es in Standard-Pascal?
K.15.4
Warum ist es nicht sinnvoll, Zeiger in einer Datei zu speichern?
K.15.5
Wie kann man die Daten eines Verbunds auf eine Datei schreiben? Wie kann man sie auf eine Textdatei schreiben?
K.15.6
Wie kann man in Turbo-Pascal den Dateizeiger auf einen bestimmten Datensatz positionieren?
K.15.7
Welche Bearbeitungsarten (lesen/schreiben) sind in Standard-Pascal bei einer mit r e s e t geöffneten Datei möglich? Welche Möglichkeiten bietet Turbo-Pascal? Ist dies auch bei Textdateien so?
K.15.8
Können Textdateien im Direktzugriif bearbeitet werden?
K.15.9
Wie kann man in Textdateien ein Zeilenende erkennen?
Übungsaufgabe:
Bitte bearbeiten Sie jetzt die Übungsaufgabe 6!
189
Lektion 16
Rekursion In dieser Lektion werden Sie keine neuen Pascal-Sprachelemente mehr kennenlernen. (Die kennen Sie ja sowieso schon fast alle!) Vielmehr werden wir uns mit einer besonderen Art von Algorithmen, sogenannten r e k u r s i v e n A l g o r i t h m e n , beschäftigen. Zur Einführung wollen wir uns die Aufgabe stellen, den größten gemeinsamen Teiler von zwei nicht-negativen ganzen Zahlen zu bestimmen. Für dieses Problem hat Euklid bereits 300 Jahre vor Christi Geburt einen geeigneten Algorithmus entwickelt. Euklid hatte herausgefunden, daß der größte gemeinsame Teiler von zwei nicht-negativen ganzen Zahlen x und y gleich dem größten gemeinsamen Teiler von y und dem Rest der ganzzahligen Division x / y ist, wenn y größer als null ist:
und
ggT( x , y ) = ggT( y , Rest von x / y )
falls y > 0
ggT( x , y ) = x
falls y = 0
Beispiel: ggT(51,18) = ggT(18,15) = ggT(15,3) = ggT(3,0) = 3 Der ggT läßt sich also berechnen, indem man den Rest von x / y berechnet und anschließend x durch y und y durch den Rest ersetzt. Dieses Verfahren muß wiederholt werden, solange y < > 0 ist: Euklidischer Algorithmus
Solange y o Berechne den Rest von x / y Ersetze x durch y Ersetze y durch den oben berechneten Rest Ubergib x als Ergebnis
16 Rekuision
190
Hätte Euklid einen Arbeitsplatzrechner mit Pascal-Ubersetzer gehabt, so h ä t t e er vielleicht etwa folgende Pascal-Funktion geschrieben: f u n c t i o n g g t ( x , y : integer): var rest:
integer;
integer;
begin w h i l e y 0 do begin r e s t := x m o d y; x := y; y := r e s t end; 1
gg* ==
V
end;
Diese Funktion verwendet einen i t e r a t i v e n A l g o r i t h m u s zur Lösung des Problems. Mail kann aber auch eine Funktion schreiben, die sich enger an die von Euklid entdeckte Formel zur Berechnung des ggT hält:
r
V
f u n c t i o n g g t ( x , y : integer):
integer;
begin if y = 0 t h e n ggt := x else ggt := g g t ( y , x m o d y) end;
Diese Funktion verwendet einen r e k u r s i v e n A l g o r i t h m u s zur Lösung des Problems. Ein rekursiver Algorithmus ist ein Algorithmus, der sich selbst a u f r u f t . Dabei muß der rekursive Aufruf von Mal zu Mal "einfacher" werden, bis schließlich ein Grenzfall erreicht wird, der so "einfach" ist, daß er ohne weiteren rekursiven Aufruf bearbeitet werden kann. Dadurch wird sichergestellt, daß die Ausführung schließlich einmal endet. Man kann zeigen, daß es für jeden rekursiven Algorithmus einen gleichwertigen iterativen Algorithmus gibt. Häufig ist die rekursive Lösung eleganter, aber meist auch zeitaufwendiger.
191
Beispielfunktion binom A n h a n d des f o l g e n d e n Beispiels werden wir das Prinzip der Rekursion g e n a u e r b e t r a c h t e n . Erinnern Sie sich a n die Binomialkoeffizienten u n d d a s P a s c a l s c h e Dreieck a u s Lektion 13.2. Jede Zahl i m P a s c a l s c h e n Dreieck ist die S u m m e der b e i d e n schräg ü b e r ihr s t e h e n d e n Zahlen. Die B e r e c h n u n g eines B i n o m i a l k o effizienten kann also auf die B e r e c h n u n g zweier anderer B i n o m i a l k o e f f i z i e n t e n zurückgeführt werden: Für 0 < Jfc < n gilt: (£) = 1
falls k = 0 o d e r k = n
C) = (2=i) + (V) M a n kann also leicht f o l g e n d e rekursive F u n k t i o n zur B e r e c h n u n g v o n B i n o m i a l koeffizienten a n g e b e n : function binom(n,k: integer):
integer;
begin if (0
204
16 Rekursion
f u n c t i o n zug( nr:
integer; xalt, yalt:
index):
boolean;
{ Rekursive Funktion, die alle v o n einem Feld aus moeglichen Springerzuege ausprobiert. n r g i b t d i e N u m m e r d e s Z u g e s a n , d i e in d e r Spielbrett-Matrix eingetragen w e r d e n soll. x a l t u n d y a l t s i n d d i e K o o r d i n a t e n d e s F e l d e s , auf d e m d e r Springer bei Aufruf der F u n k t i o n steht. Die F u n k t i o n liefert einen b o o l e s c h e n Wert: true, venn eine korrekte L o e s u n g gefunden wurde u n d f a l s e , w e n n e i n R u e c k z u g e r f o r d e r l i c h ist } ***** Deklaration lokaler Variablen der Funktion zug ***** var k:
}
{ Gibt den naechsten auszuprobierenden Springerzug an } xneu, yneu: integer; { Koordinaten des Zielfeldes nach Ausfuehrung des Zuges } erfolg: boolean; { erhaelt den Wert true, wenn eine korrekte Loesung gefunden wurde }
{
integer;
************* Anweisungsteil der Funktion zug ************
begin k := 0; e r f o l g := f a l s e ;
{ Initialisierungen
repeat k := k + 1 ; xneu yneu
:= x a l t + v e r t [ k ] ; := y a l t + h o r i z [ k ] ;
{ naechsten Zug fuer die U e b e r p r u e f u n g a u s w a e h l e n 3" { Koordinaten des zugehoe- } { rigen Zielfelds berechnen }
{ TJeberpruef e n , o b der Z u g " a n n e h m b a r " ist i f (1 = 0 then if n = 0 then fakrek else fakrek
integer;
:= 1 := n * f a k r e k ( n - 1 )
eise f a k r e k := 0 { villkuerliche Festlegung fuer ungueltige Eingabewerte end;
}
Für die Berechnung der Fakultät sollte besser der iterative Algorithmus gewählt werden. Er ist genauso einfach wie die rekursive Lösung und benötigt nicht so viel Verwaltungsaufwand wie der Ablauf der Rekursion. K.16.2
Aus Platzgründen wird hier eine etwas andere Darstellung gewählt. In der folgenden Tabelle entspricht die linke Spalte dem untersten Zettel in unserem Zettelstapel. Nach rechts wächst die Höhe des Zettelstapels, und entsprechend wächst die sogenannte R e k u r s i o n s t i e f e : hanoi(3,l,2,3) Neuer Zettel: hanoi(2,l,3,2) Neuer Zettel: hanoi(l,l,2,3) Neuer Zettel: hanoi(0,l,3,2) Zettel entfernen Bewege Scheibe von 1 nach 2 Neuer Zettel: hanoi(0,3,2,l) Zettel entfernen Zettel entfernen Bewege Scheibe von 1 nach 3 Neuer Zettel: hanoi(l,2,3,l) Neuer Zettel: hanoi(0,2,l,3) Zettel entfernen Bewege Scheibe von 2 nach 3
230
Anhang Q
Neuer Zettel: hanoi(0,l,3,2) Zettel entfernen Zettel entfernen Zettel entfernen Bewege Scheibe von 1 nach 2 Neuer Zettel: hanoi(2,3,2,l) Neuer Zettel: hanoi(l,3,l,2) Neuer Zettel: hanoi(0,3,2,l) Zettel entfernen Bewege Scheibe von 3 nach 1 Neuer Zettel: hanoi(0,2,l,3) Zettel entfernen Zettel entfernen Bewege Scheibe von 3 nach 2 Neuer Zettel: hanoi(l,l,2,3) Neuer Zettel: hanoi(0,l,3,2) Zettel entfernen Bewege Scheibe von 1 nach 2 Neuer Zettel: hanoi(0,3,2,l) Zettel entfernen Zettel entfernen Zettel entfernen Zettel entfernen K.16.3
Solange in der eingegebenen Zeichenkette noch ungelesene Zeichen vorhanden sind, werden diese durch rekursive Aufrufe der Prozedur sp eingelesen. Bei jedem Aufruf wird sozusagen ein Zeichen gespeichert. Erst wenn die eingegebene Zeichenkette vollständig eingelesen wurde, wenn also das Zeilenende erreicht ist und die Funktion eoln den Wert t r u e liefert, wird die Rekursion abgebrochen. Nun wird zuerst das Zeichen ausgegeben, das beim letzten Aufruf von sp gelesen wurde, also das letzte Zeichen der Zeichenkette. Dann wird dieser Aufruf von sp beendet, seine Daten werden vom Stapel entfernt, und der vorausgehende Aufruf wird wieder aktiv.
Lösungen zu den Kontrollaufgaben
231
Jetzt wird der Buchstabe ausgegeben, der bei diesem Aufruf eingelesen wurde, also der vorletzte Buchstabe der Zeichenkette. So geht es immer weiter rückwärts, der Stapel wird immer weiter abgebaut, bis der allererste Aufruf von sp wieder erreicht ist und alle Zeichen der Zeichenkette in umgekehrter Reihenfolge ausgegeben wurden. K.16.4
(199)
K.16.5
Hoher Verwaltungsaufwand, häufige Mehrfachberechnungen. (196 f)
K.16.6
Der Name eines Unterprogramms darf erst dann im Programmtext auftauchen, wenn er zuvor deklariert wurde. Bei sich gegenseitig aufrufenden Unterprogrammen kann diese Regel nicht eingehalten werden. Abhilfe schafft die f orward-Deklaration. ( 206 )
232
Anhang C
Übungsaufgaben Übungsaufgabe 1 A.
Erstellen Sie mit dem Editor auf dem Bildschirm eine Zeichnung in der Art des unten angegebenen Beispiels, aber nicht identisch mit diesem. Speichern Sie die Zeichnung in einer Datei mit einem von Ihnen gewählten Namen ab und drucken Sie sie aus.
Beispiel: ***
*******/ / ***
u www wwwwvv wwwwuuvv wwuuuwwuuu wvwvvwvvwvu vuwvwvwvv uwuvuuwwvu wvwuwvuw* vwvwvv *
000 ** **
((
* 0000 •000000 *» 0000
((
** *
zzzz zzzzzzzzzz zzzzzz **
**
* • *
**
*
**
•
• * »» ** *» **
•
•
*****
*
* **
**
233
Übungsaufgaben
B.
Machen Sie sich dabei mit den Kommandos Ihres Editors vertraut!
Übungsaufgabe 2 Der alte Apotheker Thomas Pyrin hat sich für seinen alchimistischen Hobbykeller einen kleinen Arbeitsplatzrechner gekauft. Er möchte damit die Gewichtsangaben aus seinen alten Rezeptbüchern umrechnen. Nach altem Apothekerbrauch soll mit folgenden Einheiten gerechnet werden : 1 1 1 1
Pfund Unze Drachme Skrupel
=12 = 8 = 3 = 20
Unzen Drachmen Skrupel Gran
Erstellen Sie ein Pascal-Programm, das eine ganzzahlige Gewichtsangabe in Gran (kleiner als maxint) einliest, diese in Pfund, Unzen, Drachmen, Skrupel und Gran zerlegt und das Ergebnis ausgibt! Gehen Sie bei der Programmentwicklung folgendermaßen vor: A.
Erstellen Sie : - umgangssprachlichen Programmtext, gegebenenfalls in mehreren Verfeinerungsstufen (handschriftlich), - ein Struktogramm (handschriftlich) und - das Pascal-Quellprogramm.
B.
Lassen Sie den Rechner das Programm übersetzen und ausführen. Korrigieren Sie auftretende Fehler solange, bis das Programm fehlerfrei abläuft.
Übungsaufgabe 3 Erstellen Sie ein Pascal-Programm, das einen hochmodernen "intelligenten Fahrstuhl" in einem Hochhaus simuliert. Mit dem Fahrstuhl soll beispielsweise folgender Dialog möglich sein:
234
Anhang C
Wollen Sie den Fahrstuhl benutzen? ja Fein. Wir sollten uns zunaechst miteinander bekannt machen. Ich bin in diesem Hause der Fahrstuhl. Zu meinem Aufgabenbereich gehoert die Bedienung von 25 Stockwerken. Sind Sie ein Mann oder eine Frau? Mann Sie befinden sich zur Zeit im Erdgeschoss. In welches Stockwerk wollen Sie, mein Herr? 30 Sie wollen aber hoch hinaus, mein Herr! Das oberste Stockwerk in diesem Hause ist das 23. Obergeschoss. In welches Stockwerk wollen Sie, mein Herr? 12 Vorsicht, wir fahren aufwaerts! 1. Obergeschoss 2. Obergeschoss 3. Obergeschoss 4. Obergeschoss 5. Obergeschoss 6. Obergeschoss 7. Obergeschoss 8. Obergeschoss 9. Obergeschoss 10. Obergeschoss 11. Obergeschoss 12. Obergeschoss Wir sind am Ziel. Es war mir ein Vergnuegen. Auf Wiedersehen, mein Herr! Wollen Sie den Fahrstuhl benutzen? nein Schade eigentlich. Ich bedaure sehr, Sie nicht kennenlernen zu duerfen. Auf Wiedersehen!
Solange weitere Fahrgäste den Fahrstuhl benutzen wollen, soll er in Betrieb bleiben. Der Einfachheit halber können neue Fahrgäste nur dort einsteigen, wo der letzte Fahrgast ausgestiegen ist. Der Fahrstuhl muß sich natürlich merken, in welchem Stockwerk er sich gerade befindet. Er muß dann je nach gewünschtem Fahrziel entscheiden, ob es aufwärts oder abwärts geht. Je nachdem, ob es sich bei dem Fahrgast um eine Frau oder einen Mann handelt, soll der Fahrstuhl die jeweils richtige Anrede benutzen. Lassen Sie nur sinnvolle Antworten auf die Fragen des Fahrstuhls zu. Bei falschen oder sinnlosen Eingaben soll der Fahrstuhl mit geeigneten Meldungen reagieren. Uberlegen Sie sich beispielsweise, was passieren soll, wenn der Fahrgast in das Stockwerk fahren möchte, in dem er sich gerade befindet.
235
Übungsaufgaben
Halten Sie sich bei dieser u n d allen weiteren Übungsaufgaben an folgende Vorgehensweise : - Fertigen Sie zuerst ein S t r u k t o g r a m m an. - Erstellen Sie d a n n das Quellprogramm und beachten Sie dabei die Hinweise aus Lektion 10 bezüglich Kommentierung und Layout. - Korrigieren Sie das P r o g r a m m solange, bis es fehlerfrei abläuft.
Übungsaufgabe 4 Der Wert von 7r soll näherungsweise als halber Umfang eines regelmäßigen, dem Einheitskreis eingeschriebenen TV-Ecks berechnet werden. Beginnt m a n mit einem Sechseck (Seitenlänge = 1) und verdoppelt die Anzahl der Seiten TV mit jedem Iterationsschritt, so erhält man immer besser werdende Näherungen an den Kreis und damit an 7r. Aus der Seitenlänge Sjv eines TV-Ecks läßt sich die Seitenlänge S2N des 2TV-Ecks nach der folgenden Vorschrift berechnen :
Der Näherungswert für 7r ist dann TV * S/2. Da die Seitenzahl TV des TV-Ecks sehr schnell wächst, reichen die im Rechner darstellbaren ganzen Zahlen nicht aus, u m TV darzustellen. TV muß daher bei der Berechnung von 7r aus der Nummer i des jeweiligen Iterationsschritts logarithmisch berechnet werden. Es gilt Ni = 6 e i l n 2 für t = 0 , l , 2 , . . . . Erstellen Sie ein PASCAL-Programm, das n mit Hilfe der beschriebenen Iteration berechnet. Die Schleife soll beendet werden, sobald ein Näherungswert von 7r kleiner oder gleich dem vorhergehenden Näherungswert ist. In jedem Iterationsschritt soll die N u m m e r des Iterationsschritts und der Näherungswert von TT ausgegeben werden. Sie werden feststellen, daß das Ergebnis recht ungenau ist. Modifizieren Sie Ihr P r o g r a m m u n d verwenden Sie zur Berechnung der Seitenlängen nun die (nach der dritten binomischen Formel) umgeformte Berechnungsvorschrift: Sn
c
¿2 N =
^2 + Das Ergebnis ist nun wesentlich genauer. Erklären Sie diesen Effekt.
236
Anhang C
Übungsaufgabe 5 Beim sogenannten "Game of Life" werden in einem quadratischen Spielfeld von 20x20 Feldern "Lebewesen" ausgesetzt. Jedes Feld (außer den Randfeldern) hat 8 Nachbarfelder und kann entweder von einem Lebewesen bewohnt oder unbewohnt sein. Weitere Generationen neuer Lebewesen werden nach folgenden Gesetzen erzeugt: 1. Geburt:
Auf einem unbewohnten Feld wird ein Lebewesen geboren, wenn auf den Nachbarfeldern genau 3 Lebewesen leben.
2. Tod:
Ein Lebewesen mit 4 oder mehr Nachbarn stirbt wegen Uberbevölkerung. Ein Lebewesen mit weniger als 2 Nachbarn stirbt wegen Einsamkeit.
3. Uberleben:
Ein Lebewesen mit 2 oder 3 Nachbarn überlebt.
Gegeben seien folgende Definitionen: const
d i m = 20; d i m p l u s e i n s = 21;
type
w e l t t y p = array [0..dimpluseins, 0 . . d i m p l u s e i n s ] of 0..1; { Das e i g e n t l i c h e S p i e l f e l d geht n u r v o n 1 b i s d i m }
Erstellen Sie eine Pascal-Prozedur, die von einer Textdatei, die z. B. mit Hilfe eines Texteditors erstellt werden kann, eine Ursprungsbevölkerung einliest und die Randfelder des Spielfelds als unbewohnte Region initialisiert. Die leeren Randfelder erleichtern das Erzeugen neuer Generationen. Erstellen Sie eine Pascal-Prozedur, die die aktuelle Bevölkerungssituation in einer Textdatei ablegt, um sie später wieder einlesen zu können. Erstellen Sie eine Pascal-Prozedur, die den aktuellen Zustand des Spielfelds auf dem Bildschirm anzeigt. Erstellen Sie eine Pascal-Prozedur, die die nächste Generation von Lebewesen erzeugt. In allen Prozeduren sollen keine globalen Variablen verwendet werden. Die Namen der Textdateien sollen jeweils vom Benutzer erfragt werden. Erstellen Sie unter Verwendung dieser Prozeduren ein Pascal-Hauptprogramm, bei dem man jeweils unter folgenden Möglichkeiten wählen kann: •
Neuen Bevölkerungszustand von einer Datei einlesen und anzeigen
•
Aktuellen Bevölkerungszustand in einer Datei ablegen
•
Neue Generation erzeugen und anzeigen
•
Programm beenden
237
Übungsaufgaben
Einige Bevölkerungen verhalten sich recht sonderbar. Das hier abgebildete "Volk" besteht beispielsweise konstant aus 5 Lebewesen und wandert von Generation zu Generation weiter nach "rechts u n t e n " . Versuchen Sie, selbst interessante Bevölkerungen zu finden. 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
1 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
Übungsaufgabe 6 Im Ankunftsbereich eines Flughafens soll eine große Anzeigetafel über die in Kürze zu erwartenden Landungen informieren. A.
Erstellen Sie ein PASCAL-Programm, das von einer Textdatei die Liste aller pro Tag ankommenden Flüge einliest u n d die Daten als Datensätze (rec o r d s ) in einer Datei ablegt. Die Namen der beiden Dateien sollen über die T a s t a t u r eingegeben werden. Die Textdatei, die Sie mit Hilfe eines Texteditors erstellen können, enthalte pro Zeile drei Zeichenketten: die Flugnummer, den Herkunftsort des Flugzeuges und die planmäßige Ankunftszeit. Z. B . : LH228 PA775 AF345 BA611
Frankfurt Berlin Nairobi London
6.35 8.15 8.55 10.10
238
B.
Anhang C
Erstellen Sie ein PASCAL-Programm, das eine menügesteuerte Bedienung der Anzeigetafel ermöglicht. Die Daten der Flüge, die auf der Anzeigetafel angezeigt werden sollen, sollen als verkettete Liste gespeichert werden. Jedes Element der verketteten Liste soll folgende Daten enthalten: Flugnummer, Herkunftsort, planmäßige Ankunftszeit, voraussichtliche tatsächliche Ankunftszeit und einen booleschen Wert, der angibt, ob die Landung bereits erfolgt ist. Die Liste soll nach den voraussichtlichen tatsächlichen Ankunftszeiten sortiert sein. Der Bediener der Anzeigetafel soll in einem Menü unter folgenden Aktionen wählen können: •
Erwarteten Flug in die Liste aufnehmen Der Bediener gibt die Flugnummer ein. In der Datei, die mit dem Programm aus Aufgabenteil A erzeugt wurde, wird der zugehörige Datensatz gesucht. Die gefundenen Daten werden auf dem Bildschirm angezeigt (bzw. Fehlermeldung, falls nicht gefunden). Der Bediener gibt die voraussichtliche tatsächliche Ankunftszeit ein. Entsprechend dieser wird ein neues Listenelement in die verkettete Liste einsortiert.
•
Voraussichtliche Zeit der Landung ändern Der Bediener gibt die Flugnummer ein. Die zugehörigen Daten werden in der verketteten Liste gesucht und auf dem Bildschirm angezeigt (bzw. Fehlermeldung, falls nicht gefunden). Der Bediener kann die voraussichtliche Ankunftszeit modifizieren. Das Listenelement wird entsprechend der neuen Zeit in die verkettete Liste durch Umhängen von Zeigern einsortiert. Dabei wird kein neues Listenelement erzeugt.
•
Landung eintragen Der Bediener gibt die Flugnummer ein. Die zugehörigen Daten werden in der verketteten Liste gesucht und auf dem Bildschirm angezeigt (bzw. Fehlermeldung). Der boolesche Wert wird a u f t r u e gesetzt.
•
Flug aus der Anzeige entfernen Der Bediener gibt die Flugnummer ein. Die zugehörigen Daten werden gesucht und angezeigt (bzw. Fehlermeldung). Falls das Flugzeug bereits gelandet ist, wird das entsprechende Listenelement aus der Liste gelöscht. Andernfalls erfolgt eine entsprechende Fehlermeldung.
•
Bedienprogramm beenden
Nach jeder Aktion soll der Inhalt der gesamten Liste (ohne boolesche Werte) auf dem Bildschirm angezeigt werden. Bei bereits erfolgter Landung soll hinter den Daten des Fluges der Hinweis g e l a n d e t ausgegeben werden. Verwenden Sie Pascal-Prozeduren und -Funktionen zur besseren Strukturierung Ihres Programms!
Übungsaufgaben
239
Übungsaufgabe 7 Sie h a b e n sich in e i n e m Freizeit-Park in ein L a b y r i n t h g e w a g t u n d s t e h e n j e t z t vor d e m D i l e m m a , daß Sie den A u s g a n g nicht m e h r finden. N a c h l ä n g e r e m S u c h e n h a b e n Sie endlich den W e g nach draußen g e f u n d e n u n d ü b e r l e g e n sich, daß dieses S u c h p r o b l e m durch ein geeignetes P a s c a l - P r o g r a m m gelöst werden k a n n . D a s L a b y r i n t h stellen Sie dabei als 20 x 20 M a t r i x v o m T y p c h a r dar. Ist ein E i n t r a g in dieser M a t r i x gleich #, dann b e f i n d e t sich an dieser Stelle eine Hecke. Die Wege zwischen den Hecken sollen durch Leerzeichen dargestellt werden. E r s t e l l e n Sie ein P a s c a l - P r o g r a m m , d a s ein L a b y r i n t h von einer T e x t d a t e i einliest u n d i m Anschluß d a r a n zu einer ü b e r T a s t a t u r e i n g e g e b e n e n S t a r t p o s i t i o n e n t s c h e i d e t , o b ein Weg von dieser P o s i t i o n zu e i n e m A u s g a n g e x i s t i e r t . F ä l l t die eingelesene P o s i t i o n a u f eine Hecke, muß die E i n g a b e wiederholt werden. L ö s e n Sie die A u f g a b e rekursiv u n d stellen Sie d a s L a b y r i n t h m i t d e m e r m i t t e l t e n Weg, falls einer e x i s t i e r t , auf d e m B i l d s c h i r m d a r .
240
Anhang D
Pascal-Versionen In diesem Anhang werden, ohne Anspruch auf Vollständigkeit, die wichtigsten Fälle beschrieben, in denen sich einige Pascal-Versionen anders verhalten, als es in Standard-Pascal vorgesehen ist oder bisher in der Pascal-Fibel beschrieben wurde. Gegliedert nach Themen der Fibel wird hier das Verhalten von Turbo-Pascal, PolyPascal und XL Pascal im Unterschied zu Standard-Pascal beschrieben. Es können in diesem Anhang nicht alle kleinen und kleinsten Abweichungen besprochen werden. Das würde den Rahmen sprengen und die Übersichtlichkeit nicht gerade erhöhen. Die genauen Einzelheiten einer Pascal-Version können Sie in jedem Fall dem zugehörigen Handbuch entnehmen. In Zweifelsfällen oder bei merkwürdigem Verhalten Ihres Ubersetzers sollten Sie also Ihr Handbuch befragen.
D a t e n t y p e n integer und real In Lektion 7 werden die Datentypen i n t e g e r und r e a l eingeführt. In Lektion 11 wird an einem Beispiel vorgeführt, daß die Beschränkung des Wertebereichs von i n t e g e r oder r e a l hinderlich sein kann und zu schwer auffindbaren Programmierfehlern führen kann. Dieses Problem lässt sich zwar nicht beseitigen, jedoch können durch größere Wertebereiche mehr Möglichkeiten für den Programmierer geschaffen werden. Turbo-Pascal stellt zu diesem Zweck zusätzlich die Datentypen l o n g i n t , double und extended zur Verfügung, l o n g i n t nimmt integer-Variablen im Wertebereich von -2147483648 bis 2147483647 auf. Der Speicherbedarf einer solchen Variable beträgt dann 4 Byte. Die Datentypen double und extended nehmen real-Variablen auf. Der Wertebereich reicht dann für double von 5.0* 10~324 bis 1.7* 10308 (Speicherbedarfs Byte) und für e x t e n d e d von 1.9 * 10" 4951 bis 1.1 * 104932 (Speicherbedarf 10 Byte). Eine Erweiterung des Wertebereiches für i n t e g e r gibt es in PolyPascal nicht. Für real-Variablen ist der Wertebereich je nach verwendeter PolyPascal-Version verschieden. Ein Blick in das Handbuch gibt hier die genaue Auskunft. XL Pascal kennt die drei Datentypen i n t e g e r , r e a l und s h o r t r e a l . Den jeweiligen Wertebereich kann man mit Hilfe der Standardkonstanten m i n i n t und maxint
241
für i n t e g e r , m i n r e a l und maxreal für r e a l sowie m i n s r e a l und maxsreal für s h o r t r e a l abfragen.
Vorrangregeln Bei arithmetischen Operationen mit Variablen der Datentypen i n t e g e r und r e a l sind in Lektion 9 bestimmte Vorrangregeln für Standard-Pascal angegeben. In der Mathematik gelten jedoch etwas andere Regeln. Turbo-Pascal, PolyPascal sowie XL Pascal halten sich an diese mathematischen Regeln, die auf der obersten Stufe den Vorzeichen und dem Negationsoperator Vorrang vor allen anderen gewähren.
string-Variablen string-Variablen sind von Standard-Pascal nicht vorgesehen, jedoch in den meisten Pascal-Versionen vorhanden. Die Definition und Verwendung eines s t r i n g s in Turbo-Pascal ist in Lektion 9 beschrieben. Für PolyPascal gelten diese Angaben entsprechend. In XL Pascal wird bei der Deklaration einer string-Variablen die maximale Länge in runden Klammern angegeben. Beispiel: ^ var Zeichenkette: String(20);
J
Häufig stehen diverse Funktionen zum Umgang mit string-Variablen zur Verfügung. Neben dem Verbinden von Zeichenketten sind Zuweisungen und Vergleiche möglich. Auch gibt es Funktionen zum Bestimmen der aktuellen Länge eines s t r i n g s . Namen und Verwendung solcher Funktionen entnehmen Sie bitte Ihrem Handbuch.
Zeigervariablen In Turbo-Pascal und XL Pascal können Zeigervariablen genau so, wie es in Lektion 14.2 beschrieben wird, mit new und d i s p o s e verwaltet werden. In PolyPascal gibt es die Prozedur dispose nicht. Stattdessen gibt es die beiden Prozeduren mark und release. Wenn mit der Prozedur new Speicherplatz für eine dynamische Variable angefordert wird, so wird dieser vom sogenannten heap (Haufen) genommen. Der heap ist in PolyPascal wie ein Stapel aufgebaut, der wächst, wenn man Speicherplatz anfordert. Ein systeminterner Zeiger (heap pointer) zeigt immer auf den Speicherplatz oberhalb des Stapels, der bereitgestellt wird, sobald Platz für die nächste dynamische Variable angefordert wird.
242
Mit Hilfe der Prozedur mark kann man sich den Stand dieses heap-Zeigers merken. Da der Stand des heap-Zeigers eine Speicheradresse ist, benötigt man zum Merken eine Variable, die eine Speicheradresse aufnehmen kann, also i r g e n d e i n e Zeigervariable von beliebigem Zeigertyp. Zum Beispiel: var heapzeiger:
"integer;
begin mark(heapzeiger);
Durch den Aufruf von mark (heapzeiger) merkt man sich in der Zeigervariablen heapzeiger den Stand des Stapels für dynamische Variablen zum Zeitpunkt des Aufrufs. Anschließend kann man mit new Speicherplatz für weitere dynamische Variablen anfordern. Ruft man dann schließlich ^ r e l e a s e (heapzeiger) ;
J
auf, so wird der gesamte Speicherplatz, der seit dem Aufruf von mark ( h e a p z e i g e r ) angefordert wurde, an das System zurückgegeben. Dabei werden keine Zeiger auf n i l gesetzt! Beim Arbeiten mit mark und r e l e a s e muß man besonders vorsichtig sein, weil man dort alle Zeiger, die auf dynamische Variablen aus dem zurückgegebenen Speicherbereich zeigen, selbst mit dem Wert n i l versehen muß. Leider ist dieses Vorgehen nicht so komfortabel wie in Standard-Pascal mit d i s p o se, denn man kann Speicherplatz immer nur am oberen Ende des Stapels zurückgeben, aber nie aus der Mitte des Stapels heraus. Turbo-Pascal bietet diese Variante mit mark und r e l e a s e zusätzlich an.
Dateibearbeitung Pascal-Systeme wie Turbo-Pascal, PolyPascal oder XL Pascal stellen im Bereich der Dateibearbeitung einige hilfreiche Funktionen zusätzlich zum Funktionsumfang von Standard-Pascal zur Verfügung. Einige solcher Funktionen sind in Lektion 15 bereits beschrieben. Diese Beschreibungen gelten für Turbo-Pascal und im Wesentlichen auch für PolyPascal und XL Pascal. Die folgenden Tabellen geben einen Uberblick über drei besonders nützliche Funktionen mit ihren unterschiedlichen Namen und Verwendungsweisen in den verschiedenen Systemen. Um näheres über das Verhalten und die Benutzung dieser Funktionen zu erfahren, wenden Sie sich bitte an Ihr Handbuch.
243
Textdateien Beschreibung Daten an eine bestehende Datei anhängen
Turbo-Pascal
PolyPascal
append(datei)
append(datei)
XL Pascal rewrite(datei, DISP=MOD)
Dateien mit Direktzugriff Beschreibung Position des Dateizeigers Länge der Datei
Turbo-Pascal
PolyPascal
XL Pascal
f ilepos(datei)
position(datei)
nicht vorhanden
f ilesize(datei)
length(datei)
nicht vorhanden
In Lektion 12 und in Lektion 15 wird das Offnen einer Datei unter Turbo-Pascal beschrieben. Mit PolyPascal geht man genauso vor. Nur XL Pascal weicht von dieser Vorgehensweise ab. Hier wird z. B. die Datei r o m a n . t x t nur durch den Befehl Mreset(datei,
'NAME=roman.txt');
"j
geöffnet. Um ein Programm trotzdem mit verschiedenen Dateien benutzen zu können, muß man zu folgendem Trick greifen:
V
{ e i n l e s e n von datnam } datnam := 'NAME=' + datnam; r e s e t ( d a t e i , datnam);
Was gibt's außerdem? Turbo-Pascal ist ein allgemein bekanntes und weit verbreitetes Pascal-Entwicklungspaket. Compiler und Editor sind in einer gemeinsamen Benutzeroberfläche integriert. Turbo-Pascal bietet zusätzlich zu den von Standard-Pascal vorgesehenen Programmkonstrukten eine Vielzahl an Erweiterungen. Einige dieser Erweiterungen sind hier kurz aufgeführt, ohne jedoch auf die genaue Anwendung und die jeweiligen Eigenschaften im einzelnen einzugehen. Zu diesem Zweck sollten Sie mal wieder einen intensiveren Blick in Ihr Handbuch oder ein spezielles Buch über Turbo-Pascal werfen.
244
- Units sind Programmeinheiten und ermöglichem eine Verteilung eines Pascal-Programms auf mehrere Quelldateien. Eine Programmeinheit besteht aus einem Kopf (Header), einem Schnittstellenteil (Interface) und einem Realisierungsteil (Implementation). Der Schnittstellenteil enthält Deklarationen von globalen Variablen und Konstanten sowie Prozedur- und Funktionsköpfe. Im Realisierungsteil steht dann der Programmtext für die oben angegebenen Prozeduren und Funktionen. In andere Programmeinheiten kann man diese dann einbinden und benutzen. - set ist das reservierte Wort für den Datentyp Menge. In Turbo-Pascal kann man eine abzählbare Menge von Elementen in einem s e t zusammenfassen. Mit type Mengenname = s e t of Mengentyp wird ein solcher Datentyp definiert. Für Mengen sind u. a. Operationen wie z. B. Vereinigung und Differenz sowie Relationen wie z. B. Ungleichheit und Teil- bzw. Obermenge definiert. - Variante Records beinhalten einen variablen Teil, der in Abhängigkeit von bestimmten Eigenschaften ein entsprechendes Aussehen annimmt. - Objekte sind Datenstrukturen, die sowohl eine Gruppe von verschiedenen Datentypen als auch zugehörige Funktionen und Prozeduren, die sogenannten M e t h o d e n aufnehmen können. Zusätzlich besitzt ein Objekt die Eigenschaft, Informationen an untergeordnete Objekte zu vererben. Diese Strukturen ermöglichen eine objektorientierte Programmierung. In andern Programmiersprachen, z. B. C + + , werden solche Objekte häufig auch als Klassen bezeichnet. - Grafikbefehle, die als Bibliotheks-Unit mitgeliefert werden, ermöglichen eine schnelle Grafikprogrammierung. Es gibt Prozeduren und Funktionen zur Ausgabe von Grafik auf dem Bildschirm. Verschiedene Grafiktreiber werden ebenfalls mitgeliefert. - Ein integrierter Debugger soll helfen, insbesondere Laufzeitfehler im Quelltext ausfindig zu machen. Dieser Abschnitt ist nicht als Werbung für Turbo-Pascal zu verstehen, sondern soll vielmehr eine Hilfestellung sein für die häufig gestellten Fragen: „Kann ich das auch in Turbo-Pascal programmieren?" oder „Wieso läuft mein Turbo-Pascal-Programm auf den Rechnern im Rechenzentrum nicht?" Wenn alle Hinweise in diesem Anhang beachtet werden, sollte die Beantwortung dieser Fragen nicht mehr gar so schwer fallen.
245
Anhang E
Literaturhinweise 1. Pascal-Lehrbücher E.-E. Doberkat / P. Rath / W.
Rupietta:
Programmieren in PASCAL Grundbegriffe und Methoden Akademische Verlagsanstalt Wiesbaden, 1981 R.
Marty:
Methodik der Programmierung in Pascal (2. korr. Auflage) Springer-Verlag Berlin, 1984 T. Ottmann / P.
Widmayer:
Programmierung mit PASCAL Teubner Studienskripten Stuttgart, 1980
2. Nachschlagewerke K. Däßler / M.
Sommer:
Pascal Einführung in die Sprache - DIN-Norm 55256 - Erläuterungen (2. Auflage) Springer-Verlag Berlin, 1985 K. Jensen / N.
Wirth:
pascal - User Manual and Report (Third Edition) Springer-Verlag Berlin, 1985
3. Weiterführende L i t e r a t u r L. Goldschlager / A.
Lister:
Informatik - Eine moderne Einführung Carl Hanser Verlag München, 1984 N.
Wirth:
Algorithmen und Datenstrukturen (3. Auflage) Teubner Verlag Stuttgart, 1983
246
Stichwortverzeichnis A Abbruchkriterium 56, 60, 81 ff ablauffähig 13, 22f abs 36f, 81 Absolutwert 36 abzählbar 52 f, 63, 65, 84, 88, 151 Addition 3f, 6, 20, 30, 33 f, 76, 79 Adresse 155 A k t u a l p a r a m e t e r 126 f f , 135, 138 - l i s t e 126, 131 Algorithmus 7f, 20, 25 - , Euklidischer 189f - , iterativer 81 f f , 190 - , rekursiver 189ff - s c h r i t t 20, 25 Alphabet 41 and 16, 42 f Anfangswert 65 Anker 160 ff Anweisung 3 f f , 8f, 20, 22 Anweisungsteil 20 Apostroph 15, 18 append 185, 243 Aprilscherz 49 Arbeitsplatzrechner 11, 40, 75 Arbeitsspeicher 11 f, 22f a r c t a n 36 Argument 35 f, 63 f, 68 A r i t h m e t i k 78 a r r a y 16, 84, 91 f f , 102 ff A S C I I 40f, 63f - -Tabelle 40 a s s i g n 96, 111 f f , 177, 184 a t 16 Aufruf 22 f, 27f, 118 Aufzählungstyp 84 ff Ausdruck 31 f - , arithmetischer 31, 33ff, 78f —, boolescher 41 f f , 48 —, Selektor- 53
—, Unter- 36 Ausführung 3f, 12 f f , 22 ff A u s g a b e 3, 13, 28ff,
- b r e i t e 29, 32 - d a t e i 112, 114 f - d a t e n 72 ff - l i s t e 138, 183 Auswahl 38 f
32, 112,
B Backtracking 199 Bedingung 39, 43 f b e g i n 16, 20, 46 f Beispielprogramm - binom 191 ff - feldsort 118f - haeufigkeit 94ff - hanoi 197f - matmult 110f - mittelwertl 58f - mittelwert2 59ff - mittelwert3 66f - monat 50 f
- reihensumme 67f
- skalarprodukt lOOf
- Springerproblem 199f
- vokal 39f Benutzeroberfläche 12 Bereich 52 f, 76 ff Bereichsüberschreitung 76f B e t r i e b s s y s t e m 12 ff Bezeichner 15, 17ff, 26 ff Bildschirm 3f, 11 ff Binomialkoeffizient 135f, 191 Blockkonzept 74, 125 Boole, George 42 boolean 42 B o t a n i k 176 B r o w n , Charlie 51
114f
247
c
do 16, 60, 65 f f Doppelpunkt 27, 29 double 240 downto 16, 65 f Drucken 3, Uff Dschungel 197 Dualdarstellung 76 f Dynamische - Datenstruktur 153 - Variable 153 f f , 241 - Verbund variable 156
call by value 129 call by reference 129 Camping-Ausrüstung 199 c a s e 16, 49 f, 52 ff, 87, 149ff, 162 char 39 ff chr 64 close 16, 98f, l l l f f , 183 Codierung 5 ff Compiler 22, 243 const 16, 101 f f , 107 cos 36f Cosinus 36 C P U 12
E
D dangling pointer 158 D a t e i 12ff, 27f, 9 5 f f , l l l f f , 173ff b e a r b e i t u n g 173ff, 183ff, 242
- b e a r b e i t u n g , mit Direktzugriff 180 - b e a r b e i t u n g , sequentielle 175 - e n d e 97f, 184 f - n a m e 22, 96, Ulf - n a m e , externer 111, 177, 184 - n a m e , interner 112, 177, 184 - t y p 96, 173 f - V e r w a l t u n g 13 f -Verzeichnis 12ff, 22 zeiger 179 f, 182 ff Daten 3f, 6, 7ff —ausgab e 3, 28 —bereich, fester 155 bereich, variabler 155 - e i n g ä b e 3, 27 - m e n g e n 173 - o b j e k t 173 f - s a t z 174 f f , 179 ff - s t r u k t u r 51, 84, 91, 100, 140 f, 153, 160,
171
- t y p 26f, 141, 173,
39, 42, 5 I f f , 84, 85f, 240
—typ, Selbstdefinierter 85 Debugger, integrierter 244 Dereferenzierung 154 f, 171 Dezimalpunkt 16, 29 Direktzugriff 174 f, 180ff, 243 Diskette 12, 175 dispose 157f, 241 d i v 16, 24, 33 f f , 70ff
Division 33 f Divisions —Operatoren 33 ff —rest 33
Editor 12ff, 22f Effizienz 148, 171, 196 Ein-/Ausgabe 27, 101, 142 -fehler 186 f - g e r a t e 12 —Vorgänge 12 eindimensional 91 Einfügen 165, 167f, 175 Eingabe 22 f, 30 - d a t e i 112 ff - d a t e n 72 f - t a s t e 23, 28, 97f Einrücken 45, 71, 74 Element 91 ff eise 16, 44 ff end 16, 20, 46 f Endwert 65 eof 98 f , 183 eoln 185 Erweiterungen 243 f Euklid 189 f e x o r 16 exp 36f Exponent 77f extended 240 e x t e r n a l 16
91 f ,
F Fachchinesisch 129 Fakultät 134 f f , 207 Fall-Index 52 f Fallkonstantenliste 52 f falsch 41 f false 42 Fehler - , logischer 25 —, semantischer 24 f —, syntaktischer 24 f F e h l e r m e l d u n g 22ff,
76ff
Stichwortverzeichnis
248
Feld 84, 91 f f , 100 f f , 118 f, 140 f f , 171, 200ff
- e l e m e n t 91 f f , 120 f F e s t e r Datenbereich 155 F e s t p l a t t e 12, 175 Festpunktschreibweise 16, 29 f i l e 16, 173 f, 184 f i l e p o s 184, 243 f i l e s i z e 184, 243 Flußdiagramme 9 Folge 9, 38 f for
16,
64ff,
69,
111 f f
F o r m a l p a r a m e t e r 126f,
129f
I i f 16, 43 f f Imaginärteil 145 i n 16 Index 93f, 105f, 108f, 141, 143 - t y p 91 f f , 104 I n i t i a l i s i e r u n g 58, 61 f, 67ff, 94 f,
99f
i n p u t 20, 27, 96 f, 185 i n t e g e r 20, 26f, 33f, 36f,
75ff,
i o r e s u l t 186f
240
I t e r a t i o n 81 f f , 190, 197, 199, 207 I t e r a t i o n s s c h r i t t 81 f Iterationsverfahren 81
- l i s t e 126ff, 131, 133 forward-Deklaration
206f
f u n c t i o n 16, 133f, 136ff, 190f Funktion 35f, 63f, 116, 133ff, 180, 183, 190 f, 201 f f , 206 f
Funktionsaufruf 63 f, 134 —deklaration 116, 133, 206 —deklarationsteil 116 - k ö p f 133 —name 133 f —parameter 138 - r é s u l t a t 133 - w e r t 138
G
K K l a m m e r 35 f, 43 —, eckige 51, 93 —, spitze 2 K o m m a 27, 52, 85, 126, 141, 148 K o m m a n d o 12 f f , 23 K o m m e n t a r 18 f, 71, 73 f, 117 K o m p l e x e Zahlen 145f, 152 K o m p o n e n t e lOOf, 140f,
28,
ganzzahlig 16, 26, 33, 35 Genauigkeit 77f, 81 f - , absolute 78, 80 - , relative 78, 80f, 91, 98 Gestaltung des P r o g r a m m t e x t e s 71, 73 f
g e t 175 ggt 189f Gleichheitszeichen 85 Gleitpunktschreibweise 16, 29 global 122, 125, 127, 135, 196 g o t o 16 Grafikbefehle 244 Grundsoftware 11 f Gültigkeitsbereich 122, 125, 133, 148
H Hängende Zeiger 158 Hanoi, T ü r m e von 197f H a r d w a r e 11, 24 H a u p t p r o g r a m m 118 f, 127 ff, 160 f heap-Zeiger 241 f
143f,
148ff
K o m p o n e n t e n t y p 91 f f , 173, 176, 183 K o n s t a n t e 101 f Konstantendefinitionsteil 101, 116 Kontrollaufgabe 1, 6, 10, 14, 18, 21, 25, 31,
37,
115,
131,
188,
207
47, 54, 138,
151,
70, 74, 83, 159,
168,
89, 171f,
L l a b e l 16 L a u f v a r i a b l e 65,
67ff
Leeranweisung 45 f Leerzeichen 17ff, 29, 40, 97, 104, 114 Leerzeile 30, 74, 117, 126 l e n g t h 243 Liste 85f, 91, 118f, 128, 153 - , v e r k e t t e t e 160ff,
168, 171,
Listenelement 121, 160f, 164f, 177 f
I n 36 f L o g a r i t h m u s 36, 67 lokal 122 f, 125, 127ff, 134f, 196 l o n g i n t 240 Lücke 77f
M M a g n e t b ä n d e r 174
176ff
167f,
106,
249
Mantisse 77ff mark 241 f Maschinen - b e f e h l 4f " P r o g r a m m 5f, 12 f, 22 f - s p r ä c h e 4 ff M a t r i x 109 f f , 113f, 200 f Matrizenmultiplikation HOf, 113 maxint 17, 76 mehrdimensional 108 f M e n g e 244 Mensaessen 31, 150 M e n ü 12, 160 ff M e t h o d e 244 Mittelwert 58 f, 62, 66 f mod 16, 33 f f , 43, 99, 190 Modulo-Operation 33 M ö n c h e 197f Multiplikation 33 f, 77, 81 f, 111, 113, 152
Multiplikationsoperatoren 35, 43
N Nachfolger 64 f, 165, 169 Näherungswert 81 Nassi-Shneiderman-Diagramme 9 Negationsoperator 43 new 154, 156, 167, 171, 241 Newton 81 nil
16, 154, 157f,
not 16, 42 f f
177ff,
242
o O b j e k t 244 Objektprogramm 5f odd 68 f f o f 16, 52, 91, 173 Öffnen 96, Ulf, 175, 177f, 183, 185, 187, 243
Operand 33 f, 42 O p e r a t o r 34 f, 42 f - , arithmetischer 33 ff - , Boolescher 42 f -Multiplikations- 35, 43 - N e g a t i o n s - 43 —Summations- 35 f, 43 -Vergleichs- 41, 43 -Vorzeichen- 36, 43 o r 16, 42ff ord 63 f , 86,
Ordinal
94
- f u n k t i o n e n 63 - t y p 52, 63 f , 84, 91 - z a h l 63 f, 86 Ordnung 40 f, 63 f, 86 o t h e r w i s e 16, 52ff output 20, 27f, 96, 185 o v e r l a y 16
P
packed 16, 104
P a r a m e t e r 28f, 125ff, 129, 131, 137f - l i s t e 27ff —Übergabe 129 Paris 18 Pascal, B l a i s e 135 Pascal-Entwicklungspaket 243 P a s c a l - S y s t e m 13, 22f Pascal-Version 2, 53, 138, 175, 241 P C 11, 40 pointer 153 —, dangling 158 Poly Pascal 240 ff
p o s i t i o n 243
Präzedenz 35, 43 pred 64, 86 p r o c e d u r e 16, 117 P r o d u k t 100, 110, 134, 152 program 16, 20 P r o g r a m m 3 f f , 11 f f , 20, 22 ff —ablauf 9 -ablaufplan 9 - b l o c k 20, 116 -entwicklung 7, 73 —entwicklungssysteme 13 —entwurf 7 f Programmier - s p r ä c h e 3 f f , 12, 23 f, 26 - u m g e b u n g 11 f Programm - k ö p f 20, 27, 73 f, 96, 116 - n a m e 13, 22 - s t r u k t u r 9, 38 f, 71 - t e x t 2, 5 f f , 12 f, 22 ff Prozedur 27ff, 116ff - a u f r u f 118, 125 - b l o c k 117 - d e k l a r a t i o n 116 f, 126 - k ö p f 117, 126 f —name 117 f P u n k t 20 put 175 Pythagoras 79
250
Stichwortverzeichnis
Q Quadratwurzel 24, 36, 81 Quellprogramm 5 f f , 12, 22
R read 27f, 97f, 105, 138, 175, 178 f, 183 ff readln 28, 98, 138, 185 r e a l 26, 29, 34, 36,
75, 7 7 f f , 81 f , 84,
240
Realteil 145 Rechenoperationen 26, 75 ff Rechner 1, 3 f f , 11 f f , 22 ff Rechnerarithmetik 75 ff record 16, 140 ff, 149 ff, 176 Record, varianter 244
- b e d a r f 240 -adresse 176 - p l a t z 26, 115, 153, 155, 157f, 196 -zelle 155 Spezialsymbol 15ff Spinat 17 Springerproblem 199 ff sqr 36 f sqrt 36f Standard -ausgabe 28 -ausgabedatei 28, 185 —bezeichner 17f, 42 eingäbe 27 -eingabedatei 27, 97, 185 -funktion 35 f, 63 f, 68, 86, 98, 116, 133,
r e e l l 16, 26, 34 f , 75, 7 7 f f , 145 f
r e p e a t 16,
138,
56ff
r e s e t 96, 111,
175,
179,
180ff,
183,
Rest 33 Resultattyp 133 rewrite 112, 175, 179, 183, 187, 243 round 35f Rundung 35, 79 Rundungsfehler 80
s S c h l e i f e 57, 5 9 f f , 64 f f , 69
Schleifenrumpf 56f, 59, 61, 65 Schreibmarke 23 seek 180 ff, 183 136ff
Selektorausdruck 53 Selektorkonstanten-Liste 149, 151 Semantik 24 f Semikolon 20, 44, 46, 53, 117, 133 sequentiell 174, 175 f f , 179, 185 set 16 set of 244 shl 16 shortreal
240f
shr 16 sin 36f Sinus 36 Skalarprodukt 100 ff Snoopy 51 S o r t i e r a l g o r i t h m u s 118ff,
Speicher 11 f
185
243
174 f ,
240
-Prozedur 27f, 116f, 131, 154, 175 Stapel 196 Stellenzahl 77f Steuerzeichen 40 s t r i n g 16, 51, 84, 104 ff, 138, 241 Struktogramm 9f, 38f, 49, 53, 57f, 60, 66,
68, 95, 100,
163,
165,
200,
209
169,
110, 170,
118, 177f,
121,
161,
189,
198,
Strukturierte Programmierung 9, 73 Subtraktion 33 f, 79 succ 64, 86 Suche
Satztyp 140
Seiteneffekt
179,
- -Pascal 16, 28, 41, 51 f f , 96 f f , 104 f,
Referenz 154 Rekursion 189 ff - , indirekte 206 f release 241 f
163ff,
181
Summation 4 ff, 30, 76 Symbol 15, 18 Syntax 24 f, 27
T Tangens 36, 133 Tastatur 3f, 11 f, 27f -eingäbe 27f Teilbereichstyp 84, 88 f, 91 t e x t 96, 184 f Text 12 f f , 95 - d a t e i 95 f f , 111 f f , 184 f , 243 - e d i t o r 22, 95 f then 16, 43 ff to
16,
65ff
Trennzeichen 17f, 44 trial and error 199 t r u e 42f trunc 127f
35ff
Turbo-Pascal 2, 13, 22f, 240 ff T ü r m e von Hanoi 197
251
Typ —anpassung 35 - d e f i n i t i o n 85 f f , 91, 173f - d e f i n i t i o n s t e i l 85, 116 t y p e 16, 85 f
u Übersetzer 5f, 12 f, 22 ff Übungsaufgabe 1, 14, 37, 74, 83, 139, 188,
208
Undefiniert 28, 31, 65, 125, 127, 151, 154 Unerreichbares O b j e k t 158f Ungenauigkeit 77f, 83 ungerade 68 ungleich 41 Unit 243 Unterausdruck 36 U n t e r p r o g r a m m 116 ff u n t i l 16, 56f
V v a r 16, 27 Variable 26 ff - , dynamische 153 f f , 241 - , globale 122, 125, 127, 135 - , lokale 122, 125, 127, 135, 196 —, statische 153 Variablen - d e k l a r a t i o n 26f -deklarationsteil 116 - p a r a m e t e r 128 f f , 196 Variabler Datenbereich 155 Variante 149 ff, 171 —, aktive 151 Variantenselektor 149 f f , 171 Varianter R e c o r d 244 Variantteil 149ff V e k t o r 100 ff Verbund 141 f f , 149 ff -anweisung 46 f - t y p 140, 142 f f , 146 —variable, dynamische 156 Vereinbarungsteil 20, 116, 177 Verfeinerung 8, 10, 116, 162, 165 Vergleich 41 Vergleichsoperatoren 41, 43
verkettete Liste 160ff vordefiniert 17, 36, 96, 131 Vorgänger 64 f Vorrangregeln 35, 43, 241 Vorzeichen 16, 33, 43 -Operatoren 36, 43
w wahr 41 ff Wahrheitswert 42 W e r t e b e r e i c h 16, 33, 75 ff, 82, 88 f, 92f, 136
W e r t p a r a m e t e r 126 f f , 196 w h i l e 16, 60ff W i e d e r h o l u n g 56 f, 60 f, 63 f W i r t h , Nikiaus 39 W i r t s t y p 89 w i t h 16, 146 ff W ö r t e r , reservierte 16ff W o r t s y m b o l 16ff w r i t e 28 ff, 112, 138, 175, 183 w r i t e l n 29f, 138, 185 W u r z e l 44, 81
X X L Pascal 240 ff
z Zahlen 16, 75 ff Zahlendarstellung 75 ff Zeichenkette 15, 30, 41, 51, 104 ff Zeichenkettenvariable 105 f Zeichensatz 39 f, 63, 94 f Zeiger 153 ff —, hängende 158 - t y p 84, 153 f -variable 153f, 241 —Zuweisung 157 Zeile 17f, 28, 109f Zeilenende 18, 97f, 185 Zeilenstruktur 17 Zentraleinheit 11 f Ziffer 16f Zuweisung 30f zweidimensional 109