Einführung in die Informatik [8., vollständig überarb. Aufl.] 9783486595390

Dieses Buch bietet eine umfassende und anschauliche Diskussion fundamentaler Konzepte der Informatik. Es führt in Grundl

328 124 54MB

German Pages 925 Year 2009

Report DMCA / Copyright

DOWNLOAD FILE

Polecaj historie

Einführung in die Informatik [8., vollständig überarb. Aufl.]
 9783486595390

Table of contents :
First Page
Title Page
Copyright Page
Foreword
Table of Contents
Chapter
SampleStart
Leseprobe
SampleEnd

Citation preview

Einführung in die Informatik von Prof. Dr. Heinz Peter Gumm und Prof. Dr. Manfred Sommer Philipps-Universität Marburg unter Mitwirkung von Prof. Dr.Wolfgang Hesse und Prof. Dr. Bernhard Seeger

8., vollständig überarbeitete Auflage

Oldenbourg Verlag München

Heinz Peter Gumm ist Professor für Theoretische Informatik in Marburg. Studium in Darmstadt und Winnipeg (Canada) von 1970 –1975. Promotion und Habilitation an der TU Darmstadt. Es folgten Gastprofessuren in Honolulu (Hawaii), Darmstadt, Kassel und Riverside (Californien). 1987–1991 Professor für Informatik an der State University of New York. Seit 1991 Professor für Theoretische Informatik an der Universität Marburg. Forschungsgebiete: Formale Methoden, Allgemeine Algebren und Coalgebren. Prof. Gumm hält Vorlesungen über Praktische Informatik, Technische Informatik, Theoretische Informatik, Beweissysteme, Verifikation, Zustandsbasierte Systeme und Abstrakte Datentypen. Manfred Sommer ist Professor für praktische Informatik in Marburg. Studium in Göttingen und München von 1964 bis 1969, dann Assistent am ersten Informatik-Institut in Deutschland an der TU München. Es folgten zehn Jahre bei Siemens in München. Seit 1984 erster InformatikProfessor in Marburg. Gründung und Aufbau des Fachgebiets Informatik in Marburg mit einem eigenständigen Hauptfachstudiengang Informatik. Prof. Sommer hält derzeit Vorlesungen über Praktische Informatik, Grafikprogrammierung, Multimediakommunikation, Programmieren in C++ und Compilerbau.

Bibliografische Information der Deutschen Nationalbibliothek Die Deutsche Nationalbibliothek verzeichnet diese Publikation in der Deutschen Nationalbibliografie; detaillierte bibliografische Daten sind im Internet über abrufbar.

© 2009 Oldenbourg Wissenschaftsverlag GmbH Rosenheimer Straße 145, D -81671 München Telefon: (089) 4 50 51- 0 oldenbourg.de Das Werk einschließlich aller Abbildungen ist urheberrechtlich geschützt. Jede Verwertung 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. Lektorat: Dr. Margit Roth Herstellung: Anna Grosser Coverentwurf: Kochan & Partner, München Gedruckt auf säure- und chlorfreiem Papier Gesamtherstellung: Kösel, Krugzell ISBN 978-3-486-58724-1

Inhalt

V

Vorwort zur achten Auflage Ursprünglich hatten wir für diese achte Auflage nur leichte Aktualisierungen und Verbesserungen geplant, aber am Ende sind doch eine Reihe interessanter Neuerungen entstanden. Im Kapitel über die Grundlagen der Programmierung gehen wir jetzt auch näher auf deklarative Sprachen ein. Wir besprechen zu diesem Zweck Prolog, als klassische logische Sprache, und Erlang, als in der Telekommunikationsindustrie etablierte funktionale Sprache. Kernelemente dieser Sprachen wie Pattern Matching und Funktionale Argumente (Closures) finden zunehmend Eingang in klassische imperative Sprachen. Was Java angeht, so haben wir bereits in der letzten Auflage generische Klassen erläutert. Wir vertiefen hier die Diskussion und gehen insbesondere auf die nicht ganz einfache Thematik der Vererbung generischer Klassen und die damit zusammenhängenden Konzepte von Kovarianz und Kontravarianz ein. Auch in der Java-Community wird der Ruf nach der Einführung von Closures lauter und es wird erwartet, dass diese Bestandteil der für Anfang 2009 angekündigten Version „Java 7“ werden. Wir erläutern das Closure-Konzept anhand von Anwendungsszenarien und diskutieren Beispielprogramme, die wir mit dem bereits im Herbst 2008 kursierenden CompilerPrototypen testen. Unsere Algorithmen und Datenstrukturen in Kapitel 4 setzen von nun an konsequent auf generische Klassen. Anhand eines abstrakten Konzeptes von Container-Datentypen, wie sie etwa durch das Java Interface Iterable definiert werden, zeigen wir, wie sich spielerisch elegant konkrete Implementierungen durch verkettete Listen ableiten lassen. Das Kapitel über Rechnerarchitektur wurde vollständig überarbeitet und enthält zum ersten Mal auch eine Einführung in das faszinierende Gebiet des CMOS-Design. Auf dieser Basis lassen sich Eigenschaften von Bauteilen und Unterschiede von Speichertechnologien besser verstehen als in der rein booleschen Logik. Auch das Kapitel über Betriebssysteme wurde aktualisiert, insbesondere haben wir die Darstellung der Prozess- und Speicherverwaltung (Segmentierung und Paging) verbessert. Für die Darstellung des Photon-Mapping im Kapitel über Graphikprogrammierung danken wir unserem Kollegen Prof. Michale Guthe. Unser ganz besonderer Dank gilt Herrn Prof. Karl Stroetmann für seine kritische und überaus sorgfältige Durchsicht des gesamten Buches und seine zahlreichen Anregungen. Dieses Buchprojekt verdankt seinen dauerhaften Erfolg nicht zuletzt der kontinuierlichen Unterstützung durch den Verlag. Dafür danken wir insbesondere der Lektorin Frau Dr. Margit

VI

Inhalt Vorwort

Roth und Herrn Dr. Jäger. Der neue Einband macht das Buch trotz seines gewaltigen Umfangs stabil und sehr gut handhabbar. Nicht zuletzt möchten wir uns bei unseren Lesern für die kontinuierlichen Anregungen, Fragen, Fehlermeldungen und Verbesserungswünsche bedanken. Wir freuen uns weiterhin auf Ihre Zuschriften an die Adressen [email protected] [email protected] Von der Homepage dieses Buches, www.informatikbuch.de, können alle Programme, weitere Software und zusätzliche Materialien zu diesem Buch heruntergeladen werden. Marburg an der Lahn, im September 2008 Heinz-Peter Gumm Manfred Sommer

Inhalt Vorwort zur achten Auflage

V

1 Einführung 1.1 Was ist „Informatik“? ............................................................................

1 1

1.1.1 1.1.2 1.1.3 1.1.4

1.2

1.3

1.4

1.5

Technische Informatik ............................................................................ Praktische Informatik ............................................................................. Theoretische Informatik ......................................................................... Angewandte Informatik .........................................................................

Information und Daten ...........................................................................

1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.2.7

Bits ......................................................................................................... Bitfolgen ................................................................................................. Hexziffern .............................................................................................. Bytes und Worte ..................................................................................... Dateien ................................................................................................... Datei- und Speichergrößen ..................................................................... Längen- und Zeiteinheiten .....................................................................

1 2 2 3

4

5 6 7 8 8 9 10

Informationsdarstellung ......................................................................... 11

1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8

Text ......................................................................................................... ASCII-Code ........................................................................................... ASCII-Erweiterungen ............................................................................ Unicode, UCS und UTF-8 ..................................................................... Zeichenketten ......................................................................................... Logische Werte und logische Verknüpfungen ........................................ Programme ............................................................................................. Bilder und Musikstücke .........................................................................

11 11 12 13 15 15 16 16

Zahlendarstellungen ............................................................................... 17

1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.4.6

Binärdarstellung ..................................................................................... Das Oktalsystem und das Hexadezimalsystem ...................................... Umwandlung in das Binär-, Oktal- oder Hexadezimalsystem ............... Arithmetische Operationen .................................................................... Darstellung ganzer Zahlen ..................................................................... Die Zweierkomplementdarstellung ........................................................

17 18 19 21 22 23

Standardformate für ganze Zahlen ......................................................... 25

1.5.1 1.5.2 1.5.3 1.5.4

Gleitpunktzahlen: Reelle Zahlen ............................................................ Real-Zahlenbereiche in Programmiersprachen ...................................... Daten – Informationen ........................................................................... Informationsverarbeitung – Datenverarbeitung .....................................

26 29 30 31

VIII

1.6

1.7

1.8

Inhalt

Hardware ................................................................................................ 31

1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 1.6.7 1.6.8 1.6.9 1.6.10 1.6.11 1.6.12 1.6.13 1.6.14 1.6.15

PCs, Workstations, Mainframes, Super-Computer ................................ Aufbau von Computersystemen ............................................................. Der Rechner von außen .......................................................................... Das Innenleben ...................................................................................... Ein Motherboard .................................................................................... Die Aufgabe der CPU ............................................................................ Die Organisation des Hauptspeichers .................................................... Speichermedien ...................................................................................... Magnetplatten ........................................................................................ Festplattenlaufwerke .............................................................................. Optische Laufwerke ............................................................................... Flash-Speicher ....................................................................................... Vergleich von Speichermedien .............................................................. Bildschirme ............................................................................................ Text- und Grafikmodus ..........................................................................

31 33 34 34 38 40 42 45 46 47 50 51 52 53 54

Von der Hardware zum Betriebssystem ................................................. 54

1.7.1 1.7.2 1.7.3 1.7.4 1.7.5 1.7.6 1.7.7

Schnittstellen und Treiber ...................................................................... BIOS ...................................................................................................... Die Aufgaben des Betriebssystems ........................................................ Prozess- und Speicherverwaltung .......................................................... Dateiverwaltung ..................................................................................... DOS, Windows und Linux ..................................................................... Bediensysteme .......................................................................................

56 58 59 59 59 62 63

Anwendungsprogramme ........................................................................ 66

1.8.1 1.8.2 1.8.3 1.8.4 1.8.5 1.8.6 1.8.7

Textverarbeitung .................................................................................... Zeichen und Schriftarten ........................................................................ Formatierung .......................................................................................... Desktop Publishing ................................................................................ Textbeschreibungssprachen ................................................................... Tabellenkalkulation: spread sheets ........................................................ Vom Fenster zur Welt zur zweiten Welt .................................................

66 66 67 69 69 73 75

2 Grundlagen der Programmierung 77 2.1 Programmiersprachen ............................................................................ 78 2.1.1 2.1.2 2.1.3 2.1.4 2.1.5 2.1.6 2.1.7

Vom Programm zur Maschine ............................................................... Virtuelle Maschinen ............................................................................... Interpreter ............................................................................................... Programmieren und Testen .................................................................... Programmierumgebungen ...................................................................... Pascal ..................................................................................................... Java ........................................................................................................

78 79 81 81 82 83 84

Inhalt

2.2

2.3

2.4

2.5

2.6

IX

Spezifikationen, Algorithmen, Programme ........................................... 84

2.2.1 2.2.2 2.2.3 2.2.4 2.2.5 2.2.6 2.2.7 2.2.8

Spezifikationen ....................................................................................... Algorithmen ........................................................................................... Algorithmen als Lösung von Spezifikationen ........................................ Terminierung .......................................................................................... Elementare Aktionen .............................................................................. Zuweisungen .......................................................................................... Vom Algorithmus zum Programm ......................................................... Ressourcen .............................................................................................

85 87 91 92 93 93 94 96

Daten und Datenstrukturen .................................................................... 98

2.3.1 2.3.2 2.3.3 2.3.4 2.3.5 2.3.6 2.3.7 2.3.8 2.3.9 2.3.10 2.3.11 2.3.12

Der Begriff der Datenstruktur ................................................................ Boolesche Werte ..................................................................................... Zahlen ..................................................................................................... Natürliche Zahlen ................................................................................... Der Datentyp Integer .............................................................................. Rationale Zahlen .................................................................................... Reelle Zahlen ......................................................................................... Mehrsortige Datenstrukturen ................................................................. Zeichen ................................................................................................... Zusammengesetzte Datentypen – Strings .............................................. Benutzerdefinierte Datenstrukturen ....................................................... Informationsverarbeitung und Datenverarbeitung .................................

98 99 101 101 103 105 105 106 108 110 111 113

Speicher, Variablen und Ausdrücke ....................................................... 114

2.4.1 2.4.2 2.4.3 2.4.4 2.4.5 2.4.6 2.4.7 2.4.8

Deklarationen ......................................................................................... Initialisierung ......................................................................................... Kontexte ................................................................................................. Ausdrücke, Terme .................................................................................. Auswertung von Ausdrücken ................................................................. Funktionsdefinitionen ............................................................................ Typfehler ................................................................................................ Seiteneffekte ...........................................................................................

115 116 116 117 120 121 123 123

Der Kern imperativer Sprachen ............................................................. 124

2.5.1 2.5.2 2.5.3 2.5.4 2.5.5 2.5.6 2.5.7 2.5.8

Zuweisungen .......................................................................................... Kontrollstrukturen .................................................................................. Drei Kontrollstrukturen genügen ........................................................... Die sequentielle Komposition ................................................................ Die Alternativanweisung ........................................................................ Die while-Schleife .................................................................................. Unterprogramme .................................................................................... Lauffähige Programme ...........................................................................

124 126 126 127 128 129 130 132

Formale Beschreibung von Programmiersprachen ................................ 133

2.6.1 2.6.2 2.6.3

Lexikalische Regeln ............................................................................... 133 Syntaktische Regeln ............................................................................... 134 Semantische Regeln ............................................................................... 137

X

Inhalt

2.7

2.8

2.9

Erweiterung der Kernsprache ................................................................ 137

2.7.1 2.7.2 2.7.3 2.7.4 2.7.5 2.7.6

Bedingte Anweisung .............................................................................. Fallunterscheidung ................................................................................. do-Schleife ............................................................................................. Allgemeinere Schleifenkonstrukte ......................................................... Die for-Schleife ...................................................................................... Arrays – indizierte Variablen .................................................................

138 139 140 142 142 144

Rekursive Funktionen und Prozeduren .................................................. 145

2.8.1 2.8.2 2.8.3 2.8.4 2.8.5 2.8.6 2.8.7 2.8.8 2.8.9

Rekursive Programme ............................................................................ Die Türme von Hanoi ............................................................................ Spielstrategien als rekursive Prädikate – Backtracking ......................... Wechselseitige Rekursion ...................................................................... Induktion – Rekursion ........................................................................... Allgemeine Rekursion ........................................................................... Endrekursion .......................................................................................... Lineare Rekursion .................................................................................. Eine Programmtransformation ...............................................................

147 148 149 151 151 152 153 155 157

Typen, Module, Klassen und Objekte .................................................... 158

2.9.1 2.9.2 2.9.3 2.9.4 2.9.5 2.9.6 2.9.7 2.9.8 2.9.9 2.9.10

Strukturiertes Programmieren ................................................................ Blockstrukturierung ............................................................................... Strukturierung der Daten ........................................................................ Objektorientierte Konstruktion neuer Datentypen ................................. Modulares Programmieren .................................................................... Schnittstellen – Interfaces ...................................................................... Objektorientiertes Programmieren ......................................................... Vererbung ............................................................................................... Summentypen in objektorientierten Sprachen ....................................... Datenkapselung ......................................................................................

159 160 160 165 167 169 171 173 175 177

2.10 Verifikation ............................................................................................ 179 2.10.1 2.10.2 2.10.3 2.10.4 2.10.5 2.10.6 2.10.7 2.10.8 2.10.9 2.10.10 2.10.11 2.10.12 2.10.13 2.10.14

Vermeidung von Fehlern ........................................................................ Zwischenbehauptungen ......................................................................... Partielle Korrektheit ............................................................................... Zerlegung durch Zwischenbehauptungen .............................................. Zuweisungsregel .................................................................................... Rückwärtsbeweis ................................................................................... if-else-Regel ........................................................................................... Abschwächungsregel und einarmige Alternative .................................. Invarianten und while-Regel .................................................................. Starke und schwache Invarianten ........................................................... Programm-Verifizierer ........................................................................... do-Schleife ............................................................................................. Terminierung .......................................................................................... Beweis eines Programmschemas ...........................................................

180 180 181 182 184 185 187 188 189 191 193 195 196 196

Inhalt

XI

2.11 Deklarative Sprachen ............................................................................. 197

2.11.1 Prolog ..................................................................................................... 198 2.11.2 Erlang ..................................................................................................... 202

2.12 Zusammenfassung ................................................................................. 206 3 Die Programmiersprache Java 207 3.1 Die lexikalischen Elemente von Java .................................................... 209 3.1.1 3.1.2 3.1.3 3.1.4

3.2

3.3

3.4

Kommentare ........................................................................................... Bezeichner .............................................................................................. Schlüsselwörter ...................................................................................... Literale ...................................................................................................

209 210 211 211

Datentypen und Methoden ..................................................................... 213

3.2.1 3.2.2 3.2.3 3.2.4 3.2.5 3.2.6 3.2.7 3.2.8 3.2.9 3.2.10 3.2.11

Variablen ................................................................................................ Referenz-Datentypen .............................................................................. Arrays ..................................................................................................... Methoden ............................................................................................... Klassen und Instanzen ............................................................................ Objekte und Referenzen ......................................................................... Objekt- und Klassenkomponenten ......................................................... Attribute ................................................................................................. Überladung ............................................................................................. Konstruktoren ......................................................................................... Aufzählungstypen ..................................................................................

213 214 215 216 218 220 221 222 223 224 225

Ausführbare Java-Programme ............................................................... 226

3.3.1 3.3.2 3.3.3 3.3.4

Java-Dateien – Übersetzungseinheiten .................................................. Programme ............................................................................................. Packages ................................................................................................. Standard-Packages .................................................................................

228 228 229 231

Ausdrücke und Anweisungen ................................................................ 232

3.4.1 3.4.2 3.4.3 3.4.4 3.4.5 3.4.6 3.4.7 3.4.8 3.4.9 3.4.10 3.4.11 3.4.12 3.4.13 3.4.14 3.4.15

Arithmetische Operationen .................................................................... Vergleichsoperationen ............................................................................ Boolesche Operationen .......................................................................... Bitweise Operationen ............................................................................. Zuweisungsausdrücke ............................................................................ Anweisungsausdrücke ............................................................................ Sonstige Operationen ............................................................................. Präzedenz der Operatoren ...................................................................... Einfache Anweisungen ........................................................................... Blöcke .................................................................................................... Alternativ-Anweisungen ........................................................................ switch-Anweisung .................................................................................. Schleifen ................................................................................................. Die for-Anweisung ................................................................................. break- und continue-Anweisungen ........................................................

232 233 234 234 234 236 236 237 238 239 239 240 241 242 244

XII

Inhalt

3.5

3.6 3.7

3.8

3.9

Klassen und Objekte .............................................................................. 244

3.5.1 3.5.2 3.5.3 3.5.4 3.5.5 3.5.6 3.5.7 3.5.8 3.5.9 3.5.10 3.5.11 3.5.12

Vererbung ............................................................................................... Späte Bindung (Late Binding) ............................................................... Finale Komponenten .............................................................................. Zugriffsrechte von Feldern und Methoden ............................................ Attribute von Klassen ............................................................................ Abstrakte Klassen .................................................................................. Rekursiv definierte Klassen ................................................................... Schnittstellen (Interfaces) ...................................................................... Wrapper-Klassen .................................................................................... Generische Klassen ................................................................................ Typschranken ......................................................................................... Vererbung generischer Typen ................................................................

246 251 252 252 253 253 255 257 261 261 262 263

Fehler und Ausnahmen .......................................................................... 263

3.6.1 3.6.2

Exceptions in Java ................................................................................. 264 Zusicherungen – Assertions ................................................................... 267

Dateien: Ein- und Ausgabe .................................................................... 271

3.7.1 3.7.2 3.7.3 3.7.4

Dateidialog ............................................................................................. Schreiben einer Datei ............................................................................. Lesen einer Datei ................................................................................... Testen von Dateieigenschaften ..............................................................

272 272 273 274

Threads .................................................................................................. 275

3.8.1 3.8.2 3.8.3 3.8.4

Thread-Erzeugung ................................................................................. Kontrolle der Threads ............................................................................ Thread-Synchronisation ......................................................................... Deadlock ................................................................................................

275 277 277 279

Grafische Benutzeroberflächen mit Java (AWT) ................................... 281

3.9.1 3.9.2 3.9.3 3.9.4 3.9.5 3.9.6 3.9.7 3.9.8

Ein erstes Fenster ................................................................................... Ereignisse ............................................................................................... Beispiel für eine Ereignisbehandlung .................................................... Buttons ................................................................................................... Grafikausgabe in Fenstern ..................................................................... Maus-Ereignisse ..................................................................................... Paint ....................................................................................................... Weitere Bedienelemente von Programmen und Fenstern ......................

281 282 284 285 286 287 291 292

3.10 Ausblick: Java 7 und dann ... ................................................................. 292

3.10.1 Closures ................................................................................................. 292 3.10.2 Ausblick ................................................................................................. 297

4 Algorithmen und Datenstrukturen 299 4.1 Suchalgorithmen .................................................................................... 301 4.1.1 4.1.2

Lineare Suche ........................................................................................ 301 Exkurs: Runden, Logarithmen und Stellenzahl ..................................... 303

Inhalt

XIII 4.1.3 4.1.4 4.1.5

4.2

4.3

4.4 4.5

4.6

4.7

Binäre Suche .......................................................................................... 304 Lineare Suche vs. binäre Suche ............................................................. 305 Komplexität von Algorithmen ............................................................... 306

Einfache Sortierverfahren ...................................................................... 309

4.2.1 4.2.2 4.2.3 4.2.4 4.2.5 4.2.6 4.2.7

Datensätze und Schlüssel ....................................................................... Invarianten und Assertions ..................................................................... BubbleSort ............................................................................................. SelectionSort .......................................................................................... InsertionSort ........................................................................................... Laufzeitvergleiche der einfachen Sortieralgorithmen ............................ ShellSort und CombSort ........................................................................

309 312 314 316 318 320 321

Schnelle Sortieralgorithmen .................................................................. 322

4.3.1 4.3.2 4.3.3 4.3.4 4.3.5 4.3.6 4.3.7 4.3.8 4.3.9 4.3.10 4.3.11 4.3.12 4.3.13

Divide and Conquer – teile und herrsche ............................................... QuickSort ............................................................................................... Die Partitionierung ................................................................................. Korrektheit von QuickSort .................................................................... Komplexität von QuickSort ................................................................... MergeSort ............................................................................................... Stabilität und RadixSort ......................................................................... Optimalität von Sortieralgorithmen ....................................................... Distribution Sort ..................................................................................... Wieso und wie gut funktioniert DistributionSort? ................................. Implementierung von DistributionSort .................................................. Laufzeit der schnellen Sortieralgorithmen ............................................. Externes Sortieren ..................................................................................

322 323 324 326 326 327 329 330 330 332 332 334 336

Abstrakte Datenstrukturen ..................................................................... 337

4.4.1 4.4.2

Datenstruktur = Menge + Operationen .................................................. 337 Die axiomatische Methode ..................................................................... 338

Stacks ..................................................................................................... 339

4.5.1 4.5.2 4.5.3 4.5.4 4.5.5 4.5.6

Stackoperationen .................................................................................... Implementierung durch ein Array .......................................................... Implementierung durch eine Liste ......................................................... Auswertung von Postfix-Ausdrücken .................................................... Entrekursivierung ................................................................................... Stackpaare ..............................................................................................

339 341 342 344 344 345

Queues, Puffer, Warteschlangen ............................................................ 347

4.6.1 4.6.2 4.6.3 4.6.4

Implementierung durch ein „zirkuläres“ Array ...................................... Implementierung durch eine zirkuläre Liste .......................................... DeQues: Queues mit zwei gleichberechtigten Enden ............................ Anwendung von Puffern ........................................................................

347 349 349 350

Container Datentypen ............................................................................ 351

4.7.1 4.7.2

Listen ...................................................................................................... 353 Einfach verkettete Listen ........................................................................ 355

XIV

Inhalt 4.7.3 4.7.4 4.7.5 4.7.6 4.7.7

4.8

4.9

Listen als Verallgemeinerung von Stacks und Queues .......................... Array-Listen ........................................................................................... Doppelt verkettete Listen ....................................................................... Geordnete Listen und Skip-Listen ......................................................... Adaptive Listen ......................................................................................

359 360 361 361 362

Bäume .................................................................................................... 363

4.8.1 4.8.2 4.8.3 4.8.4 4.8.5 4.8.6 4.8.7 4.8.8 4.8.9 4.8.10 4.8.11 4.8.12 4.8.13 4.8.14 4.8.15 4.8.16

Beispiele von Bäumen ........................................................................... Binärbäume ............................................................................................ Implementierung von Binärbäumen ...................................................... Traversierungen ..................................................................................... Kenngrößen von Binärbäumen .............................................................. Binäre Suchbäume ................................................................................. Implementierung von binären Suchbäumen .......................................... Balancierte Bäume ................................................................................. AVL-Bäume ........................................................................................... 2-3-4-Bäume .......................................................................................... B-Bäume ................................................................................................ Vollständige Bäume ............................................................................... Heaps ..................................................................................................... HeapSort ................................................................................................ Priority-Queues ...................................................................................... Bäume mit variabler Anzahl von Teilbäumen .......................................

364 365 366 367 371 372 372 379 380 382 383 384 386 389 390 390

Graphen .................................................................................................. 391

4.9.1 4.9.2 4.9.3 4.9.4 4.9.5 4.9.6 4.9.7 4.9.8 4.9.9

Wege und Zusammenhang ..................................................................... Repräsentationen von Graphen .............................................................. Traversierungen ..................................................................................... Tiefensuche und Backtracking ............................................................... Breitensuche ........................................................................................... Transitive Hülle ..................................................................................... Kürzeste Wege ....................................................................................... Schwere Probleme für Handlungsreisende ............................................ Eine Implementierung des TSP .............................................................

392 393 395 396 397 398 399 401 403

4.10 Zeichenketten ......................................................................................... 407 4.10.1 4.10.2 4.10.3 4.10.4 4.10.5

Array-Implementierung ......................................................................... Nullterminierte Strings ........................................................................... Stringoperationen ................................................................................... Suchen in Zeichenketten ........................................................................ Der Boyer-Moore-Algorithmus .............................................................

407 407 408 408 409

5 Rechnerarchitektur 411 5.1 Vom Transistor zum Chip ...................................................................... 411 5.1.1 5.1.2 5.1.3

Chips ...................................................................................................... 413 Chipherstellung ...................................................................................... 414 Kleinste Chip-Strukturen ....................................................................... 415

Inhalt

XV 5.1.4 5.1.5 5.1.6 5.1.7 5.1.8

5.2

5.3

5.4

5.5

Chipfläche und Anzahl der Transistoren ................................................ Weitere Chip-Parameter ......................................................................... Speicherbausteine ................................................................................... Logikbausteine ....................................................................................... Schaltungsentwurf ..................................................................................

415 416 416 417 418

Boolesche Algebra ................................................................................. 419

5.2.1 5.2.2 5.2.3 5.2.4 5.2.5 5.2.6 5.2.7 5.2.8 5.2.9 5.2.10 5.2.11 5.2.12 5.2.13 5.2.14 5.2.15 5.2.16

Serien-parallele Schaltungen .................................................................. Serien-parallele Schaltglieder ................................................................ Schaltoperationen ................................................................................... Boolesche Terme .................................................................................... Schaltfunktionen .................................................................................... Gleichungen ........................................................................................... Dualität ................................................................................................... SP-Schaltungen sind monoton ............................................................... Negation ................................................................................................. Boolesche Terme ................................................................................... Dualitätsprinzip ...................................................................................... Realisierung von Schaltfunktionen ........................................................ Konjunktive Normalform ....................................................................... Algebraische Umwandlung in DNF oder KNF ...................................... Aussagenlogik ........................................................................................ Mengenalgebra .......................................................................................

419 420 421 421 422 422 423 424 424 425 426 426 427 428 429 430

Digitale Logik ........................................................................................ 430

5.3.1 5.3.2 5.3.3 5.3.4 5.3.5 5.3.6 5.3.7 5.3.8 5.3.9

Logikgatter ............................................................................................. Entwurf und Vereinfachung boolescher Schaltungen ............................ KV-Diagramme ...................................................................................... Spezielle Schaltglieder ........................................................................... Gatter mit mehreren Ausgängen ............................................................ Codierer und Decodierer ........................................................................ Addierer .................................................................................................. Logik-Gitter ........................................................................................... Programmierbare Gitterbausteine ..........................................................

430 433 433 435 436 437 438 439 441

CMOS Schaltungen und VLSI Design .................................................. 442

5.4.1 5.4.2 5.4.3 5.4.4

Logikgatter in CMOS-Technik .............................................................. CMOS-Entwurf ...................................................................................... Entwurf von CMOS Chips ..................................................................... VLSI-Werkzeuge ....................................................................................

443 445 446 447

Sequentielle Logik ................................................................................. 448

5.5.1 5.5.2 5.5.3 5.5.4 5.5.5 5.5.6

Gatterlaufzeiten ...................................................................................... Rückgekoppelte Schaltungen ................................................................. Einfache Anwendungen von Flip-Flops ................................................. Technische Schwierigkeiten ................................................................... Synchrone und asynchrone Schaltungen ................................................ Getaktete Flip-Flops ...............................................................................

449 450 452 453 454 455

XVI

Inhalt 5.5.7 5.5.8 5.5.9 5.5.10 5.5.11 5.5.12 5.5.13 5.5.14 5.5.15

5.6

5.7

Zustandsautomaten ................................................................................ Entwurf sequentieller Schaltungen ........................................................ Eine Fußgängerampel ............................................................................ Die Konstruktion der Hardwarekomponenten ....................................... Tristate Puffer ........................................................................................ Speicherzellen ........................................................................................ MOS-Implementierung von Speicherzellen .......................................... Register .................................................................................................. Die Arithmetisch-Logische Einheit .......................................................

456 457 458 459 460 461 462 464 466

Von den Schaltgliedern zur CPU ........................................................... 470

5.6.1 5.6.2 5.6.3 5.6.4 5.6.5 5.6.6 5.6.7 5.6.8 5.6.9 5.6.10 5.6.11

Busse ...................................................................................................... Mikrocodegesteuerte Operationen ......................................................... Der Zugang zum Hauptspeicher ............................................................ Der Mikrobefehlsspeicher – das ROM .................................................. Sprünge .................................................................................................. Berechnete Sprünge ............................................................................... Der Adressrechner ................................................................................. Ein Mikroprogramm .............................................................................. Maschinenbefehle .................................................................................. Der Maschinenspracheinterpretierer ...................................................... Argumente ..............................................................................................

471 472 475 477 478 479 480 481 482 484 486

Assemblerprogrammierung ................................................................... 486

5.7.1 5.7.2 5.7.3 5.7.4 5.7.5 5.7.6 5.7.7 5.7.8 5.7.9 5.7.10 5.7.11 5.7.12 5.7.13 5.7.14 5.7.15 5.7.16 5.7.17 5.7.18 5.7.19 5.7.20 5.7.21 5.7.22 5.7.23

Maschinensprache und Assembler ......................................................... Register der 80x86-Familie ................................................................... Allzweckregister und Spezialregister .................................................... Flag-Register .......................................................................................... Arithmetische Flags ............................................................................... Größenvergleiche ................................................................................... Logische Operationen ............................................................................ Sprünge .................................................................................................. Struktur eines vollständigen Assemblerprogrammes ............................. Ein Beispielprogramm ........................................................................... Testen von Assemblerprogrammen ........................................................ Speicheradressierung ............................................................................. Operationen auf Speicherblöcken .......................................................... Multiplikation und Division ................................................................... Shift-Operationen ................................................................................... LOOP-Befehle ....................................................................................... Der Stack ................................................................................................ Einfache Unterprogramme ..................................................................... Parameterübergabe und Stack ................................................................ Prozeduren und Funktionen ................................................................... Makros ................................................................................................... Assembler unter DOS ............................................................................ Assembler unter Windows .....................................................................

487 488 489 490 491 493 494 495 497 498 499 501 502 503 504 505 506 507 508 510 510 511 513

Inhalt

5.8

5.9

XVII

RISC-Architekturen ............................................................................... 514

5.8.1 5.8.2 5.8.3 5.8.4 5.8.5 5.8.6 5.8.7 5.8.8

CISC ....................................................................................................... Von CISC zu RISC ................................................................................. RISC-Prozessoren .................................................................................. Pipelining ............................................................................................... Superskalare Architekturen .................................................................... Cache-Speicher ...................................................................................... Leistungsvergleich ................................................................................. Konkrete RISC-Architekturen ...............................................................

515 516 516 518 519 519 519 520

Architektur der Intel-PC-Mikroprozessorfamilie .................................. 523

5.9.1 5.9.2 5.9.3 5.9.4 5.9.5 5.9.6 5.9.7

Adressierung .......................................................................................... Die Segmentierungseinheit .................................................................... Adressübersetzung ................................................................................. Datenstrukturen und Befehle des Pentium ............................................. MMX-Befehle ........................................................................................ Betriebsarten des Pentium ...................................................................... Ausblick .................................................................................................

527 527 529 530 530 530 531

6 Betriebssysteme 533 6.1 Basis-Software ....................................................................................... 534 6.2 Betriebsarten .......................................................................................... 536 6.2.1 6.2.2

6.3

6.4

Teilhaberbetrieb ...................................................................................... 536 Client-Server-Systeme ........................................................................... 536

Verwaltung der Ressourcen ................................................................... 538

6.3.1 6.3.2 6.3.3 6.3.4 6.3.5 6.3.6 6.3.7 6.3.8 6.3.9 6.3.10 6.3.11 6.3.12

Dateisystem ............................................................................................ Dateioperationen .................................................................................... Prozesse und Threads ............................................................................. Vom Programm zum Prozess ................................................................. Prozessverwaltung .................................................................................. Prozesskommunikation .......................................................................... Kritische Abschnitte – wechselseitiger Ausschluss ............................... Semaphore und Monitore ....................................................................... Deadlocks ............................................................................................... Speicherverwaltung ................................................................................ Paging ..................................................................................................... Page faults ..............................................................................................

539 540 540 541 542 544 545 547 549 550 551 554

Das Betriebssystem UNIX ..................................................................... 555

6.4.1 6.4.2 6.4.3 6.4.4 6.4.5 6.4.6

Linux ...................................................................................................... Das UNIX-Dateisystem ......................................................................... Dateinamen ............................................................................................ Dateirechte ............................................................................................. Namen und Pfade ................................................................................... Special files ............................................................................................

555 555 557 557 558 560

XVIII

Inhalt 6.4.7 6.4.8 6.4.9 6.4.10 6.4.11 6.4.12 6.4.13 6.4.14

6.5

6.6 6.7

6.8

Externe Dateisysteme ............................................................................ UNIX-Shells .......................................................................................... UNIX-Kommandos ................................................................................ Optionen ................................................................................................. Datei-Muster .......................................................................................... Standard-Input/Standard-Output ............................................................ Dateibearbeitung .................................................................................... Reguläre Ausdrücke ...............................................................................

560 560 561 562 562 563 564 565

UNIX-Prozesse ...................................................................................... 566

6.5.1 6.5.2 6.5.3 6.5.4 6.5.5 6.5.6 6.5.7 6.5.8 6.5.9 6.5.10 6.5.11 6.5.12 6.5.13 6.5.14 6.5.15 6.5.16 6.5.17

Pipes ....................................................................................................... Sind Pipes notwendig? ........................................................................... Prozess-Steuerung .................................................................................. Multitasking ........................................................................................... UNIX-Shell-Programmierung ............................................................... Die C-Shell ............................................................................................ Kommando-Verknüpfungen ................................................................... Variablen ................................................................................................ Shell-Scripts ........................................................................................... Ausführung von Shell-Scripts ................................................................ UNIX-Kommandos und Shell-Kommandos .......................................... UNIX als Mehrbenutzersystem ............................................................. UNIX-Tools ........................................................................................... Editoren .................................................................................................. C und C++ .............................................................................................. Scanner- und Parsergeneratoren ............................................................. Projektbearbeitung .................................................................................

566 568 570 572 573 574 574 575 576 577 577 578 579 579 581 582 583

X Window System ................................................................................. 584

6.6.1 6.6.2

Window-Manager und Terminal Emulator ............................................ 585 Grafische Oberflächen ........................................................................... 586

MS-DOS und MS-Windows .................................................................. 587

6.7.1 6.7.2 6.7.3 6.7.4 6.7.5

Dynamic Link Libraries ......................................................................... Object Linking and Embedding ............................................................. Windows NT, Windows 2000 ................................................................ Windows XP .......................................................................................... Windows Vista .......................................................................................

588 588 589 590 591

Alternative PC-Betriebssysteme ............................................................ 592

7 Rechnernetze 595 7.1 Rechner-Verbindungen .......................................................................... 596 7.1.1 7.1.2 7.1.3 7.1.4

Signalübertragung .................................................................................. Physikalische Verbindung ...................................................................... Synchronisation ..................................................................................... Bitcodierungen .......................................................................................

596 598 600 601

Inhalt

7.2

7.3

7.4

7.5

XIX

Datenübertragung mit Telefonleitungen ................................................ 602

7.2.1 7.2.2 7.2.3

ISDN ...................................................................................................... 603 DSL, ADSL und T-DSL ......................................................................... 604 ADSL2+ ................................................................................................. 606

Protokolle und Netze ............................................................................. 606

7.3.1 7.3.2 7.3.3 7.3.4 7.3.5 7.3.6

Das OSI-Modell ..................................................................................... Netze ...................................................................................................... Netztopologien ....................................................................................... Netze von Netzen ................................................................................... Zugriffsverfahren ................................................................................... Wettkampfverfahren: CSMA-CD ..........................................................

Netztechnologien ................................................................................... 617

7.4.1 7.4.2 7.4.3 7.4.4

Ethernet .................................................................................................. FDDI ...................................................................................................... ATM ....................................................................................................... SONET/SDH ..........................................................................................

8.0.1

8.2 8.3 8.4 8.5

8.6

617 617 618 619

Drahtlose Netze ...................................................................................... 622

7.5.1 7.5.2

Bluetooth ................................................................................................ 622 WLAN .................................................................................................... 623

8 Das Internet 8.1

607 609 610 612 615 615

629

Bildung von Standards im Internet ........................................................ 630

Die TCP/IP Protokolle ........................................................................... 632

8.1.1 8.1.2

Die Protokolle TCP und UDP ................................................................ 633 Das IP Protokoll ..................................................................................... 635

IP-Adressen ............................................................................................ 637

8.2.1 8.2.2

Adressklassen ......................................................................................... 638 Adressübersetzung ................................................................................. 640

Das System der Domain-Namen ............................................................ 643

8.3.1 8.3.2

DNS-lookup in Java ............................................................................... 646 Programmierung einer TCP-Verbindung ............................................... 648

Intranets, Firewalls und virtuelle private Netzwerke ............................. 652 Die Dienste im Internet .......................................................................... 654

8.5.1 8.5.2 8.5.3 8.5.4 8.5.5

E-Mail .................................................................................................... News ....................................................................................................... FTP ......................................................................................................... Secure Shell ............................................................................................ Gopher ....................................................................................................

654 659 660 661 661

Das World Wide Web ............................................................................. 662

8.6.1 8.6.2 8.6.3

HTTP ...................................................................................................... 664 HTML .................................................................................................... 665 Die Struktur eines HTML-Dokumentes ................................................. 668

XX

Inhalt 8.6.4 8.6.5 8.6.6 8.6.7 8.6.8

8.7

Querverweise: Links .............................................................................. Tabellen und Frames .............................................................................. Formulare ............................................................................................... Style Sheets ............................................................................................ Weitere Möglichkeiten von HTML .......................................................

669 670 672 673 674

Web-Programmierung ............................................................................ 674

8.7.1 8.7.2 8.7.3 8.7.4 8.7.5 8.7.6 8.7.7 8.7.8

JavaScript ............................................................................................... Applets ................................................................................................... Die Struktur eines Applets ..................................................................... Der Lebenszyklus eines Applets ............................................................ Interaktionen .......................................................................................... PHP ........................................................................................................ XML ....................................................................................................... DOM, Ajax und Web 2.0 .......................................................................

674 677 678 679 679 682 684 693

9 Theoretische Informatik und Compilerbau 695 9.1 Analyse von Programmtexten ................................................................ 695 9.1.1 9.1.2

9.2

9.3

9.4

Lexikalische Analyse ............................................................................. 696 Syntaxanalyse ........................................................................................ 697

Reguläre Sprachen ................................................................................. 698

9.2.1 9.2.2 9.2.3 9.2.4 9.2.5 9.2.6 9.2.7

Reguläre Ausdrücke ............................................................................... Automaten und ihre Sprachen ................................................................ Implementierung endlicher Automaten ................................................. ε-Transitionen und nichtdeterministische Automaten ........................... Automaten für reguläre Sprachen .......................................................... Von nichtdeterministischen zu deterministischen Automaten ............... Anwendung: flex ....................................................................................

699 701 703 704 704 705 706

Kontextfreie Sprachen ........................................................................... 707

9.3.1 9.3.2 9.3.3 9.3.4 9.3.5 9.3.6 9.3.7 9.3.8

Kontextfreie Grammatiken .................................................................... Ableitungen ............................................................................................ Stackautomaten (Kellerautomaten) ........................................................ Stackautomaten für beliebige kontextfreie Sprachen ............................ Nichtdeterministische Algorithmen und Backtracking .......................... Inhärent nichtdeterministische Sprachen ............................................... Ableitungsbaum, Syntaxbaum ............................................................... Abstrakte Syntaxbäume .........................................................................

708 709 710 712 712 715 715 716

Grundlagen des Compilerbaus ............................................................... 717

9.4.1 9.4.2 9.4.3 9.4.4 9.4.5 9.4.6

Parsen durch rekursiven Abstieg (recursive descent) ............................ LL(1)-Grammatiken ............................................................................... Äquivalente Grammatiken ..................................................................... Top-down und bottom-up ...................................................................... Shift-Reduce Parser ............................................................................... Die Arbeitsweise von Shift-Reduce-Parsern .........................................

718 719 721 723 724 725

Inhalt

XXI 9.4.7 9.4.8 9.4.9 9.4.10 9.4.11 9.4.12 9.4.13 9.4.14 9.4.15 9.4.16 9.4.17 9.4.18 9.4.19

9.5

9.6

Bottom-up Parsing ................................................................................. Konflikte ................................................................................................ Ein nichtdeterministischer Automat mit Stack ...................................... Übergang zum deterministischen Automaten ........................................ Präzedenz ............................................................................................... LR(1) und LALR(1) ............................................................................... Parsergeneratoren ................................................................................... lex/flex & yacc/bison ............................................................................. Grammatische Aktionen ......................................................................... Fehlererkennung ..................................................................................... Synthetisierte Werte ............................................................................... Symboltabellen ....................................................................................... Codeoptimierung ....................................................................................

726 727 727 730 732 733 734 736 737 739 739 740 741

Berechenbarkeit ..................................................................................... 742

9.5.1 9.5.2 9.5.3 9.5.4 9.5.5 9.5.6 9.5.7 9.5.8 9.5.9 9.5.10 9.5.11 9.5.12 9.5.13 9.5.14 9.5.15 9.5.16 9.5.17 9.5.18 9.5.19 9.5.20 9.5.21

Berechenbare Funktionen ....................................................................... Beispiele berechenbarer Funktionen ...................................................... Diagonalisierung .................................................................................... Nicht berechenbare Funktionen ............................................................. Algorithmenbegriff und Churchsche These ........................................... Turingmaschinen .................................................................................... Turing-Post Programme ......................................................................... Turing-berechenbare Funktionen ........................................................... Registermaschinen ................................................................................. GOTO-Programme ................................................................................. While-Programme .................................................................................. For-Programme (Loop-Programme) ...................................................... Effiziente Algorithmen als For-Programme ........................................... Elementare (primitive) Rekursion .......................................................... Allgemeine Rekursion (µ-Rekursion) .................................................... Die Ackermannfunktion ......................................................................... Berechenbare Funktionen – Churchsche These ..................................... Gödelisierung ......................................................................................... Aufzählbarkeit und Entscheidbarkeit ..................................................... Unlösbare Aufgaben ............................................................................... Semantische Probleme sind unentscheidbar ..........................................

742 743 745 746 746 747 750 751 751 752 753 755 755 757 758 759 760 761 762 762 763

Komplexitätstheorie ............................................................................... 765

9.6.1 9.6.2 9.6.3 9.6.4 9.6.5 9.6.6 9.6.7 9.6.8

Rückführung auf ja/nein-Probleme ........................................................ Entscheidungsprobleme und Sprachen .................................................. Maschinenmodelle und Komplexitätsmaße ........................................... Sprachen und ihre Komplexität .............................................................. Effiziente parallele Lösungen ................................................................ Nichtdeterminismus ............................................................................... Die Klasse NP ........................................................................................ Reduzierbarkeit ......................................................................................

766 766 767 768 768 770 771 772

XXII

Inhalt 9.6.9 9.6.10 9.6.11 9.6.12 9.6.13

Der Satz von Cook ................................................................................. NP-Vollständigkeit ................................................................................. CLIQUE ist NP-vollständig ................................................................... Praktische Anwendung von SAT-Problemen ......................................... P = NP ? .................................................................................................

774 776 776 777 780

10 Datenbanksysteme 781 10.1 Datenbanken und Datenbanksysteme .................................................... 781 10.2 Datenmodelle ......................................................................................... 783 10.2.1 10.2.2 10.2.3 10.2.4 10.2.5 10.2.6

Entity/Relationship-Modell ................................................................... Das Relationale Datenbankmodell ......................................................... Relationen .............................................................................................. Die relationale Algebra .......................................................................... Erweiterungen des relationalen Datenmodells ...................................... Abbildung eines E/R-Datenmodells in ein relationales Modell ............

783 785 786 787 788 788

10.3 Die Anfragesprache SQL ....................................................................... 789 10.3.1 10.3.2 10.3.3 10.3.4 10.3.5 10.3.6

Datendefinition ...................................................................................... Einfache Anfragen ................................................................................. Gruppierung und Aggregate .................................................................. Verknüpfung verschiedener Relationen ................................................. Einfügen, Ändern und Löschen von Datensätzen .................................. Mehrbenutzerbetrieb ..............................................................................

789 791 792 793 793 794

10.4 Anwendungsprogrammierung in Java ................................................... 796

10.4.1 Das SQL-Paket in Java .......................................................................... 797 10.4.2 Aufbau einer Verbindung ....................................................................... 798 10.4.3 Anfragen ................................................................................................ 798

10.5 Zusammenfassung ................................................................................. 800 11 Grafikprogrammierung 801 11.1 Hardware ................................................................................................ 801

11.1.1 Auflösungen ........................................................................................... 802 11.1.2 Farben .................................................................................................... 802

11.2 Grafikroutinen für Rastergrafik ............................................................. 803

11.2.1 Bresenham Algorithmus ........................................................................ 805

11.3 Einfache Programmierbeispiele ............................................................. 806 11.3.1 11.3.2 11.3.3 11.3.4

Mandelbrot- und Julia-Mengen ............................................................. Turtle-Grafik .......................................................................................... L-Systeme .............................................................................................. Ausblick .................................................................................................

808 812 815 818

11.4 3-D-Grafikprogrammierung ................................................................... 819

11.4.1 Sichtbarkeit ............................................................................................ 820 11.4.2 Beleuchtungsmodelle ............................................................................. 821

Inhalt

XXIII 11.4.3 11.4.4 11.4.5 11.4.6

Ray-Tracing ............................................................................................ Photon-Mapping ..................................................................................... Die Radiosity Methode .......................................................................... Ausblick .................................................................................................

823 824 825 826

12 Software-Entwicklung 827 12.1 Methoden und Werkzeuge für Projekte ................................................. 828 12.2 Vorgehensmodelle .................................................................................. 830 12.2.1 12.2.2 12.2.3 12.2.4 12.2.5 12.2.6 12.2.7 12.2.8

Code and fix-Verfahren .......................................................................... Wasserfall-Modelle ................................................................................ Transformations-Modelle ....................................................................... Nichtsequentielle Vorgehensmodelle ..................................................... Prototyping und Spiralmodelle .............................................................. Modelle zur inkrementellen Systementwicklung ................................... Evolutionäre Entwicklungsmodelle ....................................................... Modelle zur objektorientierten Systementwicklung ..............................

830 831 834 834 835 836 836 837

12.3 Traditionelle Methoden zur Programmentwicklung .............................. 839 12.3.1 12.3.2 12.3.3 12.3.4 12.3.5 12.3.6

Strukturierte Programmierung ............................................................... Schrittweise Verfeinerung und Top-down-Entwurf ............................... Geheimnisprinzip, Daten-Abstraktion und Modularisierung ................. Strukturierte Analyse- und Entwurfstechniken ...................................... Entity/Relationship-Modellierung ......................................................... Systematische Test-, Review- und Inspektionsverfahren .......................

839 839 840 841 842 843

12.4 Objektorientierte Software-Entwicklungsmethoden .............................. 843

12.4.1 Prinzipien der Objektorientierung .......................................................... 843 12.4.2 Objektorientierter Entwurf ..................................................................... 844

12.5 Objektorientierte Analyse und Modellierung ........................................ 845 12.5.1 12.5.2 12.5.3 12.5.4 12.5.5 12.5.6

Standardisierung der objektorientierten Modellierung .......................... Die Modellierungssprache UML ............................................................ Software-Architektur .............................................................................. Entwurfsmuster und Frameworks .......................................................... Aspekt-orientierte Entwicklung ............................................................. Modell-getriebene Architektur ...............................................................

846 846 850 851 851 852

12.6 Projekt-Management .............................................................................. 853

12.6.1 Projektinitialisierung und -planung ........................................................ 853 12.6.2 Projektsteuerung und -koordination ....................................................... 854 12.6.3 Projektabschluss und -bericht ................................................................ 855

12.7 Software-Qualitätssicherung .................................................................. 855

12.7.1 Qualitätsnormen und Zertifizierung ....................................................... 857

12.8 Werkzeuge und Programmierumgebungen ............................................ 859

12.8.1 Klassifizierung von Werkzeugen ........................................................... 859 12.8.2 Werkzeuge zur Analyse und Modellierung ............................................ 860 12.8.3 Werkzeuge für Spezifikation und Entwurf ............................................. 860

XXIV

Inhalt 12.8.4 12.8.5 12.8.6 12.8.7

Programmier-Werkzeuge ....................................................................... Test- und Fehlerbehebungs-Werkzeuge ................................................. Tätigkeitsübergreifende Werkzeuge ...................................................... Entwicklungs-Umgebungen ..................................................................

861 861 863 864

A Literatur A.1 Einführende Bücher ............................................................................... A.2 Lehrbücher der Informatik ..................................................................... A.3 Programmieren in Java .......................................................................... A.4 Algorithmen und Datenstrukturen ......................................................... A.5 Rechnerarchitektur ................................................................................. A.6 Betriebssysteme ..................................................................................... A.7 Rechnernetze .......................................................................................... A.8 Internet ................................................................................................... A.9 Theoretische Informatik und Compilerbau ............................................ A.10 Datenbanken .......................................................................................... A.11 Grafikprogrammierung .......................................................................... A.12 Software-Entwicklung ........................................................................... A.13 Mathematischer Hintergrund ................................................................. A.14 Sonstiges ................................................................................................

867 867 867 868 869 869 870 871 871 872 873 874 875 876 876

Stichwortverzeichnis

877

1

Einführung

In diesem Kapitel werden wir wichtige Themen der Informatik in einer ersten Übersicht darstellen. Zunächst beschäftigen wir uns mit dem Begriff Informatik, dann mit fundamentalen Grundbegriffen wie z.B. Bits und Bytes. Danach behandeln wir die Frage, wie Texte, logische Werte und Zahlen in Computern gespeichert werden. Wir erklären den Aufbau eines PCs und das Zusammenwirken von Hardware, Controllern, Treibern und Betriebssystem bis zur benutzerfreundlichen Anwendungssoftware. Viele der hier eingeführten Begriffe werden in den späteren Kapiteln noch eingehender behandelt. Daher dient dieses Kapitel als erster Überblick und als Grundsteinlegung für die folgenden.

1.1

Was ist „Informatik“?

Der Begriff Informatik leitet sich von dem Begriff Information her. Er entstand in den 60er Jahren. Informatik ist die Wissenschaft von der maschinellen Informationsverarbeitung. Die englische Bezeichnung für Informatik ist Computer Science, also die Wissenschaft, die sich mit Rechnern beschäftigt. Wenn auch die beiden Begriffe verschiedene Blickrichtungen andeuten, bezeichnen sie dennoch das Gleiche. Die Spannweite der Disziplin Informatik ist sehr breit, und demzufolge ist das Gebiet in mehrere Teilgebiete untergliedert.

1.1.1

Technische Informatik

Die Technische Informatik beschäftigt sich vorwiegend mit der Konstruktion von Rechnern, Speicherchips, schnellen Prozessoren oder Parallelprozessoren, aber auch mit dem Aufbau von Peripheriegeräten wie Festplatten, Druckern und Bildschirmen. Die Grenzen zwischen der Technischen Informatik und der Elektrotechnik sind fließend. An einigen Universitäten gibt es den Studiengang Datentechnik, der gerade diesen Grenzbereich zwischen Elektrotechnik und Informatik zum Gegenstand hat. Man kann vereinfachend sagen, dass die Technische Informatik für die Bereitstellung der Gerätschaften, der so genannten Hardware, zuständig ist, welche die Grundlage jeder maschinellen Informationsverarbeitung darstellt. Naturgemäß muss die Technische Informatik aber auch die beabsichtigten Anwendungsgebiete der Hardware im Auge haben. Insbesondere muss sie die Anforderungen der Programme berücksichtigen, die durch diese Hardware ausgeführt werden sollen. Es ist ein Unterschied, ob ein Rechner extrem viele Daten in begrenzter Zeit verarbeiten soll, wie etwa bei der Wettervorhersage oder bei der Steuerung einer Raumfähre, oder ob er im kommerziellen oder im häuslichen Bereich eingesetzt wird, wo es mehr auf die Unterstützung intuitiver Benutzerführung, die Präsentation von Grafiken, Text oder Sound ankommt.

2

1.1.2

1 Einführung

Praktische Informatik

Die Praktische Informatik beschäftigt sich im weitesten Sinne mit den Programmen, die einen Rechner steuern. Im Gegensatz zur Hardware sind solche Programme leicht veränderbar, man spricht daher auch von Software. Es ist ein weiter Schritt von den recht primitiven Operationen, die die Hardware eines Rechners ausführen kann, bis zu den Anwendungsprogrammen, wie etwa Textverarbeitungssystemen, Spielen und Grafiksystemen, mit denen ein Anwender umgeht. Die Brücke zwischen der Hardware und der Anwendungssoftware zu schlagen, ist die Aufgabe der Praktischen Informatik. Ein klassisches Gebiet der Praktischen Informatik ist der Compilerbau. Ein Compiler übersetzt Programme, die in einer technisch-intuitiven Notation, einer so genannten Programmiersprache, formuliert sind, in die stark von den technischen Besonderheiten der Maschine geprägte Notation der Maschinensprache. Es gibt viele populäre Programmiersprachen, darunter BASIC, Cobol, Fortran, Pascal, C, C++, C#, Java, Perl, LISP, ML und PROLOG. Programme, die in solchen Hochsprachen formuliert sind, können nach der Übersetzung durch einen Compiler auf den verschiedensten Maschinen ausgeführt werden oder, wie es im Informatik-Slang heißt, laufen. Ein Maschinenspracheprogramm läuft dagegen immer nur auf dem Maschinentyp, für den es geschrieben wurde. Java hat eine Zwischenlösung wieder populär gemacht. Dabei erzeugt der Compiler einen so genannten Bytecode für eine virtuelle (gedachte) Maschine. Diese Maschine kann dann von den verschiedensten konkreten Rechnern interpretiert werden (siehe S. 79 ff.).

1.1.3

Theoretische Informatik

Die Theoretische Informatik beschäftigt sich mit den abstrakten mathematischen und logischen Grundlagen aller Teilgebiete der Informatik. Theorie und Praxis sind in der Informatik enger verwoben, als in vielen anderen Disziplinen, theoretische Erkenntnisse sind schneller und direkter einsetzbar. Durch die theoretischen Arbeiten auf dem Gebiet der formalen Sprachen und der Automatentheorie zum Beispiel hat man das Gebiet des Compilerbaus heute sehr gut im Griff. In Anlehnung an die theoretischen Erkenntnisse sind praktische Werkzeuge entstanden. Diese sind selbst wieder Programme, mit denen ein großer Teil des Compilerbaus automatisiert werden kann. Bevor eine solche Theorie existierte, musste man mit einem Aufwand von ca. 25 Bearbeiter-Jahren (Anzahl der Bearbeiter * Arbeitszeit = 25) für die Konstruktion eines einfachen Compilers rechnen, heute erledigen Studenten eine vergleichbare Aufgabe im Rahmen eines Praktikums. Neben den Beiträgen, die die Theoretische Informatik zur Entwicklung des Gebietes leistet, ist die Kenntnis der theoretischen Strukturen eine wichtige Schulung für jeden, der komplexe Systeme entwirft. Gut durchdachte, theoretisch abgesicherte Entwürfe erweisen sich auch für hochkomplexe Software als sicher und erweiterbar. Software-Systeme, die im Hauruck-Verfahren entstehen, stoßen immer bald an die Grenze, ab der sie nicht mehr weiterentwickelbar sind. Die Entwicklung von Software sollte sich an der Ökonomie der Theoriebildung in der Mathematik orientieren: möglichst wenige Annahmen, möglichst keine Ausnahmen. Ein wichtiger Grund etwa, warum die Konstruktion von Fortran-Compilern so kompliziert ist,

1.1 Was ist „Informatik“?

3

liegt darin, dass dieses Prinzip bei der Definition der Programmiersprache nicht angewendet wurde. Jeder Sonder- oder Ausnahmefall, jede zusätzliche Regel macht nicht nur dem Konstrukteur des Compilers das Leben schwer, sondern auch den vielen Fortran-Programmierern.

1.1.4

Angewandte Informatik

Die Angewandte Informatik beschäftigt sich mit dem Einsatz von Rechnern in den verschiedensten Bereichen unseres Lebens. Da in den letzten Jahren die Hardware eines Rechners für jeden erschwinglich geworden ist, gibt es auch keinen Bereich mehr, der der Computeranwendung verschlossen ist. Einerseits gilt es, spezialisierte Programme für bestimmte Aufgaben zu erstellen, andererseits müssen Programme und Konzepte entworfen werden, die in vielfältigen Umgebungen einsetzbar sein sollen. Beispiele für solche universell einsetzbaren Systeme sind etwa Textverarbeitungssysteme oder Tabellenkalkulationssysteme (engl. spread sheet). Auch die Angewandte Informatik ist nicht isoliert von den anderen Gebieten denkbar. Es gilt schließlich, sowohl neue Möglichkeiten der Hardware als auch im Zusammenspiel von Theoretischer und Praktischer Informatik entstandene Werkzeuge einer sinnvollen Anwendung zuzuführen. Als Beispiel mögen die Organizer, Palmtops und Tablet-PCs dienen, die im Wesentlichen aus einem Flüssigkristall-Bildschirm bestehen, den man mit einem Griffel handschriftlich beschreiben kann. Die Angewandte Informatik muss die Einsatzmöglichkeiten solcher Geräte, etwa in der mobilen Lagerhaltung, auf der Baustelle oder als vielseitiger, „intelligenter“ Terminkalender, entwickeln. Die Hardware wurde von der Technischen Informatik konstruiert, die Softwaregrundlagen, etwa zur Handschrifterkennung, von der Praktischen Informatik aufgrund der Ergebnisse der Theoretischen Informatik gewonnen. Wenn man im deutschsprachigen Raum auch diese Einteilung der Informatik vornimmt, so ist es klar, dass die einzelnen Gebiete nicht isoliert und ihre jeweiligen Grenzen nicht wohldefiniert sind. Die Technische Informatik überlappt sich stark mit der Praktischen Informatik, jene wieder mit der Theoretischen Informatik. Auch die Grenzen zwischen der Praktischen und der Angewandten Informatik sind fließend. Gleichgültig in welchem Bereich man später einmal arbeiten möchte, muss man auch die wichtigsten Methoden der Nachbargebiete kennen lernen, um die Möglichkeiten seines Gebietes entfalten und entwickeln zu lernen, aber auch um die Grenzen abschätzen zu können. Neben der in diese vier Bereiche eingeteilten Informatik haben viele Anwendungsgebiete ihre eigenen Informatik-Ableger eingerichtet. So spricht man zum Beispiel von der Medizinischen Informatik, der Wirtschaftsinformatik, der Medieninformatik, der Bio-Informatik, der Linguistischen Informatik, der Juristischen Informatik oder der Chemie-Informatik. Für einige dieser Bereiche gibt es an Fachhochschulen und Universitäten bereits Studiengänge. Insbesondere geht es darum, fundierte Kenntnisse über das angestrebte Anwendungsgebiet mit grundlegenden Kenntnissen informatischer Methoden zu verbinden.

4

1 Einführung

1.2

Information und Daten

Was tut eigentlich ein Computer? Diese Frage scheint leicht beantwortbar zu sein, indem wir einfach eine Fülle von Anwendungen aufzählen. Computer berechnen Wettervorhersagen, steuern Raumfähren, spielen Schach, machen Musik und erzeugen unglaubliche Effekte in Kinofilmen. Sicher liegt hier aber nicht die Antwort auf die gestellte Frage, denn wir wollen natürlich wissen, wie Computer das machen. Um dies zu erklären, müssen wir uns zunächst einigen, in welcher Tiefe wir anfangen sollen. Bei der Erklärung des Schachprogramms wollen wir vielleicht wissen: • • • • •

Wie wird das Schachspiel des Computers bedient? Wie ist das Schachprogramm aufgebaut? Wie sind die Informationen über den Spielstand im Hauptspeicher des Rechners gespeichert und wie werden sie verändert? Wie sind die Nullen und Einsen in den einzelnen Speicherzellen organisiert und wie werden sie verändert? Welche elektrischen Signale beeinflussen die Transistoren und Widerstände, aus denen Speicherzellen und Prozessor aufgebaut sind?

Wir müssen uns auf eine solche mögliche Erklärungsebene festlegen. Da es hier um Informatik geht, also um die Verarbeitung von Informationen, beginnen wir auf der Ebene der Nullen und Einsen, denn dies ist die niedrigste Ebene der Informationsverarbeitung. Wir beschäftigen uns also zunächst damit, wie Informationen im Rechner durch Nullen und Einsen repräsentiert werden können. Die so repräsentierten Informationen nennen wir Daten. Die Repräsentation muss derart gewählt werden, dass man aus den Daten auch wieder die repräsentierte Information zurückgewinnen kann. Diesen Prozess der Interpretation von Daten als Information nennt man auch Abstraktion.

Information Repräsentation

Abstraktion Daten

Abb. 1.1:

Information und Daten

1.2 Information und Daten

1.2.1

5

Bits

Ein Bit ist die kleinstmögliche Einheit der Information. Ein Bit ist die Informationsmenge in einer Antwort auf eine Frage, die zwei Möglichkeiten zulässt: • • • • • • •

ja oder nein, wahr oder falsch, schwarz oder weiß, hell oder dunkel, groß oder klein, stark oder schwach, links oder rechts.

Zu einer solchen Frage lässt sich immer eine Codierung der Antwort festlegen. Da es zwei mögliche Antworten gibt, reicht ein Code mit zwei Zeichen, ein so genannter binärer Code. Man benutzt dazu die Zeichen: 0 und 1. Eine solche Codierung ist deswegen nötig, weil die Information technisch dargestellt werden muss. Man bedient sich dabei etwa elektrischer Ladungen: 0 = ungeladen, 1 = geladen, oder elektrischer Spannungen 0 = 0 Volt, 1 = 5 Volt, oder Magnetisierungen 0 = unmagnetisiert, 1 = magnetisiert. So kann man etwa die Antwort auf die Frage: Welche Farbe hat der Springer auf F3? im Endeffekt dadurch repräsentieren bzw. auffinden, indem man prüft, • • •

ob ein Kondensator eine bestimmte Ladung besitzt, ob an einem Widerstand eine bestimmte Spannung anliegt oder ob eine bestimmte Stelle auf einer Magnetscheibe magnetisiert ist.

Da es uns im Moment aber nicht auf die genaue technische Realisierung ankommt, wollen wir die Übersetzung physikalischer Größen in Informationseinheiten bereits voraussetzen und nur von den beiden möglichen Elementarinformationen 0 und 1 ausgehen.

6

1 Einführung

1.2.2

Bitfolgen

Lässt eine Frage mehrere Antworten zu, so enthält die Beantwortung der Frage mehr als ein Bit an Information. Die Frage etwa, aus welcher Himmelsrichtung, Nord, Süd, Ost oder West, der Wind weht, lässt 4 mögliche Antworten zu. Der Informationsgehalt in der Beantwortung der Frage ist aber nur 2 Bit, denn man kann die ursprüngliche Frage in zwei andere Fragen verwandeln, die jeweils nur zwei Antworten zulassen: 1. Weht der Wind aus einer der Richtungen Nord oder Ost (ja/nein)? 2. Weht der Wind aus einer der Richtungen Ost oder West (ja/nein)? Eine mögliche Antwort, etwa ja auf die erste Frage und nein auf die zweite Frage, lässt sich durch die beiden Bits 1 0 repräsentieren. Die Bitfolge 10 besagt also diesmal, dass der Wind aus Norden weht. Ähnlich repräsentieren die Bitfolgen 0 0 ↔ Süd

0 1 ↔ West

1 0 ↔ Nord

1 1 ↔ Ost.

Offensichtlich gibt es genau 4 mögliche Folgen von 2 Bit. Mit 2 Bit können wir also Fragen beantworten, die 4 mögliche Antworten zulassen. Lassen wir auf dieselbe Frage (Woher weht der Wind?) auch noch die Zwischenrichtungen Südost, Nordwest, Nordost und Südwest zu, so gibt es 4 weitere mögliche Antworten, also insgesamt 8. Mit einem zusätzlichen Bit, also mit insgesamt 3 Bits, können wir alle 8 möglichen Antworten darstellen. Die möglichen Folgen aus 3 Bits sind: 000, 001, 010, 011, 100, 101, 110, 111. und die möglichen Antworten auf die Frage nach der Windrichtung sind: Süd, West, Nord, Ost, Südost, Nordwest, Nordost, Südwest. Jede beliebige eindeutige Zuordnung der Himmelsrichtungen zu diesen Bitfolgen können wir als Codierung von Windrichtungen hernehmen, zum Beispiel: 000 = Süd 001 = West 010 = Nord 011 = Ost

100 = Südost 101 = Nordwest 110 = Nordost 111 = Südwest

Offensichtlich verdoppelt jedes zusätzliche Bit die Anzahl der möglichen Bitfolgen, so dass gilt: Es gibt genau 2N mögliche Bitfolgen der Länge N.

1.2 Information und Daten

1.2.3

7

Hexziffern

Ein Rechner ist viel besser als ein Mensch in der Lage, mit Kolonnen von Bits umzugehen. Für den Menschen wird eine lange Folge von Nullen und Einsen bald unübersichtlich. Es wird etwas einfacher, wenn wir lange Bitfolgen in Gruppen zu 4 Bits anordnen. Aus einer Bitfolge wie 01001111011000010110110001101100 wird dann 0100 1111 0110 0001 0110 1100 0110 1100. Eine Gruppe von 4 Bits nennt man auch Halb-Byte oder Nibble. Da nur 16 verschiedene Nibbles möglich sind, bietet es sich an, jedem einen Namen zu geben. Wir wählen dazu die Ziffern '0' bis '9' und zusätzlich die Zeichen 'A' bis 'F'. Jedem Halb-Byte ordnet man auf natürliche Weise eine dieser so genannten Hexziffern zu: 0000 0001 0010 0011

= = = =

0 1 2 3

0100 0101 0110 0111

= = = =

4 5 6 7

1000 1001 1010 1011

= = = =

8 9 A B

1100 1101 1110 1111

= = = =

C D E F.

Damit lässt sich die obige Bitfolge kompakter als Folge von Hexziffern darstellen: 4 F 6 1 6 C 6 C. Die Rückübersetzung in eine Bitfolge ist ebenso einfach, wir müssen nur jede Hexziffer durch das entsprechende Halb-Byte ersetzen. So wie sich eine Folge von Dezimalziffern als Zahl im Dezimalsystem deuten lässt, können wir eine Folge von Hexziffern auch als eine Zahl im Sechzehner- oder Hexadezimal-System auffassen. Den Zahlenwert einer Folge von Hexziffern erhalten wir, indem wir jede Ziffer entsprechend ihrer Ziffernposition mit der zugehörigen Potenz der Basiszahl 16 multiplizieren und die Ergebnisse aufsummieren. Ähnlich wie die Dezimalzahl 327 für den Zahlenwert 2

1

3 × 10 + 2 × 10 + 7 × 10

0

steht, repräsentiert z.B. die Hexzahl 1AF3 den Zahlenwert 6899. Dies kann man wie folgt nachrechnen: 3

2

1

0

1 × 16 + A × 16 + F × 16 + 3 × 16 = 1 × 4096 + 10 × 256 + 15 × 16 + 3 . Die Umwandlung einer Dezimalzahl in eine Hexzahl mit dem gleichen Zahlenwert ist etwas schwieriger, wir werden auf S. 19 darauf eingehen, wenn wir die verschiedenen Zahldarstellungen behandeln. Da man das Hex-System vorwiegend verwendet, um lange Bitfolgen kompakter darzustellen, ist eine solche Umwandlung selten nötig. Die Hex-Darstellung wird von Assembler-Programmierern meist der Dezimaldarstellung vorgezogen. Daher findet man oft auch die ASCII-Tabelle (siehe S. 11), welche eine Zuordnung der 256 möglichen Bytes (s.u.) zu den Zeichen der Tastatur und anderen Sonderzeichen festlegt, in Hex-Darstellung. Für das ASCII-Zeichen 'o' (das kleine „Oh“, nicht zu verwechseln

8

1 Einführung

mit der Ziffer „0“) hat man dann den Eintrag 6F, was der Dezimalzahl 6 × 16 + 15 = 111 entspricht. Umgekehrt findet man zu dem 97-sten ASCII-Zeichen, dem kleinen „a“, die HexDarstellung 61, denn 6 × 16 + 1 = 97. Allein aus der Ziffernfolge „61“ ist nicht ersichtlich, ob diese als Hexadezimalzahl oder als Dezimalzahl aufzufassen ist. Wenn eine Verwechslung nicht ausgeschlossen ist, hängt man zur Kennzeichnung von Hexzahlen ein kleines „h“ an, also 61h. Gelegentlich benutzt man die Basiszahl des Zahlensystems auch als unteren Index, wie in der folgenden Gleichung: 97 10 = 61 16 = 01100001 2

1.2.4

Bytes und Worte

Wenn ein Rechner Daten liest oder schreibt, wenn er mit Daten operiert, gibt er sich nie mit einzelnen Bits ab. Dies wäre im Endeffekt viel zu langsam. Stattdessen arbeitet er immer nur mit Gruppen von Bits, entweder mit 8 Bits, 16 Bits, 32 Bits oder 64 Bits. Man spricht dann von 8-Bit-Rechnern, 16-Bit-Rechnern, 32-Bit-Rechnern oder 64-Bit-Rechnern. In Wahrheit gibt es aber auch Mischformen: Rechner, die etwa intern mit 32-Bit-Blöcken rechnen, aber immer nur Blöcke zu 64 Bits lesen oder schreiben. Stets jedoch ist die Länge eines Bitblocks ein Vielfaches von 8. Eine Gruppe von 8 Bits nennt man ein Byte. Ein Byte besteht infolgedessen aus zwei Nibbles, man kann es also durch zwei Hex-Ziffern darstellen. Es gibt daher 162 = 256 verschiedene Bytes von 0000 0000 bis 1111 1111. In Hexzahlen ausgedrückt erstreckt sich dieser Bereich von 00h bis FFh, dezimal von 0 bis 255. Für eine Gruppe von 2, 4 oder 8 Bytes sind auch die Begriffe Wort, Doppelwort und Quadwort im Gebrauch, allerdings ist die Verwendung dieser Begriffe uneinheitlich. Bei einem 16Bit Rechner bezeichnet man eine 16-Bit Größe als Wort, ein Byte ist dann ein Halbwort. Bei einem 32-Bit Rechner steht „Wort“ auch für eine Gruppe von 4 Bytes.

1.2.5

Dateien

Eine Datei ist eine beliebig lange Folge von Bytes. Dateien werden meist auf Festplatten, USB-Sticks, CD-ROMs gespeichert. Jede Information, mit der ein Rechner umgeht, Texte, Zahlen, Musik, Bilder, Programme, muss sich auf irgendeine Weise als Folge von Bytes repräsentieren lassen und kann daher als Datei gespeichert werden. Hat man nur den Inhalt einer Datei vorliegen, so kann man nicht entscheiden, welche Art von Information die enthaltenen Bytes repräsentieren sollen. Diese zusätzliche Information versucht man durch einen geeigneten Dateinamen auszudrücken. Insbesondere hat es sich eingebürgert, die Dateinamen aus zwei Teilen zusammenzusetzen, einem Namen und einer Erweiterung. Diese beiden Namensbestandteile werden durch einen Punkt „.“ getrennt. Beispielsweise besteht die Datei mit Namen „FoxyLady.wav“ aus dem Namen „FoxyLady“ und der Erweiterung „wav“. Die Endung „wav“ soll andeuten, dass es sich um eine Musikdatei handelt, die mit einer entsprechenden Software abgespielt werden kann. Nicht alle Betriebssysteme verwenden diese Konventionen. In UNIX ist es z.B. üblich den Typ der Datei in den ersten Inhalts-Bytes zu kennzeichnen.

1.2 Information und Daten

1.2.6

9

Datei- und Speichergrößen

Unter der Größe einer Datei versteht man die Anzahl der darin enthaltenen Bytes. Man verwendet dafür die Einheit B. Eine Datei der Größe 245B enthält also 245 Byte. In einigen Fällen wird die Abkürzung B auch für ein Bit verwendet, so dass wir es vorziehen, bei Verwechslungsgefahr die Einheiten als Byte oder als Bit auszuschreiben. Dateien von wenigen hundert Byte sind äußerst selten, meist bewegen sich die Dateigrößen in Bereichen von Tausenden oder gar Millionen von Bytes. Es bietet sich an, dafür die von Gewichts- oder Längenmaßen gewohnten Präfixe kilo- (für tausend) und mega- (für million) zu verwenden. Andererseits ist es günstig, beim Umgang mit binären Größen auch die Faktoren durch Zweierpotenzen 2, 4, 8, 16, ... auszudrücken. Da trifft es sich gut, dass die Zahl 1000 sehr nahe bei einer Zweierpotenz liegt, nämlich 2

10

= 1024 .

Daher stehen in vielen Bereichen der Informatik das Präfix kilo für 1024 und das Präfix mega für 2

20

= 1024 × 1024 = 1048576 .

Die Abkürzungen für die in der Informatik benutzten Größenfaktoren sind daher k = 1024 = 2 10 (k = kilo) M = 1024 × 1024 = 2 20

(M = mega)

G = 1024 × 1024 × 1024 = 2 30

(G = giga)

T = 1024 × 1024 × 1024 × 1024 = 2 40

(T = tera)

P = 1024 × 1024 × 1024 × 1024 × 1024 = 2 50

(P = peta)

E = 1024 × 1024 × 1024 × 1024 × 1024 × 1024 = 2 60

(E = exa)

Die obigen Maßeinheiten haben sich auch für die Angabe der Größe des Hauptspeichers und anderer Speichermedien eingebürgert. Allerdings verwenden Hersteller von Festplatten, DVDs, und Blu-ray-Discs meist G(Giga) für den Faktor 109 statt für 230. So kann es sein, dass der Rechner auf einer 500 GByte Festplatte nur 465 GByte Speicherplatz erkennt. Dies liegt daran, dass folgendes gilt: 500

× 10 9 ≈ 465 × 2 30

Die IEC (International Electrotechnical Commission) schlug bereits 1996 vor, die in der Informatik benutzten Größenfaktoren, die auf den Zweierpotenzen basieren, mit einem kleinen „i“ zu kennzeichnen, also ki, Mi, Gi, etc. und die Präfixe k, M, G für die Zehnerpotenzen zu reservieren. Dann hätte obige Festplatte 500 GB, aber 465 GiB(ausgesprochen: Gibibyte). Dieser Vorschlag hat sich aber bisher nicht durchgesetzt.

10

1 Einführung

Anhaltspunkte für gängige Größenordnungen von Dateien und Geräten sind: ~ 200 ~ 4 ~ 100 ~ 1,4 ~ 4 ~ 40 ~ 700 ~ 2 ~ 27 ~ 8 ~ 1

B kB kB MB MB MB MB GB GB GB TB

eine kurze Textnotiz dafür benötigter Platz auf der Festplatte formatierter Brief, Excel Datei, pdf-Dokument ohne Bildern Diskettenkapazität Musiktitel im mp3-Format, Windows Programm Musiktitel im wav-Format CD-ROM Kapazität PC Hauptspeicher Blu-ray Disc (dual-layer: 54 GB) USB-Stick, SD-Karten Moderne Festplatten

Natürlich hängt die genaue Größe einer Datei von ihrem Inhalt, im Falle von Bild- oder Audiodateien auch von dem verwendeten Aufzeichnungsverfahren ab. Durch geeignete Kompressionsverfahren lässt sich ohne merkliche Qualitätsverluste die Dateigröße erheblich reduzieren. So kann man z.B. mithilfe des MP3-Codierungsverfahrens einen Musiktitel von 40 MB Größe auf ca. 4 MB komprimieren. Dadurch ist es möglich, auf einer einzigen CD den Inhalt von 10 - 12 herkömmlichen Musik-CDs zu speichern.

1.2.7

Längen- und Zeiteinheiten

Für Längen- und Zeitangaben werden auch in der Informatik dezimale Einheiten benutzt. So ist z.B. ein 2,6 GHz Prozessor mit 2,6 × 109 = 2600000000 Hertz (Schwingungen pro Sekunde) getaktet. Ein Takt dauert also 1/(2,6 × 109) = 0,38 × 10-9 sec, das sind 0,38 ns. Das Präfix n steht hierbei für nano, also den Faktor 10-9. Die anderen Faktoren kleiner als 1 sind: m = 1 ⁄ 1000 = 10 – 3 (m = milli), µ = 1 ⁄ 1000000 = 10 – 6

(µ = mikro),

n = 1 ⁄ 1000000000 = 10 – 9

(n = nano),

p = … = 10 – 12 (p = pico), f = … = 10 – 15 (f = femto). Für Längenangaben wird neben den metrischen Maßen eine im Amerikanischen immer noch weit verbreitete Einheit verwendet. 1" = 1 in = 1 inch = 1 Zoll = 2,54 cm = 25,4 mm. Für amerikanische Längenmaße hat sich nicht einmal das Dezimalsystem durchgesetzt. So gibt man Teile eines Zolls in Brüchen an, wie z.B. 3½" (Diskettengröße) oder 5¼" (frühere Standardgröße von Disketten). Für die Angabe der Bildschirmdiagonalen benutzt man ebenfalls die Maßeinheit Zoll, z.B. 19" oder 22". International verwendet man aber dezimale Abstufungen, z.B. 14,1" als gebräuchliche Bildschirmgröße eines Laptops.

1.3 Informationsdarstellung

1.3

11

Informationsdarstellung

Als Daten bezeichnen wir, wie bereits im ersten Abschnitt dieses Kapitels erläutert, die Folgen von Nullen und Einsen, die irgendwelche Informationen repräsentieren. In diesem Abschnitt werden wir die Darstellung von Texten, logischen Werten, Zahlen und Programmen durch Daten erläutern.

1.3.1

Text

Um Texte in einem Rechner darzustellen, codiert man Alphabet und Satzzeichen in Bitfolgen. Mit einem Alphabet von 26 Kleinbuchstaben, ebenso vielen Großbuchstaben, einigen Satzzeichen wie etwa Punkt, Komma und Semikolon und Spezialzeichen wie „+“, „&“ und „%“ hat eine normale Tastatur eine Auswahl von knapp hundert Zeichen. Die Information, wo ein Zeilenumbruch stattfinden oder wo ein Text eingerückt werden soll, codiert man ebenfalls durch spezielle Zeichen. Solche Sonderzeichen, dazu gehören das CR-Zeichen (von englisch carriage return = Wagenrücklauf) und das Tabulatorzeichen Tab, werden nie ausgedruckt, sie haben beim Ausdrucken lediglich die entsprechende steuernde Wirkung. Sie heißen daher auch Steuerzeichen oder nicht-druckbare Zeichen.

1.3.2

ASCII-Code

Auf jeden Fall kommt man für die Darstellung aller Zeichen mit 7 Bits aus, das ergibt 128 verschiedene Möglichkeiten. Man muss also nur eine Tabelle erstellen, mit der jedem Zeichen ein solcher Bitcode zugeordnet wird. Dazu nummeriert man die 128 gewählten Zeichen einfach durch und stellt die Nummer durch 7 Bit binär dar. Die heute fast ausschließlich gebräuchliche Nummerierung ist die so genannte ASCII-Codierung. ASCII steht für „American Standard Code for Information Interchange“. Sie berücksichtigt einige Systematiken, wie: • • •

die Kleinbuchstaben sind in alphabetischer Reihenfolge durchnummeriert, die Großbuchstaben sind in alphabetischer Reihenfolge durchnummeriert, die Ziffern 0 bis 9 stehen in der natürlichen Reihenfolge.

Einige Zeichen mit ihren ASCII-Codes sind: Zeichen A B ... Z a b ... z

ASCII 65 66 ... 90 97 98 ... 122

Zeichen 0 1 ... 9 CR = > ?

ASCII 48 49 ... 57 13 61 62 63

12

1 Einführung

Da der ASCII-Code zur Datenübertragung (information interchange) konzipiert wurde, dienen die ersten Zeichen, ASCII 0 bis ASCII 31, sowie ASCII 127 verschiedenen Signalisierungs- und Steuerungszwecken. Um sie von der Tastatur einzugeben, muss man die Steuerungstaste (auf der Tastatur mit „Ctrl“ oder mit „Strg“ bezeichnet) gedrückt halten. Die ASCII-Codes von 1 bis 26 entsprechen dabei den Tastenkombinationen Ctrl-A bis Ctrl-Z. Zum Beispiel entspricht ASCII 7 (das Klingelzeichen) der Kombination Ctrl-G und ASCII 8 (Backspace) ist Ctrl-H. Einige dieser Codes können in Editoren oder auf der Kommandozeile direkt benutzt werden. Die gebräuchlichen Tastaturen spendieren den wichtigsten dieser ASCII-Codes eine eigene Taste. Dazu gehören u.a. Ctrl-I (Tabulator), Ctrl-H (Backspace), Ctrl-[ (Escape=ASCII 27) und Ctrl-M (Return). Die 128 ASCII-Zeichen entsprechen den Bytes 0000 0000 bis 0111 1111, d.h. den Hex-Zahlen 00 bis 7F. Eine Datei, die nur ASCII-Zeichen enthält, also Bytes, deren erstes Bit 0 ist, nennt man ASCII-Datei. Oft versteht man unter einer ASCII-Datei auch einfach eine Textdatei, selbst wenn Codes aus einer ASCII-Erweiterung verwendet werden.

1.3.3

ASCII-Erweiterungen

Bei der ASCII-Codierung werden nur die letzten 7 Bits eines Byte genutzt. Das erste Bit verwendete man früher als Kontrollbit für die Datenübertragung. Es wurde auf 0 oder 1 gesetzt, je nachdem ob die Anzahl der 1-en an den übrigen 7 Bitpositionen gerade (even) oder ungerade (odd) war. Die Anzahl der 1-en in dem gesamten Byte wurde dadurch immer gerade (even parity). Wenn nun bei der Übertragung ein kleiner Fehler auftrat, d.h. wenn in dem übertragenen Byte genau ein Bit verfälscht wurde, so konnte der Empfänger dies daran erkennen, dass die Anzahl der 1-en ungerade war. Bei der Verwendung des ASCII-Codes zur Speicherung von Texten und auch als Folge der verbesserten Qualität der Datenübertragung wurde dieses Kontrollbit überflüssig. Daher lag es nahe, nun alle 8 Bit zur Zeichenkodierung zu verwenden. Somit ergab sich ein weiterer verfügbarer Bereich von ASCII 128 bis ASCII 255. Der IBM-PC benutzt diese zusätzlichen Codes zur Darstellung von sprachspezifischen Zeichen wie z.B. „ä“ (ASCII 132), „ö“ (ASCII 148) „ü“ (ASCII 129) und einigen Sonderzeichen anderer Sprachen, darüber hinaus auch für Zeichen, mit denen man einfache grafische Darstellungen wie Rahmen und Schraffuren zusammensetzen kann. Diese Zeichen können über die numerische Tastatur eingegeben werden. Dazu muss diese aktiviert sein (dies geschieht durch die Taste „Num“), danach kann bei gedrückter „Alt“-Taste der dreistellige ASCII-Code eingegeben werden. Leider ist auch die Auswahl der sprachspezifischen Sonderzeichen eher zufällig und bei weitem nicht ausreichend für die vielfältigen Symbole fremder Schriften. Daher wurden von der International Organization for Standardization (ISO) verschiedene andere Optionen für die Nutzung der ASCII-Codes 128-255 als sog. ASCII-Erweiterungen normiert. In Europa ist die ASCII-Erweiterung ISO Latin-1 nützlich, die durch die Norm ISO 8859-1 beschrieben wird.

1.3 Informationsdarstellung

13

Einige Rechner, insbesondere wenn sie unter UNIX betrieben werden, benutzen nur die genormten ASCII-Zeichen von 0 bis 127. Auf solchen Rechnern sind daher Umlaute nicht so einfach darstellbar. Die Verwendung von Zeichen einer ASCII-Erweiterung beim Austausch von Daten, E-Mails oder Programmtexten ist ebenfalls problematisch. Benutzt der Empfänger zur Darstellung nicht die gleiche ASCII-Erweiterung, so findet er statt der schönen Sonderzeichen irgendwelche eigenartigen Symbole oder Kontrollzeichen in seinem Text. Schlimmstenfalls geht auch von jedem Byte das erste Bit verloren. Einen Ausweg bietet hier die Umcodierung der Datei in eine ASCII-Datei (z.B. mit dem Programm „uuencode“) und eine Dekodierung beim Empfänger (mittels „uudecode“). Viele E-Mail Programme führen solche Umkodierungen automatisch aus.

1.3.4

Unicode, UCS und UTF-8

Wegen der Problematik der ASCII-Erweiterungen bei der weltweiten Datenübertragung entstand in den letzten Jahren ein neuer Standard, der versucht, sämtliche relevanten Zeichen aus den unterschiedlichsten Kulturkreisen in einem universellen Code zusammenzufassen. Dieser neue Zeichensatz heißt Unicode und verwendet eine 16-Bit-Codierung, kennt also maximal 65536 Zeichen. Landesspezifische Zeichen, wie z.B. ö, ß, æ, ç oder à gehören ebenso selbstverständlich zum Unicode-Zeichensatz wie kyrillische, arabische, japanische und tibetische Schriftzeichen. Die ersten 128 Unicode-Zeichen sind identisch mit dem ASCII-Code, die nächsten 128 mit dem ISO Latin-1 Code. Herkömmliche Programmiersprachen lassen meist keine Zeichen aus ASCII-Erweiterungen zu. Java erlaubt als erste der weit verbreiteten Sprachen die Verwendung beliebiger UnicodeZeichen. Allerdings heißt dies noch lange nicht, dass jede Java-Implementierung einen Editor zur Eingabe von Unicode mitliefern würde. Unicode wurde vom Unicode-Konsortium (www.unicode.org) definiert. Dieses arbeitet ständig an neuen Versionen und Erweiterungen dieses Zeichensatzes. Die Arbeit des Unicode-Konsortium wurde von der ISO (www.iso.ch) aufgegriffen. Unter der Norm ISO-10646 wurde Unicode als Universal Character Set (UCS) international standardisiert. Beide Gremien bemühen sich darum, ihre jeweiligen Definitionen zu synchronisieren, um unterschiedliche Codierungen zu vermeiden. ISO geht allerdings in der grundlegenden Definition von UCS noch einen Schritt weiter als Unicode. Es werden sowohl eine 16-Bit-Codierung (UCS-2) als auch eine 31-BitCodierung (UCS-4) festgelegt. Die Codes von UCS-2 werden als basic multilingual plane (BMP) bezeichnet, beinhalten alle bisher definierten Codes und stimmen mit Unicode überein. Codes, die UCS-4 ausnutzen sind für potenzielle zukünftige Erweiterungen vorgesehen. Die Einführung von Unicode bzw. UCS-2 und UCS-4 führt zu beträchtlichen Kompatibilitätsproblemen, ganz abgesehen davon, dass der Umfang von derart codierten Textdateien wächst. Es ist daher schon frühzeitig der Wunsch nach einer kompakteren Codierung artikuliert worden, die kompatibel mit der historischen 7-Bit ASCII Codierung ist und die den neueren Erweiterungen Rechnung trägt. Eine solche Codierung, mit dem Namen UTF-8, wurde auch tatsächlich in den 90er Jahren eingeführt. Sie wurde von der ISO unter dem Anhang R zur Norm ISO-10646 festgeschrieben und auch von den Internetgremien als RFC2279 (siehe dazu Kapitel 8) standardisiert.

14

1 Einführung

Die Bezeichnung UTF ist eine Abkürzung von UCS Transformation Format. Dadurch wird betont, dass es sich lediglich um eine andere Codierung von UCS bzw. Unicode handelt. UTF-8 ist eine Mehrbyte-Codierung. 7-Bit ASCII-Zeichen werden mit einem Byte codiert, alle anderen verwenden zwischen 2 und 6 Bytes. Die Idee ist, dass häufig benutzte Zeichen mit einem Byte codiert werden, seltenere mit mehreren Bytes. Die Kodierung erfolgt nach den folgenden Prinzipien: • •

Jedes mit 0 beginnende Byte ist ein Standard 7-Bit ASCII Zeichen. Jedes mit 1 beginnende Byte gehört zu einem aus mehreren Bytes bestehenden UTF-8 Code. Besteht ein UTF-8 Code aus n ≥ 2 Bytes, so beginnt das erste (Start-)Byte mit n vielen 1en, und jedes der n-1 Folgebytes mit der Bitfolge 10.

Der erste Punkt garantiert, dass Teile eines Mehrbyte UTF-8 Zeichens nicht als 7-Bit-ASCII Zeichen missdeutet werden können. Der zweite Punkt erlaubt es, Wortgrenzen in einer UTF-8 codierten Datei leicht zu erkennen, was ein einfaches Wiederaufsetzen bei einem Übertragungsfehler ermöglicht. Auch einfache syntaktische Korrektheitstests sind möglich. Es ist ziemlich unwahrscheinlich, dass eine (längere) korrekte UTF-8 Datei in Wahrheit anders zu interpretieren ist. UTF-8 kann die verschiedenen UCS-Codes (und Teilmengen davon) auf einfache Weise repräsentieren: • • • • • •

1-Byte-Codes haben die Form 0xxx xxxx und ermöglichen die Verwendung von 7 (mit x gekennzeichneten) Bits und damit die Codierung von allen 7-Bit ASCII Codes. 2-Byte-Codes haben die Form 110x xxxx 10xx xxxx und ermöglichen die Codierung aller 11-Bit UCS-2 Codes. 3-Byte-Codes haben die Form 1110 xxxx 10xx xxxx 10xx xxxx. Mit den 16 noch verfügbaren Bits können alle 16-Bit UCS-2 Codes dargestellt werden. 4-Byte-Codes der Form 1111 0xxx 10xx xxxx 10xx xxxx 10xx xxxx ermöglichen die Verwendung von 21 Bits zur Codierung aller 21-Bit UCS-4 Codes. 5-Byte-Codes können alle 26-Bit UCS-4 Codes darstellen. Sie haben die Form: 1111 10xx 10xx xxxx 10xx xxxx 10xx xxxx 10xx xxxx 6-Byte-Codes ermöglichen die Codierung des kompletten 31-Bit UCS-4 Codes: 1111 110x 10xx xxxx 10xx xxxx 10xx xxxx 10xx xxxx 10xx xxxx

UTF-8 codierte Dateien sind also kompatibel zur 7-Bit ASCII Vergangenheit und verlängern den Umfang von Dateien aus dem amerikanischen und europäischen Bereich gar nicht oder nur unwesentlich. Diese Eigenschaften haben dazu geführt, dass diese Codierungsmethode der de facto Standard bei der Verwendung von Unicode geworden ist. Bei den Webseiten des Internets wird UTF-8 immer häufiger verwendet – alternativ dazu können in HTML-Dateien Sonderzeichen, also z.B. Umlaute wie „ä“ durch so genannte Entities als „ä“ umschrieben werden. Bei den Bytecode Dateien von Java ist die UTF-8 Codierung schon von Anfang an verwendet worden. Neben UTF-8 gibt es noch andere Transformations-Codierungen wie UTF-2, UTF-7 und UTF-16, die allerdings nur geringe Bedeutung erlangt haben.

1.3 Informationsdarstellung

1.3.5

15

Zeichenketten

Zur Codierung eines fortlaufenden Textes fügt man einfach die Codes der einzelnen Zeichen aneinander. Eine Folge von Textzeichen heißt auch Zeichenkette (engl. string). Der Text „Hallo Welt“ wird also durch die Zeichenfolge H,

a,

l,

l,

o,

,

W,

e,

l,

t

repräsentiert. Jedes dieser Zeichen, einschließlich des Leerzeichens „ “, ersetzen wir durch seine Nummer in der ASCII-Tabelle und erhalten: 072 097 108 108 111 032 087 101 108 116 Alternativ können wir die ASCII-Nummern auch hexadezimal schreiben, also: 48 61 6C 6C 6F 20 57 65 6C 74 Daraus können wir unmittelbar auch die Repräsentation durch eine Bitfolge entnehmen: 01001000 01100001 01101100 01101100 01101111 00100000 01010111 01100101 01101100 01110100. Es soll hier nicht unerwähnt bleiben, dass im Bereich der Großrechner noch eine andere als die besprochene ASCII-Codierung in Gebrauch ist. Es handelt sich um den so genannten EBCDI-Code (extended binary coded decimal interchange). Mit dem Rückgang der Großrechner verliert diese Codierung aber zunehmend an Bedeutung.

1.3.6

Logische Werte und logische Verknüpfungen

Logische Werte sind die Wahrheitswerte Wahr und Falsch (engl. true und false). Sie werden meist durch die Buchstaben T und F abgekürzt. Auf diesen logischen Werten sind die booleschen Verknüpfungen NOT (Negation oder Komplement), AND (Konjunktion), OR (Disjunktion) und XOR (exklusives OR) durch die folgenden Verknüpfungstafeln festgelegt:

NOT

AND

F

T

OR

F

T

XOR

F

T

F

T

F

F

F

F

F

T

F

F

T

T

F

T

F

T

T

T

T

T

T

F

Abb. 1.2:

Logische Verknüpfungen

Die AND-Verknüpfung zweier Argumente ist also nur dann T, wenn beide Argumente T sind. Die OR-Verknüpfung zweier Argumente ist nur dann F, wenn beide Argumente F sind. Die

16

1 Einführung

XOR-Verknüpfung zweier Argumente ist genau dann T, wenn beide Argumente verschieden sind. Beispielsweise gilt T XOR F = T , denn in der XOR-Tabelle findet sich in der Zeile neben T und der Spalte unter F der Eintrag T. Da es nur zwei Wahrheitswerte gibt, könnte man diese durch die beiden möglichen Werte eines Bits darstellen, z.B. durch F = 0 und T = 1. Da aber ein Byte die kleinste Einheit ist, mit der ein Computer operiert, spendiert man meist ein ganzes Byte für einen Wahrheitswert. Eine gängige Codierung ist F = 0000 0000 und T = 1111 1111. Man kann beliebige Bitketten auch als Folgen logischer Werte interpretieren. Die logischen Verknüpfungen sind für diese Bitketten als bitweise Verknüpfung der entsprechenden Kettenelemente definiert. So berechnet man z.B. das bitweise Komplement NOT 01110110 = 10001001 oder die bitweise Konjunktion 01110110 AND 11101011 = 01100010.

1.3.7

Programme

Programme, das heißt Anweisungen, die einen Rechner veranlassen, bestimmte Dinge zu tun, sind im Hauptspeicher des Rechners oder auf einem externen Medium gespeichert. Auch für die Instruktionen eines Programms benutzt man eine vorher festgelegte Codierung, die jedem Befehl eine bestimmte Bitfolge zuordnet. Wenn Programme erstellt werden, sind sie noch als Text formuliert, erst ein Compiler übersetzt diesen Text in eine Reihe von Befehlen, die der Rechner versteht, die so genannten Maschinenbefehle. So repräsentiert z.B. die Bytefolge „03 D8“ den Maschinenbefehl „ADD BX, AX“, welcher den Inhalt des Registers AX zu dem Inhalt von BX addiert. Solche Befehle werden im Abschnitt über Assemblerprogrammierung ab S. 486 erläutert.

1.3.8

Bilder und Musikstücke

Auch Bilder und Musikstücke können als Daten in einem Computer verarbeitet und gespeichert werden. Ein Bild wird dazu in eine Folge von Rasterpunkten aufgelöst. Jeden dieser Rasterpunkte kann man durch ein Bit, ein Byte oder mehrere Bytes codieren, je nachdem, ob das Bild ein- oder mehrfarbig ist. Eine Folge solcher Codes für Rasterpunkte repräsentiert dann ein Bild. Es gibt viele Standardformate für die Speicherung von Bildern, darunter solche, die jeden einzelnen Bildpunkt mit gleichem Aufwand speichern (dies nennt man eine Bitmap) bis zu anderen, die das gespeicherte Bild noch komprimieren. Dazu gehören das gif- und das jpeg-Format. Offiziell heißt dieses, von der joint photographic expert group (abgekürzt: jpeg) definierte Format jfif, doch die Bezeichnung jpeg ist allgemein gebräuchlicher. Das letztere Verfahren erreicht sehr hohe Kompressionsraten auf Kosten der Detailgenauigkeit – es ist i.A. verlustbehaftet, d.h. man kann das Original meist nicht wieder exakt zurückgewinnen. Bei Musikstücken muss das analoge Tonsignal zunächst digital codiert werden. Man kann sich das so vorstellen, dass die vorliegende Schwingung viele tausend mal pro Sekunde abgetastet

1.4 Zahlendarstellungen

17

wird. Die gemessene Amplitude wird jeweils als Binärzahl notiert und in der Datei gespeichert. Mit der mp3-Codierung, die gezielt akustische Informationen unterdrückt, welche die meisten Menschen ohnehin nicht wahrnehmen, können die ursprünglichen wav-Dateien auf ungefähr ein Zehntel ihrer ursprünglichen Größe in mp3-Dateien komprimiert werden.

1.4

Zahlendarstellungen

Wie alle bisher diskutierten Informationen werden auch Zahlen durch Bitfolgen dargestellt. Wenn eine Zahl wie z.B. „4711“ mitten in einem Text vorkommt, etwa in einem Werbeslogan, so wird sie, wie der Rest des Textes, als Folge ihrer ASCII-Ziffernzeichen gespeichert, d.h. als Folge der ASCII Zeichen für „4“, „7“, „1“ und „1“. Dies wären hier die ASCII-Codes mit den Nummern 52, 55, 49, 49. Eine solche Darstellung ist aber für Zahlen, mit denen man arithmetische Operationen durchführen möchte, unpraktisch und verschwendet unnötig Platz. Man kann Zahlen viel effizienter durch eine umkehrbar eindeutige (eins-zu-eins) Zuordnung zwischen Bitfolgen und Zahlen kodieren. Wenn wir nur Bitfolgen einer festen Länge N betrachten, können wir damit 2N viele Zahlen darstellen. Gebräuchlich sind N = 8, 16, 32 oder 64. Man repräsentiert durch die Bitfolgen der Länge N dann • • •

die natürlichen Zahlen von 0 bis 2N – 1, oder die ganzen Zahlen zwischen –2N–1 und 2N–1 – 1, oder ein Intervall der reellen Zahlen mit begrenzter Genauigkeit.

1.4.1

Binärdarstellung

Will man nur positive ganze Zahlen (natürliche Zahlen) darstellen, so kann man mit N Bits den Bereich der Zahlen von 0 bis 2N – 1, das sind 2N viele, überdecken. Die Zuordnung der Bitfolgen zu den natürlichen Zahlen geschieht so, dass die Bitfolge der Binärdarstellung der darzustellenden Zahl entspricht. Die natürlichen Zahlen nennt man in der Informatik auch vorzeichenlose Zahlen, und die Binärdarstellung heißt demzufolge auch vorzeichenlose Darstellung. Um die Idee der Binärdarstellung zu verstehen, führen wir uns noch einmal das gebräuchliche Dezimalsystem (Zehnersystem) vor Augen. Die einzelnen Ziffern einer Dezimalzahl stellen bekanntlich die Koeffizienten von Zehnerpotenzen dar, wie beispielsweise in 4711 = 4 × 10 3 + 7 × 10 2 + 1 × 10 1 + 1 × 10 0 = 4 × 1000 + 7 × 100 + 1 × 10 + 1 × 1 . Für das Binärsystem (Zweiersystem) hat man anstelle der Ziffern 0 ... 9 nur die beiden Ziffern 0 und 1 zur Verfügung, daher stellen die einzelnen Ziffern einer Binärzahl die Koeffizienten der Potenzen von 2 dar. Die Bitfolge 1101 hat daher den Zahlenwert: 1 × 2 3 + 1 × 2 2 + 0 × 2 1 + 1 × 2 0 = 1 × 8 + 1 × 4 + 0 × 2 + 1 × 1 = 13 .

18

1 Einführung

Dies können wir durch die Gleichung (1101)2 = (13)10 ausdrücken, wobei der Index angibt, in welchem Zahlensystem die Ziffernfolge interpretiert werden soll. Der Index entfällt, wenn das Zahlensystem aus dem Kontext klar ist. Mit drei Binärziffern können wir die Zahlenwerte von 0 bis 7 darstellen: 000 = 0, 100 = 4,

001 = 1, 101 = 5,

010 = 2, 110 = 6,

011 = 3 111 = 7.

Mit 4 Bits können wir analog die 16 Zahlen von 0 bis 15 erfassen, mit 8 Bits die 256 Zahlen von 0 bis 255, mit 16 Bits die Zahlen von 0 bis 65535 und mit 32 Bits die Zahlen von 0 bis 4 294 967 295.

1.4.2

Das Oktalsystem und das Hexadezimalsystem

Neben dem Dezimalsystem und dem Binärsystem sind in der Informatik noch das Oktalsystem und das Hexadezimalsystem in Gebrauch. Das Oktalsystem stellt Zahlen zur Basis 8 dar. Es verwendet daher nur die Ziffern 0 ... 7. Bei einer mehrstelligen Zahl im Oktalsystem ist jede Ziffer di der Koeffizient der zugehörigen Potenz 8i, also: n

( d n d n – 1 ... d 0 ) = d n × 8 + d n – 1 × 8 8 3

n–1

0

+ ... + d 0 × 8 ,

2

0

Beispielsweise gilt ( 4711 ) 8 = 4 × 8 + 7 × 8 + 1 × 8 + 1 × 8 = ( 2505 ) 10 . Ähnlich verhält es sich mit dem Hexadezimalsystem, dem System zur Basis 16. Die 16 Hexziffern 0,1,2,3,4,5,6,7,8,9,A,B,C,D,E,F drücken den Koeffizienten derjenigen Potenz von 16 aus, der ihrer Position entspricht. Beispielsweise stellt die Hexadezimalzahl 2C73 die Dezimalzahl 11379 dar, es gilt nämlich: 3

2

1

0

( 2C73 ) 16 = 2 × 16 + 12 × 16 + 7 × 16 + 3 × 16 = ( 11379 ) 10 . Prinzipiell könnte man jede beliebige positive Zahl, sogar 7 oder 13, als Basiszahl nehmen. Ein Vorteil des Oktal- und des Hexadezimalsystems ist, dass man zwischen dem Binärsystem und dem Oktal- bzw. dem Hexadezimalsystem ganz einfach umrechnen kann. Wenn wir von einer Binärzahl ausgehen, brauchen wir lediglich, von rechts beginnend, die Ziffern zu Vierergruppen zusammenzufassen und jeder dieser Vierergruppen die entsprechende Hexziffer zuzuordnen. Die resultierende Folge von Hexziffern ist dann die Hexadezimaldarstellung der Binärzahl. So entsteht z.B. aus der Binärzahl 10110001110011 die Hexadezimalzahl 2C73, denn (10110001110011)2= (10 1100 0111 0011)2 = (2C73)16. Gruppieren wir jeweils drei Ziffern einer Binärzahl und ordnen jeder Dreiergruppe die entsprechende Oktalziffer zu, so erhalten wir die Oktaldarstellung der Zahl. Mit demselben Beispiel wie eben also: (10110001110011)2= (10 110 001 110 011)2 = (26163)8.

1.4 Zahlendarstellungen

19

Warum dies so einfach funktioniert, lässt sich leicht plausibel machen. Sei dazu die Binärzahl ... b6 b5 b4 b3 b2 b1 b0 gegeben. Aus den Dreiergruppen können wir immer höhere Potenzen von 8 ausklammern: 6 5 4 3 2 1 0 ... + b × 2 + b × 2 + b × 2 + b × 2 + b × 2 + b × 2 + b × 2 = 6 5 4 3 2 1 0 2 2 1 0 1 2 1 0 0 ... + ( ... + b 6 ) × 8 +  b 5 × 2 + b 4 × 2 + b 3 × 2  × 8 +  b 2 × 2 + b 1 × 2 + b 0 × 2  × 8

In jeder Klammer befindet sich jetzt eine Zahl zwischen 0 und 7, also eine Oktalziffer. Insgesamt kann man die Darstellung als Oktalzahl auffassen. Genauso zeigt sich, dass die beschriebene Umwandlung zwischen Binärzahlen und Hexadezimalzahlen korrekt ist. Dabei muss man nur jeweils wachsende Potenzen von 16 ausklammern. Die Umwandlung, beispielsweise vom Oktal- in das Hexadezimalsystem oder umgekehrt, erfolgt dann am einfachsten über den Umweg des Binärsystems. Als Beispiel: (4711)8 = ( 100 111 001 001)2 = (1001 1100 1001)2 = (9C9)16.

1.4.3

Umwandlung in das Binär-, Oktal- oder Hexadezimalsystem

Die Umwandlung einer Binär-, Oktal- oder Hexadezimalzahl in das Dezimalsystem ist einfach. Man muss nur die Ziffern mit den entsprechenden Potenzen von 2, 8 oder 16 multiplizieren und die Ergebnisse aufsummieren. Bevor wir die etwas schwierigere Umwandlung von Dezimalzahlen in andere Zahlensysteme besprechen, wollen wir eine, auch für spätere Kapitel wichtige, zahlentheoretische Beobachtung vorwegschicken: Wenn man eine natürliche Zahl z durch eine andere natürliche Zahl d ≠ 0 exakt teilt, erhält man einen Quotienten q und einen Rest r. Beispielsweise gilt 39/8 = 4 Rest 7. Die Probe ergibt: 39 = 4 × 8 + 7. Der Rest ist immer kleiner als der Divisor d. In der Informatik bezeichnet man die Operation des Dividierens ohne Rest mit div und die Operation, die zwei Zahlen den Divisionsrest zuordnet, mit mod. Im Beispiel haben wir also 39 div 8 = 4 und 39 mod 8 = 7. Der Rest ist immer kleiner als der Divisor, also 0 ≤ ( z mod d ) < d

und die Probe muss die ursprüngliche Zahl ergeben: z = ( z div d ) × d + ( z mod d ) Für eine Zahl z im Dezimalsystem ist (z mod 10) gerade die letzte Ziffer und (z div 10) erhält man durch Streichen der letzten Ziffer : 4711 mod 10 = 1 und 4711 div 10 = 471. Dies gilt analog für alle Zahlensysteme. Insbesondere für die Binärdarstellung einer Zahl z ist z = ( b n b n – 1 ... b 1 b 0 ) = b n × 2 n + b n – 1 × 2 n – 1 + … + b 1 × 2 1 + b 0 . 2

20

1 Einführung

Klammern wir aus dem vorderen Teil die 2 aus, so erhalten wir z = ( b n × 2 n – 1 + bn – 1 × 2 n – 2 + … + b1 ) × 2 + b 0 . Dies können wir nun schreiben als z = ( b n b n – 1 ... b 1 ) × 2 + b 0 . 2 Somit ist die letzte Ziffer b0 der gesuchten Binärdarstellung gerade der Rest, der beim Dividieren durch 2 entsteht, also z mod 2, und die Folge der ersten Ziffern bnbn-1...b1 ist die Binärdarstellung von z div 2. Dies liefert sofort eine einfache Methode, die Binärdarstellung einer beliebigen natürlichen Zahl z zu finden. Bei fortgesetztem Teilen durch 2 ergeben die Reste nacheinander die Ziffern der Darstellung von z im Zweiersystem. Die Binärziffern entstehen dabei von rechts nach links. Wir zeigen dies beispielhaft für die Zahl 200910 = 111110110012: z

z div 2

z mod 2

2009 1004 502 251 125 62 31 15 7 3 1

1004 502 251 125 62 31 15 7 3 1 0

1 0 0 1 1 0 1 1 1 1 1

Abb. 1.3:

Berechnung der Binärdarstellung einer natürlichen Zahl

Alles, was über die Binärzahlen gesagt wurde, gilt sinngemäß auch in anderen Zahlensystemen. Verwandeln wir z.B. die dezimale Zahl 2009 in das Oktalsystem, so liefert fortgesetztes Teilen durch die Basiszahl 8 die Reste 3, 2, 7, 3. Genauer gilt: z

z div 8

z mod 8

2009 251 31 3

251 31 3 0

1 3 7 3

1.4 Zahlendarstellungen

21

Wir lesen an den Resten die Oktaldarstellung (3731)8 ab. Ganz entsprechend erhalten wir die Hexadezimaldarstellung durch fortgesetztes Teilen durch 16. Die möglichen Reste 0 bis 15 stellen wir jetzt durch die Hex-Ziffern 0 ... 9, A ... F dar und erhalten z

z div 16

z mod 16

2009 125 7

125 7 0

9 D 7

also die Hex-Darstellung 7D9. Zur Sicherheit überprüfen wir, ob beidesmal tatsächlich der gleiche Zahlenwert erhalten wurde, indem wir vom Oktalsystem in das Binär- und von dort ins Hexadezimalsystem umrechnen: (3731)8 = (011 111 011 001)2 = (0111 1101 1001)2 = (7D9)16.

1.4.4

Arithmetische Operationen

Zwei aus mehreren Ziffern bestehende Binärzahlen werden addiert, wie man es analog auch von der Addition von Dezimalzahlen gewohnt ist. Ein an einer Ziffernposition entstehender Übertrag wird zur nächsthöheren Ziffernposition addiert. Ein Übertrag entsteht immer, wenn bei der Addition zweier Ziffern ein Wert entsteht, der größer oder gleich dem Basiswert ist. Bei Binärziffern ist dies bereits der Fall, wenn wir 1+1 addieren. Es entsteht ein Übertrag von 1 in die nächste Ziffernposition. Es folgt ein Beispiel für die Addition zweier natürlicher Zahlen in allen besprochenen Zahlensystemen, dem Dezimalsystem, dem Binär-, Oktal- und Hexadezimalsystem. Wir haben die Beispiele so gewählt, dass immer an der zweiten und der dritten Position (von rechts) ein Übertrag entsteht. Man beachte, dass wir dezimal und oktal zwar die gleichen Ziffernfolgen addieren, es handelt sich aber nicht um die gleichen Zahlenwerte! Die Ergebnisse sind daher auch verschieden:

Dezimal: Dezimal:

Binär: Binär:

Oktal: Oktal:

Hexadezimal: Hexadezimal:

2 7 5 2 + 4 2 6 1

1 0 0 1 0 +1 0 0 1 1 1

2 7 5 2 + 4 2 6 1

2 7 C A + A F 9 3

7 0 1 3

1 1 1 0 0 1

7 2 3 3

D 7 5 D

Abb. 1.4:

Addition in verschiedenen Zahlensystemen

Auch Subtraktion, Multiplikation und Division verlaufen in anderen Zahlensystemen ähnlich wie im Dezimalsystem. Wir wollen dies hier nicht weiter vertiefen, sondern stattdessen die Frage ansprechen, was geschieht, wenn durch eine arithmetische Operation ein Zahlenbereich

22

1 Einführung

überschritten wird. Bei der Addition ist dies z.B. dadurch zu erkennen, dass in der höchsten Ziffernposition ein Übertrag auftritt. Dieser nicht mehr in das Zahlenformat passende Übertrag (engl. carry) wird von dem Prozessor zwar angezeigt, gleichwohl liegt als Ergebnis der Addition die durch die Addition entstandene Bitfolge bereit. Man sagt, dass der Prozessor die Carry-Flagge (engl. carry flag) setzt. So wie ein Rennfahrer manchmal glaubt, schneller sein zu können, wenn er die gelbe Flagge ignoriert, so kommt es oft vor, dass ein Programmierer oder auch eine Programmiersprache es versäumen, nachzuprüfen, ob die ausgeführte Operation ein gültiges Ergebnis geliefert hat. Dann wird mit der entstandenen Bitfolge weitergerechnet – vielleicht, weil man nicht an die Möglichkeit eines Fehlers gedacht hat, vielleicht weil man mit der (in den meisten Fällen unnötigen) Überprüfung keine Zeit verlieren will. Als Beispiel addieren wir zwei durch 8 Bit dargestellte natürliche Zahlen, wobei der Zahlenbereich 0 ... 255 überschritten wird. Der Übertrag wird nicht beachtet und die Zifferndarstellung ergibt einen falschen Wert: 197 + 213 ergibt 154!! Dies zeigt die folgende Rechnung: (197)10 + (213)10 = (C5)16 + (D5)16 = (19A)16. Der Übertrag in das (von rechts gesehen neunte) Bit geht verloren, und wir erhalten das inkorrekte Ergebnis (9A)16 = (154)10. Immerhin kann man sich bei einem solchen Fehler noch damit trösten, dass (197 + 213) mod 256 = 154.

1.4.5

Darstellung ganzer Zahlen

Als ganze Zahlen bezeichnet man die natürlichen Zahlen unter Hinzunahme der negativen Zahlen. Für die Kenntnis einer ganzen Zahl ist also nicht nur der absolute Zahlenwert nötig, sondern auch noch das Vorzeichen, „+“ oder „-“. Für diese zwei Möglichkeiten benötigen wir ein weiteres Bit an Information. Zunächst bietet sich eine Darstellung an, in der das erste Bit das Vorzeichen repräsentiert (0 für „+“) und (1 für „-“) und der Rest den Absolutwert. Diese Darstellung nennt man die Vorzeichendarstellung. Stehen z.B. 4 Bit für die Zahldarstellung zur Verfügung und wird ein Bit für das Vorzeichen verwendet, so bleiben noch 3 Bit für den Absolutwert. Mit 4 Bit kann man also die Zahlen von –7 bis +7 darstellen. Wir erhalten: 0000 = + 0 0001 = + 1 0010 = + 2 0011 = + 3

0100 = + 4 0101 = + 5 0110 = + 6 0111 = + 7

1000 = – 0 1001 = – 1 1010 = – 2 1011 = – 3

1100 = – 4 1101 = – 5 1110 = – 6 1111 = – 7

Bei näherem Hinsehen hat diese Darstellung aber eine Reihe von Nachteilen. Erstens erkennt man, dass die Zahl Null durch zwei verschiedene Bitfolgen dargestellt ist, durch 0000 und durch 1000, also +0 und –0. Zweitens ist auch das Rechnen kompliziert geworden.Um zwei Zahlen zu addieren, kann man nicht mehr die beiden Summanden übereinanderschreiben und schriftlich addieren.

+

–2 +5 +3

= = ≠

1010 + 0101 1111

1.4 Zahlendarstellungen

23

Es ließe sich natürlich eine Methode angeben, mit der man solche Bitfolgen trotzdem korrekt addieren kann. Es gibt aber eine bessere Darstellung von ganzen Zahlen, die alle genannten Probleme vermeidet und die wir im nächsten Abschnitt besprechen wollen.

1.4.6

Die Zweierkomplementdarstellung

Die Zweierkomplementdarstellung ist die gebräuchliche interne Repräsentation ganzer positiver und negativer Zahlen. Sie kommt auf sehr einfache Weise zu Stande. Wir erläutern sie zunächst für den Fall N = 4. Mit 4 Bits kann man einen Bereich von 24 = 16 ganzen Zahlen abdecken. Den Bereich kann man frei wählen, also z.B. die 16 Zahlen von –8 bis +7. Man zählt nun von 0 beginnend aufwärts, bis man die obere Grenze +7 erreicht, anschließend fährt man an der unteren Grenze –8 fort und zählt aufwärts, bis man die Zahl –1 erreicht hat.

-9 -8 -7 -6 -5 -4 -3 -2 -1 Abb. 1.5:

0

1

2

3

4

5

6

7

8

9

Zweierkomplementdarstellung

Auf diese Weise erhält man folgende Zuordnung von Bitfolgen zu ganzen Zahlen: 0000 = +0 0001 = +1 0010 = +2 0011 = +3

0100 = +4 0101 = +5 0110 = +6 0111 = +7

1000 = –8 1001 = –7 1010 = –6 1011 = –5

1100 = –4 1101 = –3 1110 = –2 1111 = –1

Jetzt erkennt man auch, wieso der Bereich von –8 bis +7 gewählt wurde und nicht etwa – 7 bis +8. Bei dem bei 0 beginnenden Hochzählen wird bei der achten Bitfolge zum ersten Mal das erste Bit zu 1. Springt man also ab der achten Bitfolge in den negativen Bereich, so hat man die folgende Eigenschaft: Bei den Zweierkomplementzahlen stellt das erste Bit das Vorzeichen dar. Den einer Bitfolge bn ... b0 zugeordneten Zahlenwert wollen wir mit (bn ... b0)z bezeichnen, also z.B. (0111)z = +7 und (1000)z = –8. Verfolgen wir die Bitfolgen und die zugeordneten Zahlenwerte, so sehen wir, dass diese zunächst anwachsen bis 2n-1 erreicht ist, alsdann fallen sie plötzlich um 2n und wachsen dann wieder an bis zu (1...1)z = –1. Für die positiven Zahlen von 0 bis 2n–1 stimmen daher Zweierkomplementdarstellung und Binärdarstellung überein, d.h.: (0 bn-1 ... b0)z = (0 bn-1 ... b0)2. Für die negativen Werte gilt

24

1 Einführung

hingegen (1 bn-1 ... b0)z = –2n + (0 bn-1 ... b0)2. Das erste Bit der Zweierkomplementdarstellung steht also für –2n, die übrigen Bits haben ihre normale Bedeutung daher gilt die Formel: (bn bn-1 ... b0)z = bn ×(–2n) + bn-1×2n-1+ ... + b1×21 + b0. 0000

1111 1110 1101

-1

0

0001 1

-2

0010 2

-3 Die Zahlen sind modulo 16 in natürlicher Reihenfolge

-4

1100

1011

4 5

-5

1010

0011

3

6

-6 -7 1001

-8

0100 0101

0110

7 0111

1000 Abb. 1.6:

Zweierkomplementdarstellung in einem Zahlenkreis

Die folgende Eigenschaft gibt den Zweierkomplementzahlen ihren Namen: Addiert man eine beliebige Ziffernfolge bnbn–1...b1b0 zu ihrem bitweisen Komplement (siehe S. 16), so erhält man stets 11...11. Diese stellt die Zahl (–2n) + 2n–1 + ... + 21 + 20 = –2n + (2n–1) = –1 dar. So erhält man zu der Zweierkomplementzahl bnbn–1...b1b0 die dazugehörige negative Zahl, indem man zu ihrem bitweisen Komplement 1 addiert. Da das Negative einer Zahl so einfach zu bilden ist, führt man die Subtraktion auf die Negation mit anschließender Addition zurück. Beispielsweise erhält man die Zweierkomplementdarstellung von –6, indem man zuerst die Binärdarstellung von +6, also 0110 bildet, davon das bitweise Komplement 1001, und zum Schluss 1 addiert. Also hat – 6 die Zweierkomplementdarstellung 1001+1=1010. Um (2 – 6) zu berechnen, addieren wir 2 + (– 6), also 0010 + 1010 = 1100. Das erste Bit zeigt, dass das Ergebnis eine negative Zahl ist. Ihren Betrag finden wir, indem wir das Zweierkomplement bilden, hier 0011, und 1 addieren. Der Betrag des Ergebnisses ist 0100, also 4. Folglich haben wir als Resultat: 2 – 6 = – 4. Auch bei der Addition (und der Subtraktion) von Zweierkomplementzahlen kann es Überschreitungen des gewählten Bereiches geben. Bei einer Darstellung durch N = n+1 Bits betrachten wir die Addition der Zweierkomplementzahlen anan–1...a0 und bnbn–1...b0. Ob das

1.5 Standardformate für ganze Zahlen

25

Ergebnis gültig ist, kann nur von den höchsten Bits an und bn und einem eventuellen Übertrag c von dem (n–1)-ten in das n-te Bit abhängen. Da der Übertrag c den Wert c × 2n = (–c)×(–2n) repräsentiert, ist das Ergebnis genau dann gültig, wenn 0 ≤ (an + bn – c) ≤ 1 gilt. Ist dies nicht der Fall, so setzt der Prozessor das Overflow-Flag. Was den Prozess der Addition angeht, spielt es keine Rolle, ob eine Bitfolge eine Binärzahl darstellt oder eine Zweierkomplementzahl. Der Prozessor benötigt also kein gesondertes Addierwerk für Zweierkomplementzahlen. Ob das Ergebnis aber gültig ist, das hängt wohl davon ab, ob die addierten Bitfolgen als Binärzahlen oder als Zweierkomplementzahlen verstanden werden sollen. Als Summe von Binärzahlen ist die Addition genau dann gültig, wenn kein Carry aus n-ten Bitposition entsteht, also wenn (an + bn + c) ≤ 1gilt, und als Summe von Zweierkomplementzahlen, wenn 0 ≤ (an + bn – c) ≤ 1 erfüllt ist. Die Addition der beispielhaften Bitfolgen 1101 und 0101 ergibt also in jedem Falle die Bitfolge 0010. Es gab einen Übertrag in die erste Stelle, also c = 1. Somit gilt an + bn + c = 2 und an + bn – c = 0. Als Summe der Binärzahlen 13 + 5 ist das Ergebnis 2 daher ungültig, als Summe von Zweierkomplementzahlen –3 + 5 ist das Ergebnis 2 gültig. Addiert man die Bitfolgen 0100 und 0101, so entsteht das Ergebnis 1001. Der Übertrag in die erste Stelle war c = 1. Als Summe der Binärzahlen 4 und 5 ist das Ergebnis 9 gültig. Als Summe der Zweierkomplementzahlen 4 und 5 ist das Ergebnis –7 ungültig, da an + bn – c = 0+0–1 = –1. Wandelt man Zweierkomplementzahlen in ein größeres Format um, also z.B. von 4-Bit nach 8 Bit, so muss man bei positiven Zahlen vorne mit Nullen auffüllen, negative aber mit 1-en. Beispielsweise ist –6 = (1001)z als 4-Bit Zahl, jedoch (1111 1001)z als 8-Bit-Zahl und (1111 1111 1111 1001)z als 16-Bit Zahl. In beiden Fällen entsteht durch Addition von 5 die Zahl –1 = (1111)z = (1111 1111)z = (1111 1111 1111 1111)z.

1.5

Standardformate für ganze Zahlen

Prinzipiell kann man beliebige Zahlenformate vereinbaren, in der Praxis werden fast ausschließlich Zahlenformate mit 8, 16, 32 oder 64 Bits eingesetzt. In den meisten Programmiersprachen gibt es vordefinierte ganzzahlige Datentypen mit unterschiedlichen Wertebereichen. Je größer das Format ist, desto größer ist natürlich der erfasste Zahlenbereich. Die folgende Tabelle zeigt Zahlenformate und ihre Namen, wie sie in den Programmiersprachen Delphi (vormals Turbo-Pascal) und Java vordefiniert sind. Durch die Wahl eines geeigneten Formats muss der Programmierer dafür sorgen, dass der Bereich nicht überschritten wird. Dennoch auftretende Überschreitungen führen häufig zu scheinbar gültigen Ergebnissen: So hat in Java die Addition 127+5 im Bereich byte das Ergebnis –124!

26

1 Einführung Bereich –128 ... 127

Format Delphi 8 Bit Shortint

Java byte

–32768 ... 32767

16 Bit Integer

short

–231 ... 231–1

32 Bit Longint

int

–263 ... 263–1 0 ... 255

64 Bit

long

0 ... 65535

1.5.1

8 Bit Byte 16 Bit Word

Gleitpunktzahlen: Reelle Zahlen

Mit beliebigen reellen Zahlen kann man in einem Computer nur näherungsweise rechnen – es sei denn, es handelt sich um ganze oder rationale Zahlen. Irrationale Zahlen, wie z.B. 2 oder die Kreiszahl π, kann man nur näherungsweise darstellen. Man könnte sich nun auf eine Genauigkeit mit einer festen Anzahl von Stellen hinter dem Komma festlegen, die genannten Beispiele würden dann etwa durch 1.414213 bzw. 3.141592 angenähert. Ein solches festes Format hätte aber den Nachteil, dass in gewissen Anwendungen, vielleicht in der Pharmazie, die Genauigkeit nicht ausreichen würde, in anderen Bereichen, etwa in der Astronomie, eine Genauigkeit von sechs Stellen hinter dem Komma unsinnig wäre. Daher suchen wir eine Darstellung, die bei festem Bitformat • •

ein möglichst großes Intervall der reellen Zahlen umfasst und deren Genauigkeit bei kleinen Zahlen sehr hoch, bei großen Zahlen niedriger ist.

Die Gleitpunktdarstellung (engl.: floating point) erfüllt diese beiden Forderungen. Die Idee ist ganz einfach: Kleine Zahlen benötigen wenige Stellen vor dem Dezimalpunkt, so dass wir ihnen viele Stellen hinter dem Punkt und damit eine größere Genauigkeit spendieren können. Bei großen Zahlen ist es umgekehrt. Somit benötigen wir für die Darstellung einer reellen Zahl als Gleitpunktzahl nicht nur die Ziffernfolge (Mantisse), sondern auch die Kommaposition. Dies ist aber gerade der Exponent in der technisch wissenschaftlichen Notation. So bedeutet zum Beispiel 384 × 106, dass der Dezimalpunkt in der Ziffernfolge 384 um 6 Positionen nach rechts geschoben werden muss. Dazu äquivalent sind die Darstellungen 3.84 × 108 oder 0.384 × 109. Für kleine Zahlen muss das Komma nach links geschoben werden, der Exponent wird dabei negativ wie in 1.74 × 10–31. Gleitpunktzahlen bestehen nach dem Gesagten aus drei Bestimmungsstücken: • • •

dem Vorzeichenbit: V, dem Exponenten: E, der Mantisse: M.

Das Vorzeichenbit gibt an, ob die vorliegende Zahl positiv oder negativ ist. Der Exponent ist eine Binärzahl, zum Beispiel im Bereich –64 bis +63, die angibt, mit welcher Potenz einer Basiszahl b die vorliegende Zahl zu multiplizieren ist. Meist wird b = 2 als Basiszahl verwendet. Die Mantisse besteht aus Binärziffern m1...mn und wird als

1.5 Standardformate für ganze Zahlen

27

m1 ⋅ 2 – 1 + m2 ⋅ 2 – 2 + … + mn ⋅ 2 –n interpretiert. Eine zur Basis 2 normierte Gleitpunktzahl ist eine solche, bei der der Exponent so gewählt wird, dass die Zahl in der Form ± 1. m 1 m 2 …m n ⋅ 2 E dargestellt werden kann. Bei der Verwendung derart normierter Gleitpunktzahlen braucht die 1 vor dem Punkt gar nicht mehr gespeichert zu werden, da sie immer da sein muss. Jede Gleitpunktzahl kann in eine normierte Gleitpunktzahl umgewandelt werden, weil eine Verschiebung der Bits um eine Stelle nach rechts bzw. links den Zahlenwert nicht ändert, wenn gleichzeitig der Exponent um 1 erhöht bzw. erniedrigt wird. Normierte Gleitpunktzahlen haben den Vorteil, dass die Mantissenbits optimal ausgenutzt werden, da keine überflüssigen Nullen gespeichert werden müssen. Eine normierte Gleitpunktzahl mit Vorzeichen V, Mantisse m1...mn und Exponent E stellt also den folgenden Zahlenwert dar: V

( –1 ) × ( 1 + m1 ⋅ 2

–1

+ ... + m n ⋅ 2

–n

)×2

E

.

Formal ist 0 so nicht repräsentierbar, daher wird die kleinste darstellbare Gleitpunktzahl als 0 interpretiert. Es folgen zwei Beispiele von 32-Bit-Gleitpunktzahlen, bei denen 23 Bits für die Mantisse verwendet werden: V

E

Mantisse

Zahlenwert

+

–13

01001010110101101111111 0.00015777567163622

-

+44

10101011000000000000000

–29343216566272

Bei der Auswahl einer Darstellung von Gleitpunktzahlen in einem festen Bitformat, etwa durch 32 Bit oder durch 64 Bit, muss man sich entscheiden, wie viele Bits für die Mantisse und wie viele für den Exponenten reserviert werden sollen. Die Berufsvereinigung IEEE (Institute of Electrical and Electronics Engineers) hat zwei Normen verabschiedet, die in den meisten Rechnern heute verwendet werden. short real: Vorzeichen: 1 Bit, Exponent: 8 Bit, Mantisse: 23 Bit long real : Vorzeichen: 1 Bit, Exponent: 11 Bit, Mantisse: 52 Bit. Zum Exponenten addiert man einen so genannten bias (engl. für Neigung, Vorurteil) von 127 und speichert das Ergebnis als vorzeichenlose 8-Bit-Zahl. Dies ist eine weitere Methode, um positive und negative Zahlen darzustellen. Sie wird im Falle von Gleitpunktzahlen angewendet, um Vergleiche zwischen verschiedenen Gleitpunktzahlen technisch besonders einfach zu machen. Für die obigen Beispiele erhält man damit als komplette Bitdarstellung im IEEE Short Real Format:

28

1 Einführung

V(1 Bit)

E (8 Bit)

Mantisse (23 Bit)

alle 32 Bit hexadezimal

0

01110010

01001010110101101111111

39256B7F

1

10101011

10101011000000000000000

D5D58000

Besser vertraut sind wir mit dezimalen Gleitpunktzahlen wie zum Beispiel: 0.12347123 × 10 – 5 und 0.874456780 × 10 19 . Dezimale Gleitpunktzahlen und binäre Gleitpunktzahlen kann man ineinander umrechnen. Dieser Umrechenvorgang geht aber in beiden Richtungen nicht immer auf, wenn wir jeweils eine bestimmte Zahl von Ziffern für die Mantisse vorschreiben. So lässt sich zum Beispiel die dezimale Zahl 0.1 nicht exakt durch eine binäre 32-Bit-Gleitpunktzahl darstellen. Die beiden 32-Bit-Gleitpunktzahlen, welche am nächsten bei 0.1 liegen, sind in der folgenden Tabelle aufgeführt: V(1 Bit)

E (8 Bit)

Mantisse (23 Bit)

Zahlenwert

0

01111011

10011001100110011001100

0.0999999940...

0

01111011

10011001100110011001101

0.1000000015...

Bereits beim Umrechnen dezimaler Gleitpunktzahlen in binäre Gleitpunktzahlen treten also Rundungsfehler auf. Weitere Ungenauigkeiten entstehen bei algebraischen Rechenoperationen. Schon beim Addieren von 0.1 zu 0.2 erhält man in Java: 0.1 + 0.2 = 0.30000000000000004 . Die Ungenauigkeit an der siebzehnten Nachkommastelle ist meist unerheblich, in einem Taschenrechner würde man sie nicht bemerken, weil sie in der Anzeige nicht mehr sichtbar wäre. Vorsicht ist dennoch beim Umgang mit Gleitpunktzahlen erforderlich, weil sich die Rechenungenauigkeiten unter ungünstigen Umständen verstärken können. Daher verwenden viele Rechner intern noch eine 80 Bit lange Darstellung von temporären Gleitkommazahlen. Eine Übersicht über alle besprochenen Gleitpunktformate gibt die folgende Tabelle: Bit

Vorzeichen Exponent Mantisse gültige Dezimalst. Bereich

von ... bis

32

1 Bit

8 Bit

23 Bit

~7

±1 × 10–38

±3 ⋅ 1038

64

1 Bit

11 Bit

52 Bit

~ 15

±1 × 10–308

±1 ⋅ 10308

80

1 Bit

15 Bit

64 Bit

~ 19

±1 × 10–4932 ±1 ⋅ 104932

1.5 Standardformate für ganze Zahlen

29

Im langen Gleitpunktformat hat die Dezimalzahl 1234711 die binäre Darstellung 0 100 0001 0011 0010 1101 0111 0001 0111 0000 0000 0000 0000 0000 0000 0000 0000. Nach dem Vorzeichen 0 folgen die 11 Bits 100 0001 0011 des Exponenten, welche binär den Wert 1043 darstellen. Im langen Gleitpunktformat muss davon nicht 127, sondern 1023 subtrahiert werden, um den tatsächlichen Exponenten 1043 – 1023 = 20 zu erhalten. Allgemein gilt: Werden für die Darstellung des Exponenten e Bits verwendet, so muss von der Binärzahl, die den Exponenten darstellt, ein bias von 2e–1–1 subtrahiert werden, um den wahren Exponenten zu ermitteln. Arithmetische Operationen mit Gleitkommazahlen sind erheblich aufwändiger als die entsprechenden Operationen mit Binärzahlen oder Zweierkomplementzahlen. Früher musste der Prozessor für jede Gleitkommaoperation ein gesondertes Unterprogramm starten. Später wurden Coprozessoren als gesonderte Bauteile entwickelt, die den Hauptprozessor von solchen aufwändigen Berechnungen entlasten sollen. Heute ist in allen gängigen Prozessoren eine FPU (von engl. floating point unit) integriert.

1.5.2

Real-Zahlenbereiche in Programmiersprachen

In den meisten Programmiersprachen gibt es mehrere Real-Datenbereiche mit unterschiedlichen Wertemengen. Diese sind jeweils endliche Teilbereiche der Menge der reellen Zahlen. Delphi und Java verwenden die folgenden Real-Datentypen: Bereich

Bytes

Delphi

Java

±2.9 E –39 ... 1.7 E 38

6 Real

±1.5 E –45 ... 3.4 E 38

4 Single

float

8 Double

double

±5.0 E –324 ... 1.7 E 308 ±3.4 E –4932 ... 1.1 E 4932

10 Extended

In einem Programmtext schreibt man konstante Werte entweder als Dezimalzahlen (z.B. 314.15) oder in Zehnerpotenzdarstellung (z.B. 3.14 × 102). Für die Darstellung in einem ASCII-Text gibt man letztere in der Notation „3.14 E 2“ an. „E“ soll an „Exponent“ erinnern. Diese Notation ist in der Norm IEEE 754 festgelegt und in vielen Programmiersprachen üblich. Statt des deutschen Kommas wird im Englischen (und daher in fast allen Programmiersprachen) der Dezimalpunkt verwendet.

30

1.5.3

1 Einführung

Daten – Informationen

Daten sind Folgen von Bits. Wenn man Daten findet, so kann man längst noch keine Information daraus extrahieren. Eine Folge von Bits oder Bytes hat für sich genommen keine Bedeutung. Erst wenn man weiß, wie diese Bytes zu interpretieren sind, erschließt sich deren Bedeutung und damit eine Information. Betrachten wir die Bitfolge 0100 0110 0111 0110

0100 0001 0011 1110

0110 0110 0111 0110

0101 1100 0100 0100

0111 0110 0010 0010

0010 0010 0000 0100 0010 1100 0010 0000 0110 1001 0000 0111 0010 0111 0101 1110,

so kann man diese zunächst als Folge von Bytes in Hex-Notation darstellen: 44 65 72 20 42 61 6C 6C 20 69 73 74 20 72 75 6E 64 2E. Ist eine Folge von 1-Byte-Zahlen im Bereich –128...127 gemeint, bedeutet dieselbe Folge: 68 101 114 32 ... , ist eine Folge von 2-Byte-Zahlen gemeint, so beginnt die gleiche Bitfolge mit 17509 29216 16993 ... . Als Text in ASCII-Codierung interpretiert, erkennen wir eine bekannte Fußballweisheit: „Der Ball ist rund.“ Wir stellen also fest, dass sich die Bedeutung von Daten erst durch die Kenntnis der benutzten Repräsentation erschließt. Betrachten wir Daten mithilfe eines Texteditors, so nimmt dieser generell eine ASCII-Codierung an. Handelt es sich in Wirklichkeit aber um andere Daten, etwa ein Foto, so wird manch ein Texteditor ebenfalls Buchstaben und Sonderzeichen anzeigen, es ist aber unwahrscheinlich, dass sich diese zu einem sinnvollen Text fügen. Nur ein Bildbetrachter interpretiert die Daten wie gewünscht.

Bytefolge Abb. 1.7:

ASCIIInterpretation

Die gleichen Daten ... verschieden interpretiert

jpgInterpretation

1.6 Hardware

1.5.4

31

Informationsverarbeitung – Datenverarbeitung

Die Interpretation von Daten nennt man, wie bereits ausgeführt, Abstraktion. Zu den elementaren Fähigkeiten, die der Prozessor, das Herz des Rechners, beherrscht, gehören das Lesen von Daten, die Verknüpfung von Daten anhand arithmetischer oder logischer Operationen und das Speichern von veränderten Daten im internen Hauptspeicher oder auf einem externen Medium. Die Tätigkeit des Rechners wird dabei ebenfalls durch Daten gesteuert, nämlich durch die Daten, die die Befehle des Programms codieren. Information

Informationsverarbeitung

Information

Repräsentation

Abstraktion Daten

Abb. 1.8:

Datenverarbeitung

Daten

Informationsverarbeitung und Datenverarbeitung

Information wird also durch Daten repräsentiert. Wenn wir Information verarbeiten wollen, so müssen wir die informationsverarbeitenden Operationen durch Operationen auf den entsprechenden Daten nachbilden. Informationsverarbeitung bedeutet demnach, dass Information zunächst auf Daten abgebildet wird, diese Daten verändert werden und aus den entstandenen Daten die neue Information abstrahiert wird.

1.6

Hardware

Hardware ist der Oberbegriff für alle materiellen Komponenten eines Computersystems. In diesem Kapitel werden wir die Hardware eines PCs samt Peripheriegeräten ansehen und untersuchen, wie diese funktionieren und kooperieren. Insbesondere werden wir uns mit dem Innenleben und dem Zusammenspiel der einzelnen Komponenten, Prozessor, Controller, Busse etc. befassen. Die Frage, wie diese Komponenten selber aus Transistoren, Schaltern und Gattern aufgebaut sind, verschieben wir jedoch auf das Kapitel über Rechnerarchitektur.

1.6.1

PCs, Workstations, Mainframes, Super-Computer

Bei Computersystemen unterscheiden wir zwischen Personal Computern (PCs), Workstations, Servern, Mainframes und Super-Computern. Bei PCs unterscheiden wir je nach Art der Anwendung Arbeitsplatzrechner oder Desktop-Systeme, die überwiegend stationär eingesetzt werden, und verschiedene Typen von mobilen PCs: Laptop-, Notebook-, Subnotebook- oder neuerdings auch Netbook-PCs PCs und Workstations wurden früher unterschieden im Hinblick auf Preis und Leistung, sie sind aber heute technisch gesehen und auch im Hinblick auf die Anwendbarkeit kaum zu unterscheiden. Daher wird der Begriff heute nur noch selten verwendet. Server sind spezielle PCs, die in

32

1 Einführung

einem Netzwerk dedizierte Dienste anbieten oder für besonders rechenintensive Aufgaben zu einem sogenannten Cluster zusammengeschaltet werden. Ein durchschnittlich ausgestatteter PC hat heute eine mit ca. 3 GHz getaktete 32-Bit oder 64Bit CPU, 2 GB Arbeitsspeicher, 500 GB Festplatte, eine Grafikkarte mit 256 MB eigenem Arbeitsspeicher und kostet mit einem 19" TFT-Flachbildschirm weniger als 1000 €. Workstations sind leistungsfähiger, aber auch entsprechend teurer als PCs. Wegen der zunehmenden Leistungsfähigkeit von PCs gibt es nur noch wenige Hersteller von Workstations. Dazu zählt die Firma SGI mit Produkten wie Silicon Graphics Prism™ Family of Visualization. Mainframes haben die Datenverarbeitung bis in die frühen 80er Jahre des letzten Jahrhunderts bestimmt. Im Gegensatz zu PCs und Workstations handelt es sich um Zentralrechner, mit denen hunderte oder gar tausende Benutzer gleichzeitig mittels Tastatur und Monitor oder über ein Datennetz verbunden sind. Auf einem Mainframe können gleichzeitig mehrere Betriebssysteme laufen, den Benutzern werden eigene virtuelle Rechner vorgegaukelt. Mainframes findet man als Zentralrechner in Banken, als Datenbankserver und generall dort wo riesige Datenmengen zu verwalten sind. In diesen Anwendungen lässt sich auch ein gewisses comeback der Mainframes beobachten. Was die reine Rechenleistung angeht, so können sie im Preis-Leistungsvergleich nicht mit Clustern von PCs oder Workstations mithalten. Die Preise für Mainframes beginnen bei einigen Hunderttausend Euro und liegen typischerweise bei mehreren Millionen. In der Wartung können Mainframes aber billiger sein als ein entsprechend mächtiges Cluster von Workstations. Führende Hersteller von Mainframes sind IBM, Hitachi und Fujitsu. Super-Computer sind die schnellsten Computer der Welt. Sie finden ihren Einsatz in extrem rechenintensiven Anwendungen, insbesondere im militärischen Bereich und in der Wissenschaft. Im Gegensatz zu Mainframes sind sie auf Rechenleistung statt auf Datendurchsatz optimiert. Ihre Leistung wird durch parallele Zusammenarbeit von mehrereren tausend Prozessoren erreicht. Die Schwierigkeit bei der Programmierung von Super-Computern besteht darin, die verfügbare Parallelität geschickt auszunutzen. Ihre Leistung wird in FLOPS (Fließkomma-Operationen pro Sekunde) gemessen. In der Rekordliste der nicht-militärischen Supercomputer lag bei der Erstellung der letzten Auflage dieses Buches der Earth-Simulator in Yokohama mit 35 Tera-FLOPS vorne. Er kostete ca. 55 Millionen €. Seit einigen Jahren sind Rechner der Serie BlueGene an der Spitze der Liste mit derzeit bis zu 478 TFlops. Ein noch im Aufbau befindliches System namens Roadrunner hat bereits jetzt eine Leistung von 1026 TFlops erbracht und damit die Peta-Flop Marke geknackt. Nähere Informationen kann man auf der Seite www.top500.org finden. Ursprünglich waren Personal Computer die einzigen Computer, die privat überhaupt bezahlbar waren. Dafür musste man Abstriche bei der Leistung in Kauf nehmen. Heute haben Personal Computer eine Leistung, die die Leistung des ersten von IBM im Jahre 1981 angebotenen PCs um viele Größenordnungen übertrifft. Dieser am 12. August 1981 angekündigte IBM-PC hatte in der Grundausstattung 16 kB Arbeitsspeicher, einen 4,77 MHz 8088-Mikroprozessor, ein Diskettenlaufwerk und kostete 3000.- US$. Er besaß weder Festplatte noch Grafikkarte.

1.6 Hardware

1.6.2

33

Aufbau von Computersystemen

Die folgenden Ausführungen beziehen sich vorwiegend auf Personal Computer.

Ein- und Ausgabegeräte

AusgabeGeräte

EingabeGeräte

Abb. 1.9:

Rechner und Peripherie

Ein Computersystem besteht aus dem Rechner und aus Peripheriegeräten. In der Abbildung sind bereits Tastatur und Maus als Eingabegeräte sowie Bildschirm und Drucker als Ausgabegerät dargestellt. Als weitere Eingabegeräte sind verbreitet: • • • •

Scanner zum Einlesen von Bildern, Mikrofon, Joystick oder ähnliche Geräte zur Bedienung von Spielen, Stift (Pen) als Universaleingabegerät.

Als Ausgabegeräte findet man häufig: • • •

Drucker, Plotter zur Ausgabe von großformatigen Stift-Zeichnungen, Lautsprecher/Kopfhörer.

Viele Peripheriegeräte werden typischerweise zur Ein- und zur Ausgabe verwendet. Einige davon sind bereits im Inneren des Rechnergehäuses fest eingebaut: • • • • • • • •

Diskettenlaufwerke (in neueren Rechnern kaum noch zu finden), CD-ROM-, DVD- bzw. Blu-ray-Laufwerke bzw. Brenner, Festplattenlaufwerke als zentrale Massenspeichergeräte, Streamer (z.B. Magnetbandlaufwerke) zur Sicherung von Daten, Videogeräte zum Überspielen und Bearbeiten von Videoclips, angeschlossen an eine serielle Firewire-Schnittstelle, (externe) Festplatten, weitere optische Laufwerke als zusätzliche Massenspeicher, Speicherkarten: USB Stick, Compact Flash (CF), Secure Digital Memory (SD, SDHC) Modem, ISDN-Karte, DSL-Modem etc. zur Kommunikation mit dem „Rest der Welt“.

34

1.6.3

1 Einführung

Der Rechner von außen

Die überwiegende Mehrzahl heutiger PCs sind „IBM-kompatible PCs“. Daher wollen wir hier den Aufbau eines derartigen PCs genauer unter die Lupe nehmen. Zunächst werden wir uns mit dem Äußeren des Rechners beschäftigen, dann mit seinem Innenleben. Die folgende Abbildung zeigt einen handelsüblichen PC von vorne und von hinten. An der Vorderseite erkennen wir die wichtigsten Bedienelemente wie Ein-Ausschaltknopf, Resetknopf und einige LCDs, die den Betriebszustand anzeigen. Außerdem befinden sich an der Vorderseite die Öffnungen für ein Diskettenlaufwerk und für Blu-ray, DVD- bzw. CD-ROM-Laufwerke. Netzbuchse für Stromversorgung Lüfter Buchsen für Maus und Tastatur

DVD / CD-ROM Brenner

Parallele Schnittstelle für Drucker

Diskettenlaufwerk

VGA Anschluss

Ein/Aus-Schalter

Firewire Schnittstelle 6 USB Schnittstellen

Kontrollleuchten Und Resetknopf

Netz (Ethernet) Audio Buchsen Grafikkarte mit zwei Anschlüssen für Bildschirme Abb. 1.10:

Klappe. Dahinter: USB Buchsen etc.

Der Rechner von hinten und vorne

An der Rückseite finden wir eine Reihe von Buchsen und Steckern, an die nicht nur das Stromkabel, sondern auch die externen Geräte wie Tastatur, Maus, Monitor, Drucker, Lautsprecher oder ein Netzwerk angeschlossen werden können. Zum Glück haben fast alle Buchsen unterschiedliche Formen, so dass ein falscher Anschluss der externen Geräte so gut wie ausgeschlossen ist. Lediglich Maus- und Tastaturanschluss könnten verwechselt werden, meist sind aber alle Buchsen farbig gekennzeichnet. Bei vielen neueren Computern wird auf spezielle Buchsen für Maus und Tastatur verzichtet; es stehen dann nur noch USB Schnittstellen zum Anschluss externer Geräte zur Verfügung.

1.6.4

Das Innenleben

Mit wenigen Handgriffen können wir das Gehäuse eines PC öffnen und das Innenleben in Augenschein nehmen. Auf jeden Fall muss vorher das Netzkabel entfernt worden sein! Nachdem einige Schrauben gelöst worden sind und die Gehäuseabdeckung entfernt wurde, bietet sich ein Bild ähnlich wie in Abb. 1.11. In einem Metallrahmen sitzen eine Reihe farbiger, meist grü-

1.6 Hardware

35

ner Platinen, auf denen schwarze oder metallene Chips verschiedener Formen und Größen aufgesteckt oder eingelötet sind.

Netzteil Stromanschlüsse Prozessor (CPU) mit Lüfter Karten Flachband kabel

Abb. 1.11:

1. Schacht für Laufwerke: Blu-ray, DVD, CD und Festplatten 2. Schacht für Laufwerke: Festplatten und Diskettenlaufwerk verdeckt: Hauptplatine (Motherboard)

Das Innere des Rechners

Außerdem befinden sich im Gehäuse ein Netzteil und Metallrahmen zum Einbau von Laufwerken. In diese Schächte können ein Diskettenlaufwerk, die Festplatte und Blu-ray bzw. DVD bzw. CD-ROM sowie weitere Laufwerke eingebaut werden. Die einzelnen Teile sind durch zusätzliche Kabel verbunden, darunter ein graues biegsames Flachbandkabel, das aus einer Vielzahl von parallelen Leitungen besteht. Es verbindet unter anderem die Laufwerke mit einigen der Platinen. Zentraler Bestandteil des Rechners ist die Hauptplatine, auch Mainboard oder Motherboard genannt. Es ist die größte der Platinen. Auf ihr finden wir verschiedene elektronische Bauelemente wie Chips, Widerstände und Kondensatoren sowie Steckleisten (Sockel) verschiedener Größen und Formen. Einige dieser Sockel sind leer, in anderen sind weitere Platinen (so genannte Karten) eingesteckt. Diese sitzen also senkrecht auf dem Motherboard. Viele der Buchsen, die wir auf der Rückseite des PCs erkannt haben, darunter die Buchsen für Tastatur, Maus, USB-Schnittstelle, serielle und parallele (Drucker-) Schnittstelle, sitzen in Wirklichkeit auf der hinteren Kante der Hauptplatine oder auf einer der Karten. Die bereits erwähnten Chips, meist als schwarze rechteckige Bauteile zu erkennen, enthalten Millionen von mikroskopisch kleinen elektronischen Elementen wie z.B. Transistoren, Widerstände und Kondensatoren, die durch mehrere Ebenen von Leiterbahnen innerhalb des Chips verbunden sind. Jeder Chip ist in ein kleines Kunststoffgehäuse eingegossen, aus dem die Anschlüsse, die so genannten Pins, herausragen.

36

1 Einführung

Die CPU ist bei einigen Rechnern als großer meist quadratischer Chip auf dem Motherboard auszumachen. Er steckt in einem Sockel mit hunderten von Anschlüssen. Gelegentlich trägt er noch einen Kühlkörper und/oder einen Lüfter „huckepack“. Der Prozessor eines Rechners erzeugt sehr viel Wärme, daher ist eines der auffälligsten Bauteile in einem Rechner das Kühlsystem für die CPU. Es verdeckt auf dem obigen Bild den eigentlichen Prozessor.

Abb. 1.12:

Prozessoren von Intel und AMD

Der Prozessor ist das Kernstück des Computers. Seine wichtigste Aufgabe ist es, arithmetische und logische Rechenoperationen auszuführen, Daten von außen entgegenzunehmen und neu erzeugte Daten nach außen abzugeben. Die eingehenden Daten können Tastatureingaben, Joystickbewegungen, Musik vom Mikrofon, Bildinformation vom Scanner oder Daten von der Festplatte sein. Die von der CPU ausgegebenen Daten steuern Drucker, Bildschirm, Modem und alle Ausgabegeräte. Allerdings kann die CPU nur Daten entgegennehmen, die bereits als Folgen von Bytes vorliegen. Die Wandlung von analogen Daten (Bildhelligkeit, Musik, Mausbewegungen) in digitale Bytefolgen, welche die CPU verarbeiten kann, geschieht durch gesonderte Bauteile, die sich zum Teil auf den bereits erwähnten Karten befinden, also den Platinen, die senkrecht in den Sockelleisten der Hauptplatine eingesteckt sind. Auf diesen Karten, bzw. in Chips, die auf dem Motherboard integriert sind, befindet sich also Elektronik zur Steuerung der Peripherie. Daher werden sie auch häufig als Controller bezeichnet. Ein Plattencontroller z.B. dient zur Steuerung von Disketten- und Festplattenlaufwerken. Im Prinzip ist es die Aufgabe des Controllers, die Anfragen oder Befehle des Prozessors in Aktionen des angeschlossenen Gerätes umzusetzen. Wenn z.B. der Prozessor Daten von der Festplatte lesen will, so muss der Festplattencontroller dafür sorgen, dass sich die Platte dreht, dass der Lesekopf an die richtige Stelle bewegt wird und dass die dort gefundenen Schwankungen des Magnetfeldes in Bitfolgen übersetzt und so dem Prozessor geliefert

1.6 Hardware

37

werden. Auf diese Weise ist es auch möglich, dass Peripheriegeräte verschiedener Hersteller gegeneinander ausgetauscht werden können. Der Controller muss lediglich in der Lage sein, die Wünsche des Prozessors umzusetzen. Ein Grafikcontroller, andere Namen sind Grafikkarte oder Grafikadapter, dient zur Ansteuerung des Bildschirms. Eine Grafikkarte besitzt einen eigenen Speicher und einen eigenen Grafikprozessor, so dass die Aktualisierung und Neuberechnung des Bildschirminhalts, insbesondere bei Spielen, dezentral und schnell geschehen kann. Eine Soundkarte ist ein Controller, der für die Ansteuerung von Lautsprechern und Mikrofon zuständig ist. Er muss unter anderem Tonschwingungen digitalisieren und umgekehrt auch digitale Toninformationen in analoge Schwingungen der Lautsprechermembran umsetzen. Die meisten Controller benötigen keine gesonderte Karte mehr, sie sind direkt auf dem Motherboard oder auch in dem betreffenden Gerät angebracht. Viele der heute üblichen Motherboards benötigen überhaupt keine zusätzliche Karte mehr. Meist findet man auch bereits einen Grafikadapter auf der Hauptplatine, allerdings wir von vielen Nutzern trotzdem eine leistungsfähigere Grafikkarte genutzt. Auf der hinteren Kante von Karten, die externe Geräte (Lautsprecher/Mikrofon, Monitor, Telefon/Netz) steuern, befinden sich jeweils die Buchsen für die entsprechenden Geräte. Auf der Rückseite des PCs sind an den betreffenden Stellen Öffnungen gelassen. Die Karten stecken, wie bereits erwähnt, in Sockelleisten, den so genannten Slots, auf der Hauptplatine. Die Slots sind untereinander und mit der Hauptplatine durch einen so genannten Bus (auch Systembus genannt) verbunden. Hierbei handelt es sich um eine Serie paralleler Datenleitungen, über die Daten zwischen Prozessor und Peripheriegeräten ausgetauscht werden. Busse gibt es mit unterschiedlich vielen Leitungen und mit unterschiedlichen Protokollen (Standards, Konventionen) zur Übertragung von Daten über diese Leitungen. Die Leistungsfähigkeit eines Busses wird an der Anzahl von Bytes gemessen, die in einer Sekunde übertragen werden können. Moderne Busse schaffen einige GByte pro Sekunde. Da an dem Bus viele unabhängig und gleichzeitig arbeitende Peripheriegeräte angeschlossen sind, muss auch der Buszugang durch einen Controller (den Buscontroller) geregelt werden. Die Standards für Technik und Datenübertragung des Busses haben sich in den letzten Jahren sehr verändert. Einige dieser Standards heißen ISA (industry standard architecture), EISA, Local Bus, PCI (peripheral component interconnect) oder AGP (accelerated graphics port). Mit den Standards änderten sich auch die Dimensionen der Steckplätze. PCIExpress ist der Nachfolger von PCI und AGP und bietet eine höhere Datenübertragungsrate. Im Gegensatz zum PCI-Bus ist PCI-Express kein paralleler Bus, sondern eine serielle Punkt-zuPunkt-Verbindung, aber trotzdem kompatibel zu den Vorgängern. Schließlich erkennen wir auf dem Mainboard noch einige Sockelleisten, in denen kleine längliche Karten eingesteckt sind. Es handelt sich um so genannte SIMMs, DIMMs, oder RIMMs, Karten mit jeweils 8 oder 9 gleichartigen Speicherchips (memory modules), die zusammen den Hauptspeicher bilden. Dieser ist direkt mit dem Memory-Controller verbunden, da der Umweg über den Bus den Speicherzugriff verlangsamen würde. Als letzte wichtige Bestandteile des Motherboards seien noch der BIOS-Chip (basic input output system) und der Taktgeber erwähnt. Dem BIOS kommt insbesondere beim Start des Rech-

38

1 Einführung

ners eine entscheidende Bedeutung zu, während der Taktgeber für die Synchronisation der Komponenten untereinander notwendig ist. In dem folgenden Grobschaltbild erkennt man auch, dass Prozessor und Hauptspeicher direkt über den Memory-Controller verbunden sind, während die Verbindung zu allen anderen Komponenten über den Bus und die Controller führt.

BIOS

Uhr

Cache/ MemoryController

Grafik-Karte

Prozessor

CPU

Slots

Sound-Karte

PCIController

ISDN-Karte Div. Controller: Laufwerke

Hauptspeicher Prozessor-Hauptspeicher Subsystem

Abb. 1.13:

1.6.5

BUS

Schnittstellen: seriell, parallel USB, Firewire

Komponenten eines Rechners

Ein Motherboard

Dem prinzipiellen Aufbau wollen wir nun das Foto eines Motherboards (siehe Abb. 1.14.) gegenüberstellen, so wie Sie es vielleicht vorfinden, wenn Sie Ihren Rechner öffnen. Es handelt sich hier um das DQ35JO Motherboard der Firma Intel. Es ist für Prozessoren vom Typ Core 2 Duo und Core 2 Quad geeignet, die einen LGA 775 CPU-Sockel benötigen. Vier Sockel können Speicherkärtchen aufnehmen. Von der Form her werden sie als DIMM (dual inline memory module) bezeichnet. Die Speicherbausteine müssen vom Typ DDR2-SDRAM800 sein. Man steckt diese mit leichtem Druck in den Sockel, wobei sie einrasten. Verwendet man DIMMs mit jeweils 1 oder 2 GB Speicherkapazität, kann der Hauptspeicher auf bis zu 8 GB ausgebaut werden. Die Speicherchips werden auch als RAM bezeichnet. Diese Abkürzung steht für „random access memory“ und soll andeuten, dass man direkt, durch Angabe ihrer Adresse, auf eine beliebige Speicherzelle zugreifen und dabei deren Inhalt lesen oder auch verändern kann. Auf dem Foto erkennt man deutlich eine weiße und drei dunklere Steckplätze (slots), quasi die „Haltestellen auf der Buslinie“, in die die Controllerkarten eingesteckt werden. Bei dem Bus handelt es sich um den konventionellen PCI-Bus und den neueren PCI-Express-Bus, in zwei Varianten: PCI Express x1 und x16. Auf dem Motherboard ist bereits ein Grafikadapter integriert, man kann aber auch eine leistungsfähigere Grafikkarte in den längeren PCI Express x16 Slot stecken. Auf dem Motherboard kann man keine langen Steckplätze für den mittlerweile veralteten ISA-Bus mehr finden.

1.6 Hardware

39

Chip-Satz: Memory Controller IO Controller,…

Schnittstellen (teilweise verdeckt) Sound, Netz,USB

PCI Express Slots PCI Slot

LGA 775 CPU-Sockel für Core 2 Duo Prozessor

Stecker für IDE Geräte Batterie

4 Sockel für DDR2-SDRAM (DIMMs)

6 Serial ATA Anschlüsse Krypt. Chip Abb. 1.14:

Raid Controller

NetzAnschluss

Ein Motherboard

Das BIOS ist in einem 32 MB Flashspeicher untergebracht, der bei diesem Motherboard aber auf dem Bild nicht identifizierbar ist. In diesem Speicher befinden sich elementare Ein-Ausgabe-Programme sowie Routinen, die sofort nach dem Einschalten des Rechners ausgeführt werden. Sie testen die angeschlossenen Hardwarekomponenten und laden anschließend das Betriebssystem. Eigentlich bedeutet ROM soviel wie read-only-memory, also Speicher, der nur gelesen werden kann. Das Flash-ROM kann in der Tat nicht byteweise verändert werden wie das RAM, eine gleichzeitige Neubeschreibung ganzer Speicherbereiche ist aber möglich. Auf diese Weise kann man gelegentlich neue BIOS-Versionen von der homepage des Herstellers laden und den Chip damit aktualisieren. Anders als bei RAM-Chips bleibt der Inhalt des ROM auch nach dem Abschalten des Rechners erhalten. Die Batterie oder der Akku auf dem Motherboard ist daher nur für die Versorgung des Taktgebers (Timer) nötig, damit auch nach dem Abschalten der Rechner die aktuelle Uhrzeit behält. Auf der rückwärtigen Kante der Hauptplatine und der anderen Karten befinden sich die Buchsen (Schnittstellen) für extern anzuschließende Geräte wie Tastatur, Bildschirm, Maus, Netzwerk und Drucker. Sie sind über eine Öffnung in der Rückwand des Gehäuses von außen zugänglich. Bei heutigen Rechnern findet man fast nur noch Buchsen für die USB-Schnittstelle (Universal Serial Bus). Dabei handelt es sich um eine Technologie, die vor einiger Zeit eingeführt wurde und mit der fast alle externen Geräte angeschlossen werden können. Das Hinzufügen eines neuen Gerätes, auch im laufenden Betrieb, ist an einer USB-Schnittstelle so einfach wie das Anschließen eines Toasters, einer Lampe oder einer Mikrowelle an das Stromnetz. Wenn das

40

1 Einführung

Betriebssystem mitspielt, nennt man das „plug and play“, früher wurde der Slogan oft als „plug and pray“ verballhornt. Firewire

Anschlüsse für Bildschirm Abb. 1.15:

1.6.6

Netzwerk

6 x USB

Audio

Teil der rückwärtigen Kante eines Motherboards mit externen Schnittstellen

Die Aufgabe der CPU

Der Prozessor ist das Kernstück eines Computers. Er dient der Verarbeitung von Daten, die sich in Form von Bytes im Speicher des Rechners befinden. Daher rührt auch die Bezeichnung CPU (central processing unit). Zwei wesentliche Bestandteile der CPU sind Register und ALU (arithmetical logical unit). Die ALU ist eine komplizierte Schaltung, welche die eigentlichen mathematischen und logischen Operationen ausführen kann. In Kapitel 5, ab S. 470, werden wir eine prototypische ALU entwerfen. Register sind extrem schnelle Hilfsspeicherzellen, die direkt mit der ALU verbunden sind. Die ALU-Operationen erwarten ihre Argumente in bestimmten Registern und liefern ihre Ergebnisse wieder in Registern ab. Auch der Datentransfer vom Speicher zur CPU läuft durch die Register. Die Aufgabe der CPU ist es, Befehle zu verarbeiten. Heutige CPUs haben ein Repertoire von einigen hundert Befehlen. Die meisten davon sind Datentransferbefehle und Operationen, also Befehle, die Registerinhalte durch arithmetische oder logische Befehle verknüpfen. Typische Befehle sind: • • • • • • •

LOAD: Laden eines CPU-Registers mit einem Wert aus dem Speicher STORE: Speichern eines Registerinhalts in einen Speicherplatz des Speichers ADD, SUB, MUL, DIV: Arithmetische Operationen auf Registern NOT, OR, AND, XOR: Logische Befehle auf Registern COMPARE: Vergleich des Inhalts zweier Register MOVE: Verschiebung von ganzen Datenblöcken im Speicher OUT, IN: Ein- und Ausgabe von Daten an Register der Peripheriegeräte

Normalerweise werden Befehle in der im Speicher vorliegenden Reihenfolge der Opcodes ausgeführt. Sprungbefehle (BRANCH bzw. JUMP) erlauben aber auch die Fortsetzung der Verarbeitung an einer anderen Stelle, je nachdem ob eine vorher getestete Bedingung erfüllt ist oder nicht. Dabei wird einfach der Befehlszähler auf eine andere Position im Programm umgelenkt, statt ihn, wie im Normalfall, um eins zu erhöhen.

1.6 Hardware

41

Zur Erläuterung zeigen wir hier ein einfaches Beispiel eines Programms für den IBM-PC. Es dient dazu, die Zahlen von 1 bis 1000 zu addieren und das Ergebnis in der Speicherzelle Nummer 403000h abzulegen. Wir benutzen zwei Register mit Namen EAX und EBX, die mit 0 bzw. 1000 initialisiert werden. Der Befehl add EAX, EBX addiert den Inhalt von EBX zu EAX, und dec EBX erniedrigt den Inhalt von EBX um 1. Der Sprungbefehl JNZ schleife (JumpNotZero) prüft, ob das Ergebnis der letzten Operation 0 war. Wenn nicht, springt der Befehl an die angegebene Stelle im Programm – hier an die mit „schleife:“ markierte Position. Schließlich speichert mov [403000h], EAX den Inhalt von EAX in die Speicherzelle mit Hex-Adresse 403000h. Der Text von einem Semikolon bis zum Zeilenende ist jeweils ein erläuternder Kommentar, der von dem Programm aber ignoriert wird.

Abb. 1.16:

Ein Maschinenprogramm – im Klartext

Jeder Befehl besitzt einen OpCode, den man sich vereinfacht als Befehlsnummer vorstellen kann. So hat zum Beispiel der Befehl dec EBX die Nummer 4Bh, der Befehl mov EBX die Nummer BBh. Ein Assembler übersetzt den obigen Programmtext in eine Folge von Opcodes mit ihren Argumenten. Der Befehl mov EBX, 1000 wird dabei in die fünf Bytes „BB E8 03 00 00“ übersetzt. Davon stehen die letzten 4 Bytes für das 32-Bit Wort (00 00 03 E8)16=(1000)10. Offensichtlich wird also das vier-Byte Wort „00 00 E8 03“ in umgekehrter Reihenfolge low-Byte-high-Byte im Speicher abgelegt. Diese Konvention der 80x86 CPUFamilie nennt man auch Little-Endian.

Abb. 1.17:

Das Maschinenprogramm und seine interne Darstellung

Bei der Übersetzung werden symbolische Sprungmarken, wie in unserem Beispiel die Marke „schleife:“ durch ihre Position im Speicher ersetzt. Genauer wird „jnz schleife“ übersetzt in „75 FB“, wobei 75h der Opcode für jnz (JumpNotZero) ist und (FB)16= -5 einen Rücksprung um 5 Byte veranlasst. Die folgende Abbildung zeigt einen Blick in den Hauptspeicher ab Adresse

42

1 Einführung

401000h. Rechts erkennen wir den um Kommentare und symbolische Sprungadressen bereinigten Quellcode, in der zweiten Spalte von links den fertigen Maschinencode in Hex-Darstellung und am linken Rand die Speicheradressen, an denen die Codebytes sich befinden. Ein direkter Zugriff auf eine Speicherzelle, wie oben geschildert, ist unter einem Multitasking-Betriebssystem wie Windows oder Linux allerdings nicht mehr erlaubt, denn die Speicherzelle könnte von einem anderen, gleichzeitig laufenden Prozess benutzt werden und diesen zum Absturz bringen. Dennoch kann man auch unter solchen Betriebssystemen in Assembler programmieren, allerdings ist der Zugriff auf die Rechnerhardware nur in Zusammenarbeit mit dem Betriebssystem möglich. Wir werden dies in dem Kapitel über Assemblerprogrammierung ab S. 486 näher erläutern. Das als Folge von Opcodes vorliegende Programm wird in einer Datei gespeichert, die in der PC-Welt meist eine der Endungen „.com“, „.exe“, „.dll“ trägt. Wenn ein Programm ausgeführt werden soll, wird diese Programmdatei in den Speicher geladen. Die CPU übernimmt die Kontrolle, liest die Opcodes in der Datei und führt die entsprechenden Befehle aus. Der Befehlszähler zeigt immer auf den als Nächstes auszuführenden Opcode. Die CPU durchläuft dabei immer wieder den sogenannten Load-Increment-Execute-Zyklus: LOAD

( Lade den Opcode, auf den der Befehlszähler zeigt )

INCREMENT ( Erhöhe den Befehlszähler ) EXECUTE

( Führe den Befehl, der zu dem Opcode gehört, aus )

Diese stark vereinfachte Darstellung mag an dieser Stelle genügen, sie wird im Kapitel über Rechnerarchitektur näher erläutert.

1.6.7

Die Organisation des Hauptspeichers

Im Hauptspeicher oder Arbeitsspeicher eines Computers werden Programme und Daten abgelegt. Die Daten werden von den Programmen bearbeitet. Der Inhalt des Arbeitsspeichers ändert sich ständig – insbesondere dient der Arbeitsspeicher nicht der permanenten Speicherung von Daten. Fast immer ist er aus Speicherzellen aufgebaut, die ihren Inhalt beim Abschalten des Computers verlieren. Beim jedem Einschalten werden alle Bits des Arbeitsspeichers auf ihre Funktionsfähigkeit getestet und dann auf 0 gesetzt. Die Bits des Arbeitsspeichers sind byteweise organisiert. Jeder Befehl kann immer nur auf ein ganzes Byte zugreifen, um es zu lesen, zu bearbeiten oder zu schreiben. Den 8 Bits, die ein Byte ausmachen, kann noch ein Prüfbit beigegeben sein. Mit dessen Hilfe überprüft der Rechner ständig den Speicher auf Fehler, die z.B. durch eine Spannungsschwankung oder einen Defekt entstehen könnten. Das Prüfbit wird also nicht vom Programmierer verändert, sondern automatisch von der Speicherhardware gesetzt und gelesen. Meist setzt diese das Prüfbit so, dass die Anzahl aller Einsen in dem gespeicherten Byte zusammen mit dem Prüfbit geradzahlig (engl. even parity) wird, daher heißt das Prüfbit auch Parity-Bit. Ändert sich durch einen Defekt oder einen Fehler genau ein Bit des Bytes oder das Parity Bit, so wird dies von der Speicherhardware erkannt, denn die Anzahl aller Einsen wird ungerade. Die Verwendung von

1.6 Hardware

43

Prüfbits verliert allerdings seit einiger Zeit wegen der höheren Zuverlässigkeit der Speicherbausteine an Bedeutung. Jedes Byte des Arbeitsspeichers erhält eine Nummer, die als Adresse bezeichnet wird. Die Bytes eines Arbeitsspeichers der Größe 128 MB haben also die Adressen: 0, 1, 2, ... (227 – 1) = 0, 1, 2, ... 134 217 727. Adressen:

0

1

8 Datenbits

Abb. 1.18:

2

3

Prüfbit

Speicher mit Prüfbits

Auf jedes Byte kann direkt zugegriffen werden. Daher werden Speicherbausteine auch RAMs genannt (random access memory). Dieser Name betont den Gegensatz zu Speichermedien, auf die nur sequentiell (Magnetband) oder blockweise (Festplatte) zugegriffen werden kann. Technisch wird der Arbeitsspeicher heutiger Computer aus speziellen Bauelementen, den Speicherchips, aufgebaut. Diese werden nicht mehr einzeln, sondern als Speichermodule angeboten. Dabei sind jeweils mehrere Einzelchips zu so genannten SIMM oder DIMMModulen(single/dual inline memory module) zusammengefasst. Das sind 168-Pin Mini-Platinen, auf denen jeweils 8 oder 9 gleichartige Speicherchips sitzen.

1 GBit DRAM 1 GBit DRAM

K o n t a k t e

Ein SIMM

1 GBit DRAM 1 GBit DRAM 1 GBit DRAM

Durch SIMMs realisierter Hauptspeicher

1 GBit DRAM 1 GBit DRAM 1 GBit DRAM 1 GBit DRAM

Abb. 1.19:

Speichermodule (Schema)

Üblich sind heute Speicherchips mit 256 MBit, 512 MBit, 1 GBit oder 2 GBit Speicherkapazität. 4 GBit Chips werden nicht mehr lange auf sich warten lassen. 8 Chips mit jeweils 1 GBit auf einem SIMM- oder DIMM-Modul ergeben eine Hauptspeichergröße von 1 GByte.

44

1 Einführung

Auf einem typischen Motherboard befinden sich meist Steckplätze für 4 DIMM-Module. Befinden sich 9 Chips auf einem DIMM, so dient der neunte Chip zur Paritätsprüfung. Es gibt zwei Arten von Speicherzellen: DRAMs und SRAMs. Dabei steht das D für „Dynamic“ und das S für „Static“. DRAMs sind langsamer als SRAMs, dafür aber billiger. Man verwendet daher häufig zwei Speicherhierarchien: DRAM für den eigentlichen Arbeitsspeicher und SRAM für einen Pufferspeicher zur Beschleunigung des Zugriffs auf den Arbeitsspeicher. Für diesen Pufferspeicher ist die Bezeichnung Cache gebräuchlich (siehe auch S. 37). Heute kommt meist eine verbesserte Version von DRAM, das so genannte SDRAM (synchronous dynamic RAM) zum Einsatz, dessen Taktrate optimal auf die CPU abgestimmt ist. Weiterentwickelt wurde dieser Speichertyp als DDR2-SDRAM (wobei DDR für Double Data Rate steht) und als DDR3-SDRAM. Auf dem folgenden Bild ist ein neueres DDR2-5300 Modul abgebildet, der für das oben gezeigte Motherboard geeignet ist.

Abb. 1.20:

DDR2-5300 SDRAM-DIMM Modul mit 1 GB: Vorder- und Rückseite

CPU und Hauptspeicher sind nicht durch den vorher diskutierten Systembus verbunden, sondern haben einen eigenen internen Bus, der sich aus einem Adressbus und einem Datenbus zusammensetzt. Über den Adressbus übermittelt die CPU eine gewünschte Speicheradresse an den Hauptspeicher, über den Datenbus werden die Daten zwischen CPU und Hauptspeicher ausgetauscht. Der Datenbus der ersten IBM-PCs hatte 20 Adressleitungen. Jede Leitung stand für eine Stelle in der zu bildenden binären Adresse. Somit konnte man nur maximal 220 verschiedene Adressen bilden, also höchstens 1 MByte Speicher adressieren. Heutige PCs haben typischerweise mindestens 1 bis 4 GB Speicher, so dass eine Adresslänge von 32 Bit benötigt wird. Für den zukünftigen weiteren Ausbau des Speichers werden mehr als 32 Bit zur Adressierung benötigt. Man kann davon ausgehen, das Rechner demnächst eine Adressbreite von 36, 48 oder 64 Bit nutzen werden. Mit 64 Bits kann auf absehbare Zeit mehr als genung Speicher adressiert werden. Adressbits

max. Speichergröße

16

64 kB

20

1 MB

24

16 MB

32

4 GB

64

16 ExaByte

Zur Kennzeichnung von Computern werden oft die Bezeichnungen 8-Bit-Computer, 16-BitComputer, 32- oder 64-Bit-Computer verwendet. Tatsächlich sind in diesem Zusammenhang aber mindestens drei unterschiedliche Kenngrößen D, S und A von Interesse:

1.6 Hardware

45

D: Die Anzahl der Datenbits, die von dem verwendeten Mikroprozessor in einem arithmetischen Befehl verknüpft werden können, S: Die Anzahl der Bits, die von einem LOAD- oder STORE- Befehl gleichzeitig zwischen dem Speicher und einem CPU-Register transportiert werden können, A: Die Anzahl der Bits, die zur Adressierung verwendet werden können. Ggf. werden physikalische und virtuelle Adressen unterschieden. Im Folgenden seien einige Beispiele aufgeführt: Prozessor

D

S

A

8080/8085

8

8

16

8088

16

8

20

8086/80186

16

16

20

80286

16

16

24

80386/80486

32

32

32

68000/68010

32

16

24

68020/68030/68040/68060

32

32

32

PowerPC

32

64

32

Pentium (I, II, III, 4)

32

64

32

Itanium 2

64

64

50/64

Opteron und Athlon 64

64

64

40/48

Neuere Pentium-4 und Core 2 Duo

64

64

36/48

1.6.8

Speichermedien

Der Arbeitsspeicher eines Rechners verliert seinen Inhalt, wenn er nicht in regelmäßigen Abständen (z.B. alle 15 µs) aufgefrischt wird. Insbesondere gehen alle Daten beim Abschalten des Computers verloren. Zur langfristigen Speicherung werden daher andere Speichertechnologien benötigt. Für diese Medien sind besonders zwei Kenngrößen von Bedeutung – die Speicherkapazität und die Zugriffszeit. Darunter versteht man die Zeit zwischen der Anforderung eines Speicherinhaltes durch den Prozessor und der Lieferung der gewünschten Daten durch die Gerätehardware. Da die Zugriffszeit von vielen Parametern abhängen kann, z.B. von dem Ort, an dem die Daten auf dem Medium gespeichert sind, bei Festplatten oder Disketten auch von der gegenwärtigen Position des Lesekopfes, spricht man lieber von einer mittleren Zugriffszeit. Magnetbänder haben zum Beispiel eine hohe Kapazität (mehrere GByte) und sind zudem sehr billig, dafür ist die mittlere Zugriffszeit extrem lang, insbesondere weil man auf die Daten nicht direkt zugreifen kann. Man muss das Band bis zu der gesuchten Stelle vor- oder zurückspulen, daher spricht man von einem sequentiellen Zugriff. Auf Daten, die auf Disketten, Festplatten oder CD-ROMs

46

1 Einführung

gespeichert sind, kann demgegenüber blockweise zugegriffen werden. Dies bedeutet, dass zum Lesen eines bestimmten Bytes immer der ganze Speicherblock (z.B. 512 Byte), der das gesuchte Byte enthält, gelesen werden muss. Auf diesen Block hat man unmittelbaren Zugriff.

1.6.9

Magnetplatten

Prinzipiell sind Festplatten und Disketten sehr ähnlich aufgebaut. Eine Aluminium- oder Kunststoffscheibe, die mit einem magnetisierbaren Material beschichtet ist, dreht sich unter einem Lese-Schreibkopf. Durch Anlegen eines Stromes wird in dem kleinen Bereich, der sich gerade unter dem Lese-Schreibkopf befindet, ein Magnetfeld induziert, das dauerhaft in der Plattenoberfläche bestehen bleibt. Durch Änderung der Stromrichtung und infolgedessen der Magnetisierungsrichtung können Daten aufgezeichnet werden. Beim Lesen der Daten misst man den Strom, der in der Spule des Lese-Schreibkopfes induziert wird, wenn sich ein magnetisierter Bereich unter ihm entlangbewegt, und wandelt diese analogen Signale wieder in Bits um. Der Lese-Schreibkopf lässt sich von außen zum Zentrum der rotierenden Platte und zurück bewegen, so dass effektiv jede Position der sich drehenden Magnetscheibe erreicht werden kann. Die Zugriffszeit einer Festplatte und auch eines Diskettenlaufwerkes hängt also davon ab, wie schnell der Kopf entlang des Plattenradius bewegt und positioniert werden kann, aber auch von der Umdrehungsgeschwindigkeit der Platte, denn nach korrekter Positionierung des Kopfes muss man warten, bis sich der gewünschte Bereich unter dem Kopf befindet. Bei Diskettenlaufwerken sind 360 Umdrehungen pro Minute (rpm = rotations per minute) üblich, Festplattenlaufwerke erreichen 15000 rpm. Für die exakte radiale Positionierung des Lese-Schreibkopfes ist ein Schrittmotor zuständig. In jeder Position können während einer Umdrehung der Scheibe Daten, die auf einer kreisförmigen Spur (engl. track) gespeichert sind, gelesen oder geschrieben werden. Vor Inbetriebnahme muss eine Platte formatiert werden. Dabei wird ein magnetisches Muster aufgebracht, das die Spuren und Sektoren festlegt. Die Anzahl der Sektoren wird üblicherweise so gewählt, dass ein Sektor jeder Spur 512 Byte aufnehmen kann. Da ein Sektor auf einer äußeren Spur einem längeren Kreisbogen entspricht als ein Sektor auf einer inneren Spur, werden die Daten innen dichter aufgezeichnet als außen. Dies bewirkt, dass der zentrale Bereich der Platte nicht ausgenutzt werden kann. Moderne Festplatten bringen daher auf den äußeren Spuren mehr Sektoren unter als auf den inneren. Da man auf diese Weise das Medium besser ausnutzt, kann mit dieser „multiple zone recording“ genannten Technik die Kapazität der Platte um bis zu 25 % gesteigert werden. Eine weitere Kapazitätssteigerung erreicht man, wenn es gelingt, die Spuren enger beeinander anzuordnen. Man erreicht heute Spurdichten bis zu 12000 tpi (tracks per inch). Mit einem Schrittmotor kann man derart dicht liegende Spuren nicht genau genug anfahren, daher verwendet man einen Spindelantrieb (actuator), der die korrekte Position des Kopfes kontinuierlich nachregelt. Hierzu dient ein magnetisches Muster, das beim Formatieren zusätzlich auf der Platte aufgebracht wird und anhand dessen man im Betrieb die korrekte Position des Kopfes erkennen und nachführen kann. Diese Technik wird als „closed loop positioning“ bezeichnet.

1.6 Hardware

47

Se kto r

1

Spur 0

Spur 79

2 tor Sek Sekt or

3

Arm mit Lese-Schreibkopf Abb. 1.21:

Magnetplatten: Spuren und Sektoren

Bei der beschriebenen Formatierung handelt es sich genau genommen um die physikalische oder Low-Level Formatierung. Diese wird heute bereits in der Fabrik aufgebracht. Bei der High-Level Formatierung, die der Besitzer der Festplatte vor ihrer ersten Benutzung durchführen muss, werden je nach Betriebssystem noch ein Inhaltsverzeichnis angebracht und ggf. noch Betriebssystemroutinen zum Starten des Rechners von der Magnetplatte. Folglich gehen bei der High-Level Formatierung vorher gespeicherte Daten nicht wirklich verloren. Sie sind noch auf der Platte vorhanden. Da aber das Inhaltsverzeichnis neu erstellt wird, ist es schwierig, die alten Datenbruchstücke aufzufinden und korrekt zusammenzusetzen.

1.6.10

Festplattenlaufwerke

Festplattenlaufwerke enthalten in einem luftdichten Gehäuse einen Stapel von Platten, die auf einer gemeinsamen Achse montiert sind. Jede Platte hat auf Vorder- und Rückseite jeweils einen Schreib-Lesekopf. Alle Köpfe bewegen sich synchron, so dass immer gleichzeitig alle Spuren der gleichen Nummer auf den Vorder- und Rückseiten aller physikalischen Platten bearbeitet werden können. Diese Menge aller Spuren gleicher Nummer wird auch als Zylinder bezeichnet. Die Aufzeichnungsdichte einer Festplatte hängt eng damit zusammen, wie nahe die Platte sich unter dem Kopf entlangbewegt. Bei der so genannten Winchester-Technik fliegt ein extrem leichter Schreib-Lesekopf aerodynamisch auf einem Luftkissen über der Plattenoberfläche. Der Abstand zur Platte ist dabei zum Teil geringer als 0,1µm. Das ist wesentlich weniger als die Größe normaler Staubpartikel, die bei 1 bis 10µm liegt. Der geringe Abstand des Schreib-Lesekopfes von der Platte erfordert daher einen luftdichten Abschluss des Laufwerkes in einem Gehäuse, das mit Edelgas gefüllt ist.

Achse mit Lesearmen Abb. 1.22:

Zylinder 0

1 Einführung

Zylinder 30000

48

Plattenstapel mit Zylindern, Spuren und Sektoren

Den Flug des Schreib-Lesekopfes in der genannten Höhe könnte man mit dem Flug eines Jumbojets in einer vorgeschriebenen Flughöhe von 40 cm vergleichen. Berührt ein Kopf die Plattenoberfläche, so wird die Platte zerstört und alle Daten sind verloren. Um einen solchen Plattencrash zu vermeiden, sollte die Platte, solange sie in Betrieb ist, vor starken Erschütterungen bewahrt werden. Wenn die Platte nicht in Betrieb ist, werden die Köpfe in einer besonderen Landeposition geparkt. Um den Abstand zwischen Kopf und Platte noch weiter zu verringern, was eine höhere Aufzeichnungsdichte zulässt, fertigt man neuerdings die Platten auch aus Glas statt aus Aluminium. Glasplatten lassen sich mit einer glatteren Oberfläche herstellen, allerdings ist die Brüchigkeit noch ein Problem. Die höhere träge Masse von Glas gegenüber Aluminium spielt keine Rolle, da eine Festplatte in einem stationären Rechner sich ohnehin ständig dreht.

Abb. 1.23:

Ein geöffnetes Festplattenlaufwerk und eine Microdrive Festplatte

1.6 Hardware

49

Nimmt man eine Festplatte in die Hand, so erkennt man, dass auf dem Gehäuse noch einiges an Elektronik untergebracht ist. Diese erfüllt teilweise die Funktionen eines Controllers, zusätzlich ist ein eigener Cache integriert, um die mittlere Zugriffszeit zu verbessern. Eine weitere Aufgabe dieser Elektronik ist die Fehlerkorrektur. Dabei setzt man so genannte fehlerkorrigierende Codes (z.B. Reed Solomon Code) ein, wie sie in der mathematischen Codierungstheorie entwickelt werden. Mithilfe einer leicht redundanten Aufzeichnung kann man später beim Lesen geringfügige Fehler nicht nur erkennen, sondern auch korrigieren. Auch die Fehlerrate gehört zu den Charakteristika einer Festplatte, wie sie in dem Datenblatt einer aktuellen Festplatte (ST373453LW) zu finden ist. • • • •

Kapazität: 68,359 GB ( = 73,4 × 109 Byte) Mittlere Zugriffszeit: 3,6 ms Datenrate (Lesen): 320 MB/s Interner Cache: 8 MB

Zuverlässigkeit (mittels 352-bit Reed-Solomon Code): • •

korrigierbare Fehler: 10 auf 1012 gelesene Bits erkennbare aber nicht korrigierbare Fehler: 1 auf 1015 gelesene Bits

Physikalische Charakteristika: • • •

4 Platten, 8 Köpfe 15000 U/min Stromaufnahme im Leerlauf: 12 Watt

Dieses Laufwerk ist auf Geschwindigkeit optimiert – nicht hinsichtlich der Kapazität. Moderne Laufwerke können heute eine Kapazität von 1 Terabyte bei einer durchschnittlichen Zugriffszeit von ca. 8 Millisekunden erreichen. Die Festplatte wird auch in absehbarer Zukunft noch der wichtigste nichtflüchtige Massenspeicher bleiben. Auch für den Transport großer Datenmengen sind neuerdings externe Festplatten populär geworden, die man einfach mit USB- oder Firewire-Kabel an den Rechner anschließt. Sie sind mit Kapazitäten bis zu 1 TByte erhältlich. Das physikalisch kleinste Festplattenlaufwerk wird derzeit von der Firma Hitachi hergestellt. Kleiner als eine Streichholzschachtel, aber nur 5mm dick und 16 g schwer, kann das Microdrive genannte Laufwerk bis zu 8 GByte Daten speichern. Solche Laufwerke waren für den Einsatz in digitalen Kameras, Organizern oder Mobiltelefonen gedacht. Da aber auch Flash-Speicherkarten mittlerweile eine vergleichbare Speicherkapazität aufweisen, noch kleiner sind und frei von fehleranfälliger Mechanik, verdrängen diese mehr und mehr die Festplattenwinzlinge. FlashKarten haben sich schnell zu einem Marktrenner entwickelt, da sie ausserdem schneller, lautlos, robust, stromsparender und günstiger in der Herstellung sind.

50

1.6.11

1 Einführung

Optische Laufwerke

Optische Platten wie CD und DVD sind mit einer Tellur-Selen-Legierung beschichtet. In diese Schicht werden bereits bei der Herstellung mechanisch Rillen gepresst. Auf den dazwischenliegenden Spuren können Daten mithilfe von Löchern (pits) gespeichert werden, die etwa ein Mikrometer groß sind. Zum Lesen richtet man einen Laserstrahl auf die rotierende Platte. Eine Fotodiode misst das unterschiedliche Reflexionsverhalten der Stellen, an denen ein Loch eingebrannt ist (pits), im Vergleich zu den unbeschädigten Stellen (Lands). CDs haben eine Speicherkapazität bis zu 1 GB und eignen sich als Speichermedien für Datenbanken, Telefonbücher, Lexika, Multimedia-Präsentationen und Musik. Weit verbreitet sind CDs mit der Bezeichnung „700MB“. Diese CDs sind mit 360000 Sektoren formatiert. Als Daten-CDs beinhaltet jeder Sektor 2048 Bytes so dass die Kapazität 737280000 Bytes also ca. 703,1 MB ist. Als Audio-CD enthält jeder Sektor 2352 Bytes. Die Gesamtkapazität ist daher 807,4 MB und entspricht einer Spieldauer von 80 Minuten. Normale CDs können nicht wieder beschrieben werden. Zur Herstellung wird zunächst ein Negativabdruck gefertigt, aus dem zahlreiche CDs mechanisch gepresst werden können. So genannte CD-Rs sind einmal beschreibbar. Die pits werden mit einem energiereichen Laserstrahl eingebrannt. CD-Rs sind billig und besonders auch für Backups oder für Archivierungszwecke geeignet. Es gibt auch mehrfach wiederbeschreibbare CDs, so genannte „rewriteables“ oder CD-RW. Während die Zugriffszeit für das Lesen von CDs in Bereiche vorstößt, die vor einigen Jahren noch Festplatten vorbehalten waren, ist das Beschreiben von CD-Rs und CD-RWs noch vergleichsweise langsam. Das Lesen und Schreiben optischer Platten ist fehleranfällig. Durch Mehrfachaufzeichnung und durch die Verwendung fehlerkorrigierender Codes wird die Fehlerrate aber auf Werte reduziert, die besser als bei den für Magnetplatten üblichen Aufzeichnungsverfahren sind. Selbst Kratzer können daher dem ungeschützten Datenträger nichts anhaben. Zur Speicherung von Videos auf CDs ist deren Kapazität zu knapp bemessen. Hier gibt es seit geraumer Zeit Abhilfe in Form der so genannten DVD (digital versatile disk), auf der 4.7 bis 17 GB Daten Platz finden. Dies ist ausreichend auch für mehrstündige Spielfilme. Die DVD ist zur CD-ROM abwärts kompatibel, was heißen soll, dass man in den DVD-Laufwerken auch CD-ROMs lesen kann. Multimediadaten wie Musik oder Filme müssen ohne Verzögerung (engl. in real time) von dem Speichermedium gelesen werden können. Die Datenübertragungsrate, gemessen in MByte/s ist daher eine wichtige Kenngröße für CD-ROMs und für DVDs. Man gibt meist an, um wievielfach die Übertragungsrate höher ist als bei den ersten CD-ROM-Laufwerken: 48-fach und 52-fach Laufwerke sind heute üblich. Bei den DVDs gibt es ein einseitiges Format mit einer Schicht (Kapazität 4.7 GB), ein einseitiges Format mit zwei Schichten (Kapazität 8.5 GB), ein zweiseitiges Format mit je einer Schicht (Kapazität 9.4 GB) und ein zweiseitiges Format mit je zwei Schichten (Kapazität 17 GB). Anmerkung: Bei DVDs sind Kapazitätsangaben üblich, bei denen mit Giga 109 gemeint ist.

1.6 Hardware

51

Die allgemeine Einführung von beschreibbaren DVDs wurde dadurch behindert, dass sich die Herstellerfirmen nicht auf einen gemeinsamen Standard einigen konnten. Mittlerweile gibt es konkurrierend die Standards DVD-RAM, DVD-R, DVD+R, DVD-RW und DVD+RW für einmal bzw. mehrfach beschreibbare Datenträger. Zusätzlich wird noch die Bezeichnung DL (double Layer) verwendet, wenn die DVD mit zwei Schichten beschreibbar ist. Als Nachfolgestandard konnte sich mittlerweile die Blu-ray Disk (BD) durchsetzen. Dieser Standard wurde seit 2002 entwickelt, erste Produkte gab es ab 2006. Derzeit sind Blu-ray Laufwerke allgemein erhältlich. Der konkurrierende Standard High Density DVD (HD-DVD) konnte sich nicht durchsetzen. Während bisher CDs und DVDs mit einem Laserstrahl im roten Farbbereich bearbeitet werden, arbeiten die DVD Nachfolger mit blauem bzw. violettem Laserlicht. Blu-ray Disk haben eine Kapazität 27 GB bzw. mit zwei Schichten 54 GB. Auch bei Blu-ray Disks sind Kapazitätsangaben üblich, bei denen mit Giga 109 gemeint ist.

1.6.12

Flash-Speicher

Bei einem Flash-Speicher werden Bits als elektrische Ladungen auf einem sogenannten Floating Gate eines Feldeffekttransistors gespeichert. Dieses ist durch eine sehr dünne Isolatorschicht von der Stromzufuhr getrennt. Ein einmal gespeicherter Ladungszustand ändert sich normalerweise nicht. Eine Änderung des Ladungszustands kann nur durch Anlegen bestimmter relativ hoher Spannungen an die übrigen Elemente des Feldeffekttransistors erfolgen. Dabei nutzt man den quantenphysikalischen Tunneleffekt, der es Elektronen erlaubt, einen Isolator zu durchqueren. Bei allen Flash-Speichern muss der Speicher vor einer Schreiboperation gelöscht werden. Dies ist nur jeweils für einen bestimmten Teil (z.B. 1/16) des gesamten Speichers möglich. Der Controller eines Flash-Speichers muss daher jeweils vor dem Löschen Speicherbereiche, deren Inhalt sich nicht ändern soll, auslesen und nach dem Löschen zusammen mit ggf. geänderten Daten wieder beschreiben. Es gibt zwei verschiedene Flash-Architekturen. Bei NAND-Flash Bausteinen sind die Speicherbereiche in größeren Gruppen hintereinandergeschaltet. Das ermöglicht relativ große Kapazitäten, im Jahr 2008 bis zu 32 GBit, macht das Lesen- und Schreiben aber aufwändiger. Bei NOR-Flash Bausteinen sind die Speicherbereiche parallel geschaltet. Das ermöglicht einen wesentlich effizienteren Lese- und Schreibzugriff, ermöglicht aber nur geringere Speicherkapazitäten - im Jahr 2008 bis zu 2 GBit. Während sich NOR-Flash Bausteine vor allem für die Speicherung des Rechner-BIOS und für Controllerbausteine eignen, werden NAND-Flash Bausteine eingesetzt für: • • • •

USB-Sticks, Speicherkarten für Handys, Digitalkameras und Videokameras, MP3-Player, Festplatten-Ersatz für Netbooks.

Für diese Anwendungen ist in den letzten Jahren ein schnell expandierender Markt mit hohen Stückzahlen entstanden. Dies hatte stark sinkende Preise zur Folge. USB-Sticks und SD Speicherkarten mit 2 bis 8 GB waren im Jahr 2008 für 10 bis 30 Euro zu haben.

52

1 Einführung

Bei einem USB-Stick findet man einen (oder mehrere) NAND-Flash Bausteine zusammen mit einem Controller, der den Speicher und die USB-Schnittstelle ansteuert. Der Markterfolg der Flash-Speicher begann mit dem Siegeszug der Digitalkameras. Am weitesten verbreitet sind derzeit SD-Karten (Secure Digital Memory Card). Die Spezifikation wurde ab 2001 von der Firma SanDisk entwickelt. Der Namensbestandteil "Secure" steht für eine Absichtserklärung, dass der Controller der Karte zusätzliche Hardware-Funktionen für das Digital Rights Management (DRM) anbieten soll. Inwieweit diese Funktionen tatsächlich genutzt werden ist derzeit unklar. SD-Karten werden mit einer Kapazität bis zu 2 GB angeboten. Zur Überwindung der 2GB-Grenze wurde eine Erweiterung der Spezifikation unter dem Namen SDHC (SD High Capacity) erstellt, die Speicherkapazitäten bis zu 32 GB ermöglicht. SDHC-Karten funktionieren nur mit Geräten die die erweiterte Spezifikation beherrschen.

Abb. 1.24:

Speicherkarte (Vorder- und Rückseite) und USB-Stick

1.6.13

Vergleich von Speichermedien

Von den Registern im Inneren einer CPU bis hin zu optischen Platten existiert heute eine Hierarchie von Speichermedien, die wahlfreien Zugriff auf die gespeicherten Daten gewähren. Die folgende Tabelle listet zusammenfassend wichtige Merkmale der verschiedenen Technologien innerhalb der heute üblichen Grenzen auf. Erreichbare Datentransferrate in MB/s

Medium

Kapazität Untergrenze

Kapazität Obergrenze

Mittlere Zugriffszeit

Register

16 bit

128 bit

0,1 ns

70000

Cache

10 kB

32 MB

2 ns

20000

256 MB

64 GB

10 ns

6000

Flashkarten

1 GB

32 GB

1 ms

20

Festplatten

60 GB

1 TB

3,5 ms

320

Optische Platten

100 MB

60 GB

25 ms

10

Disketten

1.4 MB

250 MB

100 ms

0,2

Arbeitsspeicher

1.6 Hardware

1.6.14

53

Bildschirme

Mithilfe eines Bildschirmes können Texte, Bilder und grafische Darstellungen sichtbar gemacht werden. Verwendet werden die bei Fernsehgeräten üblichen Elektronenstrahlröhren (kurz Bildröhren) oder aber Flüssigkristallanzeigen. In beiden Fällen wird das Bild aus vielen einzelnen Punkten zusammengesetzt. Diese können schwarz-weiß oder mehrfarbig sein und werden Bildpunkte oder Pixel genannt. Der Begriff Pixel ist aus der englischen Entsprechung picture elements entstanden. Abb. 1.25 zeigt einen modernen Flüssigkristall-Bildschirm.

Abb. 1.25:

TFT Bildschirm

Die Anzahl der Bildpunkte pro Fläche definiert die Auflösung eines Bildes. Diese ist bei Fernsehbildröhren mit 300.000 Pixeln relativ gering. Computermonitore müssen feinere Details darstellen und sie befinden sich viel näher am Auge des Betrachters. Sie haben, je nach Größe, bis zu 1.500.000 Pixel. Bildschirmgrößen werden in Zoll angegegeben, wobei die Diagonale des sichtbaren Bildes gemessen wird. Das Bild eines 17’’ Bildschirmes misst also in der Diagonalen 42.5 cm. Bei einer Bildröhre wird das Bild durch einen Elektronenstrahl auf die Phosphorschicht des Bildschirmes gezeichnet. Nach kurzer Zeit verschwindet das Bild aber, wenn es nicht erneut gezeichnet wird. Dieser ständige Wechsel zwischen Zeichnen und Verlöschen äußert sich als Flimmern. Je häufiger also ein Bild pro Sekunde wiederholt wird, um so weniger flimmert es. Die Arbeit an einem Monitor strengt weniger an, wenn das Bild flimmerfrei ist. Daher werden für Computersysteme Monitore mit Bildwiederholraten von mindestens 70 Hertz empfohlen. Flüssigkristallanzeigen sind von Natur aus flimmerfrei, da ein Bildpunkt seine Farbe so lange aufrecht erhält, wie ein entsprechendes Signal anliegt. Sie werden daher als Computermonitore und als Fernsehgeräte immer populärer und haben Röhrengeräte fast völlig verdrängt. Daher ist auch der zunächst vergleichbar hohe Preis mittlerweile stark gefallen. Üblich ist heute eine Technik, bei der jeder Bildpunkt aus drei Transistoren aufgebaut wird. Diese werden überwiegend in der so genannten TFT-Technik (thin film transistor) gefertigt. Preisgünstige 17" Monitore erreichen eine Auflösung von 1024x1280 Pixeln. Teurere Modelle mit einer Bildschirmdiagonalen von 21" bis 30" erlauben eine Auflösung von 1200x1600 bis 1600x2560 Pixel.

54

1 Einführung

1.6.15

Text- und Grafikmodus

Bildschirme können in einem Textmodus oder in einem Grafikmodus betrieben werden. Beim Textmodus wird der Bildschirm in eine feste Anzahl von Zeilen und Spalten (z.B. 25 x 80) eingeteilt. An jeder dieser 2000 Schreibpositionen kann ein ASCII-Zeichen mit bestimmten Attributen wie Farbe, Blinkmodus etc. dargestellt werden. Zur Kennzeichnung eines Zeichens werden zwei Bytes benötigt, für einen ganzen Bildschirm daher 4000 Byte. Die Umwandlung von Zeichen und Attribut einer Schreibposition in Bildpunkte erfolgt durch einen Zeichengenerator. Beliebige Grafiken und Bilder können auf einem solchen Textbildschirm nicht dargestellt werden, bestenfalls kann man aus den verfügbaren Sonderzeichen eine grobe Figur zusammensetzen. Im Grafikmodus kann jedes einzelne Pixel direkt angesprochen werden. Ein gängiges Grafikformat für einen 17" Bildschirm besteht, wie oben bereits erwähnt, aus 1024 Zeilen und 1280 Spalten, also aus 1 310 720 Bildpunkten. Wenn jeder Bildpunkt 256 mögliche Farbwerte jeweils für die Farben rot, grün und blau annehmen kann, sind pro Bildpunkt drei Bytes erforderlich, und zur Beschreibung eines ganzen Bildschirms werden etwa 3,9 MByte benötigt. Höher auflösende Bildschirmformate werden im Kapitel über Grafikprogrammierung diskutiert. Der Textmodus ist technisch einfacher zu realisieren und hat anfangs die Welt der Personal Computer beherrscht. Heute ist der Betrieb von Personal Computern im Grafikmodus zum Standard geworden. Der Grafikmodus ist die technische Voraussetzung für den Einsatz moderner grafischer Benutzeroberflächen, wie sie im folgenden Abschnitt diskutiert werden.

1.7

Von der Hardware zum Betriebssystem

Bisher haben wir die Hardware und Möglichkeiten der Datenrepräsentation diskutiert. Ohne Programme ist beides aber nutzlos. Die Programme, die einen sinnvollen Betrieb eines Rechners erst möglich machen, werden als Software bezeichnet. Man kann verschiedene Schichten der Software identifizieren. Sie unterscheiden sich durch ihren Abstand zum menschlichen Benutzer bzw. zur Hardware des Computers. Dazu stellen wir uns einmal einen „blanken“ Computer vor, d.h. eine CPU auf einem Motherboard mit Speicher und Verbindung zu Peripheriegeräten wie Drucker und Plattenlaufwerk, aber ohne jegliche Software. Die CPU kann, wie oben dargelegt, nicht viel mehr als • • • •

Speicherinhalte in Register laden, Registerinhalte im Speicher ablegen, Registerinhalte logisch oder arithmetisch verknüpfen, mit IN- und OUT-Befehlen Register in Peripheriegeräten lesen und schreiben.

In Zusammenarbeit mit den Controllern der Peripheriegeräte (Tastatur, Bildschirm, Plattenlaufwerk, Soundkarte) kann man auf diese Weise bereits • • •

ein Zeichen von der Tastatur einlesen, ein Zeichen an einer beliebigen Position des Textbildschirms ausgeben, ein Pixel an einer beliebigen Stelle des Grafikbildschirm setzen,

1.7 Von der Hardware zum Betriebssystem • •

55

einen Sektor einer bestimmten Spur einer Platte lesen oder schreiben, einen Ton einer bestimmten Frequenz und Dauer erzeugen.

All diese Tätigkeiten bewegen sich auf einer sehr niedrigen Ebene, die wir einmal als Hardwareebene bezeichnen wollen. Man müsste, um einen Rechner auf dieser Basis bedienen zu können, sich genauestens mit den technischen Details jedes einzelnen der Peripheriegeräte auskennen. Außerdem würde das, was mit dem Peripheriegerät eines Herstellers funktioniert, mit dem eines anderen Fabrikates vermutlich fehlschlagen. Von solchen elementaren Befehlen ist es also noch ein sehr weiter Weg, bis man mit dem Rechner z.B. folgende Dinge erledigen kann: • • • • • •

Briefe editieren und drucken, Faxe versenden, E-Mail bearbeiten und senden, Fotos und Grafiken bearbeiten und retuschieren, Musikstücke abspielen oder bearbeiten, Steuererklärungen berechnen, Adventure-, Strategie- und Simulationsspiele ausführen.

Hier befinden wir uns auf der Benutzerebene, wo der Rechner als Arbeitsgerät auch für technische Laien dienen muss. Niemand würde einen Rechner für die genannten Tätigkeiten einsetzen, wenn er sich bei jedem Tastendruck überlegen müsste, an welchem Register des Tastaturcontrollers die CPU sich das gerade getippte Zeichen abholen muss, wie sie feststellt, ob es sich um ein Sonderzeichen („Shift“, „Backspace“, „Tab“) oder um ein darstellbares Zeichen handelt; wie dieses gegebenenfalls in ein Register der Grafikkarte geschrieben und durch den Controller am Bildschirm sichtbar gemacht wird. Gar nicht auszumalen, wenn dabei noch alle Zeichen des bereits auf dem Bildschirm dargestellten Textes verschoben werden müssten, um dem eingefügten Zeichen Platz zu machen. Zwischen dem von einem Anwender intuitiv zu bedienenden Rechner und den Fähigkeiten der Hardware klafft eine riesige Lücke. Wir werden, um diese zu überbrücken, zwei Zwischenebenen einziehen, von denen jeweils eine auf der anderen aufbaut, nämlich • •

das Betriebssystem (Datei-, Prozess- und Speicherverwaltung sowie Werkzeuge), das grafische Bediensystem (Menüs, Fenster, Maus).

Jede Schicht fordert von der niedrigeren Schicht Dienste an. Diese wiederum benötigt zur Erfüllung der Anforderung selber Dienste von der nächstniedrigeren Schicht. Auf diese Weise setzen sich die Anforderungen in die tieferen Schichten fort, bis am Ende die Hardware zu geeigneten Aktionen veranlasst wird. Man kann es auch so sehen, dass jede Schicht der jeweils höherliegenden Schicht Dienste anbietet. Ein Programmierer muss daher nur die Schnittstelle (siehe unten) zur direkt unter seiner aktuellen Ebene befindlichen Schicht kennen.

56

1 Einführung Anwendungsprogramme

Anforderungen

Grafisches Bediensystem

Verwaltung von Fenstern, Menüs, Maus, Ereignissen, ...

Betriebssystem

Dateisystem, Speicherverwaltung, Prozesssystem

Hardware

Prozessor, Speicher, Laufwerke, Bildschirm, ...

Abb. 1.26:

1.7.1

Bereitstellung von Diensten

z. B. Word, Excel, Firefox, Photoshop, Schach, ....

Grafisches Bediensystem und Betriebssystem als Mittler zwischen Anwender und Hardware.

Schnittstellen und Treiber

Wenn eine CPU mit den Endgeräten (z.B. den Laufwerken) verschiedener Hersteller zusammenarbeiten soll, dann muss man sich zunächst auf eine gemeinsame Schnittstelle verständigen. Eine Schnittstelle ist eine Konvention, die eine Verbindung verschiedener Bauteile festlegt. Man kann sich das an dem Beispiel der elektrischen Steckdose verdeutlichen. Die Schnittstellendefinition, die u.a. die Größe, den Abstand der Kontaktlöcher, die Lage der Schutzkontakte und die Strombeschaltung (230 Volt Wechselstrom) festlegt, eröffnet den Produzenten von Steckdosen die Möglichkeit, diese in verschiedenen Farben, Materialien und Varianten zu produzieren. Die Hersteller elektrischer Geräte wie Lampen, Bügeleisen, Toaster oder Computer-Netzteile können sich darauf verlassen, dass ihr Gerät an jeder Steckdose jedes Haushaltes betrieben werden kann.

stellt Dienste bereit

Abb. 1.27:

verlässt sich auf Dienste

Schnittstelle

Schnittstellen in der Informatik bestimmen nicht nur, wie Stecker und die passenden Dosen aussehen oder wie sie beschaltet sind (beispielsweise die Anschlüsse für Drucker, Monitor, Tastatur, Lautsprecher, etc.), sie können z.B. auch Reihenfolge und Konvention des Signalund Datenaustausches festlegen. Daher spricht man z.B. auch von der parallelen Schnittstelle

1.7 Von der Hardware zum Betriebssystem

57

statt von der „Druckersteckdose“. Jedes Gerät, das mit einem passenden Stecker ausgestattet ist und die entsprechende Konvention des parallelen Datenaustausches beherzigt, kann an eine USB-Steckdose angeschlossen werden. Wenn wir einen Moment bei dem Beispiel des Druckers bleiben, so wird die Aktion des Druckkopfes, die Drehung der Walze, der Einzug eines nächsten Blattes von Signalen gesteuert, die der Drucker auf bestimmten Leitungen von der parallelen Schnittstelle empfängt. Allerdings gibt es viele verschiedene Drucker, manche besitzen keinen Druckkopf, stattdessen einen Spiegel, der einen Laserstrahl ablenkt, andere simulieren nur einen Drucker, während sie in Wahrheit die Seite als Fax über das Telefon senden. Ein Textverarbeitungsprogramm kann nicht im Voraus alle verschiedenen Drucker kennen, die es dereinst einmal bedienen soll. Wenn der Benutzer den Befehl „Drucken“ aus dem DateiMenü auswählt, soll der Druck funktionieren, egal welcher Drucker angeschlossen ist. Dazu bedient sich das Programm auch einer Schnittstelle, diesmal aber einer solchen, zu der kein physikalischer Stecker gehört. Diese Schnittstelle wird im Betriebssystem definiert und beinhaltet u.a. Befehle wie „Drucke ein kursives ’a’ “, „Neue Zeile“, „Seitenumbruch“. Die Umsetzung dieser Befehle in Signale für einen bestimmten Drucker leistet ein Programm, das der Druckerhersteller beisteuert. Solche Programme nennt man Treiberprogramme, kurz auch Treiber. Treiber sind allgemein Übersetzungsprogramme zur Ansteuerung einer Software- oder Hardware-Komponente. Ein Treiber ermöglicht einem Anwendungsprogramm die Benutzung einer Komponente, ohne deren detaillierten Aufbau zu kennen. Die Anforderungen eines Anwendungsprogramms an das zugehörige Gerät werden dann vom Betriebssystem an den entsprechenden Treiber umgeleitet, dieser wiederum sorgt für die korrekte Ansteuerung des Druckers. Treiber für gängige Geräte sind meist schon im Betriebssystem vorhanden. Wenn aber ein neues Gerät, das dem Betriebssystem noch nicht bekannt ist, an einen Rechner angeschlossen wird, so muss üblicherweise auch ein zugehöriger Treiber installiert werden. Dabei wird das Treiberprogramm auf die Festplatte kopiert und dem Betriebssystem angemeldet. Früher, unter dem PC-Betriebssystem DOS, erkannte man Treiberprogramme an der Endung „sys“ und die Anmeldung geschah durch einen Eintrag des Treibernamens in die Konfigurierungsdatei „config.sys“. Unter Windows werden Treiber meist als dynamic link libraries (DLL) erstellt und mit einem eigenen Installationsprogramm „setup.exe“ installiert. Treiber können auch virtuelle Geräte (engl. virtual devices) bedienen. Ein Beispiel ist eine RAM-Disk, also eine Festplatte, die nicht wirklich existiert, sondern nur durch die Treibersoftware vorgespiegelt wird. Die Verzeichnisse und Dateien dieser virtuellen Platte werden in Wirklichkeit in einem eigens dafür reservierten Bereich des Hauptspeichers gehalten. Dadurch ist das Schreiben und Lesen einer Datei auf der RAM-Disk extrem schnell. Für den Benutzer geschieht der Zugriff transparent, das heißt, dass er und seine Programme keinen Unterschied zwischen dem realen und dem simulierten Gerät feststellen können, außer dass der Zugriff auf eine Datei sehr viel schneller vor sich geht – und dass beim Abschalten des Rechners alle Daten der RAM-Disk verloren gehen.

58

1 Einführung

1.7.2

BIOS

Ein fester Bestandteil der Hardware eines IBM-kompatiblen PCs ist, wie bereits erwähnt, der BIOS-Chip (basic input output system), in dem grundlegende und sehr elementare Hilfsprogramme zur Ansteuerung von Hardwarekomponenten wie Tastatur, Maus, Festplatte und Grafikcontroller abgelegt sind. Diese Programme werden auch als Interrupts (engl. für Unterbrechungen) bezeichnet, weil sie je nach Prioritätenstufe andere gleichzeitig laufende Programme unterbrechen dürfen. Solche Interrupts bestehen meist aus einer Reihe verwandter Unterfunktionen. Beispielsweise dient die Funktion Nr. 0Ch des Interrupts 13h dazu, eine Anzahl von Sektoren einer bestimmten Spur durch einen bestimmten Schreib-Lesekopf der Festplatte zu lesen und die gelesenen Daten in einen Puffer des Hauptspeichers zu übertragen. Ist der Puffer zu klein gewählt, stürzt das System ab! Jeder Tastendruck, jede Mausbewegung führt zum Aufruf eines Interrupts. Dabei werden die Parameter des aufgetretenen Ereignisses ermittelt und es wird ggf. darauf reagiert. Zusätzlich enthält das BIOS Programme, die nach dem Einschalten des Rechners ausgeführt werden. Dazu gehört eine Prüfung, welche Geräte angeschlossen sind, ein Funktionstest (z.B. des Speichers) und ein Laden des Betriebssystems von Festplatte, Netz, USB-Stift oder CDROM. Dies ist ein mehrstufiger Prozess – ein Programm im BIOS lädt und startet ein Ladeprogramm (loader). Dieses Ladeprogramm lädt dann das eigentliche Betriebssystem. Den gesamten Vorgang nennt man booten (von dem englischen Wort „bootstrapping“ – Schuh schnüren). Parameter des BIOS, etwa das aktuelle Datum oder die Boot-Sequenz (in welcher Reihenfolge auf den externen Geräte nach einem ladbaren Betriebssystem gesucht wird), lassen sich über ein BIOS-Setup verändern. In dieses Setup Programm gelangt man bei vielen Rechnern durch Drücken der „Entf“ („Del“)-Taste bzw. der F2-Taste kurz nach dem Einschalten. Zusammenfassend kann man also sagen, dass das BIOS und die Treiber die Brücken zwischen der Hardware und dem im Anschluss zu diskutierenden Betriebssystem bilden.

Betriebssystem Treiber

Treiber

Treiber

BIOS Hardware

CPU, Speicher, Peripherie

Abb. 1.28:

Hardware und Betriebssystem

1.7 Von der Hardware zum Betriebssystem

1.7.3

59

Die Aufgaben des Betriebssystems

Der Rechner mit seinen Peripheriegeräten stellt eine Fülle von Ressourcen zur Verfügung, auf die Benutzerprogramme zugreifen. Zu diesen Ressourcen gehören • • • •

CPU (Rechenzeit), Hauptspeicher, Plattenspeicherplatz, externe Geräte (Drucker, Modem, Scanner).

Die Verwaltung dieser Ressourcen ist eine schwierige Aufgabe, insbesondere, wenn viele Benutzer und deren Programme gleichzeitig auf diese Ressourcen zugreifen wollen. Die zentralen Bestandteile eines Betriebssystems sind dementsprechend • • •

Prozessverwaltung, Speicherverwaltung, Dateiverwaltung.

1.7.4

Prozess- und Speicherverwaltung

Der Aufruf eines Programms führt oft zu vielen gleichzeitig und unabhängig voneinander ablaufenden Teilprogrammen. Diese werden auch Prozesse genannt. Ein Prozess ist also ein eigenständiges Programm mit eigenem Speicherbereich, der vor dem Zugriff durch andere Prozesse geschützt ist. Allerdings ist es erlaubt, dass verschiedene Prozesse Daten austauschen, man sagt kommunizieren. Zu den Aufgaben des Betriebssystems gehört es daher auch, die Kommunikation zwischen diesen Prozessen möglich zu machen, ohne dass die Prozesse sich untereinander beeinträchtigen oder gar zerstören. Das Betriebssystem muss also alle gleichzeitig aktiven Prozesse verwalten, so dass einerseits keiner benachteiligt wird, andererseits aber kritische Prozesse mit Priorität behandelt werden. Selbstverständlich können Prozesse nicht wirklich gleichzeitig laufen, wenn nur eine CPU zur Verfügung steht. Das Betriebssystem erzeugt aber eine scheinbare Parallelität dadurch, dass jeder Prozess immer wieder eine kurze Zeitspanne (wenige Millisekunden) an die Reihe kommt, dann unterbrochen wird, während andere Prozesse bedient werden. Nach kurzer Zeit ist der unterbrochene Prozess wieder an der Reihe und setzt seine Arbeit fort. Wenn die Anzahl der gleichzeitig zu bedienenden Prozesse sich im Rahmen hält, hat ein Benutzer den Eindruck, dass alle Prozesse gleichzeitig laufen. Ähnlich verhält es sich mit der Verwaltung des Hauptspeichers, in dem nicht nur der Programmcode, sondern auch die Daten der vielen Prozesse gespeichert werden. Neuen Prozessen muss freier Hauptspeicher zugeteilt werden und der Speicher beendeter Prozesse muss wiederverwendet werden. Die Speicherbereiche verschiedener Prozesse müssen vor gegenseitigem Zugriff geschützt werden.

1.7.5

Dateiverwaltung

Die dritte wichtige Aufgabe des Betriebssystems ist die Dateiverwaltung. Damit ein Benutzer sich nicht darum kümmern muss, in welchen Sektoren bzw. auf welchen Spuren noch Platz ist, um den gerade geschriebenen Text zu speichern, oder wo die Version von gestern gespeichert war, stellt das Betriebssystem das Konzept der „Datei“ als Behälter für Daten aller Art

60

1 Einführung

zur Verfügung. Die Übersetzung von Dateien und ihren Namen in bestimmte Spuren, Sektoren und Köpfe der Festplatte nimmt das Dateisystem als Bestandteil des Betriebssystems vor. Moderne Dateisysteme sind hierarchisch aufgebaut. Mehrere Dateien können zu einem Ordner (engl. folder) zusammengefasst werden. Für diese sind auch die Bezeichnungen Katalog, Verzeichnis, Unterverzeichnis (engl. directory, subdirectory) in Verwendung. Da Ordner sowohl „normale“ Dateien als auch andere Ordner enthalten können, entsteht eine hierarchische (baumähnlich verzweigte) Struktur. In Wirklichkeit ist ein Ordner eine Datei, die Namen und einige Zusatzinformationen von anderen Dateien enthält. Von oben gesehen beginnt die Hierarchie mit einem Wurzelordner, dieser enthält wieder Dateien und Ordner, und so fort. Der linke Teil der folgenden Grafik zeigt einen Dateibaum, wie er in dem Windows-Betriebssystem dargestellt wird. Unter dem Wurzelordner mit Namen „A:“ befinden sich die zwei Unterordner „Programme“ und „Texte“. Letzterer enthält Unterordner „Briefe“ und „Buch“ etc. Normale Dateien können sich auf allen Stufen befinden. Das Bild zeigt in der rechten Hälfte alle Dateien im Unterordner „Bilder“. Jede Datei erhält einen Namen, unter der sie gespeichert und wiedergefunden werden kann. Der Dateiname ist im Prinzip beliebig, er kann sich aus Buchstaben Ziffern und einigen erlaubten Sonderzeichen zusammensetzen. Allerdings hat sich als Konvention etabliert, Dateinamen aus zwei Teilen zu bilden, dem eigentlichen Namen und der Erweiterung. Ein Punkt trennt den Namen von der Erweiterung. Die vorige Abbildung ist z.B. in der Datei Dateisystem.ppt abgespeichert.

Abb. 1.29:

Dateihierarchie

Anhand des Namens macht man den Inhalt der Datei kenntlich, anhand der Erweiterung die Art des Inhaltes. Von letzterem ist nämlich abhängig, mit welchem Programm die Datei bearbeitet werden kann. In diesem Falle zeigt die Erweiterung ppt an, dass es sich um eine Datei handelt, die mit dem Programm „Powerpoint“ erstellt worden ist. Unter Windows häufig benutzte Erweiterungen, ihre Bedeutungen und Programmen zu ihrer Bearbeitung sind: txt doc, docx

einfacher ASCII-Text Brief, formatiertes Textdokument

Notepad, UltraEdit, Notepad++ Word, Open Office Writer

1.7 Von der Hardware zum Betriebssystem xls html,htm bmp jpeg,gif pdf ps java class zip,rar mp3

Spreadsheet Internet Dokument Bitmap Grafik komprimierte Grafik Dokument (portable document format) Postscript Dokument (Text und Grafik) Java Programm im Quelltext compiliertes Java Programm komprimiertes Dateiarchiv komprimierte Musikdatei

61 Excel, Open Office Calc alle Internet Browser Paint ACDSee, Gimp Acrobat Ghostscript, Ghostview Java Compiler Java Virtuelle Machine WinZip, WinRAR WinAmp

In den Windows-Betriebssystemen haben folgende Erweiterungen feste Bedeutungen: bat, exe, com für direkt lauffähige Programme, dll für (Sammlungen von) Bibliotheksprogrammen, drv,sys,vxd für Treiberdateien. Obwohl auch Dateinamen ohne Erweiterung möglich sind, ist es sinnvoll, sich an die Konventionen zu halten, da auch die Anwenderprogramme von diesem Normalfall (engl. default) ausgehen. Ein Anwendungsprogramm wie z.B. Word wird beim ersten Abspeichern einer neuen Datei als default die Endung „.doc“ vorgeben. Es kann leicht vorkommen, dass zwei Dateien, die sich in verschiedenen Ordnern befinden, den gleichen Namen besitzen. Dies ist kein Problem, da das Betriebssystem eine Datei auch über ihre Lage im Dateisystem identifiziert. Diese Lage ist in einer baumartigen Struktur wie dem Dateisystem immer eindeutig durch den Pfad bestimmt, den man ausgehend von der Wurzel traversieren muss, um zu der gesuchten Datei zu gelangen. Den Pfad kennzeichnet man durch die Folge der dabei besuchten Unterverzeichnisse. Der Pfad zu dem Unterverzeichnis „Bilder“ von „Kapitel 01“ in der vorigen Abbildung ist: A:\Texte\Buch\Kapitel 01\Bilder Man erkennt, dass die einzelnen Unterordner durch das Trennzeichen „\“ (backslash) getrennt wurden. In den Betriebssystemen der UNIX-Familie (Linux, SunOs, BSD) wird stattdessen „/“ (slash) verwendet. Der Pfad, zusammen mit dem Dateinamen (incl. Erweiterung), muss eine Datei eindeutig kennzeichnen. Die Powerpoint-Datei, die die Grafik „Dateisystem“ aus Kapitel 1 des Buches enthält, hat also den vollen Namen: A:\Texte\Buch\Kapitel 01\Bilder\Dateisystem.ppt Üblicherweise will ein Benutzer nicht solche Bandwurmnamen eintippen, daher ist immer ein „aktueller Ordner“ aktiviert. In der obigen Grafik ist dieser als aufgeklapptes Ordnersymbol kenntlich gemacht. Der Pfad von der Wurzel zu diesem aktuellen Ordner wird vom Betriebssystem automatisch dem Dateinamen vorangestellt. Dies ist gleichbedeutend damit, dass eine Datei immer im aktuellen Ordner erstellt oder gesucht wird. Neben der reinen Verwaltung der Dateien kann das Betriebssystem auch einige Operationen mit diesen Dateien ausführen. Dazu gehört das Anlegen, das Löschen und das Umbenennen

62

1 Einführung

einer Datei. Anwenderprogramme haben häufig einen Menüpunkt „Datei“, in dem unter anderem diese Dienste des Betriebssystems angeboten werden. Stellen wir uns vor, dass wir mit einem Textbearbeitungsprogramm wie MS-Word einen Brief schreiben wollen. Dies führt zu einer Anfrage von Word an das Betriebssystem nach der Liste aller Dateien mit der Endung „.doc“ im aktuellen Ordner. Diese werden grafisch dargestellt. Ein Klick auf „Oma.doc“ veranlasst Word zu einer Anfrage an das Betriebssystem nach dem Inhalt dieser Datei. Das Betriebssystem sucht in seinem Katalog nach der Position der Datei auf der Festplatte, wo die Daten des Dokuments gespeichert sind. Anschließend ruft es geeignete BIOS-Interrupts auf, die dann in Verbindung mit den Treibern die entsprechenden Sektoren lesen. Die gefundenen Daten werden anschließend an Word hochgereicht und auf dem Bildschirm dargestellt. Beim Speichern oder Sichern laufen ähnliche Ereignisketten ab. Hierbei muss ggf. das Betriebssystem seine Katalogeinträge, den Dateinamen und die Lage des Inhaltes auf der Festplatte aktualisieren.

1.7.6

DOS, Windows und Linux

Frühere Betriebssysteme für Personal Computer waren eigentlich nur Dateiverwaltungssysteme. Dazu gehörte CPM und auch das daraus hervorgegangene DOS (Disk Operating System). Daher konnte immer nur ein Programm nach dem anderen ausgeführt werden. Spätestens für die Realisierung einer grafischen Benutzeroberfläche mit Fenstern, Maus und Multimediafähigkeiten ist ein Betriebssystem mit effizientem Prozesssystem notwendig. Viele Prozesse müssen gleichzeitig auf dem Rechner laufen und sich dessen Ressourcen teilen. Sie dürfen sich aber nicht gegenseitig zerstören. So wurde zunächst an DOS ein Prozesssystem und ein Speicherverwaltungssystem „angebaut“. Da aber bei der Entwicklung von Windows alle alten DOS-Programme weiter lauffähig bleiben sollten, war die Entwicklung von Windows als DOS-Erweiterung von vielen Kompromissen geprägt, die das Ergebnis in den Augen vieler zu einem Flickwerk gerieten ließen, das zu groß, zu instabil und zu ineffizient war. Versionen dieser Entwicklungsreihe sind Windows 95, 98 und ME. Mit Windows NT wurde ein neuer Anfang gemacht, es folgten Windows 2000 und Windows XP, die aktuelle Version ist Windows Vista. Während Windows 98 und Windows ME vorwiegend auf den privaten Anwender-Markt zielten, sollte Windows NT, vom DOS-Ballast befreit, den Firmen- und Server-Markt bedienen. Das seit 2001 verfügbare Windows XP führt die beiden Linien von Windows wieder zusammen. Vista ist eine Weiterntwicklung von XP und soll einfacher zu bedienen sein als alle Vorgänger und besser mit den heutigen Sicherheitsproblemen im Internet umgehen können. Eine Alternative zu Windows, die in letzter Zeit rasant an Popularität gewonnen hat ist Linux. Dieses an UNIX angelehnte Betriebssystem wurde ursprünglich von dem finnischen Studenten Linus Torvalds entworfen und wird seither durch eine beispiellose weltweite Zusammenarbeit tausender enthusiastischer Programmierer fortentwickelt. Der Quellcode für Linux ist frei zugänglich. Mit einer Auswahl von grafischen Benutzeroberflächen ausgestattet, ist dieses System heute genauso einfach zu bedienen wie Windows, hat aber den Vorteil, dass es erheblich effizienter, schneller und stabiler ist als Windows und dazu kostenfrei aus dem Internet erhältlich. Auf CD-ROM geprägte Versionen zusammen mit Tausenden von Anwendungsprogrammen und Handbuch (so genannte Distributionen) sind zu geringen Preisen auch käuflich zu

1.7 Von der Hardware zum Betriebssystem

63

erwerben. Auch im kommerziellen Bereich, insbesondere dort wo Stabilität und Effizienz im Vordergrund stehen, fasst Linux immer mehr Fuß. Insbesondere als Betriebssystem für Webserver ist Linux äußerst beliebt. IBM setzt Linux sogar auf ihren Großrechnern ein. Einen kostenlosen, unverbindlichen und absolut mühelosen Weg, in Linux hinein zu schnuppern bot als erstes Knoppix, ein komplettes graphisches Linux-System auf einer bootfähigen CD. Heute kann man alle gängigen Linux-Distributionen als sogenannte Live-Systeme von USB-Stiften booten und dann sofort loslegen. Alle gängigen Hardwarekomponenten werden automatisch erkannt, alle wichtigen Werkzeuge und Programme – von Internet Browsern und Bürosoftware über wissenschaftliche Satzsysteme (LaTeX), Programmiersprachen und Spiele – sind vorhanden. Es werden keinerlei Änderungen an einem vorhandenen System vorgenommen, so dass nach dem Experimentieren mit Linux mit dem alten Betriebssystem weitergearbeitet werden kann. Auch ohne den Rechner herunterzufahren, kann man heute in ein anderes Betriebssystem wechseln. VMware Server, Virtual PC und VirtualBox sind kostenlose Programme, die einen Standard PC simulieren. Auf diesem virtuellen PC kann man beliebige andere Betriebssysteme installieren. Dies eröffnet einen einfachen Weg, Programme in verschiedenen Betriebssystemumgebungen zu testen, oder gefahrlos im Internet zu surfen. Schadprogramme sind auf den Sandkasten des virtuellen Betriebssystems beschränkt und können diesen nicht verlassen.

1.7.7

Bediensysteme

Ein Bediensystem ist eine Schnittstelle des Betriebssystems zu einem Benutzer, der einfache Dienste über Tastatur oder Maus anfordern kann. Die einfachste Version eines solchen Bediensystems zeigt eine Kommandozeile. Der Benutzer tippt ein Kommando ein, das dann vom Betriebssystem sofort ausgeführt wird. Ein solcher Kommandointerpreter (engl. shell) ist in Form des Programms „cmd.exe“ auch in Windows enthalten. Startet man es, so öffnet sich ein Fenster, das einen Textbildschirm simuliert. Darin kann man jetzt Kommandos eingeben. Um zum Beispiel die Namen aller Dateien im aktuellen Verzeichnis zu sehen, tippt man das Kommando dir ein; um die Datei Brief.doc in Oma.doc umzubenennen, das Kommando: ren Brief.doc Oma.doc. Programme werden durch Eingabe des Programmnamens, ggf. mit Argumenten, gestartet. Allerdings ist diese Möglichkeit, ein Betriebssystem zu betreiben, wenig benutzerfreundlich. Der Benutzer muss die Kommandonamen kennen und fehlerlos eintippen. Ein erster Schritt zur Verbesserung der Benutzerfreundlichkeit von Betriebssystemen sind so genannte Menüsysteme. Die möglichen Aktionen werden dem Benutzer in Form von Kommandomenüs angeboten. Der Benutzer kann sich unter den angebotenen Kommandos das Passende aussuchen und mithilfe weiterer Menüs Einzelheiten oder Parameter eingeben. Ein solches Bediensystem, der Norton Commander, war zu DOS-Zeiten äußerst beliebt. Unter Linux ist er als Midnight Commander (mc) wieder auferstanden, unter Windows gibt es ebenfalls Programme in dieser Tradition, SpeedCommander, Total Commander oder den kostenfreie muCommander.

64

Abb. 1.30:

1 Einführung

Windows-Kommandozeile

Eine wesentliche Verbesserung der Bedienung von Computern wurde erst mithilfe von grafikfähigen Bildschirmen und der Maus als Zeigeinstrument möglich: die fensterorientierte Bedienoberfläche. Sie wurde bereits in den 70er Jahren in dem Forschungszentrum PARC (Palo Alto Research Center) der Firma Xerox in Kalifornien entwickelt. Ebenfalls aus dieser Denkfabrik stammt die so genannte „desktop metapher“, die zum Ziel hat, die Werkzeuge eines Büros (Schreibmaschine, Telefon, Uhr, Kalender, etc.) als grafische Analogien auf dem Rechner nachzubilden, um so einerseits die Scheu vor dem Rechner zu mindern und andererseits einen intuitiveren Umgang mit den Programmen zu ermöglichen. Durch eine einheitliche Gestaltung der Bedienelemente gelingt es heute auch Laien, mit einfachen Programmen sofort arbeiten zu können, ohne lange Handbücher zu wälzen oder Kommandos zu pauken. Historisch wurde das erste grafische Bediensystem (engl.: graphical user interface oder GUI) von der Firma Xerox auf ihren Workstations (Alto, Dorado, Dolphin) angeboten. Der kommerzielle Durchbruch gelang erst 1984 mit den auf dem Xerox-Konzept aufbauenden Macintosh-Rechnern der Firma Apple. Nur langsam zog auch die Firma Microsoft nach. Nach einer gemeinsamen Entwicklung mit IBM entstand zunächst das Betriebssystem OS/2. Dann verließ Microsoft das gemeinsame Projekt und entwickelte, auf DOS aufbauend, Windows 3.1, danach Windows 95, 98 und ME sowie als separates, nicht mehr mit DOS verquicktes Betriebssystem, Windows NT, 2000, XP und Vista. Auch für das Betriebssystem UNIX und seine Abkömmlinge (Linux, SunOs, BSD) sind grafische Benutzeroberflächen mittlerweile Standard. Zunächst hatte jeder Workstation-Hersteller seine eigene Oberfläche. Schließlich konnten sich die wichtigsten Anbieter doch auf ein einheitliches grafisches Bediensystem, das CDE (common desktop environment), einigen. Unter Linux hat der Benutzer die Wahl zwischen verschiedenen grafischen Bediensystemen, dem CDE-clone „KDE“, dem „GNOME“-System (GNU Object Model Environment) oder sogar einem Fenstersystem, das nahezu identisch aussieht und funktioniert wie das von Windows. Grafische Bediensysteme präsentieren sich dem Benutzer in Fenstern. Es können stets mehrere Fenster aktiv sein, die nebeneinander, übereinander oder hintereinander angeordnet sind.

1.7 Von der Hardware zum Betriebssystem

65

Sie können in der Größe verändert, in den Vordergrund geholt oder ikonisiert, d.h. auf minimale Größe verkleinert und in den Hintergrund verdrängt werden.

Abb. 1.31:

KDE-Desktop unter Knoppix

Die Fenster sind dem Bediensystem bzw. dem Betriebssystem selber oder Anwendungsprogrammen zugeordnet. Diese werden auf dem Desktop durch kleine Sinnbilder (icons) repräsentiert. Ein Doppelklick mit der Maus startet das Programm. Dieses öffnet dann ein oder mehrere Fenster. Über Menüs, Eingabefelder, Auswahlknöpfe und Systemmeldungen kann ein Benutzer mit dem Programm kommunizieren. Wenn in einem Fenster ein Ausschnitt aus einem Textdokument gezeigt wird, sind am Fensterrand Symbole zum Verschieben des Ausschnittes zu finden. Die wichtigsten Bedienelemente sind meist in einer Menüleiste am oberen Fensterrand zu finden. Das Anklicken eines Menüabschnittes führt dazu, dass in einem Pulldown-Menü eine umfangreiche Auswahl angeboten wird. Unter der Menüleiste befinden sich vielfach weitere Werkzeugleisten mit häufig verwendeten Kommandos. Es ist wünschenswert, dass alle Anwenderprogramme eines Computersystems einheitliche Auswahlfenster benutzen, so zum Beispiel bei der Dateiauswahl, zur Druckersteuerung, zur Auswahl von Schriftarten etc. Daher gehört es zu den Grundfunktionen eines grafischen Bediensystems, solche Benutzerschnittstellen anzubieten. Jedes Anwendungsprogramm, das darauf zurückgreift, kann davon ausgehen, dass die entsprechenden Programmteile sofort von dem Benutzer bedient werden können.

66

1.8

1 Einführung

Anwendungsprogramme

In den letzten Abschnitten haben wir uns langsam von der Hardware eines Computers über das BIOS und Treiber zum Betriebssystem, darauf aufbauend zu der grafischen Bedienoberfläche mit Fenstern und Maus emporgearbeitet. Wir schließen nun das erste Kapitel mit einem kurzen Blick auf einige wichtige Anwendungsprogramme ab. Zu diesen gehören Textverarbeitungsprogramme, Programme zur Kommunikation mit elektronischer Post (E-Mail) und Browser zur Erkundung des Internets.

1.8.1

Textverarbeitung

Die Erstellung und Bearbeitung von Textdokumenten ist eine der Hauptaufgaben von Rechnern in einer Büroumgebung. Textdokumente können Briefe sein, Rechnungen, Webseiten, mathematisch-technische Veröffentlichungen, Diplomarbeiten oder Bücher. Für die Textverarbeitung in einem Büro haben sich heute Textbearbeitungsprogramme durchgesetzt, die dem Benutzer vor allem ästhetisch ansprechende Ergebnisse versprechen. Dabei wird gefordert, dass zu jedem Zeitpunkt auf dem Bildschirm ein Abbild des Dokuments zu sehen ist, so wie es der Drucker auch ausgeben wird. Für diese Forderung hat man das scherzhafte Schlagwort „wysiwyg“ (what you see is what you get) geprägt. Ein Problem bei der Umsetzung dieses Prinzips ist die unterschiedliche Auflösung, also die Anzahl der Bildpunkte pro Flächeneinheit, von Bildschirmen und Druckern. Die Auflösung wird in der Einheit dpi (dots per inch) angegeben und kann in horizontaler und vertikaler Richtung verschieden sein. In fast allen Fällen haben Drucker eine höhere Auflösung als Bildschirme. Sie werden nur noch von den Lichtsatzmaschinen der Verlagshäuser übertroffen. Preiswerte Tintenstrahldrucker erreichen heute bereits 1200 x 1200 dpi. Allerdings ist ihre Druckqualität auch von der Tröpfchengröße und der Papierqualität abhängig. Demgegenüber hat ein 17”-Bildschirm (Breite ca. 13”, Höhe ca. 10”), dessen Grafikkarte auf eine Darstellung von 1280 x 1024 Pixeln eingestellt ist, bloß eine Auflösung von ca. 100 x 100 dpi.

1.8.2

Zeichen und Schriftarten

Ein wichtiges ästhetisches Merkmal eines Textdokumentes ist die Darstellung der Zeichen. Eine Schriftart, auch font genannt, ist ein gemeinsames Design für alle Buchstaben des Alphabets. Zwar kann man in einem Text (sogar in einem einzelnen Wort) mehrere Schriftarten verwenden, das Ergebnis ist aber häufig: Fontsalat. Man unterscheidet zwischen den Fonts fester Zeichenbreite und den so genannten Proportionalschriften, in denen die Breite der einzelnen Buchstaben, beispielsweise von „m“ und „i“, verschieden ist. Die Schriftart Courier ist ein Font fester Breite, während die meisten populären Fonts, wie z.B. Times Roman, Bookman, Lucida und Garamond Proportionalschriften sind. Ein weiteres ästhetisches Merkmal ist die Verwendung von Serifen, das sind die kleinen Verbreiterungen an den Enden der Striche, die den Buchstaben bilden. Serifenlose Schriften kennzeichnet man mit dem Attribut „sans serif“ oder nur „sans“. Einige Schriftbeispiele sind:

1.8 Anwendungsprogramme

67

Courier, eine Schriftart fester Breite Times Roman, eine Proportionalschrift Bookman, eine breite Proportionalschrift Garamond, eine „leichte“ Proportionalschrift Lucida Sans, eine serifenlose Proportionalschrift. Die meisten Schriften liegen in verschiedenen Schrifttypen wie z.B. kursiv (engl. italic) und fett (engl. bold) vor, aber auch in verschiedenen Schriftgraden. Der Schriftgrad bestimmt die maximale Höhe der einzelnen Zeichen. Aus historischen Gründen wird als Maß meist der typografische Punkt (pt) verwendet: 1 pt = 0,353 mm

1 cm = 28,33 pt

1" = 2,54 cm = 72 pt

Eine Zeile eines Dokumentes besteht nun aus einer Folge von Zeichen, die auf einer gemeinsamen Grundlinie (engl. baseline) angeordnet sind. Einige Zeichen, wie z.B. „g“, „p“, „q“, „y“ und das „f“ im kursiven Schrifttyp (siehe: „Schrifttyp“) unterschneiden die Grundlinie. Zeichen können auch als Superscript oder als Subscript verwendet werden. Dabei werden sie über oder unter die Grundlinie gehoben bzw. gesenkt und automatisch in kleinerer Schriftgröße gesetzt. Wir halten also als wichtigste Attribute von Zeichen fest: • • • •

Schriftart (font) Schrifttyp (normal, fett, kursiv oder unterstrichen), Schriftgrad (die Zeichengröße in pt) und Schrifthöhe (relativ zur Grundlinie).

1.8.3

Formatierung

Ein wichtiges Formatierungsmerkmal eines größeren Textdokumentes ist die Darstellung der Absätze. Ein Absatz ist eine Gruppe von Sätzen, in denen ein Gedanke entwickelt wird. Die Absatzformatierung bestimmt den Umbruch der einzelnen Zeilen innerhalb eines Absatzes, dabei kann eine automatische Silbentrennung durchgeführt werden. Absatzmerkmale sind u.a.: • • • •

linker und rechter Einzug (Abstand vom Rand), der zusätzliche Einzug der ersten Zeile des Absatzes, der Abstand der Zeilen untereinander, die Abstände zum vorigen und zum folgenden Absatz.

Für die Absatzformatierung hat man meist die Auswahl zwischen Flattersatz, Blocksatz oder Zentrierung. Beim Blocksatz wird durch Einfügen von zusätzlichen Wortzwischenräumen dafür gesorgt, dass alle Zeilen auf einer gemeinsamen rechten Randlinie enden. Beim Flattersatz sind alle Wortzwischenräume gleich groß, die Zeilen werden so umgebrochen, dass sie nur ungefähr auf einer gemeinsamen rechten Randlinie enden. Selten, so etwa bei Gedichten und Zitaten, wird ein Absatz zentriert gesetzt. Dabei wird die Zeile um die Absatzmitte zentriert und flattert nach beiden Seiten.

68

1 Einführung Einzug Kapitelüberschrift

Absätze: linksbündig Blocksatz

zentriert

Abb. 1.32:

Anwendungsprogramme

59

Ein Absatz ist eine Gruppe von Sätzen, in denen ein Gedanke entwickelt wird. Ein Unterkapitel besteht aus einer Überschrift und einer Reihe von Absätzen. Ein Kapitel besteht aus einer Kapitelüberschrift und einer Reihe von Unterkapiteln. Dieser Absatz ist in Blocksatz gesetzt. Durch zusätzliche Zwischenräume zwischen den Worten wird erreicht, daß der Text sowohl links- als auch rechtsbündig wird. Werden lange, nichtgetrennte Worte in einem schmalen Absatz verwendet, können manchmal sehr häßliche Lücken zwischen den einzelnen Worten entstehen. Dieser Absatz ist zentriert gesetzt. Dieses Formatierungsmerkmal wird äußerst selten und meist nur in sehr kurzen Absätzen verwendet.

Formatierungsmerkmale einer Seite

Die Seitenformatierung legt das so genannte Layout einer Seite fest. Layout-Eigenschaften sind: • • • • • • •

Definition der Seitengröße, z.B. DIN A4 hoch oder quer, Definition des Bereiches, der zur Textdarstellung verwendet werden darf – also die Ränder links, rechts, oben und unten, Position und Darstellung von Seitenüberschriften und -unterschriften, Art und Position der Seitenzahlen (Paginierung), Anordnung der Fußnoten (am Fuß der Seite, am Ende des Kapitels oder Dokumentes) Unterscheidung von linken und rechten Seiten und die Größe des Bundsteges, Anzahl der Spalten und Abstand der Spalten untereinander.

Die Erstellung eines professionell formatierten Textdokumentes mit verschiedenen Schriftarten, -graden und -typen sowie einem ausgefeilten Seitenlayout, dessen Darstellung auf einem Bildschirm und dessen Druckausgabe sind die Hauptaufgabe eines Textsystems. Daneben gibt es traditionelle Hilfsmittel zur Textbearbeitung, die von guten Textsystemen unterstützt werden. Dazu gehört die Bearbeitung von Tabulatoren, Textbausteinen und Serienbriefen. Letztere ermöglichen die Erstellung mehrerer Briefe mit ähnlichem Inhalt. Dazu wird ein Musterbrief erstellt, der an einigen Positionen Einfügestellen enthält. Der Inhalt der Einfügestellen wird entweder aus einer Steuerdatei oder als direkte Benutzereingabe abgerufen. Weitere Hilfsmittel professioneller Textverarbeitungssysteme sind die automatische Erstellung von Inhaltsverzeichnissen, Stichwortverzeichnissen und die Behandlung von Querverweisen. Bei einem Querverweis (z.B. „siehe Seite 77“) wird ein Verweis auf eine eigens markierte Textstelle gesetzt. Bei jedem Umbruch wird automatisch die richtige Seitenzahl in den Querverweis eingetragen. Sehr nützlich sind auch eine automatische Rechtschreibprüfung sowie ein Thesaurus, der bei der Wahl eines treffenden Wortes behilflich ist. Textdokumente, wie zum Beispiel dieses Buch, enthalten Abbildungen, Tabellen und Illustrationen. Die klassische Methode, diese einzuarbeiten, besteht darin, Lücken zu lassen (der Fachausdruck ist freischlagen) und die getrennt erstellten Illustrationen vor der Erstellung des Druckfilms an der richtigen Stelle einzukleben. In modernen Textsystemen können Ergeb-

1.8 Anwendungsprogramme

69

nisse verschiedenster Anwendungen importiert werden. Noch günstiger ist die dynamische Einbettung, dabei führt ein Anklicken des importierten Objektes zu einem Öffnen des Anwendungsprogrammes, mit dem das Objekt verändert werden kann. In der Windows-Umgebung ist dies als OLE (object linking and embedding) bekannt. Näheres dazu findet sich auf S. 588.

1.8.4

Desktop Publishing

Früher schickte ein Autor das Manuskript seiner Veröffentlichung an einen Verlag. Dort wurde das Werk von einem Schriftsetzer gesetzt, d.h., in einem Druckstock wurden einzelne Buchstaben, die jeweils als metallenes Negativ auf einem Holzblock montiert waren, zu dem Negativ einer Seite zusammengefügt. Mit Keilen wurde der Abstand zwischen Wörtern ausgeglichen. Dabei beachteten Setzer zahlreiche ästhetische Gesichtspunkte. Dazu gehörten u.a. die Verwendung verschiedener Schrifttypen (Fonts), korrekter Zwischenräume bei Absatz, Kapitel und Seitenumbruch, der Einsatz von Ligaturen (Buchstabenverbindung auf einem Typenkörper wie „ft“, historisch auch „ß“) und Unterschneidungen wie z.B. in „Waffel“. Dabei werden zwei Buchstaben zusammengeschoben, bis der eine den anderen überragt. Professionelle Setzer beachteten auch den ästhetischen Gesamteindruck einer gesetzten Seite. Dazu gehört die Vermeidung von Flüssen (vertikales Zusammentreffen von Wortzwischenräumen), Schusterjungen und Hurenkindern. Mit diesen plastischen Begriffen bezeichnet man einen Seitenumbruch nach der ersten Zeile bzw. vor der letzten Zeile eines Absatzes. All diese Kunstfertigkeiten sind gemeinhin dem Autor unbekannt. Setzt er selber sein Werk mithilfe einer einfachen Textverarbeitung, so erkennt man den Unterschied zu einem professionellen Satz leicht an dem nicht immer logischen Einsatz verschiedener Fonts, der nicht konsistenten Verwendung von Abständen und an Feinheiten wie Flüssen, Schusterjungen und Hurenkindern. Im Hinblick auf solche Details kann der Autor am Computer sein Werk oft erst nach dem endgültigen Ausdruck beurteilen, weil bei den meisten Systemen Bildschirmfonts und Druckerfonts verschiedene Maße und verschiedenes Aussehen haben. Was in den Augen des Autors am Bildschirm ansprechend und gut lesbar erschien, kann nach der Druckausgabe unästhetisch wirken.

1.8.5

Textbeschreibungssprachen

Aufgrund der geschilderten Schwierigkeiten ist es eine gute Idee, ein Textdokument logisch zu beschreiben. Dies soll bedeuten, dass man die einzelnen Textteile entsprechend ihrer Rolle oder Funktion in dem Dokument markiert. Ein Textteil kann zum Beispiel eine Kapitelüberschrift, eine Abschnittsüberschrift, eine Definition, ein betonter Ausdruck, ein Element einer Aufzählung, eine Fußnote, ein Zitat, eine mathematische Formel, eine Gleichung, ein Gleichungssystem u. ä. sein. Statt jedes Mal festzulegen, in welchem Stil der aktuelle Textteil zu setzen ist, markiert man die Textbereiche nach ihrer logischen Funktion. Das Textverarbeitungssystem übernimmt dann die angemessene Formatierung („form follows function“). Die Parameter, die diese Übersetzung bestimmen, kann man meist in einer so genannten Dokumentvorlage (engl. style file, style sheet) festlegen.

70

1 Einführung

Ein weiterer Vorteil dieser Vorgehensweise ist, dass man zur Erstellung eines professionellen Textdokumentes wenige Ressourcen, ja nicht einmal einen Grafikbildschirm benötigt. Die einzelnen Textbestandteile werden durch Kommandos markiert. Dies ist die Vorgehensweise des Satzsystems TeX/LaTeX von Donald Knuth, das seit über 20 Jahren das System der Wahl für praktisch alle Bücher und Zeitschriften in Mathematik, theoretischer Informatik, Physik und Chemie ist. Viele Zeitschriften oder Konferenzen in diesen Gebieten akzeptieren Manuskripte nur in Form von LaTeX-Dateien. LaTeX-Befehle beginnen mit einem backslash „\“, mögliche Parameter werden in geschweifte Klammern „{“ und „}“ eingeschlossen. Für besondere Textbereiche wie z.B. Zitate, Gedichte, Definitionen, Sätze, Beweise, können gesonderte Stile definiert werden. Solche Bereiche werden mit \begin{bereichsname} und \end{bereichsname} umschlossen. Das folgende Beispiel zeigt ein komplettes LaTeX-Dokument im Quelltext: \documentclass[a4,12pt]{article} \newtheorem{theorem}{Satz} \begin{document} \title{Der Satz von Gau\ss.} \date{} \maketitle \begin{abstract} Mit vollst\"andiger Induktion zeigen wir die Gau\ss sche Formel f\"ur die Summe aller Zahlen von $0$ bis $n$. \end{abstract} \section{Der Satz} Die Gau\ss sche Formel sollte jeder kennen, auch in der Informatik kommt sie immer wieder vor. \begin{theorem} Sei $n \in \mathbf{N} $, dann gilt f\"ur die Summe aller Zahlen von $0$ bis $n$: \[ \sum_{i = 0}^{n}{i} = \frac{n(n+1)}{2}. \] \end{theorem} \noindent\underline{Beweis}: F\"ur den Induktionsanfang $n=0$ setzen wir ein und erhalten $\sum_{i = 0}^{0}{i} = 0 = {0(0+1)}/{2}$. Als Induktionshypothese nehmen wir an, da\ss\ die Formel f\"ur $n= 0 \ldots k$ schon gilt und zeigen f\"ur $n=k+1$: \begin{eqnarray}\sum_{i=0}^{k+1}{i} &=& \sum_{i=0}^{k}{i} + (k+1) \\ &=& \frac{k(k+1)}{2} + (k+1) \\ &=& \frac{(k+2)(k+1)}{2} \\ &=& \frac{n(n+1)}{2}\ . \end{eqnarray} \end{document} LaTeX produziert aus dem gezeigten Quelltext eine Datei mit der Endung .dvi. Die Abkürzung steht für „device independent“, also „geräteunabhängig“. Die dvi-Datei beschreibt die forma-

1.8 Anwendungsprogramme

71

tierte Ausgabe, also die Position, Größe und Schriftart der einzelnen Zeichen. Die Formen dieser Zeichen, also die fonts, werden erst bei der Wiedergabe der dvi-Datei, etwa durch einen Bildschirm-Betrachter oder durch einen Druckertreiber, beigefügt. Dateien im dvi-Format können auch leicht in Postscript, pdf oder ähnliche standardisierte Formate umgewandelt werden. Das Ergebnis des obigen Quellcodes ist in der folgenden Abbildung wiedergegeben.

DER SATZ VON GAUSS. Abstract. Mit vollst¨andiger Induktion zeigen wir die Gauß sche Formel f¨ ur die Summe aller Zahlen von 0 bis n.

1. Der Satz Die Gauß sche Formel sollte jeder kennen, auch in der Informatik kommt sie immer wieder vor. Theorem. Sei n ∈ N, dann gilt f¨ ur die Summe aller Zahlen von 0 bis n: n " n(n + 1) i= . 2 i=0 F¨ ur den Induktionsanfang n = 0 setzen wir ein und erhalten !Beweis: 0 i=0 i = 0 = 0(0 + 1)/2. Als Induktionshypothese nehmen wir an, daß die Formel f¨ ur n = 0 . . . k schon gilt und zeigen f¨ ur n = k + 1: (1)

k+1 "

i =

i=0

(2) (3) (4)

k "

i + (k + 1)

i=0

k(k + 1) + (k + 1) 2 (k + 2)(k + 1) = 2 n(n + 1) . = 2

=

1

Abb. 1.33:

Aus dem LaTeX Quelltext erzeugtes pdf-Dokument

Mit LaTeX kann man nicht nur mathematische und chemische Formeln erstellen, sondern auch beliebige Diagramme und Graphiken. Alternativ lassen sich in anderen Formaten

72

1 Einführung

erzeugte Dokumente einbetten. TeX und LaTeX sind frei und kostenlos verfügbar. Dazu gibt es unzählige Erweiterungen, die man sich bei Bedarf von der Seite www.Dante.de herunterladen kann. Dort finden sich auch komplett vorkonfigurierte LaTeX-Systeme, wie z.B. MikTeX, die ohne Probleme unter Windows oder unter Linux lauffähig sind. Während eingefleischte und langjährige Nutzer von LaTeX darauf schwören, ihre Dokumente nur im Quellcode, mit expliziten Formatierungskommandos wie oben gezeigt, zu verfassen, und dann zu kompilieren, um das Ergebnis (oder die erzeugte Fehlermeldungen) zu betrachten, gibt es seit kurzer Zeit eine sehr interessante Alternative in dem kostenfreien Editor LyX. Dieser erzeugt bereits beim Editieren eine angenäherte Vorschau des fertigen Dokumentes, inklusive der mathematischen Formeln. Das Prinzip wird auch als wysiwym (what you see is what you mean) bezeichnet. Anfänger können alle Befehle und Formelgestaltungen über Formatvorlagen und Menüs erledigen, während fortgeschrittene Benutzer stattdessen auch ihre gewohnten Latex-Kommandos verwenden können. Sobald das Kommando eingetippt ist, bzw. der Cursor den Formelbereich verlässt, erscheint die fertige Ausgabe. Bis auf den Zeilen- und den Seitenumbruch ist der Inhalt des Editors kaum vom fertigen Dokument zu unterscheiden, so dass der Umgang mit LyX heute genauso einfach ist, wie der Umgang mit Open Office Writer, Staroffice, oder Microsoft Word.

Abb. 1.34:

LyX beim Editieren der ersten Formel

1.8 Anwendungsprogramme

73

TeX/LaTeX ist ein Beispiel einer Auszeichnungssprache (engl. markup language), also einer formalen Sprache zur logischen Beschreibung von Dokumenten. In solchen Auszeichnungssprachen beschriebene Dokumente können ohne Änderung auf verschiedenen Medien mit verschiedenen Seitengrößen ein- oder mehrspaltig ausgegeben werden. Aus der logischen Beschreibung und den Dimensionen des Ausgabemediums kann eine ästhetische Formatierung automatisch erstellt werden. Dieser Kerngedanke wurde vor wenigen Jahren wieder aufgegriffen, als eine Sprache für die Beschreibung von Internet-Dokumenten benötigt wurde. Auch hier hat jeder Betrachter einen anderen Bildschirm, mancher gar nur einen Textbildschirm. Daher bot sich auch hier die Verwendung einer Auszeichnungssprache an – man entwickelte HTML (hypertext markup language), Näheres dazu findet sich im Abschnitt „HTML“ auf S. 665. Zusätzlich zu den üblichen Auszeichnungsbefehlen besitzt HTML noch Verweise (engl. link), die auf andere Dokumente verweisen. Jeder Link wird im Dokument verankert. Ein Anker kann ein Stück Text, ein Bild oder eine Grafik sein. Die Anker werden in der Darstellung meist hervorgehoben, so dass der Benutzer erkennt: Ein Mausklick auf diesen Anker führt sofort zu dem Dokument, auf das der entsprechende Link verweist. Auf diese Weise entsteht ein Netz von untereinander verknüpften Multimedia-Dokumenten, das beste Beispiel dafür ist das World Wide Web.

1.8.6

Tabellenkalkulation: spread sheets

Rechnen wie auf einem Blatt Papier – das ist die Grundidee aller Tabellenkalkulationsprogramme (engl. spread sheet). Viele Berechnungen werden mit einem Taschenrechner ausgeführt, die Zwischenergebnisse werden auf einem karierten Blatt Papier aufgeschrieben. Das Ergebnis sind einzelne Werte oder eine Tabelle von Werten. Ein Programm namens VisiCalc hat die Durchführung solcher Arbeiten erstmals mit einem Computer ohne das besagte Blatt Papier möglich gemacht und so verallgemeinert, dass eine Anzahl neuer und zusätzlicher Anwendungen eröffnet wurden. Aus historischen Gründen ist Microsoft Excel am weitesten verbreitet, obwohl das kostenfreie Open Office Calc diesem mittlerweise ebenbürtig ist, aber neben dem proprietären Excel Format auch das offene Open Document Format unterstützt. Auf dem Bildschirm werden Arbeitsfelder gezeigt, die in einer Tabelle angeordnet sind. Die Felder können benannt werden, ansonsten werden sie durch Angabe ihrer Position (Zeile und Spalte) angesprochen. So bezeichnet z.B. „D2“ das Feld in Zeile 2 und Spalte D, also in der vierten Spalte. Jedes Feld verhält sich wie ein eigenständiger Taschenrechner, dessen Anzeige in diesem Feld zu sehen ist. Der Wert eines Feldes kann entweder als Konstante direkt über die Tastatur eingegeben werden, oder er ergibt sich aufgrund einer Formel aus den Werten anderer Felder. Als Beispiel für eine Tabellenkalkulation betrachten wir eine Aufgabe aus dem Gebiet der Zinsrechnung. Wir beginnen mit der Eingabe von Werten in eine Tabelle:

Abb. 1.35:

Tabelle: Ausgangsdaten

74

1 Einführung

Tragen wir in Feld D2 die Formel „=B2*E1“ ein, so erscheint dort sofort das Ergebnis 570. Bei jeder Änderung eines der Felder, von denen D2 abhängig ist, also von B2 oder von E1, passt sich sofort auch der Wert von D2 entsprechend an. Da die nächste Zeile unseres Beispiels die Situation im folgenden Jahr widerspiegeln soll, tragen wir dort ein: in A3: =A2+1

in B3: =B2-C2+D2

in C3: =C2

in D3: =B3*E1

Die Tabelle sieht danach folgendermaßen aus:

Abb. 1.36:

Tabelle - nach „Kopieren“ der ersten Zeile

Wir könnten mit der nächsten Zeile so weitermachen, aber es wird langweilig, da die dort einzutragenden Formeln sich von denen in der aktuellen Zeile nur anhand der Zeilennummern der referenzierten Zeilen unterscheiden. So wird aus =B2-C2+D2 in B3 lediglich =B3-C3+D3 in B4. Diese Situation ist typisch für Tabellen, daher wird eine solche Anpassung automatisch durchgeführt, wenn wir die Formel aus B3 nach B4 kopieren. In der Formel für B3 kennzeichnet nämlich C2 in Wirklichkeit einen relativen Bezug auf das Feld „eine Zeile höher und eine Spalte weiter rechts“. Die Formel in B3 kann also gelesen werden als = - + . Bei dieser Lesart sind tatsächlich die Formeln in B3, B4, B5, ... identisch und können einfach kopiert werden. Relative Bezüge passen sich automatisch an, wenn eine Formel kopiert wird. In unserem Beispiel ist dies jedoch an einer Stelle nicht erwünscht: Der Zinssatz soll immer dem Feld E1 entnommen werden, nie aus E2, oder E3, denn diese Felder bleiben leer. Der Bezug auf E1 soll daher ein absoluter Bezug sein, er wird durch die Notation „$E$1“ als solcher gekennzeichnet. Korrigieren wir den Inhalt von D3 zu: =B3*$E$1, so können wir auch den Inhalt dieses Feldes nach D4 kopieren. Dort lesen wir dann die korrekte Formel =B4*$E$1. Auf diese Weise entsteht allein durch Kopieren und Einfügen (copy/paste) der Zeile 3 in alle folgenden Zeilen sofort die komplette Tabelle. Diese wurde dann durch Formatierung der Zellen als „Währung € mit 2 Nachkommastellen“ noch etwas verschönert. Die Abhängigkeit eines Feldes von den Werten anderer Felder ist die Grundidee aller Tabellenkalkulationsprogramme. Wenn sich ein Wert einer Tabelle ändert, kann das Auswirkungen auf alle anderen Felder haben. Deswegen werden bei jeder Änderung alle Felder der Tabelle neu berechnet. Auf diese Weise können Planspiele nach dem Motto: Was wäre, wenn ...

1.8 Anwendungsprogramme

75

durchgeführt werden. In dem obigen Beispiel könnte man z.B. den Zinssatz oder die jährliche Tilgung ändern und die Auswirkung sofort beobachten.

Abb. 1.37:

Tabelle nach „Kopieren“ und Formatieren

Die Reihenfolge der Berechnung der Felder kann bei der Auswertung der Tabelle eine Rolle spielen. Nehmen Felder wechselseitig aufeinander Bezug, können mehrere iterative Neuberechnungen der Tabelle notwendig sein, bis sich die Ergebnisse zweier aufeinander folgender Neuberechnungen nicht mehr signifikant unterscheiden. Mithilfe von Tabellenkalkulationsprogrammen können beliebig komplexe Berechnungen zu Finanzplanung, Etatplanung, Gewinn- und Verlustrechnung, Budgetkontrolle, Preiskalkulation, Stundensatzabrechnung etc. durchgeführt werden. Durch die Änderung einzelner Parameter kann das Modell dann jeweils in verschiedenen Varianten erprobt werden. Nützliche mathematische oder statistische Modelle können auch ohne tiefere Programmierkenntnisse erstellt werden. Eine umfangreiche Sammlung vordefinierter Funktionen aus Mathematik, Statistik und Finanzwirtschaft steht ebenfalls zur Verfügung. Die Ergebnisse einer Tabellenkalkulation können schließlich auf vielfältige Weise grafisch aufbereitet und in Publikationen importiert werden.

1.8.7

Vom Fenster zur Welt zur zweiten Welt

Die aufregendste Entwicklung der Informatik in den letzten zwanzig Jahren hat sich im Bereich des Internets abgespielt. Insbesondere seit der Entwicklung des World Wide Web (WWW) kann jeder Personal Computer als Fenster zur Welt genutzt werden. Einen Zugang zum Internet via DSL (oder mit Modem bzw. ISDN-Karte) vorausgesetzt, stehen unerschöpfliche Mengen an nützlichen und überflüssigen Informationen, Texten, Bildern, Filmen, Musikstücken, Programmen etc. zur Verfügung. Jede Firma, jede Institution bietet Informationen, Selbstdarstellungen, oft auch kommerzielle Angebote, die von jedem anderen Rechner in der Welt gelesen werden können. Menschen aus den entlegensten Gegenden der Welt können miteinander plaudern (chatten), spielen, Musik, Filme (youTube) und Daten austauschen oder mithilfe einer Kamera (webcam) und eines Mikrofons ohne Zusatzkosten bild-telefonie-

76

1 Einführung

ren (skype). Man kann Bücher kaufen, an Auktionen teilnehmen, Flüge buchen und via Online-Kamera das Wetter und den Wellengang am Urlaubsort beobachten. Interessant sind auch die neuen Möglichkeiten, ausgefeilte Programme über das Netz zu benutzen, ohne sie auf dem eigenen Rechner installiert zu haben. Es begann mit email-Programmen, die verschiedene Provider frei anbieten und setzte sich fort über Kalender, Textverarbeitungsprogramme, Spreadsheets und Präsentationsprogramme, die mittlerweile alle wirklich benötigten Funktionen der entsprechenden Office-Pakete anbieten. So kann man unabhängig von einem Rechner an jedem Ort der Welt und von jedem Rechner mit Internetzugang seine Termine, Daten, Spreadsheets und Präsentationen alleine oder mit Mitarbeitern auf irgendeinem anderen Kontinent gemeinsam bearbeiten. Mit einem mobilen Kleinstrechner (NetBook), der im wesentlichen als Interface zum Internet benutzt wird, kann man heute prinzipiell von jedem Ort der Welt, der nur einen WLAN Anschluss hat, sein Büro führen. Die Daten lagern bei einem Provider, wie z.B. Google, der sich um die Haltung und Sicherung der Daten kümmert. Allerdings setzt dies ein hohes Maß an Vertrauen in die Firma voraus und in deren Mitarbeiter, die mit den Daten evtl. Missbrauch treiben könnten. In den letzten Jahren ist das Internet immer mehr auch zu einem sozialen Medium geworden. Menschen mit ähnlichen Interessen kommunizieren, geben Ratschläge, knüpfen Beziehungen, spielen mit- oder gegeneinander oder führen gar ein zweites virtuelles Leben. In erfolgreiche Portale wie FaceBook, xing, youToube, StudiVZ, etc. investieren Firmen unglaubliche Summen. In Second Life können Benutzer in eine zweite virtuelle Persönlichkeit schlüpfen, die frei von den Unzulänglichkeiten der eigenen ist. Sie können virtuelle Freunde treffen, virtuelle Feste feiern, sich virtuell verlieben. Allerdings benötigt man reales Geld um die virtuelle Persönlichkeit angemessenen mit virtuellen Kleidern, Autos, Häusern, etc. auszustatten.

Abb. 1.38:

Second Life

Möglich wurde diese neue Qualität der Kommunikation durch zwei Dinge – die weltweite Vernetzung von Rechnern über ein TCP/IP genanntes Protokoll (Datenaustauschverfahren) und durch HTML, eine Beschreibungssprache für vernetzte Internet-Dokumente zusammen mit Betrachtern für solche Dokumente, so genannten Browsern. Der dem Internet zugrundeliegenden Technik werden wir ein eigenes Kapitel in diesem Buch widmen.

2

Grundlagen der Programmierung

In diesem Kapitel bereiten wir die Grundlagen für ein systematisches Programmieren. Wichtigstes Ziel ist dabei die Herausarbeitung der fundamentalen Konzepte von Programmiersprache. Wir benutzen bereits weitgehend die Syntax von Java, obwohl in dieser Sprache die Trennlinien zwischen einigen grundlegenden Konzepten von Programmiersprachen, wie z.B. zwischen Ausdruck und Anweisung nicht mehr so deutlich zu erkennen sind, wie bei der vor allem aus didaktischen Erwägungen konzipierten Sprache Pascal. Gelegentlich stellen wir aber den Java-Notationen die entsprechende Pascal-Syntax gegenüber, auch um zu zeigen, dass nicht immer das aus wissenschaftlicher Sicht bessere Konzept sich auch in der Praxis durchsetzt. Insbesondere was die Syntax einer Sprache angeht, hat Java bewusst an die Sprache C angeknüpft, vor allem, weil für viele C-Programmierer damit der initiale Aufwand, in eine andere Sprache umzusteigen, gering war. Obwohl wir also Java als Vehikel für die Vorstellung der wichtigsten Programmiersprachenkonzepte benutzen und obwohl wir auch schon zeigen, wie man die vorgestellten Konzepte möglichst einfach testen kann, bleibt eine umfassende Einführung in Java dem folgenden Kapitel vorbehalten. Wir beginnen mit einer Erläuterung der Begriffe Spezifikation, Algorithmus und Abstraktion. Der Kern einer Programmiersprache, Datenstrukturen, Speicher, Variablen und fundamentale Kontrollstrukturen schließt sich an. Einerseits ist unsere Darstellung praktisch orientiert – die Programmteile kann man sofort ausführen – andererseits zeigen wir, wie die Konzepte exakt mathematischen Begriffsbildungen folgen. Der Leser erkennt, wie zusätzliche Kontrollstrukturen aus dem Sprachkern heraus definiert werden, wie Typkonstruktoren die mathematischen Mengenbildungsoperationen nachbilden und wie Ojekte und Klassen eine systematische Programmentwicklung unterstützen. Zusätzlichen theoretischen Konzepten, Rekursion und Verifikation, sind jeweils eigene Unterkapitel gewidmet. Der eilige Leser mag sie im ersten Durchgang überfliegen, ein sorgfältiges Studium lohnt sich aber in jedem Fall. Rekursion gibt dem mathematisch orientierten Programmierer ein mächtiges Instrument in die Hand. Ein Verständnis fundamentaler Konzepte der Programmverifikation, insbesondere von Invarianten, führt automatisch zu einer überlegteren und zuverlässigeren Vorgehensweise bei der Programmentwicklung.

78

2.1

2 Grundlagen der Programmierung

Programmiersprachen

Die Anweisungen, die wir dem Computer geben, werden als Text formuliert, man nennt jeden solchen Text ein Programm. Programme nehmen Bezug auf vorgegebene Datenbereiche und auf Verknüpfungen, die auf diesen Datenbereichen definiert sind. Allerdings, und das ist ein wichtiger Aspekt, können innerhalb eines Programmes nach Bedarf neue Datenbereiche und neue Verknüpfungen auf denselben definiert werden. Der Programmtext wird nach genau festgelegten Regeln formuliert. Diese Regeln sind durch die sogenannte Grammatik einer Programmiersprache festgelegt. Im Gegensatz zur Umgangssprache verlangen Programmiersprachen das exakte Einhalten der Grammatikregeln. Jeder Punkt, jedes Komma hat seine Bedeutung, selbst ein kleiner Fehler führt dazu, dass das Programm als Ganzes nicht verstanden wird. In frühen Programmiersprachen standen die verfügbaren Operationen eines Rechners im Vordergrund. Diese mussten durch besonders geschickte Kombinationen verbunden werden, um ein bestimmtes Problem zu lösen. Moderne höhere Programmiersprachen orientieren sich stärker an dem zu lösenden Problem und gestatten eine abstrakte Formulierung des Lösungsweges, der die Eigenarten der Hardware, auf der das Programm ausgeführt werden soll, nicht mehr in Betracht zieht. Dies hat den Vorteil, dass das gleiche Programm auf unterschiedlichen Systemen ausführbar ist. Noch einen Schritt weiter gehen so genannte deklarative Programmiersprachen. Aus einer nach bestimmten Regeln gebildeten mathematischen Formulierung des Problems wird automatisch ein Programm erzeugt. Im Gegensatz zu diesen problemorientierten Sprachen nennt man die klassischen Programmiersprachen auch befehlsorientierte oder imperative Sprachen. Zu den imperativen Sprachen gehören u.a. BASIC, Pascal, C, C++ und Java, zu den deklarativen Sprachen gehören z.B. Prolog, Haskell und ML. Allerdings sind die Konzepte in der Praxis nicht streng getrennt. Die meisten imperativen Sprachen enthalten auch deklarative Konzepte (z.B. Rekursion), und die meisten praktisch relevanten deklarativen Sprachen beinhalten auch imperative Konzepte. Kennt man sich in einer imperativen Sprache gut aus, so ist es nicht schwer eine andere zu erlernen, ähnlich geht es auch mit deklarativen Sprachen. Der Umstieg von der imperativen auf die deklarative Denkweise erfordert einige Übung, doch zahlt sich die Mühe auf jeden Fall aus. Deklarative Sprachen sind hervorragend geeignet, in kurzer Zeit einen funktionierenden Prototypen zu erstellen. Investiert man dagegen mehr Zeit für die Entwicklung, so gelingt mit imperativen Sprachen meist eine effizientere Lösung.

2.1.1

Vom Programm zur Maschine

Programme, die in einer höheren Programmiersprache geschrieben sind, können nicht unmittelbar auf einem Rechner ausgeführt werden. Sie sind anfangs in einer Textdatei gespeichert und müssen erst in Folgen von Maschinenbefehlen übersetzt werden. Maschinenbefehle sind elementare Operationen, die der Prozessor des Rechners unmittelbar ausführen kann.

2.1 Programmiersprachen

79

Sie beinhalten zumindest Befehle, um • • • •

Daten aus dem Speicher zu lesen, elementare arithmetische Operationen auszuführen, Daten in den Speicher zu schreiben, die Berechnung an einer bestimmten Stelle fortzusetzen (Sprünge).

Die Übersetzung von einem Programmtext in eine Folge solcher einfacher Befehle (auch Maschinenbefehle oder Maschinencode genannt), wird von einem Compiler durchgeführt. Das Ergebnis ist ein Maschinenprogramm, das in einer als ausführbar (engl. executable) gekennzeichneten Datei gespeichert ist. Eine solche ausführbare Datei muss noch von einem Ladeprogramm in den Speicher geladen werden, und kann erst dann ausgeführt werden. Ladeprogramme sind im Betriebssystem enthalten, der Benutzer weiß oft gar nichts von deren Existenz. So sind in den WindowsBetriebssystemen ausführbare Dateien durch die Endung „.exe“ oder „.com“ gekennzeichnet. Tippt man auf der Kommandozeile den Namen einer solchen Datei ein und betätigt die „Return“-Taste, so wird die ausführbare Datei in den Hauptspeicher geladen und ausgeführt.

2.1.2

Virtuelle Maschinen

Die Welt wäre einfach, wenn sich alle Programmierer auf einen Rechnertyp und eine Programmiersprache einigen könnten. Man würde dazu nur einen einzigen Compiler benötigen. Die Wirklichkeit sieht anders aus. Es gibt (aus gutem Grund) zahlreiche Rechnertypen und noch viel mehr verschiedene Sprachen. Fast jeder Programmierer hat eine starke Vorliebe für eine ganz bestimmte Sprache und möchte, dass seine Programme auf möglichst jedem Rechnertyp ausgeführt werden können. Bei n Sprachen und m Maschinentypen würde dies n × m viele Compiler erforderlich machen.

Pascal

Sun

C/C++ Prolog

PC

Basic LISP

Apple

... Abb. 2.1:

n × m viele Compiler

80

2 Grundlagen der Programmierung

Schon früh wurde daher die Idee geboren, eine virtuelle Maschine V zu entwerfen, die als gemeinsames Bindeglied zwischen allen Programmiersprachen und allen konkreten Maschinensprachen fungieren könnte. Diese Maschine würde nicht wirklich gebaut, sondern man würde sie auf jedem konkreten Rechner emulieren, d.h. nachbilden. Für jede Programmiersprache müsste dann nur ein Compiler vorhanden sein, der Code für V erzeugt. Statt n × m vieler Compiler benötigte man jetzt nur noch n Compiler und m Implementierungen von V auf den einzelnen Rechnertypen, insgesamt also nur n + m viele Übersetzungsprogramme – ein gewaltiger Unterschied.

Pascal

Sun

C/C++ Prolog

Virtuelle Maschine

Java

PC

Smalltalk Apple

... Abb. 2.2:

Traum: Eine gemeinsame virtuelle Maschine

Leider ist eine solche virtuelle Maschine nie zu Stande gekommen. Neben dem Verdacht, dass ihr Design eine bestimmte Sprache oder einen bestimmten Maschinentyp bevorzugen könnte, stand die begründete Furcht im Vordergrund, dass dieses Zwischenglied die Geschwindigkeit der Programmausführung beeinträchtigen könnte. Außerdem verhindert eine solche Zwischeninstanz, dass spezielle Fähigkeiten eines Maschinentyps oder spezielle Ausdrucksmittel einer Sprache vorteilhaft eingesetzt werden können.

Sun

Java

Virtuelle Java Maschine

PC

Apple Abb. 2.3:

Realität: Virtuelle Java-Maschine

2.1 Programmiersprachen

81

Im Zusammenhang mit einer festen Sprache ist das Konzept einer virtuellen Maschine jedoch mehrfach aufgegriffen worden – jüngst wieder in der objektorientierten Sprache Java, der das ganze nächste Kapitel gewidmet sein wird. Ein Java-Compiler übersetzt ein in Java geschriebenes Programm in einen Code für eine virtuelle Java-Maschine. Auf jeder Rechnerplattform, für die ein Emulator für diese virtuelle Java-Maschine verfügbar ist, wird das Programm dann lauffähig sein. Weil man also bewusst auf die Ausnutzung besonderer Fähigkeiten spezieller Rechner verzichtet, wird die Sprache plattformunabhängig. Eine virtuelle Maschine allein für Windows Betriebssysteme, die dafür aber eine Zwischensprache für eine großes Spektrum von Hochsprachen bereitstellt – von Visual Basic über C++ bis C# (Microsoft’s Java Konkurrent) – bietet die .NET-Plattform von Microsoft.

2.1.3

Interpreter

Ein Compiler übersetzt immer einen kompletten Programmtext in eine Folge von Maschinenbefehlen, bevor die erste Programmanweisung ausgeführt wird. Ein Interpreter dagegen übersetzt immer nur eine einzige Programmanweisung in ein kleines Unterprogramm aus Maschinenbefehlen und führt dieses sofort aus. Anschließend wird mit der nächsten Anweisung genauso verfahren. Interpreter sind einfacher zu konstruieren als Compiler, haben aber den Nachteil, dass ein Befehl, der mehrfach ausgeführt wird, jedesmal erneut übersetzt werden muss. Grundsätzlich können fast alle Programmiersprachen compilierend oder interpretierend implementiert werden. Trotzdem gibt es einige, die fast ausschließlich mit Compilern arbeiten. Dazu gehören Pascal, Modula, COBOL, Fortran, C, C++ und Ada. Andere, darunter BASIC, APL, LISP und Prolog, werden überwiegend interpretativ bearbeitet. Sprachen wie Java und Smalltalk beschreiten einen Mittelweg zwischen compilierenden und interpretierenden Systemen – das Quellprogramm wird in Code für die virtuelle Java- bzw. Smalltalk-Maschine, so genannten Bytecode, compiliert. Dieser wird von der virtuellen Maschine dann interpretativ ausgeführt. Damit ist die virtuelle Maschine nichts anderes als ein Interpreter für Bytecode.

2.1.4

Programmieren und Testen

Ein Programm ist ein Text und wird wie jeder Text mit einem Textverarbeitungsprogramm erstellt und in einer Datei gespeichert. Anschließend muss es von einem Compiler in Maschinencode übersetzt werden. Üblicherweise werden während dieser Übersetzung bereits Fehler erkannt. Die Mehrzahl der dabei entdeckten Fehler sind so genannte Syntaxfehler. Sie sind Rechtschreib- oder Grammatikfehlern vergleichbar – man hat sich bei einem Wort vertippt oder einen unzulässigen Satzbau (Syntax) verwendet. Eine zweite Art von Fehlern, die bereits beim Compilieren erkannt werden, sind Typfehler. Sie entstehen, wenn man nicht zueinander passende Dinge verknüpft – etwa das Alter einer Person zu ihrer Hausnummer addiert oder einen Nachnamen an einer Stelle speichert, die für eine Zahl reserviert ist. Programmiersprachen unterscheiden sich sehr stark darin, ob und wie sie solche Fehler erkennen. Syntaxfehler kann man sofort verbessern und dann einen erneuten Compilierversuch machen. Sobald das Programm fehlerlos compiliert wurde, liegt es als Maschinenprogramm vor und kann testweise ausgeführt werden.

82

2 Grundlagen der Programmierung

Dabei stellen sich oft zwei weitere Arten von Fehlern heraus. •



Laufzeitfehler entstehen, wenn beispielsweise zulässige Wertebereiche überschritten werden, wenn durch 0 dividiert oder die Wurzel einer negativen Zahl gezogen wird. Laufzeitfehler können i.A. nicht von einem Compiler erkannt werden, denn der konkrete Zahlenwert, mit dem gearbeitet wird, steht oft zur Compilezeit nicht fest, sei es, weil er von der Tastatur abgefragt oder sonstwie kompliziert errechnet wird. Denkfehler werden sichtbar, wenn ein Programm problemlos abläuft, aber eben nicht das tut, was der Programmierer ursprünglich im Sinn hatte. Denkfehler können natürlich nicht von einem Compiler erkannt werden.

Einen Fehler in einem Programm nennt man im englischen Jargon auch bug. Das Suchen und Verbessern von Fehlern in der Testphase heißt konsequenterweise debugging. Laufzeitfehler und Denkfehler können bei einem Testlauf sichtbar werden, sie können aber auch alle Testläufe überstehen. Prinzipiell gilt hier immer die Aussage von E. Dijkstra: Durch Testen kann man die Anwesenheit, nie die Abwesenheit von Fehlern zeigen. Dennoch werden bei den ersten Tests eines Programms meist Fehler gefunden, die dann einen erneuten Durchlauf des Zyklus Editieren – Compilieren – Testen erforderlich machen. Die Hoffnung ist, dass dieser Prozess zu einem Fixpunkt, dem korrekten Programm, konvergiert. Editieren Testen (Debugging) Abb. 2.4:

2.1.5

Compilieren

Zyklus der Programmentwicklung

Programmierumgebungen

Interpretierende Systeme vereinfachen die Programmerstellung insofern, als die Compilationsphase entfällt und auch kleine Programmteile interaktiv getestet werden können, sie erreichen aber nur selten die schnelleren Programmausführzeiten compilierender Systeme. Außerdem findet bei vielen interpretierenden Systemen keine Typüberprüfung statt, so dass Typfehler erst zur Laufzeit entdeckt werden. Einen Kompromiss zwischen interpretierenden und compilierenden Systemen stellte als erstes das Turbo-Pascal System dar. Der in das Entwicklungssystem eingebaute Compiler war so schnell, dass der Benutzer den Eindruck haben konnte, mit einem interpretierenden System zu arbeiten. Für fast alle Sprachen gibt es heute ähnlich gute „integrierte Entwicklungssysteme“ (engl.: integrated development environment – IDE), die alle zur Programmerstellung notwendigen Werkzeuge zusammenfassen: • • •

einen Editor zum Erstellen und Ändern eines Programmtextes, einen Compiler bzw. Interpreter zum Ausführen von Programmen, einen Debugger für die Fehlersuche in der Testphase eines Programms.

2.1 Programmiersprachen

83

Kern dieser Systeme ist immer ein Text-Editor zum Erstellen des Programmtextes. Dieser hebt nicht nur die Schlüsselworte der Programmiersprache farblich hervor, er markiert auch zugehörige Klammerpaare und kann auf Wunsch den Programmtext auch übersichtlich formatieren. Klickt man auf den Namen einer Variablen oder einer Funktion, so wird automatisch deren Definition im Text gefunden und angezeigt. Soll das zu erstellende Programm zudem eine moderne Benutzeroberfläche erhalten, so kann man diese mit einem GUI-Editor erstellen, indem man Fenster, Menüs, Buttons und Rollbalken mit der Maus herbeizieht, beliebig positioniert und anpasst. Für Java sind u.a. die Systeme Eclipse, (www.eclipse.org), netbeans (www.netbeans.org), Sun ONE Studio 4 (www.sun.com), JCreator (www.jcreator.com), JBuilder (www.borland.com) kostenlos erhältlich. Für Anfänger ist das BlueJ-System (www.bluej.org) zu empfehlen.

2.1.6

Pascal

Pascal ist eine Programmiersprache, die zwischen 1968 und 1974 von Niklaus Wirth an der ETH in Zürich für Unterrichtszwecke entwickelt wurde. Es hat eine einfache, systematische Struktur und eignet sich in besonderer Weise für das Erlernen des algorithmischen Denkens. Die als wichtig erkannten Konzepte von Programmiersprachen – klare und übersichtliche Kontrollkonstrukte, Blockstrukturierung, Rekursion und Unterprogramme, sind in Pascal klar und sauber verwirklicht. Allerdings sind seit der Entwicklung von Pascal neue Prinzipien insbesondere für das Strukturieren sehr großer Programme entstanden. Mithilfe von Modulen können Programme in einzelne funktionale Einheiten zerlegt werden, mithilfe von Objekten und Klassen können Datenobjekte hierarchisch geordnet, zusammengefasst und wiederverwendet werden. Viele Pascal-Systeme, insbesondere das am weitesten verbreitete Turbo-Pascal, haben das ursprüngliche Pascal um die genannten Konzepte erweitert. Ab der Version 4.0 gab es in Turbo-Pascal ein Modulkonzept – Module hießen hier „Units” – und seit der Version 5.5 enthielt Turbo-Pascal auch objektorientierte Zusätze. Turbo-Pascal wurde 1993 von „BorlandPascal” abgelöst, eine neuere Version heißt seit 1995 „Delphi”. In Delphi kann man u.a. auch grafische Benutzeroberflächen bequem programmieren. Pascal ist nicht mehr die modernste Programmiersprache – aber immer noch eine Sprache, die zum Einstieg in die Welt des Programmierens hervorragend geeignet ist. Pascal erzieht zu einer Klarheit des Denkens, da das Prinzip der Orthogonalität hier besonders gut durchgehalten wurde: Für jede Aufgabe bietet sich ein (und möglichst nur ein) Konzept der Sprache als Lösungsweg an. Im Gegensatz dazu können sich in anderen Sprachen verschiedene Konzepte oft weitgehend überlappen. So lässt sich in C beispielsweise eine while-Schleife auch mithilfe des for-Konstruktes ersetzen und umgekehrt (siehe S. 144). Der größte Vorteil der von Pascal erzwungenen Disziplin ist, dass Programmierfehler in den meisten Fällen bereits bei der Compilierung des Programmes erkannt werden. Laufzeitfehler, also Fehler, die erst bei der Ausführung des Programmes auftreten (siehe S. 82), treten bei Pascal deutlich seltener auf als z.B. in C. Solche Fehler sind nur mit großem Aufwand zu finden und zu beheben. Schlimmer noch, sie treten manchmal nur bei ganz bestimmten Konstellationen

84

2 Grundlagen der Programmierung

von Eingabedaten auf. Wie es Murphy’s Gesetz will, treten entsprechend unglückliche Konstellationen nicht in der Testphase auf, sondern erst wenn das Programm beim Kunden installiert ist. Der klare und saubere Programmierstil von Pascal hat aber auch Nachteile. Insbesondere beim Erstellen von systemnahen Programmen kann die erzwungene Programmierdisziplin störend oder gar effizienzhemmend sein. In diesen Fällen greifen Programmierer gern zu Sprachen wie C oder C++, in denen – genügend Selbstdisziplin vorausgesetzt – ein sauberes und klares Programmieren zwar möglich ist, aber nicht erzwungen wird. Turbo-Pascal und seine Nachfolger Borland-Pascal und Delphi haben sich u.a. auch insofern in diese Richtung geöffnet, als sie erlauben, auf Daten mit solchen Methoden zuzugreifen, die sich spezielle interne Repräsentationen zu Nutze machen. So darf man in Delphi beispielsweise Zahlen auch als Bitfolgen auffassen und mit Operationen wie xor (siehe S. 6) manipulieren. Das Ergebnis ist dann aber von der speziellen internen Repräsentation der Zahlen abhängig und dadurch möglicherweise auf anderen Rechnersystemen fehlerhaft.

2.1.7

Java

Java ist eine junge Programmiersprache, die man als Weiterentwicklung der populären Sprache C++ ansehen kann. Java ist konsequent objektorientiert und räumt mit vielen Hemdsärmeligkeiten von C und C++ auf. Insbesondere gibt es ein sicheres Typsystem, und die in C++ notorisch fehleranfällige Pointerarithmetik wurde abgeschafft. Pointer, also Speicheradressen, leben nur noch in der harmlosen Form von sog. Objektreferenzen fort. Java wird durch die interpretative Ausführung mittels einer virtuellen Maschine plattformunabhängig. Zusätzlich besitzt es als erste Sprache geeignete Sicherheitskonzepte für die Verbreitung von Programmen über das Internet und die Ausführung von Programmen (so genannten Applets) in Internet-Seiten. Dies und die Unterstützung durch Firmen wie SUN und Netscape haben Java zu einer außergewöhnlich schnellen Verbreitung und einer enormen Akzeptanz verholfen. In Kapitel 3 werden wir Java genauer kennen lernen und in Kapitel 8 (ab S. 677) zeigen wir, wie man Java-Applets in Internet-Seiten einbauen kann. Auch um die Grundkonzepte des Programmierens im gegenwärtigen Kapitel zu studieren, wollen wir uns an Syntax und Semantik von Java orientieren.

2.2

Spezifikationen, Algorithmen, Programme

Vor dem Beginn der Programmierung sollte das zu lösende Problem zuerst genau beschrieben, d.h. spezifiziert werden. Anschließend muss ein Ablauf von Aktionen entworfen werden, der insgesamt zur Lösung des Problems führt. Ein solcher Ablauf von Aktionen, ein Algorithmus, stützt sich dabei auf eine bereits in der Beschreibungssprache vorgegebene Strukturierung der Daten. Die hierbei zentralen Begriffe Spezifikation, Algorithmus und Datenstruktur, sollen im Folgenden näher erläutert werden.

2.2 Spezifikationen, Algorithmen, Programme

2.2.1

85

Spezifikationen

Eine Spezifikation ist eine vollständige, detaillierte und unzweideutige Problembeschreibung. Dabei heißt eine Spezifikation • • •

vollständig, wenn alle Anforderungen und alle relevanten Rahmenbedingungen angegeben worden sind, detailliert, wenn klar ist, welche Hilfsmittel, insbesondere welche Basis-Operationen zur Lösung zugelassen sind, unzweideutig, wenn klare Kriterien angegeben werden, wann eine vorgeschlagene Lösung akzeptabel ist.

Informelle Spezifikationen sind oft umgangssprachlich und unpräzise formuliert und genügen damit nur beschränkt den obigen Kriterien. Spezifikationen können formal in der Sprache der Logik oder in speziellen Spezifikationssprachen wie VDM oder Z ausgedrückt werden. Als Beispiel betrachten wir folgende informelle Spezifikation eines Rangierproblems: „Eine Lokomotive soll die in Gleisabschnitt A befindlichen Wagen 1, 2, 3 in der Reihenfolge 3, 1, 2 auf Gleisstück C abstellen.” 1

2

3

A

B Abb. 2.5:

C Rangierproblem

Diese Spezifikation lässt in jeder Hinsicht noch viele Fragen offen, beispielsweise: Vollständigkeit: Wie viele Wagen kann die Lokomotive auf einmal ziehen? Wie viele Wagen passen auf Gleisstück B? Detailliertheit: Welche Aktionen kann die Lokomotive ausführen (fahren, koppeln, entkoppeln, ... )? Unzweideutigkeit: Ist es erlaubt, dass die Lokomotive am Ende zwischen den Wagen steht? Als zweites Beispiel betrachten wir die Aufgabe, den größten gemeinsamen Teiler zweier Zahlen zu finden. Eine informelle Spezifikation könnte lauten: „Für beliebige Zahlen M und N berechne den größten gemeinsamen Teiler ggT(M, N), also die größte Zahl, die sowohl M als auch N teilt.” Auch diese Spezifikation lässt viele Fragen offen:

86

2 Grundlagen der Programmierung

Vollständigkeit: Welche Zahlen M, N sind zugelassen? Dürfen M und N nur positive Zahlen oder auch negative oder gar rationale Zahlen sein? Ist 0 erlaubt? Detailliertheit: Welche Operationen sind erlaubt? ( +, -, oder auch dividieren mit Rest ? ) Unzweideutigkeit: Was heißt berechnen? Soll das Ergebnis ausgedruckt oder vielleicht in einer bestimmten Variablen gespeichert werden? Eine einfache Methode, Probleme formal zu spezifizieren, besteht in der Angabe eines Paares P und Q von logischen Aussagen. Diese stellt man in geschweiften Klammern dar:{ P }{ Q }. Dabei wird P Vorbedingung und Q Nachbedingung genannt. In der Vorbedingung werden alle relevanten Eigenschaften aufgeführt, die anfangs, also vor Beginn der Programmausführung gelten, in der Nachbedingung alle relevanten Eigenschaften, die gelten sollen, wenn das Programm beendet ist. In unserem Rangierbeispiel beschreibt die Vorbedingung die anfängliche Position von Lok und Waggons und die Nachbedingung die Position, die erreicht werden soll. Dies wollen wir grafisch veranschaulichen: 1

2

3

A

A 3

B Abb. 2.6:

C

B

1

2

C

Vorbedingung {P} – Nachbedingung {Q} beim Rangierproblem

Im Falle des größten gemeinsamen Teilers drückt die Vorbedingung aus, dass M und N positive ganze Zahlen sind. Wenn man noch in Betracht zieht, dass ein Programm immer nur mit Zahlen in einem endlichen Bereich umgeht, dann kann man noch spezifizieren, dass M und N in einem solchen Bereich liegen sollen. Die Nachbedingung verlangt, dass in einer Variablen z der Wert ggT(M,N) gespeichert ist. Vorbedingung: { M und N sind ganze Zahlen mit 0 < M < 32767 und 0 < N < 32767} Nachbedingung: { z = ggT(M,N), d.h., z ist Teiler von M und N und für jede andere Zahl z’, die auch M und N teilt, gilt z' ≤ z } Natürlich wollen wir keine Lösung der Programmieraufgabe akzeptieren, die M und N verändert, also etwa N und M zu 1 umwandelt und dann z = 1 als Lösung präsentiert. Daher müssen N und M konstant bleiben – wir drücken das durch ihre Schreibweise aus:

2.2 Spezifikationen, Algorithmen, Programme

87

Konvention: In diesem Unterkapitel sollen großgeschriebene Namen, wie z.B. M, N, Betrag, etc. Konstanten bezeichnen, d.h. nichtveränderbare Größen. Kleingeschriebene Namen, wie z.B. x,y,betrag, etc. bezeichnen Variablen, also Behälter für Werte, die sich während des Ablaufs eines Programms ändern können. Die Spezifikation von Programmieraufgaben durch Vor- und Nachbedingungen ist nur dann möglich, wenn ein Programm eine bestimmte Aufgabe erledigen soll und danach beendet ist. Wenn dies nicht der Fall ist, man denke z.B. an ein Programm, das die Verkehrsampeln einer Stadt steuert, muss man zu anderen Spezifikationsmethoden übergehen. Man kann Eigenschaften, die sich in der Zeit entwickeln, z.B. in der temporalen Logik ausdrücken: „Irgendwann wird ampel3 grün sein” oder „x ist immer kleiner als ggT(M, N) “. Wir werden jedoch nicht näher auf diese Methoden eingehen.

2.2.2

Algorithmen

Nachdem in einer Spezifikation das Problem genau beschrieben worden ist, geht es darum, einen Lösungsweg zu entwerfen. Da die Lösung von einem Rechner durchgeführt wird, muss jeder Schritt exakt vorgeschrieben sein. Wir kommen zu folgender Begriffsbestimmung: Ein Algorithmus ist eine detaillierte und explizite Vorschrift zur schrittweisen Lösung eines Problems. Im Einzelnen beinhaltet diese Definition: • • •

Die Ausführung des Algorithmus erfolgt in einzelnen Schritten. Jeder Schritt besteht aus einer einfachen und offensichtlichen Grundaktion. Zu jedem Zeitpunkt muss klar sein, welcher Schritt als nächster auszuführen ist.

Ein Algorithmus kann daher von einem Menschen oder von einer Maschine durchgeführt werden. Ist der jeweils nächste Schritt eindeutig bestimmt, spricht man von einem deterministischen, ansonsten von einem nichtdeterministischen Algorithmus. Es gibt zahlreiche Methoden, Algorithmen darzustellen. Flussdiagramme sind grafische Notationen für Algorithmen. Sie haben den Vorteil, unmittelbar verständlich zu sein. Für umfangreiche Algorithmen werden sie aber bald unübersichtlich. Flussdiagramme setzen sich aus elementaren Bestandteilen zusammen:

88

2 Grundlagen der Programmierung Anfang: An diesem Kreis beginnt die Ausführung. In diesen Kästchen steht jeweils eine Elementaraktion. Pfeile zeigen auf die anschließend auszuführenden Aktionen. F

T

Test: In dem Karo steht eine Bedingung. Ist sie erfüllt, folge dem T-Pfeil, ansonsten dem F-Pfeil. Erreicht man diesen Kreis, dann endet der Algorithmus.

Das folgende Flussdiagramm stellt einen Algorithmus zur Lösung des ggT-Problems dar. Die Aktionen, die in den Rechtecken dargestellt sind, werden als elementare Handlungen des Rechners verstanden, die nicht näher erläutert werden müssen.

xx ← ←M M yy ← ←N N ;; T

T xx ← ← x-y x-y

Abb. 2.7:

xx >> yy

xx ≠≠ yy

F

zz ← ← xx

F yy ← ← y-x y-x

Flussdiagramm für ggT

In unserem Falle handelt es sich dabei um so genannte Zuweisungen, bei denen ein Wert berechnet und das Ergebnis in einer Variablen gespeichert wird. So wird z.B. durch x← x–y

der Inhalt der durch x bzw. y bezeichneten Speicherplätze subtrahiert und das Ergebnis in dem Speicherplatz x gespeichert. Deren alter Wert wird dabei gelöscht und mit dem neuen Wert überschrieben. Die zweite Sorte von elementaren Aktionen sind die Tests, die in den Karos stehen. Sie werden entweder zu true (T) oder false (F) ausgewertet. Dabei verändern sich die Werte der Variablen nicht, es wird, im Gegensatz zu den Zuweisungen, nichts gespeichert, gelöscht oder verändert. Für diesen Algorithmus verwenden wir folgende Eigenschaften der natürlichen Zahlen:

2.2 Spezifikationen, Algorithmen, Programme

89

Eine Zahl t teilt x und y, genau wenn sie x und x-y teilt (im Falle x ≥ y ) bzw. wenn sie y und y-x teilt, im Falle x ≤ y . Die Strategie ist also, schrittweise x und y durch x-y und y bzw. durch x und y-x zu ersetzen. Dabei ändert sich der ggT der ursprünglichen Werte von x und y nicht, die aktuellen Werte von x und y nähern sich aber immer mehr an, bis x=y ist. Der ggT von x und x ist aber x. Flussdiagramme sind zweidimensionale Gebilde und eignen sich daher nicht, einen Algorithmus einem Rechner mitzuteilen. Textuelle Notationen zur Beschreibung von Algorithmen nennt man Programmiersprachen. Auch sind einige der gerne in Flussdiagrammen verwendeten Symbole wie z.B. ≠ oder ← auf Standard-Tastaturen nicht zu finden. Statt des LinksPfeils verwendet z.B. Pascal die Kombination := , so dass die obige Zuweisung als x := x - y geschrieben würde. C/C++, C# und Java ersetzen den Pfeil durch das normale Gleichheitszeichen, man schreibt das gleiche als x=x-y; wobei das Semikolon zu der Anweisung gehört. Die Wahl von = als Ersatz für ← ist allerdings nicht gut durchdacht, denn um zu testen, ob zwei Ausdrücke gleich sind, braucht man jetzt ein neues Zeichen, man verwendet ein doppeltes Gleichheitszeichen: == . Anfänger oder Umsteiger, die das einmal vergessen und mit x=y statt mit x==y testen wollen, ob x und y den gleichen Wert haben, bewirken damit versehentlich, dass der Wert von y in x gespeichert wird, so dass x und y gewaltsam gleich werden. Das Symbol ≠ wird in Pascal durch die Kombination ersetzt und in Java durch !=. In der Programmiersprache Java kann man den ggT-Algorithmus nun wie folgt hinschreiben: { x = M; y = N; while (x != y) if (x > y) x = x-y; else y = y-x; z = x; } Die elementaren Aktionen sind entweder Zuweisungen ( x = M; , y = N; , x=x-y; , y=y-x; , z=x; ) oder Tests ( x != M , x > y ). Die Anweisungen (Kommandos) werden normalerweise in der Reihenfolge ausgeführt in der sie im Programmtext erscheinen, es sei denn, die Kontrollstrukturen verlangen eine Abweichung von diese Reihenfolge. Im obigen Fall erscheinen die folgenden Kontrollanweisungen:

90

2 Grundlagen der Programmierung while( ... )... if( ... ) ... else ... { ... }

bedingte Schleife, bedingte Anweisung, Klammern.

Mit Hilfe dieser Kontrollstrukturen, die wir im Folgenden noch genauer erläutern, kann man im Prinzip jeden deterministischen Algorithmus ausdrücken, doch stellen alle praktischen Programmiersprachen zusätzliche Mittel bereit, um auch große Programme prägnant und übersichtlich formulieren zu können. Im obigen Beispiel bewirkt if (x > y) x = x-y; else y = y-x; dass entweder x=x-y; ausgeführt wird, oder y = y-x; je nachdem, ob x>y ist oder nicht. Diese bedingte Anweisung wird selber wieder von einer while-Schleife kontrolliert. Das bedeutet, dass in while (x != y) if (x > y) x = x-y; else y = y-x; die bedingte Anweisung so oft ausgeführt wird, wie die Bedingung (x!=y) wahr ist. Im Flussdiagramm ist die Schleife an einem Ring von Pfeilen erkennbar. Die gezeigten Kontrollstrukturen kontrollieren jeweils nur eine Anweisung. Das obige while(x != y)... also nur die unmittelbar folgende Anweisung, das war die ifAnweisung. Die Zuweisung z=x; wird also erst ausgeführt, nachdem die while-Schleife vollständig abgearbeitet ist. Will man mehrere Anweisungen A1, A2, ... , An kontrollieren, so muss man sie durch geschweifte Klammern zu einem Block gruppieren: { A1 A2 ... An } Das oben gezeigte Programmbeispiel ist insgesamt ein Block von 4 Anweisungen. Diese werden der Reihe nach abgearbeitet. Die dritte, die while-Anweisung, umfasst die if-else-Anweisung. In der Regel kontrollieren while oder if-else mehrere zu einem Block zusammengefasste Anweisungen, so dass ihre Syntax vielfach gleich durch while( ... ){ ... } bzw. if( ... ){ ... } else { ... } angegeben wird. Als Spezialfall ist mit n=0 auch der leere Block {} erlaubt. Es handelt sich um die leere Anweisung, die man gelegentlich auch als skip bezeichnet. So werden in if( Bedingung ){ Anweisungen } else { } die Anweisungen nur durchgeführt, falls die Bedingung erfüllt ist. Wenn sie nicht erfüllt ist, passiert nichts. Da diese Form sehr oft benötigt wird, gibt es die äquivalente Kurzform if( Bedingung ){ Anweisungen }. Die Formatierung des Programmtextes dient nur der Übersichtlichkeit. In den meisten Programmiersprachen (Ausnahmen sind z.B. Haskell oder Python) hat das Einrücken oder der Zeilenumbruch keinerlei Bedeutung.1

2.2 Spezifikationen, Algorithmen, Programme

91

Was die richtige Grammatik angeht, so nimmt ein Compiler es sehr genau. So würde z.B. ein Komma oder ein Punkt anstelle eines Semikolons in x=x-y; vom Compiler nicht akzeptiert werden. Die Grammatikregeln für Pascal sehen zwar ähnlich, im Detail aber ganz anders aus. Statt der geschweiften Klammern benutzt man die Schlüsselworte begin und end und man trennt die Anweisungen in einem Block durch Semikola, so dass ein Pascal-Block so aussieht: begin A1 ; A2 ; ... ; An end. Daher ist in Pascal ein Semikolon nach der letzten Anweisung eines Blockes nicht notwendig, obwohl der Compiler bereit wäre, es zu tolerieren. Das obige Beispielprogramm sähe in Pascal dann folgendermaßen aus: BEGIN x := M ; y := N ; WHILE x y DO IF x > y THEN x := x-y ELSE y := y-x ; z := x END In C und in Java hingegegen wird durch ein Semikolon (;) eine einfache Anweisung wie eine Zuweisung oder ein Funktionsaufruf beendet. Das Semikolon ist also, wie in dem obigen Beispiel ersichtlich, Teil der Anweisung1 und ein weiteres Trennzeichen zwischen den Anweisungen eines Blockes ist daher nicht notwendig.

2.2.3

Algorithmen als Lösung von Spezifikationen

Eine Spezifikation beschreibt also ein Problem, ein Algorithmus gibt eine Lösung des Problems an. Ist das Problem durch ein Paar { P } { Q } aus einer Vorbedingung P und einer Nachbedingung Q gegeben, so schreiben wir {P} A {Q } , falls der Algorithmus A die Vorbedingung P in die Nachbedingung Q überführt. Genauer formuliert bedeutet dies: Wenn der Algorithmus A in einer Situation gestartet wird, in der P gilt, dann wird, wenn A beendet ist, Q gelten. In diesem Sinne ist ein Algorithmus eine Lösung einer Spezifikation. Man kann eine Spezifikation als eine Gleichung mit einer Unbekannten ansehen: 1. Außer in Strings und Kommentaren. 1. Genau genommen dient ein Semikolon in C zur Umwandlung eines Ausdrucks in eine Anweisung, merkwürdigerweise ist es aber auch nach einer do-while-Schleife erforderlich.

92

2 Grundlagen der Programmierung Zu der Spezifikation { P } { Q } ist ein Algorithmus X gesucht mit { P } X { Q } .

Nicht jede Spezifikation hat eine Lösung. So verlangt { M < 0 } { x = log M } , den Logarithmus einer negativen Zahl zu finden. Wenn aber eine Spezifikation eine Lösung hat, dann gibt es immer unendlich viele. So ist jeder Algorithmus, der den ggT berechnet – gleichgültig wie umständlich er dies macht – eine Lösung für die Spezifikation { X>0,Y>0 }{ Z=ggT(X,Y) }.

2.2.4

Terminierung

In einer oft benutzten strengeren Definition des Begriffes Algorithmus wird verlangt, dass ein solcher nach endlich vielen Schritten terminiert, also beendet ist. Das stößt aber auf folgende Schwierigkeiten: • •

Manchmal ist es erwünscht, dass ein Programm bzw. ein Algorithmus nicht von selber abbricht. Ein Texteditor, ein Computerspiel oder ein Betriebssystem soll im Prinzip unendlich lange laufen können. Es ist oft nur schwer oder überhaupt nicht feststellbar, ob ein Algorithmus in endlicher Zeit zum Ende kommen wird. Verantwortlich dafür ist die Möglichkeit, Schleifen zu bilden, so dass dieselben Grundaktionen mehrfach wiederholt werden.

In Flussdiagrammen erkennt man Schleifen an einer Folge von Pfeilen, die wieder zu ihrem Ausgangspunkt zurückkommen, in Programmen werden Schleifen mithilfe von while, do und for-Konstrukten gebildet. Um sich davon zu überzeugen, dass ein Algorithmus terminiert, muss man jede Schleife untersuchen. Eine Strategie besteht darin, einen positiven Ausdruck zu finden, welcher bei jedem Schleifendurchlauf um einen festen Betrag kleiner wird, aber nie negativ werden kann. In dem Flussdiagramm für den ggT erkennt man eine Schleife. Man kann sich davon überzeugen, dass die Summe von x und y zwar bei jedem Schleifendurchlauf um mindestens 1 verringert wird, aber dennoch nie negativ werden kann. Folglich kann die Schleife nur endlich oft durchlaufen werden. Leider ist es selbst bei sehr kleinen Algorithmen nicht immer einfach zu erkennen, ob sie terminieren. So ist bis heute – trotz intensiver Bemühungen – nicht geklärt, ob der folgende Algorithmus für beliebige Anfangswerte von n terminiert. Man kann ihn umgangssprachlich so formulieren: Ulam-Algorithmus: Beginne mit einer beliebigen Zahl n. Ist sie ungerade (engl. odd), multipliziere sie mit 3 und addiere 1, sonst halbiere sie. Fahre so fort, bis 1 erreicht ist. In Java kann man ganze Zahlen a und b mit Rest teilen. a/b liefert den ganzzahligen Quotienten und a%b den Rest. Somit lautet der Ulam-Algorithmus: { while(n > 1) if(n%2==1) n = 3*n+1; else n = n/2; }

2.2 Spezifikationen, Algorithmen, Programme

93

Es ist ein bisher ungelöstes Problem, ob dieser Algorithmus für jede Anfangszahl n terminiert. Dieses Problem ist als Ulam-Problem oder als Syrakus-Problem bekannt. In einer Spezifikation { P } A { Q } gehen wir immer von der Terminierung des Algorithmus A aus. Wenn A nicht terminiert, so erfüllt er trivialerweise die Spezifikation. Insbesondere ist eine Spezifikation { P } A { false } dann und nur dann erfüllt, wenn der Algorithmus A, gestartet in einer Situation, in der P erfüllt ist, nicht terminiert. Daher ist man bei einer Spezifikation durch Vor- und Nachbedingung meist nur an terminierenden Algorithmen interessiert.

2.2.5

Elementare Aktionen

Wir haben bisher noch nicht erklärt, welche „offensichtlichen Grundaktionen” wir voraussetzen, wenn wir Algorithmen formulieren. In der Tat sind hier eine Reihe von Festlegungen denkbar. Wir könnten zum Beispiel in einem Algorithmus formulieren, wie man ein besonders köstliches Essen zubereitet. Die Grundaktionen wären dann einfache Aufgaben, wie etwa „Prise Salz hinzufügen”, „umrühren” und „zum Kochen bringen”. Der Algorithmus beschreibt dann, ob, wann und in welcher Reihenfolge diese einfachen Handlungen auszuführen sind. Denken wir an das Rangierbeispiel, so könnten wir uns „ankoppeln”, „abkoppeln”, „fahren” als einfache Grundaktionen vorstellen. Es geht uns hier aber um einfachere Dinge als Kochen und Lokomotive fahren. In der ggTAufgabe etwa verlangen wir nur, dass der Rechner mit Zahlen operieren kann, vielleicht auch mit logischen Werten, und die Ergebnisse zeitweise speichern kann.

2.2.6

Zuweisungen

In einer Programmiersprache kann man Speicherzellen für Datenwerte mit Namen kennzeichnen. Diese nennt man auch Variablen. Man darf den Inhalt einer Variablen lesen oder ihr einen neuen Wert zuweisen. Der vorher dort gespeicherte Wert geht dabei verloren, man sagt, dass er überschrieben wird. Eine Grundaktion besteht jetzt aus drei elementaren Schritten: • • •

einige Variablen lesen, die gefundenen Werte durch einfache Rechenoperationen verknüpfen, das Ergebnis einer Variablen zuweisen.

Eine solche Grundaktion heißt Zuweisung. In Java wird sie als v = e;

geschrieben. Dabei ist v eine Variable, „ = “ ist der Zuweisungsoperator und e kann ein beliebiger (arithmetischer) Ausdruck sein, in dem auch Variablen vorkommen können. In dem Programm in Abb. 2.8 erkennen wir u.a. die Zuweisungen x=84; und x=x-y; . Eine solche Zuweisung wird ausgeführt, indem der Ausdruck der rechten Seite berechnet und der ermittelte Wert in der Variablen der linken Seite gespeichert wird. Nach den Zuweisungen { x=84; y=30; x=x-y;} hat zum Beispiel x den Inhalt 54 und y den Inhalt 30.

94

2 Grundlagen der Programmierung

Es handelt sich nicht um Gleichungen, denn die Variablen, die auf der rechten Seite des Zuweisungszeichens vorkommen, stehen für den alten und die Variable auf der linken Seite für den neuen Wert nach der Zuweisung. Man könnte eine Zuweisung daher als Gleichung zwischen alten und neuen Variablenwerten deuten, etwa: xNeu = xAlt – yAlt ; . Besser aber ignoriert man die Koinzidenz des Zuweisungszeichens „ = “ mit dem mathematischen Gleichheitszeichen und spricht es als „erhält“ aus: „ x erhält x-y “ für „ x=x-y; “. In so genannten befehlsorientierten oder imperativen Programmiersprachen sind Zuweisungen die einfachsten Aktionen. Aus diesen kann durch Kontrollstrukturen wie while, if und else im Prinzip jeder gewünschte Algorithmus aufgebaut werden. Hat man erst einmal einige nützliche Algorithmen programmiert, so kann man diese in anderen Programmen benutzen – man sagt dazu aufrufen – und wie eine elementare Aktion behandeln. Dazu muss man sie nur mit einem Namen versehen und kann danach diesen Namen anstelle des Algorithmus hinschreiben. Einige solcher zusätzlichen Aktionen, in Java auch Methoden genannt, sind bei allen Sprachen bereits „im Lieferumfang” enthalten. So ist die Prozedur System.out.println standardmäßig in Java enthalten. Ihre Wirkung ist die Ausgabe von Werten in einem Terminalfenster. Ein Aufruf, etwa System.out.println("Hallo Welt !"); ist also auch eine elementare Aktion.

2.2.7

Vom Algorithmus zum Programm

Damit ein Algorithmus als ein lauffähiges Programm akzeptiert wird, muss man ihm noch einige Erläuterungen beifügen und ihn auf eine bestimmte Weise „einpacken“. In C und in Java ist für einen unmittelbar lauffähigen Algorithmus eine Funktion mit dem vorgegebenen Namen main notwendig. In Java ist sogar die Kopfzeile public static void main(String [] args) vorgeschrieben. Der Körper der Funktion kann ein beliebiger Block sein, welcher den Algorithmus darstellt. Alle verwendeten Variablen müssen vor ihrer ersten Benutzung deklariert werden. Eine solche Deklaration veranlasst den Compiler, entsprechenden Platz im Speicher zu reservieren. In unserem Beispielprogramm werden zwei Variablen x und y zur Speicherung von ganzen Zahlen benötigen. Mit int x,y; wird für sie Platz im Speicher reserviert. Der Inhalt der Variablen ist zu diesem Zeitpunkt noch völlig unbestimmt. Man könnte sie daher gleich noch mit einem Startwert initialisieren, etwa durch int x=0, y=0; dies ist aber nicht erforderlich. Die Initialisierung kann auch in einer späteren Zuweisung erfolgen. In einer solchen Zuweisung

2.2 Spezifikationen, Algorithmen, Programme

95

v=e; wird bekanntlich jede Variable, die in dem Ausdruck e vorkommt, gelesen, damit der Wert von e berechnet und in die Variable v geschrieben. Eine Besonderheit von Java ist es, dass der Compiler nachprüft, ob garantiert auch jede Variable einen Wert erhalten hat, bevor sie zum ersten Mal gelesen wird. In C könnte man eine main-Funktion mit dem Algorithmus einfach in eine Datei packen, und diese zu einem lauffähigen Programm compilieren. Da Java eine objektorientierte Sprache ist, setzen sich Java-Programme aus sogenannten Klassen zusammen. Die Funktion main muss also noch in eine Klasse gepackt werden, im Beispiel haben wir diese ggT genannt. Das Ergebnis ist in Abb. 2.8 zu sehen. Wird die Datei, die auch den Namen ggT.java trägt, mit dem Java-Compiler übersetzt, so entsteht daraus ein lauffähiges Programm, ggT.class, das mit dem Kommando java ggT gestartet werden kann. Dabei wird die Funktion main aufgerufen. Gibt der Benutzer zusätzliche Argumente auf der Eingabezeile an, so werden diese als Folge (Array) von Strings in der Parametervariablen args übergeben.

Kopf

Javamain Klasse Methode

Abb. 2.8:

Block = Deklarationen und Anweisungen

Java-Programm zur Berechnung des ggT – und Ausgabe

Das gezeigte Programm besteht aus einer Variablendeklaration und vier Anweisungen. Die dritte, eine while-Anweisung, enthält selber wieder eine if-Anweisung. Die letzte Anweisung gibt einen Text in einem Terminalfenster aus. Dieser Text besteht aus einem in Anführungszeichen eingeschlossenen festen Text „Das Ergebnis ist: “, an den durch den Operator + der berechnete Wert der Variablen x angehängt wird. Dabei wird der Wert der Integer-Variablen x automatisch in eine dezimale Textdarstellung umgewandelt. In anderen Sprachen ist es analog. Während in C und in Java an beliebigen Stellen im Programm Variablen deklariert werden können, trennt Pascal jedes Programm in einen Deklara-

96

2 Grundlagen der Programmierung

tionsteil, in dem alle nötigen Variablen deklariert werden müssen, und einen Anweisungsteil. Variablendeklarationen werden durch das Schlüsselwort VAR angekündigt, danach listet man die gewünschten Variablen und ihre Typen. In der in Abb 2.9 gezeigten Pascal-Version des obigen Programms deklarieren wir zwei Variablen, die x, y heißen sollen und ganze Zahlen (integer) speichern können. Der Anweisungsteil wird in die Schlüsselworte BEGIN und END eingeschlossen. Dort steht der eigentliche Algorithmus. Die ersten beiden Anweisungen sind Zuweisungen, die die Variablen x und y initialisieren. Die dritte Anweisung ist eine while-Anweisung, die selber eine if-then-else-Anweisung kontrolliert, und die vierte und letzte Anweisung ist eine Schreib-Anweisung writeln, die ihre beiden Argumente, die Zeichenkette „Das Ergebnis ist: “ und den Inhalt der Variablen x in einem Terminalfenster ausgibt. Fügt man dem Programm noch einen Kopf mit einem beliebigen Namen, hier „ggT“, hinzu und beendet es durch einen Punkt, so erhält man nach der Kompilation ein direkt lauffähiges Pascal-Programm. In der folgenden Abbildung sehen wir das Programm in dem mittlerweile frei erhältlichen Turbo-Pascal Entwicklungssystem. Das kleine schwarze Fenster ist das Terminalfenster, in dem die Ausgabe des Programms erscheint.

Programmkopf Deklarationsteil PascalProgramm Block

Abb. 2.9:

Anweisungsteil

Pascal-Programm zur Berechnung des ggT – und Ausgabe

Offensichtlich sehen die Programmtexte, ob in Pascal oder in Java, ähnlich aus. Beide Sprachen gehören schließlich zur Familie der imperativen Sprachen. Deren Prinzip ist die gezielte schrittweise Veränderung von Variableninhalten, bis ein gewünschter Zustand erreicht ist. Hat man eine solche Sprache erlernt, kann man ohne große Mühe in eine andere umsteigen.

2.2.8

Ressourcen

Fast alle ernsthaften Programme nutzen externe Ressourcen, seien es Funktionen des Betriebssystems oder andere Programme und Funktionen, die in Bibliotheken erhältlich sind. Unsere obigen Beispielprogramme nutzen eine simple Bildschirmausgabe des Betriebssystems – mittels der Funktion System.out.println in Java, bzw. writeln in Pascal. Allerdings ist

2.2 Spezifikationen, Algorithmen, Programme

97

die Kommunikation mit Programmen über Terminalfenster heute nicht mehr zeitgemäß. Insbesondere, wenn Programme Ressourcen der grafischen Benutzeroberfläche nutzen wollen, müssen sie entsprechende Bibliotheken anfordern, in denen diese enthalten sind. Im Falle von Java importiert man einfach am Anfang alle Klassen, in denen die benötigten grafischen Elemente enthalten sind. Wollen wir zum Beispiel unser ggT-Programm dadurch verbessern, dass es in Eingabefenstern vom Benutzer die Eingabe zweier Zahlenwerte verlangt, von denen es den ggT berechnet und diesen anschließend in einer MessageBox ausgibt, so kann man entsprechende Hilfsmittel aus dem Paket javax.swing importieren. Dies geschieht durch eine importAnweisung vor Beginn der eigentlichen Klasse: import javax.swing.*; Dadurch werden alle Ressourcen der Bibliothek javax.swing verfügbar gemacht. Wir interessieren uns hier insbesondere für die Bibliotheksklasse JOptionPane und die darin enthaltenen Funktionen showInputDialog und showMessageDialog. Die erstere fordert den Benutzer auf, einen Text einzutippen, letztere gibt ein Ergebnis aus. Allerdings muss der eingegebene Text noch als ganze Zahl erkannt und in eine solche explizit umgewandelt werden. Dies leistet die Funktion parseInt aus der Klasse Integer.

Abb. 2.10:

Das Java-Programm mit grafischem Input und Output

Das fertige Programm und die Fenster, die bei einem Aufruf erzeugt wurden, zeigt die obige Abbildung. Wir haben hier die freie Java-Entwicklungs- und Testumgebung BlueJ (www.bluej.org) benutzt, die zum Erlernen von Java besonders geeignet ist. Eine ausführliche Einführung in das Installieren und Benutzen dieses Systems findet sich auf der Webseite dieses Buches.

98

2.3

2 Grundlagen der Programmierung

Daten und Datenstrukturen

Daten sind die Objekte, mit denen ein Programm umgehen soll. Man muss verschiedene Sorten von Daten unterscheiden, je nachdem, ob es sich um Wahrheitswerte, Zahlen, Geburtstage, Texte, Bilder, Musikstücke, Videos etc. handelt. Alle diese Daten sind von verschiedenem Typ, insbesondere verbrauchen sie unterschiedlich viel Speicherplatz und unterschiedliche Operationen sind mit ihnen durchführbar. So lassen sich zwei Geburtstage oder zwei Bilder nicht addieren, wohl aber zwei Zahlen. Andererseits kann ein Bild komprimiert werden, bei einer Zahl macht dies keinen Sinn. Zu einem bestimmten Typ von Daten gehört also immer auch ein charakteristischer Satz von Operationen, um mit diesen Daten umzugehen. Jede Programmiersprache stellt eine Sammlung von Datentypen samt zugehöriger Operationen bereit und bietet zugleich Möglichkeiten, neue zu definieren.

2.3.1

Der Begriff der Datenstruktur

Die Kombination von Datentyp und zugehörigem Satz von Operationen nennt man eine Datenstruktur. Oft werden die Begriffe Datentyp und Datenstruktur auch synonym verwendet. Datenstruktur: Eine Menge gleichartiger Daten, auf denen eine Sammlung von Operationen definiert ist. Eine Operation ist dabei eine Verknüpfung, die einer festen Anzahl von Eingabedaten ein Ergebnis zuordnet. Die Anzahl der Eingabewerte, die eine Operation benötigt, nennt man ihre Stellenzahl. Beispielsweise hat die Addition „+“ die Stellenzahl 2, da zwei Eingabewerte benötigt werden, um einen Ausgabewert, hier die Summe, zu produzieren. Man sagt auch, dass die Addition eine zweistellige Operation ist. Unter einer Operation versteht man eine Verknüpfung von Daten bestimmter Typen zu einem Ergebniswert. Beispielsweise ist + eine Operation, die zwei Zahlen verknüpft und eine neue Zahl liefert, ihre Summe. Ähnlich ist ggT eine Operation, die zwei Zahlen ihren ggT zuordnet. Mathematisch handelt es sich bei einer Operation um eine Abbildung f : τ1 × τ 2 × … × τ n → σ

einem Argumentetupel ( a 1, a 2, …, a n ) von Daten a i ∈ τ i ein Ergebnis f ( a 1, a 2, …, a n ) ∈ σ zuordnet. Für arithmetische und boolesche Operationen sind zahlreiche Spezialformen zugelassen. So schreibt man zweistellige Operationszeichen wie +, -, *, /, div, mod, or, and, xor zwischen die Argumente. Diese Notation heißt Infix-Notation. Einstellige Operationszeichen werden meist den Operanden vorangestellt (Präfix-Notation), ohne diesen in Klammern einzuschließen, wie z.B. sin 45, –12 oder not x. Schließlich ordnet man den Operatoren Präzedenzen zu, aufgrund derer man bei zusammengesetzten Ausdrücken Klammern sparen kann. Die Präzedenzen einiger wichtiger Operationen (in Klammern jeweils die Java-Notation) sind in absteigender Reihenfolge:

2.3 Daten und Datenstrukturen • • • •

99

einstellige Operationen wie: not (!), ... multiplikative Operationen wie: * , div (/), mod (% ), and (&), ... additive Operationen wie: + , - , or (|) , ... vergleichende Operationen wie: = (==), ≠ (!=) , < , ≤ ( , ≥ ( >=).

Ausdrücke mit Operatoren gleicher Präzedenz werden als linksgeklammert angenommen. Aufgrund dieser Regeln kann man viele Klammern sparen und z.B. x / y * z + u % v schreiben anstatt ((x / y) * y) + (x mod y) . Im Folgenden betrachten wir systematisch die Datenbereiche, die in allen Programmiersprachen vorhanden sind zusammen mit den wichtigsten darauf erklärten Operationen.

2.3.2

Boolesche Werte

Der einfachste Datentyp besteht aus den booleschen Werten true und false und den Verknüpfungen or, and und not (vgl. auch S. 6). Man bezeichnet diesen Datentyp mit dem englischen Ausdruck boolean. Dabei sind or und and zweistellige Operationen, und not ist einstellig. Dass or eine zweistellige Operation ist, also einem Paar von booleschen Werten einen neuen booleschen Wert zuordnet, kann man mathematisch durch die Schreibweise or : Boolean × Boolean → Boolean ausdrücken. Für die einstellige Operation not, schreibt man daher entsprechend not : Boolean → Boolean. Man könnte sich nun fragen, ob es auch nullstellige Operationen gibt. Eine nullstellige Operation müsste null Eingabewerten einen (und damit immer denselben konstanten) Ausgabewert zuordnen. In der Tat ist es oft nützlich, Konstanten als solche nullstellige Operationen aufzufassen. Im Falle des Datentyps boolean haben wir zwei Konstanten, true und false und schreiben daher: true, false : → Boolean Durch die bisherigen Angaben ist der Datentyp noch nicht vollständig bestimmt. Man müsste noch genau angeben, welche Eingabewerte bei den einzelnen Operationen zu welchem Ausgabewert führen. Eine Möglichkeit, dies festzulegen, wäre durch die Operationstabellen für not, and und or, die wir bereits im vorigen Kapitel gesehen haben. Allerdings sind wir bei dem booleschen Datentyp in der glücklichen Lage, dass wir es nur mit zwei möglichen Werten, true und false, zu tun haben. Die meisten Datentypen, die wir später noch kennen lernen werden, haben dagegen (prinzipiell) unendlich viele Elemente. Da wir keine unendlichen Tabellen aufschreiben können, wollen wir uns bereits jetzt daran gewöhnen, die Operationen durch Gleichungen zu beschreiben.

100

2 Grundlagen der Programmierung

Eine Gleichung drückt im Allgemeinen einen Zusammenhang zwischen den vorhandenen Operationen aus und kann manchmal auch dazu verwendet werden, den Wert einer Operation auf gegebenen Eingabewerten auszurechnen, indem man einfach den Ausdruck anhand der Gleichungen so lange vereinfacht, bis man auf eine Konstante stößt. Im Falle des booleschen Datentyps gelingt dies bereits mit den folgenden Gleichungen: true or x false or x

= true = x

not true = false

true and x = x false and x = false not false = true

Auch zusätzliche Operationen kann man durch solche Gleichungen einführen (definieren). Eine wichtige Operation ist z.B. das exclusive oder (xor), welches durch die folgende Gleichung definiert wird: x xor y = (x and not y) or (not x and y) Die obigen Gleichungen reichen bereits hin, alle möglichen Werte der booleschen Operationen auszurechnen. Einen „schöneren” Satz von Gleichungen werden wir in dem Kapitel über Rechnerarchitektur (S. 411) kennen lernen, im Moment kommt es uns aber nicht auf die Schönheit an, sondern auf die Einfachheit und Praktikabilität. Für den Datentyp Boolean können wir uns also eine Art von Karteikarte anlegen: Datentyp: boolean Werte: true, false Operationen: or, and not true, false

: Boolean × Boolean → Boolean : Boolean → Boolean : → Boolean

Gleichungen: Für alle x, y ∈ Boolean gilt true or x = true true and x = x false or x = x false and x= false not true = false not false = true Der Datentyp boolean in Java In manchen Programmiersprachen, wie z.B. in Pascal, schreibt man die Operationen als and, or, not aus, in Java werden sie durch die Zeichen &, | und ! dargestellt, also z.B.: true | false ergibt true, true & false ergibt false, und !true ergibt false. Für die xor-Operation, die das „ausschließliche oder“ implementiert, und die man durch a xor b = (a and not b) or (not a and b), bzw. äquivalent durch a xor b = ( a ≠ b )

2.3 Daten und Datenstrukturen

101

definieren könnte, verwendet Java das Zeichen ^. Man hat also z.B.: true ^ false ergibt true, true ^ true ergibt false. Verkürzte Auswertung An den Gleichungen true or x = true sowie false and x = false erkennt man, dass das Ergebnis einer or-Operation bereits festliegt, wenn eines der Argumente true ist. Analog ist es mit der and-Operation, wenn eines der Argumente schon false ist. In beiden Fällen kann darauf verzichtet werden, das andere Argument x noch zu bestimmen, man spricht von einer verkürzten Auswertung. Manche Programmiersprachen werten boolesche Operationen grundsätzlich verkürzt aus, in Pascal kann man dies durch eine Compileroption erzwingen. In Java erreicht man verkürzte Auswertungen durch Verwendung der speziellen Operatoren || oder &&. In der Regel verwenden Programmierer diese Varianten, weil sie nicht nur überflüssige Rechnungen, sondern auch Laufzeitfehler vermeiden helfen. Beispielsweise wird im Ausdruck x > 0 && y > 0 && ggT(x,y)==1 vermieden, den Ausdruck ggT(x,y) zu berechnen, wenn nicht sowohl x als auch y positiv sind. Natürlich beruht diese Eigenschaft auf der Tatsache, dass in Java, wie in den meisten Programmiersprachen, Ausdrücke von links nach rechts ausgewertet werden.

2.3.3

Zahlen

Mathematisch ist man gewohnt, natürliche Zahlen 0,1,2,3, ... oder ganze Zahlen ... -2, -1, 0, 1, 2, 3, ... als spezielle reelle Zahlen anzusehen. Technisch ist es aber sinnvoll, zwischen diskreten Zahlenbereichen und kontinuierlichen Bereichen zu unterscheiden. Dies hat mehrere Gründe. Zum einen kann man diskrete Zahlen exakt repräsentieren und mit ihnen exakt rechnen. Reelle Zahlen, wie z.B. die Kreiszahl π oder die Eulersche Zahl e kann man nur angenähert repräsentieren und bei der Rechnung mit reellen Zahlen entstehen Ungenauigkeiten durch die Tatsache, dass man immer nur eine bestimmte Anzahl von Nachkommazahlen berücksichtigen kann.

2.3.4

Natürliche Zahlen

Natürliche Zahlen sind die Zahlen, die man zum Zählen verwendet, also 0, 1, 2, 3, ... . Zu jeder Zahl gibt es genau einen Nachfolger (engl.: successor) und jede natürliche Zahl entsteht aus 0, indem oft genug der Nachfolger s gebildet wird: 1 = s(0), 2 =s(s(0)), etc. Damit ist jede natürliche Zahl entweder 0, oder s(n) für eine andere natürliche Zahl n. Damit stehen auch schon die Grundoperationen für die natürlichen Zahlen fest:

102

2 Grundlagen der Programmierung 0 s

: → Nat : Nat → Nat

Es gibt keine Gleichungen, d.h. keine der durch die Operationen gebildeten Ausdrücke fallen zusammen, es sei denn, es handelt sich um identisch gebildete Ausdrücke. Das bedeutet, dass die natürlichen Zahlen die Elemente { 0, s(0), s(s(0)), s(s(s(0))), ... } enthalten und dass alle diese Elemente verschieden sind. Selbstverständlich kann man diese Zahlen bequemer durch die Notation 0, 1, 2, 3, ... bezeichnen. Die obige Darstellung der natürlichen Zahlen gestattet einfache Definitionen abgeleiteter Operationen, wie z.B. der Addition + : 0 +x=x s(x) + y = s(x+y) und darauf aufbauend der Multiplikation * : 0 * x= 0 s(x) * y = x * y + y Man überzeugt sich leicht, dass die obigen Gleichungen die Addition und die Multiplikation eindeutig definieren – der Ausdruck auf der rechten Seite ist immer einfacher als der auf der linken Seite. Die Operation s ist zwar in Pascal vorhanden, dort heißt sie succ, viele Programmiersprachen verzichten aber, sie einzuführen, da sie ja mit der Operation +1 zusammenfällt. In der Tat folgt aus den obigen Gleichungen: s(x) = s(0+x) = s(0) + x = 1 + x. Eine Subtraktion kann man allgemein nicht definieren, da das Ergebnis aus dem Zahlbereich herausführen könnte. Auch eine exakte Division liefert im Allgemeinen rationale (gebrochene) Zahlen, es sei denn, man interessiert sich für eine Division mit Rest. Mathematisch bezeichnet man die Division mit Rest durch x ⁄ y und schreibt r ≡ x ( mod y ) , wenn bei der Division der Rest r übrig bleibt. Pascal definiert die Operatoren div und mod, während man in Java die Zeichen / und % verwendet. Diese Java-Notation werden wir in diesem Kapitel auch übernehmen. Teilt man also eine Zahl m durch eine Zahl n, so erhält man - einen Quotienten: - einen Rest :

q = m / n und einen r = m % n.

Der Rest r muss kleiner als der Divisor n sein, also 0 ≤ r < n , und die Probe muss m = q*n + r ergeben, also m = (m / n) * n + (m % n). So gilt zum Beispiel 7/3=2 und 7%3=1 und die Probe zeigt in der Tat: 2*3+1=7.

2.3 Daten und Datenstrukturen

103

Die obigen Bedingungen legen die Operatoren / und % auf den natürlichen Zahlen eindeutig fest. Daher werden sie auch der Erweiterung der Operationen auf ganze Zahlen zugrunde gelegt. Datentyp: Nat Werte:

Alle natürlichen Zahlen, 0, 1, 2, ...

Operationen: 0 s +, *, /, %

: → Nat : Nat → Nat : Nat × Nat → Nat

Gleichungen: Für alle x, y ∈ Nat gilt 0+x=x s(x) + y = s(x + y) 0*y=0 s(x) * y = x*y + y (x / y) * y + (x % y) = x. Gut gewählte Datenstrukturen können das Programmieren erheblich vereinfachen und führen zu besseren, effektiveren Algorithmen. Mithilfe von % erhalten wir bereits eine erheblich schnellere Variante des ggT-Algorithmus: { while ((x > 0) && (y > 0)) if(x >= y) x = x % y; else y = y % x; z = x+y; } Aus der Gleichung (x/y)*y + (x%y) = x liest man sofort ab, dass eine Zahl t genau dann sowohl x als auch y teilt, wenn sie gleichzeitig x%y und y teilt. Daher können wir x getrost durch x%y oder analog auch y durch y%x ersetzen. Aus x ≥ y > 0 folgt zudem 0 ≤ x%y < y ≤ x , so dass die Zuweisung x = x%y; garantiert den Inhalt von x verkleinert. (Analoges gilt, wenn man die Rollen von x und y vertauscht.) Zum Schluss ist x=0 oder y=0, in jedem Fall ist also z=x+y ihr ggT.

2.3.5

Der Datentyp Integer

Viele Programmiersprachen – eine Ausnahme ist z.B. Modula – verzichten darauf, einen gesonderten Datentyp für natürliche Zahlen bereitzustellen. Stattdessen stellen sie eine oder mehrere Varianten des Datentyps Integer zur Verfügung. Darunter versteht man die ganzen Zahlen, also die Menge { ... , -3, -2, -1, 0, 1, 2, 3, ... } mit den üblichen arithmetischen Operationen +, -, *, /, %. Genau genommen gibt es zwei Operationen, die mit dem Zeichen „–“ geschrieben werden, die einstellige Negation - : Integer → Integer.

104

2 Grundlagen der Programmierung

mit den Gleichungen - 0 = 0 - (- x) = x und die zweistellige Subtraktion - : Integer × Integer → Integer. Wie auch in der Mathematik üblich, benutzt man hier das gleiche Zeichen „-“ für zwei verschiedene Operationen. Man sagt, dass das Zeichen „-“ überladen ist. Taschenrechner haben für die einstellige Negation eine besondere Taste, die meist mit ± beschriftet ist. Negation und Subtraktion sind bekanntlich durch die Gleichungen x-y -x

= x + (-y) = 0-x

gegenseitig definierbar. Analog lässt sich die Multiplikation auf die ganzen Zahlen erweitern durch die Gleichung: (- x) * y = x * (- y) = - (x * y). Setzt man die Division mit Rest / analog auf die ganzen Zahlen fort: (- x) / y = x / (- y) = - (x / y), so ergibt sich für die Berechnung % des Rests x % y = x - (x / y) * y , woraus man leicht die Gleichungen gewinnt: (–x) % y = – (x % y) , und x % (–y) = x % y . Wir können also festhalten, dass der Datentyp Integer die Natürlichen Zahlen umfasst und durch folgende Eigenschaften definiert ist: Datentyp: Integer umfasst Nat Werte: Alle ganzen Zahlen ... -3, -2, -1, 0, 1, 2, 3, ... Operationen: +, -, *, /, % 0,1,...

: Integer × Integer → Integer : Integer → Integer : → Integer

Gleichungen: die Gleichungen von Nat, sowie -0 = 0 -(-x)= x x - y = x + (-y) (-x / y) = -(x / y) = x /(-y)

2.3 Daten und Datenstrukturen

105

(-x % y) = -(x % y) x % (-y) = x % y In Pascal würde der Ausdruck 22/7 als Quotient reeller Zahlen 22.0/7.0 aufgefasst und als Ergebnis 3.14285 liefern, man müsste 22 div 7 schreiben um das Ergebnis 3 zu erhalten. Die meisten Sprachen stellen mehrere Integerdatentypen bereit. Es handelt sich stets um endliche Zahlenbereiche, die durch Bitfolgen fester Länge definiert sind (siehe S. 17). Programmierer müssen darauf achten, dass bei der Berechnung kein Zwischenergebnis entsteht, das aus dem gewählten Bereich herausfällt. In Java hat man z.B. die Integer-Datentypen: byte: short: int: long:

8 bit 16 bit 32 bit 64 bit

( -128 ( -215 ( -231 ( -263

... +127 ), ... +215-1 ), ... +231-1 ), ... +263-1 )

In einigen Programmiersprachen kann man (fast) beliebig große natürliche Zahlen verwenden. Zahlen können so groß sein, wie es der nutzbare Speicher zulässt. Java benutzt zu diesem Zweck die Klasse java.math.BigInteger.

2.3.6

Rationale Zahlen

Die meisten Programmiersprachen verzichten auf einen gesonderten Datentyp für rationale Zahlen. Eine Ausnahme bildet die Sprache Smalltalk, die auf rationalen Zahlen exakt rechnet. 7 In Smalltalk liefert die Eingabe 1 ⁄ 6 + 3 ⁄ 10 das Ergebnis ------ . 15

2.3.7

Reelle Zahlen

Mathematisch gesehen sind die ganzen Zahlen nur spezielle reelle Zahlen. Für einen Computer gibt es aber gute Gründe, mit ganzen Zahlen anders zu rechnen als mit reellen Zahlen. Reelle Zahlen werden als Gleitpunktzahlen dargestellt, ganze Zahlen als Zweierkomplementzahlen (siehe S. 23). Wir nennen die so repräsentierten Zahlen Real-Zahlen. Auf reellen Zahlen sind die üblichen arithmetischen Operationen +, - , *, / definiert. Dazu kommen eine Reihe technisch-wissenschaftlicher Funktionen, darunter ln, exp, sqrt sowie die trigonometrischen Funktionen. Gelegentlich sind Operationen auf gewissen Eingabewerten nicht erklärt, wie etwa im Falle von /, ln und von sqrt. Solche Operationen heißen auch partielle Operationen. Wir deuten dies durch ein „::“ an und spezifizieren, auf welchen Werten der Operator definiert ist. Datentyp: Real Werte: Alle Real-Zahlen. Operationen: +, -, * / -, exp sqrt

: Real × Real → Real :: Real × Real → Real, ( x/y nur definiert, falls y ≠ 0 ) : Real → Real, :: Real → Real, ( sqrt(x) nur definiert, falls x ≥ 0 )

106

2 Grundlagen der Programmierung ln sin, tan, ...

:: Real → Real ( ln(x) nur definiert, falls x > 0 ) : Real → Real.

Konstanten: Alle Real-Zahlen. Gleichungen: Die üblichen Gleichungen. Zum Beispiel: x+0 = 0+x = x x+(y+z) = (x+y)+z x*(y+z) = x*y + x*z ... sqrt(x)*sqrt(x) = x sin(x) / cos(x) = tan(x) Konkrete Implementierung der reellen Zahlen sehen nur eine begrenzten Zahlenbereich und eine begrenzte Genauigkeit bei der Zahlendarstellung und bei der Durchführung der Operationen vor, siehe S. 28. Eine höhere Genauigkeit kann man nur auf Kosten eines größeren Speicherplatzverbrauches erreichen. Java bietet zwei Implementierungen des Datentyps Real an, siehe auch Abschnitt 1.5.2 auf S. 29: float : ±1.5 E –45 ... 3.4 E 38 double : ±5.0 E –324 ... 1.7 E 308

2.3.8

Mehrsortige Datenstrukturen

Die meisten Programme gehen mit Daten verschiedener Typen gleichzeitig um. Insbesondere kommen Operationen vor, die Daten verschiedener Typen verknüpfen, um Daten eines anderen Typs zu erhalten. Ein Beispiel ist der Java-Operator „_?_:_“. Er verknüpft einen booleschen Wert b und zwei Werte w1 und w2 eines beliebigen Datentyps T und liefert einen Wert aus T. Somit hat der Operator die Signatur _?_:_ : boolean × T × T → T er ist also ein mehrsortiger Operator, weil er Werte unterschiedlicher Datentypen verknüpft. Genauer gilt: true ? w1 : w2 = w1 false ? w1 : w2 = w2 Eine Anwendung wäre etwa die Definition des Maximums zweier Zahlen: max(x,y) = (x>y) ? x : y Prädikate Prädikate sind Operationen mit Ergebnistyp boolean. Da als Ergebnis eines Prädikates nur true oder false sein kann, dienen sie dazu, eine Eigenschaft der verknüpften Daten festzustellen. So gibt es für die meisten Datentypen das Prädikat „=“, um festzustellen, ob zwei Werte gleich sind, also

2.3 Daten und Datenstrukturen

107

x = y ergibt true, falls x und y denselben Wert repräsentieren, sonst false. Allen bereits besprochenen Datentypen müssten wir noch die Operation „=“ hinzufügen, z.B. =: =: =: =:

Boolean × Boolean → Boolean Nat × Nat → Boolean Integer × Integer → Boolean Real × Real → Boolean

In jedem der obigen Fälle ist „=“ zwar eine andere Operation, begrifflich sind diese aber insofern gleich, als sie alle die gleichen Gesetze erfüllen. Man spricht hier von „Polymorphie”. Wegen der bei konkreten Implementierungen auftretenden Rechenungenauigkeiten sind Ergebnisse solcher Vergleiche auf bestimmten Datentypen mit Vorsicht zu interpretieren. Wir hatten bereits auf S. 28 gesehen, dass in einem Java-Programm 0.1 + 0.2 ≠ 0.3 gilt und somit die Operation = und auch die darauf basierenden Operationen ≤ , ≠, etc. mit Bedacht zu benutzen sind. Zudem sind Datentypen denkbar, auf denen die Operation = keinen rechten Sinn macht oder zumindest schwierig zu realisieren wäre wie etwa bei Musikstücken oder Videos. Ähnlich wie mit = verhält es sich auch mit < , ≤ , > , ≥ und ≠. Auch sie sind auf Nat, Integer und Real definiert und liefern einen booleschen Wert. In Pascal setzt man auf den booleschen Werten noch willkürlich false < true und hat damit auch die Ordnungsrelationen übertragen – in Java ist dies nicht erlaubt. Prädikate kann man immer auch als Relationen verstehen, denn ein Prädikat P : A1 × A2× ... × An → Boolean ist eindeutig bestimmt durch die Wertetupel, die auf true abgebildet werden { (a1,a2,...,an) | P(a1,a2,...,an) = true }. Umgekehrt bestimmt jede n-stellige Relation also jede Teilmenge R ⊆ A1 × … × A n ein charakteristisches Prädikat durch

PR : A1 × A2× ... × An → Boolean PR(a1,...,an) = ( (a1,...,an) ∈ R ).

Konversionen Werden Integer und Real-Zahlen verknüpft, so werden die ersteren automatisch in Real-Zahlen umgewandelt. Addiert man z.B. 1 + 1.3, so wird zunächst die ganze Zahl 1 in die reelle Zahl 1.0 umgewandelt bevor die reelle Multiplikation 1.0+1.3 ausgeführt wird. In Pascal ist die Division / nur auf Real-Zahlen erklärt, so dass zur Berechnung eines Ausdrucks wie z.B. 22/7 zunächst beide Argumente in Real-Zahlen umgewandelt werden und dann eine Real-Division mit Ergebnis 3.14285 stattfindet. Java interpretiert 22/7 als Division mit Rest und liefert als Ergebnis 3. Erst wenn ein Argument als Real-Zahl kenntlich ist, etwa

108

2 Grundlagen der Programmierung

im Falle 22.0/7 oder 22/7.0 wird das andere Argument auch nach Real umgewandelt und danach eine Real-Division gestartet. Solche Konversionen geschehen implizit, der Benutzer muss nichts dazutun. Es ist aber auch möglich, explizite Datentypkonversionen vorzunehmen, sofern dies einen Sinn macht. Beispielsweise liefert die explizite Konversion einer reellen Zahl in eine ganze Zahl den ganzzahligen Anteil. In Java werden solche Konversionen durch sogenannte casts vorgenommen. Dem umzuwandelnden Typ wird einfach der Name des Zieltyps in Klammern vorangestellt. So liefert z.B. (int) -3.75 (double) 3 (char) 981 (float)’a’ (byte) 12345

= = = = =

-3 3.0 φ

97.0 57

Wie man an dem letzten Beispiel sieht, kann bei der Konversion Präzision verlorengehen. Wenn man eine solche Konversion in einem Java-Programm verwendet gibt der Compiler eine Warnung „possible loss of precision“ aus.

2.3.9

Zeichen

Ein weiterer einfacher Datentyp ist der Datentyp char. Die Wertemenge ist eine geordnete Menge von Zeichen. Während Pascal dazu die 256 ASCII-Zeichen verwendet, stellt Java bereits den gesamten UNICODE-Zeichensatz (vgl.S. 13) zur Verfügung, der neben den lateinischen Schriftzeichen auch kyrillische, chinesische, japanische, koreanische und zahlreiche weitere Zeichensätze enthält. UNICODE benutzt 2 Bytes für die Darstellung eines Zeichens. Die zueinander inversen casts (byte) und (char) übersetzen zwischen den ASCII-Zeichen und den zugehörigen Nummern 0 ... 255, analog übersetzen (int) und (char) zwischen UNICODEZeichen und Integerzahlen. Konstante Werte vom Typ char können in der Form ’x’ notiert werden. Dabei muss x ein darstellbares Zeichen des ASCII-Codes sein. Weil der ASCII-Code sowohl die Kleinbuchstaben als auch die Großbuchstaben in derselben Reihenfolge aufzählt, kann man leicht ein Zeichen xKlein in sein großgeschriebenes Pendant xGross umwandeln. Wegen (int) xGross - (int) xKlein = (int) ’A’ - (int) ’a’ erhalten wir durch Auflösen nach xGross: xGross = (char)( (int) ’A’ - (int) ’a’ + (int) xKlein ). Da Java Konversionen von char in Zahlentypen automatisch vornimmt, können wir das sogar verkürzen zu xGross = (char)(’A’ - ’a’ + xKlein).

2.3 Daten und Datenstrukturen

109

Sonderzeichen (wie z.B. ’^’, „neue Zeile“, ’ß’) können ebenfalls mittels der Funktion (char) niedergeschrieben werden, z.B. gilt laut ASCII-Tabelle: ’^’ = (char)94, „neue Zeile“ = (char)10, ’ß’ = (char)223. Datentyp:

Char

Werte: Alle UNICODE-Zeichen Operationen: (char) (int)

: Integer → Char : Char → Integer

Konstanten: Alle Zeichen in der Notation ’x’ Gleichungen: (int)(char) n = n (char)(int) c = c Das folgende Bild gibt noch einmal einen Überblick über die besprochenen einfachen Datenstrukturen. Man sieht, dass char eigentlich nur ein Spiegelbild von int (bzw. byte oder short) ist. Während Umwandlungen zwischen den Typen in Java generell durch casts erledigt werden, muss man in Pascal jeweils auf spezielle Funktionen zurückgreifen. Für die Umwandlung von real nach integer stehen z.B. die Funktionen truncate und round zur Verfügung. Die erstere schneidet die Nachkommastellen ab, verhält sich also wie der entsprechende cast in Java, die andere rundet zur nächstgelegenen ganzen Zahl. +, -, *, div , mod, ( /, % )

+, -, *, /, sin, log, sqrt,…

float float double double

Reelle Reelle Zahlen Zahlen

cast

Ganze Ganze Zahlen Zahlen

cast

byte byte short short int int long long

char char

Vergleichsoperationen: = , ≤ , ≠ (!=), ≥ , < , >

boolean Abb. 2.11:

and, or, not ( &, |, ! )

Einfache Datentypen und ihre Operationen. In Klammern die Operationsnamen in Java.

110

2.3.10

2 Grundlagen der Programmierung

Zusammengesetzte Datentypen – Strings

Die bisher besprochenen Datentypen sind in einem gewissen Sinne atomar: Ihre Werte sind nicht weiter in sinnvolle Bestandteile zerlegbar. Nicht-atomare Datentypen sind aus einfacheren Bestandteilen zusammengesetzt. Als Konstruktoren bezeichnet man die Funktionen, mit denen man die zusammengesetzen Daten aus den einfacheren Bestandteilen zusammensetzen kann. Umgekehrt heißen die Funktionen, mit denen man auf die einzelnen Bestandteile zugreift, Selektoren. Als erstes Beispiel eines zusammengesetzten Typs betrachten wir Strings, zu deutsch Zeichenketten. Strings werden konstruiert, indem man eine beliebige Folge von Zeichen in besondere Begrenzungszeichen einschließt. Pascal benutzt dafür einzelne Anführungszeichen oben: ’Ich bin ein String in Pascal’, Java benutzt doppelte Anführungszeichen oben: "Ich bin ein Java-String!". Solche unmittelbar niedergeschriebenen Strings werden auch als Stringliterale bezeichnet. Der leere String "" ist ein Spezialfall eines Stringliterals. Aus zwei vorhandenen Strings s1 und s2 kann man durch Verkettung (Konkatenation) einen String s1 + s2 gewinnen. Beispielsweise gilt "Hallo" + " Welt !" = "Hallo Welt !" Auf die Bestandteile, also auf die einzelnen Zeichen eines Strings kann man unter Angabe ihrer Position zugreifen. Pascal bezeichnet mit s[i] das i-te Zeichen des Strings s, Java verwendet dazu die Funktion charAt. Somit sind "_" und + Konstruktoren, während charAt (in Java) bzw „_[_]“ (in Pascal) Selektoren sind. Neben Konstruktoren und Selektoren gibt es weitere nützliche Operationen, so z.B. length, um die Länge eines Strings zu bestimmen, oder indexOf (in Pascal: pos ), um einen Teilstring in einem größeren String zu finden. Das Ergebnis ist -1 (in Pascal: 0) bei Misserfolg, ansonsten die Position, an der der Substring gefunden wurde. Java numeriert die Positionen beginnend mit 0, so z.B."test".charAt(2)==’s’ und "test".indexOf("es")==1. Der Vergleich von Zeichenketten liefert ein Ergebnis, das von Länge und Inhalt abhängt. Die Zeichenfolgen werden positionsweise von links nach rechts verglichen. Sobald sich die erste Ungleichheit ergibt, bestimmen die beiden Zeichen an dieser Position das Ergebnis. Wenn die Zeichenketten bis zum Ende der kürzeren Kette gleich sind, gilt diese als kleiner. Diese Art, Strings zu ordnen, entspricht der Reihenfolge der Worte in einem Lexikon oder einem Wörterbuch. Man nennt sie daher auch lexikalische Ordnung. Alle Programmiersprachen stellen Funktionen zum Stringvergleich bereit, manche verwenden sogar die Relationen =, = . Datentyp: String Werte: Alle Zeichenketten Operationen: + "" charAt length

: String × String → String : → String : String × Integer → Char : String → Integer

2.3 Daten und Datenstrukturen =, 100 f(n) =  f(f(n + 11)) , sonst.  Ein weiteres Beispiel einer rekursiven Definition, die nicht dem primitiv rekursiven Schema folgt, ist die Definition der Fibonacci-Funktion. Angeblich kann man damit die Anzahl der neugeborenen Kaninchenpaare nach n Jahren bestimmen, wenn man gewisse mathematische Annahmen an deren Fortpflanzungsverhalten macht, so etwa, dass in jedem Jahr die ein- und die zweijährigen genau ein Paar Nachkommen zeugen: Am Anfang, im Jahre 0, beginnt es mit einem Kaninchenpaar. Im Jahre 1 hat dieses Paar 1 Paar Nachkommen. Im folgenden Jahr haben diese und auch die Eltern je ein Paar Nachkommen, insgesamt werden also 1+1=2 Paar geboren. Ein Jahr später haben diese und auch ihre Eltern je

2.8 Rekursive Funktionen und Prozeduren

153

ein Paar Nachkommen. Die Großeltern sind inzwischen in Rente und nicht mehr fortpflanzungswillig. Es werden also 1+2=3 Paare geboren. So entsteht also die mit 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, ... beginnende Folge, die nach Leonardo von Pisa, dem Sohn (filius) von Bonacci, kurz Fibonacci, benannt ist. Der Funktionskörper hat hier zwei rekursive Aufrufe: falls n ≤ 1  1, fibo(n) =  fibo(n – 2) + fibo(n – 1) , sonst.  In der folgenden Figur sind die entstehenden rekursiven Aufrufe als Baum dargestellt: fibo(5)

fibo(4)

fibo(3)

fibo(2)

fibo(1)

fibo(3)

fibo(0)

fibo(1)

fibo(1)

fibo(2)

fibo(1) Abb. 2.29:

fibo(1)

fibo(2)

fibo(0)

fibo(0)

Aufrufverhalten der Fibonacci-Funktion

Ein Aufruf mit einem genügend großen Argument führt also zu zwei neuen Aufrufen, diese jeweils zu zwei erneuten Aufrufen etc. fibo(5), beispielsweise, ruft fibo(3) und fibo(4) auf, fibo(3) ruft fibo(1) und fibo(2), und fibo(4) ruft erneut fibo(3) und fibo(2). Rekursive Definitionen sind fast immer eleganter als ihre iterativen Gegenstücke, sie sind daher auch einfacher zu programmieren. Gute Compiler erzeugen daraus auch effizienten Code, dennoch sind iterative Formulierungen von Programmen meist etwas schneller. Bevor man aber daran geht, eine elegante rekursive Lösung in ein unübersichtliches iteratives Programm umzuwandeln, sollte man sich überlegen, ob der Aufwand lohnt. Wir betrachten jetzt zwei spezielle Typen von Rekursion, für die wir anschließend effiziente iterative Umwandlungen angeben: Endrekursion und lineare Rekursion.

2.8.7

Endrekursion

Eine Funktionsdefinition f ist endrekursiv (engl. tail recursive), falls sie die Form hat:  g(x) , falls P(x) f(x) =  f(r(x)) , sonst. 

154

2 Grundlagen der Programmierung

Da geschachtelte Ausdrücke, wie f(r(x)) , von innen nach außen ausgewertet werden – zunächst wird r auf x angewendet, dann f auf das Ergebnis – ist der Ausdruck endrekursiv berechtigt, denn der rekursive Aufruf von f ist die letzte Aktion im else-Zweig. Zur Illustration zeigen wir die gleiche Funktion, even, sowohl in einer endrekursiven Definition (even1) als auch in einer nicht endrekursiven Definition (even2):  ( n=0 ), falls n ≤ 1 even 1 ( n ) =   even 1 ( n – 2 ), sonst falls n ≤ 1  ( n=0 ), even 2 ( n ) =   not even 2 ( n – 1 ), sonst

Die letzte Version ist nicht endrekursiv, da nach dem rekursiven Aufruf von even_2 noch die Operation not auszuführen ist. Betrachtet man das Aufrufverhalten einer endrekursiven Funktion, so fällt auf, dass ein ursprünglicher Aufruf immer wieder durch einen neuen Aufruf derselben Funktion, allerdings mit einfacheren Argumenten, ersetzt wird. Für even_1 gilt z.B.: even 1(7) = even 1(5) = even 1(3) = even 1(1) = false . Für die nicht-endrekursive Form gilt dagegen: even 2(3) = not even 2(2) = not not even 2 ( 1 ) = not not not even 2(0) = not not not true = not not false = not true = false. An den Aufruf mit dem einfacheren Argument schließt sich in diesem Fall die Anwendung von not an. So wie oben bei der Funktion even1 lesen wir auch aus dem allgemeinen Schema einer tailrekursiven Funktion ab, wie sie sich iterativ berechnen lässt: 3

k

k

f ( n ) = f ( r ( n ) ) = f ( r ( r ( n ) ) ) = f ( r ( n ) ) = ... = f ( r ( n ) ) = g ( r ( n ) ) ) , wobei k die kleinste natürliche Zahl ist, für die P(rk(n)) = true ist. Auf diese Weise können wir f auch durch ein iteratives Programm berechnen: while (!P(x)) x = r(x); return g(x); Die Korrektheit des Programms leuchtet ein. Wir werden in Abschnitt 2.10 Methoden kennen lernen, dies auch formal zu beweisen. Für diesen Fall sei bereits hier angemerkt, dass wegen f(x)=f(r(x)) der Wert von f(x) in der gesamten Schleife gleich bleibt, obwohl x verändert wird.

2.8 Rekursive Funktionen und Prozeduren

2.8.8

155

Lineare Rekursion

Das häufig vorliegende Schema der linearen Rekursion verallgemeinert sowohl das primitiv rekursive Schema, als auch das der Endrekursion  g(x) , falls P(x) f(x) =   h(x ,f(r(x))) , sonst Wählt man für r die Vorgängerfunktion (r = pred), so erhält man die primitive Rekursion und setzt man h(x,y) = y , so erhält man die Endrekursion. Mit der Fakultätsfunktion hatten wir eine linear rekursive Funktion vorliegen. Sie ergibt sich, mit h ( x, y ) = x * y , r ( x ) = x – 1 , g ( x ) = 1 und P ( x ) ≡ ( x = 1 ) . Auch linear rekursive Funktionen lassen sich in nichtrekursive Form bringen. Expandieren wir die rekursive Definition k-mal, so finden wir: f(x)

= h ( x, f ( r ( x ) ) ) ) 2 = h ( x, h ( r ( x ), f ( r ( x ) ) ) ) = ... 2 k–1 k = h ( x, h ( r ( x ), h ( r ( x ), h ( …, h ( r ( x ), g ( r ( x ) ) ) … ) ) ) ) ,

wobei k wieder die kleinste natürliche Zahl ist mit P(rk(x)) = true. Die Folge x, r(x), r2(x), ... ist leicht zu berechnen. Sobald P(rk(x)) = true ist, berechnen wir den obigen Ausdruck von innen nach außen. Dazu benötigen wir allerdings die ri(x) in der umgekehrten Reihenfolge ihrer Erzeugung. Wir müssen sie also speichern und rückwärts dem Speicher wieder entnehmen. Für solche Zwecke ist die Datenstruktur des Stacks (oder Stapels) geschaffen, die wir in Kapitel 4 noch genau besprechen werden. Mit push(x,s) legen wir den Wert x zuoberst auf den Stapel s, und sofern der Stapel nicht leer ist, was wir mit Empty überprüfen können, dürfen wir mit top den obersten Wert einsehen. Mit pop können wir den obersten Wert des Stapels wieder entfernen. Wichtig ist, dass der zuletzt abgelegte Wert als erstes wieder entfernt werden muss – last in first out. Unser Programm lautet somit: while ( !P(x)){ push(x,s); x:=r(x); } f = g(x); while (! Empty(s)){ x = top(s); pop(s); f = h(x,f); } return f;

156

2 Grundlagen der Programmierung

Linear rekursive Funktionen lassen sich unter gewissen Umständen bereits in eine endrekursive Form bringen. Falls nämlich die zweistellige Operation h(x, y) die folgenden zwei Bedingungen erfüllt, (i) (ii)

∀( x, y, z ). h ( x, h ( y, z ) ) = h ( h ( x, y ), z ) ∃e . ∀x.h ( e, x ) = x

dann heißt h eine assoziative Operation mit Linkseinheit. In diesem Falle definieren wir: falls P(x)  h(a ,g(x)) , fAux(x ,a) =  fAux(r(x) ,h(a ,x)) , sonst.  Offensichtlich ist fAux eine endrekursive Funktion, und wir behaupten darüber hinaus: f(x) = fAux(x,e). Diese Aussage möchten wir durch Induktion beweisen. Es zeigt sich aber, dass ein Induktionsbeweis nicht unmittelbar gelingt. Erst wenn wir die Behauptung verschärfen, ergibt sich die Behauptung als Spezialfall der folgenden Behauptung: Für alle a und alle x, für die f(x) terminiert, gilt: fAux(x,a) = h(a,f(x)). Beweis: Wenn f(x) terminiert, so muss es ein n geben mit P(rn(x)). Wir führen die Induktion über die kleinste Zahl k, für die P(rk(x)) gilt. Ist k = 0, dann gilt P(x) und die Behauptung ist trivial. Sei nun k = n + 1, dann gilt not P(x) und P(rk(x)) also P(rn(r(x))). Nach Induktionsvoraussetzung gilt daher die Behauptung, wenn wir x durch r(x) und a durch h(a,x) ersetzen, wir können also bereits fAux(r(x),h(a,x)) = h(h(a,x),f(r(x))) voraussetzen. Somit gilt: fAux(x ,a)

= = = =

fAux(r(x) ,h(a ,x)) h(h(a ,x) ,f(r(x))) h(a ,h(x ,f(r(x)))) h(a ,f(x))

{ wegen not P(x) } { Ind.voraussetzung } { Assoziativität von h } { Def. von f(x) }.

Mit a = e folgt also f(x) = h(e ,f(x)) = fAux(x ,e) . Beispiele linear rekursiver Funktionen, die die obigen Bedingungen erfüllen, sind u.a. falls n=0  1, fact(n) =  n ⋅ fact(n – 1) , sonst, 

und

falls n=0  0, sum(n) =  n + sum ( n – 1) , sonst.  Im ersten Fall gilt h(x ,y) = x ⋅ y und e = 1, im zweiten h(x,y) = x + y und e = 0.

2.8 Rekursive Funktionen und Prozeduren

2.8.9

157

Eine Programmtransformation

Zum Abschluss dieses Kapitels wollen wir noch zeigen, wie sogar die Fibonacci-Funktion in eine endrekursive und letztlich also iterative Form umgewandelt werden kann. Wir wenden dabei die Technik der akkumulierenden Parameter an, die jeder „Funktionale Programmierer” beherrschen sollte. Bei der Fibonacci-Funktion fällt auf, dass viele Teilergebnisse immer wieder erneut berechnet werden. Wenn es gelingt, diese aufzubewahren, kann man sich alle erneuten Berechnungen sparen. Es zeigt sich in diesem Spezialfall, dass man immer nur zwei Werte speichern muss, nämlich fibo(n – 1) und fibo(n – 2). Daraus lässt sich fibo(n) berechnen. Speichert man anschließend fibo(n) und fibo(n – 1), so lässt sich daraus fibo(n + 1) berechnen etc. Am elegantesten geht dies, wenn wir die Fibonacci-Funktion verallgemeinern: fiboAux erhält drei Parameter, N, Acc1 und Acc2. In den letzten beiden Parametern halten wir immer die Werte von fibo(k – 2) und fibo(k – 1). Diese heißen auch akkumulierende Parameter, weil sich in ihnen das Ergebnis ansammelt. Der erste Parameter zählt herunter, wie oft fibo(k – 2) und fibo(k – 1) durch fibo(k – 1) und fibo(k) zu ersetzen sind. Die verallgemeinerte Funktion ist: int fiboAux (int n, int acc1, int acc2){ if (n==0) return acc1; else return fiboAux(n-1, acc2, acc1+acc2); } Diese Funktion ist offensichtlich endrekursiv und berechnet die Fibonacci-Funktion fibo(n), falls wir sie mit acc1=1 und acc2=1 aufrufen: int fibo(int n){ return fiboAux (n,1,1); } Der Aufruf von fibo(5) führt damit zu einer erheblich effizienteren Berechnung: fibo(5) = fiboAux(5, 1, 1) = fiboAux(4, 1, 2) = fiboAux(3, 2, 3) = fiboAux(2, 3, 5) = fiboAux(1, 5, 8) = fiboAux(0, 8, 13) = 8. Dass die durchgeführten Umwandlungen zulässig sind, bedarf eines Beweises. Wir vergleichen also die Definitionen falls n ≤ 1  1, fibo(n) =  fibo(n – 2) + fibo(n – 1) , sonst.  falls n=0  a 1, fiboAux ( n, a 1, a 2 ) =   fiboAux ( n – 1, a 2, a 1 + a 2 ), sonst.

Die Behauptung ist: fibo(n) = fiboAux(n ,1 ,1) . Wieder lässt sich diese Behauptung nicht direkt per Induktion beweisen. Sie folgt aber für den speziellen Fall k = n aus der folgenden allgemeineren Behauptung:

158

2 Grundlagen der Programmierung

Behauptung: Für alle k ,n ∈ Nat mit k ≤ n gilt: fiboAux(k, fibo(n – k), fibo(n – k + 1)) = fibo(n) . Beweis (Induktion über k): Für k = 0 ergibt sich fiboAux(0, fibo(n), fibo(n + 1)) = fibo(n) sofort aus der Definition von fiboAux. Für k = r + 1 rechnen wir nach: fiboAux(r + 1, fibo(n – r – 1), fibo(n – r)) = fiboAux(r, fibo(n – r), fibo(n – r – 1) + fibo(n – r)) = fiboAux(r, fibo(n – r), fibo(n – r + 1)) = fibo(n) .

2.9

Typen, Module, Klassen und Objekte

Mit den bisher eingeführten Konzepten können klar gegliederte Programme für jeden Zweck erstellt werden. Im Laufe der letzten 40 Jahre sind die Anforderungen an eine Programmiersprache aber erheblich gestiegen. Es werden Konzepte benötigt, um sehr große Programme zu schreiben, die trotzdem übersichtlich bleiben und nachträglich noch geändert und erweitert werden können. Strukturiertes Programmieren ging einen großen Schritt in diese Richtung. Sprachkonzepte, die zwar effizienten aber unübersichtlichen Code produzierten wurden durch klare mathematische Konzepte ersetzt. Dies betraf sowohl die Programmstruktur als auch die Datentypen. An kommerziellen Programmen arbeiten Gruppen von Programmierern gleichzeitig, sie müssen ihre Komponenten getrennt testen und die einzelnen Teile sollen am Schluss zusammenpassen. Schließlich will man Teile bereits vorhandener Programme für andere Zwecke wiederverwenden können. Eine Lösung dafür wurde in dem Konzept des Modularen Programmierens gefunden, das in den 70er Jahren eingeführt wurde. Mit der Einführung der grafischen Bedienoberflächen in den 80er Jahren tauchten aber die nächsten Schwierigkeiten auf. Ein Programm, das in einer fensterbasierten Umgebung arbeitet und das von einer Maus gesteuert wird, muss mit den sehr komplexen Ressourcen des Betriebssystems und vor allem des Fenstersystems umgehen können. Es ist für den Programmierer unvorhersehbar, wann der Benutzer seine Maus in welches Fenster bewegt, und er kann nicht überall Warteschleifen einbauen, um dauernd solche Ereignisse abzufangen. Statt dessen möchte er beim Programmieren von der Vorstellung ausgehen, dass die Maus mit dem Betriebssystem kommuniziert, indem sie ihm Botschaften (engl. messages) schickt und dass die einzelnen Objekte des Fenstersystems auf solche Ereignisse (engl. events) reagieren. Diese Sichtweise führte ab Mitte der 80er Jahre zum Siegeszug des Objektorientierten Programmierens.

2.9 Typen, Module, Klassen und Objekte Strukturiertes Strukturiertes Programmieren Programmieren

1960

1970

Modulares Modulares Programmieren Programmieren

1980

159 Objektorientiertes Objektorientiertes Programmieren Programmieren

1990

2000

2010

Entwicklung moderner Programmierkonzepte

Java ist eine objektorientierte Programmiersprache, aber die Lektionen, die das strukturierte und das modulare Programmieren uns gelehrt haben, sind nicht vergessen und werden auch weiter gültig bleiben. Der eilige Leser, der nur an den Möglichkeiten aktueller Sprachen interessiert ist, mag die folgenden drei Unterkapitel überlesen und gleich zur Objektorientierten Konstruktion neuer Datentypen springen. Wenn er aber später mit Muße zurückkommt, wird er entdecken, dass viele schöne, mathematisch klare und bewährte Konzepte in neuen Sprachen wie Java leider nicht vorhanden sind und er wird sich vielleicht danach sehnen, dass sie in einer neuen Version wiederkommen. Für die Erläuterung des strukturierten Programmierens müssen wir notgedrungen auf die Pascal-Syntax zurückgreifen. Eine hierarchische Strukturierung des Programmcodes hat es in C nie gegeben und konsequenterweise fehlt jede solche Möglichkeit auch in Java. Die Strukturierung der Datentypen, wie Pascal sie vormachte, verträgt sich nur teilweise mit der Objektorientierung, daher fehlt auch diese in Java. Um die Konzepte des Modularen Programmierens zu erläutern, können wir wieder auf Java zurückgreifen. Man kann nämlich in Java auch modular programmieren, ohne sich weiter um die Objektorientierung zu kümmern. Um die Konzepte zu verdeutlichen, wollen wir auch so vorgehen. Wir werden uns zunächst darum kümmern, wie man mit Schnittstellen und Klassen Programme aus einzelnen weitgehend unabhängigen Bestandteilen zusammenstecken kann, und werden dazu Klassen und Schnittstelle (interfaces) diskutieren. Anschließend führen wir Objekte und Objektmethoden ein und diskutieren die fundamentalen Konzepte Vererbung, Datenkapselung und Polymorphie. Dies folgt ganz und gar nicht dem Schlachtruf „Objects First“, unter dem einige Didaktiker angetreten sind, objektorientiertes Programmieren zu vermitteln. Unser Anliegen in diesem Kapitel ist aber nicht primär eine Einführung in Java oder in das Objektorientierte Programmieren, sondern vielmehr ein Kennenlernen fundamentaler Konzepte imperativer Programmiersprachen. Java dient uns als Vehikel, nicht als Objekt der Untersuchung. Dennoch wird hier der Grundstein für ein eingehendes Studium von Java gelegt, das wir im folgenden Kapitel in Angriff nehmen werden.

2.9.1

Strukturiertes Programmieren

Die ersten Programmiersprachen orientierten sich noch sehr an den unmittelbar verfügbaren Möglichkeiten der CPU, einfache Rechen- und Speicheroperationen durchzuführen, Bedingungen zu testen und nach Bedarf an eine andere Stelle im Code zu springen. Symptomatisch dafür war die auch in höheren Programmiersprachen verfügbare goto-Anweisung, mit der man die Berechnung an jeder beliebigen Stelle im Programm fortsetzen konnte. Schon mittel-

160

2 Grundlagen der Programmierung

große Programme wurden auf diese Weise schnell unübersichtlich und bereits 1968 kritisierte E. W. Dijkstra in einem berühmten Artikel „Go To Statement Considered Harmful“ dessen Benutzung. Er schlug vor, sich stattdessen auf if-, while- und repeat-Anweisungen zu beschränken, oder, besser noch, Schleifen durch Rekursion zu ersetzen. Diese Ansicht hat sich heute auf breiter Front durchgesetzt, und obwohl viele moderne Programmiersprachen ein goto noch zulassen, ist sein Gebrauch heute geächtet. Java hat zwar kein goto mehr, hat vorsichtigerweise aber das Schlüsselwort goto reserviert.

2.9.2

Blockstrukturierung

Ein zweiter Aspekt des strukturierten Programmierens ist die Lokalität der Namen, insbesondere der Namen von Variablen und Funktionen. Jedes Programm und jede Funktion in Pascal besteht aus einem Kopf und einem Block. Ein Block setzt sich aus einem Deklarationsteil und einem Anweisungsteil zusammen. Im Deklarationsteil wurden Variablen und Funktionen definiert und im Anweisungsteil können diese benutzt werden. Wichtig ist, dass die innerhalb eines Blocks definierten Namen außerhalb des Blocks nicht mehr existieren und somit anderweitig wiederbenutzt werden konnten. So ist ein Pascal-Programm eine Hierarchie von Funktionsblöcken. Die äußerste Funktion ist das Programm. Im Deklarationsteil werden Variablen und Funktionen definiert. In deren Deklarationsteil evtl. wieder Hilfsvariablen und Hilfsfunktionen, etc. Ein Pascal-Programm ist also eine sehr geordnete und übersichtliche Angelegenheit und viele Fehler, die aus der unüberlegten Benutzung der gleichen Variablen an verschiedenen Stellen des Programms entstehen könnten, werden vermieden. Globale Namen, also solche, die überall sichtbar sind, versucht man durch lokale Namen zu ersetzen, also solche, die nur in dem Block, in dem sie gebraucht werden, gültig sind. Diese Möglichkeit, die Gültigkeit von Namen einzuschränken, hat man in Pascal – im Unterschied zu C oder Java – nicht nur für Namen von Variablen, sondern auch von Funktionen oder Typen. Auf diese Weise kann man sogar ein großes Programmpaket übersichtlich in einer einzigen Datei bearbeiten und verwalten.

2.9.3

Strukturierung der Daten

Theoretisch gibt es nichts, was man nicht durch Zahlen repräsentieren könnte. Statt mit Personen kann man mit Personalnummern arbeiten, die Farben rot, grün, blau in einem Graphikpaket könnte man durch 0,1,2 ersetzen und selbst Zahlentupel (a,b) kann man in einer Zahl kodieren, z.B. als n*a+b, wenn man davon ausgeht, dass die Werte für b nie größer als n sind. Allerdings entsteht auf diese Weise sicher kein übersichtliches Programm. Aus diesem Grund hat Pascal bereits eine bequeme Strukturierung von Daten vorgesehen. Mit einer sogenannten Typdefinition kann man sich neue Typen bequem definieren, Variablen zu diesen selbstdefinierten Typen deklarieren und mitgelieferte Funktionen anwenden. Auch die Notation ist sehr gut durchdacht, weil sie weitgehend den mathematischen Gebrauch wiederspiegelt. Typen sind Mengen gleichartiger Daten und Variablen eines Typs stehen für Elemente der entsprechenden Menge. So lässt sich eine Variablendeklaration in Pascal

2.9 Typen, Module, Klassen und Objekte

161

VAR x,y : T; a, b, c : Color; lesen wie die Einleitung eines mathematischen Textes: SEIEN x,y



T und a,b,c ∈ Color;

Um neue Typen zu bilden, hat man in Pascal die gleichen Werkzeuge, die auch Mathematikern zur Verfügung stehen, um neue Mengen zu bilden. Wir wollen die wichtigsten davon kurz ansprechen: 1. Mengenbildung durch Aufzählung: T = { a1, a2, ... , an} Dem entsprechen in Pascal sogenannte Aufzählungstypen: TYPE Farbe = (rot, gruen, blau, weiss, grau, schwarz); Monat = (Jan, Feb, Mar, Apr, Mai, Jun, Jul, Aug, Sep, Okt, Nov, Dez); Man könnte z.B. mit einer FOR-Schleife alle Monate durchlaufen – unter Auslassung der Wintermonate: VAR m : Monat; ... BEGIN FOR m := Mar TO Okt DO ... 2. Intervall einer vorhandenen geordneten Menge: T = [alow, ahigh] Repräsentiert ein Typ eine geordnete Menge, so kann man durch Angabe zweier Elemente einen Abschnitt heraustrennen und als neuen Typ deklarieren: TYPE Bunt = [ rot .. blau]; Sommer = [ Mai .. Sep]; Temperatur = [-40 .. 100]; Alter = [0 .. 100]; Sind Variablen erklärt, wie in VAR x : Temperatur; y : Alter; z : Integer; so überprüft der Compiler, dass man nicht versehentlich Typen mischt. So würden z.B. die Anweisungen x := y oder x := z als falsch zurückgewiesen.

162

2 Grundlagen der Programmierung

3. Aufzählungen, Maps: TI I sei dabei eine Indexmenge und T beliebig. Ein Element von TI ist eine Abbildung a von I nach T, d.h. für i ∈ I ist a(i) ein Element aus T. Wenn wir a[i] statt a(i) schreiben, erkennen wir, dass a nichts anderes ist, als ein Array – gegenüber Arrays in Java aber mit dem entscheidenden Vorteil, dass der Index nicht notwendigerweise aus den Zahlen von 0 bis n-1 bestehen muss, er kann ein beliebiger diskret geordneter Typ sein. Wir können also schreiben: TYPE TemperaturListe = ARRAY [Jan .. Dez] OF Temperatur; Jede Variable tl vom Typ TemperaturListe könnte z.B. genutzt werden, um die monatliche Durchschnittstemperatur festzuhalten: tl[Okt] := (tl[Sep] + tl[Nov]) div 2; Genauer wäre eine Tabelle die alle Tagestemperaturen enthält: TYPE TempTab = ARRAY[1 .. 31,Jan .. Dez ]OF Temperatur; Ist tt eine Variable vom Typ TempTab, so tragen wir Temperatur von heute (1. August) ein: tt[1,Aug] := 23; In Vergleich dazu wirken Java Arrays reichlich primitiv. Die Beschränkung auf Indexbereiche der Form 0..n-1 zwingt uns zur unnatürlichen Definition int[][] tt = new int[30][11]; und die Temperatur heute (am 1.8.) müssen wir eintragen als tt[0][7] = 23; 4. Potenzmengenbildung: ℘ ( T ) = { U ( U ⊆ T ) } Jedes Element dieses Typs repräsentiert eine Teilmenge des Basistyps. TYPE Mischung = SET OF Farbe; Entsprechend sind auch die Mengenoperationen wie Vereinigung (+) und Schnitt (*) und die Abfrage, ob ein Element in der Teilmenge ist (IN), verfügbar: VAR u, v, w : Mischung; f : Farbe; BEGIN u := [rot,grün]; v := u+[gelb,grün,grau]; IF f IN v THEN ...

2.9 Typen, Module, Klassen und Objekte

163

5. Produktbildung: T = T1 × T 2 × … × Tn Sind Typen T1, ..., Tn gegeben, so bezeichnet T1 × T2 × … × Tn das kartesiche Produkt, also die Menge aller Tupel ( t1, t 2, …, t n ) mit t i ∈ Ti . Typisch für einen Produkttyp wäre z.B. ein Termin, der aus Tag, Monat und Jahr besteht, oder eine Person, die einen Namen, einen Vornamen und ein Geburtsdatum hat: TYPE Termin = RECORD tag:1..31; mon:Monat; jahr:Integer END; Person = RECORD name : String[15]; vorn : String[15]; gebDat:Termin END; Auf die Felder der Tupel können wir über ihre Namen zugreifen. Ist baby z.B. als Variable vom Typ Person erklärt, so können wir seinen Namen und sein Geburtsjahr eintragen: baby.name := "Otto"; baby.gebDat.tag := 17; 6. Summen: T = T1 ⊕ T 2 ⊕ … ⊕ Tn Hierbei handelt es sich um die disjunkte Vereinigung der Typen T1, T2, …, T n . Ein Element der disjunkten Vereinigung ist ein Paar (i,t) mit t ∈ Ti . Es ist also ein Element t, das zur Vereinigung der T i gehört, zusammen mit der Information, aus welchem der T i es stammt. Beispielsweise könnten wir sagen, dass ein Wissenschaftler entweder ein Professor oder ein Mitarbeiter oder ein Student ist. Haben wir nun eine Variable w vom Typ Wissenschaftler, so wollen wir wissen, ob wir einen Professor oder einen Studenten vor uns haben, bevor wir seine Matrikelnummer oder sein Gehalt abfragen. In Pascal könnte eine erste Modellierung des Summentyps folgendermaßen aussehen: TYPE Wissenschaftler = RECORD CASE status : (prof, mitar, studi) OF prof : ( fach : String[20]; alter, gehalt : Integer ); mitar: ( fach, dissThema : String[20] ); studi: ( fach : String[20]; matrNr : Integer ) END;

164

2 Grundlagen der Programmierung

Hierbei bestimmt eine Variable status, von welchem Typ der aktuelle Mitarbeiter sein soll. Je nach Ergebnis sind die weiteren Felder entweder die von prof, von mitar, oder von studi.1 Offensichtlich gibt es ein Feld, nämlich das fach, welches in jedem der Fälle vorhanden ist. Wir können es nach vorne ziehen und erhalten: TYPE Wissenschaftler = RECORD fach : String[20]; CASE status : ( prof, mitar, studi) OF prof : ( alter,gehalt:Integer ); mitar: ( dissThema: String[20] ); studi: ( semZahl,matrNr:Integer ); END; Offensichtlich ist ein Wissenschaftler nun als Element der folgenden Menge modelliert: String × ( ( Integer × Integer ) ⊕ ( String ) ⊕ ( Integer × Integer ) ) . Haben wir einen Wissenschaftler vor uns, dann können wir über das Feld status seine Gruppenzugehörigkeit erfahren und dann auf die angemessenen Felder zugreifen: IF w.status = studi THEN w.semZahl := w.semZahl+1; 7. Induktiv definierte Typen Summen sind Ausgangspunkte für die Definition induktiver Datentypen wie z.B. von variabel langen Listen oder von Bäumen. Beispiele für solche Bäume sind z.B. die Hierarchien von Verzeichnissen und Unterverzeichnissen wie Abb 1.29 auf S. 60 eine zeigt. Listen sind entweder leer oder sie haben ein erstes Element und die Liste aller restlichen Elemente. Formatieren wir diese Definition, so erhalten wir. Eine Liste ist

leer oder hat ein erstes Element und eine Restliste .

Die logische Struktur legt folgende rekursive Definition der Menge Liste nahe. Hier bezeichnet [] die leere Liste und {[]} die ein-elementige Menge, die die leere Liste enthält. Liste = { [ ] } ⊕ ( Element × Liste )

In Pascal kann man dies auch direkt modellieren: TYPE DateiListe = RECORD CASE leer : Boolean OF true : (); false: (e:Datei; rest: ^DateiListe); END; 1. Die RECORD CASE Konstruktion ist insofern ein Schwachpunkt in Pascal, als man die Variable status nachträglich neu setzen kann. Aus einem Professor wird so ein Mitarbeiter, dessen fach oder dissThema den Speicherinhalt liefert, wo man vorher fach, alter und gehalt des Professors eingetragen hatte. So kann man das Typsystem von Pascal austricksen.

2.9 Typen, Module, Klassen und Objekte

165

Allerdings fällt das Pointerzeichen ^ vor dem rekursiven Wiedererscheinen von Liste auf. Dies ist notwendig, weil Pascal an der entsprechenden Stelle, also in der Variablen rest nicht wirklich die komplette Restliste speichert, sondern nur einen Zeiger, der auf diese deutet. Wir wollen fortfahren und Dateibäume wie in Abb 1.29 repräsentieren. Die Definition ist: Ein Dateibaum ist

eine Datei mit einem Namen oder ein Unterverzeichnis mit einem Namen und einer Liste von Dateibäumen.

Die Modellierung ist jetzt klar, aber wir ziehen noch das gemeinsame Namensfeld heraus: TYPE

DateiBaum = RECORD name: String; CASE istDatei : Boolean OF true : (); false: (inhalt: ^DateiBaumListe); END;

Eine DateiBaumListe kann man natürlich analog zur DateiListe aus dem vorangegangenen Beispiel definieren.

2.9.4

Objektorientierte Konstruktion neuer Datentypen

Ein Datentyp ist eine Menge von Daten zusammen mit einer Familie von Operationen. Mit den bereits besprochenen selbstdefinierten Funktionen sind wir in der Lage, einem vorhandenen Datentyp neue Operationen beizufügen. Im Zusamnmenhang mit Java haben wir bisher nur über die praktisch in jeder anderen Programmiersprache schon vorhandenen Datentypen gesprochen: Boolesche Werte (boolean), Ganze Zahlen (byte, short, int, long), reelle Zahlen (float, double) Zeichen (char) und Zeichenketten (String). Obwohl wir mit diesen Bordmitteln schon viel anstellen können, wäre es schön, wenn wir auch in einer objektorientierten Sprache diese Datentypen um neue, selbstgemachte, erweitern könnten. Nehmen wir zum Beispiel an, wir wollten ein mathematisches Programmpaket 8 schaffen, um mit Brüchen exakt zu rechnen, so dass 1--- + 1--- = ----- ist, statt 0.533333333333333. 3

5

15

In diesem Fall müssen wir von jedem Bruch den Zähler und den Nenner separat speichern. Auch rein objektorientierte Sprachen stellen Mittel bereit, um zusammengehörige Daten zu einem neuen Datenobjekt zu verpacken. Einen Bruch wollen wir z.B. als ein Paar (zaehler, nenner) repräsentieren, eine Person z.B. durch ein Tripel (name, vorname, geburtsDatum) und ein Datum als ein Tripel (tag, monat, jahr). In den objektorientierten Sprachen bedient man sich dabei des Begriffes der Klasse. Jede Klasse repräsentiert Tupel gleicher Bauart. Die Klasse Bruch repräsentiert Paare ganzer Zahlen, die Klasse Person repräsentiert Tripel aus zwei Strings und einem Datum und die Klasse Datum repräsentiert Tripel von Zahlen.

166

2 Grundlagen der Programmierung

In Java könnten wir z.B. eine Klasse Bruch mit den Feldern zaehler und nenner definieren: class Bruch{ int zaehler; int nenner; } oder eine Klasse Person mit den Feldern name, vorname, geburtsDatum: class Person{ String name; String vorname; Datum geburtsDatum; } Felddeklarationen folgen offenbar der gleichen Syntax wie Variablendeklarationen, so dass wir z.B. die Klasse Datum auch kurz (aber vielleicht weniger lesbar) schreiben könnten als: class Datum{ int tag, monat, jahr; } Anschließend können wir Variablen vom Typ Bruch, Person oder Datum deklarieren: Bruch p,q,r; Person freundin, kollege; Datum heute, stonesKonzert; Jede der Variablen kann dann ein Tupel, man sagt Objekt, der entsprechenden Klasse repräsentieren. Auf die Komponenten der Objekte können wir lesend oder schreibend zugreifen. Wir benennen dazu die Variable und die gewünschte Komponente getrennt durch einen Punkt p.zaehler, kollege.name, freundin.geburtsDatum, heute.tag und weiter, da geburtsdatum wieder ein Datum und name ein String ist: freundin.geburtsDatum.tag, kollege.name.charAt(0). Felder von Objekten verhalten sich wie normale Variablen und können entsprechend manipuliert werden: p.nenner = 5; kollege.vorname = "Manfred"; kollege.geburtsDatum.tag = stonesKonzert.tag-91; freundin.geburtstag = heute; Bis zu diesem Punkt unterscheiden sich Sprachen wie Java, C, Pascal und Modula nur geringfügig. In den beiden letztgenannten Sprachen benutzt man statt class das Schlüsselwort RECORD und folgt bei der Definition der Felder natürlich auch der in Pascal bzw. Modula gebräuchlichen Syntax für Variablendeklarationen. Wie bereits erwähnt, muss man in Java jede Variable vor ihrer ersten Benutzung initialisieren. Dies ist besonders für Variablen relevant, die Objekte von Klassen repräsentieren. Während

2.9 Typen, Module, Klassen und Objekte

167

bei Pascal und Modula bereits der Compiler den benötigten Speicherplatz für die verwendeten Variablen organisiert und reserviert, geschieht dies bei Java erst zur Laufzeit. Objektvariablen in Java speichern in Wirklichkeit nämlich nur eine Referenz (Verweis) auf ein Objekt, das sich zur Laufzeit irgendwo im Speicher befinden kann. Eine uninitialisierte Objektvariable hat daher zunächst den Wert null, der einer ungültigen Referenz gleichkommt. Der Programmierer muss explizit ein neues Objekt der expliziten Klasse erzeugen und an die Variable binden. Dies geschieht durch Aufruf eines sogenannten Konstruktors der Klasse im Rahmen eines new-Ausdrucks. Zu jeder Klasse gibt es automatisch eine Funktion ohne Parameter, die den Namen der Klasse trägt. Diese ist der Standard-Konstruktor. Im Falle der Klasse Bruch hat man also den Standard-Konstruktor Bruch(). Der Aufruf eines Konstruktors hat den Sinn, Platz für einen Bruch, also für zwei int-Variablen im Speicher zu finden. Mit dem Schlüsselwort new wird eine Referenz (ein Verweis) auf diesen Speicherplatz als Wert zurückgegeben, der in der Variablen gespeichert werden kann. q = new Bruch(); q.zaehler=1; q.nenner=3; Person baby = new Person(); baby.name = "Otto"; baby.geburtstag = new Date(); baby.geburtstag.tag=29; Die gezeigte Methode, die Felder eines Objektes zu initialisieren, ist sehr umständlich. Man kann es einfacher haben, wenn man sich einen Konstruktor mit Parametern definiert. Der Klasse Bruch könnten wir z.B. einen weiteren Konstruktor hinzufügen: Bruch(int z,int n){ zaehler=z;

nenner=n; }

worauf wir z.B. den Bruch q einfacher erzeugen könnten durch q = new Bruch(1,3);

2.9.5

Modulares Programmieren

Bis jetzt haben wir nur die Repräsentation der Daten betrachtet, die Brüche, Personen oder Zeitpunkte repräsentieren. Wie aber sieht es mit den Operationen aus? Wir benötigen Operationen, um z.B. Brüche zu addieren, subtrahieren, multiplizieren etc., um die Gleichheit zweier Brüche zu bestimmen, Brüche auszudrücken. Kurz, wir möchten eine Datenstruktur für Brüche mit mindestens folgender Funktionalität haben: Datentyp: Bruch x Werte: Alle Brüche -- mit x, y ∈ Integer . y

Operationen: zähler,nenner add,mult,...

: Bruch → Integer, : Bruch × Bruch → Bruch

168

2 Grundlagen der Programmierung : Bruch → Bruch, : Bruch × Bruch → Boolean, : Bruch → String

invers equal toString Konstruktor: Bruch

: Integer × Integer → Bruch

Gleichungen: Die üblichen Gleichungen. Zum Beispiel: x zaehler  -- = x  y

x nenner  -- = y  y

x u x⋅v+u⋅y -- + --- = -------------------------y v y⋅v u x -- = --- ⇔ x ⋅ v = u ⋅ y v y

Da es sich um einen „selbstgemachten“ Datentyp handelt, können wir nicht erwarten, dass die Sprache uns spezielle Syntax für die Operationen oder für Bruchkonstanten bereitstellt, so dass wir etwa das +-Zeichen für die Addition verwenden könnten, oder dass wir einen Bruch 1 in der Notation 1 ⁄ 3 oder gar --- im Programm verwenden könnten. 3

Selbstverständlich gelingt es uns aber, eine Operation add auf Brüchen zu erstellen. Sie nimmt als Parameter zwei Brüche und liefert einen Bruch, der ihre Summe repräsentiert: static Bruch add(Bruch p, Bruch q){ Bruch z = new Bruch(); z.zaehler = p.zaehler*q.nenner + q.zaehler*p.nenner; z.nenner = p.nenner*q.nenner; return z; } Diese Vorgehensweise entspricht noch nicht der üblichen Methode des objektorientierten Programmierens. Eher reflektiert sie das sogenannte modulare Programmieren. Diese soll den Programmierer dabei unterstützen, die Repräsentation der Daten und der Operationen, die auf den Daten operieren, zusammen zu halten und gemeinsam zu entwickeln und zu pflegen. Im einfachsten Falle bedeutet dies, dass die Datenrepräsentation und die Operationen in einer Datei zu halten sind. Im Falle von Java bedeutet dies, dass die Operationen, die auf den Daten operieren in der gleichen Klasse definiert werden. Die Klasse Bruch hat also die Struktur class Bruch{ int zaehler; int nenner; static Bruch add(Bruch p, Bruch q){ ... Anweisungen ... } static Bruch invers(Bruch p){ ... Anweisungen ... } } Das Schlüsselwort static ist eine Besonderheit von Java. Es besagt an dieser Stelle, dass add und invers Funktionen der Klasse Bruch sind. Der volle Name von add ist daher

2.9 Typen, Module, Klassen und Objekte

169

Bruch.add, insbesondere wenn ein Programmteil von außerhalb der Klasse Bruch diese Funktion verwenden will. Die enge Kopplung von Daten und Operationen auf diesen Daten wurde von Standard-Pascal noch nicht unterstützt. Erst Modula und später auch Turbo-Pascal und Delphi erlaubten die Trennung großer Programme in sogenannte Module, daher wohl auch der Name. Jeder Modul definierte eine Datenstruktur. Die Module konnten sogar unabhängig voneinander compiliert werden, so dass sie separat als Bausteine für ein komplexes Programm zusammengesetzt und wiederverwendet werden konnten.

2.9.6

Schnittstellen – Interfaces

Modula geht sogar einen Schritt weiter und trennt die Spezifikation einer Datenstruktur von ihrer Implementierung. Zu jeder Datenstruktur kann eine Schnittstellenbeschreibung definition module als Spezifikation separat bereit gestellt werden. Diese Schnittstelle legt fest, welche Operationen eine Datenstruktur unterstützt, deren Namen und Signaturen, d.h. Anzahl und Typen der Argumente sowie Typ des Ergebnisses. Im implementation module kann die Datenstruktur implementiert werden. Analog dazu besteht in Turbo-Pascal eine Unit aus einer interface-section und einer implementation-section. Schnittstellen sind nicht von Programmierern erfunden worden. Komplexe Geräte wie Computer oder Automobile werden nicht nur von einem Hersteller gebaut. Gibt es Vereinbarungen über die Schnittstellen, so werden die dezentral gefertigten Teile hinterher zusammenpassen. Wie ein Hersteller seine Komponente konstruiert, bleibt ihm überlassen, vielleicht ist dies auch sein Betriebsgeheimnis. Die Idee der separaten Schnittstellenbeschreibungen hat auch Java übernommen. Hier spricht man nicht von Modulen, sondern von Klassen und statt definition module gibt es den Begriff der Schnittstelle (engl.: interface). Das folgende Beispiel zeigt ein vollständiges Interface, welches bereits vom Java-Compiler übersetzt werden kann. Selbstverständlich wird kein lauffähiger Code erzeugt, weil von den Operationen nur ihre Signatur vorliegt, aber noch kein Programmcode. interface Datum{ int getDay(); int getMonth(); int getYear(); String getWeekDay(); Datum heute(); void addDays(int d); } Ein Hauptprogramm, das die Operationen der Datenstruktur verwendet, kann bereits nach Vorliegen des definition module compiliert werden. Unabhängig davon kann die Datenstruktur mit ihren Operationen tatsächlich implementiert und ebenfalls separat kompiliert werden.

170

2 Grundlagen der Programmierung

Obwohl das komplette Programm erst laufen kann, wenn alle Teile fertig sind, hat diese Vorgehensweise enorme Vorteile: – Erstens trennt sie das Programm in überschaubare Teile, deren Schnittstellen klar definiert sind. Dadurch lässt sich das Projekt besser übersehen und später auch warten. – Zweitens wird die Programmentwicklung entzerrt. Teams können das Hauptprogramm erstellen und compilieren, während unabhängig davon andere Programmierer die Datenstrukturen implementieren. – Drittens erhöht es die Flexibilität und Adaptionsfähigkeit eines Programms. Verschiedene Implementierungen der Schnittstelle können ausprobiert werden. Im Hintergrund steht die Idee, ein großes Programm aus Bauteilen zusammenstöpseln zu können, wobei die Schnittstellen die Verbindungssteine sind, die das ganze zusammenhalten und strukturieren.

Implemen tierungen

interface

Implemen tierung Abb. 2.30:

Anwendung

interface

Am Beispiel eines Kalenderprojektes wollen wir dies erläutern. Angenommen, wir wollen ein Terminplanungssystem programmieren. Ein solches wird aus vielen Klassen bestehen. Die meisten können separat von verschiedenen Entwicklergruppen programmiert werden. Eine davon könnte eine Datumsklasse sein, wie sie oben in dem interface Datum spezifiziert wurde. Nach Vorliegen dieser Schnittstelle kann das Hauptprogramm bereits alle im interface aufgeführten Methoden addDays, getWeekDay, heute, etc. benutzen und das Programm damit kompilieren. Zur selben Zeit kann ein anderes Team schon dieses interface implementieren.

Anwendung, Interface und Implementierungen

Während in Modula die Namen des Definitionsmoduls und des Implementierungsmoduls bis auf die Dateiendung übereinstimmen, ist gerade dies in Java nicht erlaubt. Stattdessen kündigt jede Klasse, die ein interface implementiert dies in der Kopfzeile an: class Termin implements Datum{ ... } Dies hat den Vorteil, dass eine Klasse mehrere Schnittstellen auf einmal implementieren kann class Termin implements Datum, Uhrzeit, Comparable{ ... } aber auch, dass man mehrere alternative Implementierungen für das gleiche Interface bereitstellen kann. Beispielsweise könnte man in einer Implementierung das Datum durch die Angabe dreier Zahlen, für tag, monat und jahr angeben class Termin implements Datum{ // Felder int tag;

2.9 Typen, Module, Klassen und Objekte

171

int monat; int jahr; // Methoden int getDay(){ return tag; } void addDays(int n){ ... } ... } oder alternativ durch Angabe der Millisekunden seit dem 1.1.1970. Darauf baut schließlich die Zeitberechnung in Unix auf. Dabei wird zwar die Methode addDays ziemlich einfach, doch getDay wird entsprechend schwieriger. class UnixTime implements Datum{ // Felder long millis; // Methoden int getDay(){ ... } void addDays(int d){ millis += d*24*60*60*1000; } ... } In unserem Kalenderprojekt kann nach Belieben auf beide Implementierungen zurückgegriffen werden. Beispielsweise könnte man eine Variable vom Typ Datum deklarieren: Datum deadline; man kann ihr aber erst einen konkreten Wert zuordnen, wenn eine der Implementierungen tatsächlich vorhanden ist: deadline = new Termin(); oder deadline = new UnixTime();

2.9.7

Objektorientiertes Programmieren

Während Daten im klassischen Programmieren noch „dumme“ Objekte waren, auf die man „intelligente“ Funktionen loslies, um sie zu verknüpfen oder sonstwie zu manipulieren, so ändert sich diese Sichtweise beim objektorientierten Programmieren. Datenobjekte werden nicht mehr nur als Futter oder Ergebnis von Funktionen betrachtet, sondern Daten werden die aktiven Elemente, die sich gewisser Methoden bedienen, um mit anderen Objekten zu kommunizieren. Objekte sollen schließlich Dinge der realen Welt modellieren, wie Personen, Autos, Bankkonten oder Fernsehgeräte. Bereits beim modularen Programmieren ist aus dem Beispiel des Datums, das ursprünglich nur ein Tripel aus drei Zahlen war class Datum{ int tag, monat, jahr; }

172

2 Grundlagen der Programmierung

ein Objekt geworden, das „weiß“, wie man seinen Wochentag bestimmt, Tage addiert, etc. Man erhält also einen neuen allgemeineren Datenbegriff, der sich im Begriff des Objektes niederschlägt, kurz: Objekt = Daten + Methoden. Jedes Objekt trägt einen Köcher von Methoden mit sich herum. Diese Methoden werden genauso aufgerufen, wie man auch auf Felder zugreift: deadline.addDays(-14); baby.geburtsDatum.getWeekDay(); Das Objekt (deadline, bzw baby.geburtsDatum) ruft die ihm zugeordnete Funktion auf. Dies kann man als Verallgemeinerung eines Feldzugriffs ansehen, das Ergebnis hängt aber möglicherweise von einem weiteren Parameter ab. Im Inneren der Methode ist das Objekt selber bekannt, die Methoden sind schließlich die „Intelligenz“ des Objektes und kennen selbstverständlich ihren Wirt. Wenn sie ihn benennen wollen, können sie das mit dem Schlüsselwort this tun. Betrachten wir als Beispiel noch einmal die Klasse Bruch und die Operation add. Sie ist noch auf die alte Weise programmiert, weil sie zwei Objekte entgegennimmt und einfach addiert. Man muss sie über den Klassennamen als Bruch.add(p,q) aufrufen. Objektorientiert gesehen, sollte sie als Methode programmiert werden, die jedem Bruch zugeordnet ist. Ein Bruch p addiert einen zweiten Bruch zu sich: p.add(q) Obwohl dies hier etwas ungewohnt klingen mag, entspricht es der Vorgehensweise der objektorientierten Programmierung. In dieser Hinsicht ist das folgende Beispiel vielleicht überzeugender, in dem wir eine Klasse Konto zur Modellierung von Bankkonten erstellen wollen. Was sind die Daten und was sind die Fähigkeiten eines Bankkontos? Ein Konto benötigt sicher eine Nummer, einen Besitzer, und einen Saldo. Man muss einen Betrag einzahlen oder auf andere Konten überweisen können. Spezielle Konten, wie z.B. Sparkonten können sich sogar Zinsen gutschreiben, aber dazu kommen wir später. Eine direkte Modellierung von Konten könnte so verlaufen: public class Konto{ int nummer; int saldo; String inhaber; Konto(int nr, String name){ nummer=nr; saldo=0; inhaber=name; } void einzahlen(int betrag){ saldo += betrag; }

2.9 Typen, Module, Klassen und Objekte

173

void überweisen(Konto empfänger, int betrag){ if(betrag>0){ einzahlen(-betrag); empfänger.einzahlen(betrag); } }} Statt direkt die Salden der Konten zu manipulieren, haben wir uns bei der Methode überweisen entschieden, die Methode einzahlen mit dem negativen Betrag für das aktuelle Konto und danach positiv für das Empfänger-Konto aufzurufen. Mit dem Konstruktor könnten wir jetzt zwei Konten erzeugen, Geld einzahlen und etwas überweisen. In dem BlueJ-System könnten wir diesen Code direkt auf dem Codepad testen: Konto a = new Konto(1,"Anna"); Konto b = new Konto(2,"Bert"); a.einzahlen(300); a.überweisen(b,100);

2.9.8

Vererbung

Objekte sollen Dinge der täglichen Welt modellieren. Sie sind nicht nur passive Daten, sondern sie interagieren mit anderen Objekten. Diesem Zweck dienen die Objektmethoden. Eine Klasse dient einerseits als Schablone für die Erzeugung von Objekten, sie kann aber auch als die Menge aller derjenigen Objekte aufgefasst werden, die mit den Konstruktoren der Klasse erzeugt werden können. Modelliert man die reale Welt, so findet man immer wieder, dass Dinge sich hierarchisch klassifizieren lassen. Bekannt sind die Klassifikationshierarchien der Zoologie, solche Hierarchien treten einem auch in Bereichen entgegen, die schon lange informatisch beschrieben und behandelt werden. Wir haben die Klasse Person kurz beschrieben. Studenten sind auch Personen, aber neben der Tatsache, dass sie einen Namen und ein Geburtsdatum haben, besitzen sie zusätzlich noch eine Matrikelnummer und ein Studienfach. Informatikstudenten sind spezielle Studenten, sie haben zusätzlich eine Kennung im Fachbereichsnetz. Jede Fähigkeit, die eine Person hat, jedes Feld und jede Methode, muss auch für einen Studenten existieren, aber zusätzlich hat er eine Matrikelnummer. Jeder Informatikstudent muss alles können, was ein Student kann, nur kann er sich zusätzlich im Fachbereichsnetz anmelden. Analoges könnten wir auch von Bankkonten sagen. Sparkonten sind spezielle Konten. Würden wir eine Klasse Sparkonten entwerfen, so möchten wir, dass die Felder und Methoden allgemeiner Konten automatisch vorhanden sind, dass sie also vererbt werden, dass aber zusätzlich noch Felder, wie z.B. float zinssatz oder Methoden, wie z.B. void zinsGutschrift() vorhanden sein sollen. Dies kann man in der Tat erreichen. In Java deklariert man die Unterklasse durch das Schlüsselwort extends, weil die Vererbung von Feldern und Methoden einer Erweiterung der Liste der vorhandenen Felder und Methoden gleichkommt.

174

2 Grundlagen der Programmierung

Wir deklarieren die Klasse Student als Erweiterung (gebräuchlicher ist der Begriff Unterklasse) der vorhandenen Klasse Person. Wir entscheiden uns für je ein weiteres Feld für die Matrikelnummer und das Girokonto, weil demnächst für die Rückmeldung Studiengebühren fällig sein werden. Die Felder studienGebühr und uniKonto für das Konto der Hochschule sind für alle Studenten gleich. Daher brauchen sie auch nur einmal in der Klasse gespeichert werden. Dieses erreicht man in Java durch das Schlüsselwort static. Man nennt diese Felder auch Klassenfelder. Analog gibt es Klassenmethoden. Wir haben sie bereits früher verwendet, etwa in der ersten Version der Klasse Bruch oder bei den ersten Tests von Java-Funktionen als wir noch nichts von Objekten und Vererbung wussten. class Student extends Person{ static studienGebühr=500; static Konto uniKonto; Konto giro; int matrikelNummer; void rückmelden(){ giro.überweisen(uniKonto,studienGebühr); } } class Informatikstudent extends Student{ String kennung; } Jeder Informatikstudent ist ein Student und jeder Student ist eine Person, also ist jeder Informatikstudent eine Person. Insbesondere kann er sich rückmelden und hat einen geburtstag. Einer Variablen vom Typ Person kann man auch einen Studenten zuweisen, aber nicht umgekehrt. Das kennen wir analog schon von den Integer-Zahlen und den Real-Zahlen: Einer floatVariablen kann man einen int-Wert zuordenen, es geschieht eine automatische Konversion. Genauso kann ein Student automatisch als eine Person aufgefasst werden, man braucht bloß die Existenz einiger Felder und Methoden zu vergessen. Allerdings gehen die Felder nicht wirklich verloren. Durch einen cast kann man das in der Personenvariablen gespeicherte Studentenobjekt wieder zurückgewinnen. Das folgende Experiment kann man leicht im BlueJCodepad nachvollziehen: Person p = new Person(); Person q = p; Student s = new Student(); p = s; s = p; s = (Student) p; s = (Student) q;

// erlaubt // nicht erlaubt // O.K. // nicht erlaubt

2.9 Typen, Module, Klassen und Objekte

175

Die folgende Abbildung zeigt eine Interaktion mit BlueJ. Die Klassen werden in BlueJ durch Dateisymbole visualisiert, Vererbung durch Pfeile zwischen den Klassen. Gestrichelte Pfeile zeigen an, dass eine Klasse ein interface implementiert, bzw. eine andere Klasse benutzt. Im Kontextmenü der Klassen erscheinen die statischen Methoden. Die Klasse Student hat offenbar eine statische, also eine Klassenmethode, getUniKonto(), um das Konto für die Überweisung der Studiengebühr herauszufinden. Die Methode wurde statisch erklärt, weil sie bei jedem Studenten exakt das gleiche Ergebnis liefert, und somit nur einmal in der Klasse existieren soll. Jeder Konstruktor ist natürlich auch eine statische Methode. Wählen wir einen aus, wird ein Objekt erzeugt, das links unten in dem Objektkasten (object bench) erscheint. Dort wurden bereits mehrere Objekte erzeugt, unter anderem auch die Person otto und der Student hacker. Sein Kontextmenü offeriert nicht nur seine einzige Objektmethode, getKennung(), sondern auch die Objektmethoden seiner Oberklassen Student, Person und Object. Letztere ist die „Mutter aller Klassen“, die oberste in der Klassenhierarchie, deren Methoden letzlich jedes Objekt „erbt“, also aufrufen kann. Jedes Objekt ist also auch ein Element der Klasse Object. In der Abbildung haben wir uns aber offenbar entschlossen, die Methode getName() der Oberklasse Student auszuwählen.

Abb. 2.31:

2.9.9

Experimente mit Klassen, Objekten und Java-Code im BlueJ-System.

Summentypen in objektorientierten Sprachen

Klassen in objektorientierten Sprachen ersetzen offensichtlich die in klassischen Sprachen vorhandenen Produkttypen. Selbstverständlich gehen sie in vielerlei Hinsicht weit über jene hinaus, weil neben den Feldern auch Methoden gespeichert werden und weil mit der Verer-

176

2 Grundlagen der Programmierung

bung eine wichtige Modellierungshilfe hinzugewonnen wurde. Wie aber modelliert man in objektorientierten Sprachen Summentypen, so wie in dem vorangegangenen Beispiel Wissenschaftler, welche entweder Studenten oder Mitarbeiter oder Professoren sein konnten ? Angenommen, wir hätten bereits je eine Klasse Professor, Mitarbeiter und Student implementiert, so könnten wir diese zu einer sogenannten abstrakten Klasse zusammenfassen, die wir Wissenschaftler nennen können: abstract class Wissenschaftler{ abstract boolean istProf(); boolean istMitarbeiter(){ return !istProf() && !istStudent(); } abstract boolean istStudent(); } Die booleschen Methoden benötigt man, um festzustellen, ob jemand zur Unterklasse der Professoren, der Mitarbeiter oder der Studenten gehört. Sowohl istProf() als auch istStudent() sind nicht implementiert, es handelt sich um sogenannte abstrakte Methoden, die erst in den konkreten Unterklassen implementiert werden müssen. Die Methode istMitarbeiter() ist bereits voll definiert und wird so automatisch an alle Unterklassen vererbt. Die Klasse Professor, z.B., beginnt jetzt folgendermaßen: class Professor extends Wissenschaftler{ boolean istStudent(){return false;} boolean istProf(){return true;} ... } Die abstrakte Klasse Wissenschaftler repräsentiert also in der Tat die disjunkte Vereinigung der (konkreten) Klassen Professor, Mitarbeiter und Student. Damit kein Objekt in sie hineinschlüpfen kann, das zu keiner dieser drei Unterklassen gehört, darf sie keine Konstruktoren haben. Jedes Objekt der Klasse Wissenschaftler muss in einer der Unterklassen erzeugt worden sein, so dass es sich tatsächlich um eine disjunkte Vereinigung handelt. Induktiv definierte Klassen sind jetzt kein Problem mehr. Wir erinnern an die Definition: Eine Liste ist

entweder leer oder hat ein erstes Element und eine Restliste .

Eine Liste kann man also als abstrakte Klasse Liste definieren, wobei wir auf eine Methode istNichtLeer() verzichten können, denn istNichtLeer() == !istLeer(): abstract class Liste{ abstract istLeer(); } Für die konkreten Unterklassen erhalten wir:

2.9 Typen, Module, Klassen und Objekte

177

class LeereListe extends Liste{ boolean istLeer(){ return true; } } Im Falle der leeren Liste fällt auf, dass kein Feld vorhanden ist. Zwei Objekte der Klasse LeereListe können sich daher gar nicht unterscheiden. Dies ist auch beabsichtigt, denn das einzige Objekt soll eben die leere Liste repräsentieren, und sonst nichts. class NichtleereListe extends Liste{ Element e; Liste rest; boolean istLeer(){ return false; } } Jede NichtleereListe hat ein Objekt e einer (hypothetischen) Klasse Element und einen rest, der aus der Klasse Liste sein muss. Vergleicht man diese Implementierung mit der in Pascal, so fällt auf, dass es objektorientiert sehr viel aufwendiger geworden ist. Auf die objektorientierte Implementierung von Dateibäumen, die begrifflich keine weiteren Schwierigkeiten macht, wollen wir hier verzichten.

2.9.10

Datenkapselung

Ein wichtiger Aspekt der objektorientierten Programmierung ist die sogenannte Datenkapselung. Daten und Methoden sollen gekapselt werden, möglichst sollte um den Kern eines Objektes, das die Daten enthält, eine Schale von Methoden gelegt werden, über man kontrolliert auf die Daten zugreifen kann. Ein direkter Zugriff auf die Felder, der nicht die vorgesehenen Zugriffsfunktionen benutzt, sollte möglichst unterbunden werden. Dies erweitert das Prinzip des Information Hiding, einer fundamentalen Forderung im Programmentwurf. Warum aber sollte man in einer freien Welt Informationen verbergen? Denken Sie z.B. an einen Computer, einen Fernseher, oder irgendein kompliziertes Gerät, das aus vielen Bestandteilen zusammengebaut ist. Obwohl im Inneren viele Drähte, Widerstände, Motoren, Lüfter stecken, sind alle diese Teile nach außen durch ein Gehäuse verborgen. Der Zugang zu den Diensten eines Rechners geschieht nur über wohldefinierte Schnittstellen – Tastatur und Monitor, Buchsen, Laufwerke, Ein/Ausschaltknopf. Würde man das Gehäuse entfernen und alle Möglichkeiten, die in einem so komplizierten System stecken freigeben, könnte jemand auf die Idee kommen, an gewissen Kabeln Strom für die Kaffeemaschine zu zapfen oder ein Handtuch am CPU-Lüfter zu trocknen und den Rechner durch Abziehen einiger Kabel ein- und auszuschalten. Aus ähnlichen Gründen will man auch Softwareobjekte mit einem Gehäuse versehen, man sagt „kapseln“. So würde man z.B. die Felder der Konto-Objekte verstecken wollen und lesenden bzw. schreibenden Zugriff nur über spezielle Methoden gestatten. Auf diese Weise kommt kein Benutzer in die Versuchung, etwa das Feld saldo auf einen beliebigen Wert zu setzen oder bei einem negativen Saldo das Feld inhaber auf „St. Nikolaus“ zu setzen.

178

2 Grundlagen der Programmierung

Meth1 Meth3

Meth2 Daten

...

... Methn

Abb. 2.32:

Datenkapselung

Die Methoden, die Java als Programmiersprache für diese Zwecke zur Verfügung stellt, sind recht primitiv. Man kann jedem Feld und jeder Methode ein Attribut voranstellen, das seine Berechtigung festlegt. Für die Berechtigungen sind die Stufen private, protected, package, public vorgesehen, die wir im folgenden Kapitel genauer erklären werden. Die Inflation von sogenannten Attributen zusammen mit der Tatsache, dass diese für jedes Feld wiederholt werden müssen und auch noch vor dem eigentlichen Feld- und Methodennamen stehen, macht JavaProgramme nicht gerade übersichtlich. Im Falle der Klasse Konto würde man wohl alle Felder verstecken. Um ihre Inhalte aber dennoch lesen (aber nicht ändern) zu können, müsste man gesonderte Funktionen zur Verfügung stellen. Für die Namen solcher Funktionen haben sich gewisse Konventionen herausgebildet. So beginnt man Lesemethoden meist mit dem Präfix get und Schreibmethoden mit dem Präfix set, also: public class Konto{ private int nummer; private int saldo; private String inhaber; public int getNummer(){ return nummer; } public int getSaldo(){ return saldo; } public String getInhaber(){ return inhaber; } public void einzahlen(int betrag){ saldo += betrag; } ... etc. ... } Mit dieser Spezifikation ist eine Klasse entstanden, die man sich wie in der folgenden Figur symbolisieren kann:

2.10 Verifikation

179

Klasse Konto getSaldo überweisen einzahlen

private Felder und Methoden

getNummer getInhaber Abb. 2.33:

2.10

Kapselung der Klasse Konto

Verifikation

Jeder Programmierer macht Fehler. Viele dieser Fehler sind einfach zu erkennen, weil es sich um syntaktische Fehler handelt. Moderne Programmierumgebungen lokalisieren den Ort eines syntaktischen Fehlers, der entsprechende Programmtext erscheint in einem Editor, eine Erklärung des Fehlers wird angezeigt, und die Schreibmarke befindet sich dort, wo die Verbesserung des Fehlers erwartet wird. Syntaktische Fehler sind also leicht zu finden und zu verbessern. Ein Programm ohne syntaktische Fehler ist aber dennoch nicht fehlerfrei. Selbst wenn das Programm einwandfrei compiliert, können zur Laufzeit Fehler auftreten, die aus Bereichsüberschreitungen entstehen: Das Resultat einer arithmetischen Operation kann den zulässigen Bereich über- oder unterschreiten, Zeiger können auf undefinierte Daten zeigen, oder Operationen können Argumente erhalten, für die sie nicht definiert sind (beispielsweise Division durch Null). Diese Fehler sind normalerweise schwerer zu finden, sie treten erst zur Ausführungszeit auf und können, je nach Eingabedaten, auftreten oder unterbleiben. Immerhin haben sie die Eigenschaft, dass sie im Fall ihres Auftretens vom Rechner angezeigt werden. Programmierer versuchen, sich gegen solche Fehler zu wappnen, indem sie ihre Programme mit verschiedenen Eingabedaten testen. Insbesondere wird versucht, mit extremen Kombinationen von Eingabedaten einen Fehler, falls vorhanden, in der Testphase sichtbar zu machen. Die bis jetzt angesprochenen Fehler machen sich noch eindeutig bemerkbar. Kritischer wird es, wenn ein Programm unbeabsichtigt in eine Endlosschleife gerät, wenn es also nicht terminiert. Am Verhalten des Programms ist dies oft nicht eindeutig zu erkennen, nicht einmal durch Inspektion des Programmcodes – man denke etwa an das sehr kurze Programm Ulam, von dem man trotzdem nicht weiß, ob es für alle Eingabewerte terminiert: void ulam(int n){ while (n>1) if (n%2==1) n=3*n+1; else n=n/2;

}

180

2 Grundlagen der Programmierung

Zu guter Letzt kommen wir zu einer Sorte von Fehlern, die nicht allein den Programmcode betreffen, sondern die Vorstellung, die der Programmierer mit dem Programm verbindet. Das Programm wird compiliert und läuft ohne Beanstandung, es tritt kein Laufzeitfehler auf und es terminiert für alle Eingabewerte. Dennoch erledigt es nicht die Aufgabe, die es lösen sollte. Ist die Anforderung, die Spezifikation, eindeutig beschrieben, so kann man mit formal mathematischen Methoden eindeutig klären, ob das Programm die Spezifikation erfüllt oder nicht. Mit solchen Methoden wollen wir uns in diesem Abschnitt beschäftigen, doch wollen wir zunächst einige der praktischen Methoden der Fehlervermeidung ansprechen.

2.10.1

Vermeidung von Fehlern

Fehler verhindert man dadurch, dass man keine Fehler macht. Zu diesem Zweck sollte man zuallererst gute und vernünftige Werkzeuge benutzen. Dazu gehören eine dem Problem angemessene Sprache und gute, zuverlässige Compiler. Als Nächstes sollte man die Maxime „Erst überlegen, dann programmieren” beherzigen. Das Programm, die Module, die Datenstrukturen sollten vorher geplant sein. Wenn das Design nicht stimmt, kann das Programm später sehr kompliziert werden. Je komplizierter ein Programm ist, um so höher ist seine Fehleranfälligkeit. Früh begangene Fehler rächen sich später. Je später ein Fehler gefunden wird, desto mehr Kosten verursacht er. Als dritter Punkt ist ein sauberer, klarer Programmierstil wichtig. Der Versuchung, durch undurchsichtige Tricks noch ein Quentchen Effizienz hervorzukitzeln, sollte unbedingt widerstanden werden. Erst nachdem ein Programm korrekt läuft, ist es angebracht, mit Werkzeugen wie Profilern die Stellen herauszufinden, wo sich eine Effizienzsteigerung auswirken würde, und diese Stellen punktuell zu optimieren. Zu einem guten Programmierstil gehört natürlich auch, das Programm verständlich und übersichtlich zu halten. Unabdingbar sind eine gute Dokumentation, ein Zerlegen in überschaubare Teilaufgaben, die Vermeidung von globalen Variablen und Seiteneffekten. Auch dem gewissenhaftesten Programmierer unterläuft trotz aller Vorsichtsmaßnahmen gelegentlich ein Fehler. Um solche Fehler zu finden und ggf. zu vermeiden, bietet der Compiler gewisse Möglichkeiten: Durch Schalter (switches) lassen sich Prüfroutinen in den Code einbinden, die Bereichsüberschreitungen, Stack-Überlauf und falsche Eingabewerte abfangen. Erst wenn auf diese Weise das Vertrauen in die Software gestärkt wurde, stellt man die Schalter so, dass die Prüfroutinen weggelassen werden.

2.10.2

Zwischenbehauptungen

Auch ohne Hilfsmittel kann der Programmierer das interne Verhalten des Programms diagnostizieren. Durch print-Befehle können Inhalte von Variablen, Parametern und Zwischenwerten ausgegeben werden. Zwischenbehauptungen, d.h. Annahmen über die Beziehung von Variablen untereinander, können getestet werden, wenn sie als boolesche Ausdrücke formuliert vorliegen. Ist eine solche Zwischenbehauptung nicht erfüllt, so wird ein Fehler gemeldet. Als Testroutine könnte die folgende Prozedur dienen:

2.10 Verifikation

181

void assert(boolean behauptung,String message){ if ( ! behauptung) System.out.println(message); } Im Programm kann man nun an beliebiger Stelle Zwischenbehauptungen anbringen: assert((x>0)&&(sum==x*(x+1)/2),"Summe stimmt nicht"); Wird während des Programmablaufes eine solche Zwischenbehauptung verletzt, so erfolgt eine entsprechende Fehlermeldung. Java besitzt seit Version 1.4 sogar eine assert-Anweisung, die das obige leistet. Der obige Aufruf entspräche der Java-Anweisung assert (x>0) && (sum==x*(x+1)/2) : "Summe stimmt nicht"; Die assert-Anweisungen werden nur ausgeführt, wenn ein besonderer Parameter (-ea) beim Programmaufruf gesetzt ist. Daher muss die Überprüfung der Assertions nicht das ausgelieferte Programm belasten. Alle diese Methoden sind Testmethoden. Sie sind einfach durchzuführen, führen schnell zum Ziel und sind auch bei beliebig komplexen Programmen anwendbar. Leider sind Testmethoden unzuverlässig, weil man nie alle möglichen Eingabedaten testen kann. Ein Zitat von E. Dijkstra bringt dies auf den Punkt: Durch Testen kann man die Anwesenheit, nie aber die Abwesenheit von Fehlern zeigen. Dem wollen wir nun eine Methode entgegensetzen, mit der die Korrektheit von Programmen formal bewiesen werden kann. Die Methode ist nicht schwierig, aber dennoch aufwändiger als das Testen. Daher lohnt sich eine formale Verifikation nur bei Anwendungen, deren absolute Zuverlässigkeit gewährleistet sein muss. In Analogie zur Mathematik kann man feststellen, dass sich das Testen von Programmen zur Verifikation von Programmen verhält wie das Ausprobieren von mathematischen Sätzen zu deren Beweis.

2.10.3

Partielle Korrektheit

Eine formale Beschreibung der Anforderungen an ein Programm heißt Spezifikation. Wir gehen hier von Programmen aus, die in einem wohldefinierten Zustand gestartet werden sollen, um nach einiger Zeit zu terminieren. Solche Anforderungen kann man durch Paare P und Q beschreiben. P ist eine Eigenschaft, die vor dem Start des Programms erfüllt ist, die so genannte Vorbedingung, und Q ist eine Eigenschaft, die nach Beendigung des Programms erfüllt sein soll, sie heißt Nachbedingung. Das Paar P, Q, geschrieben als { P } { Q } , heißt Spezifikation. Ist S ein Programm, so schreiben wir {P} S {Q} falls gilt: Falls zu Beginn P gilt, so gilt nachdem S terminiert, Q .

182

2 Grundlagen der Programmierung

Wir sagen in diesem Fall, dass S die Spezifikation { P } { Q } erfüllt. Es ist wichtig festzuhalten, dass { P } S { Q } auf jeden Fall richtig ist, falls S nicht terminiert. Es gilt sogar: { P } S { false } ⇔ Wenn P beim Start von S gilt, terminiert S nicht. Da Q nur im Falle der Terminierung von S garantiert werden kann, heißt { P } S { Q } auch partielle Korrektheitsaussage (engl. PCA = partial correctness assertion). Methoden, welche die Terminierung von S formal zeigen, wollen wir später betrachten. Zunächst beschäftigen wir uns mit einem konkreten Beispiel einer partiellen Korrektheitsaussage. Der Einfachheit halber betrachten wir nur Programme, deren Variablen alle als Integer erklärt sind. Zusätzlich vereinbaren wir, dass Konstantennamen mit Großbuchstaben beginnen sollen, Variablennamen mit Kleinbuchstaben. Mit diesen Konventionen ist es legitim, nur den Anweisungsteil eines Programmes P aufzuführen. { N > 0 } { sum = 0; i = 0 ; while (i < N) { i = i+1 ; sum = sum + i } } { sum == 1 + 2 + ... + N } Die Behauptung lautet also: Wenn das Programm in einer Situation gestartet wird, in der die Konstante N > 0 ist, dann wird nach der Terminierung die Variable sum den Wert 0 + 1 + 2 + 3 + ... N enthalten.

2.10.4

Zerlegung durch Zwischenbehauptungen

Um diese komplexe Aussage zu beweisen, zerlegen wir sie in einfachere Aussagen, indem wir an geeigneter Stelle eine Zwischenbehauptung formulieren. Wir schieben diese zunächst als Kommentar nach den ersten beiden Anweisungen, also vor dem while in die komplexe PCA ein : { N>0 & sum==0 & i==0} Damit haben wir die Aufgabe in zwei einfachere Teilaufgaben zerlegt. Die gewählte Zwischenbehauptung wird zur Nachbedingung der ersten und zur Vorbedingung der zweiten PCA. Wenn die beiden kleineren PCAs richtig sind, muss auch die ursprüngliche Behauptung stimmen.

2.10 Verifikation

183

&

{{

}}

{{

{N>0 {N>0 && sum==0 sum==0 && i==0 i==0 }}

&

{{ NN >> 00 }} sum sum == 0; 0; ii == 00 ;; {{ N>0 N>0 && sum==0 sum==0 && i==0} i==0} while while (i (i > 00 }} sum sum == 0; 0; ii == 00 ;;

{N>0 {N>0 && sum==0 sum==0 && i==0} i==0} while( while( i i 0 & sum == 0 } ergeben sich zwei neue PCAs, die offensichtlich nicht mehr weiter zerlegbar sind.

}}

{N>0 {N>0 && sum sum == == 00 && ii == == 0} 0}

Abb. 2.35:

&

&

{{ NN >> 00 }} sum sum == 0; 0; {{ NN >> 00 && sum sum == == 00 ii == 00 ;;

{{ NN >> 00 }} sum sum == 0; 0; {{ NN >> 00 && sum sum == == 00 }} {{ NN >> 00 && sum sum == == 00 }} ii == 00 ;; {{ N>0 N>0 && sum sum == == 00 && i==0 i==0 }}

Weitere Zerlegung des ersten Teils

Bereits zum zweiten Mal haben wir die folgende offensichtliche Regel angewendet: { P } S1 { R }, { R } S2 { Q } { P } S1 S2 { Q }

Hintereinanderausführungsregel

Diese Regel beschreibt, wie eine PCA zu beweisen ist, deren Programmteil eine Hintereinanderausführung von zwei einfacheren Programmen darstellt: Um eine PCA der Form {P} S1 S2 {R} zu beweisen, finde eine geeignete Zwischenbehauptung R und zeige sowohl {P} S1 {R} als auch {R} S2 {Q}. Die Wahl der geeigneten Zwischenbehauptung ist selbstverständlich kritisch: Ist R zu stark, behaupten wir also zuviel, so ist {P} S1 {R} möglicherweise nicht zu beweisen. Ist R zu

184

2 Grundlagen der Programmierung

schwach, behaupten wir also zu wenig, so werden wir es nicht schaffen, {R} S2 {Q} zu zeigen. Wir werden aber später sehen, dass es eine einfache Methode gibt, geeignete Zwischenbehauptungen zu finden.

2.10.5

Zuweisungsregel

Die in unserem Beispiel bis jetzt entstandenen einfacheren Behauptungen, { N>0 } sum=0; { N>0 & sum == 0 } sowie { N>0 & sum == 0 } i=0; { N>0 & sum == 0 & i == 0 } erscheinen banal. Sie sind von der Gestalt {P} v = t; {Q}, also durch Zwischenbehauptungen nicht weiter zerlegbar. Um mit einem nicht ganz so trivialen Beispiel zu arbeiten, betrachten wir { yy-x }. Auch diese PCA ist von der Form {P} v = t; {Q}. Die Variablen in P beziehen sich auf den Zeitpunkt vor der Ausführung der Zuweisung v = t; und die Variablen in Q auf den Zeitpunkt danach. Eine Gleichsetzung der Variablen in P und Q kommt daher nicht in Frage. Daher bezeichnen wir zunächst die Variablen in Q durch neue Variablen, die wir aus den alten Variablen durch Anfügen eines Striches gewinnen: Aus x und y werden x' und y'. P bezieht sich nun auf x und y, die Variablen vor der Zuweisung, Q auf x' und y', die Variablen nach der Zuweisung. In der Zuweisung wird x' zu x + 1 gesetzt, so ergibt sich die mathematische Gleichung x' = x + 1. Alle anderen Variablen bleiben unverändert, also gilt insbesondere y' = y. Damit lässt sich unser Problem auf eine Implikation zurückführen: y < 2(x + 1) ∧ x' = x + 1

⇒ x' > y' – x'.

Da y nicht verändert wird, gilt y = y'. Ebenso lässt sich mit der Voraussetzung x' = x + 1 auch x' eliminieren und wir erhalten die folgende Bedingung, die offensichtlich für beliebige x und y wahr ist: y < 2(x + 1) ⇒ (x + 1) > y – (x + 1) . Die gerade gezeigte Methode funktioniert für beliebige PCAs der Form { P } v=t

; {Q} .

Zunächst wird die Variable v in Q durch v' ersetzt. Aus Q entsteht somit Q[v/v'] (d.h. Q mit v ersetzt durch v'). Die Zuweisung führt zu v' = t, deshalb bleibt zu beweisen: ( P ∧ ( v' = t ) ) ⇒ Q [ v ⁄ v' ]. Aufgrund der Gleichung v' = t können wir v' durch t eliminieren. Also erhalten wir P ⇒ Q [ v ⁄ v' ] [ v' ⁄ t ] .

2.10 Verifikation

185

Die rechte Seite bedeutet: Ersetze in Q zunächst v durch v' und dann v' durch t. Das kann man auch einfacher haben, indem man sofort v durch t ersetzt. Daraus entsteht: {P} v = t

; {Q}

gdw.

P ⇒ Q [ v/t ] .

Als Regel können wir dies folgendermaßen formulieren: P ⇒ Q[v/t ] { P } v = t; { Q }

Zuweisungsregel

Beispielsweise gilt: {true} x=5; {x y–x} y=y–x; {x > y}, weil x > y ⇒ (x > y)[y/y – x], d.h. x > y – x ⇒ x > y – x.

2.10.6

Rückwärtsbeweis

Anhand eines kleinen Programms wollen wir die beiden bisherigen Regeln testen. Es geht um ein trickreiches Programm, mit dessen Hilfe der Inhalt zweier Integer-Variablen x und y vertauscht werden kann, ohne eine Hilfsvariable zu benutzen: x = x – y; y = x + y; x = y – x. Dies ist ein Beispiel für Trickprogrammierung, die wir ansonsten ablehnen. In diesem Fall geht es aber nur darum, ein schwer zu durchschauendes Programm formal zu analysieren. Zunächst stellt sich die Frage, wie wir die Aufgabe, den Inhalt von x und y zu vertauschen, beschreiben können. Mit beliebigen Konstanten A und B, die den festen, aber beliebigen anfänglichen Inhalt von x und y darstellen, können wir spezifizieren: Vorbedingung : Nachbedingung :

{ x == A & y == B} { x == B & y == A}.

Damit erhalten wir also die PCA: {x == A & y == B}; x = x – y; y = x + y; x = y – x; {x == B & y == A}.

186

2 Grundlagen der Programmierung

Das entstandene Programm hat die Form S1 S2 mit der Zerlegung S1 ≡ x=x–y; y=x+y;

und

S2 ≡ x=x–y;.

Wir müssen nun eine geeignete Zwischenbehauptung R finden, mit der wir zeigen können, dass Folgendes gilt: {x==A & y==B} S1 { R }

und

{ R } x=y–x; {x==B & y==A}.

Um die erste PCA erledigen zu können, wählen wir R so schwach wie möglich. Damit wir die Zuweisungsregel für die zweite PCA anwenden können, benötigen wir zumindest R ⇒ ( x==B & y==A )[x/y–x],

also

R ⇒ ( y–x==B & y==A).

Wählen wir nun R ≡ (y–x==B & y==A), so haben wir diese Implikation trivialerweise erfüllt, und trivialerweise auch die zweite PCA. Einsetzen von R in die erste PCA liefert: {x==A & y==B} x=x–y; y=x+y; {y–x==B & y==A}. Diese PCA ist erneut von der Form {P} S1 S2 {Q}, wir benötigen also nochmals eine Zwischenbehauptung R, so dass {x==A & y==B} x=x–y; {R} und {R} y=x+y; {y–x==B & y==A}. Erneut bestimmen wir R so schwach wie möglich, um die Zuweisungsregel für die zweite PCA anwenden zu können. Wir benötigen R ⇒ (y–x==B & y==A)[y/x + y], also R ⇒ (x+y)–x==B & x+y==A. Mit R ≡ (y==B & x+y==A) ist die zweite PCA erledigt, es bleibt noch zu zeigen: {x==A & y==B} x=x–y; {y==B & x+y==A}. Weil (x==A & y==B) ⇒ (y==B & x+y==A)[x/x – y], also (x==A & y==B) ⇒ (y==B & x–y+y==A) eine Trivialität ist, folgt also auch die letzte PCA aus der Zuweisungsregel. Festzuhalten bleibt als Strategie, die PCA von hinten nach vorne durchzuarbeiten und dabei immer zu der jeweils letzten Anweisung eine möglichst schwache Vorbedingung zu ermitteln, welche gerade noch die PCA wahr macht. Diese nennt man auch die schwächste Vorbedingung (engl. weakest precondition). Der Beweis eines alternativen (und klareren) Programms zur Vertauschung der Inhalte zweier Variablen ist in der folgenden Abbildung dargestellt.

2.10 Verifikation

& &

{{ x==A x==A && y==B y==B }} temp temp == xx ;; {y==B {y==B && temp==A} temp==A} xx == yy ;; {{ x==B x==B && temp==A temp==A }} yy == temp temp ;; {{ x==B x==B && y==A y==A }}

Abb. 2.36:

187 {x==A {x==A && y==B y==B }} temp temp == xx ;; {y==B {y==B && temp==A} temp==A}

x==A x==A && y==B y==B y==B y==B && x==A x==A

{y==B {y==B && temp==A} temp==A} xx == yy ;; {x==B {x==B && temp==A temp==A }}

y==B y==B && temp==A temp==A y==B y==B && temp==A temp==A

{x==B {x==B && temp==A temp==A }} yy == temp temp ;; {x==B {x==B && y==A y==A }}

x==B x==B && temp==A temp==A x==B x==B && temp==A temp==A

Beweis des Vertauschungsprogramms

Die verwendeten Zwischenbehauptungen, aufgrund derer die ursprüngliche PCA auf drei kleinere PCAs und diese schließlich auf rein logische Aussagen zurückgeführt werden, ergeben sich wie im vorigen Beispiel durch Bestimmung einer möglichst einfachen Vorbedingung und Analyse des Programms von hinten nach vorne.

2.10.7

if-else-Regel

Die bisherigen Beispielprogramme bestanden nur aus Zuweisungen und Hintereinanderausführungen. Jetzt betrachten wir Alternativanweisungen der Form if und if-else. Eine PCA der Form {P} if(B) S1 else S2 {Q} muss zwei Fälle einschließen. Wenn P wahr ist, gilt zusätzlich entweder P ∧ B oder P ∧ ¬B . Im ersten Fall wird S1 ausgeführt, im zweiten Fall S2. In jedem Fall muss hinterher Q garantiert sein. So erhält man die einfache Regel: { P ∧ B } S1 { Q } , { P ∧ ¬B } S2 { Q } { P } if( B ) S1 else S2 { Q }

Alternativregel

Als Beispiel diene ein kleines Programm, das einer Variablen z den Absolutwert von x zuordnet. Die Spezifikation ist: {x==A}{z==|A|}. Wenn x anfangs einen beliebigen Wert A hat, dann soll z zum Schluss den Absolutwert der gleichen Zahl A haben. Vorsicht: Die Spezifikation {x==A}{z==|x|}würde auch durch z=0; x=0; erfüllt! Mit der richtigen Spezifikation kann das korrekte Programm, und damit die PCA z.B. lauten: {x == A} if (x>0) z = x; else z = -x; {z == |A|} Nach der obigen Regel genügt es, die PCAs

188

2 Grundlagen der Programmierung { x==A & x>0 } z=x; { z==|A|}

sowie { x=A & !(x>0) } z=-x; { z==|A|} zu zeigen, was nun leicht gelingt.

2.10.8

Abschwächungsregel und einarmige Alternative

Um auch die einarmige Alternative if (B) S behandeln zu können, erinnern wir uns daran, dass diese durch if (B) S else{} definiert werden kann. Aus einer PCA der Form { P } if (B) S { Q } wird also { P } if (B) S else{} { Q }, und daraus { P ∧ B } S { Q } sowie { P ∧ ¬B } {} {Q}. Für die leere Anweisung {} finden wir die folgende offensichtliche Regel1 P⇒Q { P } {} { Q }

Skipregel - oder Abschwächungsregel

Kommen wir nun zur bedingten Anweisung zurück, so erhalten wir die Regel: { P ∧ B } S { Q }, P ∧¬B ⇒ Q { P } if(B) S { Q }

if-Regel

Als Beispiel wollen wir einer Variablen ihren Absolutwert zuordnen. { x==A } if(x y) x = x-y; else y = y-x; Kommen wir nun endlich auf das Gauß’sche Summationsbeispiel zurück: while(i0 fact==i! fact==i! && i

pruef(Prof,eva).

Backtracking und pattern matching sind die Arbeitspferde von Prolog. Aber Prolog ist nicht nur eine Datenbankanfragesprache, sondern eine sehr angenehme und einfache Programmiersprache. Als Beispiel stellen wir das Prolog-Programm zur Berechnung des ggT vor. Es fällt zunächst auf, dass Funktionen f(x1,...,xn) auch als Relationen geschrieben werden. Die ersten Komponenten repräsentieren die Argumente und die letzte Komponenten den Funktionswert. Die zweistellige Funktion ggT repräsentieren wir daher als dreistellige Funktion ggTRel(x,y,z) mit der Bedeutung: ggT ( x, y ) = z ⇔ ggTRel ( x, y, z )

200

2 Grundlagen der Programmierung

Die allgemeine rekursive Definition des ggT:  x, falls x = y  ggT ( x, y ) =  ggT ( x – y, y ) , falls ( x > y )   ggT ( x, y – x ) , falls ( x ≤ y )

übersetzt sich sofort in ein entsprechendes Prolog-Programm. Der Infix-Operator „is“ berechnet den rechts davon stehenden Ausdruck und bindet ihn an die links stehende Variable. ggTRel(X,X,X). ggTRel(X,Y,Z) :- X >= Y, U is X-Y, ggTRel(U,Y,Z). ggTRel(X,Y,Z) :- X < Y, U is Y-X, ggTRel(X,U,Z). Wir können das Programm ganz normal aufrufen, z.B. mit dem goal ?>

ggTRel(30,42,X).

und erhalten X=6. Interessant wird Prolog aber erst, wenn es um symbolische Daten geht, statt um Zahlen. Dazu kann es von Hause aus mit Listen umgehen. Die Notation [] steht für die leere Liste, [otto,3,eva,5] für eine Liste mit den 4 Atomen otto, 3, eva, 5. Ist L eine Liste und E ein Element, so konstruiert man mit [E|L] die Liste mit E als neuem erstem Element und Restliste L. Ist z.B. L=[3,5,7] und E=2, so ist [E|L] = [2,3,5,7]. Die Operatoren [] und [|] dienen nicht nur dazu, Listen zu konstruieren, sondern auch zum pattern matching. Versucht man z.B. das pattern [E|Es] mit der Liste [tic,tc,to] zu matchen, so liefert dies die Bindung E=tic und Es = [tc,to]. Ein einfaches Beispielprogramm app zeigt, wie man zwei Listen Xs und Ys zu einer Liste Zs zusammenfügen kann. Manche Leute schreiben L1+L2 für die Liste, die entsteht, wenn man L1 und L2 aneinanderhängt.1 Offensichtlich gilt [] + L = L und falls Xs+Ys = Zs ist, folgt [H | Xs] +Ys = [H | Zs]. Diese zwei Gleichungen sind auch schon das Programm, wenn man sie in der Prolog-Konvention hinschreibt: app([],Ys,Ys). app([H|Xs],Ys,[H|Zs]) :- app(Xs,Ys,Zs). Wir können das Programm auf verschiedene Weisen aufrufen: ?> app([2,3,5],[9,13,42],L). ?> app(U,[7,9],[2,3,5,7,9]]). ?> app(X,Y,[a,b,c]).

1. Java verwendet die gleiche Konvention für Strings, die ja nichts anderes als Listen von Zeichen sind.

2.11 Deklarative Sprachen

201

Das erste goal liefert L=[2,3,5,9,13,42], das zweite U=[2,3,5]. Offensichtlich kann das gleiche Programm also auch benutzt werden, um zwei Listen zu „subtrahieren“. Das dritte goal liefert alle Möglichkeiten, die Liste [a,b,c] aus zwei Listen zu kombinieren: X=[],Y=[a,b,c]; X=[a],Y=[b,c]; X=[a,b],Y=[c]; X=[a,b,c],Y=[]. Zum Abschluss zeigen wir, wie einfach und klar in Prolog Programme geraten. Als Beispiel programmieren wir ein schnelles Sortierprogramm, Quicksort. Quicksort sortiert eine Liste mit der Methode Teile und Herrsche: Es wählt ein beliebiges Element E aus der Liste und zerlegt den Rest der Liste in zwei Teile: Lo, die Elemente kleiner als E, und Hi, die Elemente ≥ E . Dann sortiert es rekursiv Lo und Hi zu LoSortiert und HiSortiert. Die müssen nur noch zum Resultat Res zusammengefügt werden: links LoSortiert, rechts HiSortiert und dazwischen E. Das Prolog-Programm setzt genau das in die Tat um: qSort([],[]). qSort([E|Es],Res)

:- split(E,Es,Lo,Hi), qSort(Lo,LoSortiert), qSort(Hi,HiSortiert), app(LoSorted,[E|HiSortiert],Res).

Die Hilfsfunktion split spaltet eine Liste anhand eines Elementes E in die kleineren und die größeren. Es liefert beide Listen in den beiden letzten Argumenten zurück. Wenn das erste Element E der Liste kleiner als das Vergleichselement X ist, wird es der Liste Lo zugeschlagen, sonst der Liste Hi. Wenn die aufzuteilende Liste leer ist, sind auch Lo und Hi leer: split(X,[],[],[]). split(X,[E|Es],[E|Lo],Hi) :split(X,[E|Es],Lo,[E|Hi]) :-

E < X, split(X,Es,Lo,Hi). E >= X, split(X,Es,Lo,Hi).

Eine Sprache, die so einfach ist, wie Prolog, erweckt natürlich Misstrauen. Sie sei zu ineffizient, nur für Spielbeispiele zu gebrauchen, fehleranfällig, weil nicht typisiert, etc. In Wirklichkeit stimmt keines dieser Argumente. Prolog ist weniger fehleranfällig, weil es so einfach und klar ist. In gängigen Sprachen muss man für die gleiche Aufgabe das Vielfache an Text schreiben und hat dadurch viel mehr Gelegenheit, Fehler zu machen. Darüber hinaus gibt es auch statisch getypte Varianten von Prolog (z.B.: Visual Prolog). Effizienz ist ein Kampfwort hinter dem sich heutzutage nur noch Bequemlichkeit, sich mit neuen Konzepten zu beschäftigen, verbergen lässt. In Prolog sind große und effiziente Systeme erstellt worden. Auch der in diesem Kapitel besprochene Programm-Verifizierer NPPV, samt Parser, Beweiser und Benutzeroberfläche mit Fenstern, Menüs, Editor, etc. ist komplett in Visual-Prolog (www.pdc.dk) geschrieben. Visual Prolog 6.3 ist eine Weiterentwicklung des vormaligen Turbo-Prolog, besitzt wie dieses einen Compiler und erzeugt Maschinencode, der in der Geschwindigkeit einem von C oder Java erzeugten Code nicht nachsteht. Der Name Visual deutet an, dass man mit moderner Entwicklungsumgebung (IDE) inklusive GUI-Editor und Debugger auch Programme für Windows erstellen kann.

202

Abb. 2.43:

2 Grundlagen der Programmierung

Prolog Interpreter (tuProlog) in Aktion

Für das erste Experimentieren mit Prolog empfiehlt sich für Anfänger ein kostenfreies und interpretiertes Prolog. Sehr beliebt ist hier SWI-Prolog (www.swi-prolog.org), allerdings haben wir unsere Beispiele in dem kleinen experimentellen tuProlog-System erstellt und getestet. Dieses hat den Charme, dass man aus tuProlog auch Java aufrufen kann und aus Java tuProlog. So hat man das Beste aus zwei Welten, die vielen APIs aus Java und die Eleganz und Einfachheit von Prolog. Weil tuProlog die Virtuelle Maschine von Java (JVM) nutzt, ist tuProlog automatisch plattformunabhängig. Es ist von (alice.unibo.it/xwiki/bin/view/Tuprolog/) erhältlich und kommt als .jar-Datei, die man zum Starten nur anklicken muss.

2.11.2

Erlang

Erlang ist eine funktionale Sprache, deren Namen angeblich1 den Mathematiker Agner Krarup Erlang ehren soll. Erlang ist eine der wenigen funktionalen Programmiersprachen, die auch in der Industrie eingesetzt werden. Zu den Anwendern gehören insbesondere Telekommunikationsunternehmen (T-Mobile, Ericsson, Nortel, Cellpoint) aber auch Internet-Firmen, wie z.B, Amazon, Facebook, etc. Erlang ist sehr einfach zu verstehen. Es handelt sich um eine funktionale Sprache, was bedeutet, dass Funktionen als Argumente und Werte anderer Funktionen benutzt werden können. Klassische Beispiele sind die Funktio1. Manche meinen, Erlang stehe für „Ericsson Language“

2.11 Deklarative Sprachen

203

nen map und filter, die jeweils eine Liste L und eine Funktion F als Argumente nehmen. map wendet F auf jedes Listenelement an und liefert die Ergebnisliste: map(F,[]) map(F,[X|Es])

-> []; -> [F(X)|map(F,Es)].

Erlang verwendet die gleiche Syntax für Variablen und Atome wie Prolog und übernimmt auch dessen Methode des Pattern matching. Der erste Erlang-Interpreter war schließlich in Prolog implementiert. Jede Erlang-Funktion kann man auch von der Kommandozeile aufrufen, wobei man der Funktion (hier map) den Namen des Moduls, in dem sie definiert ist (hier: listfun) voranstellt. Als erstes Argument können wir uns eine beliebige Funktion ausdenken, eine sogenannte anonyme Funktion, manchmal auch Closure genannt: fun(X) -> X*X end. In diesem Beispiel handelt es sich offenbar um die Quadratfunktion. > Liste = [3,5,7,11]. [3,5,7,11] > listfun:map(fun(X) -> X*X end,Liste). [9,25,49,121] filter entfernt alle Elemente X, für die die Eigenschaft P(X) falsch ist. Hier muss P offensichtlich eine Funktion mit booleschem Ergebnis sein: filter(_,[]) -> []; filter(P,[E|Es]) -> case P(E) of true -> [E|filter(P,Es)]; false -> filter(P,Es) end. Wir testen die Funktion interaktiv, indem wir aus einer Liste alle ungeraden Zahlen entfernen (rem ist die modulo-Funktion): > Zahlen = [2,3,4,5,6]. [2,3,4,5,6] > listfun:filter(fun(X)-> X rem 2 == 0 end,Zahlen). [2,4,6] map und filter sind zentrale Funktionen, denn mit ihnen kann man beliebige Listen so beschreiben, wie man es von Mengen gewohnt ist: Ist L eine Liste von Zahlen, so ist die Liste [ x × x | x ∈ L, x rem 2=0 ]

gerade map(fun(x)->x*x end, filter(fun(x)->x rem 2==0 end,L)). Aus diesem Grunde unterstützt Erlang die folgende Notation, die intern in Aufrufe von map und filter umgesetzt wird: [ X*X || X []; qSort([E|Es]) ->

qSort([ X || X Pid = spawn(area_server, loop, [0]), register(flaechenServer, Pid). Erlang Prozesse können mit dem Operator „!“ beliebige Objekte als Nachrichten versenden. Im folgenden Beispiel sendet Client ! X*X das Quadrat von X an den Prozess Client. Mit dem Konstrukt receive ... end wartet ein Prozess auf eine Nachricht. Zwischen den Schlüsselwörtern gibt man die Muster der erwarteten Nachrichten vor, und wie jeweils darauf zu reagieren ist.

2.11 Deklarative Sprachen

205

Unsere Funktion loop bietet Flächenberechnungen an. Sie erwartet als Nachrichten Tupel der Form { ProzessId, Anfrage } wobei in ProzessId der Client seine Pid sendet, so dass die Antwort auf die Anfrage an ihn zurückgesendet werden kann. Für jedes Anfragemuster ist eine Klausel der Form Pattern -> Aktionen definiert. Kommt z.B. eine Anfrage, die auf das Muster {C, { quadrat, X}} passt, so wird X*X berechnet und an C gesandt; ist sie von der Form { C , { rechteck, X, Y}} so wird X*Y gesandt. Im Anschluss ruft sich loop immer wieder selber auf, mit der Summe der bereits berechneten Flächen als Argument. Diesen Wert stellt unser Server ebenfalls auf die Anfrage { C, {summe} } zur Verfügung. loop(Total) -> receive {Client, {quadrat, X}} -> Client ! X*X, loop(Total + X*X); {Client, {rechteck,X,Y}} -> Client ! X*Y, loop(Total + X*Y); {Client, {summe}} -> Client ! Total, loop(Total) end. Der Client kann als unabhängiges Programm auf irgendeinem entfernten Rechner eine Anfrage stellen, z.B. das Quadrat von 10 zu berechnen, und auf eine Antwort warten. Mit self erfährt er seine ProzessId, um diese mitzuschicken: Server ! {self(), {square, 10}}, receive Area -> Area end. Das receive des Clients nimmt hier alles was kommt und gibt dieses als Wert zurück. Wir schreiben ein kleines Testprogramm, mit dem wir den Server testen können. Nach dem Starten der Erlang-Shell compilieren wir in Zeilen 1> und 2> die einzelnen Programme mit der Systemfunktion c. Dann starten wir den flaechenServer. Der Aufruf einer Erlang-Funktion hat immer die Syntax modul:funktion(argumente). Nacheinander rufen wir Funktionen aus dem Modul bsp auf, die den Server testen. Genauso einfach wie Prozesse innerhalb eines Knotens kommunizieren können, funktioniert es auch mit Erlang-Systemen auf verschiedenen Knoten des Internets. Dafür wollen wir aber auf die Dokumentation von Erlang bei www.erlang.org verweisen, wo auch Erlang-Systeme für alle möglichen Plattformen erhältlich sind.

206

Abb. 2.44:

2 Grundlagen der Programmierung

Erlang Interaktion in Notepad++

Der Umstieg von imperativen Sprachen, die auf der schrittweisen Veränderung von Variableninhalten und der Kontrolle durch while- und for-Schleifen beruhen, wie z.B. Pascal, Java unad C, zu deklarative Sprachen, wie Prolog, ML, LISP, Haskell oder Erlang, die solche Konstrukte nicht besitzen, und in denen man alles durch Rekursion ausdrückt, erfordert ein Umdenken, das anfangs schwer fällt. Hat man den Gipfel aber erklommen, will man nicht mehr zurück, weil erst von oben sichtbar ist, wie einfach und elegant man Probleme formulieren und lösen kann.

2.12

Zusammenfassung

In diesem Kapitel haben wir die Grundlagen des imperativen Programmierens kennen gelernt. Dabei haben wir die wichtigsten programmiersprachlichen Konzepte und ihre mathematischen Hintergründe beleuchtet. Zum Schluss haben wir einen kurzen Ausblick in das logische und funktionale Programmieren gegeben. Die neuen Konzepte des objektorientierten Programmierens konnten wir bisher nur schlaglichtartig behandeln. Im nächsten Kapitel werden wir mit Java eine aktuelle imperative Programmiersprache genauer kennen lernen und dabei auch tiefer in das objektorientierte Programmieren einsteigen.

3

Die Programmiersprache Java

Im letzten Kapitel haben wir die theoretischen Grundlagen der Programmierung diskutiert. Jetzt werden wir mit „Java“ eine konkrete Programmiersprache kennen lernen. Die Sprache Java wurde seit 1991 von einem kleinen Team unter Leitung von James Gosling bei SUN unter dem Arbeitstitel OAK (Object Application Kernel) entwickelt. Ursprünglich wollte man eine Programmiersprache entwerfen, die sich in besonderer Weise zur Programmierung von elektronischen Geräten der Konsumgüterindustrie eignen sollte – also von Toastern, Kaffeemaschinen, Videogeräten, Decodern für Fernsehgeräte etc. 1993 wurde die Zielrichtung des Projektes geändert: Eine Programmiersprache zu entwickeln, die sich in besonderer Weise zur Programmierung auf verschiedensten Rechnertypen im Internet eignen sollte. Als neuer Name wurde Java gewählt. Java, die Hauptinsel Indonesiens, ist im Amerikanischen ein Synonym für guten Bohnenkaffee. Der Name hat also keinen direkten Zusammenhang mit den neuen Zielen des Projektes. Seit 1995 bietet SUN kostenlos den Kern eines Programmiersystems JDK (Java Development Kit) zusammen mit einer Implementierung des Java-Interpreters (Java Virtual Machine) an. Seitdem hat diese Sprache sich schneller verbreitet als jede andere neue Programmiersprache der letzten Jahre. Einige Ursachen für dieses Phänomen sind: •



• •

Java Programme sind portabel, sie können also ohne jede Änderung auf unterschiedlichen Rechnern eingesetzt werden. Dies ist eine Voraussetzung für die Integration von JavaAnwendungen, so genannten Applets, in Internet-Seiten. Für diesen Zweck besitzt Java spezielle Sicherheitsmechanismen. Java ist ein modernisiertes C++. Diese Sprache hatte sich in den letzten Jahren zum Industriestandard entwickelt, daher gibt es viele Programmierer, die ohne großen Aufwand auf Java umsteigen können. Java ist weniger komplex als C++, verbietet den unkontrollierten Umgang mit Zeigern und verkörpert moderne Konzepte der objektorientierten Programmierung. Java hat sich an Universitäten verbreitet, weil in dieser Sprache viele der Konzepte enthalten sind, die Sprachen wie Pascal, Modula und Oberon für die Lehre so populär gemacht haben. Anders als die vorgenannten Sprachen konnte sich Java aber auch in der Praxis durchsetzen. Java Entwicklungsumgebungen von hoher Qualität sind zum Teil kostenlos verfügbar. Hinter Java steht die Firma SUN Microsystems, ein bedeutender Hersteller von Servern und Workstations. Das Java Development Kit (JDK), ein Java-Interpreter (Java Virtual Machine), Werkzeuge und Dokumentationen zu Java werden von SUN entwickelt und im

208

3 Die Programmiersprache Java

Internet bereitgestellt. Derzeit (Mitte 2008) ist die Version 1.6 des JDK aktuell. Das JDK 1.7, das bereits in einer Betaversion vorliegt, wird weitere neue Konzepte (u.a. Closures) einführen, auf die wir am Ende des Kapitels näherer eingehen. Die Änderungen, die sich mit der Einführung der Version 1.2 des JDK ergeben haben, waren so umfangreich, dass SUN die Bezeichnung Java2-Plattform einführte. Java wird seit dieser Zeit in drei verschiedenen Ausgaben angeboten, einer Standard-Edition SE, einer Micro-Edition ME und einer Enterprise-Edition EE. In diesem Kontext wird die Standard Ausgabe der Version 1.6 des JDK häufig als Java SE 6 bezeichnet. Auf diese Ausgabe beziehen wir uns in diesem Buch. Derzeit sind kostenlos erhältliche Programmierumgebungen wie NetBeans und Eclipse populär – es handelt sich um sehr große und umfangreiche Systeme. Ein schlankes, aber nicht minder geeignetes System ist die kostenlose Variante von JCreator, in dessen Editor man den Rumpf nicht interessierender Methoden und Klassen ausblenden kann, so dass nur noch deren Signaturzeile zu sehen ist. Hartgesottene Programmierer schwören auf universelle Editoren wie Ultraedit oder Emacs. Zum Erlernen und Experimentieren mit Java ist besonders das BlueJ System hervorragend geeignet (siehe auch S. 84 und S. 227). In allen Fällen ist aber Vorraussetzung, dass das bei java.sun.com erhältliche Java-Development-Kit (JDK) installiert ist.

Abb. 3.1:

Java-Editor mit ausgeblendeten Methoden- und Klassenrümpfen

3.1 Die lexikalischen Elemente von Java

209

Den vielen Vorteilen von Java steht derzeit allerdings auch ein kleiner Nachteil gegenüber: Die Portabilität wird durch eine interpretative Programmausführung erreicht. Das bedeutet einen Verzicht auf optimale Programmlaufzeiten. Die Messergebnisse für die Laufzeit von Sortieralgorithmen im nächsten Kapitel belegen jedoch, dass dieser Nachteil auf modernen Prozessoren geringer ausfällt als vermutet.

3.1

Die lexikalischen Elemente von Java

Die meisten Programmiersprachen basieren auf dem weit verbreiteten ASCII-Zeichensatz. Landesspezifischen Zeichen, wie z.B. ö, ß, æ, ç oder Ã, sind dabei nicht zugelassen. Da alle ASCII-Zeichensätze 7 oder 8 Bit für die Darstellung eines Zeichens verwenden, ist die Anzahl der codierbaren Zeichen auf 256 beschränkt. Java legt den neueren Zeichensatz Unicode zugrunde, der praktisch alle weltweit geläufigen Zeichensätze vereint, siehe auch S. 13. Die einzelnen Zeichen von Unicode werden durch Attribute als Buchstaben oder Ziffern klassifiziert. Aufbauend auf dieser Klassifikation kann man folgende Java-spezifische lexikalische Elemente definieren: • • • • • • •

Buchstaben: Alle in Unicode zulässigen Buchstaben und aus „historischen“ Gründen auch der Unterstrich „ _ “ sowie das Dollarzeichen „$“. Die Ziffern von 0 bis 9. Zeilenende: Eines der Zeichen Wagenrücklauf (CR=carriage return: ASCII-Wert 13) oder Zeilenwechsel (LF=line feed: ASCII-Wert 10) oder deren Kombination CR LF. Leerzeichen (Whitespace): Das Leerzeichen selbst (SP=space: ASCII-Wert 32), eines der folgenden Steuerzeichen: Tabulator (HT=horizontal tabulator: ASCII-Wert 9), Formularvorschub (FF=form feed: ASCII-Wert 9) oder ein Zeilenende. Trennzeichen: ( ) { } [ ] ; , . Operatoren: = > < ! ~ ? : == = != && || ++ -- + - * / & | ^ % > >>> += -= *= /= &= |= ^= %= = >>>= Kommentare, Bezeichner und Literale.

3.1.1

Kommentare

Java kennt drei Arten von Kommentaren. Die erste Form beginnt mit // und erstreckt sich bis zum Ende der Zeile: // Dieser Kommentar endet automatisch am Zeilenende Die zweite Form des Kommentars kann sich über mehrere Zeilen erstrecken. Er besteht aus allen zwischen den Kommentarbegrenzern /* und */ stehenden Zeichen. Kommentare dieser Form dürfen nicht geschachtelt werden: /* Die folgende Methode berechnet eine schwierige Funktion auf die ich sehr stolz bin */

210

3 Die Programmiersprache Java

Eine Variante beginnt mit /** und endet mit */. Solche Kommentare werden von dem Zusatzprogramm javadoc erkannt, und in eine standardisierte Dokumentation übernommen. Durch zusätzliche Marken wie z.B. @param, @result sowie beliebige HTML-Formatierungsanweisungen kann der Benutzer die Strukturierung der Dokumentation beeinflussen. Die Dokumentation der Java-API (siehe Abb. 3.12) ist auf diese Weise automatisch erzeugt.

Abb. 3.2:

3.1.2

JavaDoc-Kommentar und Teil der erzeugten HTML-Dokumentation (in BlueJ)

Bezeichner

Bezeichner beginnen mit einem Java-Buchstaben. Darauf können weitere Java-Buchstaben, Ziffern und Unterstriche folgen. Die Länge eines Bezeichners ist nur durch die maximal verwendbare Zeilenlänge begrenzt. Beispiele sind: x y meinBezeichner Grüße Üzgür λ_halbe ελλασ èlmùt_çôl Einige der Bezeichner haben eine besondere, reservierte Bedeutung und dürfen in keinem Fall anders verwendet werden. Dazu gehören die Schlüsselwörter der Programmiersprache Java und drei spezielle konstante Werte (Literale): null, false, true. Drei weitere besondere Bezeichner sind Namen vordefinierter Klassen: Object, String, System. Technisch gesehen könnte man diese Bezeichner auch mit einer anderen Bedeutung verwenden. Man sollte das aber vermeiden.

3.1 Die lexikalischen Elemente von Java

3.1.3

211

Schlüsselwörter

Die folgenden Bezeichner sind Schlüsselwörter der Programmiersprache Java: abstract case continue enum for instanceof new return switch transient

assert catch default extends goto int package short synchronized try

boolean char do final if interface private static this void

break class double finally implements long protected strictfp throw volatile

byte const else float import native public super throws while

Die Schlüsselwörter, const und goto, sind zwar reserviert, werden aber in den aktuellen Versionen von Java nicht benutzt. Das Schlüsselwort enum ist neu ab JDK 1.5.

3.1.4

Literale

Literale sind unmittelbare Darstellungen von Elementen eines Datentyps. 2, –3 und 32767 sind Beispiele für Literale vom Typ int. Insgesamt gibt es folgende Arten von Literalen: • • • • •

Ganzzahlige Literale, Gleitpunkt-Literale, boolesche Literale: false und true, die Null-Referenz: null, Literale für Zeichen und Zeichenketten.

Ganzzahlige Literale Für ganze Zahlen verwendet Java die Datentypen byte (8 Bit), short (16 Bit), int (32 Bit) und long (64 Bit). Für ganzzahlige Literale sind neben der Standardschreibweise auch noch die oktale und die hexadezimale Schreibweise erlaubt. Letztere wird mit den Zeichen 0x eingeleitet, danach können normale Ziffern (0,...,9) oder hexadezimale Ziffern (A,...,F oder a,...,f) folgen. Als Beispiel wird in der Java-Literatur gerne die dezimale Zahl –889275714 hexadezimal als 0xCafeBabe oder als 0xCAFEBABE notiert. Ganzzahlige Literale bezeichnen normalerweise Werte des Datentyps int. Will man sie als Werte des Datentyps long kennzeichnen, so muss man das Suffix L (oder l) anhängen. Beispiele für ganzzahlige Literale sind: 2

17

-3

32767

0x1FF

4242424242L

0xC0B0L

Gleitpunkt-Literale Die Datenformate für reelle Zahlen in Java sind float (floating point number, 32 Bit) und double (double precision number, 64 Bit). Gleitpunkt-Literale werden als Dezimalzahlen mit

212

3 Die Programmiersprache Java

dem im Englischen üblichen Dezimalpunkt notiert. Optional kann ein ganzzahliger Exponent folgen. Gleitpunkt-Literale bezeichnen normalerweise Werte des Datentyps double. Durch Anhängen eines der Suffixe F oder D (bzw. f oder d) spezifiziert man sie explizit als Werte der Datentypen float oder double. Beispiele für Gleitpunkt-Literale des Datenyps double sind: 3.14

.3

2.

6.23e-10

3.7d

1E+137

3.7F

1E+38F

Beispiele für Gleitpunkt-Literale des Datenyps float: 3.14f

.3f

2.f

6.23e-10f

Zeichen-Literale Ein Zeichen-Literal ist ein einzelnes, in einfache Apostrophe eingeschlossenes Unicode-Zeichen. Falls das eingeschlossene Zeichen selbst ein Apostroph oder ein \ sein soll, muss eine der folgenden Ersatzdarstellungen, auch Escape-Sequenzen genannt, verwendet werden: • • • • • • • • •

\b für einen Rückwärtsschritt (BS=backspace: ASCII-Wert 8) \t für einen horizontalen Tabulator (HT) \n für einen Zeilenwechsel (LF) \f für einen Formularvorschub (FF) \r für einen Wagenrücklauf (CR) \" für ein " \' für ein ' \\ für ein \ \uxxxx für ein Unicode-Zeichen. xxxx steht dabei für genau 4 hexadezimale Ziffern.

Beispiele für Zeichen-Literale: 'a'

'%'

'\t'

'\\'

'\''

'\"'

'\u03a9' '\uFFFF' '\177' 'α'

Zeichenketten-Literale Zeichenketten-Literale (meist String-Literale genannt) sind Folgen von Unicode-Zeichen, die in doppelte Anführungszeichen eingeschlossen sind. Falls ein Zeichen des Strings selber ein doppeltes Anführungszeichen oder ein \ sein soll, verwendet man eine der oben angegebenen Ersatzdarstellungen. Ein String-Literal muss auf der gleichen Zeile beginnen und enden. Längere Strings erhält man durch Konkatenation (Verkettung) von Strings mit dem +-Operator: "Hallo Welt !" "Erste Zeile \n zweite Zeile \n dritte Zeile." "Dieser String passt nicht in eine Zeile, daher" + "wurde er mit \"+\" aus zwei Teilen zusammengesetzt."

3.2 Datentypen und Methoden

3.2

213

Datentypen und Methoden

Wie bei allen höheren Programmiersprachen gibt es auch in Java einfache und strukturierte Datentypen. Die strukturierten Datentypen werden auch als Referenzdatentypen bezeichnet. Einfache Datentypen: boolean, char und die numerischen Datentypen: byte, short, int, long, float, double. Referenz-Datentypen: Alle array-, class- und interface-Datentypen. Einfache Datentypen werden so repräsentiert und abgespeichert wie im ersten Kapitel besprochen – byte, short, int und long als Zweierkomplementzahlen, float und double als Gleitkommazahlen, boolean durch ein Byte, char als ein Unicode-Zeichen. Referenz-Datentypen werden als Referenz (Zeiger) auf einen Speicherbereich, in dem die Komponenten abgelegt sind, repräsentiert.

3.2.1

Variablen

Variablen eines Datentyps sind Behälter für genau einen Wert eines einfachen Datentyps oder für genau eine Referenz auf ein Speicherobjekt.

int x = 42;

Programm

Object y = new Object(); Abb. 3.3:

x:

42

y:

Speicher

Objekt

Einfache und Referenz-Variable im Programm und im Speicher

Variablen müssen vor ihrer Benutzung deklariert worden sein. Dazu stellt man dem Namen einer oder mehrerer Variablen den Datentyp voran. Optional kann man eine Variable auch gleich mit einem Anfangswert initialisieren: int x,y,z; double r = 7.0 ; boolean fertig = false ; Variablen können gelesen und geschrieben werden. Ein Lesen der Variablen ist notwendig, wenn sie auf der rechten Seite einer Zuweisung auftaucht. Beispielsweise werden in x = x+y; die Variablen x und y gelesen, ihre Summe berechnet und als neuer Wert in die Variable x geschrieben. Den ersten schreibenden Zugriff auf eine Variable nennt man Initialisierung.

214

3 Die Programmiersprache Java

Nach ihrer Deklarierung haben Variablen einen undefinierten Wert. Vor ihrer ersten Benutzung, d.h. bevor eine Variable zum erstenmal gelesen wird, muss sie initialisiert worden sein. Diese Vorgabe wird statisch, d.h. zur Übersetzungszeit, vom Compiler durch eine Datenflussanalyse überprüft. Dabei geht der Compiler auf „Nummer Sicher“. So würde direkt nach der obigen Deklaration die folgende Anweisung zu einer Fehlermeldung führen, obwohl das Ergebnis 0 weder von x noch von y abhängt: if (x==x) return 0; else return y; Der Compiler stellt nur fest, dass x für die Auswertung der Bedingung gelesen werden muss und dass im zweiten Ast der Anweisung auf y zugegriffen werden könnte. Default-Werte Für jeden Java-Typ existiert ein Standard-Wert, auch default genannt. Im Einzelnen sind dies: • • • •

0 für die numerischen Datentypen, false für boolean \u0000 für char und null für alle Referenztypen.

Objekte oder Arrays werden durch expliziten Aufruf des new-Operators initialisiert. Dabei werden auch alle enthaltenen Komponenten initialisiert – wenn nicht anders festgelegt, mit dem Standard-Wert.

3.2.2

Referenz-Datentypen

Die strukturierten Datentypen werden in Java als Referenz-Datentypen bezeichnet, da man auf diese Daten nur indirekt über einen Zeiger zugreifen kann – eine Referenz. Eine solche Referenz kann entweder mit dem Default-Wert null initialisiert werden int[] x = null; oder es wird durch Aufruf des new-Operators ein entsprechendes Objekt geschaffen und ein Zeiger auf dieses neu angelegte Objekt zurückgegeben, wie z.B. in Object p = new Object(); int [ ] lottoZahlen = new int[6]; Im letzten Fall wird ein Array mit 6 Feldern angelegt, die alle mit 0 initialisiert sind. Initialisierungen mit null sind gefährlich, weil formal zwar das Objekt, nicht aber seine Komponenten initialisiert werden. Sie hebeln daher die vorgenannte statische Überprüfung, ob eine Variable vor ihrer Benutzung initialisiert wurde, aus. So würde nach obiger „Initialisierung“ von x der folgende Code compilieren: x[0]=x[0]+1; Zur Laufzeit würde das Programm aber mit einem NullPointerException abbrechen, weil zwar x, nicht aber x[0] initialisiert ist.

3.2 Datentypen und Methoden

3.2.3

215

Arrays

Zu jedem beliebigen Datentyp T kann man einen zugehörigen Array-Datentyp T[ ] definieren. Ein T-Array der Länge n ist immer eine von 0 bis n-1 indizierte Folge von Elementen aus T. Array-Elemente: Indizes: Abb. 3.4:

17 17

--5 5

42 42

47 47

99 99

--33 33

42 42

19 19

0

1

2

3

4

5

6

7

--42 42 191 191 8

9

Ein Array mit 10 Elementen vom Typ int

Es gibt zwei Möglichkeiten, Objekte eines Array-Datentyps zu erzeugen. Eine Möglichkeit besteht in der expliziten Aufzählung der Komponenten: int[] int1Bsp = { 17, -5, 42, 47, 99, -33, 42, 19, -42, 191}; char[] char1Bsp = {'A', 'a', '%', '\t', '\\', '\'', '\u03a9'}; double[] double1Bsp = { 3.14, 1.42, 234.0, 1e-9d}; Die andere Methode ist, ein Array-Objekt mithilfe des new-Operators zu erzeugen. Dabei muss die Anzahl der Elemente, die das Array haben soll, angegeben werden. Der new-Operator reserviert Speicherplatz für ein neues Array-Objekt mit der gewünschten Zahl von Elementen und gibt eine Referenz auf dieses zurück. Da diese Speicherplatzreservierung, anders als z.B. in Pascal, erst zur Laufzeit des Programmes erfolgt, kann die Anzahl der Komponenten durch einen beliebigen arithmetischen Ausdruck bestimmt werden, dessen Wert auch erst zur Laufzeit ausgewertet wird. Man sagt, dass die Erzeugung von Arrays, allgemeiner von Objekten, dynamisch erfolgt. Bei dieser Gelegenheit erhalten die einzelnen Elemente des neuen Array-Objektes Standardwerte. Die Größe des Array-Objektes kann danach nicht mehr verändert werden. char[] float[] int int[]

asciiTabelle = new char[256]; tagesTemperatur = new float[365]; orte = 100; distanzen = new int[orte*(orte-1)/2];

Wenn n die Anzahl der Komponenten eines Arrays ist, dann werden die einzelnen Elemente mit Indizes angesprochen, deren Wertebereich das Intervall 0 bis n –1 ist. Mit tagesTemperatur[17] = tagestemperatur[16]+1.5; setzen wir die Temperatur des 18. Tages um 1.5 Grad höher als die des Vortages. (Da die Zählung mit 0 beginnt, ist tagesTemperatur[17] das 18. Arrayelement !) Jedes Array-Objekt besitzt ein Feld length, das die Anzahl der Elemente des Arrays speichert. Daher kann man zum Durchlaufen eines Arrays Schleifen der folgenden Art benutzen: for (int i=0; i < distanzen.length; i++){ distanzen[i]=0;}

216

3 Die Programmiersprache Java

Java kennt keine abkürzende Schreibweise für mehrdimensionale Arrays. Solche werden als Arrays aufgefasst, deren Komponenten selbst wieder einen Array-Datentyp haben. Der zum Datentyp T[ ] gehörende Array-Datentyp ist konsequenterweise: T[ ][ ]. Die Größe eines Arrays ist nicht Bestandteil des Typs. Daher sind auch nicht-rechteckige Arrays möglich: int[][] int4Bsp = new int[42][42]; int[][] binomi = {{ 1 }, {1, 1}, {1, 2, 1}, {1, 3, 3, 1} }; binomi[n][k] = binomi[n-1][k-1] + binomi[n-1][k];

3.2.4

Methoden

Methoden sind Algorithmen zur Manipulation von Daten und Objekten. Methoden umfassen und ersetzen die in anderen Programmiersprachen üblichen Begriffe wie Unterprogramm, Prozedur und Funktion. Methoden sind immer als Komponenten eines Objektes oder einer Klasse definiert. Eine Methodendeklaration hat die folgende Syntax:

Attribute Attribute

Ergebnistyp Ergebnistyp

MethodenName MethodenName

{{ Abb. 3.5:

((

Parameterliste Parameterliste

Anweisungen Anweisungen

))

}}

Die Syntax von Methoden

Der Ergebnistyp kann ein beliebiger Java-Datentyp sein, dann handelt es sich um eine Funktion, die ein Ergebnis produzieren muss, oder es kann der leere Datentyp sein: void. Dann handelt es sich um eine Prozedur, die kein Ergebnis berechnet. Jeder Parameter wird durch Angabe seines Datentyps und seines Namens definiert. Mehrere Parameterdefinitionen werden durch Kommata getrennt. Wenn die Parameterliste leer ist, muss man dennoch die öffnende und die schließende Klammer hinschreiben. Auf die Parameterliste folgt ein Block, der aus einer in geschweifte Klammern „{“ und „}“ eingeschlossene Folge von Java-Anweisungen besteht. Das Ergebnis einer Methode muss mit einer return-Anweisung zurückgegeben werden. Dies beendet die Methode. In einer statischen Analyse überprüft der Compiler, dass garantiert jeder Zweig des Programms mit einer return-Anweisung beendet wird. Da void-Methoden keinen Wert zurückliefern, dürfen diese auf eine return-Anweisung verzichten. Beispiel: Eine Funktion zur Berechnung der Fakultät (siehe S. 145). Diese Methode hat einen Parameter n vom Typ int und gibt ein Funktionsergebnis des gleichen Typs zurück. int fak(int n){ if (n y) x -= y; else y -= x; System.out.println(x); } Java-Anweisungen können auch Variablen deklarieren und ihnen einen Wert zuweisen. Diese Variablen sind in dem Block gültig, in dem sie definiert sind und in allen darin geschachtelten Blöcken. Es ist in Java nicht erlaubt in einem inneren Block eine Variable zu definieren, die den gleichen Namen trägt, wie eine Variable eines umgebenden Blockes. Beispiel: Eine Prozedur zum Vertauschen von Elementen eines Array. Die Elemente an den Positionen i und k werden unter Verwendung der lokalen Variablen temp vertauscht. Es wird unterstellt, dass i und k gültige Indizes sind. void swap(int[] a, int i, int k){ int temp = a[i]; a[i] = a[k]; a[k] = temp; } Seit Version 1.5 des JDK können Methoden auch eine variable Anzahl von Argumenten haben. Der formale Parameter wird als Array aufgefasst und in der Deklaration durch „...“ gekennzeichnet. Eine Funktion, die beliebig viele Zahlen akzeptiert und deren Summe berechnet, können wir jetzt wie folgt programmieren: int sum(int ... args){ int sum=0; for(int i : args) sum+=i; return sum; } Ein Aufruf könnte beispielsweise in einer Ausgabeanweisung so erfolgen: System.out.println(sum(12,42,-17,3,8,26)); Nur der letzte Parameter einer Methode darf eine variable Argumentanzahl haben, da sonst die aktuellen Parameter nicht eindeutig den formalen Parametern zugeordnet werden könnten.

218

3 Die Programmiersprache Java

3.2.5

Klassen und Instanzen

Intuitiv ist eine Klasse eine Ansammlung gleichartiger Objekte – diese nennt man auch Instanzen der Klasse. Die Klassendefinition legt die Komponenten fest, aus denen jedes Objekt der Klasse bestehen soll. Soweit könnte man unter einer Klasse K auch einen Datentyp K, etwa vom Recordtyp, verstehen und unter Instanz jede Variable vom Typ K. Im Unterschied zu den Datenstrukturen aus Kapitel 2 können für die Komponenten einer Klasse aber nicht nur Werte, sondern auch Methoden spezifiziert werden. Die Klasse spezifiziert für ihre Instanzen also sowohl die Datenfelder, über deren Inhalte sich die einzelnen Instanzen voneinander unterscheiden, als auch die Methoden, mit denen die Instanzen mit den Instanzen anderer Klassen interagieren können. Kurz: Klasse = Felder + Methoden Klassendefinitionen haben die folgende – vereinfachte – syntaktische Struktur: Feld Feld class class

KlassenName KlassenName

{{

}}

Methode Methode Abb. 3.6:

Die Syntax einer Klassendefinition

Im folgenden Beispiel wird eine Klasse Punkt mit den Feldern x und y (jeweils vom Typ int) und eine Klasse Kreis mit den Feldern radius (vom Typ int) und mitte (vom Typ Punkt) sowie der Methode flaeche definiert. Letztere hat den Ergebnistyp double. class Punkt {int x; int y;

}

class Kreis{ int radius; Punkt mitte; double flaeche( ){ return 3.14*radius*radius; } } Um ein Objekt einer Klasse zu erhalten, reicht es nicht aus, Variablen dieses Typs zu deklarieren, man muss mit dem Operator new zunächst Instanzen der Klasse erzeugen und kann diese dann den Variablen zuweisen: Kreis Kreis Punkt Kreis Kreis

a b p c d

= = = = =

new Kreis(); new Kreis(); new Punkt(); null; a;

3.2 Datentypen und Methoden

219

Hierbei ist das Ergebnis von „new Kreis()“ eine Referenz auf ein neues Objekt der Klasse Kreis. Seine Felder radius und mitte besitzen Standardwerte. Kreis a erklärt a als Variable vom Typ Kreis. a enthält zwar nur einen Zeiger auf das Objekt, es ist im Programmtext aber nie notwendig, diesen wie z.B. in C zu dereferenzieren. c enthält die Null-Referenz. a und d zeigen auf das selbe Kreis-Objekt. Dies verdeutlicht die folgende Abbbildung: a: b: p: c:

0 0

null

d:

0 null

Punkt-Objekt Abb. 3.7:

0 null

Kreis-Objekte

Referenzen auf Punkte und Kreise

Der Zugriff auf die Felder eines Objekts folgt der Syntax objektname.feldname Ist zum Beispiel a eine Instanz von Kreis, so kann man mit a.radius auf die radius-Komponente von a zugreifen, etwa um ihm neue Werte zuzuweisen. p.x = 1; p.y = 2; b.radius = 5; b.mitte = p; a.radius = 6; d.mitte = new Punkt(); a.mitte.x = 3; a.mitte.y = 4; System.out.printf("%8.3f %n", a.flaeche()); System.out.printf("%8.3f %n", b.flaeche()); Die beiden letzten Anweisungen erzeugen als Ausgabe 113,040 bzw. 78,500. Den Effekt derAnweisungen stellt die folgende Abbildung dar: a: b: p: c: d:

null

1 2

3 4

Punkt-Objekte Abb. 3.8:

6 Kreis-Objekte

Punkte und Kreise nach Ausführung der Anweisungen

5

220

3 Die Programmiersprache Java

3.2.6

Objekte und Referenzen

Da Variablen von Referenz-Datentypen immer nur einen Zeiger auf das wirkliche Objekt speichern, wird bei einer Zuweisung auch nur ein Zeiger kopiert. In unserem obigen Beispiel bewirkt die Anweisung Kreis d = a; dass fortan d und a denselben Kreis bezeichnen – sie enthalten Zeiger auf das gleiche Objekt. Eine Änderung eines Feldes von d, etwa d.mitte = new Punkt(); wirkt sich gleichermaßen auf a aus – und umgekehrt. Möchte man in d eine eigenständige und unabhängige Kopie von a speichern, so muss man ein neues Objekt erzeugen und alle Felder kopieren: d = new Kreis(); d.radius = a.radius; Bei der folgenden Zuweisung d.mitte = a.mitte; wird aber auch wieder nur eine Referenz – diesmal auf das gleiche Punkt-Objekt – kopiert. Um a und d auch jeweils eigene Mittelpunkte zu geben, muss man einen neuen Punkt erzeugen und auch dessen Felder kopieren: d.mitte = new Punkt(); d.mitte.x = a.mitte.x; d.mitte.y = a.mitte.y; Besser als das obige fehlerträchtige Verfahren ist es, mit der für alle Referenztypen verfügbaren Java-Methode clone eine vollständige (tiefe) Kopie von a erzeugen d = a.clone(); Bei dem Test auf Gleichheit zweier Objekte wird entsprechend auch nur getestet, ob die Referenzen gleich sind, ob sie also auf dasselbe Objekt zeigen. Zum Test auf inhaltliche Gleichheit muss man die Methode equals benutzen (oder ggf. definieren). Man muss also genau unterscheiden zwischen einem Objekt und einer Variablen, die einen Zeiger auf dieses Objekt enthält. Dennoch wird diese Differenzierung sprachlich meist unterdrückt. Man spricht in dem obigen Falle von „dem Kreis a“ statt „der Referenz a auf einen Kreis“. Da die Dereferenzierung bei Feldzugriffen syntaktisch nicht mehr sichtbar ist (a.radius statt wie bei Pascal oder C), wird diese Identifizierung nahegelegt. Wir werden dieser Sprechweise auch folgen.

3.2 Datentypen und Methoden

3.2.7

221

Objekt- und Klassenkomponenten

Die Felder x und y der Klasse Punkt, wie auch die Felder radius und mitte der Klasse Kreis können für jede Instanz der Klasse andere Werte tragen. Sie können daher auch nur in Verbindung mit einem vorhandenen Objekt abgefragt werden, wie in a.radius oder b.mitte.x, wobei a und b Objekte der Klasse Kreis sein müssen. Ebenso verhält es sich mit den Methoden, so etwa mit flaeche(). So soll a.flaeche() die Fläche von a zurückgeben und b.flaeche() die Fläche von b. Manchmal benötigt man aber Komponenten, die unabhängig von den Objekten einer Klasse sind oder – und das ist eine äquivalente Sichtweise – die für alle Instanzen der Klasse den gleichen Wert haben sollen. Beispielsweise könnte die Klasse Kreis eine zusätzliche Komponente besitzen, in der die Kreiszahl π gespeichert ist, etwa als float pi = 3.14; Diese sollte aber für alle Instanzen der Klasse immer den gleichen Wert haben, insofern sollte sie eine Komponente der Klasse sein. Solche Klassenkomponenten kann man durch das vorangestellte Attribut static festlegen: static float pi = 3.14; Damit wird pi von außen entweder direkt über den Namen der Klasse ansprechbar als Kreis.pi oder, falls eine Instanz a von Kreis zur Verfügung steht, auch als a.pi. Funktionen, die unabhängig von Instanzen einer Klasse verwendet werden sollen, deklariert man ebenfalls als statisch. Sie dürfen daher intern keine Felder von Instanzen der Klasse verwenden, da man sonst ja eine konkrete Instanz benötigen würde. So enthält das Java System immer eine Klasse Math, in der nützliche mathematische Funktionen wie sin, cos, max, min, random, etc. deklariert sind, etwa als static double random(){ ... }. Man ruft sie über den Klassennamen auf, ohne eine Instanz von Math zu erzeugen: double zufall = Math.random(); Klassenkomponenten werden in Java also mit dem Attribut static deklariert und infolgedessen als statische Komponenten bezeichnet. Die Komponenten der Instanzen heißen im Gegensatz dazu dynamisch. Aber der Name trügt – statische Felder sind keineswegs so statisch wie der Name vermuten lässt – über jede Instanz a der Klasse Kreis, oder direkt über den Klassennamen können wir auch statische Felder beliebig verändern, etwa wie im Folgenden: a.pi = 3.1416; Kreis.pi = a.pi - 0.00001; Um solches zu verhindern, kann man Komponenten als final deklarieren. Finale Variablen erhalten ihren Wert bei der Initialisierung und können nicht mehr verändert werden, sie sind also konstant. Die Attribute können auch kombiniert werden. So enthält z.B. die Klasse Math wichtige mathematische Konstanten, wie e oder π.

222

3 Die Programmiersprache Java

Der C-Konvention folgend notiert man Konstanten gänzlich in Großbuchstaben, also: static final double E = 2.718281828459045; static final double PI = 3.141592653589793;

3.2.8

Attribute

Neben den Attributen static und final erlaubt Java auch Attribute, die die Sichtbarkeit und damit die Zugriffsmöglichkeit auf eine oder mehrere Komponenten festlegen. Hier werden nur zwei Attribute kurz erwähnt. Im 6. Abschnitt dieses Kapitels werden wir genauer darauf eingehen. • •

Das Attribut private bewirkt, dass die entsprechende Komponente nur innerhalb der Klasse angesprochen werden darf. Das Attribut public macht die entsprechende Komponente öffentlich. Für Komponenten mit diesem Attribut gibt es keine Zugriffsbeschränkungen.

Eine Felddeklaration kann mit einer Reihe von Attributen beginnen. Es folgen Typ und Name des Feldes und optional ein Ausdruck, der den Anfangswert bestimmt. Wenn ein solcher nicht definiert worden ist, erhalten die Felder einen Standardanfangswert. Die Syntax einer Felddeklaration ist: Feld:

,,

Attribute Attribute

Datentyp Datentyp

Name Name

;; ==

Abb. 3.9:

Ausdruck Ausdruck

Die Syntax von Felddeklarationen

In einer einzigen Felddeklaration können offenbar mehrere Felder gleichen Typs deklariert werden, manche initialisiert, andere nicht, wie z.B. in private int tag, monat = 1, jahr = 2005; In dem folgenden Beispiel wird eine Klasse Datum mit drei Feldern (jahr, monat,tag) jeweils vom Typ int und mit einigen Methoden definiert. Alle Felder haben das Attribut private und Anfangswerte. Wenn eine Instanz der Klasse Datum erzeugt wird, repräsentiert diese zunächst den 1.4.2005. Die Felder jahr, monat und tag können durch die in derselben Klasse definierten Methoden verändert werden – nicht aber von Methoden außerhalb der Klasse. class Datum{ private int jahr = 2008, monat = 7, tag = 15; public int getJahr(){ return jahr;} public int getMonat(){ return monat;} public int getTag(){ return tag;}

3.2 Datentypen und Methoden

223

public void setJahr(int j){ jahr = j;} public void setMonat(int m){ monat = m;} public void setTag(int t){ tag = t;} public void addMonate(int m){ monat += m; while(monat > 12){jahr++; monat -= 12;} while(monat < 1) {jahr--; monat += 12;} } public String toString(){ return "Jahr: " +jahr+ "\tMonat: " +monat+ "\tTag: "+tag; } } Typisch für die objektorientierte Programmierung ist die Datenkapselung. Diese wird in dem Beispiel durch das Attribut private der Felder Jahr, Monat und Tag erreicht. Auf diese Felder kann man von außen nur indirekt, nämlich über die öffentlichen Zugriffsmethoden getJahr(), setJahr(int j) usw. zugreifen. Die Methode addMonate(int m) erlaubt es, zu dem Datum, das durch ein Objekt einer Klasse vom Typ Datum repräsentiert wird, Monate zu addieren. Es gibt keine Einschränkung hinsichtlich des Wertebereiches der zu addierenden Anzahl von Monaten. Auch negative Zahlen sind zulässig. Wir müssen daher darauf achten, dass der sich ergebende Monat im Bereich 1 bis 12 liegt. Durch Definition einer Methode toString kann man zu jeder Klasse eine StandardStringrepräsentation definieren. Diese erlaubt es, Objekte dieser Klasse z.B. mit den Standard-Ausgaberoutinen (u.a. println) zu bearbeiten.

3.2.9

Überladung

Wenn der gleiche Bezeichner unterschiedliche Dinge bezeichnet, spricht man von Überladung (engl. overloading). In einer Klasse dürfen mehrere Methoden mit dem gleichen Namen definiert sein. Sie müssen dann aber unterschiedliche Signaturen besitzen. Dabei versteht man unter der Signatur einer Methode die Folge der Typen ihrer Parameter. Zwei Signaturen sind verschieden, wenn sie verschiedene Längen haben oder sich an mindestens einer Position unterscheiden. Der Ergebnistyp der Methode bleibt dabei unberücksichtigt. Ebenso sind die Namen der Parameter unerheblich. In der Klasse Datum könnten wir statt oder zusätzlich zu der Methode public void addMonate(int m) die folgenden Methoden definieren: public void add(int tage){ ... } public void add(int tage, int monate){ ... } public void add(int tage, int monate, int jahre){ ... } Es wäre allerdings nicht möglich, eine zusätzliche Methode

224

3 Die Programmiersprache Java public void add(int monate)

hinzuzufügen, da diese die gleiche Signatur hat wie eine bereits vorhandene. Unproblematisch ist eine zusätzliche Funktion mit einer variablen Anzahl von Parametern public void add(int ... xs){ ... } Sie wird nur ausgeführt, wenn add mit null oder mehr als drei Argumenten aufgerufen wird.

3.2.10

Konstruktoren

Die einzige Möglichkeit, Objekte einer Klasse zu erzeugen, besteht in der Anwendung des new-Operators. Das Argument dieses Operators ist immer ein Konstruktor, d.h. eine Klassenmethode, die beschreibt, wie ein Objekt der Klasse zu erzeugen ist. Der Name eines Konstruktors ist immer identisch mit dem Namen der Klasse. Wenn, wie in der Klasse Datum, kein Konstruktor explizit definiert worden ist, wird nur der parameterlose Standard-Konstruktor (Default-Konstruktor) ausgeführt, der Speicherplatz für das Objekt reserviert und die Zuweisung der Standardwerte an die Felder erledigt. Im folgenden Programmausschnitt wird mit dem Default-Konstruktor Datum() ein Objekt der Klasse Datum erzeugt. Es wird ausgedruckt, verändert und erneut ausgedruckt: Datum d = new Datum(); System.out.println(d); d.addMonate(42); System.out.println(d); Das Resultat ist: Jahr: 2008 Jahr: 2012

Monat: 7 Monat: 1

Tag: 15 Tag: 15

Die obige Klasse Datum hatte explizite Anfangswerte spezifiziert. Der üblichere Weg wäre, Konstruktoren zu definieren, mit deren Hilfe Objekte vom Typ Datum initialisiert werden. Syntaktisch sind Konstruktoren Methoden mit einigen speziellen Eigenschaften: • • •

ihr Name muss mit dem Namen der Klasse übereinstimmen, sie dürfen keinen Ergebnistyp, auch nicht void, haben, sie können nur mit dem new-Operator aufgerufen werden und erzeugen ein Objekt der Klasse.

Ansonsten verhalten sich Konstruktoren wie alle anderen Methoden der Klasse. Insbesondere kann es mehrere Konstruktoren der gleichen Klasse geben. Diese müssen natürlich verschiedene Signaturen haben. Daher hätten wir die Klasse Datum auch wie folgt definieren können: class Datum { int jahr, monat, tag; Datum(){ jahr = 2008; monat = 7; tag = 15;} Datum(int j){ jahr = j; monat = 1; tag = 1;}

3.2 Datentypen und Methoden

225

Datum(int j,int m){jahr = j; monat = m; tag = 1;} Datum(int j,int m,int t){jahr = j; monat = m; tag = t;} } Die Klasse verfügt über vier Konstruktoren. Der erste ist parameterlos und initialisiert das Datum mit demselben Tag wie die konstruktorlose Version. Der zweite hat einen Parameter und erzeugt ein Datum mit dem gewünschten Jahr. Alle nicht explizit initialisierten Felder der Klasse erhalten den Wert 1. Analog gibt es Konstruktoren mit zwei und drei Parametern. So können wir ein ganzes Array verschiedener Daten konstruieren: Datum[] datumsListe ={ new Datum(), new Datum(2009), new Datum(2009,9), new Datum(2009,9,9)}; und in einer for-Schleife, deren genaue Syntax wir später erklären werden, alle d aus der datumsListe ausdrucken: for(Datum d : datumsListe) System.out.println(d); Der Programmausschnitt produziert das folgende Resultat: Jahr: Jahr: Jahr: Jahr:

3.2.11

2008 2009 2009 2009

Monat: Monat: Monat: Monat:

7 1 9 9

Tag: Tag: Tag: Tag:

15 1 1 9

Aufzählungstypen

Seit dem JDK 1.5 kann man in Java auch Aufzählungstypen definieren. Es handelt sich dabei um Klassen mit einer fest vorgegebenen Menge von Elementen. Eine solche Klasse wird durch eine enum-Anweisung erzeugt, in der alle möglichen Werte aufgelistet werden, wie in: enum Farbe {Rot, Grün, Blau, Weiss, Schwarz} enum Ampel {Grün, Gelb, Rot, GelbRot} enum Wochentag {Mo, Di, Mi, Do, Fr, Sa, So} Wochentag ist nun eine Klasse mit genau 7 Objekten, weitere können nicht erzeugt werden. Daher fehlt in der folgenden Zuweisung der gewohnte new-Operator: Wochentag tag = Wochentag.Mi; Die Elemente der Klasse verhalten sich offenbar wie statische Objekte der Klasse. Objekte verschiedener enum-Typen sind natürlich nicht kompatibel, selbst wenn sie gleiche Namen tragen. Der folgende Vergleich muss somit zu einem Typfehler führen: Farbe.Grün == Ampel.Grün Enum-Klassen können benutzerdefinierte Felder und -methoden erhalten. Selbst Konstruktoren sind möglich. Diese können natürlich nicht dazu benutzt werden, neue Objekte zu erzeugen, wohl aber, um die vorhandenen Objekte geeignet zu initialisieren. In dem folgenden Beispiel reichern wir die Klasse Wochentag um ein Feld istArbeitsTag und eine gleichnamige

226

3 Die Programmiersprache Java

Methode an. Der Konstruktor wird bei der Definition der enum-Klasse eingesetzt, um in den vorhandenen Objekten die Variable istArbeitsTag geeignet zu initialisieren: enum WochenTag { Mo(true), Di(true), Mi(true), Do(true), Fr(true), Sa(false), So(false); private boolean istArbeitsTag; boolean istArbeitsTag(){ return istArbeitsTag; } WochenTag(boolean mussArbeiten){ // Konstruktor istArbeitsTag = mussArbeiten; } } Die statische Methode values() liefert für jede Aufzählungsklasse einen Behälter mit allen Objekten der Klasse, diese kann man dann z.B. mit einer for-Schleife aufzählen: for(WochenTag t : WochenTag.values()) if(t.istArbeitsTag()) System.out.println(t); Eine weitere nützliche Methode ordinal() liefert die Nummer jedes Enum-Objektes. Daher hätten wir istArbeitstTag() auch einfacher definieren können als boolean istArbeitsTag(){ return this.ordinal() javac HalloWelt.java und dann das erzeugte ausführbare Program ausführt > java HalloWelt Einzelne Klassen kompilieren, testen und untersuchen kann man besonders einfach mit dem kostenlosen BlueJ-System (www.bluej.org). Klassen werden darin als beschriftete rechteckige Felder dargestellt. Mit der rechten Maustaste kann man neue Objekte erzeugen, Methoden von Klassen und Objekten auswählen und starten. Objekte können „inspiziert“ und Veränderungen direkt beobachtet werden.

Abb. 3.10:

Erstellen und Testen einer Java-Klasse im BlueJ-System

In den traditionellen Java-Entwicklungsumgebungen kann man Klassen nur testen, indem man sie in einem Rahmenprogramm, d.h. in einer Klasse mit einer main-Methode benutzt. Die Klasse Datum, die wir im letzten Abschnitt vorgestellt haben, kann z.B. aus dem folgenden Rahmenrogramm getestet werden. Datum.java und DatumTest.java können getrennt kompiliert werden. Aufgerufen wird die Klasse DatumTest.

228

3 Die Programmiersprache Java class DatumTest { public static void main(String ... args) { Datum d = new Datum(); System.out.println(d); d.addMonate(42); System.out.println(d); } } class Datum { private int jahr = 2008; ..... }

3.3.1

Java-Dateien – Übersetzungseinheiten

Eine zu kompilierende Java-Datei ist eine Textdatei mit dem Suffix .java. Offiziell bezeichnet man eine solche Datei auch als Übersetzungseinheit. Sie kann den Quelltext einer oder mehrerer Java-Klassen enthalten. Datei1.java

class A{…} class B{…}

B.class B.class

Datei2.java

class C{…} Abb. 3.11:

Object.class Object.class

A.class A.class

Compiler

C.class C.class

JVM

Betriebs system

System.class System.class Xyz.class Xyz.class

Übersetzung und Ausführung von Java-Programmen

Bei erfolgreicher Übersetzung mit einem Javacompiler entstehen in dem Verzeichnis, das die Übersetzungseinheit enthält, Dateien mit der Endung .class, und zwar eine pro übersetzter Klasse. Diese Klassendateien enthalten alle zur Ausführung der Klasse notwendigen Informationen, insbesondere den Java-Byte-Code, der von der virtuellen Maschine JVM ausgeführt wird. In einer Übersetzungseinheit darf höchstens eine Klasse das Attribut public haben. Wenn dies der Fall ist, muss deren Name der Dateiname der Übersetzungseinheit sein.

3.3.2

Programme

Die Ausführung von Java-Programmen beginnt mit einer Klasse, die eine wie oben spezifizierte Methode main enthält. Diese wollen wir Hauptprogramm-Klasse nennen. Sie kann andere Klassen benutzen, jene wieder andere Klassen etc. Die benutzten Klassen können in derselben Übersetzungseinheit definiert sein, oder in anderen Übersetzungseinheiten beliebiger Dateiverzeichnisse. Klassen dürfen sich auch wechselseitig rekursiv benutzen.

3.3 Ausführbare Java-Programme

229

Das Gesamtprogramm besteht aus der Hauptprogramm-Klasse und allen benutzten Klassen. Diese werden geladen, wenn sie in einem übersetzten Programm zum ersten Mal angesprochen werden. Danach kann man auf alle Felder und Methoden dieser Klasse, die das Attribut static haben, über den Namen der Klasse zugreifen. Alle anderen Felder und Methoden kann man nur über dynamisch erzeugte Objekte der jeweiligen Klasse ansprechen. In dem obigen Programmausschnitt bewirkt die Zeile System.out.println(d1); dass die Klasse System geladen wird, denn out ist ein statisches Feld der Klasse System. Es enthält ein Objekt der Klasse PrintStream, in der die Instanzmethode println definiert ist. All diese Information und mehr findet man in der HTML-Dokumentation der Java-Klassen.

Abb. 3.12:

3.3.3

Blick in die Java-API Dokumentation

Packages

Pakete (engl.: packages) sind Zusammenfassungen von Java-Klassen für einen bestimmten Zweck oder einen bestimmten Typ von Anwendungen. Das wichtigste Paket, java.lang enthält alle Klassen, die zum Kern der Sprache Java gezählt werden. Daneben enthält das JDK eine Unmenge weiterer Pakete, z.B. java.awt mit Klassen zur Programmierung von Fenstern und Bedienelementen, java.net mit Klassen zur Netzwerkprogrammierung, java.util mit nützlichen Klassen wie Calendar, Date, ArrayList und Schnittstellen Collection, List, Iterator, etc. Seit dem JDK 1.5 gibt es hier auch eine Klasse Scanner zum bequemen Input von der Kommandozeile.

230

3 Die Programmiersprache Java

Pakete können in irgendeinem Verzeichnis auf dem eigenen Rechner liegen. In frühen Java-Versionen wurde propagiert, sie könnten auch auf einem Rechner irgendwo im Internet sein. Davon ist man aus Sicherheitsgründen, und um die Konsistenz von Versionen besser kontrollieren zu können, abgekommen. Um eine Klasse eines Paketes anzusprechen kann man grundsätzlich die Paketadresse dem Namen voranstellen, etwa java.util.Arrays.copyOf(meinArray, 4711); java.util.Calendar.getInstance(); Einfacher ist es, wenn man eine sogenannte import-Anweisung benutzt. Damit können eine oder alle Klassen eines Paketes direkt angesprochen werden, ohne ihnen jeweils die Paket-Adresse voranzustellen. Schickt man also eine Import-Anweisung wie import java.util.Calendar; voraus, so kann man anschließend Objekte und Methoden der Klasse Calendar benutzen, ohne den vollen Pfad zu ihnen anzugeben: Calendar c = Calendar.getInstance(); Die Namen von Standardpaketen beginnen grundsätzlich mit java, javax oder sun. Die zugehörigen Dateien liegen im Java-System. Compiler und Interpreter wissen wie sie diese Dateien finden. Um alle Klassen eines Pakets zu importieren, kann man das Zeichen „*“ einsetzen. So werden z.B. mit import java.util.*; sämtliche Klassen des Pakets util importiert. Allerdings ist die Bezeichnung „importieren“ irreführend, da nichts importiert wird, es werden im Programm angesprochene Klassen lediglich an den bezeichneten Stellen aufgesucht. Auch mit einer import-Anweisung ist es notwendig, statische Felder und Methoden einer Klasse zusammen mit dem Namen der Klasse anzugeben. Seit Java 1.5 gibt es als zusätzliche Möglichkeit die import-static-Anweisung. Diese ermöglicht den unqualifizierten Zugriff auf statische Felder und Methoden einer Klasse. Man sollte diese Möglichkeit aber mit Vorsicht einsetzen. Ein kurzes Beispiel: import static java.lang.Math.*; import static java.lang.System.*; class TestImportStatic { public static void main(String ... args) { out.println(sqrt(PI + E)); } } Damit Java weiß, in welchem Verzeichnis des eigenen Rechners nach Paketen zu suchen ist, gibt es eine Umgebungsvariable CLASSPATH, die Dateipfade zu Java-Paketen enthält. Über ihren Inhalt kann man sich z.B. mit folgender Anweisung informieren:

3.3 Ausführbare Java-Programme

231

System.getProperty("java.class.path"); Wenn auf dem lokalen Rechner ein Paket im Pfad D:\Buch\Kap3\Test\MeinPaket gesucht wird und wenn D:\Buch\Kap3 Teil des Klassenpfades ist, dann darf Test.MeinPaket als legaler Paketname in einem Java Programm verwendet werden. Um selber Pakete zu erzeugen, kann man veranlassen, dass die Klassen einer Java-Datei zu einem bestimmten Paket gehören sollen. Dies erreicht man mit einer package-Anweisung am Anfang der Übersetzungseinheit: package PaketName;

3.3.4

Standard-Packages

Java-Programme können vordefinierte Standard-Pakete benutzen, die auf jedem Rechner mit einer Java-Programmierumgebung zu finden sein müssen. Kern der Standard-Pakete ist java.lang. (lang steht für language.) Dieses Paket enthält die wichtigsten vordefinierten Klassen. Einige haben wir bereits kennen gelernt oder erwähnt: System, String und Object. Daneben sind Klassen mit vordefinierten Methoden für die meisten primitiven Datentypen definiert: Boolean, Character, Number, Integer, Long, Float und Double. Die Klasse Math mit mathematischen Standardfunktionen wie z.B. sin, cos, log, abs, min, max, Methoden zur Generierung von Zufallszahlen und den Konstanten E und PI haben wir bereits erwähnt. Alle Klassen im Paket java.lang können ohne Verwendung von qualifizierten Namen benutzt werden. Die Anweisung import java.lang.*; wird also vom Compiler automatisch jedem Programm hinzugefügt. Für alle anderen Standard-Pakete muss man entweder import-Anweisungen angeben oder alle Namen voll qualifizieren. Die folgenden Standard-Pakete gehören zum Sprachumfang von Java: • •

java.io: Enthält Klassen für die Ein- und Ausgabe. java.util: Neben nützlichen Klassen wie Date, Calendar etc. finden sich hier vor allem Klassen und Schnittstellen für Behälterklassen.

Interessant ist seit dem JDK 1.5 auch die Klasse Scanner, mit deren Methoden next und nextInt man auf einfache Weise von dem Konsolenfenster lesen kann, wie folgendes kleine Testprogramm zeigt: import java.util.*; public class Test{ public static void main (String ... args){ Scanner s = new Scanner(System.in); System.out.print("Dein Vorname bitte: "); String name = s.next(); System.out.println("Hallo "+name+"\nWie alt bist Du ?");

232

3 Die Programmiersprache Java int value = s.nextInt(); if(value>30) System.out.println("Hallo alter Hase"); else System.out.println("Noch lange bis zur Rente ..."); s.close(); } }

Weitere Standard-Pakete gehören zwar nicht zum Sprachumfang von Java, sie werden aber von praktisch allen Java-Programmierumgebungen angeboten. Mit jeder neuen Version des JDK kommen neue hinzu. Einige ausgewählte Standard-Pakete sind: • • • • • • • • •

java.net: Dieses Paket ist nützlich zum Schreiben von Internet-Anwendungen. java.text: Ein Paket zum Editieren und Formatieren von Text, Zahlen und Datumsangaben unabhängig von den Gepflogenheiten verschiedener Länder. java.awt: AWT steht für Abstract Windowing Toolkit. Klassen dieses Paketes werden zur Programmierung von Programmen mit grafischer Benutzerschnittstelle genutzt. java.awt.geom: Definiert Klassen für Operationen auf 2D Geometrieobjekten. java.awt.color: Ein Unterpaket von awt zur Farbdarstellung. java.awt.image: Definiert Klassen zum Erzeugen und Modifizieren von Bildern. java.awt.font: Zur Bearbeitung von Schriftarten. java.awt.print: Ein Unterpaket von awt zum Drucken. java.applet: Dieses Paket definiert Klassen zur Programmierung von Applets.

3.4

Ausdrücke und Anweisungen

Ausdrücke dienen dazu, Werte, also Elemente von Datentypen, zu berechnen. Ausdrücke können aus Literalen, Variablen oder Feldern, Methodenaufrufen, Operatoren und Klammern aufgebaut sein. Variablen und Felder können auch indiziert sein, also z.B. a[i]. Die Namen von Variablen und Felder können zusammengesetzt sein, mit Punkten wird dabei auf Komponenten verwiesen, also z.B. a.b.c.d. An jeder Stelle eines Programms, an der ein Wert eines bestimmten Datentyps erwartet wird, darf auch ein Ausdruck verwendet werden, der zum Zeitpunkt der Übersetzung des Programms oder aber zur Laufzeit zu einem entsprechenden Wert ausgewertet werden kann.

3.4.1

Arithmetische Operationen

Die gebräuchlichsten Operatoren sind die zweistelligen arithmetischen Operationen Addition, Subtraktion, Multiplikation, Division und Modulo. Diese sind für alle numerischen Datentypen definiert: + - * / % Plus und Minus sind auch als einstellige Vorzeichen-Operationen definiert. / ist auf den RealDatentypen die normale Division, auf den ganzzahligen Datentypen die Division ohne Rest.

3.4 Ausdrücke und Anweisungen

233

Die Operatoren haben die übliche Präzedenz (Punktrechnung vor Strichrechnung) und sind linksassoziativ. (Insbesondere gilt z.B. 3 – 2 – 1 = 0.) Will man davon abweichen, so muss man Klammern verwenden. Der +-Operator wird auch zur Konkatenation, d.h. zur Verkettung, von Strings benutzt. Konkateniert man einen String mit einer Zahl, so wird letztere in einen String umgewandelt. Insbesondere gilt z.B. "1" + 1 ergibt "11" "1" + 1 + 1 ergibt "111" "1" + (1 + 1) ergibt "12"

3.4.2

Vergleichsoperationen

Die Vergleichsoperationen sind == , != , > , >= , < und 1)) ... Der zweite Teilausdruck wird nur ausgewertet, wenn der erste Teilausdruck den Wert true liefert, wenn nenner also tatsächlich von Null verschieden ist.

3.4.4

Bitweise Operationen

Die bitweisen Operatoren beruhen auf den Darstellungen von Werten der einfachen Datentypen als Bitfolgen. Sie sind für Werte des Datentyps char und für alle ganzzahligen Datentypen definiert. Die ersten vier Operatoren führen die booleschen Operationen bitweise aus und sind auch auf dem Datentyp boolean erlaubt. ~ & | ^

Komplement (bitweise Negation) Konjunktion ( bitweises and) Disjunktion (bitweises or) Exclusives Oder (bitweises xor).

Andere bitweise Operationen schieben (engl. to shift) die Bits nach links bzw. rechts. Die Anzahl der Positionen, um die geschoben wird, ist durch den zweiten Operanden definiert: > >>>

3.4.5

Links-Shift. Rechts wird mit 0 aufgefüllt Rechts-Shift. Links wird mit Vorzeichenbit aufgefüllt (arithmetic shift) Rechts-Shift. Links wird mit 0 aufgefüllt (logical shift).

Zuweisungsausdrücke

Java erbt von C die semantische Vermischung von Ausdrücken und Anweisungen : • •

viele Ausdrücke haben nicht nur einen Wert, sondern auch Seiteneffekte, viele Anweisungen haben nicht nur einen Effekt, sondern auch einen Wert.

3.4 Ausdrücke und Anweisungen

235

Wie in C ist auch hier das Gleichheitszeichen = als Zuweisungsoperator definiert. Für eine Variable v und einen Ausdruck (engl.: expression) e ist daher v=e der Zuweisungsausdruck. Er bewirkt die Berechnung des Wertes von e und dessen Zuweisung an die Variable v. Sein Wert ist der Wert von e. Ein Zuweisungsausdruck wird meist nur wegen seines Seiteneffektes benutzt. Weil v = e syntaktisch ein Ausdruck ist, ist auch eine Mehrfachzuweisung wie im folgenden Beispiel (mit v = „x“ und e = „y = x+5“ ) legal: x = y = x+5; Da = als rechtsassoziativ definiert ist, wird der Ausdruck von rechts nach links abgearbeitet. Die Zuweisung y = x+5 liefert den Wert von x+5. Dieser wird x zugewiesen. Die anderen Zuweisungsoperatoren sind alle von der Form op=, wobei op ein Operator ist: +=

-=

*=

/=

&=

|=

^=

%=

=

>>>=

Für jeden Operator op ist v op= e eine Kurzschreibweise für v = v op e, jedoch erfolgt dabei nur ein einziger Zugriff auf v. Dieses bietet gelegentlich einen Effizienzvorteil, insbesondere wenn v eine indizierte Variable ist und die Berechnung der Indexposition zeitaufwändig: a[ findIndex() ] += 5 ; Allerdings wäre ein derartiges Problem auch mit einer Hilfsvariablen lösbar, etwa durch: { int temp = findIndex(); arr[temp] = arr[temp] + 5; } Weitere populäre Kurzschreibweisen für Zuweisungen ermöglichen die Autoinkrementoperatoren ++ und --. Es gibt sie in einer Präfix- und einer Postfixversion. Diese unterscheiden sich durch den Zeitpunkt, an dem das Ergebnis des Ausdrucks ermittelt wird – vor oder nach dem Inkrementieren, bzw. Dekrementieren. Für eine Variable v sind äquivalent: ++v mit v += 1 sowie --v mit v -= 1. Als Postfixoperatoren liefern die Autoinkremente den ursprünglichen Variablenwert zurück: v++ hat den Wert von v aber den Effekt von v += 1. v-- hat den Wert von v aber den Effekt von v -= 1. Wenn Ausdrücke mit Seiteneffekt benutzt werden, kann das Ergebnis auch von der Reihenfolge der Auswertung der Teilausdrücke eines Ausdrucks abhängen. Daher definiert Java für die Auswertung von Ausdrücken eine Standardreihenfolge. Seien int[] a = {1,2}; int i = 0; dann hat (a[i] * ++i) den Wert 1, dagegen (++i * a[i]) den Wert 2! Ähnlich undurchsichtig ist:

236

3 Die Programmiersprache Java int i = 42; System.out.println(i + " " + ++i + " " + i++ + " " + i); System.out.println(i + " " + --i + " " + i-- + " " + i);

Dieses Programmfragment produziert folgendes Ergebnis: 42 43 43 44 44 43 43 42

3.4.6

Anweisungsausdrücke

In C konnte man aus jedem Ausdruck eine Anweisung machen, indem man ihm ein Semikolon (;) nachstellte. In Java geht das nur mit bestimmten Ausdrücken, man nennt sie Anweisungsausdrücke. Dazu zählen: • • •

Zuweisungsausdrücke (also z.B. auch Autoinkrementausdrücke wie v++). Methodenaufrufe (wie z.B. fact(5) oder a.fläche() oder auch a.init() ) Instanzerzeugungen durch den new-Operator ( wie z.B. in new Datum() ).

3.4.7

Sonstige Operationen

Neben den bisher vorgestellten gibt es in Java eine Reihe weiterer Operatoren: Typumwandler (casts): In einigen Fällen erfolgt eine implizite Typumwandlung. Dies ist immer möglich, wenn ein Typ ausgeweitet wird. Dies ist bei char → int der Fall und ebenso bei den numerischen Typen in der Richtung: byte → short → int → long → float → double . In der umgekehrten Richtung ist eine automatische Konvertierung jedoch nicht möglich, da durch die Umwandlung ein Teil des Wertes abgeschnitten oder verfälscht werden kann. Wenn der Programmierer sicher ist, dass die Umwandlung im Einzelfall korrekt funktioniert, darf er eine explizite Typumwandlung spezifizieren. Dabei wird der gewünschte Typ in Klammern vor den umzuwandelnden Ausdruck gestellt. Dies nennt man auch cast (engl. Abguss oder Rollenbesetzung beim Film). int a = 42; float f = a; int neu = (int) f;

// automatische Typumwandlung //explizite Typumwandlung erforderlich!

Allgemein gibt es zu jedem Datentyp einen expliziten Typumwandlungsoperator (type cast). Er hat den gleichen Namen wie der Datentyp, muss aber in runde Klammern eingeschlossen werden, hat also die Form (Datentyp) Die Anwendung eines solchen Operators auf einen Ausdruck kann zur Compilezeit oder auch zur Laufzeit zu einer Fehlermeldung führen, wenn die geplante Umwandlung nicht zulässig

3.4 Ausdrücke und Anweisungen

237

ist. Wenn Zahlen abgeschnitten werden, führt dies nicht zu Fehlermeldungen. Der Programmierer hat die Typumwandlung ja selbst gewollt. Zulässig ist daher z.B.: double d = 123456E300; int neu = (int) d; Der Wert von neu nach der Umwandlung ist in diesem Fall der maximale Wert des Typs int. Ob das für den Programmierer Sinn macht ist eine andere Frage. Bedingte Ausdrücke: Diese kann man mit dem Operator „ ? : “ bilden, dem einzigen Operator mit drei Operanden: op1 ? op2 : op3 Der erste Operand muss ein Ergebnis vom Typ boolean liefern. Wenn dieses true ist, wird der zweite Operand ausgewertet und bestimmt das Ergebnis des Ausdrucks. Andernfalls ist das Ergebnis des bedingten Ausdruckes gleich dem Wert des dritten Operanden. Als Beispiel können wir die Fakultätsfunktion von S. 216 prägnanter formulieren: int fakt(int n){ return (n >>>

Vergleiche

< > >= >>=

3.4.9

Einfache Anweisungen

Bei den einfachen Anweisungen zeigen sich wichtige Unterschiede zwischen Java und Pascal. Anweisungen sind auch Ausdrücke und Ausdrücke sind Anweisungen. Wie bereits erwähnt, ist eine Zuweisung v=e ein sogenannter Zuweisungsausdruck und liefert als Wert den Wert des Ausdrucks e. Als Seiteneffekt wird dieser in der Variablen v gespeichert. Beendet man einen Zuweisungsausdruck mit einem Semikolon „ ; “, so wird daraus eine Anweisung. Ein einzelnes Semikolon „ ; “ ist daher ein „skip“, also eine leere Anweisung. Meist dient das Semikolon aber dazu, einen Anweisungsausdruck in eine Anweisung zu verwandeln. Allgemein hat eine einfache Anweisung die Form: Anweisungsausdruck ; Eine Variablendeklaration ist ebenfalls eine einfache Anweisung. Eine oder mehrere Variablen des selben Datentyps werden deklariert und können optional Anfangswerte erhalten. Eine Variablendeklarationsanweisung kann mitten in einer Berechnung auftauchen, überall dort wo eine Anweisung erlaubt ist. ,,

Typ Typ

Name Name

final final Abb. 3.13:

Die Syntax von Variablendeklaration

;; ==

Ausdruck Ausdruck

3.4 Ausdrücke und Anweisungen

239

Im Unterschied zur Syntax einer Felddeklaration ist in einer Variablendeklaration nur das Attribut final erlaubt. Dieses signalisiert, dass der Variablen genau einmal ein Wert zugewiesen werden darf bzw. muss. Zu den einfachen Anweisungen gehört auch die return-Anweisung. Sie beendet einen Methodenaufruf. Das Ergebnis des Methodenaufrufs ist der Wert des return-Ausdruckes. Bei einer Methode mit Ergebnistyp void entfällt dieser. Die return-Anweisung hat also die Form return Ausdruck; oder einfach nur return ;

3.4.10

Blöcke

Ein Block ist eine Anweisung, die aus einer in geschweifte Klammern eingeschlossenen Folge von Anweisungen besteht. Wird in einem Block eine Variable deklariert (eine solche nennt man lokale Variable), so erstreckt sich deren Gültigkeit vom Ort der Definition bis zum Ende des Blocks. {

Anweisung1 Anweisung2 ... Anweisungn

}

Die scheinbar fehlenden Semikola „ ; “ sind Bestandteile der Anweisungen. Da die Anweisungen auch wieder Blöcke sein dürfen, ergibt sich eine Blockschachtelung und damit eine Schachtelung von Gültigkeitsbereichen der darin deklarierten Variablen. Allerdings ist es verboten, eine lokale Variable in einem inneren Block erneut zu definieren. Der äußere Block im folgenden Beispiel enthält eine Variablendeklarationsanweisung, einen Block und einen Methodenaufruf. Der innere Block enthält eine Deklaration mit Initialisierung der Variablen temp, und zwei Zuweisungen. Im inneren Block sind i, j und temp zugreifbar, im äußeren nur i und j. Insgesamt werden die Inhalte von i und j vertauscht. {

int i=0,j=1; { int temp = i; i=j; j=temp; } System.out.println("i:"+i+" j:"+j);

} Wenn an einer Stelle syntaktisch eine Anweisung verlangt ist, man aber mehrere Anweisungen benutzen möchte, so muss man diese zu einem Block gruppieren. Da dies fast der Regelfall ist, verwendet man in in Programmfragmenten häufig die Notation { ... } um anzudeuten, dass an der bezeichneten Stelle eine oder mehrere Anweisungen stehen können.

3.4.11

Alternativ-Anweisungen

Die zwei Formen der bedingten Anweisung sind if (Bedingung) Anweisung und if (Bedingung) Anweisung1 else Anweisung2

240

3 Die Programmiersprache Java

Bedingung steht für einen Ausdruck mit Ergebnistyp boolean. Je nachdem, ob die Bedingung erfüllt ist oder nicht, wird Anweisung1 oder Anweisung2 ausgeführt.

if if

Abb. 3.14:

((

Bedingung Bedingung

))

Anweisung Anweisung11

else else

Anweisung Anweisung22

Die Syntax von if-Anweisungen

Bei der Schachtelung von bedingten Anweisungen entsteht auch in Java das bekannte dangling-else-Problem, dass der Ausdruck if(B1) if(B2) A1 else A2 auf zwei Weisen gelesen werden kann. Wie üblich, verwendet man auch hier die Regel: „Ein

else ergänzt das letzte unergänzte if.“ Die obige Anweisung ist damit gleichbedeutend mit if(B1) { if (B2)A1 else A2 }.

3.4.12

switch-Anweisung

Wenn in Abhängigkeit von dem Ergebnis eines Ausdruckes eine entsprechende Anweisung ausgeführt werden soll, kann man diese in der switch-Anweisung zusammenfassen. switch switch

((

Switch-Ausdruck Switch-Ausdruck

))

{{

Fall Fall

}}

Fall: case case

Konstante Konstante default default

Abb. 3.15:

::

Anweisungsfolge Anweisungsfolge

::

Die Syntax von switch-Anweisungen

Der Typ des switch-Ausdrucks muss char, byte, short, int oder ein enum-Typ sein. Für alle möglichen Ergebniswerte dieses Ausdruckes kann jeweils ein Fall formuliert werden. Dieser besteht immer aus einer Konstanten und einer Folge von Anweisungen. Schließlich ist noch ein Default-Fall erlaubt. Wenn der switch-Ausdruck den Wert eines der Fälle trifft, so wird die zugehörige Anweisungsfolge und die aller folgenden Fälle (!) ausgeführt, ansonsten wird, falls vorhanden, der Default-Fall und alle folgenden Fälle (!) ausgeführt. Die switch-Anweisung ist gewöhnungsbedürftig und führt häufig zu unbeabsichtigten Fehlern. Die Regel, dass nicht nur die Anweisung eines Falles, sondern auch die Anweisungen aller auf einen Treffer folgenden Fälle ausgeführt werden, ist ein Relikt von C. Wenn man möchte, dass immer nur genau ein Fall ausgeführt wird, so muss man jeden Fall mit einer

3.4 Ausdrücke und Anweisungen

241

return-Anweisung oder mit einer break-Anweisung enden lassen. Mit letzterer verlässt man den durch die switch-Anweisung gebildeten Block. Im folgenden Beispiel endet jeder Fall mit einer return-Anweisung. Hier erweitern wir die Klasse Datum um eine Methode, die die Anzahl der Tage eines Monats bestimmt: static int tageProMonat(int j, int m){ switch (m){ case 1: case 3: case 5: case 7: case 8: case 10: case 12: return 31; case 2: if (schaltJahr(j)) return 29; else return 28; default: return 30; } } Auch für enum-Typen, die in Wirklichkeit spezielle Klassen darstellen, kann man switch verwenden. Im folgenden Beispiel definieren wir den enum-Typ Wochentag mit einer Methode, die feststellt, ob ein Wochentag ein Werktag ist: enum Wochentag { Mo, Di, Mi, Do, Fr, Sa, So; boolean istWerktag(){ switch(this){ // ’this’ bezeichnet das Objekt selber case Sa: case So: return false; default: return true; } } } Wir testen dies mit einem kleinen Hauptprogramm: public static void main(String ... args) { for (Wochentag t : Wochentag.values() ) { System.out.print(t + " ist "); if (! t.istWerktag())System.out.print('k'); System.out.println( "ein Werktag."); } } }

3.4.13

Schleifen

Bei der while-Schleife wird die Schleifenbedingung vor jeder Iteration getestet. Semantisch unterscheidet sie sich nicht von der while-Schleife in anderen Programmiersprachen.

while while Abb. 3.16:

((

Bedingung Bedingung

))

Anweisung Anweisung

Die Syntax von while-Anweisungen

Die do-while-Schleife dagegen testet ihre Bedingung nach jeder Iteration. Ihr Rumpf wird daher immer mindestens einmal ausgeführt. Von C sind auch zwei syntaktische Eigenarten der do-while-Schleife übriggeblieben:

242 • •

3 Die Programmiersprache Java

sie wird explizit mit einem Semikolon beendet, und zwischen do und while steht eine Anweisung, nicht eine Anweisungsfolge.

do do Abb. 3.17:

Anweisung Anweisung

((

while while

Bedingung Bedingung

))

;;

Die Syntax von do-while-Anweisungen

Typisch für eine do-while-Anweisung ist eine Eingabeaufforderung, die mindestens einmal ausgeführt und dann so lange wiederholt wird, bis der Benutzer den gewünschten Input liefert. Unser Beispiel verwendet ein Eingabefenster aus javax.swing.JOptionPane: String input; do{ input = JOptionPane.showInputDialog("Passwort bitte"); } while (!input.equals("Geheim")); System.out.println("Willkommen, Meister!");

3.4.14

Die for-Anweisung

Die for-Schleife als iterierende Anweisung ist mächtiger als das Äquivalent bei vielen anderen Programmiersprachen. Sie kann sogar die while- und die do-while-Schleifen ersetzen. Dafür ist sie auch unstrukturierter, insbesondere muss eine for-Schleife nicht terminieren! for for Abb. 3.18:

((

Init Init

;;

Test Test

;;

Update Update

))

Anweisung Anweisung

Die Syntax von for-Anweisungen

Init und Update stehen für (möglicherweise leere) Folgen von Anweisungsausdrücken. In diesem Falle ist die Schleife for (Init1 , ... , Initm ; Test ; Update1 , Anweisung

... , Updaten)

äquivalent zu {

Init1 ; ... Initm ; while ( Test ) { Anweisung ; Update1 ; ... Updaten;

}} Statt der Initialisierungsausdrücke darf man auch eine Variablendeklaration verwenden. Offensichtlich definiert die for-Anweisung einen Block, so dass eine eventuell in dem InitAbschnitt deklarierte lokale Variable in ihrer Gültigkeit auf die for-Schleife beschränkt bleibt.

3.4 Ausdrücke und Anweisungen

243

Wir haben von der letzteren Form der for-Schleife bereits früher in diesem Kapitel Gebrauch gemacht. Insbesondere eignen sich solche for-Schleifen zur Iterierung durch ein Array: for( int i=0; i

Marburg

6 6 2006

42 1023

Meist leicht zeitweise

686

8 Das Internet

Der auffälligste Unterschied zu HTML ist die Möglichkeit, selbstdefinierte Marken zu verwenden. Während die Informationen in der HTML-Fassung durch eher nichts sagende Absatzmarken

gegliedert sind, geben die entsprechenden XML-Marken bereits Hinweise auf die Bedeutung des Textes, den sie umgeben. Weitere Informationen können in Attributen der Marken untergebracht werden. Im Beispiel sind das Informationen über den Messpunkt und die Einheiten der Messungen. Allerdings muss jedes Attribut mit einem Wert versehen und dieser in einfachen oder doppelten Hochkommata eingeschlossen sein. Im Unterschied zu HTML muss jede öffnende Marke mit einer schließenden Marke gepaart sein. Ein solches Markenpaar verhält sich wie ein Paar von Klammern, die einen Dokumentbereich umfassen. Verschiedene solche Bereiche dürfen geschachtelt werden, sie dürfen sich aber nicht anderswie überlappen. Statt von Marken spricht man in XML von Elementen und meint damit den Namen der Marke und den Inhalt, den das Markenpaar einschließt. Es muss genau ein äußeres Element, das sogenannte Wurzelelement geben, das alle Elemente des Dokuments umfasst. Ein XML-Dokument heißt wohlgeformt, falls es diesen Syntaxvorgaben entspricht. Mit den Zeichenfolgen „“ begrenzte Marken sind sogenannte processing instructions (PI). Sie enthalten Text, der durch spezielle Programme bearbeitet werden soll. Im obigen Beispiel finden wir die Anweisung „“, die sich an einen XML-Prozessor richtet, so wie wir im vorigen Kapitel bereits die processing instruction „“ kennengelernt haben, die Anweisungen für den php-Präprozessor enthält. Offensichtlich repräsentiert ein wohlgeformtes XML-Dokument immer einen Baum. Die Knoten entsprechen den XML-Elementen und die Blätter entweder unstrukturiertem Text oder Elementen, die keinen weiteren Inhalt enthalten. Solche terminalen Elemente können als kombinierte Anfangs- und Endemarke notiert werden, indem die schließende spitze Klammer „>“ durch „/>“ ersetzt wird, wie in dem Element „“ des obigen Beispiels. Es ist durchaus erlaubt, dass sie noch Attribut-Werte-Paare enthalten. Die folgende Abbildung zeigt die erwähnte XML-Datei mit dem Wetterbericht in zwei Ansichten: rechts in der gewohnten Textansicht und links in der zugehörigen Baumansicht. Die Knoten des Baumes entsprechen den XML-Elementen. Die Blätter des Baumes sind entweder leere Elemente, wie z.B. , der Dateninhalt von Elementen wie z.B. „42“, oder Attribut-Werte-Paare, wie z.B. messpunkt="Kirchspitze". Letztere kann man stattdessen auch als Modifikatoren der Knoten zu denen sie gehören, betrachten. Interessant ist die Mischung von Text und strukturierter Information in dem Element . Jeder Textabschnitt zwischen den strukturierten Inhalten wird zu einem eigenen Blatt. Jedes XML-Dokument repräsentiert also einen Baum. Umgekehrt lässt sich jede Baumstruktur auch in XML abbilden. Für jeden Knotentyp kann man ein entsprechendes Element erfinden, das im Text des zugehörigen XML-Dokuments alle seine Söhne umschließt. Auf diese Weise bietet sich XML als Sprache für den Datenaustausch im Internet an.

8.7 Web-Programmierung

Abb. 8.24:

687

Ein Wetterbericht als XML-Dokument: Baum- und Textansicht

DTD – Document Type Definition Während die Bedeutung der Markierungen in HTML vordefiniert ist, kann man in XML eigene, problemspezifische Markierungen erfinden. Die Einführung solcher undefinierter Markierungen in XML-Dokumenten macht wenig Sinn, wenn man nicht auch Regeln für deren Gebrauch festlegt. Erst dann können XML-Dokumente, die diese Marken verwenden von Programmen bearbeitet oder zwischen Personen einer Gruppe ausgetauscht und verstanden werden. Eine Festlegung, welche Elemente in einem Wetterbericht auftauchen können, wie sie geschachtelt werden dürfen, welche Attribute erlaubt sind etc., kann in einer so genannten DTD (Document Type Definition) getroffen werden. Diese definiert die Struktur einer zugehörigen Klasse von Dokumenten und sollte alle verwendeten Elemente und deren Attribute beschreiben. Die DTD entspricht damit weitgehend den Grammatikregeln einer Programmiersprache und jedes XML-Dokument, das einer solchen DTD genügt, einem syntaktisch korrekten Programm. Ein XML-Dokument, das nicht nur wohlgeformt ist, sondern auch der DTD genügt, heißt gültig (engl.: valid). In einer DTD wird für jedes Element beschrieben, wie es aus anderen Elementen aufgebaut werden darf, und welche Art von Daten es enthalten kann. Dabei bedient man sich weitgehend den Konventionen der erweiterten Backus-Naur-Form (EBNF) bzw. der regulären Ausdrücke. So bestimmt z.B. , dass das Element Wetter eine Folge der Elemente Ort, Datum, Temperatur, Luftdruck, Besonderheiten ist. Letzteres ist durch das Fragezeichen als optional gekennzeichnet und ist selber

688

8 Das Internet

eine Folge von beliebig vielen der Elemente Hagel oder bewoelkt gemischt mit beliebigem Text (#PCDATA=parsed character data):

Das Element Hagel wird durch EMPTY als leeres Element festgelegt:

Weiterhin kann für jedes Element festgelegt werden, welche Attribute es haben darf und welche Werte für diese Attribute erlaubt sind. Die Zeile

verlangt, dass das Element Ort ein Attribut messpunkt haben muss, dessen Wert ein beliebiger String (CDATA=character data) sein darf. Das Element Luftdruck, hat ein Attribut einheit, dessen Wert entweder „mbar“, „Hpa“ oder „F“ sein darf. Als Voreinstellung ist „mbar“ angegeben:

Eine DTD kann im Vorspann eines XML-Dokuments eingefügt werden. Dazu benötigt dieses eine Zeile , wobei zwischen den eckigen Klammern die in der DTD gezeigten Definitionen stehen. Üblicherweise wird jedoch die DTD als eigene Datei gespeichert und jedes XML-Dokument, das ihren Regeln genügt, verweist auf diese Datei, die entweder auf dem gleichen System liegt, wie in dem obigen Beispiel oder an irgendeiner Stelle im Internet. Im Falle unserer Wetter-DTD könnten Hobby-Meteorologen in der ganzen Welt danach ihre Daten in dem gemeinsamen Format speichern, austauschen und gemeinsame Programme zur Bearbeitung verwenden.

Abb. 8.25:

Ein Dokumententyp (DTD) für Wetterberichte

8.7 Web-Programmierung

689

XML-Schema (XSD als Abkürzung für XML Schema Definition) Mit einer DTD kann die Struktur von XML-Dokumenten beschrieben werden. Da die Ausdrucksmöglichkeiten einer DTD von vielen Anwendern als etwas eingeschränkt angesehen wird, hat man XML-Schema als alternative detailliertere Beschreibungssprache für XMLDokumente eingeführt. In einem XML-Schema können Datentypen ähnlich wie in einer Programmiersprache definiert werden. Zusätzlich gibt es u.a. die Möglichkeit, den Inhalt von Elementen und Attributen auf Zahlenbereiche zu beschränken oder zulässige Texte durch reguläre Ausdrücke zu definieren. Ein XML-Schema ist selbst ein XML-Dokument und wird meist in einer Datei mit der Endung .xsd gespeichert. Die Anhänger dieser Beschreibungsart meinen, mit einer XSD komplexere Zusammenhänge als mit einer DTD beschreiben zu können und hoffen, dass DTDs irgendwann vollständig von XML-Schemata abgelöst werden. Allerdings sind XML-Schemata durch ihre erweiterten Möglichkeiten wesentlich komplexer und nicht so einfach ohne Hilfsmittel auszuwerten. Daher werden derzeit nach wie vor DTDs häufiger verwendet als XSDs. XML-Anwendungen Viele Werzeuge und Programmsysteme stehen für das Bearbeiten und Validieren von XMLDokumenten zur Verfügung, außerdem besitzen immer mehr Sprachen Programmierschnittstellen (API) für XML-Anwendungen. Vom einfachen XML-Parser, der feststellen kann, ob eine XML-Datei gültig (valid) bezüglich ihrer als DOCTYPE erklärten DTD ist, bis zu Entwicklungssystemen, die aus einer DTD automatisch Eingabemasken für passende XMLDokumente generieren. Vor allem aber gibt es bereits viele nützliche DTDs, die standardisierte Beschreibungen von strukturierten Daten in bestimmten Problembereichen festlegen. Beispiele sind die DTDs für XHTML, für MathML, einer Auszeichnungssprache für mathematische Formeln, oder für skalierbare Vektorgraphiken (SVG). Auch als Dokumentenformat ist XML im Vormarsch: DocBook ist eine XML-Sprache (d.h. eine DTD) für Bücher und technische Dokumentationen, die sich im Open Source Umfeld schnell verbreitet hat. Als Gegengewicht zu dem proprietären Microsoft Office-Formaten ist ein offenes Dokumentenformat ODF entstanden, das bereits von Open Office, Star-Office und KEdit genutzt wird. Im wesentlichen ist ein ODFDokument ein Zip-komprimiertes Archiv von XML-Dateien, die die Struktur und das Layout eines Buches sowie evtl. Bilder und weitere Medieninhalte enthalten. Unter den Unterstützern der OpenDocument Format Alliance, finden sich u.a. IBM, Oracle und Sun Microsystems. Demnächst wird ODF auch von IBM Workplace, dem Nachfolger von Lotus Notes und von WordPerfect verwendet werden. Der Vorteil eines solchen offenen Formats ist, dass Dokumente in Zukunft zwischen verschiedenen Bürosystemen ausgetauscht werden können. Besonders wichtig ist ein solcher Standard für die Langzeitarchivierung von Dokumenten. Aus diesem Grunde hat die Europäische Union vorgeschlagen, ODF zum ISO-Standard zu machen. Nachdem der US-Staat Massachusetts verfügt hat, dass ab 2007 offizielle Dokumente nur noch in PDF oder im ODF Format gespeichert werden dürfen, denkt auch Microsoft über die Unterstützung eines offenen Formates nach, will sich aber nicht an der Open Document Alliance beteiligen, sondern lieber ein konkurrierendes Format entwickeln.

690

8 Das Internet

XHTML HTML-Dateien sind nicht ohne weiteres wohlgeformte XML-Dokumente, aber es bedarf nur geringer Modifikationen, sie in solche zu verwandeln. Die Mischung von Text und strukturierten Bereichen ist auch in XML möglich, wie in dem Element des Wetter-Beispiels ersichtlich. Da XML case-sensitiv ist, sind z.B. und aus der Sicht von XML verschiedene Elemente. Bei einer Übersetzung von HTML nach XHTML, so heißt der XML-konforme Nachfolger von HTML, müssen sie in das kleingeschriebene übersetzt werden. Während in HTML oft nur der Anfang eines Bereiches markiert wird und der Browser selber entscheiden muss, wann dessen Ende erreicht ist, muss in XHTML eine explizite Endemarke gesetzt werden. Dies betrifft insbesondere Paragraphen die in

und

eingeschlossen werden müssen, Listenelemente
  • ...
  • , Zellen in Tabellen ... etc. Auch überlappende Bereiche müssen auf ähnliche Weise eliminiert werden. Kleinere Modifikationen betreffen Attribute, die in HTML keinen Wert besitzen müssen, wie z.B. in . Sie bekommen ihren eigenen Namensstring als Wert. Aus dem obigen Beispiel wird dann , weil zusätzlich noch das Attribut das auf das gegenwärtige Element verweist, in HTML name heißt, in XML und daher auch in XHTML jedoch id. Insgesamt ist die Übersetzung von HTML nach XHTML also unproblematisch. XML-Namensräume Jede DTD definiert eigene Elemente, und damit neue Namen für Knoten und Attribute. Oft ist es sinnvoll, vorhandene DTDs zu verwenden, evtl. auch verschiedene DTDs zu kombinieren oder zu erweitern. Im Prinzip spricht nichts dagegen, existierende DTDs zu vereinigen, außer der Tatsache, dass Namenskonflikte entstehen können, wenn ein Name in mehreren DTDs definiert worden ist. Vereinigt man zum Beispiel eine DTD zur Beschreibung von Rechnungen und eine DTD zur Beschreibung von Adressen, so könnte zweimal ein Element vorkommen – einmal gedacht als Rechnungsnummer, einmal als Hausnummer. Aus diesem Grund kann man für jede der geladenen DTDs einen Namenspräfix definieren, den man mit Doppelpunkt getrennt dem Elementnamen voranstellt. Auf diese Weise entsteht ein eindeutiger Elementname, dessen Definition über das Präfix in der richtigen DTD gesucht werden muss. Im folgenden Beispiel soll ein XHTML-Dokument sowohl eine Formel x = 5 im MathML-Format, als auch eine Ellipse im SVG-Format enthalten. Im Wurzelelement werden drei Namensraumpräfixe, xhtml, mml und svg für die DTDs von XHTML, MathML und SVG definiert. Diese werden den verwendeten Elementen zur eindeutigen Unterscheidung vorangestellt. Das betrifft auch bereits das Wurzelelement, das konsequenterweise heißt:

    8.7 Web-Programmierung

    691



    XHTML mit MathML und SVG

    Jetzt eine Gleichung

    x = 5

    ... und eine Ellipse



    Die DTDs, denen ein XML-Dokument genügt, dienen als Formatbeschreibungen, von denen aus man beliebige Anwendungen entwickeln kann, die den Inhalt passender XML-Dokumente interpretieren, verarbeiten, oder darstellen können. So kann die MathML-DTD einerseits als Grundlage für die Darstellung von Formeln in PDF-Dateien oder in Browsern dienen, andererseits als Schnittstelle zu einem Computer-Algebra-System, das mit den Formeln rechnen kann. Die Möglichkeit einer standardisierten Beschreibung von Struktur, Inhalt und Format eines Dokumentes macht XML zu einem geeigneten Kandidaten für den elektronischen Austausch von Daten für Transaktionen im Bereich electronic commerce. Es ist wahrscheinlich, dass XML dabei auch den in kleineren Betrieben nur zögerlich angenommenen EDI-Standard (electronic data interchange) ersetzen wird. XSLT Um aus XML-Dokumenten Informationen zu extrahieren und das Ergebnis wieder in XMLDokumenten darzustellen, kann man sich einer spezialisierten Transformationssprache XSLT bedienen. Diese ist aus der ursprünglich zur Darstellung von XML-Dokumenten entworfenen Sprache XSL (eXtensible Style Sheet Language) hervorgegangen. XSLT liegt seit November 2005 in Version 2.0 als Empfehlung des W3C vor und kann beliebige Transformation von XML-Dokumenten beschreiben.

    692

    8 Das Internet

    Bei einer XSLT-Transformation wird das Originaldokument nicht verändert, es dient nur als Quelle für die Transformation. Ein XSLT-“Programm“ besteht vorwiegend aus Zuordnungen von Mustern und Templates. Wenn bei einer Baumwanderung durch das Originaldokument eines der Muster angetroffen wird, soll die im zugehörigen Template angegebene Ausgabe erzeugt werden. XSLT gilt daher als deklarative Sprache. Als Programmiersprache ist es ohne spezielle Editoren ziemlich unhandlich, weil ein XSLT-Dokument – wer hätte es geahnt – selber ein XML-Dokument ist. Im folgenden Beispiel (siehe Abbildung 8.26) wird das erste Template durch das Muster match="/" für das Wurzelelement des zu übersetzenden XML-Dokumentes aktiviert. Das Ergebnis ist ein HTML-Dokument mit einer Überschrift „Das Wetter“. Danach werden durch den Befehl alle anderen Templates angewendet. Das Ort-Element des Quelldokuments aktiviert das zweite Template. Dabei wird ein Paragraph erzeugt, der das messpunkt-Attribut dieses Elements als Inhalt hat. Das dritte Muster passt auf das Temperatur-Element und gibt dessen Inhalt "." und dessen einheitAttribut zurück. Alle anderen Teilbäume erzeugen keine Ausgabe, was durch leere Templates erreicht wird. Im Browserfenster erkennen wir das Ergebnis der Transformation des Wetterberichts aus Abbildung 8.24 durch das besprochene XSLT-Dokument.

    Abb. 8.26:

    XSLT-Programm und Ergebnis einer Transformation

    Selbstverständlich kann man in XSLT auch Werte zwischenspeichern, Funktionen aufrufen, bedingte Transformationen und Schleifen verwenden. Die Bedingungen und Muster beziehen sich auf Knoten oder Mengen von Knoten die in der Sprachen XPATH spezifiziert werden können.

    8.7 Web-Programmierung

    8.7.8

    693

    DOM, Ajax und Web 2.0

    Das Document Object Model (DOM) definiert eine Programmier-Schnittstelle zur Bearbeitung von XML-Dokumenten. Diese Schnittstelle ist im Wesentlichen sprachunabhängig und Implementierungen in vielen Sprachen sind von verschiedenen Quellen verfügbar. Im Falle von Java stellt das W3C geeignete Interfaces bereit, die dann natürlich noch implementiert werden müssen. Zum einen benötigt man einen sogenannten DOMParser, um das OriginalDokument in ein sogenanntes Document Object gemäß der Schnittstelle zu transformieren. Die weitere Bearbeitung findet dann auf diesem Objekt statt: DOMParser dp = new DOMParser(); dp.parse("Wetter.xml"); Document doc = dp.getDocument(); In dem resultierenden Dokumentenbaum navigiert man ähnlich wie in einem Dateibaum. Auch hier gibt es den Begriff der aktuellen Position, von wo aus man zum Vaterknoten (parent) einem Bruderknoten (nextSibling, previousSibling) oder einem Kindknoten (child) gelangen kann. Die Kindknoten kann man durch ihr id-Attribut, durch das Element, das sie repräsentieren, oder über ihre Reihenfolge ansprechen (firstChild, lastChild). Konsequenterweise bewegt man sich im Dokumentenbaum mit get-Operationen (getParent(), getFirstChild() etc.), erzeugt neue Elemente mit create-Operationen (createElement(), createTextNode(), createAttribute() ) und verändert den Baum mit entsprechenden set-Operationen. Obwohl Abbildung 8.24 es nahelegen könnte, gehören Attribute nicht eigentlich zu dem Dokumentenbaum, man kann also nicht zu einem Attribut navigieren. Attribute modifizieren lediglich den Elementknoten zu dem sie gehören. Man kann sie dennoch lesen und verändern oder Elemente um neue Attribute ergänzen. Die JavaScript-Implementierung des Document Object Model hat in der letzten Zeit zu einer neuen Klasse von interaktiven Web-Anwendungen geführt, die mit dem Begriff Web 2.0 gefeiert werden. Hierbei handelt es sich um Anwendungen, die das Web auf eine neue Art interaktiv machen. Während die Interaktion mit traditionellen Webanwendungen meist darin besteht, dass der Benutzer einen Link anklickt, oder ein Formular abschickt, worauf der Webserver dann mit einer neu aufgebauten Seite reagiert, kann man mit der neuen Technik Anwendungen realisieren, die ein sofortiges und stetiges Feedback benötigen. Eines der bekannteren Beispiele ist das Textverarbeitungssystem „Writely“, das sich bedienen lässt wie z.B. Microsoft Word. Obwohl das System nicht lokal, sondern auf einem Server writely.com läuft, hat der Benutzer, genau wie bei einem lokal installierten Programm, eine sofortige Reaktion auf jeden Tastendruck und auf jede Formatierungsanweisung etc. Analoge Anwendungen sind interaktive Tabellenkalkulationen, Kalender und Fotoalben. Sehr beliebt ist auch Google-Maps, ein Programm zur Darstellung von Landkarten und Satellitenbildern, die man verschieben, oder in die man hineinzoomen kann, ohne dass jedes Mal eine neue Seite geladen und übertragen werden muss. Für diese Sorte von Anwendungen wurde der Begriff „Web 2.0“ geprägt und als „Buzzword“ bereitwillig aufgenommen. Dabei handelt es sich weder um eine neue Version des Internets – es hat nie ein Web 1.0 gegeben – noch um eine grundlegend neue Erfindung, sondern eher um

    694

    8 Das Internet

    eine Kombination bekannter Techniken, mit denen durchaus pfiffige interaktive Web-Anwendungen realisiert werden können. Der solchermaßen unscharfe Begriff Web 2.0 wird in gewissen Bereichen von der Firma O’Reilly beansprucht, die ihn in Publikationen und in einer Konferenzreihe popularisiert und rechtlich geschützt hat. Aus technischer Sicht wird mit Web 2.0 oft eine Anwendung bestimmter, bereits länger existierender Technologien umschrieben. Die wichtigste dieser Technologien wird auch als Ajax bezeichnet und besteht selbst wiederum aus drei Komponenten: XML, DOM und JavaScript. Das Acronym Ajax steht für Asynchronous JavaScript and XML. XML und das DOM haben wir bereits angesprochen. Für eine Textverarbeitung, die auf dem Open Document Format basiert, also im wesentlichen auf XML, ist es natürlich, dass sie die DOM-Schnittstelle verwendet. Bei asynchronem JavaScript handelt es sich um eine über JavaScript realisierte asynchrone Interaktion zwischen WebServer und Client (sprich, dem Browser oder dem Benutzer). Asynchron bedeutet hier, dass der Datenaustausch auch zwischen den expliziten Benutzerinteraktionen stattfinden kann, damit z.B. ein Dokument formatiert werden oder ein höher aufgelöstes Satellitenbild automatisch nachgeladen werden kann. Eine zentrale Rolle spielt in diesem Zusammenhang das XMLHttpRequest-Objekt, das von neueren Browsern implementiert wird. Eine einfache Kommunikation, die die XMLHttpRequest-API verwendet, um eine XML-Datei zu laden und den Textinhalt in einer alert-Box darzustellen, zeigt das folgende Bild. Hier wird dem Ereignis ReadyStateChange eine Funktion zugeordnet, die den Textinhalt in einer Alert-Box anzeigt.

    Abb. 8.27:

    XMLHttpRequest-Objekt und Ausgabe

    Ajax-APIs und Entwicklungssysteme werden unter anderem von Google und von Yahoo zur Verfügung gestellt, so dass man die Anwendungen dieser Firmen für eigene Webseiten anpassen und nutzen kann. Ajax-Anwendungen finden sich in immer mehr Webseiten. Von Kritikern wird unter anderem bemängelt, dass dafür JavaScript eingeschaltet sein muss oder dass der Zurück-Knopf des Browsers nicht mehr klar definiert ist.

    9

    Theoretische Informatik und Compilerbau

    Theoretische Informatik und Mathematik schaffen die Basis für viele der technischen Entwicklungen, die wir in diesem Buch besprechen. Die boolesche Algebra (S. 419 ff.) legt die theoretischen Grundlagen für den Bau digitaler Schaltungen, die Theorie formaler Sprachen zeigt auf, wie die Syntax von Programmiersprachen aufgebaut werden sollte, damit Programme einfach und effizient in lauffähige Programme übersetzt werden können, und die Theorie der Berechenbarkeit zeigt genau die Grenzen des Berechenbaren auf. Sie zieht eine klare Linie zwischen dem was man prinzipiell programmieren kann und dem was mit Sicherheit nicht von einem Rechner gelöst werden kann. In diesem Kapitel wollen wir einen kurzen Ausflug in die theoretische Informatik unternehmen. Wir werden sehen, dass diese Theorie nicht trocken ist, sondern unmittelbare praktische Anwendungen hat. Die Automatentheorie zeigt, wie man effizient die Wörter einer Programmiersprache festlegen und erkennen kann, die Theorie der kontextfreien Sprachen zeigt, wie man die Grammatik einer Programmiersprache definieren sollte, und wie man Übersetzer und Compiler dafür bauen kann. Letzterem haben wir ein eigenes Unterkapitel Compilerbau gewidmet. Ein fehlerfreies Programm ist aber noch lange nicht korrekt. Wünschenswert wäre es, wenn man feststellen könnte, ob jede Schleife auch terminiert. Dass diese und ähnliche semantischen Eigenschaften nicht automatisch geprüft werden können, ist eine der Konsequenzen der Berechenbarkeitstheorie. Mit dieser kann man zeigen, dass, wenn man einmal von Geschwindigkeit und Speicherplatz absieht, alle Rechner in ihren mathematischen Fähigkeiten identisch sind und damit das gleiche können bzw. nicht können. Schließlich hilft uns die Komplexitätstheorie, Aussagen über den Aufwand zu machen, den man zur Lösung wichtiger Probleme treiben muss.

    9.1

    Analyse von Programmtexten

    Programme sind zuerst einmal Texte, also Folgen von Zeichen aus einem gewissen Alphabet. Die ersten Programmiersprachen erlaubten nur Großbuchstaben, Klammern und Ziffern. In Pascal und C sind auch Kleinbuchstaben und Sonderzeichen, wie _ , +, ( , ), >, : , möglich und in Java sind sogar Unicode-Zeichen, insbesondere auch ä, ö und ü erlaubt. Die Menge aller Zeichen, die in einem Programm einer gewissen Programmiersprache vorkommen dürfen,

    696

    9 Theoretische Informatik und Compilerbau

    nennt man ihr Alphabet. Aus diesem Alphabet definiert man zunächst einmal die Wörter, aus denen die Sprache aufgebaut werden soll. Im Falle von Java oder Pascal sind dies u.a. Schlüsselwörter (while, do, for, if, else, ...), Sonderzeichen (+, -, *, = , ... ), benutzerdefinierte Bezeichner ( testFunktion, betrag, _anfangsWert, x37, r2d2 ) und Konstanten. Unter letzteren unterscheidet man noch Integer-Konstanten ( 42, 386, 2004) von Gleitkommazahlen (3.14, 6.025e23, .5 ). Ein Compiler für eine Programmiersprache muss als erstes prüfen, ob eine vorgelegte Datei ein syntaktisch korrektes Programm enthält. Ihm liegt der Quelltext als String, also als eine Folge von Zeichen vor. Die Analyse des Textes zerfällt in zwei Phasen – die lexikalische Analyse und die syntaktische Analyse. Dies entspricht in etwa auch unserem Vorgehen bei der Analyse eines fremdsprachlichen Satzes: In der ersten Phase erkennen wir die Wörter, aus denen der Satz besteht – vielleicht schlagen wir sie in einem Lexikon nach – und in der zweiten Phase untersuchen wir, ob die Wörter zu einem grammatikalisch korrekten Satz zusammengefügt sind.

    9.1.1

    Lexikalische Analyse

    Die erste Phase eines Compilers nennt man lexikalische Analyse. Dabei wird der vorgebliche Programmtext in Wörter zerlegt. Alle Trennzeichen (Leerzeichen, Tab, newLine) und alle Kommentare werden entfernt. Aus einem einfachen PASCAL-Programm, wie PROGRAM ggT; BEGIN x := 54; y := 30; WHILE not x = y DO IF x > y THEN x := x-y ELSE y:= y-x; writeln('Das Ergebnis ist: ',x) END . wird dabei eine Folge von Token. Dieses englische Wort kann man mit Gutschein übersetzen. Für jede ganze Zahl erhält man z.B. ein Token num, für jeden Bezeichner ein Token id, für jeden String ein Token str. Gleichartige Token stehen für gleichartige Wörter. Andere Token, die in dem Beispielprogramm vorkommen, sind eq (=) , gt (>), minus (-), assignOp( := ), klAuf ( ( ), klZu( ) ), komma ( , ), semi (;), punkt (.). Jedes Schlüsselwort bildet ein Token für sich. Nach dieser Zerlegung ist aus dem Programm eine Folge von Token geworden. Damit ist die erste Phase, die lexikalische Analyse, abgeschlossen. Im Beispiel hätten wir: program id semi begin id assignOp num semi id assignOp num semi while not id eq id do if id gt id then id assignOp id minus id else id assignOp id minus id semi id klAuf str komma id klZu end punkt

    9.1 Analyse von Programmtexten

    697

    Aus dem Input erzeugte Token

    x:=0;

    do

    num

    Die wichtigsten UML-Diagrammtypen sind: (a) für die statische Modellierung •

    Klassenstruktur-Diagramm (class structure diagram): Darin werden Klassen, ihre Merkmale (Attribute und Operationen) sowie ihre Assoziationen (= Beziehungen untereinander) dargestellt (vgl. Abb. 12.9). Kunde Kunden-Nr: Name: Vorname: Geb-Datum: Adresse:

    int String String Date String

    anzeigen () loeschen () aendernAdr (nAdr: Adresse)

    1

    besitzt >

    Konto 0.. ∗

    hat Vollmacht > 0.. ∗ 0.. ∗

    Vollmacht Beginn-Datum: Date Ende-Datum: Date loeschen ()

    Abb. 12.9:

    UML-Klassenstruktur-Diagramm

    Konto-Nr: int Art: String Eroeff-Datum: String Kontostand: Betrag anzeigen () loeschen () buchen (btr: Betrag)

    848 •

    12 Software-Entwicklung

    Bei der Vollmacht handelt es sich um eine Assoziation, die selbst wieder Klasseneigenschaften hat, d.h. zu der z.B. eigene Objekte als Exemplare gebildet werden können. Klassen können (u.a.) generalisiert bzw. spezialisiert werden (vgl. Abbildung 12.10).



    Konto

    Sparkonto

    Girokonto

    Depotkonto

    Zinssatz: Betrag

    Depot-Geb: Betrag

    berechneZins ( Beginn-Datum: Date Ende-Datum: Date)

    gutbuchen ( wp: Wertpapier stueck: int)

    Abb. 12.10: Generalisierung/Spezialisierung von Klassen

    (b) für die dynamische Modellierung • •

    Anwendungsfall-Diagramm (use case diagram): Dies sind sehr einfach aufgebaute Diagramme, die die Interaktion verschiedener Aktoren im Anwendungssystem (wie z.B. Benutzer oder Operateure) mit den einzelnen Anwendungsfällen darstellen. Sequenz- oder Interaktions-Diagramm (sequence / interaction diagram): Dieses dient dazu, exemplarisch den Ablauf einer Operation darzustellen. Da diese in der Regel zum Aufruf weiterer Operationen führt, sind die Sequenzdiagramme besonders gut geeignet, die Interaktion mehrerer Objekte dazustellen. ab1: AuftragsBearbeiter

    % %

    % %

    Order ausführen: Wenn Kaufpreis x gedeckt, dann abbuchen (x) von Konto kt1, gutbuchen (w,y) y Stück von Wertpapier w auf Depot dp1, Ausführung bestätigen (an Kunden): “Order ausgeführt” sonst Meldung: “Kaufpreis nicht gedeckt”

    Abb. 12.11: Sequenz-Diagramm

    Order ausführen

    kt1: Konto

    dp1: Depotkonto

    abbuchen (x) gutbuchen (w,y)

    Ausführung bestätigen

    12.5 Objektorientierte Analyse und Modellierung • •

    849

    Kollaborations-Diagramm (collaboration diagram): Dieses beschreibt ebenfalls die Interaktion von Objekten und bildet somit eine Alternative zum Sequenzdiagramm – allerdings ohne die explizite Darstellung der zeitlichen Dimension. Zustands-Diagramm (statechart diagram). Damit kann man den Lebenszyklus der Objekte einer gegebenen Klasse anhand von Zuständen und Zustandsübergängen beschreiben und u.a. Operationen für die Klassendefinition ableiten bzw. deren Notwendigkeit überprüfen. Art = Kauf and Kurs > lim Limit setzen / Limit = lim

    erteilen

    erteilt stornieren

    stornieren

    limitiert Art = Kauf and Kurs ≤ lim

    ausführen

    abgeschlossen

    ausführbar

    Abb. 12.12: Zustands-Diagramm



    Aktivitäts-Diagramm (activity diagram): Hier bilden die Aktivitäten (= Operations-Ausführungen) die Knoten und ihre zeitliche Verknüpfung die Kanten des Diagramms. Aktivitäten können sequentiell (nacheinander), parallel (d.h. voneinander unabhängig) oder bedingt (von der Erfüllung einer Bedingung abhängig) ablaufen.. [Kunde erteilt Order] Order erfassen

    Kurs auf Limit lim prüfen

    [Art = Kauf and Kurs > lim]

    Limit überprüfen

    [Art = Kauf and Kurs ≤ lim] Order ausführen Order stornieren Konten buchen

    Abb. 12.13: Aktivitäts-Diagramm

    850

    12 Software-Entwicklung

    Wie diese kurzen Charakterisierungen bereits zeigen, sind die Anwendungsbereiche für die verschiedenen Arten von UML-Diagrammen nicht disjunkt, sondern überlappen sich – teilweise sogar in beträchtlichem Maße. Es ist also wenig ratsam, unbesehen alle Diagrammarten zu übernehmen und zu versuchen, jede Software-Entwicklung durch Diagramme aller Arten zu dokumentieren. Die UML hat sich in den letzten Jahren – teilweise sogar über den reinen Informatik-Bereich hinaus – weit verbreitet und ist heute de facto zur Standard-Modellierungssprache geworden. Die Version UML 2.0 hat zu einem stärkeren Zusammenwachsen der verschiedenen Diagrammarten und der darin verwendeten Elemente, aber auch zu einer größeren Auffächerung, vielen Erweiterungen und neuen Kombinationsmöglichkeiten geführt. Damit hat sich die Einsatzbreite und -vielfalt, aber auch die Komplexität und der Einarbeitungsaufwand für die UML erhöht.

    12.5.3

    Software-Architektur

    Das Paradigma der Objektorientierten Systementwicklung ist eng mit der Zielsetzung verbunden, wiederverwendbare Software-Bausteine zu schaffen, zu denen auch Entwürfe und Analysen gehören können. Um solche Bausteine unabhängig voneinander entwickeln und aneinander anpassen zu können, ist man an einer grundsätzlichen Festlegung der Struktur von OO-Systemen und an einer Standardisierung der Schnittstellen-Beschreibungen interessiert. Fragestellungen dieser Art werden unter dem Schlagwort Software-Architektur behandelt. Da strukturelle Betrachtungen unter sehr unterschiedlichen Gesichtpunkten erfolgen können, hat der Architekturbegriff eine große Bedeutungsvielfalt erlangt. Eine erste, relativ einfache Betrachtung orientiert sich an den Benutzt-Beziehungen der einzelnen Bausteine und führt (bei diszipliniertem Gebrauch solcher Beziehungen) zu einer Schichtenstruktur: Bausteine oberer Schichten benutzen Bausteine tieferer Schichten – aber nicht umgekehrt.

    Benutzer-Schnittstelle Dialog

    Batch

    Anwendungskern

    Fehler- und Ausnahmebehandlung

    Datenverwaltung Abb. 12.14: Einfaches Schichtenmodell nach Brössler/Siedersleben

    12.5 Objektorientierte Analyse und Modellierung

    851

    In der Praxis kommen weitere, komplexere Zusammenhänge hinzu. So wurde beim Softwarehaus sd&m unter dem Namen Quasar (Qualitäts-Standard-Architektur) ein Architekturmodell entwickelt, dem die strikte Trennung von Anwendungsbezogenen und technisch beeinflussten Systemteilen (A-Software und T-Software) zugrunde liegt. Für die Struktur verteilter OO-Systeme hat die Object Management Group (OMG) unter dem Namen CORBA (= Common Object Request Broker Architecture) einen Standard definiert. Diesem liegt die Metapher eines Objekt-Maklers (object request broker, technisch gesehen einer Art Software-Bus) zugrunde, dessen Aufgabe darin besteht, Anforderungen von Baustein-Nachfragern mit denen von Baustein-Anbietern zur Deckung zu bringen. Um Schnittstellen-Beschreibungen zu vereinheitlichen, wurde mit der Interface Definition Language (IDL) eine eigene Sprache definiert. Neben CORBA haben sich firmenspezifische Standards wie Java Enterprise Beans, (EJB, Firma Sun) und DCOM/COM (Firma Microsoft) verbreitet.

    12.5.4

    Entwurfsmuster und Frameworks

    Unter Software-Entwicklern ist es seit langem bekannt, dass ein großer Teil der Arbeit immer wieder zur Erledigung gleicher oder sehr ähnlicher Aufgabenstellungen wie der Definition von Listen und Bäumen und ihren Operationen oder der Aufgabenverteilung zwischen miteinander kooperierenden Klassen erbracht werden muss. Hier lässt sich durch die Verwendung von Entwurfsmustern (design patterns) viel Arbeit sparen. E. Gamma et al. haben in ihrem gleichnamigen Buch eine Reihe von solchen Mustern zusammengestellt. Zu den bekanntesten Beispielen gehören das aus der Smalltalk-Umgebung stammende MVC-Muster (wobei MVC für Model-View-Controller steht und damit für drei Typen von Klassen, die für verschiedene Aufgabenbereiche in einem OO-System zuständig sind), das Fassaden-Muster (für die Zusammenfassung von Leistungen mehrerer Klassen zu einer größeren, paketartigen Einheit und deren Angebot in einer „Fassade“) sowie das Factory-Muster (für die Generierung und Verwaltung einer Menge von gleichartigen Objekten durch ein so genanntes Factory-Object). Die Idee, Muster für eine breitere Verwendung zur Verfügung zu stellen, wurde auch auf die Analysephase ausgedehnt und hat zur Definition von Analysemustern geführt (vgl. dazu das Buch von Fowler). Eine weitere Möglichkeit, einmal erarbeitete Ergebnisse bei der Software-Konstruktion wiederzuverwenden, besteht darin, aus einem bereits eingesetzten und bewährten Stück Software die anwendungsspezifischen Teile herauszunehmen und das verbleibende Programmgerüst als Framework zur weiteren Verwendung zur Verfügung zu stellen. Typisch für solche Gerüste ist, dass sie die zu erstellenden anwendungsspezifischen Bausteine benötigen, um wieder lauffähig zu werden ( „Don't call us, we call you!“ )

    12.5.5

    Aspekt-orientierte Entwicklung

    Große Software-Systeme sind oft deshalb besonders schwer zu durchschauen, weil bestimmte Leistungen, die sie erbringen, im Code nicht an einer Stelle konzentriert sind, sondern – bedingt durch die Programmlogik oder den späteren zeitlichen Ablauf – auf viele Codestellen verstreut sind. Beispiele für solche querschneidenden (engl.: crosscutting) Aspekte sind die

    852

    12 Software-Entwicklung

    Fehlerbehandlung oder Anweisungen zur Protokollierung der jeweils durchgeführten Aktionen eines Programms. Zum Verständnis kann es erheblich beitragen, wenn solche Aspekte herausgelöst, an einer Stelle programmiert und dokumentiert werden und der betreffende Aspekt-Code dann später automatisch in das eigentliche Programm hineingewoben wird (engl.: weaving). Ein möglicher Nachteil ergibt sich daraus, dass der Programmierer eventuelle unbeabsichtigte Auswirkungen des Einwebens und damit die endgültige Form des Programms i.a. nicht sieht. Die Aspekt-orientierte Programmierung (AOP) wird durch Techniken und Werkzeuge für das Entwickeln von Aspekt-Code, seine Verankerung und das automatische Einweben unterstützt.

    12.5.6

    Modell-getriebene Architektur

    In der kommerziellen und in weiten Teilen der technisch-wissenschaftlichen Software-Entwicklung ist die Modellierung heute zur zentralen Aufgabe geworden. Modelle spiegeln einerseits als Nachbilder den Anwendungsbereich der Software (mit den dort erhobenen Anforderungen) wider, andererseits dienen sie als Vorbilder für die Programmentwicklung und die nachfolgende Implementierung der Software im Anwendungsbereich. Die Object Managment Group (OMG), ein internationales Firmenkonsortium, das die Pflege und Weiterentwicklung der Objekt-Technologie vorantreibt, hat unter dem Namen ModelDriven Architecture (MDA) eine Initiative gestartet, die diese zentrale Stellung der Modelle im Entwicklungsprozess unterstreichen und weiter stärken soll. Im Mittelpunkt der MDA steht das Plattform-unabhängige Modell (platform independent model, PIM), das den Anwendungsbereich und die dort bestehenden Anforderungen dokumentiert (vgl. Abbildung 12.15). Aus diesem Modell heraus sollen – ggf. alternativ für unterschiedliche Zielumgebungen – Plattform-spezifische Modelle (platform specific models, PSM's) generiert, d.h. weitgehend mit automatischen Mitteln erzeugt werden. In lauffähigen Code umgesetzt, können diese in der Zielumgebung zum Einsatz kommen und die herkömmliche Programmierarbeit weitgehend ersparen.

    Anforderungen (z.B. use cases) Modellierung

    PIM (Teil-) Automatisierte Transformationen

    PSM_1

    PSM_n

    Abb. 12.15: Modell-getriebene Software-Entwicklung

    12.6 Projekt-Management

    853

    Die Modelle sind für die Entwickler ständig verfügbar, können aufgesucht, eingesehen und entweder von Hand oder mit Hilfe automatisierter Modelltransformationen bearbeitet, weiterentwickelt und auf spezielle Zielsetzungen angepasst werden. Der oben erwähnte Transformationsansatz kommt hier also in der Form von so genannten Query-View-Transform (QVT)Techniken erneut zum Tragen. Jüngste Überlegungen gehen dahin, Modelle für ganze Anwendungsbereiche zu standardisieren und über viele Projekte in diesem Anwendungsbereich mehrfach nutzbar zu machen. Wenn solche standardisierten Modelle bestimmte formale Anforderungen erfüllen, bezeichnet man sie als Ontologien und den darauf beruhenden Entwicklungsansatz als Ontologie-basierte Software-Entwicklung.

    12.6

    Projekt-Management

    Modernes Software Engineering unterscheidet sich von der bloßen Anwendung traditioneller Programmiermethoden nicht zuletzt dadurch, dass es ein systematisches Projekt-Management voraussetzt und diesem einen hohen Stellenwert einräumt. Wenn mehrere Entwickler oder womöglich ganze Hundertschaften gemeinsam an einem geistigen Produkt erfolgreich arbeiten sollen, ist eine effektive Projektführung unverzichtbar. Zu ihren Aufgaben gehört die stetige Motivation, Koordination, Information und Lenkung der Entwickler, die Steuerung ihrer Tätigkeiten und Arbeitsergebnisse, die Abstimmung mit Auftraggebern und sonstigen Betroffenen sowie die ökonomische und qualitative Zielsetzung und Erfolgskontrolle des Projekte. Diese Aufgaben erfordern eine besondere Qualifikation der damit Betrauten und einen erheblichen Zeitaufwand. Die Aufgaben des Projekt-Managements lassen sich zeitlich grob in die folgenden drei Teilbereiche gliedern: • • •

    Projektinitialisierung und -planung, Projektsteuerung und -koordination, Projektabschluss und -bericht.

    12.6.1

    Projektinitialisierung und -planung

    Ein Software-Projekt wird initialisiert, indem ein Projektleiter benannt, ein Projektteam (in möglicherweise zunächst noch kleiner Besetzung) aufgestellt und diesem ein Projektauftrag erteilt wird. Zu den ersten Aufgaben des Projektleiters gehört die Aufstellung des Projektplans, der sich in einen Grobplan für das Gesamtprojekt sowie Feinpläne für dessen einzelne Abschnitte (Phasen oder Entwicklungszyklen) gliedert. Die Feinpläne werden in der Regel nach und nach während des Projekts aufgestellt und nach Bedarf verfeinert.

    854

    12 Software-Entwicklung

    Dabei spielt das Vorgehensmodell, das oft vom Unternehmen oder vom Auftraggeber vorgegeben ist, einerseits die Rolle eines Musters für die Planung der Entwickler-Tätigkeiten, sollte aber andererseits dem Projektleiter noch genügend Spielraum lassen, um es an seine projektspezifischen Erfordernisse anzupassen (engl. tailoring). Zur Planung gehört die Zuteilung von Ressourcen (Personal, Werkzeuge, sonstige Betriebsmittel) für bestimmte Zeiträume, die Festlegung von Meilensteinen und Abschlussterminen sowie von Qualitätszielen und Maßnahmen, die das Erreichen der Ziele gewährleisten sollen. Eine der schwierigsten Aufgaben, die das Management im Vorfeld des Projekts oder in einem sehr frühen Projektstadium zu bewältigen hat, betrifft die Aufwandsschätzung für das Projekt. Sie wird vor allem dann notwendig, wenn der Auftraggeber die Nennung eines Festpreises für ein Software-Produkt verlangt, was heute auch für individuell entwickelte Software der Regelfall ist. Es gibt mittlerweile eine Reihe von Schätzverfahren (z.B. das function pointVerfahren von IBM oder das object point-Verfahren von H. Sneed), die um so zuverlässiger sind, je mehr dokumentierte Erfahrungswerte beim Auftragnehmer aus Vorprojekten vorliegen und bei der Schätzung mit berücksichtigt werden können.

    12.6.2

    Projektsteuerung und -koordination

    Während des Projektablaufs übernimmt das Management im Wesentlichen steuernde und koordinierende Aufgaben. Diese beziehen sich sowohl auf die Tätigkeiten der Entwickler als auch auf den Kontakt mit Auftraggebern, Anwendern und sonstigen Betroffenen. Ein Projekt effektiv zu steuern setzt vor allem voraus, stets gut über den aktuellen Projektstand informiert zu sein. Diesem Ziel dienen u.a. regelmäßige Projektsitzungen, fest etablierte Informationskanäle (z.B. Intranet und electronic mail), gemeinsam genutzte Werkzeuge zur Produktverwaltung sowie sorgfältig geplante und eingehaltene Maßnahmen zur Qualitätssicherung. Neben der ständigen, begleitenden Erfolgskontrolle gibt es meist einige vordefinierte Kontrollpunkte, die so genannten Meilensteine, an denen wesentliche (Zwischen-) Ergebnisse einer Qualitätsprüfung unterzogen werden. Von den Resultaten solcher Prüfungen hängt der weitere Verlauf des Projekts maßgeblich ab: ob es z.B. im Sinne der ursprünglichen Planung mit Folgetätigkeiten fortgesetzt werden kann, ob Nacharbeiten oder gar ein Zurücksetzen und Anknüpfen an frühere Zwischenergebnisse notwendig sind, ob eine verstärkte personelle oder Werkzeug-Unterstützung erforderlich wird etc. Neben den Steuerungs- und Koordinationsaufgaben nach innen hat der Projekt-Manager das Projekt nach außen – gegenüber Vorgesetzten, Auftraggebern, Anwendern oder Benutzern – zu vertreten. Dazu gehören Aufgaben wie z.B. den Projektauftrag und mögliche weitere Zusatzaufträge inhaltlich abzustimmen, die Ressourcen-, Termin- und Qualitätspläne zu erstellen und ggf. anzupassen, notwendige Treffen oder Kooperationen mit Anwender- bzw. Benutzervertretern zu organisieren, zu den festgelegten Zeitpunkten Bericht zu erstatten, über eventuell auftretende Schwierigkeiten rechtzeitig zu informieren und jederzeit als Anlaufstelle zur Verfügung zu stehen.

    12.7 Software-Qualitätssicherung

    12.6.3

    855

    Projektabschluss und -bericht

    Zum Projektabschluss, aber häufig auch beim Erreichen bestimmter Meilensteine ist das Projekt-Management gehalten, Vorgesetzten, Auftraggebern oder sonstigen Kontrollinstanzen Bericht über den Projektstand zu erstatten. Diese Berichte sind nicht-technischer Natur und enthalten z.B. Angaben über den Projektverlauf, über den erbrachten Aufwand zur Einhaltung der Projekt- und Qualitätsziele bzw. über Abweichungen davon, wichtige Projektereignisse, erfolgte Maßnahmen zur Projektkontrolle und -steuerung, Kostenaufstellungen, Plan-Ist-Vergleiche etc.

    12.7

    Software-Qualitätssicherung

    Bei der Qualitätssicherung (kurz: QS) handelt es sich um einen weiteren, aus den verwandten Ingenieursdisziplinen entlehnten Begriff. Unter Software-Qualitätssicherung versteht man die Gesamtheit der vorbereitenden (konstruktiven) und auf erarbeitete bzw. bereits vorliegende Ergebnisse angewendeten (analytischen) Maßnahmen, die geeignet sind, die geforderte Qualität eines Software-Produkts, -Bausteins oder -Herstellungsprozesses zu erreichen oder zu erhalten. Die Qualität von Software lässt sich – darin unterscheidet sich Software nicht von anderen Produkten – nur dann zuverlässig beurteilen und bewerten, wenn sie von vornherein in Form von Qualitätszielen oder Qualitäts-Anforderungen in überprüfbarer (möglichst: quantifizierbarer) Form festgelegt ist. Daher beginnt die Qualitätssicherung nicht bei der Prüfung, sondern bei der Festlegung der Qualitäts-Anforderungen zu Beginn des Projekts. Solche Anforderungen können z.B. lauten: • • • •

    „Die Antwortzeiten dürfen beim Normalbetrieb nicht länger als 2 s sein“. „Der Speicherbedarf für das Gesamtsystem darf im laufenden Betrieb 16 MB Hauptspeicher nicht übersteigen“. „Das Gesamtsystem darf im laufenden Betrieb im Mittel nicht länger als 1 Stunde pro 10.000 Betriebsstunden ausfallen“. „Der Code ist zu 100 % C1-getestet.“ (Anmerkung: Dabei handelt es sich um ein TestÜberdeckungsmaß, das sinngemäß besagt, dass jeder Programmzweig im Test mindestens einmal durchlaufen wurde.)

    Diese wenigen Beispiele zeigen bereits die große Bandbreite von Qualitäts-Kriterien, an denen sich die Anforderungen orientieren können. B. Boehm nennt z.B. die folgenden Kriterien: • • • • • •

    Korrektheit, Zuverlässigkeit, Modularität, Flexibilität, Elastizität, Interoperabilität, Testbarkeit, Änderbarkeit, Wiederverwendbarkeit, Wartbarkeit, Portabilität, Effizienz, Wirtschaftlichkeit, Durchsichtigkeit, Verständlichkeit, Integrität, Verwendbarkeit, Gültigkeit, Allgemeinheit, Dokumentation.

    856

    12 Software-Entwicklung

    Die Qualitäts-Anforderungen stehen also gleichberechtigt neben den funktionalen Anforderungen und führen zu Maßnahmen bzw. Aktivitäten der Manager, Entwickler und/oder eigens dafür abgestellter Spezialisten, die darauf zielen, diese Anforderungen zu erfüllen. Mögliche Maßnahmen teilen wir grob in zwei Kategorien ein: (a) konstruktive Maßnahmen zur Qualitätssicherung: • • • •

    einen Qualitäts-Plan aufstellen, der die Kriterien, ihre Relevanz und die daraus abgeleiteten Anforderungen enthält; einen Zeitplan für abzuhaltende Reviews, Inspektionen und sonstige QS-Prüfmaßnahmen aufstellen (in Abstimmung mit der Terminplanung für die Entwicklung); Richtlinien, Standards und Muster für die zu erbringenden Ergebnisse aufstellen, verbreiten und sicherstellen, dass sie auch verwendet werden; den Entwicklungsprozess begleiten, soweit notwendig dokumentieren und Qualitäts- und Terminpläne ggf. an die aktuellen Projekterfordernisse anpassen.

    (b) analytische Maßnahmen zur Qualitätssicherung: • •

    Reviews, Inspektionen und sonstige QS-Prüfmaßnahmen durchführen und dokumentieren; Aktionen, die als Ergebnis von Prüfmaßnahmen als notwendig erachtet wurden, in Gang setzen und verfolgen.

    Natürlich ist die letztgenannte Maßnahme bereits wieder „konstruktiv“, bezogen auf den darauffolgenden Projektabschnitt. Wir erwähnen sie aber ausdrücklich in diesem Zusammenhang, weil durch sie die Prüfmaßnahmen erst ihren Sinn erhalten. Software-Prüfverfahren zielen darauf, für ein gegebenes Stück Software nachzuweisen, dass es vorgegebenen Qualitäts-Anforderungen genügt. Solche Qualitäts-Anforderungen können z.B. die Genauigkeit der Ergebnisse (Abweichung innerhalb vorgegebener Grenzen), die Gestaltung der Benutzer-Schnittstelle (z.B. die Erfüllung fest vorgegebener ergonomischer Kriterien), die Zahl der maximal zu tolerierenden Ausfälle oder Fehlfunktionen oder die Qualität der mitgelieferten Dokumentation betreffen. Das zuverlässigste, aber auch teuerste und oft an Grenzen der Praktikabilität stoßende Prüfverfahren ist die Programm-Verifikation. Diese Technik wurde ausführlich im 2. Kapitel erörtert. Ein Stück Software zu verifizieren bedeutet, sein erwartetes funktionales Verhalten in einer formalen Spezifikation festzulegen und die Korrektheit des Programms mithilfe eines mathematisch-logischen Kalküls formal zu beweisen. Für sehr kleine bis kleine Programme sind wirksame Verifikationsverfahren seit langem bekannt. Diese auf mittelgroße bis große Programmsysteme auszudehnen und zum industriellen Einsatz zu bringen, stößt allerdings wegen der dabei überproportional steigenden Komplexität auf prinzipielle Schwierigkeiten. In der industriellen Praxis ist das wichtigste Prüfverfahren der Test, d.h. ein Programm wird gemäß einer Auswahl vorher spezifizierter Testfälle zum Ablauf gebracht und dabei mit Testdaten versorgt. Die Testergebnisse werden mit erwarteten Resultaten oder mit den Ergebnissen vorheriger Abläufe verglichen, und bei erkannten Fehlfunktionen werden Maßnahmen zur Fehlerbehebung (engl: debugging) eingeleitet. Je nachdem, ob ein Modul von außen, von seiner Schnittstelle (z.B. durch Aufruf seiner nach außen sichtbaren Operationen) oder im Hinblick

    12.7 Software-Qualitätssicherung

    857

    auf seine innere Funktionstüchtigkeit (z.B. die Ablauflogik) getestet wird, spricht man vom black-box- oder vom white-box-Test (oder genauer glass-box-Test). Neben den Computer-basierten Testverfahren spielen die Begutachtungsverfahren durch menschliche Gutachter eine nach wie vor wichtige Rolle beim Software-Prüfprozess. Zuweilen wird zwischen Reviews, Inspektionen und Walkthroughs unterschieden, doch sind hier die Grenzen eher fließend. Reviews folgen oft festgelegten formalen Regeln: Es gibt einen Moderator, den Autor eines Dokuments, Vorleser, Schriftführer und ggf. weitere Gutachter. Ziel ist es, durch eine gemeinsame Begutachtung Fehler und Inkonsistenzen aufzudecken und Lösungswege miteinander abzustimmen. Daher sind Reviews eine besonders effektive Qualitätsmaßnahme, wenn es um die Überprüfung und Abstimmung von Schnittstellen-Vereinbarungen (z.B. zwischen mehreren Modulen eines größeren Programmsystems) geht. Inspektionen laufen oft nach nicht so strengen formalen Regeln ab und beziehen sich vorwiegend auf Code oder Testergebnisse, aber es gibt auch Code-Inspektionsverfahren (wie das bei IBM von M. Fagan entwickelte Verfahren), die in einem ähnlich formalen Rahmen ablaufen wie Reviews. Beim Walkthrough wird im Gegensatz zur Code-Inspektion der Code nicht sequentiell (in der Aufschreibungs-Reihenfolge) gelesen, sondern in der Ausführungs-Reihenfolge, d.h. die menschlichen Begutachter „spielen Computer“. Will man die Qualität von Software quantitativ erfassen und bewerten, so ist dazu das Gebiet der so genannten Software-Metriken (software metrics) zu Rate zu ziehen. Dort werden Qualitätsmaße definiert, mit deren Hilfe man einem gegebenen Stück Software Werte auf einer Zahlenskala zuordnen kann, die mit der Erfüllung eines bestimmten Qualitätskriteriums korrespondieren. Antwortzeiten, Speicherbedarf und Testabdeckung sind Beispiele für direkt quantifizierbare Größen. Aber auch für Kriterien, für die sich keine unmittelbaren Zahlenwerte ablesen lassen, wie die Komplexität oder Interdependenz eines Bausteins, wurden Qualitätsmaße entwickelt. Ein Beispiel ist das Maß von McCabe für die innermodulare Komplexität eines Software-Bausteins. Metriken finden ihre Grenzen dort, wo statt objektiv messbarer Kriterien subjektive Einschätzungen den Ausschlag geben. Beispiele dafür sind die Aufgaben-Angemessenheit einer Lösung sowie die Benutzerfreundlichkeit, Änderbarkeit und Wartbarkeit eines Software-Produkts. Hier bieten Umfragen und Interviews eine Möglichkeit, die Qualität von Software aufgrund von individuellen oder von statistisch aufbereiteten Einschätzungen zu bewerten und zu vergleichen.

    12.7.1

    Qualitätsnormen und Zertifizierung

    Die vielfach beschworene Analogie zwischen der Software-Entwicklung und herkömmlichen Ingenieurdisziplinen legt die Idee nahe, Software-Produkte mit einem Zertifikat (analog zu einer TÜV-Plakette) zu versehen, um damit dem Käufer und Nutzer eine Garantie für deren Qualität zu bieten. Dies betrifft natürlich in allererster Linie die Fehlerfreiheit, d.h. eine Garantie dafür, dass die Software auch wirklich das tut, was sie zu tun verspricht. Hier kommt nun ein wesentlicher Unterschied zwischen einem herkömmlichen Industrieprodukt und einem

    858

    12 Software-Entwicklung

    Stück Software zum Tragen: Turing, Dijkstra und andere haben schon vor Jahrzehnten gezeigt, dass man im Allgemeinen (d.h. für ein beliebig vorgegebenes Computerprogramm) zwar die Anwesenheit von Fehlern (falls vorhanden), nicht aber deren Abwesenheit beweisen kann. Damit schließt sich ein allgemeines Qualitäts-Zertifikat für Software-Produkte allein schon aus theoretischen Erwägungen aus. Und auch für spezifische Problemstellungen, bei denen sich theoretisch ein Korrektheitsbeweis führen ließe, ist dies in der Praxis aus Komplexitätsgründen fast immer unmöglich. Aufgrund dieser Schwierigkeiten ist man bei der SoftwareQualitätssicherung schon seit längerem dazu übergegangen, neben der Produkt-Qualität auch die Qualität des Herstellungsprozesses zu bewerten. Daraus hat sich erfolgreich ein neuer Ansatz zur Zertifizierung im Software-Bereich entwickelt: Nicht das einzelne Produkt bekommt einen Prüfstempel, sondern ganze Unternehmen oder Unternehmensteile. Um ein solches Zertifikat zu bekommen, muss sich die betreffende Institution einer umfangreichen Bewertungs-Prozedur unterziehen. Diese besteht in der Regel aus Anteilen der Selbstbewertung (gemäß einem vorgegebenen Bewertungs-Plan) und der Fremdbewertung durch akkreditierte, unabhängige Gutachter. Modelle und Verfahren für die Bewertung von Software-produzierenden Institutionen sind vielerorts entwickelt und erfolgreich angewendet worden. Zwei der bekanntesten Ansätze sind das Reifegrad-Modell (Capability Maturity Model – CMM) des US-amerikanischen Software Engineering-Instituts (SEI) und die Normenserie ISO 9000. Im Zentrum des CMM steht eine 5-stufige Bewertungsskala (vgl. Abbildung 12.16), die den Maßstab für die Begutachtungs-Aktivitäten liefert. Stufe

    Charakterisierung

    Haupt-Problemgebiete

    5 Optimizing

    Ständige Prozessverbesserung auf Basis der ermittelten Parameter und Problemanalysen

    Automatisierung der Entwicklung

    4 Managed

    (quantitativ) Wichtige Prozessparameter werden regelmäßig ermittelt und analysiert.

    Neue Technologien, Problemanalyse und -vermeidung

    3 Defined

    (qualitativ) Prozesse sind definiert und eingeführt.

    Prozessmessungen, Problemanalyse, Quantitative Q.-Pläne

    2 Repeatable

    (intuitiv) Prozesse sind unter gleichen Bedingungen wiederholbar, hängen aber von einzelnen Personen ab. PM ist vorhanden.

    Schulung, Review-,Testtechniken, Prozessorientierung

    1 Initial

    (ad hoc, chaotisch) Es werden keine spezifischen Anforderungen an das Unternehmen gestellt.

    Projektmanagement (PM), Projektplanung, Konfigurations-Management

    Abb. 12.16: Die Bewertungsskala des Capability Maturity Model – (CMM)

    12.8 Werkzeuge und Programmierumgebungen

    12.8

    859

    Werkzeuge und Programmierumgebungen

    Software-Methoden sind so lange in ihrer Wirksamkeit eingeschränkt, wie sie nicht durch Werkzeuge unterstützt werden. Jeder Software-Entwickler ist heute den Umgang mit vielfältigen Werkzeugen gewöhnt: Editoren, grafische Entwurfs- und Modellierungshilfen, Analysatoren, Konsistenzprüfer, Generatoren, Compiler, Verifikatoren, Testsysteme, Debugger, Dokumentenerzeuger, Datenlexika und Projektbibliotheken sind nur eine Auswahl möglicher Werkzeuge, die den Entwickler bei seinen verschiedenen Tätigkeiten unterstützen.

    12.8.1

    Klassifizierung von Werkzeugen

    Um sich in der Werkzeug-Landschaft einen gewissen Überblick zu verschaffen, kann man Werkzeuge nach verschiedenen Gesichtspunkten klassifizieren. Ein erstes nahe liegendes Kriterium bezieht sich auf die unterstützten Tätigkeiten. Danach lassen sich unterscheiden: •



    Tätigkeitsspezifische Werkzeuge: Diese unterstützen den Entwickler vorwiegend bei einer spezifischen Tätigkeit, z.B. beim Spezifizieren, Programmieren oder Testen. Beispiele solcher Werkzeuge sind ein grafischer Editor, ein Analysator für eine Spezifikationssprache, ein Compiler, ein Testsystem oder ein Debugger. Hierhin gehören auch spezifische Werkeuge für das Projekt-Management wie Planungsinstrumente oder Hilfsmittel zur Aufwandsschätzung. Tätigkeitsübergreifende Werkzeuge: Diese stehen dem Entwickler oder Manager über weite Strecken seiner Tätigkeit zur Verfügung, möglicherweise über den gesamten Entwicklungsprozess hinweg. Beispiele aus dieser Gruppe von Werkzeugen sind: (Text-) Editoren, Dokumentenerzeuger, Datenlexikon und Projektbibliothek.

    Ein weiteres Unterscheidungskriterium für Werkzeuge betrifft die Arbeitsweise des Entwicklers. Denert unterscheidet grundsätzlich zwei verschiedene Arbeitsweisen: die dokumenten-orientierte und die transaktionsorientierte. •



    Dokumentenorientiert zu arbeiten, bedeutet für einen Entwickler, dass er jeweils an einem Dokument (z.B. einer Spezifikation, einem Programm, einem Bericht) arbeitet und dieses dann abschließt. Typische Werkzeuge dafür sind Editor, Analysator, Compiler, Dokumentenersteller und als Verwaltungswerkzeug die Projektbibliothek. Dagegen ist die transaktionsorientierte Arbeitsweise mit der Tätigkeit eines Sachbearbeiters in einem Bank- oder Versicherungsunternehmen zu vergleichen: Er arbeitet nacheinander Transaktionen an kontinuierlich weiterentwickelten Produkten ab wie das Beschreiben eines Attributs in einem Datenmodell oder den Entwurf einer Elementarfunktion, das Beseitigen eines Fehlers oder das Fortschreiben eines Berichts. Werkzeuge für diese Arbeitsweise findet man oft unter dem Stichwort CASE (Computer Aided Software Engineering).

    860

    12 Software-Entwicklung

    Eine dritte Klassifizierungsmöglichkeit lässt sich aus dem Grad der Unterstützung ableiten: Diese reicht von der Textspeicherung und -bearbeitung über die Dokumentenverwaltung, Ausgabe von Berichten aufgrund von Teilanalysen, syntaktische und semantische Überprüfung bis zur Code-Erzeugung, Entscheidungshilfe und Entscheidungsübernahme durch das Werkzeug. Bei der folgenden, kurzgefassten Darstellung wollen wir uns an den Tätigkeiten des Software-Entwicklers bzw. -Managers orientieren. Dabei soll es weniger darum gehen, konkrete Produkte aufzuzählen und einander gegenüberzustellen, sondern eher darum, anhand der oben besprochenen Methoden typische Werkzeugfunktionen zu erläutern. Für ein vertieftes Studium verweisen wir auf die reichhaltige Literatur zu diesem Thema.

    12.8.2

    Werkzeuge zur Analyse und Modellierung

    Für traditionelle Methoden wie Structured Analysis (SA), Entity/Relationship-Modellierung (E/R) sowie für die objektorientierte Analyse und Entwurf (OOA/OOD) gibt es eine reichhaltige Palette von Werkzeugen. Diese unterstützen das Erstellen, Editieren und Verwalten von Diagrammen und oft auch die dafür notwendige Datenverwaltung. Bekannte Werkzeuge für die objektorientierte Analyse und Modellierung sind z.B. ObjektiF, Innovator, Paradigm Plus, Software through pictures, Together und Rational Rose. Neben den Methoden (und Notationen) von Coad/Yourdon, Rumbaugh (OMT) und Grady Booch wird heute fast ausnahmslos die Unified Modeling Language (UML) unterstützt.

    12.8.3

    Werkzeuge für Spezifikation und Entwurf

    Während für die Mehrzahl der eingesetzten Analyse-Werkzeuge die transaktionsorientierte, zeichnerische Arbeitsweise und damit die Grafikunterstützung im Vordergrund stehen, spielen für viele Spezifikations- und Entwurfswerkzeuge die dokumentenorientierte Arbeitsweise (vgl. S. 859) und die sprachlichen Beschreibungsmittel eine weit größere Rolle. Spezifikationssprachen gibt es viele, ein bekanntes Beispiel aus dem Forschungs- und Entwicklungsbereich ist die Sprache Z. In der Praxis findet man bei vielen Unternehmen mit eigener Software-Entwicklung eine oder mehrere „selbstgestrickte“ Entwurfs- und Spezifikationssprachen, oft lokal auf bestimmte Bereiche oder Abteilungen begrenzt. Die zugehörigen Werkzeuge umfassen einen Analysator, der die syntaktische Richtigkeit der Spezifikationen und ihre Konsistenz (auch über Modulgrenzen hinweg) überprüft, ihren Inhalt in einem Schnittstellen-Lexikon festhält und ggf. einen Code für eine oder mehrere gegebene Programmiersprachen oder Testsequenzen für spätere black-box-Tests generiert. Abbildung 12.17 zeigt die genannten Funktionen im Zusammenhang als Teile eines Spezifikations-Werkzeugs.

    12.8 Werkzeuge und Programmierumgebungen

    861

    View Auswertungen Prüfungen

    Text

    Analysator

    Schnittstellenlexikon

    CodeGenerator

    TestfallGenerator

    Abb. 12.17: Funktionen eines Spezifikations-Werkzeugs

    12.8.4

    Programmier-Werkzeuge

    Programmier-Werkzeuge gibt es mindestens so viele wie Programmiersprachen (und noch einige mehr), so dass wir uns hier auf ein paar grundsätzliche Bemerkungen zur Werkzeugunterstützung der Programmierung beschränken müssen. Hervorstechendes Werkzeug ist natürlich der Compiler mit seinen klassischen Funktionen der lexikalischen, syntaktischen und semantischen Analyse sowie Codegenerierung. Viele moderne Compiler bieten dem Programmierer optimale Unterstützung durch lückenlos eingebundene Editoren, cross-reference-checker, Fehlerbehebungshilfen (debugging aids), Binder, Modulbibliotheken und eine Fülle von Standardroutinen und allgemein verwendbaren Modulen. Die Grenze zu den Entwicklungs-Umgebungen (vgl. S. 864) ist hier fließend. Speziell für Objektorientierte Programmierung im Zusammenhang mit Netz-Anwendungen wurde die Sprache Java entwickelt, für die es mittlerweile leistungsstarke Compiler, umfassende Werkzeug-Komponenten und ständig wachsende Entwicklungsumgebungen gibt.

    12.8.5

    Test- und Fehlerbehebungs-Werkzeuge

    Bei den Testwerkzeugen finden wir eine noch größere Vielfalt und Individualität vor als bei den Spezifikations-Werkzeugen: Neben den oben bereits erwähnten Compilerfunktionen, z.B. für das debugging gibt es eine Fülle von unternehmens- oder abteilungsspezifischen Werkzeugen, die meist aus der täglichen Projektarbeit der Entwickler heraus entstanden sind.

    862

    12 Software-Entwicklung

    Wir wollen die Funktionalität solcher Werkzeuge am Beispiel eines hypothetischen (aber in ähnlicher Form vielfach realisierten) Software-Testsystems erläutern (vgl. Abbildung 12.18).

    allg. TestDaten

    TestErgebnisse

    Testrahmen

    spez. Eingabe

    Testfall 1

    Testfall 2

    ...

    TestTest- treiber fall 3

    Testling

    Ausgabe

    Vergleich frühere Ergebnisse

    Dateien

    Code (stubs, ...)

    (Simulierte) Test-Umgebung

    KontrollflußÜberwachung (C1) ProgrammZustandsÜberwachung VariablenZustandsÜberwachung SchnittstellenÜberwachung

    TestWerkzeuge

    Abb. 12.18: Struktur eines Software-Testsystems

    In der Mitte steht der so genannte Testling, d.h. der zu testende Programmbaustein – in der Regel ein Modul oder ein aus mehreren Modulen zusammengesetztes Subsystem. Ein Testrahmen stellt die allgemein verwendbaren (d.h. testlings-unabhängigen) Funktionen – z.B. zum Verwalten, Anstoßen und Auswerten von Testfällen – zur Verfügung. Der Testrahmen bietet eine Schnittstelle zum Einhängen spezifischer Testtreiber an, die für jeden Testling neu zu schreiben sind. Ein Testtreiber ist ein Programm, das einen oder mehrere Testfälle enthält und zum Ablauf bringt. Ein Testfall kann z.B. aus einer Abfolge mehrerer Operationen des Testlings bestehen. Um einen Testfall zum Ablauf zu bringen, müssen dazu notwendige Testdaten (z.B. Operations-Parameter und Daten-Vorbesetzungen) eingelesen und die Testergebnisse zur Überprüfung abgespeichert werden. Die Prüfung der Ergebnisse erfolgt entweder manuell (wobei so genannte Zusicherungen oder assertions, d.h. Aussagen über die zu erwartenden Ergebnisse aus der Spezifikation hilfreich sind) oder – im Falle so genannter Regressionstests – automatisch gegenüber früheren Abläufen des gleichen Tests. Für den letztgenannten Fall sind die Testergebnisse in einer gesonderten Datei für spätere Überprüfungen aufzubewahren. Verschiedene Werkzeuge zur Fehlerbehebung, die z.B. das Anhalten des Programms an jeder beliebigen Stelle, das Inspizieren von Variablen beim Anhalten oder an definierten Schnittstellen, das Verfolgen von Programmdurchläufen etc. unterstützen, runden die Funktionalität des Testsystems ab. Außer für den Test und für die Fehlerbehebung lassen sich Werkzeuge auch sinnvoll für Programm-Verifikation, Reviews, Inspektionen sowie für die Anwendung von Software-Metriken einsetzen. Zur Unterstützung von Reviews und Inspektionen wurden z.B. assistierende

    12.8 Werkzeuge und Programmierumgebungen

    863

    Programme und Informationssysteme entwickelt, die den Reviewer oder Inspekteur bei seiner Tätigkeit leiten, gezielt mit den notwendigen Informationen versorgen und es ihm gestatten, seine Ergebnisse zu dokumentieren und für künftige Prüfmaßnahmen weiterzugeben. Die Anwendung von Qualitäts-Maßen lässt sich durch spezifische Werkzeuge unterstützen, die (je nach Maß statisch oder dynamisch) einen gegebenen Programmtext bzw. Programmablauf im Hinblick auf das geforderte Qualitätskriterium analysieren und automatisch mit Maßzahlen bewerten.

    12.8.6

    Tätigkeitsübergreifende Werkzeuge

    Der Schwerpunkt der tätigkeitsübergreifenden Werkzeuge liegt auf den Gebieten der Dokumentation und Verwaltung von Entwicklungsergebnissen und Software-Produkten. SoftwareEntwicklung hat manches mit einer schriftstellerischen Tätigkeit gemeinsam, daher ist ein leistungsfähiger und benutzerfreundlicher Editor eine unentbehrliche Grundvoraussetzung für die meisten Entwicklungsaufgaben. Neben guten ausgereiften Werkzeugen findet man auch heute noch kryptische, aus einer Hacker-Mentalität entstandene Editoren, die dem Benutzer nicht einmal signalisieren, in welchem Modus (Texteingabe oder Funktionsmodus) er sich befindet. Daran kann man jedoch gut lernen, wie wichtig die Software-Ergonomie ist: ein Teilgebiet der Softwaretechnik, das sich u.a. mit der anwendergerechten Gestaltung von Benutzer-Schnittstellen befasst. Dokumente, die mit Editoren (und anderen Werkzeugen) bearbeitet werden, erfordern eine Verwaltung. Das beinhaltet Grundfunktionen zum Speichern, Wiederauffinden, Ersetzen, Archivieren und Löschen von Dokumenten. Solche Funktionen werden in rudimentärer Form bereits von Dateisystemen angeboten. Komfortablere Werkzeuge erlauben es, Dokumente oder Dokumententeile zu typisieren und typabhängige Operationen darauf auszuführen, wie z.B.: • • • •

    Gib zur vorliegenden Beschreibung eines Entitäts- oder Objekttyps die zugehörigen Attribut-Beschreibungen. Gib zur vorliegenden Spezifikation die zugehörige Konstruktion. Gib die Liste aller Module, die die vorliegende exportierte Funktion importieren. Überführe das vorliegende Dokument von der Arbeits- in die Integrationsversion.

    Unter den Verwaltungswerkzeugen sind die Datenlexika, Projektbibliotheken und Klassenbibliotheken hervorzuheben. Ein Datenlexikon (engl. data dictionary, kurz: DD) ist ein Werkzeug zur Verwaltung der Elemente von Anwendungsmodellen, d.h. in erster Linie von Arbeitsergebnissen der Analyse und des fachlichen Entwurfs (vgl. S. 806). Beispiele von DDEinträgen sind: Entitätstyp „Mitarbeiter“, Attribut „Personalnummer“, Funktion „Mitarbeiterdaten ändern“. Eine Projektbibliothek hat die Aufgabe, alle während der Entwicklung anfallenden, weiter benötigten und projektweit relevanten Dokumente aufzunehmen. Dazu gehören z.B. Analyseergebnisse, Spezifikationen, Konstruktionen, Programmcode, Testfälle und Testdaten für alle Module, Ergebnisse von Reviews und Inspektionen, im weiteren Sinne auch ManagementDokumente wie Grob- und Feinpläne, Balkendiagramme, Netzpläne, Soll-/Ist-Vergleiche etc.

    864

    12 Software-Entwicklung

    Klassenbibliotheken spielen eine Rolle bei der objektorientierten Software-Entwicklung. In ihnen werden die Klassendefinitionen mit den Spezifikationen ihrer Attribute und Operationen verwaltet. Vorrangiges Ziel ist dabei die Wiederverwendung: Eine einmal definierte und in der Klassenbibliothek abgelegte Klasse kann intern für andere Systemteile oder extern für weitere Systementwicklungen unverändert übernommen, referenziert, instanziiert, verfeinert, zu Subklassen spezialisiert werden etc. Die genannten Werkzeuge unterscheiden sich in der Zielrichtung, haben aber Gemeinsamkeiten und Überlappungen. Mit dem Konzept einer Entwicklungs-Datenbank (engl. repository), werden die Funktionen der genannten Werkzeuge integriert und unter einer gemeinsamen, einheitlichen Benutzer-Schnittstelle angeboten. Beispiele dafür sind das Microsoft Repository und der Enabler von Fujitsu (früher: Softlab). Schließlich wollen wir an dieser Stelle noch die phasen-übergreifendenWerkzeuge für das Projekt-Management einordnen. Dazu gehören Werkzeuge zur Ressourcen-, Termin- und Aufwandsplanung, zum Erfassen von Ist-Werten und Vergleichen mit entsprechenden SollWerten, zur Abwicklung von Qualitätssicherungs-Maßnahmen (Reviews und Inspektionen), zur Erstellung von Berichten etc. Sie sind häufig an eine Projektbibliothek oder an ein ähnliches Werkzeug gekoppelt.

    12.8.7

    Entwicklungs-Umgebungen

    Mit Software-Entwicklungs-Umgebung (kurz: SEU, engl. Software Engineering Environment, SEE) oder Software-Produktions-Umgebung (SPU) bezeichnet man einen Verbund von Werkzeugen, die alle miteinander abgestimmt und nach Möglichkeit unter einer gemeinsamen, einheitlichen Benutzer-Schnittstelle anzusteuern sind und die die durchgängige Bearbeitung einer Entwicklungs-Aufgabe über mehrere Phasen oder über den gesamten Entwicklungszyklus hinweg unterstützen. Sommerville unterscheidet in seinem Buch über Software Engineering drei Arten von Umgebungen: (a) Programmier-Umgebungen Bei diesen konzentriert sich die Unterstützung auf die Aufgaben der Programmierung, des Testens und der Fehlerbehebung, während Analyse- und Entwurfsaufgaben weniger unterstützt werden. Oft sind solche Umgebungen sprach-orientiert, d.h. ihr Ausgangspunkt ist eine bestimmte Programmiersprache mit ihrem Übersetzungs- und Laufzeitprogrammen, an die weitere Werkzeuge (Editoren, Entwurfs- und Testhilfen, Fehleranalyse-Programme etc.) angebunden werden. Beispiele für solche Umgebungen sind Interlisp, Rational Rose (in seinen Anfängen, aufbauend auf der Sprache ADA), Turbo Pascal (Delphi) und JDK für Java.

    12.8 Werkzeuge und Programmierumgebungen

    865

    (b) CASE-Arbeitsplätze (CASE workbenches) Diese haben ihren Schwerpunkt in der Unterstützung der Analyse-, Spezifikations- und Entwurfstätigkeiten. Normalerweise liefern sie nur wenig Programmier-Unterstützung, können aber mit einer Programmier-Umgebung gekoppelt werden. Typische Bestandteile sind grafische Editoren für das Bearbeiten von Diagrammen, Analyse-Instrumente für Entwürfe und Spezifikationen, Datenlexika und zugehörige Abfrage-Instrumente, Berichts- und Formular-Generatoren, Code-Generatoren und Schnittstellen zu anderen Werkzeugen, etwa zu einem Repository. (c) Software Engineering-Umgebungen Zu dieser umfassendsten Klasse gehören alle solche Werkzeugsysteme, die für die Entwicklung und Pflege großer, langlebiger Software-Systeme ausgelegt sind, Aufgaben, an denen normalerweise viele Personen beteiligt sind, die eng zusammen arbeiten und miteinander kommunizieren müssen. Ein Ansatz für diese Art von Umgebung besteht darin, verschiedene Werkzeuge, die womöglich unterschiedliche Tätigkeiten unterstützen, mit kompatiblen Schnittstellen (Protokollen) auszustatten und somit die lückenlose Bearbeitung von Bausteinen über mehrere Entwicklungsschritte hinweg zu ermöglichen. Dabei können für bestimmte Tätigkeiten auch verschiedene Werkzeuge dem „Werkzeugkasten“ entnommen und wahlweise eingesetzt werden. Heute bilden die Integrierten Entwicklungs-Umgebungen (engl. Integrated Development Environment IDE) einen wichtigen Erfolgsfaktor für Software-produzierende Unternehmen und Abteilungen. Sie unterstützen nicht nur die Entwickler bei ihren verschiedenen Tätigkeiten, sondern enthalten auch Management-Funktionen und ein umfassendes ProduktverwaltungsWerkzeug (repository). Dabei kommen die Vorteile der Objektorientierten Entwicklungstechnik voll zum Tragen, wenn z.B. spezifische Werkzeugfunktionen zur Analyse, zum Entwurf, zur Programmierung und zum Test mit den Editier-, Verwaltungs- und Retrievalfunktionen gekoppelt sind, die für eine effektive Wiederverwendung von Software-Bausteinen unerlässlich sind. Eine Plattform für eine integrierte, evolutionär anwachsende Entwicklungsumgebung bilden die Werkzeuge Eclipse. und NetBeans. Hier wird erfolgreich der open source-Ansatz zur verteilten, weitgehend voneinander unabhängigen Entwicklung von Java-basierten Werkzeugen genutzt, die dann als so genannte plug-ins in die Plattformen eingefügt und über das Internet zur Verfügung gestellt werden können.

    A

    Literatur

    In dem vorliegenden Buch wurden viele verschiedene Themen angesprochen. Zu jedem dieser Gebiete gibt es umfangreiche weiterführende Literatur. Im Folgenden ist eine Auswahl von aktuellen und grundlegenden Titeln zusammengestellt – geordnet nach den Themen der einzelnen Kapitel.

    A.1

    Einführende Bücher

    Balzert, Helmut: Lehrbuch Grundlagen der Informatik Spektrum Akademischer Verlag Heidelberg; 2004; 2. Auflage Claus, Volker: Duden. Informatik. Ein Fachlexikon für Studium und Praxis Bibliographisches Institut; 2006; 4. Auflage Herold, Helmut; Lurz, Bruno; Wohlrab, Jürgen: Grundlagen der Informatik Pearson Studium; 2007; 1. Auflage Hansen, Hans Robert: Wirtschaftsinformatik I Lucius & Lucius UTB. Verlag Stuttgart; 2005; 9. Auflage Küchlin, Wolfgang; Weber, Andreas: Einführung in die Informatik. Objektorientiert mit Java Springer-Verlag; 2004; 3. Auflage Rechenberg, Peter; Pomberger, Gustav: Informatik-Handbuch Hanser Fachbuchverlag; 2006; 4. Auflage Schneider, Uwe; Werner, Dieter: Taschenbuch der Informatik Hanser Fachbuchverlag; 2007; 6. Auflage Stahlknecht, Peter; Hasenkamp, Ulrich: Einführung in die Wirtschaftsinformatik Springer Verlag; 2004; 11. Auflage

    A.2

    Lehrbücher der Informatik

    Blieberger, Johann et al.: Informatik Grundlagen Springer-Verlag Wien; 2005; 5. Auflage

    868

    A Literatur

    Goos, Gerhard: Vorlesungen über Informatik Band 1: Grundlagen und funktionales Programmieren Springer-Verlag Heidelberg; 2005; 4. Auflage Band 2: Objektorientiertes Programmieren und Algorithmen Springer-Verlag Heidelberg; 2006; 4. Auflage Knuth, Donald E.: The Art of Computer Programming. http://www-cs-faculty.stanford.edu/~knuth/taocp.html Volume I: Fundamental Algorithms 1997; 3. Auflage Volume II: Seminumerical Algorithms;1997; 3. Auflage Volume III: Sorting and Searching;1998; 2. Auflage Volume IV: Fascicle 0: Introduction to Combinatorial Algorithms and Boolean Functions; 2008 Fascicle 1: noch nicht erschienen Fascicle 2: Generating All Tuples and Permutations; 2005 Fascicle 3: Generating All Combinations and Partitions; 2005 Fascicle 4: Generating All Trees - History of Combinatorial Generation; 2006

    A.3

    Programmieren in Java

    Arnold, Ken; Gosling, James; Holmes, David: The Java Programming Language Addison-Wesley Verlag; 2005; 4. Auflage Barnes, David J.; Kölling, Michael: Objektorientierte Programmierung mit Java. Eine praxisnahe Einführung mit BlueJ. Pearson Studium; 2006; 3. Auflage Bloch, Joshua: Effective Java: A Programming Language Guide Addison-Wesley Verlag; 2008; 2. Auflage Gosling, James; Joy, Bill; Steele, Guy; Brancha, Gilad: The Java Language Specification Addison-Wesley Verlag; 2005; 3. Auflage Flanagan, David: Java in a Nutshell O'Reilly Media; 2006; 5. Auflage Heinisch, Cornelia; Müller, Frank; Goll, Joachim: Java als erste Programmiersprache Teubner Verlag; 2007; 5. Auflage Krüger, Guido; Stark, Thomas: Handbuch der Java Programmierung Addison-Wesley Verlag; 2007; 5. Auflage Savich, Walter J.: Absolute Java Addison-Wesley Verlag; 2007; 3. Auflage Schiedermeier, Reinhard: Programmieren mit Java. Eine methodische Einführung Pearson Studium; 2005

    A.4 Algorithmen und Datenstrukturen

    869

    Schmidt-Thieme, Lars; Schader, Martin: Java Springer-Verlag; 2007; 4. Auflage Ullenboom, Christian: Java ist auch eine Insel. Galileo Press; 2007; 7. Auflage

    A.4

    Algorithmen und Datenstrukturen

    Bentley, Jon: Programming Pearls Addison-Wesley Verlag Reading; 1999; 2. Auflage Blum, Norbert: Algorithmen und Datenstrukturen. Eine anwendungsorientierte Einführung. Oldenbourg Wissenschaftsverlag München; 2004 Cormen, Thomas H.; Leiserson, Charles E.: Algorithmen – Eine Einführung Oldenbourg Wissenschaftsverlag München; 2007; 2. Auflage Güting, Ralf H.; Dieker, Stefan: Datenstrukturen und Algorithmen Teubner Verlag; 2004; 3. Auflage Lang, Hans W.: Algorithmen in Java Oldenbourg Wissenschaftsverlag München; 2006; 2.Auflage Pomberger, Gustav; Dobler, Heinz: Algorithmen und Datenstrukturen: Eine systematische Einführung in die Programmierung. Pearson Studium; 2008 Saake, Gunter; Sattler, Kai-Uwe: Algorithmen und Datenstrukturen. Eine Einführung mit Java. Dpunkt-Verlag Heidelberg; 2006; 3. Auflage Sedgewick, Robert: Algorithmen Pearson Studium; 2002; 2. Auflage Sedgewick, Robert: Algorithmen in Java. Teil 1-4: Grundlagen, Datenstrukturen, Sortieren, Suchen Pearson Studium; 2003; 3. Auflage Weiss, Mark Allen: Data Structures and Problem Solving Using Java Addison-Wesley Verlag; 2005; 3. Auflage

    A.5

    Rechnerarchitektur

    Bähring, Helmut: Mikrorechner-Technik (Sonderausgabe: 3 Bände komplett) Springer-Verlag Heidelberg; 2005 Böttcher, Axel: Rechneraufbau und Rechnerarchitektur Springer-Verlag Berlin; 2006; 1. Auflage

    870

    A Literatur

    Hoffmann, Kurt: Systemintegration. Vom Transistor zur großintegrierten Schaltung Oldenbourg Wissenschaftsverlag München; 2006; 2. Auflage Hoffmann, Dirk W.: Grundlagen der Technischen Informatik Hanser Fachbuchverlag; 2007; 1. Auflage Oberschelp, Walter; Vossen, Gottfried: Rechneraufbau und Rechnerstrukturen Oldenbourg Wissenschaftsverlag München; 2006; 10. Auflage Patterson, David A.; Hennessy, John L.: Rechnerorganisation und -entwurf Spektrum Akademischer Verlag; 2005; 3. Auflage Patterson, David A.; Hennessy, John L.: Computer Architecture. A Quantitative Approach Academic Press; 2006; 4. Auflage Schiffmann, Wolfram; Schmitz, Robert: Technische Informatik 1. Grundlagen der digitalen Elektronik; Springer Verlag Berlin; 2004; 5. Auflage Schiffmann, Wolfram; Schmitz, Robert: Technische Informatik 2. Grundlagen der Computertechnik; Springer Verlag Berlin; 2005; 5. Auflage Stroetmann, Karl: Computer-Architektur: Modellierung, Entwicklung und Verifikation mit Verilog; Oldenbourg Wissenschaftsverlag; 2007 Tanenbaum, Andrew S.: Computerarchitektur. Strukturen - Konzepte - Grundlagen Pearson Studium; 2005; 5. Auflage Tanenbaum, Andrew S.: Structured Computer Organization Prentice Hall; 2005; 4. Auflage

    A.6

    Betriebssysteme

    Baumgarten, Uwe; Siegert, Hans-Jürgen: Betriebssysteme Oldenbourg Wissenschaftsverlag München; 2006; 6. Auflage Brause, Rüdiger: Betriebssysteme – Grundlagen und Konzepte Springer-Verlag Heidelberg; 2003; 3. Auflage Mandl, Peter: Grundkurs Betriebssysteme Architekturen, Betriebsmittelverwaltung, Synchronisation, Prozesskommunikation Vieweg+Teubner Verlag; 2008; 1. Auflage Tanenbaum, Andrew S.: Modern Operating Systems Prentice Hall; 2008; 3. Auflage Tanenbaum, Andrew S.; Woodhull, Albert S.: Operating Systems Design and Implementation Prentice Hall; 2006; 3. Auflage

    A.7 Rechnernetze

    A.7

    871

    Rechnernetze

    Coulouris, George F.; Halsall, Fred: Distributed Systems: Concepts and Design: and Computer Networking and the Internet Addison-Wesley Verlag; 2005; 4. Auflage Halsall, Fred; Ince, Darrel: Computer Networking and the Internet: and Developing Distributed and E-Commerce Applications Addison-Wesley Verlag; 2005; 5. Auflage Halsall, Fred: Data Communications, Computer Networks and Open Systems Addison-Wesley Verlag; 1998; 4. Auflage Halsall, Fred: Multimedia Communications Addison-Wesley Verlag; 2000; 1. Auflage Proebster, Walter E.: Rechnernetze Oldenbourg Wissenschaftsverlag München; 2002; 2. Auflage Scherff, Jürgen: Grundkurs Computernetze Eine kompakte Einführung in die Rechnerkommunikation Vieweg+Teubner Verlag; 2006; 1. Auflage Schreiner, Rüdiger: Computernetzwerke. Von den Grundlagen zur Funktion und Anwendung. Hanser Fachbuchverlag; 2007; 2. Auflage Tanenbaum, Andrew S.: Computernetzwerke Pearson Studium; 2003; 4. Auflage Tanenbaum, Andrew S.; van Steen, Maarten: Verteilte Systeme: Prinzipien und Paradigmen Pearson Studium; 2007; 2. Auflage

    A.8

    Internet

    Alpar, Paul; Blaschke, Steffen: Web 2.0 - Eine empirische Bestandsaufnahme Vieweg+Teubner Verlag; 2008 Berners-Lee, Tim: Weaving the Web Texere Verlag London; 2000; 1. Auflage Badach, Anatol; Hoffmann, Erwin: Technik der IP-Netze. TCP/IP incl. IPv6. Funktionsweise, Protokolle und Dienste Hanser Fachbuch Verlag; 2007; 2. Auflage Comer, Douglas E.: Computernetzwerke und Internets Pearson Studium; 2004; 3. Auflage

    872

    A Literatur

    Comer, Douglas E.: Internetworking with TCP/IP, Vol.1: Principles, Protocols, and Architectures Prentice Hall; 2005; 5. Auflage Vol 2: Design, Implementation, and Internals Prentice Hall; 1998; 3. Auflage Crane, Dave; Pascarello, Eric; James, Darren: Ajax in Action Das Entwicklerbuch für das Web 2.0 Addison-Wesley Verlag; 2006 Handke, Jürgen: Multimedia im Internet Oldenbourg Wissenschaftsverlag München; 2003 Harold, Elliotte R.: XML in a Nutshell O'Reilly Media; 2004; 3. Auflage Koch, Stefan: JavaScript. Einführung, Programmierung und Referenz dpunkt-Verlag Heidelberg; 2006; 4. Auflage Lienemann, Gerhard: TCP/IP-Grundlagen dpunkt-Verlag Heidelberg; 2003; 3. Auflage Lienemann, Gerhard: TCP/IP-Praxis dpunkt-Verlag Heidelberg; 2003; 3. Auflage Mintert, Stefan; Leisegang, Christoph: Ajax dPunkt Verlag Heidelberg; 2006; Münz, Stefan; Nefzger, Wolfgang: HTML Handbuch. Studienausgabe Franzis-Verlag; 2005 Münz, Stefan: Professionelle Websites. Programmierung, Design und Administration von Webseiten Addison-Wesley; 2006; 2. Auflage

    A.9

    Theoretische Informatik und Compilerbau

    Aho, Alfred V.; Ullman, Jeffrey D.: Foundations of Computer Science W.H. Freeman & Company; 1995 Aho, Alfred V.; Sethi, Ravi; Ullman, Jeffrey D.: Compilerbau Teil 1 Oldenbourg Wissenschaftsverlag München; 1999; 2. Auflage Aho, Alfred V.; Sethi, Ravi; Ullman, Jeffrey D.: Compilerbau Teil 2 Oldenbourg Wissenschaftsverlag München; 1999; 2. Auflage Aho, Alfred V.; 21st Century Compilers Addison Wesley; 2007

    A.10 Datenbanken Appel, Andrew: Modern Compiler Implementation in Java Cambridge University Press; 2003; 2. Auflage Asteroth, Alexander; Baier, Christel: Theoretische Informatik. Eine Einführung in Berechenbarkeit, Komplexität und formale Sprachen mit 101 Beispielen Pearson Studium; 2002 Blum, Norbert: Einführung in Formale Sprachen, Berechenbarkeit, Informations- und Lerntheorie Oldenbourg Wissenschaftsverlag München; 2006 Cooper, Keith; Torczon, Linda: Engineering a Compiler Morgan Kaufmann; 2004 Davis, Martin: What is a Computation? In: Steen, L.A. (ed.): Mathematics Today. Twelve informal essays. Springer-Verlag; 1978 Hedtstück, Ulrich: Einführung in die Theoretische Informatik Oldenbourg Wissenschaftsverlag München; 2007; 4. Auflage Hopcroft, John E.; Ullman, Jeffrey D.; Motwani, Rajeev: Einführung in die Automatentheorie, Formale Sprachen und Komplexitätstheorie. Pearson Studium; 2003; 2. Auflage Linz, Peter: An Introduction to Formal Languages and Automata Jones and Bartlett Publishers International; 2006; 4. Auflage Schöning, Uwe: Theoretische Informatik kurzgefaßt Spektrum Akademischer Verlag; 2008; 5. Auflage Socher, Rolf; Märtin, Christian; Lutz, Michael: Theoretische Grundlagen der Informatik Hanser Fachbuchverlag; 2007; 3. Auflage Winter, Renate: Theoretische Informatik Oldenbourg Wissenschaftsverlag München; 2002 Wirth, Niklaus: Grundlagen und Techniken des Compilerbaus Oldenbourg Wissenschaftsverlag München; 2008; 2. Auflage

    A.10

    Datenbanken

    Date, Chris J.: An Introduction to Database Systems Addison Wesley; 2003; 8. Auflage Date, Chris J.; Darwen, Hugh: Databases, Types, and the Relational Model Longman; 2006; 3. Auflage

    873

    874

    A Literatur

    Härder, Theo; Rahm, Erhard: Datenbanksysteme. Konzepte und Techniken der Implementierung Springer-Verlag Heidelberg; 2001; 2. Auflage Kemper, Alfons; Eickler, Andre: Datenbanksysteme. Eine Einführung Oldenbourg Wissenschaftsverlag München; 2006; 6. Auflage Heuer, Andreas; Saake, Gunter; Sattler, Kai-Uwe: Datenbanken. Konzepte und Sprachen Der fundierte Einstieg in Datenbanken. Schwerpunkt: Datenbankentwurf und Datenbanksprachen. Inklusive aktuelle Trends: SQL-99, JDBC, OLAP, Textsuche mitp Verlag; 2007; 3. Auflage Saake, Gunter; Heuer, Andreas; Sattler, Kai-Uwe: Datenbanken Implementierungstechniken mitp; 2005; 2. Auflage Vossen, Gottfried: Datenmodelle, Datenbanksprachen und Datenbankmanagement-Systeme Oldenbourg Wissenschaftsverlag München; 2008; 5. Auflage Fisher, Maydene; Ellis, Jon; Bruce, Jonathan: JDBC API Tutorial and Reference Addison-Wesley Verlag Reading; 2003; 3. Auflage

    A.11

    Grafikprogrammierung

    Bungartz, Hans Joachim; Griebel, Michael; Zenger, Christoph: Einführung in die Computergraphik Vieweg+Teubner Verlag; 2002; 2. Auflage de Berg, Mark; Cheong, Otfried; van Kreveld, Marc; Overmars, Mark: Computational Geometry. Algorithms and Applications Springer-Verlag Berlin; 2008; 3. Auflage Foley, James D.; Dam, Andries van; Feiner, Steven K.; Hughes, John F.: Computer Graphics – Principles and Practice Addison-Wesley Verlag; 1990; 2. Auflage Addison-Wesley Verlag; 200?; 3. Auflage Klawonn, Frank: Computergrafik mit Java Vieweg Verlag; 2005 Watt, Alan: 3D Computer Graphics Addison-Wesley Verlag; 1999; 3. Auflage Xiang, Zhigang; Plastock, Roy A.: IT- Studienausgabe Computergrafik. mitp-Verlag; 2002

    A.12 Software-Entwicklung

    875

    Zeppenfeld, Klaus; Wolters, Regine: Lehrbuch der Grafikprogrammierung Spektrum Akademischer Verlag; 2003

    A.12

    Software-Entwicklung

    Balzert, Helmut: Softwaremanagement: Lehrbuch der Softwaretechnik: Spektrum Akademischer Verlag; 2008; 2. Auflage Balzert, Heide: UML 2 kompakt Spektrum Akademischer Verlag; 2005; 2. Auflage Booch, Grady; Rumbaugh, Jim; Jacobson, Ivar: Das UML Benutzerhandbuch. Aktuell zur Version 2.0; Addison-Wesley München; 2006 Booch, Grady: Object-Oriented Analysis and Design with Applications Addison-Wesley Longman; 2007; 3. Auflage Filman, Robert E.; Elrad, Tzilla; Clarke, Siobhan; Aksit, Mehmet: Aspect-Oriented Software Develoment; Addison-Wesley; 2004 Forbrig, Peter: Objektorientierte Softwareentwicklung mit UML Hanser Fachbuchverlag; 2007; 3. Auflage Gamma, Erich; Helm, Richard; Johnson, Ralph; Vlissides, John: Entwurfsmuster. Elemente wiederverwendbarer objektorientierter Software; Addison-Wesley; 2004 Gruhn, Volker; Pieper, Daniel; Röttgers, Carsten: MDA: Effektives Softwareengineering mit UML2 und Eclipse; Springer Verlag Berlin; 2006 Hesse, Wolfgang; Merbeth, Günter; Frölich, Rainer: Software-Entwicklung – Vorgehensmodelle, Projektführung und Produktverwaltung, Handbuch der Informatik, Bd. 5.3; Oldenbourg Wissenschaftsverlag München; 1992 Kecher, Christoph: UML 2.0: Das umfassende Handbuch Galileo Press; 2006; 2. Auflage Mellor, Stephen J., Scott, Kendall, Uhl, Axel: MDA Distilled. Addison-Wesley Professional, 2004 Myers, Glenford J.: Methodisches Testen von Programmen Oldenbourg Wissenschaftsverlag München; 2001 Oestereich, Bernd: Analyse und Design mit UML 2.1 Oldenbourg Wissenschaftsverlag München; 2006; 8. Auflage Oestereich, Bernd: Die UML-Kurzreferenz für die Praxis. Oldenbourg Wissenschaftsverlag München; 2005; 4. Auflage

    876

    A Literatur

    Rumbaugh, James; Blaha, Michael: Object-Oriented Modelling and Design with UML Prentice Hall Englewood Cliffs; 2004; 2. Auflage Seemann, Jochen; von Gudenberg, Jürgen Wolff: Software-Entwurf mit UML 2. Objektorientierte Modellierung mit Beispielen in Java. Springer-Verlag Heidelberg; 2006; 2. Auflage Siedersleben, Johannes: Software-Architektur. Dpunkt Verlag; 2004 Sommerville, Ian: Software Engineering Pearson Studium; 2007; 8. Auflage Zuser, Wolfgang; Grechenig, Thomas; Köhle, Monika: Software Engineering mit UML und dem Unified Process. Pearson Studium; 2004; 2. Auflage

    A.13

    Mathematischer Hintergrund

    Gräbe, Hans-Gert; Kofler, Michael: Mathematica 6. Einführung, Anwendung, Referenz Pearson Studium; 2007; 5. Auflage Ihringer, Thomas: Allgemeine Algebra. Mit einem Anhang über Universelle Coalgebra von H.P. Gumm Heldermann Verlag; 2003 Ihringer, Thomas: Diskrete Mathematik Heldermann Verlag; 2002; 2. Auflage Matousek, Jiri; Nesetril, Jaroslav; Mielke, Hans: Diskrete Mathematik Eine Entdeckungsreise; Springer Verlag Berlin; 2007; 2. Auflage. Schulz, Ralph Hardo: Codierungstheorie Vieweg Verlag Braunschweig; 2003; 2. Auflage Willems, Wolfgang: Codierungstheorie und Kryptographie Birkhäuser Verlag; 2008

    A.14

    Sonstiges

    Allen, Julie D.; Becker, Joe: The Unicode Standard, Version 5.0 Addison-Wesley Longman; 2006; 5.Auflage

    Stichwortverzeichnis .NET-Framework, 591 µ-Operator 759 µ-Rekursion 758 2-3-4-Baum 382 2-3-Baum 382 3SAT 772 802.11b 624 80286 523 80386 523 80486 523 8080 523 8086 523 8088 Mikroprozessor 32 Ableitungsbaum 715 Abnahmetest 833 above 493 Abschnitt 303 Abschwächungsregel 188 absoluter Bezug 74 Absorption 423 Abstandsmatrix 393 abstrakte Klasse 176, 253, 257 abstrakte Methode 176, 253 abstrakter Datentyp 112, 338 Abstraktion 4, 31, 113, 337 Abstraktionshierarchie 114 Access-Point 624 ACID Bedingung 795 Ackermannfunktion 759 action (in Formular) 672 active server pages 684 actuator 46 Ada 81

    Adapter-Klasse 283 Adaptive Listen 362 ADD 483 Addierer 438, 439 Adjazenzmatrix 394 Adressbus 44, 511 Adresse 43 Adressierung 45, 528 Adressrechner 480 Adressregister 475 Adressübersetzung 529 ADSL 603 ADSL2+ 606 AES 628 AFNIC 631 Aggregat 792 Aggregationsoperator 788 AiX 536 Ajax 693 akkumulierende Parameter 157 aktueller Ordner 61 Algorithmenbegriff 746 Algorithmus 87, 299, 743, 760 nichtdeterministisch 770 Alias-Punkte 804 Allzweck-Register 488 Alphabet 696, 698, 815 Alternativ-Anweisung 126, 128 Alternativregel 187 Alto 64 Alt-Taste 12 ALU 40, 459, 466 ambient intelligence 600 ambientes Licht 822 Analyse 835

    878 AND 15, 99 and 444 Andreesen 663 Anfrage 798 Angewandte Informatik 3 Anker 73 annotiertes Programm 192 Anweisung 125, 126, 236 Anweisungsausdruck 236 Anweisungsblock 127 Anwender-Betriebssystem 536 Anwendungsmodell 832 Apfelmännchen-Bild 809 API 281 APL 81 APNIC 631 Apple 64, 533 Applet 84, 207, 232, 675, 677 äquivalent (Zustände) 706 Arbeitsproduktivität 829 Arbeitsspeicher 42 ARIN 631 arithmetische Operation 232 ARPANET 629 Array-Datentyp 215 ArrayList 229 ASCII 11, 108 ASCII-Code 13 ASCII-Datei 12 ASCII-Erweiterung 12 ASCII-Tabelle 7 ASCII-Zeichen 54 ASP 684 Aspekt-orientierte Entwicklung 851 Aspekt-orientierte Programmierung 852 Assembler Befehl ADD 489 AND 494 CALL 507 CLD 490, 503 CMPSx 502 DEC 489, 494 DIV 503 IDIV 503 INC 489, 494

    Stichwortverzeichnis INT 512 JE 495 JMP 495 JNE 495 JNZ 495 JZ 495 LOOP 505 MOV 489 MOVSB 502 MOVSx 502 MUL 503 NEG 489 NOT 494 OR 494 POP 506 PUSH 506 RCL 505 RCR 504 REP 502 REPNZ 503 REPZ 503 RET 507, 509 ROL 504 ROR 504 SAL 504 SAR 504 SHL 504 SHR 504 STD 490, 503 SUB 489, 492 TEST 495 XOR 494 Assemblierer 487 assert-Anweisung 181, 267 Assertion 267, 312 assoziativ 156 Assoziativität 423 asynchrone Datenübertragung 600 Athlon 45, 524 ATM-Netz 618 atomar 110, 547 attachment 658 Attribut 209, 222, 247, 252, 253, 783, 843 Auflösung 53, 66 aufzählbar 762

    Stichwortverzeichnis Aufzählungstyp 213, 225 Aufzeichnungsdichte 47 Augpunkt 819 Ausdruck 91, 93, 117, 123, 124, 126, 232 Auslagerungsdatei 543 Ausnahmebehandlung 264 Aussage 429 Auswertung 120 Auszeichnungssprache 73 Autoboxing 261 Automat 456, 701 Automatentheorie 2, 134 average case 306 AVL-Baum 380, 382 Doppelrotation 381 Rotation 381 AWT 272, 281, 283, 288, 292 Axiom 338 B (Einheit Byte) 9 B+-Baum 384 B8ZS Code 601 Backbone-Netz 617 backslash 61 backtracking 150, 199, 396, 712 Bandbreite 597 Barrel-Shifter-Multiplikationswerk 469 base64-Format 659 baseline 67 bash 574 BASIC 78, 81 basic multilingual plane 13 Basisbandverfahren 599 Basis-Software 534 Basiszahl 8 Baud 602 Baum 326, 363, 556 balanciert 379 Tiefe 364 Transformation 381 Baumadresse 367 Bearbeiter-Jahr 2, 827 Bedienknöpfe 292 Bediensystem 63 bedingte Anweisung 138

    879 bedingter Sprung 495 Bedingung 128, 138 Bedingungsvariable 549 Befehl 40, 124, 125 befehlsorientierte Sprache 78 Befehlszähler 42, 488 BEGIN 127 Begrenzer 134 Behälter 243, 302, 339 Datentyp 261 Beleuchtungsmodell 821 globales 821, 823 lokales 821 von Phong Bui-Tong 822 below 493 Benutzerebene 55 Benutzeroberfläche 535 berechenbar 742 Berechnung 124 Berechtigungsmarke 611 Berners-Lee 663 best case 306 Betriebsart 536 Betriebssystem 59, 533 Kern 535 multitasking 534 multi-user 535 Beugung 823 bewerteter Graph 392 Bezeichner 133, 136, 210 Beziehung 1:1 784 n:1 784 n:m 784 Beziehungstyp 784 bias 27 Bibliotheken 541 Bildschirmspeicher 126 Bildwiederholfrequenz 803 Binärbaum 365 Binärdarstellung 17, 20 binäre Suche 301, 304 binärer Code 5 Binärsystem 17 BIOS 58, 512, 534

    880 BIOS-Chip 37, 39, 58 BIOS-Interrupt 62 BIOS-Setup 58 bipolar-AMI 601 bison 579, 582, 724, 736 Bit 5, 419 Bit/s 602 Bitcode 11 Bitcodierung 601 Bitfolge 6 Bitmap 16 bitweise Operationen 234 bitweise Verknüpfung 16 bitweises Komplement 16 Blatt 363 Block 90, 127, 216, 239 blockiert 542 Blocksatz 67 blockweise 43, 46 BlueJ 83, 97, 119, 174, 208, 227 Bluetooth 600, 622 body 658 Bookman 66 Boole 419 boolesche Algebra 425 boolesche Operation 234 boolesche Schaltung 425 boolesche Verknüpfung 15 boolescher Term 425 booten 58 Boot-Sequenz 58 Borland-Pascal 83 Botschaft 158 Bottom-up 726 Bourne-Shell 574 Boyer 409 Boyer-Moore-Algorithmus 409 bps 602 breadth first 395 break-Anweisung 244 Breitbandübertragung 599 Breitensuche 395 Bresenham 805 Bridge 613 broadcast 639

    Stichwortverzeichnis Browser 66, 76, 664 Brute-Force-Suche 409 BS 2000 536 BSD 64 BubbleSort 309, 314 Buchse 37 bug 82 Bügeleisen 56 Bus 37, 471 Buscontroller 37 Busnetz 610 busy waiting 546 Buszugang 37 Byte 8 byte (Java Typ) 26 Bytecode 2, 81 Bytes pro Pixel 803 C 81, 581 C++ 78, 81, 207, 260, 581, 844 Cache 44, 415, 463, 516, 519 CAD 418, 826 call by value 120 call-by-need 120 Capability Maturity Model 858 carriage return 11 Carry 22, 438, 446 Carry-Bit 491 carry-lookahead Addierer 449 carry-out 438 Cascading Style Sheet 673 CASE (Computer Aided Software Engineering) 842, 859 case-sensitiv 115, 134, 690 catch 267 ccTLD 644 CD 50 CDDB 646 CDE 64, 555 CD-R 50 CD-ROM 35 CD-RW 50 CGI 666 Char (Datentyp) 108 charAt 110

    Stichwortverzeichnis chatten 75 Check 402, 771 Chip 35, 413 Chrome 538 Churchsche These 747, 760 CIDR 638 CISC 514 CLASSPATH 230 Client 537, 589 Client-Server 536, 537, 585, 797 CLIQUE 765 clock 454 Clone 587 Closure 203, 293, 297 Cluster 32 CMOS-Technik 411, 442 CMS 536 COBOL 81 Code 5, 50 code and fix 830 Codepad 119 Code-Segment 498 Codierer 437 Codierung 5, 833 collect 293 Collection 229, 339 Collection Framework 302 com (Dateityp) 61 CombSort 322 command.com 63 Compiler 2, 16, 79, 81 Compilerbau 695 compiler-compiler 724 Compilezeit 82 Component 678 Computer Science 1 condition variable 549 config.sys 57 Container 281, 339, 351, 678 continue-Anweisung 244 Controller 36 Cook 409 Satz von 774 Coprozessor 29 CORBA 851

    881 Core 2 Duo 38, 45, 413, 415, 522, 526 Core Mikroarchitektur 525 Courier 66 CPU 36, 40, 54, 59, 470 CPU-Befehle ADD 40 AND 40 BRANCH 40 COMPARE 40 DIV 40 IN 40 JUMP 40 LOAD 40 MUL 40 NOT 40 OR 40 OUT 40 STORE 40 SUB 40 XOR 40 CR (ASCII-Zeichen) 11 critical section 546 CSMA/CA 624 CSMA-CD 611, 615, 617 CSS 673 Ctrl (Taste) 12 Ctrl-A 12 Ctrl-G 12 Ctrl-H 12 Ctrl-I 12 Ctrl-M 12 Ctrl-Z 12 DAC 801 DAT 33 Datei 8, 59, 538 Ein- und Ausgabe 271 Dateibaum 539 Datei-Dialog 272 Dateigröße 9 Dateihierarchie 60 Datei-Muster 562 Dateiname 8, 539 Dateisystem 60, 539 Dateiverwaltung 59

    882 Dateiverwaltungssystem 62 Daten 4, 11, 98 Daten-Abstraktionsprinzip 841 Datenbank 781, 789, 798 Datenbanksystem 781 Datenbankverwaltungssystem 781 Datenbankzugriff 797 Datenbus 44 Datendefinition 789 Datenflussdiagramm 842 Datenkapselung 206, 223 Datenmanipulation 789 Datenmodell 832 konzeptionelles 783 Datenmodellierung 842 Datenregister 475 Datenschachtel 344 Datensegment 498 Datenstruktur 98, 113, 114, 338 Datentechnik 1 Datentransferbefehl 40 Datentyp 98, 165, 213, 232 Datenübertragungsrate 50 Datenunabhängigkeit 782 DBS 800 DBVS 781, 800 DCOM/COM 851 DDR-SDRAM 44 deadlock 280, 549 Debugger 82, 490, 499 debugging 856, 861 Decodierer 437 default 61, 116 default Konstruktor 224 Default-Wert 214 definition module 169 Deklaration 114 Deklarationsteil 115 deklarativ 197 deklarative Sprache 78 delay 455 delegation event model 283 Delphi 25, 83 deMorgan’sche Regel 425 DENIC 631, 645

    Stichwortverzeichnis depth first 395 Depth-Sort-Algorithmus 820 DeQue 349 desktop 533 desktop metapher 64, 533 destination flag 502 Destruktor 340 deterministisch 87 deterministische Stackmaschine 715 Dezimalsystem 17 D-Flip-Flop 451 DHCP-Protokoll 637 Diagonalisierung 745 Dialogbox 292 Dienste 55, 63 digitale Logik 430 Dijkstra 82, 181 DIMM 38, 43 direction flag 490 directory 60, 539 Disassembler 500 disassemblieren 500 Disassemblierer 487 Disjunktion 15 disjunktive Normalform 427 Diskettenlaufwerk 35 Distribution 62 DistributionSort 330, 335 distributiver Verband 423 Distributivität 423 div 19 Divisionsrest 19 DNF 427, 428 DNS 643, 646 DocBook 689 Document Object Model 693 Dokumentvorlage 69 Dolphin 64 DOM 693 Document Object 693 Parser 693 Domain 632, 643 Domain Name Service 643, 646 Domain-Name 643 Dominanz 307

    Stichwortverzeichnis Doppelwort 8 Dorado 64 do-Regel 195 DOS 62 do-Schleife 140 dotiert 411 double 211 DownHeap 388 download 605 downstream 605 dpi 66 Drahtlose Netze 622 Drahtmodell 820 drain 412, 442 D-RAM 462 DRAM 44, 416 DSL 604, 606 DSL-MODEM 626 DTD 687 dualer Term 423 Dualität 423 Dualitätsprinzip 423, 426 Duplexverfahren 606 Duplikate 373 DVD 35, 50 Dynamic Link Library (DLL) 57, 588 dynamische Adresszuordnung 637, 641 dynamische Bindung 252 dynamische Einbettung 69 E/R-Datenmodell 783 E/R-Diagramm 784, 842 E/R-Modellierung 785 early binding 252 EBCDI 15 EBNF 687 echo 575 Eclipse 208, 865 ECMA 674 ECMAScript 674 EDI 691 EEPROM 416 effektive Bandbreite 597 Eiffel 844 eindeutiger Sortierschlüssel 310

    883 einfache Anweisung 238 einfacher Datentyp 213 Einzug 67 EISA 37 EJB 851 Electric 447 electronic commerce 691 electronic mail 610 Elektronenstrahlröhre 53 Elementarsumme 427 else-Zweig 128 emacs 208, 565, 579 E-Mail 66, 630, 654 Emission 825 emulieren 80 endrekursiv 153, 196 Entität 783 Entity (HTML) 14 Entity/Relationship 783 Entity/Relationship-Modell 842 Entity/Relationship-Modellierung 860 entprellen 452 entscheidbar 762 Entscheidungsproblem 766 Entwurf 835 Entwurfsmuster 851 enum 225, 240 EOS-Modell 838 equals 233 Ereignis 158, 282 Ereignisbehandlung 284 Erlang 202 Erweiterung 8, 60 Erzeuger-Verbraucher 350 Escape-Sequenz 212 Escape-Zeichen 701 Ethernet 617 EUV-Lithografie 415 Evaluierung 120 even 12, 42 exa (Einheit) 9 Excel 73 exe (Dateityp) 61 exklusives Oder 15, 234 Exponent 26

    884 exponentiell 308 extends 262 externes Sortieren 336 F (Wahrheitswert) 15 fachlicher Entwurf 832 Factory-Muster 851 Fakten 198 Fakultätsfunktion 130, 145, 191, 216, 237 Fallunterscheidung 139 false 15 FAQ 659 Farbwert 803 Fassaden-Muster 851 FDDI 617 Fehlerbehebung 833, 856 Fehlerkorrektur 49 Fehlerrate 50 Feld 232, 244, 252 Felddeklaration 222 femto (Einheit) 10 Fenster 64, 229, 281, 286 fensterorientierte Bedienoberfläche 64 Festplatte 35 Festplattencontroller 36 Fibonacci-Funktion 152, 157 FIFO 347 file 555 file transfer protocol 660 filter 203, 353 final 221, 252 finally 267 Firefox 664 Firewall 591 First-In-First-Out 347, 545 Flachbandkabel 35 Flächenmodell 820 Flag-Register 466, 488, 490 Flanke 454 FLASH 463 Flashkarte 52 Flash-Speicher 416 Flattersatz 67 flex 579, 582, 701, 706, 736 flimmerfrei 53

    Stichwortverzeichnis Flip-Flop 451 floating point 26 floating point unit 29 FLOPS 32 Floyd’s Algorithmus 399 Flussdiagramm 87 Flüssigkristall-Bildschirm 3, 53 folder 60 Font 66 for 83, 133, 215 for-Anweisung, for-Schleife 83, 142, 145, 206, 242 foreach 142, 293 formale Sprache 2 formatieren, Formatierung 46, 90 For-Programme 755 Fortran 81 FPU 29 fraktale Grafik 808 frame 551 Frame (Java Klasse) 281 Framework 851 Free Software Foundation 579, 582 freischlagen 68 FTP 660 Funktion 216 Funktionalität 112 Funkübertragung 600 g++ 579 GAN 610 ganze Zahl 22 ganzzahliger Datentyp 25 Garamond 66 garbage collection 245, 356 gas 579 Gate 442 gate 412 Gateway 614, 632 gcc 579 GDI 806 Geheimnisprinzip 840 GEM 593 Generator 818 Generics 297

    Stichwortverzeichnis generisch 255 generische Datentypen 261 Geogebra 308 GeOS 593 Gerätetreiber 539 GET 672 getHostbyAddr 646 getHostbyName 646 ggT 199, 217 Gibibyte 9 giga (Einheit) 9 Glasfaserkabel 599 Gleichung 99, 422 Gleitpunktdarstellung 26 Gleitpunkt-Literale 211 globales Netz 610 Gnome 64 GNU 555, 579 Gödelisierung, Gödel-Nummer 761 gopher 661 Gosling 207 GOTO-Programme 752 Goto-Tabelle 730 Gouraud-Shading 821 Grafikadapter 37 Grafikausgabe 286 Grafikcontroller 37 Grafikkarte 37, 801 Grafikmodus 54 Grafikprozessor 37, 801 grafische Oberfläche 281, 533 Grammatik 91, 708, 815 Äquivalenz 721 eindeutig 717 first 720 Follow 720 item 727 Konflikt 727 kontextfrei 708 LALR(1) 733 Linksrekursion 722 LL(1) 719 LR(1) 727 Startsymbol 708 grammatikalische Analyse 697

    885 grammatische Aktion 583, 737 Graph 391–406 graphic device interface 806 graphical user interface 64 Graphics2D 807 Gray-Code 434 greater 493 Grid-Computing 768 Gruppierung 792 gTLD 644 Guess 402, 771 guess-and-check 402 GUI 64, 281 gültig 25 G-Win 610, 619 HAL 589 Halbaddierer 438 Halb-Byte 7 Halbduplexverfahren 606 Halbleiter 411 Halteproblem 762 Hardware 1, 31 Hardware-Interrupt 538 Haskell 78, 90, 133 hasNext 259, 351 Hauptplatine 35 Hauptspeicher 16, 37, 42, 59, 114, 475, 486 Hazard 454 HD-DVD 51 HDSL 604 header 658 Heap 386 HeapSort 336, 389 Herleitung 709 Hertz 10 heuristisch 300 Hexadezimaldarstellung 18 Hexadezimalsystem 7, 18 Hex-Darstellung 21 Hex-Notation 489 Hexziffer 7 Hidden-Node Problem 624 High-Level Formatierung 47 Hintereinanderausführung 127

    886 Hintereinanderausführungsregel 183 Hoare 323 hochohmig 460 Hochsprache 2 home directory 556, 558 homogenes Koordinatensystem 819 Homomorphismus 119 Horn-Klausel 198 Host 629 Hostadresse 637 Hostcomputer 629 hot code upgrade 204 Hotspot 627 HPFS 590 HTML 76, 665, 684 Anker-Marke 669 Formular 672 Frame 671 Link 666, 669 Liste 669 Mailto-Link 666 ordered list 669 Querverweis 669 Rahmen 671 Tabellen 670 tag 665 unordered list 669 HTTP 664 Hurenkind 69 Hypermedia 662 Hypertext 662 hypertext markup language 73, 664 hypertext transfer protocol 664 IA64-Architektur 522 IANA 631 IBM-PC 32 IC 413 ICANN 631, 645 icon 65 IDE 82, 865 Idempotenz 423 Identer 439 identifier 136 IEC 9, 431

    Stichwortverzeichnis IEEE 27 IEEE 754 (Norm) 29 IEEE Norm 802.11 623 if-Anweisung 239 if-then-else-Regel 187 if-then-else-Schaltung 435 if-Zweig 128 ikonisieren 65 IMAP 654 imperativ 206 imperative Sprache 78, 94, 124 implementation module 169 implementation section 169 Implementierung 169 Implikant 435 import-Anweisung 230 IMUL 503 in ( inch ) 10 include 497 Index 790 indexOf 408 indirekte Adressierung 502 induktive Definition 151 Infix-Notation 98 Informatik 1 Information 1 information hiding 337, 840 Informationsverarbeitung 4, 31 Infrarot, Infrarot-Übertragung 600, 622 Initialisierung 116, 213 Initiator 818 inkompatibel 123 Inline Assembler 487 innerer Knoten 363 Inorder 367 InsertionSort 309 Inspektionen 857 installieren 57 Instanz 218, 245, 786 int (Java Typ) 26 Integer (Datentyp) 26, 103, 261 integrated circuit 413 Integrated Development Environment 865 Integritätsbedingung 786, 790

    Stichwortverzeichnis Intel 38 Interface Definition Language (IDL) 851 interface section 169 internes Schema 782 internes Sortieren 336 Internet 579, 596, 629 Internet Explorer 664 Internet2 610, 621 Internetadresse 637 INTERNIC 631, 645 Interpreter 81 Interrupt 58 interrupt-enable 490 Invariante 189, 194, 269, 312, 386 Inverses 425 Inverter 443 invoke 510 IP 508, 632, 633 IP-Adresse 637 IP-Switch 614 IPv4 635 IPv6 635 IrDa 622 ISA 37 ISDN 603 ISO 9000 858 ISO-10646 13 ISO8859-1 12, 665 ISO-Latin 1 13 ISSE 523 Itanium 522 Iterable 244, 259, 295, 351 Iterator 229, 259, 351, 358, 367 Jahr 2000 Problem 828 Java 13, 25, 78, 81, 84, 110, 111, 207–275, 796, 800, 844 Java Development Kit (JDK) 207 Java Program Verifier 194 Java Version 1.1 283 Java Virtual Machine (JVM) 207, 320 java.util 341 Java_7 297 Java2-Plattform 208 javacup 734

    887 Java-Grafikmethoden 807 JavaScript 666, 693 eval 676 JCreator 208 JDK 208 JFlex 734 Jini 600 JMP 483 JNZ 483 join 787 Joystick 33 Julia-Menge 808 Just In Time Compiling (JIT) 321 Kanal 350, 597 Kante 363, 391 Karnaugh-Veitch 433 Karp 409 Karte 35, 36 Katalog 60, 539 Kaustik 824 KDE 64 Kellerautomat 710 Kellerspeicher 339 kilo (Einheit) 9 Kippschaltung 451 Klasse 83, 165, 218, 226, 253, 843 Klassendatei 228 Klassenfeld 174 Klasseninvariante 270 Klassenmethode 174 Klassenmodell 832 Klassenstrukturdiagramm 847 Kleene-Stern 699 KNF 428 Knoppix 63, 593 Knoten 363, 391 Knotentiefe 364 Knuth 409 Koaxialkabel 598 Koch’sche Schneeflocke 817 Kommandointerpreter 63, 534 Kommandomenü 63 Kommandozeile 63 Kommentar 134, 209

    888 Kommunikation 59 synchron 545 Kommunikationskanal 350, 545 Kommunikationsprotokoll 606 kommunizieren 59 Kommutativität 423 kompatibel 50 Komplement 15, 425 Komplexität 306, 308 einer Sprache 768 Komponente 244, 281 Kompression 602 Konfigurierungsdatei 57 Konjunktion 15 konjunktive Normalform 428 Konkatenation 698 konkatenieren 110 Konstante 87, 232 Konstruktor 110, 224, 340 Konsument 570 Kontext 121 Kontravarianz 263, 296 Kontrollbit 12 Kontrollstruktur 94, 126 Kovarianz 263, 296 kritischer Bereich 546 kürzester Weg 399 KV-Diagramm 433 L1, L2, L3-Cache 519, 527 Label 487 LACNIC 631 Ladeprogramm 79 LAN 609 Landeposition 48 Lands 50, 413 Last-In-First-Out 339 late binding 252 LaTeX 70 Latin-1 12 Lauf 702 Laufvariable 142 Laufzeit 179 Laufzeitfehler 82, 83, 264 Layout 68

    Stichwortverzeichnis Layout-Manager 286 leere Anweisung 127, 238 leerer Datentyp 216 leeres Wort 698 Lese-Schreibkopf 46 less 493 Levelorder 367 lex 579, 701, 736 lexikalische Analyse 696 lexikalische Ordnung 110 lexikalische Regel 133 lexikographisch 310 LGA 413 Lichtfühler 822 LIFO 339 Ligatur 69 Lindenmayer 815 lineare Rekursion 155, 345 lineare Suche 301 lineare Transformation 819 Lines of Code 827 link 73, 662 linker 541 Linksableitung 710 Linkseinheit 156 Linus Torvalds 62 Linux 62, 64, 535, 536, 555 LIR 631 Lisa 533 LISP 81, 341 Liste 341, 353–363 adaptive 362 doppelt verkettete 361 geordnete 361 perfekte Skip-Liste 362 Listener 282 Literal 211, 426 LOAD 45 loader 58 Load-Increment-Execute-Zyklus 42, 485 LOCAL 510 Local Bus 37 lock 280 Logarithmus 303 Logikgatter 430

    Stichwortverzeichnis Logik-Gitter 439 login shell 561, 573 logische Adresse 529 logische Sprache 198 logischer Wert 15 lokale Variable 239 lokales Netz 609 long (Java Typ) 26 long real 27 Longint (Delphi Typ) 26 lookahead 719, 731 Loopback-Adresse 639 Loop-Programme 755 Low-Level Formatierung 47 L-System 815, 818 Lucida 66 Lynx 664 LyX 72 Macintosh 64, 533 MacOS 535 Mail-Client 654 Mailing List 659 Mail-Server 654 Mainboard 35 Mainframes 32 makefile 584 Makro 487, 510 MAN 609 Manchester Code 601 Mandelbrot 808 Mantisse 26 map 203, 353 markup language 73 Maschinenbefehl 16, 78, 482, 486 Maschinencode 79 Maschinenprogramm 79 Maschinensprache 2, 486 Maske 494 MASM 487 Master-Slave 455 Materialeigenschaft 823 MathML 689 Maus-Ereignis 287 McCarthy 91-Funktion 152

    889 Mealy-Automat 456 mega (Einheit) 9 Mehrbenutzerbetrieb 794 Mehrbenutzersystem 556, 578 Mehrfachvererbung 260 mehrsortige Datenstruktur 106 Meilenstein 854 memory management unit 551 Mengenalgebra 430 Menü 292 Menüleiste 65 Menüsystem 63 MergeSort 327 message 158 message loop 283 message passing 545 Metazeichen 565 Methode 216, 244, 252, 843 Methodenaufruf 232, 236, 239 Microdrive 49 MicroSim 474 Microsoft 64 Midnight Commander 63 mikro ( µ ) 10 Mikrobefehle 476 Mikrobefehlsspeicher 477 Mikroprogramm 418, 481, 517 milli 10 MIME-Mail 658 MIPS 520 ML 78 MMU 551 MMX 523, 530 Mnemonics 487 mod 19 model checker 779 Modell-getriebene Architektur 852 Modellierungsverfahren 820 Model-View-Controller 851 Modem 33, 602 moderierte Newsgruppe 659 Modul 83, 169, 833 Modula 81, 169, 207 modulares Programmieren 158 Modultest 833

    890 Monitor (Bildschirm) 53 Monitor (Datenstruktur) 548 Monom 426 monoton 424 Moore 409 Moore-Automat 457 Morris 409 Mosaic Browser 663 MOSFET 442 MOS-Transistor 411 Motherboard 35, 38, 413 mount 661 MOV 483 Mozilla 664 mp3-Codierung 10 MP3-Player 417 MS-DOS 535, 555 MS-Windows 535 muCommander 63 multicast 639 Multimenge 788 Multimode Glasfaser 599 multiple access 615 multiple inheritance 260 Multiplexer 435 Multiplizierer 439 multitasking 535, 570, 589 multi-touch 592 multi-user 535 Murphy’s Gesetz 84 mutual exclusion 547 MUX 435 MVC-Muster 851 MVS 536 MX-Record 655 Nachbedingung 86, 181, 269 Nachkomme 363 Name-Server 637, 645 nand 444 nano (Einheit) 10 NAT 640 Nat (Datentyp) 103 Natürliche Zahl (Datentyp) 101 NCSA 663

    Stichwortverzeichnis Negation 15, 234, 424 Negat-Multiplizierer 439 NetBeans 208, 865 Netbook 592 Netburst-Architektur 525 Netiquette 659 Netscape 84, 655, 663, 674 Netscape Navigator 664 Network Information Center 645 Netzadresse 637 Netzliste 446 Netztopologie 610 new 214 New Paltz Program Verifier 197, 201 new-Operator 215, 224, 245 News, Newsreader 659 next 259, 351 next-state Logik 456 Nibble 7 nichtdeterministisch 87 Algorithmus 770 Automat 727 polynomial 402, 769 Stackautomat 711 Turingmaschine 770 nichtdeterministische Komplexität 770 nichtdeterministische Sprache 715 nichtdeterministischer Automat 704 nicht-druckbare Zeichen 11 nil 341 n-MOS 412, 442 Nonterminal 134, 708 nor 443 normierte Gleitpunktzahl 27 Norton Commander 63 NOT 15, 99 NP 769 NPPV 194, 197 NP-vollständig 776 NRZI 601 NRZ-L 601 NTBA 604 NTFS 590 NTM 770 null 214

    Stichwortverzeichnis NullPointerException 214 Null-Referenz 211 nullstellig 99 nullterminierter String 407 Num (Taste) 12 OBDD 779 Oberklasse 246, 247, 252 Oberon 207 Object (Java Klasse) 247, 280 Object Management Group (OMG) 851 Objectory Process 837 Objekt 83, 843 Objektcode 541 Objektmodell 832 objektorientiert 843 objektorientierter Entwurf 844 objektorientiertes Programmieren 158 odd 12 Oder-Schaltung 420 Oder-Verknüpfung 234 ODF 689 Offset 502 Oktaldarstellung, Oktalsystem 18 OLE 69, 588 Ontologie 853 OO-Analyse 837, 844 OO-Entwurf 844 OO-Methode 837 OO-Programmierung 844 OpCode 41, 483 Open Document Format 73 OPEN LOOK 586 Open Office 61, 72, 73, 689 Open Software Foundation 586 OpenBeOS 593 OpenOffice 555 OpenWindows 586 Opera Browser 664 Operation 40, 98, 114, 843 Operator 209, 232 OR 15, 99, 444 Ordner 60, 539 Ordnung 304 Organizer 3

    891 Orthogonalität 83, 144 OS/2 64, 535 OSF 586 OSI-Modell 607 Outlook 655 Output Logik 456 Overflow 25, 466, 479 Overflow-Flag 479, 491 overloading 223 owner 557 P (Komplexitätsklasse) 768 package 229, 253 page fault 554 paging 551 paging area 552 Paginierung 68 Palindrom 767 Palmtops 3 parallele Datenübertragung 607 Parallelität 59 Parallelschaltung 419 parametrisierte Datentypen 261 PARC 64 parity 12, 42 Parity-Bit 42 Parser 582 Parsergenerator 734 Parsing 698 partial correctness assertion 182 partielle Funktion 742 partielle Korrektheit 181 partielle Operation 105 Partitionierung 324 Pascal 78, 81, 83, 95, 207 Pascal Schlüsselworte DO 130 END 127 REPEAT 141 VAR 115 WHILE 130 pattern 562 pattern matching 199, 203, 298 PCA 182 PCI 37

    892 PDA 595 PegasusMail 655 Pegelsprung 601 Pen 33 Pentium Prozessor 523, 530 Pentium-4 45, 417, 524, 525, 526 Pentium-M 525 Perl 666, 684 Perspektivprojektion 819 pervasice computing 600 peta (Einheit) 9 Pfad 61, 363, 392, 558 Pfadlänge 363 PGA 413 Phase 470 Photon-Mapping 824 php 666 pico 10 picture element 53 Pin 35 pipe 350, 566 Pipeline 518 pits 50 Pivot-Element 323 Pixel 53, 282, 802 PLA 441 Platine 35 Plattencontroller 36 Plattencrash 48 Plattenspeicher 59 Plattform 281 Plattform-Unabhängigkeit 81, 281, 288 Plotter 33 plug and play 40, 591 plug and pray 40 p-MOS 412, 442 Pointer 165 Pointerarithmetik 84 polling 610 polygon meshes 820 Polygonflächen 820 polymorph 119, 844 Polymorphie 107, 248 polynomial 307 POP 483

    Stichwortverzeichnis pop 339 POP3 654 POPMail 655 Port 512, 634 Nummer 633 well known 634 Portabilität 209 portieren 487 POST 672 posten 659 Postfix-Notation 344, 369 Postorder 367 POWER 520 PowerPC 521 Präambel 601 Prädikat 340 Präfix 98 Praktische Informatik 2 Pratt 409 Präzedenz 98, 237, 732 preemption 543 preemptive multitasking 543, 589 prellen 452 Preorder 367 Primärschlüssel 783, 786 Primimplikant 435 primitive Rekursion 152, 757 Primzahlzwilling 569 Priorität 542 Prioritäts-Codierer 437 Priority-Queue 390 private 222, 247, 253 Privilegierungsstufen 535 Problemanalyse 832 producer-consumer 350, 545, 568 Produktion 708 Produzent 570 Profiler 180, 300 Programm 16, 78, 226, 228 programmable logic array 441 Programmiersprache 2, 78, 89, 207 Programmierumgebung 829 Programmschema 196 Programmtransformation 157 Programm-Verifikation 856

    Stichwortverzeichnis Programm-Verifizierer 193 Projektauftrag 853 Projektion 787, 819 Projektleiter 853 Projekt-Management 831, 853 Projektorganisation 829 Projektplan 853 Projektsteuerung 854 Projektteam 853 Prolog 78, 81, 198, 747 Proportionalschrift 66 protected 247, 253 protected mode 528, 529, 531, 587 Protokoll 37, 76, 606 Provider 629, 643 Prozedur 216 Prozess 535 Prozesse 59, 350, 540 kommunizieren 545 parallele 544 quasi-parallele 544 Prozesskommunikation asynchron 545 synchron 545 Prozesskontrollblock 541 Prozessor 40 Prozesssystem 62 Prozessverwaltung 59 Prüfbit 42, 491 Pseudooperationen 510 pt 67 public 222, 228, 253 Puffer 347, 350, 432, 460, 546, 568 Pufferspeicher 44 pull-down (CMOS) 442 Pulldown-Menü 65 pull-up (CMOS) 442 PUSH 483 push 339 Pythagoras Baum 807 Python 90, 133 QNX 593 Quadwort 8 Qualitäts-Anforderungen 855

    893 Qualitätssicherung 270, 855 Qualitätsziele 855 Querverweis 68 Query-View-Transform 853 Queue 347, 350, 545, 568 QuickSort 322 Quicksort 201, 203, 323 Quotient 19 Rabin 409 Radiosity 825 RadixSort 329 railroad diagram 134 RAM 38, 39, 43, 416 RAM-Disk 57 Random Access 416 Rastergrafik 803 Rasterpunkt 16 Rational Unified Process (RUP) 837 rationale Zahl 105 Ray-Tracing 821, 823 real mode 587, 588 Realisierung 835 Real-Mode 488 Rechnernetze 595 Rechteckkurve 597 Rechtsableitung 710 recursive descent 718 Red-Black-Baum 382 reduce-reduce-Konflikt 727, 731 Reduzierbarkeit 772 Reed Solomon Code 49 Referenz 114 Referenz-Datentyp 213, 214, 220 Referenztyp 84 Reflexion 825 diffuse 822 direktionale 821 spiegelnde 821 Reflexionsverhalten 821 Register 40, 464, 471 AX 488 BP 488 BX 488 CS 488, 512

    894 CX 488 DI 488 DS 488 DX 488 EBP 508 EBX 502 EDI 502 ES 488 ESI 502 ESP 508 IP 488 SI 488 SP 488 SS 488 Registermaschine 751 Registrierung 631, 645 reguläre Sprache 698 regulärer Ausdruck 134, 565, 687, 700, 703 rekursiv 146 rekursive Funktion 145 rekursive Gleichung 140 rekursive Prozedur 147 rekursiver Abstiegs 718 Relais 425 Relation 107, 786 relationale Algebra 787 relationales Datenmodell 785 Relationenschema 786 relativer Bezug 74 remote login 661 remove 259, 351 Rendering 821 Rendezvous 545 Repeater 613 Repository 864, 865 Repräsentation 4 Requirements Engineering 832 return-Anweisung 216, 239 Review 857 Revisionspunkte 838 RFC 630 ringförmige Netze 611 RIPE 631 ripple-carry adder 449 RIR 631

    Stichwortverzeichnis RISC 514 RISC-Architekturen 520 RISC-Prozessor 418 ROM 39, 477, 534 root 556 Rotation 820 round robin 544 Router 614, 633 routing 613, 614 RPL 514 rpm 46 RS-Flip-Flop 451 rückgekoppelt 450 Rundungsfehler 28 sans serif 66 SAT 765 Satzform 708 Scala 298 Scanline-Algorithmus 820 Scanner 33, 582, 697 Schaltfunktion 422 Schalttabellen 420 Schatten 823 Schattenspeicher 552 Schattieren 821 scheduler 275, 542, 544 Schleife 92, 129 Schleifeninvarianten 269 Schlüssel 310 Schlüsselwort 133, 211 Schnittstelle 33, 35, 39, 56, 833 Schnittstellen-Lexikon 860 Schriftart 66 Schriftgrad 67 Schrifttyp 67 Schrittmotor 46 schrittweise Verfeinerung 839 Schusterjunge 69 schwächste Vorbedingung 186 SDH 619 SDRAM 44 Sechzehner-System 7 Second Life 76 Segment 497, 511, 550

    Stichwortverzeichnis Segmentregister 488, 490 Sehstrahl 822 Seiteneffekt 123 Seitentabelle 552 Sektor 46 Selbstähnlichkeit 811 selbstsynchronisierender Code 601 select 293 SelectionSort 309, 316 Selektion 787 Selektor 110, 340 Selektor-Register 514 Semantik 126, 138, 763 semantische Eigenschaft 763 Semaphore 548 sendmail 655, 658 sequentiell 43 sequentielle Komposition 126, 127 sequentielle Schaltung 457 sequentieller Zugriff 45 serielle Übertragung 607 serien-parallel 421 Serien-Parallel-Kreis 420 Serienschaltung 419 Serifen 66 Server 537, 589, 648 ServerSocket 648 setup.exe 57 shared memory 545 shell 63, 560 shell scripts 576 ShellSort 321 Shift-Operationen 504 shift-reduce parser 724 shift-reduce-Konflikt 727, 731 short (Java Typ) 26 short real 27 Shortint (Delphi Typ) 26 Sicht 790 Sichtbarkeit 820 Sichtbarkeits-Algorithmus 823 Sidebar 591 sign flag 466, 479 Signal 57 Signatur 121, 169, 223

    895 signed number 491 SIMD 530 SIMM 43 Simplexverfahren 606 single inheritance 260 Singlemode Glasfaser 599 Skalierung 820 skip 137 Skip-Liste 301, 362 slash 61 slice 303 sliding window 634 Slot 37 Slot-Time 617 Smalltalk 81, 105, 292, 533, 844 Smartphone 595 SMTP 630, 655, 658 Sockel 35 Socket 648 SoftIce 499 Software 2, 54 Bürokratie 834 Engineering 828, 830 Entwicklung 827 Metrik 857 Produkt 827 Projekt 827, 853 Prüfverfahren 856 Testsystem 862 zuverlässige 827 Software-Entwicklung evolutionäre 838 Software-Entwicklungsumgebung 829, 864 Software-Ergonomie 863 Software-Interrupts 512 Softwaretechnik 830 Solaris 536 solid modelling 820 Sonderzeichen 11, 55 SONET 619 Sortieralgorithmus 309 sortiert 304 Soundkarte 37 source 412, 442 Spannbaum 393

    896 SPARC 516 SPARC-Architektur 520 späte Bindung 252 SPEC 520 special file 556, 560 SpeedCommander 63 Speicher 124 Speicheradressierung 501 Speicherbereinigung 245 Speicherblock 46 Speicherchip 43 Speicherkapazität 45 Speicherkarte 417 Speichermodul 43 Speicherseite 551 Speicherveränderung 125 Speicherverwaltung 59 Speicherzelle 450, 461 Speicherzustand 124 Spezifikation 85, 169, 181 Spezifikationssprache 860 spezifizieren 84 Spielstrategie 149 Spiralmodell 835 Splay-Baum 382 Split-Operation 383 Sprache 699 Sprache zu einer Grammatik 710 spread sheet 3, 73 Sprung 79 Sprungadresse 487 Sprungbefehl 40, 495 SP-Term 421 Spur 46 SQL 789, 796, 800 SQL-Anfrage 790 S-RAM 462 SRAM 44, 416 ssh 661 stabil 451 stabiles sortieren 329 Stack 155, 339 Stackautomat 710 Stackmaschine 710 Stadtnetz 609

    Stichwortverzeichnis standard input 563 standard output 563 Standard-Konstruktor 167, 224 Standard-Pakete 231 Stapel 155, 339 static 168, 174, 221, 245, 250 statisch getypt 123 statische Bindung 252 Steckleiste 35 Stellenzahl 98 Stereotyp 847 sternförmiges Netz 610 Steuerzeichen 11 STORE 45 Strahlverfolgungs-Algorithmus 821 Streamer 33 Strg (Taste) 12 String 15, 212, 407 StringBuffer 111, 408 Stringliteral 110 Stringsuche 409 Structured Analysis 860 strukturierte Analyse 841 strukturierte Programmierung 159, 839 strukturierter Entwurf 841 style file 69 style sheet 69, 673 subdirectory 60 subdomain 644 Subsystem 589 Suchbaum 372 Suchmuster 409 Suchproblem 301 SUN 84, 207 SunOs 64 super 263 super (Java Schlüsselwort) 248 Super-Computer 32 superskalar 519 SVG 689 swap 217, 313 swap space 543 Swapping 551 SWI-Prolog 202 Switch 614

    Stichwortverzeichnis switch-Anweisung 139, 240 SW-technischer Entwurf 832 Symboltabelle 500, 740 synchrone Datenübertragung 600 synchronized 279 syntactic sugar 137 syntaktische Regel 134 syntaktischer Fehler 179 Syntax 81, 126, 137 Syntaxbaum 716 Syntaxdiagramm 134 Syntaxfehler 81 Synthetisierte Werte 739 Syrakus-Problem 93 Systembus 37 SystemC 446 Systementwicklung evolutionär 833, 836 inkrementell 836 objektorientiert 837 System-Integration 833 Systemtest 833 Szene 819 Szene-Würfel 819 T (Wahrheitswert) 15 Tab (ASCII-Zeichen) 11 Tablet-PC 3 T-ADSL 606 tail recursive 153 Taktgeber 37, 39, 470 TAS 547 Task 534, 540 Taskwechsel 541 TASM 487 TCP 632 TCP/IP 76, 632 TCP-Protokoll 633 TCP-Verbindung 652 TD32 500 T-DSL 604, 606 Technische Informatik 1 Teile und Herrsche 201 Teilhaberbetrieb 536 Teilnehmerbetrieb 536

    897 telnet 632 Temporale Logik 87 temporäre Gleitkommazahl 28 tera (Einheit) 9 Term 117, 421 Terminal 134, 708 Terminal-Emulator 586 Terminator 407 terminieren 92 Terminierung 182, 196 Test and Set 547 Testdaten 862 testen 181 Testfälle 862 Testrahmen 862 TeX 70 Text 11 Texteditor Datenstruktur für 346 Textmodus 54 Textur 823 TFT 53 Theoretische Informatik 2 Thesaurus 68 this (Java Schlüsselwort) 172, 248, 250 Thread 275, 541 throws 264, 266 Tiefensuche 395 Timer 39 Times Roman 66 timesharing 536, 542 TKIP 628 TLB 529, 552 TLD 644 Token 582, 611, 696 Toolbar 292 top 339 top down Entwurf 840 top-down 723 Toplevel-Domain 644 Torvalds 555 Total Commander 63 totale Korrektheit 196 Tour 401 tpi 46

    898 track 46 Transaktion 280, 536, 795, 859 Transaktionsmonitor 536 Transformations-Ansatz 834 Transistor 411, 442 transitive Hülle 398 Translation 820 Transmission 823 Transmissionsverhalten 821 transparent 57 trap flag 490 travelling salesman problem 402, 405 Traversierung 395 Treiber, Treiberprogramm 57 Trennzeichen 133, 209 Tristate Buffer 460 true 15 try 267 TSP 402, 765 tuProlog 202 Turbo Debugger 32 499 Turbo-Pascal 25, 82, 83, 114 Turbo-Prolog 201 Turing-berechenbar 751 Turingmaschine 747 Turing-Post-Language 750 Turingtabelle 750 Türme von Hanoi 148 Turtle-Grafik 804, 812 Typdefinition 160 Typdeklaration 337 Typfehler 81, 123, 125 typografischer Punkt 67 Typ-Parameter 261 Typschranke 262 Typumwandlung 236 überabzählbar 746 überladen 104 Überschwinger 597 Übersetzungseinheit 228 Übertrag 21 ubiquitous computing 600 UCS 13, 665 UCS Transformation Format 14

    Stichwortverzeichnis UDP 635 Ulam-Algorithmus 92 Ulam-Funktion 152, 179 Ulam-Problem 93 Ultraedit 208 UML 837, 845, 846, 850, 860 UML 2.0 850 unbedingter Sprung 495 Und-Schaltung 420 Und-Verknüpfung 234 ungerichteter Graph 392 Unicode 13, 108, 209, 665 Unified Modeling Language 845, 846 Unit 83, 169 Universal Character Set 13 universelles Schaltglied 436 UNIX 13, 64, 555 UNIX Befehle bg 572 cat 561, 562, 564 cd 559 chmod 558 crontab 573 csh 574 diff 564 echo 575 emacs 580 fg 571 finger 578 grep 565 head 565 jobs 571 kill 573 ksh 574 lex 582 login 561 lp 572 ls 562 make 584 man 561 mount 560 Optionen für 562 pine 655 ps 573 pwd 559

    Stichwortverzeichnis set 575 sh 574 sort 564 tail 565 talk 579 umount 560 vi 580 wall 579 who 578 write 579 yacc 582 unsigned number 491 Unterbaum 363 Unterklasse 174, 246, 247, 252 Unterprogramm 216 Unterschneidung 69 Unterverzeichnis 60 UpHeap 387 upload 605 UPnP 600 upstream 605 URL 663, 798 USB-Schnittstelle 35, 39 USB-Stick 417 use case 837, 846 user interface 535 UTF-8 13, 14 UTP 598 uudecode 13 uuencode 13 Variable 87, 93, 114, 213, 232 Variablendeklarationsanweisung 238 VB Script 684 VCC 442 VDM 85 VDSL 604 Vektorgrafik 803 Verbund 787 Verbundanweisung 127 verdrängt 543 verdrillte Kabel 598 Vererbung 246, 263, 844 Vergleichsoperation 233 Verifikationsbedingung 193

    899 Verilog 446 verkettete Liste 342 Verklemmung 280, 549 verkürzte Auswertung 101, 120, 150 verlustbehaftet 16 Vermittlungsrechner 613, 632 verteiltes System 545 Verweis 73 Verzeichnis 60, 539 VHDL 446 vi 580 virtual device 57 Virtual PC 63 VirtualBox 63 virtuelle Adresse 551 virtuelle Geräte 57 virtuelle Java-Maschine 81 virtuelle Maschine 2, 80 virtuelle Realität 820 VisiCalc 73 Vista 591 Visual Basic 796 Visual-Prolog 201 VLIW 522 VLSI 413 VLSI-Werkzeuge 447 VM/SP 536 VMware 63 void 216 Volladdierer 438 vollständiger Baum 384 Volumenmodell 820 Vorbedingung 86, 181, 269 vordefinierter Bezeichner 133 Vorfahre 363 Vorgehensmodell 830, 854 nichtsequentiell 835 Vorwärts-Referenz 151 vorzeichenbehaftet 503 Vorzeichenbit 26 Vorzeichendarstellung 22 vorzeichenlose Zahl 17 VRAM 801 VRML 826

    900 W3C 663, 665 Wafer 414 Wahrheitswert 15, 419, 429 walkthrough 857 WAN 609 Warchalking 627 Warshalls Algorithmus 398 Warteschlange 347, 570 Wasserfall-Modell 831 WDM 599, 620 weakest precondition 186 Web 662 Web 2.0 693 webcam 75 wechselseitige Rekursion 151 wechselseitiger Ausschluss 547 Weg (Graph) 392 Weiterentwicklungs-Strategie 835 Weitverkehrsnetz 609 well-known port 634 WEP 628 Werkzeugleiste 65, 292 Wettkampfverfahren 611, 615 while-Anweisung 96, 241 While-Programme 753 while-Regel 189 while-Schleife 126 Whitespace 209 Wiederholungspräfix 502 Wi-Fi 623 wildcard (Dateimuster) 562 wildcard (dateimuster) 562 wildcard (Java) 263 Win32 589 Winchester 47 Windows 7 592 Windows ME 523, 588 Windows NT 62, 523, 535, 588, 589 Windows Vista 591 Windows XP 62, 523, 535, 590 Windows2000 62, 589 Windows3.x 589 Windows95 523 Windows98 523, 588 Windows-Desktop 535

    Stichwortverzeichnis wire frame 820 Wireless LAN 623 Wirth 83 WLAN 600, 606, 622, 623, 626 Workstations 32 World Wide Web 75, 662 Worst Case 306 Wort 8, 698, 815 WPA 628 Wrapper 261 Wurzel 60, 363 WWW 75, 629, 654, 662 WWW-Client 664 WWW-Server 663 wysiwyg 66 wysiwym 72 X Window System 584 x86 522, 523 X-Client 585 Xerox 64 XHTML 689, 690 XML 663, 684 DOCTYPE 689 Elemente 686 gültig 687 Namensräume 690 processing instructions 686 Schema 689 valid 687 wohlgeformt 686 XOR 15, 84, 432 xor 100, 439 XPATH 692 XSD 689 X-Server 585 XSLT 691 xterm 586 X-Win 619, 620, 621 yacc 579, 724, 736 Z (Spezifikationssprache) 85 Zeichen 212 Zeichengenerator 54

    Stichwortverzeichnis Zeichenkette 15, 110, 407 Zeichensatz 209 Zeiger 165 Zeilenende 209 Zeitmultiplex 542 Zeitmultiplexverfahren 536 Zeitscheibe 543 Zeitscheibenverfahren 542 Zellbibliothek 418 zentriert 67 Zero-Flag 466, 479, 487, 491, 495, 503 Zoll 10 Z-Puffer-Algorithmus 820 Zugriffsprotokoll 615 Zugriffsrecht 252

    901 Zugriffszeit 45 Zuhörer 282 zusammengesetzter Typ 110 zusammenhängend 392 Zuweisung 88, 93, 124 Zuweisungsausdruck 124, 125, 234, 236 Zuweisungsoperator 235 Zuweisungsregel 184 Zuweisungszeichen 125 Zweierkomplement 23 zweistellige Operation 98 Zwischenbehauptung 180 Zyklus 392 Zylinder 47