Sie sind auf Seite 1von 369

Springer-Lehrbuch

Thomas Rießinger

Informatik für Ingenieure


und Naturwissenschaftler
Eine anschauliche Einführung
in das Programmieren mit C und Java

Mit 64 Abbildungen

123
Thomas Rießinger
Fachhochschule Frankfurt
Fachbereich Informatik
Kleiststr. 3
60318 Frankfurt am Main
Deutschland
riessinger@aol.com

Bibliografische Information der Deutschen Bibliothek


Die Deutsche Bibliothek verzeichnet diese Publikation in der Deutschen Nationalbibliografie; detaillierte
bibliografische Daten sind im Internet über http://dnb.ddb.de abrufbar.

ISBN-10 3-540-26243-1 Springer Berlin Heidelberg New York


ISBN-13 978-3-540-26243-5 Springer Berlin Heidelberg New York
Dieses Werk ist urheberrechtlich geschützt. Die dadurch begründeten Rechte, insbesondere die der Überset-
zung, des Nachdrucks, des Vortrags, der Entnahme von Abbildungen und Tabellen, der Funksendung, der
Mikroverfilmung oder der Vervielfältigung auf anderen Wegen und der Speicherung in Datenverarbeitungs-
anlagen, bleiben, auch bei nur auszugsweiser Verwertung, vorbehalten. Eine Vervielfältigung dieses Werkes
oder von Teilen dieses Werkes ist auch im Einzelfall nur in den Grenzen der gesetzlichen Bestimmungen des
Urheberrechtsgesetzes der Bundesrepublik Deutschland vom 9. September 1965 in der jeweils geltenden Fassung
zulässig. Sie ist grundsätzlich vergütungspflichtig. Zuwiderhandlungen unterliegen den Strafbestimmungen des
Urheberrechtsgesetzes.
Springer ist ein Unternehmen von Springer Science+Business Media
springer.de
© Springer-Verlag Berlin Heidelberg 2006
Printed in Germany
Die Wiedergabe von Gebrauchsnamen, Handelsnamen, Warenbezeichnungen usw. in diesem Werk berechtigt
auch ohne besondere Kennzeichnung nicht zu der Annahme, daß solche Namen im Sinne der Warenzeichen- und
Markenschutz-Gesetzgebung als frei zu betrachten wären und daher von jedermann benutzt werden dürften.
Sollte in diesem Werk direkt oder indirekt auf Gesetze, Vorschriften oder Richtlinien (z. B. DIN, VDI, VDE)
Bezug genommen oder aus ihnen zitiert worden sein, so kann der Verlag keine Gewähr für die Richtigkeit,
Vollständigkeit oder Aktualität übernehmen. Es empfiehlt sich, gegebenenfalls für die eigenen Arbeiten die
vollständigen Vorschriften oder Richtlinien in der jeweils gültigen Fassung hinzuziehen.
Satz: Digitale Druckvorlage des Autors
Herstellung: LE-TEX Jelonek, Schmidt & Vöckler GbR, Leipzig
Umschlaggestaltung: design & production GmbH, Heidelberg
Gedruckt auf säurefreiem Papier 7/3142/YL - 5 4 3 2 1 0
Vorwort

Vergessen musst du das, was bisher du gelernt.“



Meister Yoda, Jedi-Ritter

Nicht jeder ist zum Jedi-Ritter geboren, die meisten Menschen ergreifen
alltäglichere Berufe. Wenn man genauer hinsieht, dann ist das auch sehr
verständlich, denn sowohl das Berufsbild als auch der Ausbildungsgang der
Jedi-Ritter machen doch einen etwas eigenartigen Eindruck. Was treibt so ein
Ritter den ganzen Tag? Man kann schließlich nicht immer nur mit dem Licht-
schwert herumfuchteln, allen möglichen Leuten wünschen, dass die Macht mit
ihnen sein möge, und ansonsten bedeutungsschwere Sätze mit einer seltsamen
Satzstellung murmeln - ganz zu schweigen von der Frage, nach welchem Tarif
ein Jedi-Ritter wohl bezahlt werden mag. Und auch über die Ausbildung eines
Jedi weiß man wenig. Soll er wirklich alles vergessen, was er je gelernt hat, ein-
schließlich seiner Sprache und seiner Tischmanieren? Ich will es nicht hoffen,
zumal die Jedi-Ausbildung sehr lange dauert und man somit viel Gelegenheit
hat, vieles zu vergessen.
Ihre Lage ist dagegen viel angenehmer. Als angehender Ingenieur oder Na-
turwissenschaftler haben Sie eine deutlichere Vorstellung von Ihrem zukünfti-
gen Beruf, als sie ein angehender Jedi-Ritter haben kann, wenn man auch nicht
ausschließen darf, dass Sie in ferner Zukunft einmal ein Lichtschwert konstru-
ieren werden. Und was Ihre Ausbildung betrifft, so ist ihr Aufbau ziemlich
klar, und Sie können Ihrer Studienordnung entnehmen, was darin vorkommen
wird - sicher keine Schwertkämpfe und keine Übungen in der Anwendung der
geheimnisvollen Macht, dafür eher handfeste Dinge wie Mathematik, Physik
und eben auch Informatik.
Trotzdem gibt es die eine oder andere Gemeinsamkeit zwischen Ihnen und
einem auszubildenden Jedi. So wenig auch über die Ausbildung der Jedi be-
kannt ist: dass sie sich immer und immer wieder in Geduld und Konzentration
üben müssen, ist unbestritten. Das ist kein Nachteil, weder für einen Jedi noch
für einen Nicht-Jedi, denn ob man es nun mit der dunklen Seite der Macht auf-
nehmen muss oder mit einem schlichten C-Compiler - für beides braucht man
viel Geduld und bei beidem muss man sich nicht wenig konzentrieren. Gerade
wenn es um Informatik geht, kann ich Ihnen diese beiden Jedi-Eigenschaften
sehr empfehlen. Sie werden in diesem Buch zunächst mit einigen allgemei-
VI Vorwort

nen Grundlagen der Informatik Bekanntschaft machen und dabei feststellen,


dass auch das nicht ganz ohne Konzentration und Geduld funktioniert. So
richtig geht es aber erst nach den Grundlagen los, sobald Sie beginnen, Ihre
ersten Programme zu schreiben. Programmieren kann man nicht einfach so
im Vorbeigehen, sozusagen durch Fingerschnippen, sondern nur, indem man
sich erstens mit den Eigenarten der jeweiligen Programmiersprache vertraut
macht und zweitens in aller Ruhe versucht, sein Problem mithilfe eines Pro-
gramms zu lösen. Das wird nicht immer sofort gut gehen, das wird sogar
ziemlich häufig erst mal schief gehen: machen Sie sich nichts daraus, das ist
völlig normal und passiert jedem.
Genau das ist der Grund für die Gemeinsamkeiten zwischen angehenden
Jedi-Rittern und Ihnen. Die Geduld brauchen Sie zwar aus anderen Gründen,
Ihre Konzentration richtet sich auf andere Probleme, aber dringend nötig sind
sie beide, sowohl für den Jedi als auch für Sie. Ich werde Ihnen hier zwei Pro-
grammiersprachen zeigen, die prozedurale Sprache C und die objektorientierte
Sprache Java. Voraussetzen werde ich so gut wie gar nichts, abgesehen von
den Grundrechenarten und Ihrer Bereitschaft, sich an diesen beiden Sprachen
zu versuchen. Aber wie lernt man denn nun Programmieren, egal in welcher
Sprache? Ganz einfach: durch Programmieren. Natürlich können Sie nicht ein-
fach so damit anfangen, sondern müssen erst einmal lesen, wie man das macht,
aber Lesen alleine wird nicht ausreichen. Um sich an das Programmieren und
an die eingesetzten Programmiersprachen zu gewöhnen, bleibt Ihnen nichts
anderes übrig als zu üben, nicht nur einmal oder zweimal, sondern oft und
immer wieder - die Rolle von Geduld und Konzentration sollte man dabei nicht
unterschätzen. Deshalb enthält das Buch auch zu jedem Abschnitt Übungs-
aufgaben, an denen Sie das, was Sie gelernt haben, ausprobieren können.
Und was sollen Sie machen, wenn Sie mit einer Aufgabe trotz allem nicht
zurechtkommen? Nicht weiter schlimm, das kann passieren, auch dafür ist vor-
gesorgt. Die Lösungen der eher theoretischen Aufgaben habe ich am Ende des
Buches aufgeschrieben, damit Sie sie nachlesen und mit Ihren eigenen Lösun-
gen vergleichen können. Bei den Programmieraufgaben erschien mir das aber
weniger sinnvoll, denn auch Programme, mit denen Sie selbst vielleicht Pro-
bleme hatten, sollten Ihnen in einer Form zur Verfügung stehen, die das Arbei-
ten mit den Programmen erlaubt. Haben Sie gesteigerte Lust, Programmtexte
abzutippen? Nein? Dachte ich mir. Deshalb finden Sie die Lösungen aller Pro-
grammieraufgaben unter der im Lösungsteil angegebenen Webadresse, von wo
Sie sie herunterladen können.
Für den Moment habe ich wohl genug geredet, also beenden wir die An-
sprache und fangen einfach an.

Frankfurt, Juni 2005 Thomas Rießinger


Inhaltsverzeichnis

1 Grundlagen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
1.1 Einführung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
1.1.1 Das EVA-Prinzip . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
1.1.2 Unterschiede zwischen maschineller und manueller
Datenverarbeitung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
1.1.3 Aufgabenbereiche des Computers . . . . . . . . . . . . . . . . . . . . 6
1.1.4 Computertypen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
1.1.5 Informatik . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
1.2 Wie alles begann . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
1.2.1 Der Abakus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
1.2.2 Mechanische Rechenmaschinen . . . . . . . . . . . . . . . . . . . . . . 12
1.2.3 Die Lochkartenmaschine von Hollerith . . . . . . . . . . . . . . . 14
1.2.4 Die Analytische Maschine von Babbage . . . . . . . . . . . . . . 17
1.2.5 Elektromechanische und elektronische Rechner . . . . . . . . 18
1.2.6 Programmiersprachen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
1.3 Rechneraufbau . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
1.3.1 Der Aufbau eines Taschenrechners . . . . . . . . . . . . . . . . . . . 25
1.3.2 Die Architektur eines von Neumann-Rechners . . . . . . . . . 28
1.3.3 Arbeitsspeicher und Festplattenspeicher . . . . . . . . . . . . . . 31
1.4 Binäre Arithmetik . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
1.4.1 Daten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
1.4.2 Binärzahlen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
1.4.3 Umrechnungsverfahren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
1.4.4 Addition . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
1.4.5 Subtraktion und Zweierkomplement . . . . . . . . . . . . . . . . . 49
1.4.6 Multiplikation und Division . . . . . . . . . . . . . . . . . . . . . . . . . 54
1.4.7 Computerorientierte Umrechnungsverfahren . . . . . . . . . . 56
1.4.8 Hexadezimalzahlen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
1.5 Logische Schaltungen und Addierer . . . . . . . . . . . . . . . . . . . . . . . . 61
1.5.1 Die UND-Schaltung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62
1.5.2 Die ODER-Schaltung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64
VIII Inhaltsverzeichnis

1.5.3 Die NICHT-Schaltung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66


1.5.4 Der Halbaddierer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66
1.5.5 Der Volladdierer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70
1.5.6 Negative Zahlen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76

2 Strukturierte Programmierung mit C . . . . . . . . . . . . . . . . . . . . . . 81


2.1 Einführung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82
2.1.1 Algorithmen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83
2.1.2 Programmiersprachen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84
2.1.3 Software . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87
2.2 Erste C-Programme . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90
2.2.1 Die Entwicklung von C . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91
2.2.2 Ein erstes Programm . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92
2.2.3 Variablen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 96
2.2.4 Eingabe von der Tastatur . . . . . . . . . . . . . . . . . . . . . . . . . . 99
2.2.5 Arithmetik . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 101
2.2.6 Datentypen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 104
2.3 Kontrollstrukturen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109
2.3.1 Sequenz . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 110
2.3.2 Auswahl . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 114
2.3.3 Wiederholung als nicht abweisende Schleife . . . . . . . . . . . 127
2.3.4 Wiederholung als abweisende Schleife . . . . . . . . . . . . . . . . 133
2.3.5 Wiederholung als Zählschleife . . . . . . . . . . . . . . . . . . . . . . . 136
2.3.6 Felder . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 140
2.3.7 continue und break . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 149
2.4 Zeiger und dynamische Datenstrukturen . . . . . . . . . . . . . . . . . . . . 152
2.4.1 Zeiger . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 153
2.4.2 Noch einmal Felder . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 159
2.4.3 Verkettete Listen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 163
2.5 Funktionen und Rekursion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 179
2.5.1 Funktionen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 179
2.5.2 Vordefinierte Funktionen . . . . . . . . . . . . . . . . . . . . . . . . . . . 186
2.5.3 call by value und call by reference . . . . . . . . . . . . . . . . . . . 187
2.5.4 Rekursion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 190
2.6 Dateien . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 195

3 Objektorientierte Programmierung mit Java . . . . . . . . . . . . . . . 201


3.1 Strukturierte Programmierung mit Java . . . . . . . . . . . . . . . . . . . . 203
3.1.1 Die Entwicklung von Java . . . . . . . . . . . . . . . . . . . . . . . . . . 203
3.1.2 Compiler und Interpreter . . . . . . . . . . . . . . . . . . . . . . . . . . . 204
3.1.3 Erste Schritte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 208
3.1.4 Standardeingabe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 213
3.1.5 Kontrollstrukturen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 217
3.2 Klassen, Objekte und Methoden . . . . . . . . . . . . . . . . . . . . . . . . . . . 224
3.2.1 Klassen und Objekte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 224
Inhaltsverzeichnis IX

3.2.2 Methoden . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 227


3.2.3 Konstruktoren und set/get-Methoden . . . . . . . . . . . . . . . . 232
3.2.4 Überladen von Methoden . . . . . . . . . . . . . . . . . . . . . . . . . . . 237
3.2.5 Felder und Vektoren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 239
3.2.6 Hüllenklassen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 243
3.2.7 Zeiger . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 244
3.2.8 Die Klasse String . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 247
3.2.9 Methodenaufrufe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 248
3.2.10 Rekursion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 251
3.3 Vererbung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 254
3.3.1 Erweitern einer Klasse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 254
3.3.2 Konstruktoren und Zuweisungen . . . . . . . . . . . . . . . . . . . . 259
3.3.3 Zugriffsattribute . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 261
3.3.4 Überschreiben von Methoden und Polymorphie . . . . . . . . 263
3.3.5 Erweitern von Subklassen . . . . . . . . . . . . . . . . . . . . . . . . . . 268
3.3.6 Die Klasse Object . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 269
3.3.7 Innere Klassen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 270
3.4 Abstrakte Klassen und Interfaces . . . . . . . . . . . . . . . . . . . . . . . . . . 276
3.4.1 Abstrakte Klassen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 277
3.4.2 Interfaces . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 283
3.4.3 Adapterklassen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 287
3.4.4 Anonyme Klassen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 289
3.5 Ausnahmebehandlung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 293
3.5.1 Exceptions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 294
3.5.2 Auslösen von Exceptions . . . . . . . . . . . . . . . . . . . . . . . . . . . 296
3.5.3 Abfangen von Exceptions . . . . . . . . . . . . . . . . . . . . . . . . . . . 299
3.5.4 Selbstdefinierte Exceptionklassen . . . . . . . . . . . . . . . . . . . . 303
3.5.5 Finally . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 305
3.5.6 Exceptionklassen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 306
3.6 Dateien und Streams . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 308
3.6.1 Streams . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 309
3.6.2 Textdateien . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 310
3.6.3 Datendateien . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 315
3.6.4 Objektdateien . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 319
3.6.5 Die Standardstreams . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 321
3.7 Ein wenig über GUI . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 324
3.7.1 Ereignisbasierte Programmierung . . . . . . . . . . . . . . . . . . . . 324
3.7.2 Ein erstes Beispiel . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 325
3.7.3 Schaltflächen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 332

4 Lösungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 339
4.1 Grundlagen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 339
4.2 Strukturierte Programmierung mit C . . . . . . . . . . . . . . . . . . . . . . 347
4.3 Objektorientierte Programmierung mit Java . . . . . . . . . . . . . . . . 353
X Inhaltsverzeichnis

Literaturverzeichnis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 355

Sachverzeichnis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 357
1
Grundlagen

Schwer zu sehen; in ständiger Bewegung ist die



Zukunft.“
Meister Yoda, Jedi-Ritter

Wenn Sie sich mit Informatik beschäftigen wollen oder es sogar müssen, weil
der Studienplan es nun mal vorsieht, dann sollten Sie eine Vorstellung davon
haben, was so ein Computer kann und was er nicht kann, und Sie sollten ihn
weder über- noch unterschätzen. Beides habe ich allerdings immer wieder er-
lebt. Das vielleicht interessanteste Beispiel hat mir vor einigen Jahren eine
Informatikstudentin des ersten Semesters geliefert, die sich mit der Aufgabe
plagte, die bekannte und beliebte p, q-Formel zur Lösung quadratischer Glei-
chungen in der Programmiersprache Pascal zu programmieren. Nun will ich
Ihnen auf der ersten Seite dieses Buchs nicht gleich mit Formeln auf die Nerven
fallen und nur sagen, dass man bei dieser Formel aus den beiden gegebenen
Werten p und q die beiden Lösungen einer quadratischen Gleichung ausrech-
nen kann. Das zu programmieren hat die Studentin dann auch versucht; ihr
Programm sorgte dafür, dass der Benutzer die Zahlen p und q eingab, das
war in Ordnung. Und dann ließ sie den armen Benutzer auch gleich noch die
Lösung eingeben und war der Meinung, das Problem wäre jetzt gelöst.
Vielleicht haben Sie schon einmal das Wort Softwarekrise“ gehört, da-

mit bezeichnet man eine Phase etwa in den sechziger Jahren des zwanzig-
sten Jahrhunderts, in der die Informatiker merkten, dass planloses Vor-sich-
hin-Programmieren ziemlich schnell ins Chaos führt und man etwas koordi-
nierter vorgehen sollte. Diese Softwarekrise fand ihre endgültige Lösung in
dem dreizeiligen Programm meiner Studentin, denn wenn man das gewünsch-
te Ergebnis gleich selbst eingibt, dann kann das Programm selbst nichts
mehr falsch machen... Aber wieder im Ernst: indem sie die Möglichkeiten
des Computers gleichzeitig unterschätzte und überschätzte, hatte sie einen
doppelten Anfängerfehler gemacht, den ich Ihnen gerne ersparen möchte. Die
Überschätzung lag darin, dass ihr nicht klar war, wie genau man dem Rech-
ner mitteilen muss, was er im Einzelnen zu tun hat - wenn Sie dem Computer
kein genaues Rechenverfahren angeben, wird er entweder gar nichts oder nur
Unsinn ausrechnen. Und weil die geplagte Studentin anscheinend ihrem ei-
genen Optimismus dann doch nicht mehr so traute, ging sie gleich zu einer
folgenschweren Unterschätzung über: Man kann ja über Computer denken,
2 1 Grundlagen

was man will, aber wenn man ihnen die Ergebnisse selbst eingeben müsste,
könnte man sich auch den Strom sparen, mit dem man sie betreibt. Übrigens
hatte sie dann auch noch vergessen, die Ergebnisse am Bildschirm auszugeben,
aber ich will hier nicht kleinlich werden.
So etwas findet man häufiger, vom Studenten des ersten Semesters bis
hin zum Chefprogrammierer einer großen Firma. Der Grund für solche Fehl-
einschätzungen liegt wohl oft darin, dass die Leute nichts über das Innenleben
des Computers wissen und keine Vorstellung davon haben, wie dieses seltsa-
me Ding funktioniert. Damit Ihnen nicht das Gleiche passiert, will ich in die-
sem Kapitel über genau solche Fragen reden. Zuerst sollten wir uns darüber
verständigen, was man eigentlich mit Computern anstellt und wie man da-
von ausgehend den Begriff Informatik verstehen kann. Dann werde ich Ihnen
ein wenig darüber erzählen, wie sich die Datenverarbeitung im Allgemeinen
und die Rechner im Besonderen im Lauf der Zeit entwickelt haben. Daraus
werden sich dann schon erste Folgerungen darüber ergeben, wie ein Rechner
üblicherweise aufgebaut ist. Was nun im Inneren des Rechners passiert, wie
er wirklich rechnet und mit seinen Daten umgeht, das werde ich Ihnen nach
ein paar Bemerkungen zum Aufbau eines Rechners erzählen. Diese Informa-
tionen über die interne Wirkungsweise finden dann ihre Anwendung in der
Konstruktion eines so genannten Volladdierers, der Ihnen zeigen wird, welche
Schaltungen man vornehmen muss, um Additionen durchführen zu können.
Genug der Vorrede, fangen wir an.

1.1 Einführung
Welche Unterschiede zwischen von Menschen durchgeführter und maschinell
erledigter Datenverarbeitung bestehen, macht man sich am besten an einem
Beispiel klar.

1.1.1 Das EVA-Prinzip

Vermutlich hat jeder von Ihnen schon einmal irgendwo gegen Bezahlung ge-
arbeitet und Sie wissen, dass Ihr Lohn nicht einfach so bar auf die Hand
ausgezahlt wird, obwohl das im Hinblick auf die Steuern sicher gewisse Vor-
teile hätte. Natürlich müssen Ihre Gehaltszahlungen über die Lohnbuchhal-
tung erledigt werden, und die hat in jedem Unternehmen mehr oder weniger
die gleichen Abläufe. Gehen wir einmal davon aus, dass Ihr Arbeitgeber eher
altmodisch orientiert ist und dieses Teufelszeug namens Computer nicht in
seiner Firma duldet. Trotzdem wird man die Daten Ihrer Arbeitszeiten ir-
gendwie erfassen müssen; das kann zum Beispiel geschehen, indem man die
Stempelkarten der abgelaufenen Woche durcharbeitet, auf denen Sie und Ihre
Kollegen die jeweiligen täglichen Zeiten angegeben haben. Falls es keine Zeit-
erfassung dieser Art gibt, wird irgendjemand die Anwesenheitslisten durch-
gehen müssen, um Ihre An- und Abwesenheitsdaten festzustellen, denn Sie
1.1 Einführung 3

könnten ja während der Arbeitszeit auch im Freibad gewesen sein. Damit aus
diesen Daten am Ende klingende Münze erzeugt werden kann, wird dann der
zuständige Sachbearbeiter die Anzahl Ihrer Anwesenheitsstunden in seinen
Tischrechner eingeben. All diese Aktivitäten sortiere ich unter dem Ober-
begriff Eingabe ein: die vorhandenen Daten werden gesammelt und in den
Tischrechner eingegeben, gemacht wird damit erst mal gar nichts.
Vom puren Eingeben bekommen Sie aber noch keinen Lohn und das Fi-
nanzamt keine Steuern. Deshalb werden jetzt die eingegebenen Daten von
Hand verarbeitet, da die Unternehmensleitung immer noch keinen Computer
anschaffen wollte. Das muss man können, nicht jeder kennt sich in den Re-
geln und Vorschriften der Lohnbuchhaltung aus, die unter Umständen auch
noch durch spezielle Firmenregelungen ergänzt werden und natürlich auch
zu den Vorstellungen des Finanzamtes passen müssen. Der Sachbearbeiter,
vermutlich ein gelernter Lohnbuchhalter, berechnet also den Bruttolohn aus
den eingegebenen Arbeitsstunden, dem Stundenlohn und eventuellen Über-
stundenzuschlägen und geht dann zur Nettoberechnung über, um die Abzüge
auszurechnen. Davon gibt es, wie Sie wahrscheinlich schon schmerzlich er-
fahren haben, eine ganze Menge, und zwar in Form von Steuern und Sozi-
alversicherungsbeiträgen. Vielleicht haben Sie auch noch vermögenswirksame
Leistungen oder gar eine Lohnpfändung am Hals, und auch diese sonstigen
Abzüge wird der Lohnbuchhalter ausrechnen, vermutlich mit seinem Tisch-
rechner. Der klägliche Rest, der nach dem Abzug der Abzüge vom Bruttolohn
übrigbleibt, ist dann Ihr Nettolohn, der hoffentlich noch ausreicht, um die
Fahrtkosten zur Arbeit zu decken.
Das alles kann der Lohnbuchhalter aber nicht aus der hohlen Hand erledi-
gen. Er braucht dazu Informationen, die er in irgendwelchen Handbüchern
oder ähnlichen Unterlagen abgelegt hat, wie beispielsweise die Lohnsteu-
ersätze, die Sozialversicherungssätze oder auch Daten aus der Personalkar-
tei über Ihren Familienstand und die Anzahl Ihrer Kinder. Die Aktivitäten
unseres Buchhalters bezeichnet man als Verarbeitung.
Sobald nun alles verarbeitet ist, muss man mit den ermittelten Ergeb-
nisse auch noch etwas anfangen, sonst könnte man sich die ganze Rechnerei
sparen. Damit Sie Ihr Geld bekommen, muss eine Kassenanweisung geschrie-
ben werden, für Ihre Lohnmitteilung muss ein entsprechender Beleg an den
Postausgang gehen, auch das Finanzamt wird etwas über den Vorgang wissen
wollen und die Sozialversicherungskassen sicher auch. Mit anderen Worten:
die Ergebnisse der Verarbeitung werden auf verschiedenen Medien ausgege-
ben, und deshalb nennt man diesen Schritt des Prozesses auch die Ausgabe.
Und somit habe ich auch schon ein kleines Schema erstellt, mit dem ich die
manuelle Datenverarbeitung beschreiben kann, nämlich Eingabe → Verarbei-
tung → Ausgabe.
Die Situation verädert sich, wenn der Chef sich endlich einen Ruck gibt
und den einen oder anderen Computer einschließlich der nötigen Programme
anschafft - vornehm ausgedrückt spricht man allerdings nicht von Program-
men, sondern von Software. Wie sieht nun die Lohnbuchhaltung aus? Die An-
4 1 Grundlagen

wesenheitsdaten der Mitarbeiter werden jetzt vermutlich maschinell erfasst,


zum Beispiel von Eingabestationen am Eingangstor zum Firmengelände, und
automatisch an einen zentralen Computer weitergegeben. Das kann vollau-
tomatisch geschehen, sofern die Eingabestationen mit dem Zentralrechner ir-
gendwie verbunden sind; in diesem Fall spricht man von Vernetzung. Es kann
aber auch noch menschliche Hilfe gebraucht werden, indem ein Mitarbeiter die
Daten von der Eingabestation holt und dann zu Fuß zum Computer trägt - das
hängt von der eingesetzten Technik ab. In jedem Fall ist der erste Schritt der
Lohnbuchhaltung die Eingabe der nötigen Daten. Sind die Daten erst einmal
im Computer angekommen, muss er mit ihnen arbeiten, also die nötigen Ver-
arbeitungsschritte vornehmen. Die Regeln und Vorschriften, nach denen der
Lohnbuchhalter in der guten alten Zeit gearbeitet hat, stecken jetzt in den
Anweisungen des verwendeten Programms, in der Software, und dieses Pro-
gramm arbeitet mit zwei verschiedenen Datenarten: erstens mit den Daten,
die aus der Zeiterfassung in den Rechner übertragen wurden, und zweitens mit
den zusätzlichen Daten, die auch der Lohnbuchhalter gebraucht hat, nämlich
mit den Steuersätzen und Ähnlichem mehr. Da ein Computer schwerlich in
den Steuertabellen nachsehen kann, sind diese Daten auf der Festplatte des
Rechners abgespeichert, sodass er jederzeit auf sie zugreifen kann. Während
der eigentlichen Berechnung, in der wieder Bruttolohn, Abzüge und Netto-
lohn ausgerechnet werden, kommen sowohl Ihre Anwesenheitsdaten als auch
die für Ihren Fall nötigen auf der Festplatte vorrätigen Daten in den aktuel-
len Arbeitsspeicher des Rechners, damit er, wenn er nun schon mal Rechner
heißt, auch alles Gewünschte ausrechnen kann. Die Rolle des Tischrechners,
den noch der Lohnbuchhalter verwendet hatte, spielt dabei das Rechenwerk
des Computers.
Sie werden zugeben, dass die Aktivitäten des letzten Absatzes sicher die
Bezeichnung Verarbeitung verdienen. Der nächste Schritt ist wenig überra-
schend. Die berechneten Ergebnisse müssen wieder den verschiedenen betei-
ligten Stellen zur Verfügung gestellt werden, indem man Ausdrucke macht,
Bildschirmausgaben ansieht oder über eine Netzwerkverbindung direkt die
Ergebnisse an einen anderen Rechner weitergibt. Also ergibt sich als letzter
Schritt der computergestützten Datenverarbeitung wieder die Ausgabe.
Sehen Sie den prinzipiellen Unterschied zwischen der manuellen und der
maschinellen Datenverarbeitung, wie ich sie hier beschrieben habe? Nein?
Kein Wunder, es ist ja auch keiner da. Beide funktionieren nach dem gleichen
Prinzip: Zuerst werden Daten eingegeben, dann werden sie nach bestimmten
Regeln und Methoden verarbeitet, wobei unter Umständen noch weitere Da-
ten herangezogen werden, und schließlich werden die Ergebnisse ausgegeben.
Das ist ein so grundlegendes Prinzip der gesamten Datenverarbeitung, dass es
einen eigenen Namen bekommen hat. Man nennt es das EVA-Prinzip und fasst
damit die Anfangsbuchstaben der drei Schlüsselwörter Eingabe, Verarbeitung
und Ausgabe in einer eingängigen Abkürzung zusammen. Da aber in der In-
formatik alles möglichst auf Englisch formuliert sein muss, gibt es auch eine
englische Variante des EVA-Prinzips. Sie müssen nur bedenken, dass Eingabe
1.1 Einführung 5

Input, Verarbeitung Process und Ausgabe Output heißt, und schon haben Sie
alles zusammen, um auch die Abkürzung IPO-Prinzip zu verstehen.

EVA-Prinzip
Die Datenverarbeitung verläuft in aller Regel nach dem EVA-Prinzip, das
heißt nach dem Prinzip Eingabe → Verarbeitung → Ausgabe, oder in eng-
lischer Sprache Input → Process → Output, weshalb man auch vom IPO-
Prinzip spricht.

Kann es Ihnen also egal sein, ob Sie Ihre Datenverarbeitung maschinell


oder manuell erledigen lassen, wenn doch das Prinzip immer das gleiche ist?
Sicher nicht, denn auch bei gleichen Prinzipien gibt es doch in der Ausführung
recht deutliche Unterschiede.

1.1.2 Unterschiede zwischen maschineller und manueller


Datenverarbeitung

Was jedem vielleicht zuerst einfällt, ist die unterschiedliche Geschwindig-


keit. Das merken Sie schon, wenn Sie eine einfache Multiplikation schrift-
lich ausführen oder mit Hilfe eines Taschenrechners und dabei die Zeit mes-
sen. Noch viel deutlicher wird das natürlich, wenn es um die Verarbeitung
großer Datenmengen geht, also zum Beispiel um die Lohnbuchhaltung in ei-
nem großen Unternehmen. Die Routinearbeit des Datensuchens und Berech-
nens der Ergebnisse lässt sich mit einem Rechner sehr schnell durchführen,
von Hand wäre es fast eine Lebensaufgabe. Aber Vorsicht: der Geschwindig-
keitsvorteil bezieht sich vor allem auf Routineaufgaben, sobald es um sehr
spezielle Aufgaben geht, die ein gewisses Maß an Kreativität erfordern, kann
es ein Mensch immer noch oft mit einem Computer aufnehmen.
Etwas anders sieht es aus bei der Zuverlässigkeit. Man muss leider zuge-
ben, daß Computer in aller Regel deutlich zuverlässiger sind als Menschen,
auch wenn sie gelegentlich abstürzen - aber das soll auch schon bei Menschen
vorgekommen sein, vor allem am Wochenende. Sie können den Computer die
immer gleichen Aufgaben so oft durchführen lassen wie Sie wollen, er wird
keine Ermüdungserscheinungen zeigen, keine Flüchtigkeitsfehler machen oder
Sie durch bewusste Schlamperei ärgern; er macht einfach immer das, was Sie
ihm gesagt haben. Genau darin liegt allerdings auch ein Problem: wenn Sie
der Maschine die falschen Regeln mitgeteilt, sie also falsch programmiert ha-
ben, dann wird sie natürlich auch treu und zuverlässig die falschen Ergebnisse
liefern. Zwar immer die gleichen, aber eben falsche. Man kann dafür aber
nicht den Computer verantwortlich machen, der hat wie üblich nur getan,
was Sie ihm gesagt haben. Die Verantwortung liegt hier eindeutig bei dem
Programmierer oder dem Anwender, der ein falsches Programm geschrieben
oder falsche Eingabedaten verwendet hat. Aber damit so etwas nicht passiert,
haben wir ja Sie und dieses Buch, aus dem Sie lernen sollen, wie man richtige
Programme schreibt.
6 1 Grundlagen

Sie dürfen nicht vergessen, dass es beim Computereinsatz auch um wirt-


schaftliche Fragen geht, und deshalb besteht ein wesentlicher Vorteil der ma-
schinellen Datenverarbeitung in den geringeren Kosten, die sie verursachen.
Die einmalige Anschaffung von Rechnern, verbunden mit der nötigen Softwa-
re, verursacht natürlich zunächst einmal Kosten, aber wenn sie erst mal da
sind, dann sparen sie auch einiges ein. Die Maschine ist auf mittlere Sicht
nun mal billiger als der menschliche Lohnbuchhalter, denn sie bezieht kein
Gehalt und keine Lohnnebenkosten, ganz zu schweigen davon, dass man für
sie auch keine Kantine braucht. Das ist einerseits ein Vorteil, weil das Unter-
nehmen Geld spart, andererseits aber auch ein Nachteil, weil auf diese Weise
Arbeitsplätze verloren gehen - nicht immer kann man alles eindeutig bewerten.

Vorteile der maschinellen Datenverarbeitung


Wesentliche Vorteile der maschinellen Datenverarbeitung im Vergleich zur ma-
nuellen Datenverarbeitung sind
• höhere Geschwindigkeit
• höhere Zuverlässigkeit
• geringere Kosten

Sie sehen also, es ist sinnvoll, bei Aufgaben der Datenverarbeitung auf
einen Computer zurückzugreifen, und daher sollten wir einen Blick auf die
Aufgabenfelder werfen, für die so ein Rechner eingesetzt werden kann.

1.1.3 Aufgabenbereiche des Computers

Wenn man ihn schon als einen Rechner bezeichnet, dann sollte zu seinen
grundlegenden Aufgaben sicher das Rechnen gehören, genauer gesagt die
Ausführung von Berechnungen. Das können Berechnungen verschiedenster
Art sein, beispielsweise die Berechnung des Nettolohns aus dem Bruttolohn
und den Randbedingungen, über die ich vorhin gesprochen habe. Solche Be-
rechnungen könnte auch jeder Lohnbuchhalter durchführen; man braucht hier
den Computer nicht deshalb, weil die Berechnungen so kompliziert sind, dass
das kein Mensch mehr hinbekommt, sondern weil zu oft die gleichen recht
einfachen Dinge getan werden müssen. Anders sieht das aus bei bestimm-
ten technischen oder physikalischen Problemen, bei denen keine Massendaten
verarbeitet werden, sondern ausgesprochen komplizierte Rechenverfahren ab-
gearbeitet werden müssen, für die ein noch so begabter Mensch mehr Zeit
bräuchte als ein Leben hergibt. Diese Verfahren lassen sich in einem Pro-
gramm beschreiben, und der Computer kann mit etwas Glück auf Grund
seiner hohen Geschwindigkeit die nötigen Berechnungen vornehmen.
Rechnen ist nicht alles im Leben. Denken Sie wieder einmal an die maschi-
nell durchgeführte Lohnbuchhaltung, die am Ende, nachdem alles gerechnet
ist, irgendwelche Ergebnisse liefert. Nun kann es ja sein, dass die Steuer-
prüfung Ihrer Firma auf den Zahn fühlen und Ihre Abrechnungen überprüfen
1.1 Einführung 7

will, und zu diesem Zweck müssen die Ergebnisse gespeichert sein. Das kann
man auf dem Papier machen, aber das kostet eine Unmenge Platz, weshalb
man die Speicherung großer Datenmengen oft und gerne maschinell vornimmt,
also mit Computerunterstützung. Auch hier gibt es verschiedene Möglichkei-
ten. Es kann sich um langfristig anzulegende Daten handeln, die wird man
auf einer bestimmten Festplatte ablegen, die Platte in irgendeinen Schrank
stellen und hoffen, sie notfalls wieder zu finden. Dazu gehören zum Beispiel
Reproduktionen von alten Manuskripten oder Bildern, die man der Nachwelt
erhalten will. Wenn Sie aber von Frankfurt nach Teneriffa fliegen wollen und
eine Flugbuchung vorgenommen haben, dann können Sie mit einem gewissen
Recht erwarten, dass die Angestellten beim Einchecken nicht erst die richtige
Festplatte suchen, solche Daten müssen natürlich sofort verfügbar sein. Man
verwendet also Computer auch, um Daten abzuspeichern, und zwar entweder
langfristig oder direkt verfügbar.
Vielleicht haben Sie schon darauf gewartet, dass ich endlich über das In-
ternet rede. Erwarten Sie nicht zu viel, das Internet wird hier ziemlich selten
vorkommen, aber wenn es um die Grundaufgaben eines Computers geht, muss
ich es schon mal erwähnen. Es gibt ja nicht nur einen Computer auf der Welt,
und wenn man die Leistungen und Informationen dieser Computer mitein-
ander verbinden will, dann spricht man von Computernetzwerken. Über das
Internet hat man heute einen Riesenverbund von Rechnern zur Verfügung, auf
die eine Riesenmenge von Benutzern zugreifen kann, und das regelt sich nicht
von alleine. Um diese Kommunikation zwischen den Computern zu steuern,
brauchen Sie selbstverständlich wieder Computer, deren Aufgabe es ist, die
Übermittlung der gewünschten Informationen von Ort zu Ort zu steuern.
Und damit habe ich schon das nächste Schlüsselwort gefunden: Steue-
rung und, was fast immer dazugehört, Kontrolle. Nicht nur das Internet und
die allgemeine Datenkommunikation müssen gesteuert werden, meine Wasch-
maschine und mein Auto auch. Auch das übernehmen oft genug Computer,
ob Navigationssysteme oder Waschmaschinensteuerung, ob Autopiloten im
Flugzeug oder Steuerungsgeräte für Produktionsroboter, sie alle haben die
Aufgabe, Geräte zu steuern und zu kontrollieren.
Jetzt sind wir so weit, eine Definition des Begriffs Computer“ geben zu

können, den ich schon so oft benutzt habe. Mein Lexikon gibt beispielswei-
se die eher einfache Definition, ein Computer sei eine elektronische Daten-
verarbeitungsanlage, die heute in Wissenschaft, Wirtschaft, Verwaltung und
Technik eine entscheidende Rolle spiele. Das ist fein beobachtet, aber doch ein
wenig schwammig. Auch den Computer einfach nur als Rechner zu bezeich-
nen und damit auszusagen, dass man nur mit ihm rechnet, ist zu dünn. Ich
verwende deshalb die eben ermittelten Grundfunktionen und definiere einen
Computer durch die Aufgaben, für die er da ist.

Computer
Ein Computer ist ein Gerät, das Daten speichert und verarbeitet, das auf
8 1 Grundlagen

dieser Basis Berechnungen durchführt, andere Geräte steuern kann und das
mit anderen Geräten und mit Menschen in Verbindung treten kann.

Es wird nichts darüber gesagt, dass es sich unbedingt um ein elektroni-


sches Gerät handeln muss. Theoretisch kann man sich einen Computer ohne
elektrischen Strom auf der Basis fließenden Wassers vorstellen, wenn ich auch
zugeben muss, dass man von hydraulischen Computern selten etwas hört.
Wenn ich also von einem Computer rede, dann meine ich natürlich in aller
Regel ein elektronisches Gerät, das der obigen Definition genügt.

1.1.4 Computertypen

Computer gibt es natürlich in verschiedenen Ausführungen. Sie haben ver-


mutlich einen PC zu Hause stehen, auf dem Sie Ihre Computerspiele laufen
lassen und mit dem Sie hin und wieder arbeiten. Aber PCs sind nicht die
einzigen Arten von Rechnern, sie stehen eigentlich recht weit unten in der
Hierarchie, zumindest wenn man der Größe nach geht. Zuerst müsste man
da wohl den Mainframe-Computer nennen. Dabei handelt es sich um einen
Großrechner, der eher als Hintergrundrechner arbeitet, auf den Sie also als
Benutzer keinen direkten Zugriff haben, sondern beispielsweise mit Hilfe eines
Rechnernetzes über andere Computer zugreifen können. Dass eine so große
Maschine für nur einen Benutzer ziemlich sinnlos wäre, dürfte klar sein, und
daher ist eine wesentliche Eigenschaft eines Mainframe-Computers auch der
sogenannte Mehrbenutzerbetrieb: mehrere Benutzer arbeiten gleichzeitig auf
demselben Rechner, haben aber subjektiv das Gefühl, er wäre nur für sie da.
So etwas ist gerade bei vielen Benutzern nur bei sehr hoher Rechenleistung
und bei großer Speicherkapazität zu schaffen, und genau dadurch zeichnen
sich Mainframe-Computer auch aus. Wie Sie sich leicht vorstellen können,
kann man sie auf Grund ihres hohen Leistungsvermögens ausgezeichnet für
die Verarbeitung riesiger Datenmengen einsetzen und für ausgesprochen kom-
plexe Berechnungen einsetzen, bei denen ein handelsüblicher PC sich leise
weinend verabschieden würde.
Ob man die Supercomputer noch zu den besonders guten Mainframes rech-
net oder ob sie schon eine höhere Klasse darstellen, sieht jeder ein wenig
anders. Uns kann das auch ziemlich egal sein, wichtig ist nur, dass sie die
Leistungsfähigkeit der gewöhnlichen Mainframes deutlich übersteigen, indem
sie so genannte Mehrprozessorsysteme einsetzen - kein schönes Wort, aber
gar nicht so schlimm. Der Prozessor ist der Teil des Computers, der die ei-
gentlichen Verarbeitungen, die Berechnungen durchführt, und normalerweise
hat eben ein Computer auch nur einen Prozessor. Wenn man es nun so ein-
richtet, dass er gleich mehrere Prozessoren hat, dann spricht man eben von
einem Mehrprozessorsystem. Mit mehreren Prozessoren kann man auch meh-
rere Verarbeitungen gleichzeitig laufen lassen, genauso wie man auf einem
Herd mit vier Herdplatten auch gleichzeitig vier Töpfe zum Kochen bringen
kann. Man spricht daher auch von parallelen Systemen, was nur bedeutet,
1.1 Einführung 9

dass mehrere Dinge parallel, also gleichzeitig geschehen können. Allerdings


erfordert eine Verarbeitung dieser Art Programmiertechniken, die weit über
das hinausgehen, was Sie hier lernen werden.
Was man früher mittlere Datentechnik nannte, bezeichnet man heute als
Midrange-Systeme, aber beides ist ein wenig aus der Mode gekommen. Es
handelt sich dabei um Computer, die sicher keinen Mainframe-Rechner dar-
stellen, aber doch immerhin die gesamte Datenverarbeitung eines mittleren
Unternehmens erledigen und etliche im ganzen Unternehmen verteilte Ar-
beitsplätze mit Rechenleistung versorgen können. Man könnte also sagen, ein
Midrange-System ist der kleinere Bruder eines Mainframe-Computers. Heute
neigt man eher dazu, anstelle eines solchen Systems die nötige Zahl von PCs
anzuschaffen und in einem Rechnernetzwerk zusammenzuschalten, wobei man
an Stelle der PCs oft auch die leistungsfähigeren Workstations einsetzt, auf
die ich gleich zu sprechen komme.
Denn eine Workstation ist schon etwas mehr als ein simpler PC und ist
auch entsprechend teurer. Sie ist nichts anderes als ein Computer, der eine
hohe Rechenleistung an einem Arbeitsplatz konzentriert, deswegen heißt sie
ja auch Workstation. Sie kann eine große Menge von Daten bewältigen und
ist meistens auch mit Graphikmöglichkeiten versehen, die das Anzeigen bun-
ter Bilder, das Sie von Ihrem PC kennen, doch deutlich übersteigen. So etwas
braucht man dann auch nicht am häuslichen Schreibtisch, sondern am Arbeits-
platz in einem Forschungs- und Entwicklungslabor oder wenn es darum geht,
Graphiken in hoher Qualität herzustellen. Übrigens sind sie im Gegensatz zum
vertrauten PC in der Lage, den oben beschriebenen Mehrbenutzerbetrieb zu
unterstützen.
Damit haben wir das Ende der Hierarchie erreicht, und das stellt der gute
alte Personal Computer dar, der nicht so heißt, weil er ein Computer fürs
Personal ist, sondern weil es sich um einen persönlichen Computer handelt.
Er versammelt in sich die notwendige Rechenleistung, die ein Mitarbeiter an
seinem persönlichen Arbeitsplatz braucht, nicht mehr und nicht weniger. Sei-
ne interne Ausstattung, die Technik, die er in seinem Innenleben verwendet,
ist etwas schlichter als die einer Workstation, aber solange es nur um die
Belange eines Mitarbeiters an einem Arbeitsplatz geht, ist nichts gegen ihn
einzuwenden. Man sollte auch nicht verschweigen, dass die Grenzen zwischen
PCs und Workstations inzwischen einigermaßen fließend geworden sind; ein
PC mit umfangreicher Ausstattung und den nötigen Zugangsmöglichkeiten zu
den gewünschten Rechnernetzen kann durchaus auch das Niveau einer Work-
station erreichen.

Computertypen
Teilt man die Computer nach ihrer Leistungsfähigkeit ein, so erhält man die
folgende Hierarchie:
• Mainframes und Supercomputer
• Midrange-Systeme
10 1 Grundlagen

• Workstations
• Personal Computer

1.1.5 Informatik

Über Computer und ihre Einteilung habe ich jetzt erst mal genug geredet.
Sie sollten nicht aus dem Auge verlieren, dass diese Computer nur ein Mit-
tel zum Zweck sind, und der Zweck ist der im EVA-Prinzip beschriebene:
die Eingabe, Verarbeitung und Ausgabe von Daten. Da das Ganze mit Hilfe
des elektonischen Gerätes Computer stattfinden soll, spricht man auch, wie
Sie natürlich wissen, gerne von Elektronischer Datenverarbeitung, abgekürzt
EDV. Und was ist jetzt Informatik? Auch da gehen die Definitionen etwas
auseinander, zumal in der englischen Sprache das Wort Informatik gar nicht
so recht existiert, da sagt man Computer Science“, also die Wissenschaft

vom Computer. So falsch ist das auch gar nicht, denn schließlich gäbe es ohne
Computer so etwas wie Informatik überhaupt nicht, und daher muss die In-
formatik wohl ziemlich viel mit der Elektronischen Datenverarbeitung zu tun
haben. Ich halte mich deshalb an die folgende Definition.

Informatik
Informatik ist die Wissenschaft, die sich mit den theoretischen Grundlagen,
den Mitteln und Methoden sowie mit der Anwendung der Elektronischen Da-
tenverarbeitung beschäftigt, das heißt mit der Informationsverarbeitung unter
Einsatz von Computern.

Genau das werden wir hier machen, und wir haben ja auch schon in diesem
Abschnitt damit angefangen: wir gehen der Frage nach, wie man mit Com-
putern Datenverarbeitung betreibt. Was man unter einem Computer versteht
und welche Computertypen man heute unterscheidet, haben wir hier schon
geklärt. Im nächsten Abschnitt werde ich Ihnen berichten, wie es zu der Ent-
wicklung gekommen ist, die zu dem Computer von heute geführt hat.

Übungen

1.1. Analysieren Sie im Hinblick auf das EVA-Prinzip die Tätigkeiten, die ein
Mensch bzw. ein Computer durchführen muss, um zwei Gleichungen mit zwei
Unbekannten zu lösen, also zum Beispiel

ax + b = e
cx − dy = f

mit den Unbekannten x und y. Geben Sie dabei genau an, welche Einga-
ben nötig sind, welche Verarbeitungsschritte durchgeführt werden und wie
die Ausgabe gestaltet werden sollte.
1.2 Wie alles begann 11

1.2. Analysieren Sie die möglichen Nachteile, die die maschinelle Datenverar-
beitung im Vergleich zur manuellen Datenverarbeitung hat.

1.3. Ein Handlungsreisender startet in Frankfurt eine Rundreise, die ihn durch
19 weitere Städte und zum Schluss wieder zurück nach Frankfurt führt. In
jeder Stadt will er genau einmal Station machen (außer natürlich in Frankfurt,
wo er sowohl startet als auch ankommt).
(a) Zeigen Sie, daß es genau

19 · 18 · 17 · · · 3 · 2 · 1

verschiedene Reiserouten gibt. Man nennt diese Zahl 19! = Fakultät von
19.
(b) Der Reisende hat einen Computer zur Verfügung, der pro Sekunde 100
Millionen verschiedene Routen durchchecken kann. Wie lange braucht er,
um alle möglichen Reiserouten durchzugehen und so die kürzeste Route
herauszufinden?

1.2 Wie alles begann


Es ist schwer zu sagen, wann die Menschen mit der Datenverarbeitung ange-
fangen haben, schon deshalb, weil der Begriff etwas vage ist. Sicher hat der
Hirte aus grauer Vorzeit, der für jedes seiner Tiere einen Stein beiseite ge-
legt hat, um auch ohne einen genauen Zahlenbegriff seine Herde abzählen zu
können, schon Daten verarbeitet, und das gar nicht mal schlecht, aber man
würde doch zögern, die Geschichte der Datenverarbeitung mit ihm beginnen
zu lassen. Sinnvoller ist es, nach den ersten Hilfsmitteln zu suchen, mit dem
der Prozess des Rechnens unterstützt wurde, und da wird man auch schnell
fündig. Die erste wichtige Erfindung in dieser Richtung war wohl das indisch-
arabische Zahlensystem - klingt recht exotisch, aber das sind einfach nur die
Zahlen, wie wir sie heute kennen, also das Dezimalsystem einschließlich der
Null. Man kannte sie in Indien seit etwa dem sechsten Jahrhundert, in Arabi-
en etwas später, und es waren die Araber, die diese praktischen Zahlen dann
im neunten Jahrhundert nach Europa einführten. Vermutlich haben Sie zu Ih-
rer Schulzeit auch das römische Zahlensystem lernen dürfen und können sich
noch an seine Umständlichkeiten erinnern, da war der Übergang zum Dezimal-
system sicher eine Wohltat für alle Beteiligten. Trotzdem musste man noch
immer mit Stäbchen im Sand kratzen oder mit der Feder übers Pergament,
um Rechnungen durchzuführen, es fehlte an maschineller Unterstützung.

1.2.1 Der Abakus

Aber nicht lange. In den verschiedensten Weltgegenden, vom alten Rom bis
hin zum fernen China wurde ein Rechengerät entwickelt, das in Ostasien auch
12 1 Grundlagen

Abb. 1.1. Einfacher Abakus

heute noch zum gängigen Instrumentarium eines Kaufmanns gehört: der Aba-
kus.
Man vermutet, dass er ursprünglich in Babylonien entstand, von dort aus
seinen Siegeszug über die halbe Welt antrat und etwa seit dem sechsten Jahr-
hundert in verschiedenen Ausprägungen den Leuten beim Rechnen half. Die
Grundidee ist einfach genug. Man spannte ein paar Schnüre in einen recht-
eckigen Kasten und zog eine bestimmte Anzahl Perlen auf diese Schnüre. Im
einfachsten Modell waren das neun Perlen pro Schnur. Hatte man also bei-
spielsweise die Konstellation wie in Abbildung 1.1, dann war damit die Zahl
7254 dargestellt, denn die Schnüre, von rechts nach links betrachtet, entspra-
chen einfach den Einer-, Hunderter-, Tausenderstellen usw., wie wir sie heute
noch benutzen.
Etwas raffinierter war dann das Modell aus Abbildung 1.2, heute würde
man es vielleicht Turbo-Abakus oder gar Abakus Extended Version nennen. Es
realisierte eine Art Fünfersystem, indem die unteren Perlen auf einer Schnur
genauso zu verstehen waren wie in der einfachen Version, aber die obere Kugel
die Zahl Fünf symbolisierte. Man konnte also die Vier durch vier Kugeln in
der unteren Hälfte darstellen, die Sechs aber durch eine Kugel unten und eine
oben, wobei die Kugeln zählten, die bis zur Grenzlinie geschoben waren.
Mit so einem Abakus konnte man nicht nur Zahlen gefällig auf ein Brett
übertragen, sondern auch rechnen, sogar sehr schnell rechnen. Fahren Sie ein-
mal nach China und kaufen Sie in irgendeinem kleinen Laden ein; mit etwas
Glück können Sie dann sehen, dass ein geübter Abakusbenutzer beim Rechnen
eine Geschwindigkeit an den Tag legt, von der Sie als taschenrechnergeprägter
Europäer nur träumen können. Und das geht mit etwas Übung bei allen vier
Grundrechenarten.

1.2.2 Mechanische Rechenmaschinen

Schön und gut, aber noch immer nicht das Wahre. Das Ziel, die Rechen-
vorgänge ganz einer Maschine zu überlassen und sich als Benutzer nur noch
1.2 Wie alles begann 13

Abb. 1.2. Verbesserter Abakus

um die Eingabe kümmern zu müssen, war noch eine ganze Weile entfernt,
erst im siebzehnten Jahrhundert wurde die erste mechanische Rechenmaschi-
ne gebaut. Die Verhältnisse waren eher ungünstig für solche etwas esoteri-
schen Erfindungen, 1618 hatte der Dreißigjährige Krieg angefangen und drei-
ßig lange Jahre würde man in weiten Teilen Deutschlands, das in unzählige
Fürstentümer aufgeteilt war, genug mit dem eigenen Überleben beschäftigt
sein. Dennoch hat 1623 ein Tübinger Professor namens Wilhelm Schickard ei-
ne zahnradgetriebene Rechenmaschine für sechsstellige Additionen, Subtrak-
tionen, Multiplikationen und Divisionen konstruiert und gebaut. Für die da-
malige Zeit war das eine technische Meisterleistung, zumal Schickard auch
der automatische Zehnerübertrag gelang, der dafür sorgte, dass bei Additio-
nen wie etwa 9 + 7 auch wirklich eine weitere Stelle, die Zehnerstelle, ent-
steht und 16 heraus kommt. Er scheint überhaupt ein heller Kopf gewesen zu
sein, denn ursprünglich wurde er in Tübingen angestellt als Professor für He-
bräisch, Aramäisch und sonstige biblische Sprachen, übernahm aber nach dem
Tod des berühmten Kepler auch noch ohne Probleme die Fächer Astronomie,
Mathematik und Geodäsie. Seine Maschine hat den Krieg leider ebensowe-
nig überlebt wir ihr Erfinder, er starb 1635 mit 43 Jahren an der Pest; die
Rechenmaschine wurde im Lauf des Krieges zerstört.
Mehr Glück hatte da der französische Mathematiker und Philosoph Blai-
se Pascal. Frankreich war zwar - vor allem auf Betreiben des Ministers und
Kardinals Richelieu - ausgesprochen aktiv am dreißigjährigen Krieg beteiligt,
führte ihn aber mit Vorliebe durch Stellvertreter und auf deutschem Boden.
So konnte Pascal in aller Ruhe schon mit 19 Jahren 1643 eine mechanische Re-
chenmaschine entwickeln, die zu achtstelligen Additionen und Subtraktionen
in der Lage war und wie die Maschine von Schickard den automatischen Zeh-
nerübertrag beherrschte - nach dem gleichen Grundprinzip, das heute noch
in mechanischen Kilometerzählern verwendet wird. Er hatte die Maschine auf
Zahnradbasis übrigens nicht zum Spaß gebaut, sondern für seinen Vater, der
als Steuerbeamter für das Eintreiben der Steuern in einem Bezirk Frankreichs
zuständig und deshalb dankbar war für eine Vereinfachung der täglichen Re-
14 1 Grundlagen

chenplackerei - bedenken Sie, dass der König und sein Minister Geld brauch-
ten, sogar viel Geld, um den Krieg am Kochen zu halten. Aus diesem Grund
konnte Pascals Maschine auch mit dem nicht durchgängig dezimal gehaltenen
französischen Währungssystem umgehen, was spezielle Umsetzungen bei den
Zahnrädern benötigte. Später wurde Pascal ein europaweit bekannter Mathe-
matiker und Physiker, scheint dabei aber etwas unzufrieden gewesen zu sein,
denn in seinen späten Jahren wandte er sich vor allem der Religion zu und
befasste sich mit religionsphilosophischen Fragen.
Es wird niemanden wundern, dass sich auch der Universalgelehrte Gott-
fried Wilhelm Leibniz mit mechanischen Rechenmaschinen beschäftigte und
dann 1673 auch eine konstruierte, die für alle vier Grundrechenarten zu ge-
brauchen war. Er fand, dass die langweilige Routinearbeit des Rechnens von
Hand des menschlichen Geistes unwürdig war und deshalb einer Maschine
übertragen werden sollte. Allerdings musste er beim konkreten Zusammen-
bauen feststellen, dass die Verwendung des Dezimalsystems zu recht großen
mechanischen Problemen führte, und das veranlasste ihn, über einfachere Zah-
lensysteme nachzudenken. Das Ergebnis können Sie sich vielleicht schon vor-
stellen: Leibniz war der Erste, der die dualen oder auch binären Zahlen, also
das Zweiersystem entwickelte, ohne das heutige Rechner nicht mehr vorstell-
bar sind. Vielleicht wäre es auf eine Rechenmaschine mehr oder weniger nicht
angekommen, aber der Gedanke, dass eine Maschine einfacher zur Basis zwei
als zur Basis zehn rechnen kann, hatte sehr weitreichende Folgen, und er geht
auf Leibniz zurück.

Entwickler der frühen mechanischen Rechenmaschinen


Die ersten mechanischen Rechenmaschinen wurden entwickelt von:
• Wilhelm Schickard, 1623
• Blaise Pascal, 1643
• Gottfried Wilhelm Leibniz, 1673
Ihre Rechenmaschinen basierten auf dem Einsatz von Zahnrädern und
ermöglichten den automatischen Zehnerübertrag. Leibniz war außerdem der
Schöpfer des Zweiersystems.

1.2.3 Die Lochkartenmaschine von Hollerith

Was sie bisher gesehen haben, waren Rechenmaschinen, die dem Benutzer
zwar die Durchführung der Grundrechenarten abnehmen konnten, mehr aber
auch nicht. Ob man das schon zur Informatik zählen will, sei dahingestellt,
vielleicht hat es mehr mit dem Ingenieurwesen zu tun. Aber eine andere Ent-
wicklung ist wohl ziemlich sicher auch zur Informatik zu zählen. Wir machen
also einen kleinen Sprung über etwa 200 Jahre und sehen uns die Lochkarten-
maschinen von Herrmann Hollerith an. Eine Lochkarte ist für sich betrachtet
noch nichts Besonderes, nur eine kleine Pappkarte, in die Löcher eingestanzt
1.2 Wie alles begann 15

sind. Aber eben in diesen Löchern liegt die tiefere Bedeutung der Lochkar-
te, denn verschiedene Lochkombinationen beinhalten in verschlüsselter Form
Informationen, seien es nun Daten oder Verfahrensvorschriften zum Sortieren
oder zum gesteuerten Zusammenzählen von Daten.

Abb. 1.3. Lochkarte (Quelle: http://www.heimcomputer.de/kurios/lochkarte.html)

Ihre erste Anwendung fanden die Lochkarten schon 1728 bei der automati-
schen Steuerung von Webstühlen, damals noch als gelochte Holzplättchen und
nicht als Pappkarte. Im frühen neunzehnten Jahrhundert wurde diese Tech-
nik dann immer mehr verbessert, sodass mit Hilfe automatisch gesteuerter
Webstühle komplizierte Muster gewebt werden konnten, ohne dass man noch
gelernte Fachkräfte brauchte, sondern nur noch angelernte Hilfsarbeiter - eine
gewaltige Rationalisierung für die Textilindustrie, sicher, aber das war auf der
anderen Seite auch eine der Ursachen, die zur zunehmenden Verelendung der
Weber und damit zu den schlesischen Weberaufständen von 1844 führten.
Ein amerikanischer Ingenieur mit dem so gar nicht amerikanischen Namen
Herrmann Hollerith ließ sich von den automatisierten Webstühlen inspirieren
und wendete ihr Prinzip auf die Auswertung der Daten der elften amerikani-
schen Volkszählung 1890 an. Mit seiner Idee rannte er offene Türen ein, denn
die Auswertung der zehnten Volkszählung von 1880 hatte sieben Jahre lang
gedauert und 500 Leute beschäftigt, weil alle gesammelten Daten manuell
ausgewertet werden mussten. Da es sich dabei im Wesentlichen um Sortier-
und Additionsvorgänge an großen Datenmengen handelte, kam Hollerith auf
die Idee, zu diesem Zweck Lochkartenmaschinen einzusetzen. Die Daten der
Amerikaner wurden nun nicht mehr auf Zählblättchen vermerkt, die hinterher
die menschlichen Datenverarbeiter zu untersuchen hatten, sondern eben auf
Lochkarten, und zwar in Form von klar definierten Lochkombinationen, die
einer maschinellen Verarbeitung zugänglich waren. Während man anfangs 240
lochbare Quadrate auf einer Karte unterbrachte und damit 240 Fragen mit
Ja oder Nein beantworten konnte, ging man später zu Karten mit 960 mögli-
chen Löchern über, die auch die Speicherung von Zahlen oder Zeichenketten
zuließen. Das Verarbeitungsprinzip war gar nicht schwer, man musste nur die
16 1 Grundlagen

Löcher in elektromechanische Aktivitäten umsetzen: eine Lochkarte passierte


einen so genannten Kontaktapparat, wobei zu jeder abzulesenden Stelle ein
elektrischer Schaltkreis gehörte. Wies die Stelle ein Loch auf, so schloß sich
der Stromkreis und eine bestimmte Verarbeitung konnte angestoßen werden,
das heißt die passende Klappe eines Sortierapparates entsprechend geöffnet
werden; war kein Loch vorhanden, so floss natürlich kein Strom und die zu-
gehörige Verarbeitung unterblieb. Es handelte sich also um eine Zähl- und
Sortiermaschine, und genau diese Zwecke waren ja auch bei der Volkszählung
vordringlich. Der Erfolg war überwältigend, die Zahl der Mitarbeiter konn-
te von 500 auf 43 gesenkt werden, die Verarbeitungsdauer betrug nur noch
vier Wochen anstatt sieben Jahre. Ganz getraut hat man der Sache bei den
Behörden aber nicht, weil das Ergebnis den Verantwortlichen nicht passte:
man hatte mit deutlich mehr Menschen im Lande gerechnet, und so wur-
den die Resultate zurückgehalten, die gesamte Zählarbeit noch einmal durch-
geführt, und erst drei Monate später, als sich alles bestätigt hatte, wurden die
Ergebnisse veröffentlicht. Die Statistik passt eben nicht immer zur Politik.
Offenbar waren Lochkartenmaschinen sehr sinnvoll und effektiv einsetzbar
für die routinemäßige Verarbeitung großer Datenmengen. Sie wurden zuerst
von Holleriths eigener Firma hergestellt, dann von der IBM, in der Holle-
riths Firma aufgegangen war, und wurden bis weit in die sechziger Jahre des
zwanzigsten Jahrhunderts benutzt. Die Lochkarten selbst waren aber noch ein
ganzes Stück länger in Gebrauch, was auch ich bezeugen kann. Noch zu Beginn
der achtziger Jahre, als ich anfing zu studieren, wurden ganze Programme mit
Begeisterung in Lochkarten gestanzt und dann einem Lochkartenleser anver-
traut in der Hoffnung, dass das Programm irgendwann im Laufe des Tages zur
Ausführung kommen würde, mehr oder weniger nach Laune des zuständigen
Operateurs. Und auch die Studentendaten hat man auf Lochkarten erfasst
und dann per Lochkartenleser eingelesen, weshalb es vor zwanzig Jahren auch
noch den ehrenwerten Beruf des Lochkartenstanzers gab. Die Hollerithmaschi-
nen waren allerdings keine wirklich programmierbaren Computer, sie waren
ausgezeichnet geeignet zum Zählen und Sortieren, und weiter waren sie gar
nichts.
Hollerith selbst lebt allerdings noch heute in den Programmiersprachen
FORTRAN und PL1 weiter, in denen sich das so genannte H-Format“ vom

Anfangsbuchstaben des Namens Hollerith ableitet.

Lochkartenmaschinen
Die Lochkartenmaschinen von Herrmann Hollerith dienten vor allem dem
schnellen Zählen und Sortieren großer Datenmengen und wurden zum ersten
Mal sehr erfolgreich bei der amerikanischen Volkszählung von 1890 eingesetzt.
Maschinen dieses Typs waren bis in die sechziger Jahre des zwanzigsten Jahr-
hunderts aktiv.
1.2 Wie alles begann 17

1.2.4 Die Analytische Maschine von Babbage

Bei der Lochkartenmaschine handelte es sich offenbar um einen Vorläufer der


elektronischen Datenverarbeitung, genau genommen um elektromechanische
Datenverarbeitung, über die ich gleich noch etwas mehr erzählen werde, und
daher befinden wir uns jetzt schon im Bereich der Informatik. Noch wichti-
ger war aber vielleicht ein anderer Vorläufer, auch wenn seine Maschine nie
gebaut wurde, und das war der englische Mathematiker Charles Babbage. Ei-
nerseits ein sehr erfolgreicher Mann, er wurde 1828 Professor in Cambridge
und brachte es fertig, elf Jahre lang nicht eine einzige Vorlesung zu halten
und auch seinen Wohnsitz nicht in Cambridge zu nehmen - Verhältnisse, die
einen deutschen Professor von heute mit leisem Neid erfüllen. Andererseits
recht erfolglos, weil er das große Projekt seines Lebens, die Konstruktion ei-
ner programmgesteuerten Rechenmaschine, also eines Universalrechners, nie
verwirklichen konnte, obwohl er dreißig Jahre daran gearbeitet hat. Aber ob
die Maschine physisch konstruiert wurde oder nicht, darauf kommt es gar
nicht so an, entscheidend ist, dass Babbage als erster Prinzipien entwickelt
hat, die man heute noch in der Computerarchitektur findet. Die Maschinen,
über die ich bisher berichtet habe, waren entweder reine Rechenmaschinen
oder eben Lochkartenmaschinen, die zu einem bestimmten Zweck konstruiert
waren. Babbage wollte mehr. Er wollte eine Analytische Maschine bauen, die
von Fall zu Fall programmiert werden konnte, um an flexiblen Eingabeda-
ten ebenso flexible Berechnungen vornehmen zu können. Falls Sie das an die
Funktion eines modernen Computers erinnert, haben Sie völlig recht, Babbage
hatte nichts anderes vor als einen richtigen Computer auf rein mechanischer
Basis zu konstruieren.
Erstaunlicherweise entsprach nicht nur sein Ziel einem modernen Compu-
terbegriff, sondern er dachte sich auch ein Konstruktionsprinzip aus, das stark
heutigen Konstruktionen ähnelt. Er hatte ein vollautomatisches Rechenwerk
für die vier Grundrechenarten vorgesehen, das er mit Hilfe von Zahnrädern
realisieren wollte, allerdings auf dezimaler Basis und nicht zur Basis zwei.
Die zu verarbeitenden Zahlen, die auszugebenden Zahlen und die Zwischen-
ergebnisse sollten in einem separaten Zahlenspeicher gehalten werden, in den
immerhin 1000 Zahlen zu je 50 Stellen passten. Es gab ein auf Lochkarten
basierendes Eingabegerät und ein Ausgabegerät, letzteres sogar verbunden
mit einem Druckwerk, aber vor allem hatte sich Babbage das Prinzip der
Steuereinheit ausgedacht, mit der so etwas wie eine Programmsteuerung vor-
genommen werden sollte. Im Gegensatz zu den alten Rechenmaschinen sollte
die Analytische Maschine also nicht nur die Funktion eines verbesserten Aba-
kus übernehmen, und im Gegensatz zu Holleriths Lochkartenmaschine ging
es nicht nur um Zählen und Sortieren, sondern Babbage wollte echte flexible
Programme zur Verarbeitung seiner Eingabedaten schreiben können, bis hin
zu Verzweigungen in den Programmen, die je nach Eingabedaten verschiedene
Verarbeitungsmöglichkeiten eröffneten. Dabei war vorgesehen, die Program-
mierung auf Lochkarten festzuhalten. Die Idee war revolutionär und hat in
18 1 Grundlagen

den heutigen Computern überlebt. Leider war die Durchführung mit den da-
maligen technischen Mitteln unmöglich, viele Jahre nach Babbage hat man
aber anhand seiner Pläne mit moderneren technischen Mitteln seine Analy-
tische Maschine nachgebaut und dabei festgestellt, dass seine Konstruktion
in jeder Hinsicht funktionsfähig gewesen wäre; Babbage hatte seine guten
Ideen nur ein wenig zu früh gehabt. Für die Idee einer Allzweckmaschine,
bei der die Grundrechenarten fest in der Mechanik der Zahnräder installiert
waren, und die eigentlichen Instruktionen, die Programmanweisungen, über
Lochkarten der Maschine vermittelt wurden und dann durch ein kompliziertes
Hebelsystem in mechanische Vorgänge umgesetzt werden konnten - für eine so
moderne Idee war die Zeit wohl einfach noch nicht reif. Erst als man die reine
Mechanik durch die Elektromechanik und dann durch die Elektronik ersetzen
konnte, rückten solche Ideen in die Nähe des Möglichen.

Analytische Maschine
Die Analytische Maschine von Charles Babbage war, obwohl sie aufgrund
mechanischer Probleme nie gebaut wurde, ein Vorläufer der modernen pro-
grammgesteuerten Computer. Ihr Konstruktionsplan sah neben einer Ein- und
einer Ausgabevorrichtung ein Rechenwerk, einen Speicher und ein Steuerwerk
vor, das die Operationen des Programms koordinieren sollte.

Die anfangs gestellte Frage, wie alles begann, haben wir jetzt geklärt: es
gab einen Bogen vom Abakus über die mechanischen Rechenmaschinen des
siebzehnten Jahrhunderts bis hin zu den Ideen von Babbage und Hollerith, die
man bereits der Informatik zuordnen konnte. Das war eine lange Entwicklung
mit vielen Pausen und Stillständen, und die nächsten ernsthaften Fortschritte
konnten erst gemacht werden, als man anfing, die Elektrizität anzuwenden. Zu
den eigentlichen Anfängen der Datenverarbeitung gehört das nun nicht mehr,
die hören mit dem Ende des neunzehnten Jahrhunderts auf, aber eine kurze
Zusammenfassung sind die Computer, die man im zwanzigsten Jahrhundert
baute, allemal wert.

1.2.5 Elektromechanische und elektronische Rechner

Den ersten funktionsfähigen Rechenautomaten mit Programmsteuerung hat


wohl Konrad Zuse 1941 vorgestellt, mitten im zweiten Weltkrieg, der uns
gleich noch einmal begegnen wird. Es dauerte also mehr als 100 Jahre, bis
die Ideen von Babbage in die Tat umgesetzt werden konnten, und dann auch
noch von einem Konstrukteur, der von Babbage noch nie etwas gehört hat-
te. Er hatte schon 1938 einen ersten noch rein mechanischen Rechner mit
Programmsteuerung konstruiert, den ZUSE Z1, musste aber die gleiche Er-
fahrung machen wie Babbage: die Beschränkung auf die Mechanik reduzierte
die Funktionsfähigkeit des Systems, und deshalb machte er gleich anschließend
einen Versuch mit einem Gerät, das wenigstens teilweise auf elektromagneti-
schen Relais beruhte. Dieser ZUSE Z2 ist leider genauso wie sein Vorgänger
1.2 Wie alles begann 19

in den Kriegswirren verloren gegangen. Erst als er 1940 von der Deutschen
Versuchsanstalt für Luftfahrt den Auftrag zum Bau eines weiteren Geräts er-
hielt, konnte er sich ungehindert dem Bau des ZUSE Z3 widmen, der dann
1941 tatsächlich fertiggestellt wurde und vollständig auf der Relaistechnik be-
ruhte. So ein Relais ist ein elektromechanisches Bauteil, mit dem es möglich
ist, einen Stromkreis mit Hilfe eines anderen Stromkreises zu schließen. Der Z3
konnte die Relaistechnik besonders gut einsetzen, weil man die beiden mögli-
chen Zustände eines Stromkreises, nämlich offen und geschlossen, natürlich
verbinden kann mit den beiden Ziffern des dualen Zahlensystems, nämlich 0
und 1, und Zuse in seiner Konstruktion das duale Zahlensystem verwendete.
Auf diese Weise verband er also die alte Idee von Leibniz mit neuen elek-
tromechanischen Konzepten und schuf einen programmgesteuerten Rechner,
der für die Durchführung einer Multiplikation oder Division immerhin nur 3
Sekunden brauchte.

Abb. 1.4. ZUSE Z3 (Quelle: http://irb.cs.tu-berlin/∼zuse/de/Rechner Z3.html)

In seinen späten Jahren hat er sich übrigens der Malerei zugewendet und
war auch in diesem sehr von der Informatik und dem Ingenieurswesen ver-
schiedenen Gebiet erfolgreich.
Ich hatte schon erwähnt, dass der zweite Weltkrieg noch einmal vorkom-
men würde, und das ist auch kein Wunder. Schon der erste Weltkrieg war,
verglichen mit den Kriegen des neunzehnten Jahrhunderts, sehr stark von
neuen Technologien geprägt, und im zweiten war das nicht anders. Da die
Waffentechnik weit fortgeschritten war und insbesondere Granatenwerfer mit
großer Reichweite zur Verfügung standen, waren die Militärs an Berechnun-
gen interessiert, die ihnen vorhersagen konnten, wo ihre Granaten nach dem
Abschuss nun eigentlich einschlagen würden. Solche ballistischen Rechnungen
waren für die mechanischen Rechenmaschinen viel zu kompliziert, und um die
nötigen Berechnungen durchführen zu können, machte man sich in Harvard
an den Bau einer programmgesteuerten Rechenmaschine namens ASCC, den
Automatic Sequence Controlled Computer. Geistiger Vater dieses Rechners
20 1 Grundlagen

war der Mathematiker Howard Aiken. Im Gegensatz zu Zuse verwendete er


das Dezimalsystem und benutzte die elektromechanischen Relais, um schnelle
Kopplungsmechanismen zwischen den Zahnrädern herzustellen, die in seiner
Maschine noch eine bedeutende Rolle spielen. Vielleicht war das der Grund
für die riesigen Ausmaße des ASCC; er bestand aus 750000 Einzelteilen, hatte
eine Länge von 16 Metern, eine Höhe von etwa 2,5 Metern und wog 35 Tonnen
- ziemlich schwer, wenn man bedenkt, dass jeder Taschenrechner heute mehr
oder weniger dasselbe kann wie der ASCC. Für eine Multiplikation brauchte
der Computer 6 Sekunden, für eine Division 10 Sekunden, und obwohl er seit
1944 15 Jahre lang lief, wurde schnell klar, dass diese langen Rechenzeiten
das Ende der Relaisrechner bedeuten würden. Es war einfach noch zu viel
Mechanik in diesen Geräten, man brauchte etwas Besseres, schnellere Kopp-
lungsmechanismen, die wesentlich höhere Rechengeschwindigkeiten erlaubten.
Der ASCC hieß übrigens nicht lange ASCC, er wurde dann in MARK I um-
benannt und durch die Geräte MARK II bis MARK IV weiter entwickelt.

Relaisrechner
Die ersten programmgesteuerten Rechner auf der Basis elektromechanischer
Relais waren der ZUSE Z3 von Konrad Zuse (1941) und der ASCC oder auch
MARK I von Howard Aiken (1944). Während der Z3 auf dem Zweiersystem
beruhte, arbeitete der ASCC noch mit Dezimalzahlen.

Die Abkehr von der Mechanik ließ dann auch nicht lange auf sich war-
ten. Noch bevor der MARK I ganz fertig gestellt war, kam man auf die Idee,
Elektronenröhren beim Bau von Rechnern einzusetzen, mit denen man den
mechanischen Schwerfälligkeiten der Relais aus dem Weg gehen konnte. Elek-
tronenröhren gab es schon lange, spätestens seit dem Beginn des zwanzigsten
Jahrhunderts, aber sie litten unter dem Problem ihrer großen Empfindlichkeit
und geringen Lebensdauer, weshalb ihr Einsatz in einem Rechner zunächst
sehr bedenklich aussah. Auf der anderen Seite ist eine Elektronenröhre ein
vollelektronisches Bauteil, ohne jede Mechanik, und auf genauso etwas hatten
die Rechnerkonstrukteure gewartet. Als die Röhre dann nach einer etwa fünf-
zigjährigen Entwicklung einen vertretbaren Standard erreicht hatte, sodass
man sie als Schaltelement in Computern verwenden konnte, war dann auch
kein Halten mehr, die Rechner der so genannten ersten Computergeneration
konnten gebaut werden. Schon 1946 wurde der erste vollelektronische Rech-
ner in Amerika in Betrieb genommen, der Electronic Numerical Integrator
and Computer, abgekürzt ENIAC, von John Eckert und John Maunchly von
der Universität von Pennsilvanya. Alle Schaltungen im ENIAC, ausgenom-
men die Ein- und Ausgabe, waren vollelektronisch auf der Basis von Röhren
konstruiert; für die Zahlendarstellung wurde zwar nach wie vor das Dezi-
malsystem benutzt, aber trotzdem konnte man sie auf Elektronenröhren im
Rechner abbilden. Die Maschine hatte den Nachteil, dass zur Programmierung
keine Lochkarten oder ähnliches verwendet wurden. Wollte man also etwas in
1.2 Wie alles begann 21

der Programmierung ändern, dann blieb dem geplagten Programmierknecht


nichts anderes übrig als das neue Programm mühevoll auf einer Schaltta-
fel mit Leitungen und Steckern zusammenzustecken. Vielleicht führten diese
unangenehmen Erfahrungen kurz darauf zur Entwicklung der ersten höheren
Programmiersprachen, denn es war einfach kein guter Zustand, dass der Rech-
ner komplizierte mathematische Probleme in wenigen Sekunden lösen konnte,
die Benutzer aber Tage brauchten, um ihn von einem Problem auf das andere
umzurüsten.

Abb. 1.5. ENIAC (Quelle: http://de.wikipedia.org/wiki/ENIAC)

In dieser komplizierten Maschine versammelten sich 18000 Röhren auf ei-


ner Bodenfläche von etwa 150 Quadratmetern; Sie können sich leicht vorstel-
len, welcher Aufwand getrieben werden musste, um sie bei der immer noch
hohen Gefahr des Röhrenausfalls am Laufen zu halten. Tatsächlich haben die
Ingenieure es geschafft, den wöchentlichen Ausfall bei zwei bis drei Röhren zu
stabilisieren und damit die Leistungsfähigkeit zu garantieren. Vergessen Sie
nicht: der ENIAC kam nur zwei Jahre nach dem ASCC heraus, aber die Zeit
für eine Multiplikation war aufgrund der deutlich verbesserten Technologie
von sechs Sekunden auf drei Millisekunden gefallen.
Was die technische Seite betraf, so beruhte die zweite von Eckert und
Maunchly konzipierte Maschine, der EDVAC, wieder auf den Elektronenröhren,
die schon den ENIAC ausgefüllt hatten. Die Programmierung war allerdings
deutlich einfacher, denn inzwischen war man auf die Idee gekommen, den
Speicherbereich, in dem die Daten abgelegt werden, auch für die Ablage der
Programminstruktionen zu nutzen und damit die Programmierung eines Com-
puters deutlich flexibler und einfacher zu gestalten. Wesentlich beteiligt an der
22 1 Grundlagen

Entwicklung und Ausarbeitung dieser Idee war der Mathematiker John von
Neumann, von dem im nächsten Abschnitt über den Rechneraufbau noch die
Rede sein wird. Jetzt wurden die Rechnungen auch im Dualsystem durch-
geführt, und der gesamte Aufbau des Rechners entsprach ziemlich genau der
Architektur der Theorie gebliebenen Analytischen Maschine von Babbage.

Die erste Computergeneration


Unter der ersten Computergeneration versteht man die auf der Basis von Elek-
tronenröhren funktionierenden Rechner der späten vierziger und der fünfziger
Jahre des zwanzigsten Jahrhunderts wie beispielsweise der ENIAC und der
EDVAC, aber auch der UNIVAC, der auf den Prinzipien des EDVAC beruht.

Auf die erste Computergeneration folgte die zweite, aber hier wird es
tatsächlich Zeit, diesen historischen Abriss zu beenden, denn inzwischen ha-
ben wir uns doch schon bedenklich der heutigen Zeit genähert. Mit dem Erset-
zen der Elektronenröhren durch Transistoren als Bauelemente, was sowohl im
Hinblick auf die Zuverlässigkeit als auch auf die Rechengeschwindigkeit und
die Übersichtlichkeit der Ausmaße deutliche Fortschritte brachte, wurden die
Computer der zweiten Generation eingeführt, und damit begann auch der
Übergang vom Experimentalcomputer zum Massencomputer. Während alle
Rechner, die ich Ihnen vorgestellt habe, Experimentalcomputer waren, Ein-
zelstücke, die nur einmal auf der Welt vorkamen, ging man bald darauf zur
fabrikmäßigen Computerfertigung über - eine Folge der neuen Technologie, die
in der zweiten Computergeneration zum Einsatz kam. Gab es beispielsweise
1953 in den USA gerade einmal 50 Computer und in der restlichen Welt eher
weniger als mehr, so waren 1959 in den USA schon etwa 4000 Rechner im Ein-
satz und an die 900 in der restlichen Welt. Schon 1970 war die stattliche Zahl
von weltweit 100000 Computern reichlich überschritten, und wie viele Millio-
nen Apparate heute die Büros, Wohnzimmer und Kinderzimmer beherrschen,
weiß niemand zu sagen. Beachtlich, wenn man bedenkt, dass Howard Aiken,
der Schöpfer des MARK I, noch 1948 meinte, in den USA gebe es einen Bedarf
für fünf, höchstens sechs Computer, und mehr würde kein Mensch brauchen.

Die zweite Computergeneration


Unter der zweiten Computergeneration versteht man die auf der Basis von
Transistoren funktionierenden Rechner der späten fünfziger Jahre und der
sechziger Jahre des zwanzigsten Jahrhunderts.

1.2.6 Programmiersprachen

Noch ein Wort zu einem anderen Aspekt, der bisher etwas untergegangen
ist. Wie Sie sehen konnte, verlief die Entwicklung der Computer von den
ersten lochkartengesteuerten Maschinen hin zu besser konzipierten Rechnern
wie dem EDVAC, die auf dem Konzept des gespeicherten flexiblen Programms
1.2 Wie alles begann 23

beruhten. Das war ja schon mal ganz gut, aber auf welche Weise sollte man
diese Programme denn schreiben? Manche Leute hielten diese Frage für falsch
gestellt; als man den großen Logiker Kurt Gödel fragte, meinte er, Program-
miersprachen seien unnötig, Logik genüge. Vielleicht lag es unter anderem
auch an solchen Auffassungen, dass er später verrückt geworden ist.
Die Ingenieure und Benutzer der alten Maschinen waren vielleicht willens
und in der Lage, die Programme in maschinenlesbarer Form zu formulieren,
aber wenn man für eine weitere Verbreitung der Computer sorgen wollte,
dann musste man Möglichkeiten zu einer komfortableren Programmierung
bereit stellen. Um beispielsweise den MARK I von Aiken zu programmieren,
musste der Benutzer für jede Instruktion eine Unmenge von Löchern in einen
Papierstreifen stanzen. Das war kein reines Vergnügen, und es war klar, dass
es so nicht bleiben konnte: die Idee der höheren Programmiersprachen war ge-
boren, ohne die man sich die Informatik überhaupt nicht mehr denken kann.
Dieser Idee nachzugehen, war nicht so selbstverständlich, wie Sie vielleicht
glauben. Howard Aiken zum Beispiel, der Schöpfer des MARK I, scheint in
der komplizierten Programmierung seiner Maschine überhaupt kein Problem
gesehen zu haben. Einer neuen Mitarbeiterin, im Hauptberuf immerhin ei-
ne von Computerproblemen bis dahin unbelastete Mathematikprofessorin in
Harvard, zeigte er einmal einen großen Kasten und meinte, das sei eine Re-
chenmaschine und sie solle doch bitte bis Donnerstag damit die Koeffizienten
der Arcustangensreihe ausrechnen. Die arme Frau konnte nichts anderes sagen
als Ja, Sir“ und sich verzweifelt fragen, an welchen Verrückten sie da geraten

war. Sie hat es übrigens gut überstanden und es später noch bis zur Admiralin
im wissenschaftlichen Dienst der Navy gebracht.
Es fing an mit einer gar nicht hohen, sondern sehr maschinennahen Spra-
che, dem Assembler, bei dem jeder Befehl noch einem Maschinenbefehl oder
doch nur einer sehr kleinen Zahl von Maschinenbefehlen entsprach. Damit
war zwar das Programmieren etwas menschlicher geworden, weil man nicht
mehr direkt mit den Einsen und Nullen hantieren musste, aber jeder, der
schon einmal mit Assembler zu tun hatte, weiß, dass diese Art der Program-
mierung eine ziemliche Plage sein kann. Die erste ernstzunehmende und auch
noch erfolgreiche Hochsprache war dann FORTRAN, der Formula Translator,
aus dem Jahr 1957. Mit FORTRAN konnte man endlich Formeln annähernd
so in einen Computer programmieren, wie man sie vom Papier her gewöhnt
war, und musste sich nicht mehr mit den maschineninternen Einsen oder Nul-
len herumärgern. Außerdem ging diese Bequemlichkeit nicht zu Lasten der
Geschwindigkeit: ein FORTRAN-Programm, das vom mitgelieferten Compi-
ler in ein maschinenlesbares Programm übersetzt worden war, lief genauso
schnell wie ein von Hand direkt in die Maschine geschriebenes Programm, bei
dem sich der Programmierer noch selbst Gedanken über die Verteilung der
Nullen und Einsen machen musste. Beide Aspekte zusammen, die Bequem-
lichkeit und die Geschwindigkeit, waren schon sehr überzeugende Argumen-
te, aber vielleicht lag das überzeugendste Argument in dem Umstand, dass
FORTRAN nun mal vom Marktführer IBM angeboten wurde. Trotz deutlich
24 1 Grundlagen

neuerer Programmiersprachen wird FORTRAN übrigens auch heute noch in


aktualisierten Versionen eingesetzt.
Während FORTRAN eher für den Bedarf des Technikers und des Natur-
wissenschaftlers gedacht war, musste sich der Betriebswirt oder der Lohn-
buchhalter anderweitig nach einer passenden Programmiersprache umsehen,
so etwas wie FORTRAN passte einfach nicht zu betriebswirtschaftlichen Pro-
blemen. Die kommerziellen Anwender mussten aber nicht lange warten, denn
bereits 1959 kam die Common Business Oriented Language, abgekürzt CO-
BOL, heraus und beherrschte für lange Zeit den Markt der Programmier-
sprachen für betriebswirtschaftliche Anwendungen. COBOL ist eine sehr ge-
schwätzige Programmiersprache, die Erfinder wollten sie so gestalten, dass
man den Programmtext schon beim Lesen verstehen konnte, ohne weitere
Dokumentation, und legten deshalb Wert darauf, manche Operationen durch
Worte anstatt durch algebraische Symbole auszudrücken. Funktioniert hat
das natürlich nicht, denn wer wird schon so etwas wie MULTIPLY A BY B
schreiben, wenn ein schlichtes A * B auch funktioniert? Immerhin hat sich
auch COBOL sehr lange gehalten und ist heute noch im Gebrauch, wenn
auch nicht mehr so intensiv wie früher. Sein früher und durchschlagender Er-
folg könnte natürlich auch ein wenig damit zusammenhängen, dass schon 1960
die US-Regierung verlauten ließ, sie würde keinen Computer mehr kaufen, auf
dem COBOL nicht liefe, ein schönes Beispiel für freie Marktwirtschaft.
Zum Jahreswechsel 1999/2000 geisterte es sogar noch einmal durch die
Zeitungen, als die EDV-Abteilungen auf die Idee kamen, dass die alten seit
Urzeiten laufenden COBOL-Programme beim Übergang auf das Jahr 2000 zu
großen Schwierigkeiten führen könnten. Für kurze Zeit muss in Florida die
Hölle los gewesen sein, zumindest geht das Gerücht, dass die alten längst pen-
sionierten COBOL-Programmierer amerikanischer Firmen unter Androhung
hoher Honorare von ihren Golfplätzen und Stränden gescheucht wurden, weil
sie die einzigen waren, die noch mit den COBOL-Programmen umgehen und
die Gefahrenquellen beseitigen konnten. Passiert ist dann beim Jahrtausend-
wechsel nur sehr wenig, aber immerhin konnten sich ein paar amerikanische
Rentner noch etwas ihren Ruhestand vergolden.

Programmiersprachen Die ersten höheren Programmiersprachen waren


der Formula Translator FORTRAN und dieCommon Business Oriented Lan-
guage COBOL.

Plötzlich sind wir schon im Jahr 2000 angekommen, und das hat nun
wirklich mit den Anfängen nichts mehr zu tun. Jetzt wird es Zeit, sich etwas
genauer anzusehen, was aus den Ideen von Charles Babbage geworden ist, und
sich mit dem Aufbau eines heutigen Rechners zu befassen.
1.3 Rechneraufbau 25

Übungen

1.4. Lochkarten wurden in der frühen Zeit der Datenverarbeitung oft zur Spei-
cherung von Daten verwendet. Diskutieren Sie die Vor- und Nachteile von
Lochkarten als Datenspeicher.

1.5. Informieren Sie sich über die Funktionsweise eines Relais und erläutern
Sie sie. Wofür wurden Relais in den ersten elektromechanischen Rechenma-
schinen eingesetzt? Wo liegt ihre wesentliche Schwäche im Vergleich zu den
Elektronenröhren oder Transistoren?

1.6. Wie sind die ersten elektronischen Rechner im Hinblick auf ihre Program-
miermöglichkeiten zu beurteilen? Diskutieren Sie die Auffassung des Logikers
Kurt Gödel, Programmiersprachen seien unnötig, Logik genüge völlig.

1.3 Rechneraufbau

In der alten Zeit der Datenverarbeitung war jeder Recher anders. Es handelte
sich im wesentlichen um Einzelstücke, und die Konstrukteure suchten über
einen langen Zeitraum nach einer Architektur, die den Problemen angemes-
sen war und dem Rechner eine hinreichend schnelle und effiziente Arbeitsweise
ermöglichten. Dass sich so eine standardisierte Architektur nicht von heute auf
morgen entwickeln und etablieren kann, dürfte klar sein, aber immmerhin hat
sich das Grundprinzip schon in den vierziger Jahren des zwanzigsten Jahr-
hunderts herauskristallisiert, als der ungarische Mathematiker John von Neu-
mann in einem Artikel das Konzept des gespeicherten Programms erläuterte.
Er war keineswegs grenzenlos computergläubig, was seine Arbeiten zur Rech-
nerarchitektur wohl noch etwas glaubhafter und überzeugender machte, als
sie es inhaltlich ohnehin schon waren. In seiner amerikanischen Zeit war er
Mitglied einer Kommission zur Bewilligung von Geldern für die Entwicklung
von Computern, und als eine Marinedienststelle einmal einen Computer bean-
tragte mit dem etwas vagen Argument, man wolle bestimmte Probleme damit
lösen, fragte von Neumann gezielt nach, um welche Probleme es sich denn
handle. Der Antragsteller nannte ein mathematisches Problem, woraufhin von
Neumann zehn Minuten nachdachte und dann die Lösung des Problems an die
Tafel schrieb. Da der verdutzte Marineoffizier kein weiteres Problem wusste,
mit dem man die Anschaffung des Computers hätte rechtfertigen können, war
die Welt vor einem unnötigen Computer bewahrt worden.

1.3.1 Der Aufbau eines Taschenrechners

John von Neumann wusste also genau, wofür man einen Computer braucht
und wofür nicht, und ich werde Ihnen jetzt Schritt für Schritt zeigen, wie ein
Rechner nach der Architektur von Neumanns aufgebaut ist. Dazu fangen wir
26 1 Grundlagen

klein an und werfen zunächst einen Blick auf die grundsätzliche Architektur
eines Taschenrechners. Das muss kein großer ernsthaft leistungsfähiger Ta-
schenrechner sein, für den Moment genügt mir einer, der zur Addition ganzer
Zahlen fähig ist. Was passiert beispielsweise, wenn Sie mit Ihren Taschenrech-
ner die Addition 5 + 2 ausführen wollen? Sie werden die Zahl 5 eingeben,
dann die +-Taste drücken und anschließend die Zahl 2 eingeben. Das ist nicht
weiter aufregend. Wie Sie aber schon im letzten Abschnitt gesehen haben,
wird ein Rechner nicht mit der dezimalen Zahl 5 oder auch 3 arbeiten; da er
nun mal elektronisch ist und auf der Frage Strom oder nicht Strom“ beruht,

arbeitet er nur mit zwei Zuständen, also mit den Ziffern 0 und 1. Ihr Rechner
muss also auf irgendeine Weise die beiden eingegebenen Zahlen in jeweils eine
Folge aus Nullen und Einsen umwandeln, anders gesagt: er muss die beiden
Dezimalzahlen in die von Leibniz erfundenen Dualzahlen umwandeln. Wie so
etwas im Einzelnen geht, werde ich Ihnen im nächsten Abschnitt zeigen. Im
Augenblick genügt die Erkenntnis, dass dezimale Ziffern in duale Zahlen co-
diert werden müssen und dass man das mit Hilfe einer Art von Codetabelle
erledigen kann, die die Informationen für die Umrechnung bereitstellt.
Jetzt habe ich also die binäre Darstellung der 5 bestimmt, ich verrate Ih-
nen schon einmal, dass dabei 0101 herauskommt. Bevor nun das Pluszeichen
eingetippt wird, muss diese 0101 gespeichert werden, denn mit ihr will ich
nachher noch rechnen. Ihr Rechner braucht daher einen Arbeitsspeicher, der
die eingegebenen Zahlen und Symbole aufnehmen kann. In diesem Arbeitsspei-
cher landet also die Zahl 0101, aber der Speicher ist nur eine Zwischenstation,
in ihm wird nichts gerechnet. Dafür ist das Rechenwerk da, in das die einge-
gebene Zahl 0101 aus dem Arbeitsspeicher überführt wird. Das anschließend
eingegebene Pluszeichen wird auch erst einmal vom Arbeitsspeicher aufge-
nommen. Ein Pluszeichen ist aber keine Zahl, hat also im Rechenwerk nichts
zu suchen, sondern man könnte sagen, dass es die Verarbeitung steuert, die
mit den beiden Eingabewerten durchgeführt werden soll. Deshalb gibt es auch
noch ein Steuerwerk, das die nötigen Operationen an den Eingabedaten orga-
nisiert, und genau dort wird das Pluszeichen abgelegt. Was dann mit der Zahl
2 passiert, wird keinen überraschen: sie muss erst ins Dualsystem umgerechnet
werden, was zu der dualen Zahl 0010 führt, und über den Arbeitsspeicher ins
Rechenwerk transportiert werden. Nun haben wir alles an der richtigen Stelle.
Im Steuerwerk steht der gewünschte Befehl, im Rechenwerk die Eingabedaten,
und daher sorgt der Additionsbefehl dafür, dass die beiden in binärer Form
vorliegenden Daten addiert werden. Auch über die Addition solcher Zahlen
werde ich Ihnen im nächsten Abschnitt berichten, für den Moment werden
Sie mir hoffentlich glauben, dass dabei die duale Zahl 0111 herauskommt.
Wird nun der Taschenrechner die Ziffernfolge 0111 anzeigen? Das ist eher un-
wahrscheinlich, Sie wollen ja schließlich eine anständige Zahl in Ihrem Display
sehen. Der Rechner wird also diese binäre Ziffernfolge in seinen Arbeitsspei-
cher laden und sie erneut mit Hilfe einer Codetabelle in die Dezimalzahl 7
umrechnen, die dann zum guten Schluss auf Ihrer Ausgabeeinrichtung, dem
Display, erscheint.
1.3 Rechneraufbau 27

Tastatur
Zentraleinheit

Codetabelle
Prozessor

Steuerwerk
Arbeitsspeicher
Rechenwerk

Codetabelle

Ausgabe

Abb. 1.6. Architektur eines Taschenrechners

In Abbildung 1.6 können Sie den eben beschriebenen Ablauf noch einmal
in einer Graphik sehen. Die Zentraleinheit umfasst also sowohl den Prozessor,
der sich aus Rechenwerk und Steuerwerk zsammensetzt, als auch den Arbeits-
speicher. Beides ist schon unverzichtbar, wenn Sie so einfache Aufgaben wie
die Addition zweier ganzer Zahlen bewältigen wollen.
Einerseits ist das keine gute Nachricht, denn ein Taschenrechner ist doch
wohl kein allzu kompliziertes Gerät und hat trotzdem schon eine recht auf-
wendige Struktur. Aber andererseits sieht die Welt doch wieder etwas heller
aus, wenn ich Ihnen verrate, dass Sie hier schon wesentliche Teile des Aufbaus
eines anständigen programmierbaren Rechners gefunden haben, von dem wir
gar nicht mehr so weit entfernt sind. Natürlich kann man einen Taschenrech-
ner nicht unbegrenzt einsetzen, seine begrenzte Leistungsfähigkeit lässt seinen
Einsatz nur bei recht übersichtlichen Aufgaben sinnvoll erscheinen. Sie können
beispielsweise in seinem Arbeitsspeicher nur wenige Ziffern unterbringen, für
größere Datenmengen ist er schlichtweg zu klein. Die Rechenoperationen wird
sich der Rechner kaum merken können; wenn Sie also mehrmals hintereinan-
der das Gleiche tun wollen, wenn auch vielleicht mit anderen Zahlen, dann
bleibt Ihnen nichts anderes übrig als mehrmals hintereinander das Gleiche ein-
zutippen. Und falls Sie zufällig an den falschen Schalter kommen und Ihrem
Taschenrechner den Strom abdrehen, ist der gesamte Inhalt des Arbeitsspei-
chers gelöscht, weil er keine Daten dauerhaft speichern kann. Es ist also klar,
dass wir für größere Aufgaben eine Erweiterung der Architektur vornehmen
müssen, aber keine Bange: der Grundstein ist mit dem Aufbau des Taschen-
rechners bereits gelegt.
28 1 Grundlagen

Architektur eines Taschenrechners


Ein Taschenrechner benötigt neben der Eingabevorrichtung Tastatur und der
Ausgabevorrichtung Display eine Zentraleinheit, in der die nötigen Verarbei-
tungen vorgenommen werden. Sie besteht im Wesentlichen aus dem Prozessor
und dem Arbeitsspeicher. Der Arbeitsspeicher nimmt die über die Tastatur
eingegebenen Daten auf, leitet sie an den Prozessor weiter und nimmt wieder
eventuelle Ergebnisse auf. Der Prozessor besteht aus dem Steuerwerk, das die
Abarbeitung der gewünschten Operationen organisiert, und dem Rechenwerk,
in dem die eigentlichen Rechnungen durchgeführt werden.

1.3.2 Die Architektur eines von Neumann-Rechners

Zunächst einmal ist klar, dass Sie für die Bearbeitung komplexerer Aufgaben
mehr als nur die eine oder andere Ziffer in Ihrem Arbeitsspeicher abspeichern
können müssen; man braucht also eine deutliche Erweiterung des Arbeits-
speichers. Aber warum heißt der Arbeitsspeicher eigentlich Arbeitsspeicher?
Natürlich weil man mit ihm und seinen Inhalten arbeiten will, und das be-
deutet, dass er nicht irgendwo extern, beispielsweise in Form einer Festplatte
vorliegen kann, sondern weiterhin direkt in der Zentraleinheit des Rechners,
denn Ihr Prozessor muss schnell auf die Daten des Arbeitsspeichers zugrei-
fen können. Genau deshalb verschwindet auch sein Inhalt, wenn Sie der Ma-
schine den Saft abdrehen: der Arbeitsspeicher ist auf vollelektronischer Basis
organisiert, und wenn da kein Strom mehr fließt, dann hat der Kaiser sein
Recht verloren. Genau in dieser Organisationsform liegt allerdings auch sein
Vorteil, da auf diese Weise eine schnelle Kommunikation zwischen dem Pro-
zessor, der die Rechenarbeit erledigen soll, und dem Arbeitsspeicher, der die
dazu notwendigen Daten bereitzustellen hat, stattfinden kann. Beachten Sie,
dass sowohl die nötigen Befehle als auch die Daten im gleichen Arbeitsspei-
cher unterkommen; man hat darauf verzichtet, den Datenspeicher und den
Befehlsspeicher grundsätzlich voneinander zu trennen, und das ist auch gut
so. Schließlich wird von Problem zu Problem das Verhältnis zwischen reinen
Daten und Befehlen ein anderes sein, und wenn Sie zwei sauber getrennte
Speicherbereiche hätten, dann wäre je nach Art der Aufgabe die Gefahr der
Speicherplatzverschwendung recht groß. Da es aber nur einen Arbeitsspeicher
gibt, in dem man ganz nach Bedarf mal dieses und mal jenes parkt, kann das
Problem der Verschwendung erst gar nicht auftreten.
In Ihrem Arbeitsspeicher sollen nun also die Daten abgelegt werden und die
Befehle zu ihrer Verarbeitung. Diese Befehle beschränken sich nicht mehr nur
auf simple Rechenoperationen wie Plus oder Minus; Sie haben schon am Bei-
spiel der Programmiersprachen FORTRAN und COBOL gesehen, dass auch
kompliziertere algebraische Ausdrücke möglich sind. Also braucht der Rech-
ner eine erweiterte Tastatur, die flexible Eingaben ermöglicht: die Ihnen allen
vertraute Computertastatur, wesentlich größer und leistungsfähiger als die
Tastatur eines üblichen Taschenrechners. Und auch die Ausgabe erfordert ein
1.3 Rechneraufbau 29

etwas aussagekräftigeres Gerät als das Display eines Taschenrechners, womit


wir beim ebenfalls vertrauten Computerbildschirm angelangt sind.
Aber wie sieht es nun im Inneren der Zentraleinheit aus, dem eigentlichen
Herzstück des Computers? Nicht viel anders als in einem Taschenrechner, nur
eben größer, besser und hoffentlich auch schneller. Auch hier besteht der Pro-
zessor aus einem Steuerwerk, das die organisatorische Arbeit erledigt, und
einem Rechenwerk, dem die eigentliche Aufgabe des Rechnens zugeordnet ist
- das werden Sie in den nächsten Abschnitten noch genauer sehen. Die Ein-
heit von Steuerwerk und Rechenwerk nennt man auch auf Englisch die Central
Processing Unit abgekürzt CPU. Der Arbeitsspeicher enthält, wie besprochen,
Daten und Befehle, also sowohl das Programm, mit dem die Daten zu bear-
beiten sind, als auch die Daten, die das Programm zu bearbeiten hat. Hier
sehen Sie schon, wie wichtig das Steuerwerk für die korrekte Verarbeitung der
Daten ist, seine Rolle ist in etwa die eines Organisators oder Projektleiters in
einem Betrieb. Es sorgt dafür, dass die einzelnen Befehle des Programms aus
ihren Parkplätzen im Arbeitsspeicher geholt werden, es besorgt dem Rechen-
werk die nötigen Daten aus dem Arbeitsspeicher, damit das Rechenwerk auch
etwas zu rechnen hat, es veranlasst das Rechenwerk, mit den übermittelten
Befehlen die übermittelten Daten zu bearbeiten, und am Ende schaufelt es
die berechneten Ergebnisse wieder in den Arbeitsspeicher, damit der Benutzer
seine Freude daran hat. Kurz gesagt: das Rechenwerk erledigt die Rechenauf-
gaben, das Steuerwerk dagegen die organisatorischen Aufgaben.

Eingabegeräte
Zentraleinheit

Prozessor
Arbeitsspeicher
Steuerwerk
Daten

Rechenwerk Programme

Externer Daten Ausgabegeräte


Speicher
Programme

Abb. 1.7. Aufbau eines von Neumann-Computers


30 1 Grundlagen

Wenn Sie nun einen Blick auf Abbildung 1.7 werfen, dann sehen Sie schon
die Lösung eines Problems, das ich noch gar nicht erwäht hatte. Was macht
man denn, wenn der Arbeitsspeicher zu klein ist? So ein Speicher, der auf
elektronischen Schaltungen beruht, kann nicht unbegrenzt groß werden, und
es kann leicht passieren, dass nicht alle Befehle oder alle Daten, die für eine
bestimmte Verarbeitung gebraucht werden, in diesen Speicher hineinpassen.
Und das ist nicht mal das einzige Problem, ein anderes liegt noch viel näher:
was passiert denn, wenn Sie Ihre Daten brav eingetippt, Ihre Programme
fleißig dem Arbeitsspeicher übermittelt haben und dann der Strom ausfällt?
Die Antwort kennen Sie schon, dann ist eben alles verloren, man kann von
vorne anfangen und fühlt sich so wie Sisyphos, der dazu verurteilt war, den
immer gleichen Felsbrocken immer wieder auf den immer gleichen Hügel zu
rollen und kurz vor dem Ziel an der immer gleichen Stelle festzustellen, dass
der elende Stein ihm schon wieder entglitt und nach unten rollte.
Nun hatte aber Sisyphos die Götter arg erzürnt und musste seine ewige
Strafe in der Unterwelt abbüßen, während Sie vollständig am Leben sind und
hoffentlich keinen Ärger mit irgendwelchen Göttern haben. Deshalb gibt es
für unsere Probleme auch eine einfache Lösung. Die Daten und Programme
landen eben nicht gleich im Arbeitsspeicher, sondern man pflegt sie in einem
externen Speicher abzulegen, der seine Informationen absolut unelektronisch
aufbewahrt und daher nicht vom Stromfluss abhängig ist. Normalerweise be-
ruht so ein externer Speicher auf den üblichen Festplatten, die auf Magneti-
sierungsbasis arbeiten, aber auch Magnetbänder oder aber CDs sind denkbar,
die mit Laser arbeiten. In jedem Fall sind Ihre Eingaben dann vor plötzlichen
Stromausfällen gesichert, und es muss nur für eine Verbindung zwischen dem
externen Speicher und dem Arbeitsspeicher gesorgt werden. Da der externe
Speicher nicht so flüchtig ist wie der Arbeitsspeicher, nennt man ihn auch
Festspeicher, und da sich umgekehrt der Arbeitsspeicher im Gegensatz zum
externen Speicher innerhalb der Zentraleinheit befindet, bezeichnet man ihn
auch als den internen Speicher. Um der Wahrheit die Ehre zu geben, ordnen
manche Autoren auch den externen Speicher der Zentraleinheit zu tun, womit
er genau genommen zum internen Speicher wird, aber das ist im Grunde reine
Definitionssache.
Der externe Speicher, üblicherweise in Form einer Festplatte, hat also meh-
rere Funktionen. Erstens sorgt er dafür, dass eingegebene Daten und Pro-
gramme mehrfach verwendbar sind, unabhängig von der Stromversorgung des
Computers. Zweitens kann man natürlich auch umgekehrt die Ergebnisse, die
der Computer liefert, nicht nur dem unsicherern Arbeitsspeicher überlassen,
sondern sie auch auf dem Festspeicher ablegen, damit die Nachwelt auch noch
etwas davon hat; insofern gehört auch die Festplatte zu den Ausgabegeräten,
die in Abbildung 1.7 neben dem externen Speicher platziert wurden. Und
drittens können Sie mit Hilfe des Festspeichers auch das oben angesprochene
Problem der zu groß geratenen Programme oder Datensätze lösen. Wenn bei-
spielsweise ein Programm so groß geworden ist, dass es nicht mehr vollständig
in den Arbeitsspeicher, aber immer noch locker auf die Festplatte passt, dann
1.3 Rechneraufbau 31

ist es möglich, das Programm eben nicht ganz, sondern nur häppchenweise
zusammen mit den jeweils nötigen Daten in den Arbeitsspeicher zu laden,
dann vom Prozessor die geladenen Befehle ausführen zu lassen und nach ihrer
Ausführung einfach die nächste Portion von Befehlen aus dem Festplatten-
speicher zu holen. Das klingt recht einfach, ist aber recht kompliziert, denn ir-
gendjemand muss das alles ja steuern. Sie werden kaum den Drang verspüren,
Ihre mühsam geschriebenen Programme auch noch in leicht verdauliche Por-
tionen aufzuteilen, Sie wollen das Programm starten und zusehen, ob es richtig
ist. Deshalb wird dieses häppchenweise Einlagern von Programmteilen auch
nicht vom Benutzer durchgeführt, sondern vom so genannten Betriebssystem,
einem speziellen Programm, das die Abläufe auf dem Rechner organisiert.

Architektur eines von Neumann-Computers


Ein von Neumann-Computer ist durch das Konzept des gespeicherten Pro-
gramms gekennzeichnet. In seinem Arbeitsspeicher werden sowohl reine Da-
tensätze als auch Programmteile gespeichert, auf die seine CPU direkt zugrei-
fen kann, was eine schnelle Abarbeitung der Programme gewährleisten soll.
Zusätzlich verfügt er in der Regel über externe Speicher, oft in Form einer
Festplatte, auf denen größere Datenmengen und Programme abgelegt wer-
den können als im Arbeitsspeicher. Über das Betriebssystem wird gesteuert,
welche Programmteile vom Arbeitsspeicher aufgenommen werden.

1.3.3 Arbeitsspeicher und Festplattenspeicher

Nun habe ich die ganze Zeit über verschiedene Speicher geredet, und wir soll-
ten wenigstens einen kurzen Blick darauf werfen, wie so ein Speicher aussieht.
Der Arbeitsspeicher oder auch Hauptspeicher besteht einfach nur aus einer
Folge von vielen gleichgroßen Speicherelementen, die auf der Basis elektroni-
scher Schaltungen organisiert sind. Da es hier um elektrischen Strom geht und
man nur unterscheidet, ob Strom fließt oder nicht, gibt es für ein Speicherele-
ment genau zwei verschiedene Möglichkeiten der Belegung: Strom fließt oder
nicht, also 1 oder 0. Ein einzelnes Element, das nur 0 oder 1 sein kann, wird
als Bit bezeichnet, was nichts weiter ist als eine Abkürzung für binary digit,
also binäre Einheit, und darauf anspielt, dass Sie sich bald mit der Arith-
metik binärer Zahlen werden herumschlagen dürfen. Nun kann man aber in
einem Bit nicht allzuviele Informationen unterbringen, und deshalb fasst man
acht Bits zusammen zu einem Byte. Bei Licht betrachtet sind diese Bytes die
Grundeinheiten der Speicherung und nicht die kleineren Bits, aus denen sich
die Bytes zusammensetzen. Auf ein einzelnes Bit zuzugreifen ist gar nicht so
einfach und außerdem meistens ziemlich sinnlos, da es Ihnen keine nennens-
werte Information liefern kann. Die Bytes im Arbeitsspeicher sind dagegen
Einheiten, mit denen man etwas anfangen kann; es ist möglich, ein ganzes
Zeichen wie etwa einen Buchstaben in Form eines Bytes abzuspeichern, und
vor allem hat jedes Byte seine eigene Adresse im Hauptspeicher. Einfacher
32 1 Grundlagen

kann man es sich gar nicht mehr vorstellen, die erste Adresse ist die 0, und
dann geht es ganz schlicht linear weiter, indem man schön der Reihe nach
nummeriert. Sie können es sich vorstellen wie in einem Gefängnis, in dem
die Bytes in ordentlich durchnummerierten Zellen gefangen gehalten werden,
und vielleicht liegt es an dieser Assoziation, dass man die mit einer Adresse
gekennzeichneten Speicherbereiche meistens als Speicherzellen bezeichnet. Da
eine Adresse so gut ist wie eine andere, kann man auf jede Speicherzelle so
schnell zugreifen wie auf jede andere; man spricht deshalb auch vom wahlfreien
Zugriffsspeicher, auf Englisch Random Access Memory, abgekürzt RAM.

Speicherstelle 0
Speicherstelle 1
Speicherstelle 2
Speicherstelle 3
Speicherstelle 4
Speicherstelle 5

Abb. 1.8. Aufbau des Arbeitsspeichers

In Abbildung 1.8 sehen Sie, dass der Arbeitsspeicher ganz einfach als Ab-
folge von hintereinander liegenden Speicherzellen aufgebaut ist. Die Numme-
rierung erfolgt natürlich in Wahrheit mit binären Zahlen, aber die lernen Sie
ja erst im nächsten Abschnitt, also habe ich hier noch mit Dezimalzahlen
durchgezählt. Dass der Arbeitsspeicher etwas mehr als nur fünf Speicherzel-
len haben wird, dürfte klar sein, und in der Regel wird es bei den heutigen
Speichergrößen auch etwas zu schwerfällig, die Bytes einzeln zu zählen, son-
dern man ordnet sie in Tausender- und Millionenpaketen - wenn auch nicht
ganz. Während ein Kilogramm genau 1000 Gramm entspricht, finden Sie in
einem Kilobyte nicht ebenso 1000 Byte, sondern genau 1024. Warum so eine
krumme Zahl? Weil sie überhaupt nicht so krumm ist,wie sie auf den ersten
Blick aussieht. Ich habe schon mehrfach erwähnt, dass in einem modernen
Computer alle Rechnungen und eben auch alle Adressierungen auf der Basis
der Binärzahlen vorgenommen werden, und die beruhen auf der Basis 2. Und
wie man im Dezimalsystem sich immer an den Zehnerpotenzen 1, 10, 100, 1000
usw. orientiert, bilden im Binärsystem die Zweierpotenzen 1, 2, 4, 8 usw. die
nötigen Anhaltspunkte. Wegen 1024 = 210 ist aber 1024 die zu 1000 nächst-
liegende Zweierpotenz, und daher hat ein Kilobyte keine 1000 Byte, sondern
1024. Genauso ist es bei der nächstgrößeren Einheit: ein Megabyte, das in Ge-
wicht gerechnet einer Tonne entsprechen würde, hat nicht eine Million Byte,
1.3 Rechneraufbau 33

sondern 220 = 1048576 Byte. Niemand merkt sich diese genauen Zahlen, für
ungefähre Rechnungen reicht in aller Regel die Gleichsetzung von einem Ki-
lobyte mit etwa 1000 Byte und einem Megabyte mit etwa einer Million Byte.
Ein Megabyte war noch in den achtziger Jahren des zwanzigsten Jahr-
hunderts gar nicht mal wenig, man hatte Festplattenspeicher von vielleicht
20 Megabyte und fragte sich verzweifelt, wie man die jemals voll bekommen
sollte, ein üblicher Arbeitsspeicher fasste gerade 512 Kilobyte. Das hat sich
gründlich geändert und wird sich auch weiter ändern. Vor mir liegt ein ak-
tueller Werbeprospekt eines großen Medienhauses, das mir immer wieder in
der Werbung versichert, ich sei doch nicht blöd. Der PC, den mir das Pro-
spekt heute, Anfang des Jahres 2005, vorstellt, hat einen Arbeitsspeicher von
1024 Megabyte, und einen Festplattenspeicher, für den man schon gar nicht
mehr in Megabyte rechnet: die nächsthöhere Einheit ist das Gigabyte, das
1024 Megabyte entspricht, und der aktuelle Prospektrechner hat freundliche
500 Gigabyte auf seiner Festplatte. Verglichen mit meinem ersten PC hat die-
se Maschine also einen 2048-mal so großen Arbeitsspeicher und sogar einen
51200-mal so großen Festplattenspeicher - keine schlechte Entwicklung für
ungefähr 15 Jahre.

Arbeitsspeicher Der Arbeitsspeicher oder auch Hauptspeicher besteht aus


linear angeordneten Speicherzellen, die auf der Basis elektronischer Schaltun-
gen organisiert sind. Eine Speicherzelle kann ein Byte aufnehmen, wobei ein
Byte aus acht Bit besteht und jedes Bit entweder den Wert 0 oder den Wert 1
hat. Jede Speicherzelle im Arbeitsspeicher hat eine Adresse, was dazu führt,
dass jede Speicherzelle über ihre Adresse gleich schnell angesprochen werden
kann. Man spricht deshalb auch vom wahlfreien Zugriffsspeicher, auf Englisch
Random Access Memory, abgekürzt RAM.

Nur kurz ein paar Worte über die Festplatte, den externen Speicher. Er
heißt deshalb extern, weil er nicht direkt zur Zentraleinheit gehört, und er hat
den unangenehmen Nachteil, dass man nicht so schnell auf die dort gespeicher-
ten Daten zugreifen kann wie auf die Daten im Arbeisspeicher. Aufgrund sei-
ner völlig anderen Speicherungstechnik ist die Festplatte aber kein flüchtiger
Speicher, sondern eben ein permanenter, was sie zur dauerhaften Speicherung
von Daten unverzichtbar werden lässt. Der Trick ist eigentlich ganz einfach
und funktioniert ähnlich wie bei einer alten Schallplatte. Auf eine kreisförmige
Scheibe aus stabilen Trägermaterial wird eine magnetisierbare Schicht aufge-
tragen, die dann in mehrere konzentrische Spuren aufgeteilt wird - mehrere
Kreise mit verschiedenen Radien, deren Mittelpunkt mit dem Mittelpunkt
der Scheibe übereinstimmt. Das ist übrigens ein großer Unterschied zu den
guten alten Schallplatten, denn da gab es nur eine Spur, die sich spiralförmig
über die gesamte Kreisfläche der Platte wand. Deshalb brauchte ja auch ein
Plattenspieler nur einen einzigen Tonabnehmer, denn sobald er einmal den
Anfang der Spur gefunden hatte, blieb ihm gar nichts anderes übrig, als sich
34 1 Grundlagen

bis zum Ende der Spur durchzuarbeiten, sofern nicht gerade die Hauskatze
auf den Plattenspieler springen musste.
Das ist bei der Festplatte etwas anders. Da beide Seiten der Platte als
Datenträger verwendet werden, braucht man natürlich auch zwei Schreib-
/Leseköpfe, einen für oben, einen für unten. Die können aber nicht einfach
so wie bei einer Schallplatte vorne anfangen und dann durch die ganze Platte
marschieren, das würde viel zu lang dauern und würde der Tatsache nicht
gerecht, dass wir es mit verschiedenen Spuren zu tun haben. Der Schreib-
/Lesekopf muss also erst einmal zur richtigen Spur gebracht werden, und dann
muss man auf der Spur auch noch die richtige Stelle finden. Damit die Adres-
sierung etwas übersichtlicher wird, sind die Spuren noch in einzelne Sektoren
aufgeteilt, und wenn auf der Festplatte eine bestimmte Datei gesucht wird,
dann wird der Schreib-/Lesekopf zur passenden Spur transportiert und dann
die Platte so gedreht, dass der Sektor, in dem die gesuchte Datei beginnt,
sich unter dem Schreib-/Lesekopf befindet. Sie sehen also: die Festplatte heißt
nicht deshalb Festplatte, weil sie fest und unbeweglich ist, das ist sie ganz
und gar nicht, denn zur Suche nach dem passenden Sektor muss sie rotieren
können. Sie wird auch nicht nur zu festlichen Gelegenheiten aus dem Mate-
rialschrank gekramt, der einzige Grund für ihren Namen ist der Umstand,
dass sie ihre Daten fest und stromunabhängig speichern kann. Ich würde mich
allerdings nicht der Illusion hingeben, eine Festplatte sei ein absolut siche-
res Speichermedium für alle Zeiten. Sie ist zwar in aller Regel in einem gut
geschützten Gehäuse untergebracht, weshalb Schäden wie etwa katzenbeding-
te Kratzspuren eher unwahrscheinlich sind, aber sie heißt auch nicht ohne
Grund Magnetplatte. Ob an einer bestimmten Speicherstelle eine Eins oder
eine Null steht, hängt davon ab, ob diese Stelle magnetisiert ist oder nicht,
und wenn Sie beispielsweise einen starken Magneten an Ihrem Rechenr vorbei
tragen und damit die Magnetisierung der Festplatte ändern, dann sind Ihre
Daten genauso hinüber wie bei einem Arbeitsspeicher ohne Strom.

Abb. 1.9. Festplatte mit Spuren und Sektoreneinteilung

Vermutlich haben Sie schon die Gründe für den leichten Nachteil der Fest-
platte gegenüber dem Arbeitsspeicher bemerkt. Um Datenelemente aus dem
1.3 Rechneraufbau 35

Arbeitsspeicher zu holen, muss der Computer nicht die geringste Bewegung


durchführen, alles beruht auf elektrischem Strom. Dagegen kommen bei ei-
ner Festplatte in jedem Fall mechanische Elemente zum Tragen, der Schreib-
/Lesekopf muss bewegt werden und die Platte wird zum Einstellen des passen-
den Sektors ein wenig rotieren. Solche echten physischen Bewegungen dauern
ihre Zeit, aber keine Angst, es hält sich in Grenzen. Die Zugriffszeit auf eine
moderne Festplatte liegt im Bereich weniger Millisekunden, und es ist zu er-
warten, dass im Lauf der nächsten Jahre die Zeiten genauso wie übrigens die
Preise noch fallen werden.

Festplatten Eine Festplatte ist ein magnetischer Datenspeicher, der in


Spuren und Sektoren eingeteilt ist. Um eine gesuchte Datei auf der Festplatte
aufzufinden, sind sowohl Bewegungen eines Schreib-/Lesekopfes als auch eine
Rotation der Platte selbst nötig; auf Grund dieser mechanischen Bewegun-
gen sind die Zugriffszeiten höher als beim vollelektronischen Arbeitsspeicher.
Festplatten eignen sich zur permanenten Speicherung großer Datenmengen.

Ich will Sie nicht mehr langweilen als unbedingt nötig, und deshalb werde
ich jetzt nicht mehr lang und breit über Disketten und ihre Laufwerke reden.
Im Prinzip ist eine Diskette nichts anderes als eine abgespeckte Festplatte, die
aber, wie Sie wissen, nicht fest im Rechner verankert ist, sondern nur von Fall
zu Fall als Speichermedium verwendet wird, vor allem zur Datensicherung und
um Daten von einem Rechner zum anderen zu transportieren. Beide Zwecke
sind aber nicht mehr so aktuell wie früher, was nicht zuletzt auf die gerin-
ge Speicherkapazität der üblichen Disketten zurückzuführen ist: mit nur 1, 44
Megabyte locken Sie heute keinen Hund mehr hinter dem Ofen hervor, diese
Kapazität reicht nicht aus, um mit der heute üblichen Dateigröße mitzuhal-
ten. Für so etwas wird heute gerne das optische Speichermedium Compact
Disc, abgekürzt CD, oder seit neuester Zeit die DVD benützt. Ihre Speicher-
kapazität ist ungleich größer, und in absehbarer Zeit wird wohl kaum noch
jemand die klassischen Disketten verwenden.
Lassen Sie mich kurz den Inhalt dieses Abschnitts rekapitulieren: Sie wis-
sen jetzt im Groben, wie ein heutiger von Neumann-Computer aufgebaut ist
und haben einen Eindruck von den für den Arbeitsspeicher und den Festspei-
cher verwendeten Methoden. Was ich bisher noch überhaupt nicht angesehen
habe, ist der Teil der Zentraleinheit, in dem die eigentliche Rechenarbeit ge-
schieht, nämlich das Rechenwerk. Das hat seine Gründe. Die Arbeit im Re-
chenwerk geschieht auf binärer Basis, es wird nur mit Zahlen zur Basis 2
gerechnet, und das sind nicht gerade die Zahlen, mit denen Sie täglich zu tun
haben. Um zu verstehen, was im Rechenwerk vor sich geht, kann ich Ihnen
also die Mühe nicht ersparen, sich im nächsten Abschnitt verstärkt mit den
Rechenmethoden des Zweiersystems zu beschäftigen, vornehm formuliert: mit
der binären Arithmetik. Auf der Basis dieser Arithmetik kann ich Ihnen dann
im fünften Abschnitt zeigen, mit welchen konkreten Schaltungen ein Compu-
ter rechnen kann.
36 1 Grundlagen

Übungen

1.7. Wie Sie gesehen haben, besitzt ein heutiger Computer sowohl einen Ar-
beitsspeicher als auch einen Festspeicher.
(a) Untersuchen Sie, ob man einen modernen Computer auch ohne einen voll-
elektronischen Arbeitsspeicher konstruieren kann. Gehen Sie dabei auf die
grundsätzliche Möglichkeit und auf das Problem der Arbeitsgeschwindig-
keit eines solchen Rechners ein.
(b) Gehen Sie den Fragen aus (a) nach unter der Voraussetzung, dass zwar ein
vollelektronischer Arbeitsspeicher, dafür aber keine Festplatte vorhanden
ist.

1.8. Diskutieren Sie, warum man in der Zentraleinheit einerseits Speicherzel-


len zur puren Aufnahme von Daten und andererseits ein Rechenwerk zum
Rechnen mit diesen Daten vorsieht. Warum hat man nicht gleich die Spei-
cherzellen des Arbeitsspeichers mit Rechenfähigkeiten versehen und sich ein
separates Rechenwerk gespart?

1.9. Berechnen Sie die Kapazität des Arbeitsspeichers und des Festplatten-
speichers meines alten PC.

1.10. Gegeben sei ein Speicher mit einer Kapazität von vier Megabyte. Exakt
um 12 Uhr wird in diesen Speicher ein Zeichen (also ein Byte) eingelesen,
um 12.01 Uhr vier weitere Zeichen, um 12.02 Uhr 16 weitere Zeichen, um
12.03 Uhr 64 weitere Zeichen usw. Zu welchem Zeitpunkt kann keine weitere
Zeichenreihe eingelesen werden und wieviel Speicherplatz ist dann belegt?

1.11. Eine Festplatte rotiert mit 7200 Umdrehungen pro Minute und hat einen
Durchmesser von 3, 5 Zoll, wobei ein Zoll 2, 54 Zentimetern entspricht. Mit
welcher Geschwindigkeit, gemessen in Metern pro Sekunde, bewegt sich der
äußerste Rand der Platte?

1.4 Binäre Arithmetik

Über die Aufgabe des Computers, Daten zu verarbeiten, haben wir uns schon
mehrfach geeinigt, und Sie haben auch schon etwas darüber gelernt, wie er das
macht. Was soll man aber eigentlich unter Daten verstehen? Da gehen die Mei-
nungen etwas auseinander. Man kann natürlich sagen, dass jede in irgendeiner
Form aufgeschriebene Zeichenfolge schon so etwas wie Daten darstellt, aber
das ist wenig befriedigend und erinnert an die Auffassung mancher privaten
Fernsehsender, alles was auf dem Bildschirm herumzappelt, sei unterhaltend.
Irgendein Sinn sollte mit Daten verbunden sein, sonst wäre der Versuch, sie
zu verarbeiten, völlig sinnlos und die Datenverarbeitung wäre ein Spiel mit
inhaltsleeren Zeichenketten.
1.4 Binäre Arithmetik 37

1.4.1 Daten
Ich will daher davon ausgehen, dass Daten einen Bezug zu irgendeinem Pro-
blem haben, an dessen Lösung man interessiert ist. Genau wie es sich später
bei den Algorithmen herausstellen wird, sind Daten Hilfsmittel, um ein Pro-
blem zu lösen, und nicht sinnlos hingekritzelte Zeichenketten.

Daten
Unter Daten versteht man Angaben zu Personen, Sachen oder Sachverhalten,
die zur Lösung eines Problems zweckdienlich sein können.

Niemand kann die Zweckdienlichkeit eines bestimmten Datensatzes für ein


bestimmtes Problem garantieren, aber man sollte mit Daten wenigstens die
Hoffnung verbinden, dass ihre Verwendung zur Lösung des einen oder anderen
Problems sinnvoll sein kann.
Je nach Betrachtungsweise können Sie nun die Daten auf verschiedene Wei-
se unterteilen. Eine beliebte Einteilung ist die nach dem Grad der Veränder-
lichkeit. Daten über die Angestellten in einer Firma wie beispielsweise Name,
Geburtsdatum oder Kinderzahl werden sich nicht alle fünf Minuten verändern,
sie bleiben über lange Zeiträume konstant und werden deshalb Stammdaten
genannt. Umgekehrt hat Ihr Mitarbeiter aber auch Daten, die sich sehr wohl
jeden Tag verändern können wie zum Beispiel die tägliche Arbeitszeit oder
der Stundenlohn bei Akkordentlohnung. Da diese Daten ständig in Bewegung
sind, spricht man hier gerne von Bewegungsdaten - der große Jedi-Ritter Mei-
ster Yoda würde sie wahrscheinlich für sehr zukunftsträchtig halten, da nach
seiner Meinung auch die Zukunft in ständiger Bewegung ist.
Das ist aber nicht die einzige Einteilungsmöglichkeit. Sie können sich auch
dafür entscheiden, die Daten nach ihrer Stellung im Verarbeitungsprozess ein-
zuteilen in Eingabedaten und Ausgabedaten, oder sie nach der Art ihrer Ver-
wendung zu klassifizieren. Eine Personalnummer ist sicher etwas anderes als
eine Gehaltsangabe, denn die Personalnummer dient der Sortierung und Iden-
tifizierung Ihrer Mitarbeiterdaten, während das Gehalt nichts weiter als eine
Mengenangabe ist, genauso wie die schon erwähnte Kinderzahl oder die An-
zahl der geleisteten Arbeitsstunden. Daten, die zur Sortierung und Identifi-
zierung dienen, nennt man Ordnungsdaten, und solche Daten, die schlichte
Mengenangaben liefern, heißen aus offensichtlichen Gründen Mengendaten.
Selbstverständlich sind auch noch andere Verwendungszwecke denkbar. Sie
hatten schon im letzten Abschnitt gesehen, dass die Daten irgendwo im Ar-
beitsspeicher abgelegt werden müssen, damit sie einer Verarbeitung zugänglich
werden, und die dabei verwendeten Speicherzellen haben bestimmte Adres-
sen. Auch das sind Daten, diese Adressen müssen schließlich verwaltet wer-
den, um an die abgelegten Datensätze wieder heranzukommen, und weil hier
die Abläufe im Rechner gesteuert werden, nennt man die zugehörigen Daten
Steuerdaten.
Hinaus will ich aber im Moment auf eine dritte Einteilungsmöglichkeit. Es
macht einen Unterschied, ob Sie nur numerische Daten verwenden, mit de-
38 1 Grundlagen

nen Sie anschließend rechnen, oder so genannte alphanumerische, die sich aus
Buchstaben und Zahlen zusammensetzen können und zum Rechnen offenbar
nur bedingt geeignet sind. Je nach der Art der Zeichen kan die anschließende
Verarbeitung ausgesprochen unterschiedlich sein. Haben Sie beispielsweise ein
Programm zur Lösung quadratischer Gleichungen geschrieben, dann wird es
irgendwann die Eingabe bestimmter Zahlenwerte erwarten, aus denen es dann
die Lösungen berechnet, und an dieser Stelle dürfen Sie keine Buchstaben und
schon gar keine Sonderzeichen eingeben, sonst bricht Ihr Programm zusam-
men. Die Art der verwendeten Zeichen richtet sich also stark nach der Art der
durchzuführenden Verarbeitung.

Einteilungsmöglichkeiten für Daten


Es gibt verschiedene Möglichkeiten, Daten einzuteilen, beispielsweise:
• nach der Veränderlichkeit,
• nach der Verwendung,
• nach der Stellung im Verabeitungsprozess,
• nach der Art der verwendeten Zeichen.

In diesem Abschnitt geht es mir vor allem um die Verarbeitungsmethode


Rechnen“. Es ist in den letzten Abschnitten immer wieder deutlich geworden,

dass sämtliche Verarbeitungen im Rechner auf binärer Basis vor sich gehen,
mit Nullen und Einsen, und das spielt vor allem im Rechenwerk eine große
Rolle. Die dort anzutreffenden Schaltungen beruhen auf dem Rechnen mit
binären Zahlen, weshalb ich Ihnen in diesem Abschnitt zeigen will, wie die
binäre Arithmetik funktioniert.

1.4.2 Binärzahlen

Ich habe schon mehrfach die Tatsache angesprochen, dass ein Computer ir-
gendwie mit Einsen und Nullen rechnet. Das klingt für normale Menschen
zunächst etwas seltsam: wie soll man denn mit Einsen und Nullen rechnen
können, da kommt man doch nicht weit. Stimmt aber nicht. Auch das ver-
traute Deziamlsystem hat nur zehn Ziffern, und trotzdem kann man damit
weiter als bis zehn zählen, warum sollte etwas Ähnliches dann nicht auch mit
zwei Ziffern funktionieren?
Versetzen Sie sich einmal in die Lage eines Computers. Von außen ak-
zeptiert er selbstverständlich die üblichen dezimalen Zahlen in der üblichen
Darstellung, und er gibt auch seiner Ergebnisse wieder in dieser Form aus,
aber seine Verarbeitungen macht er völlig anders. Die internen Schaltungen
eines Rechners beruhen auf dem Fließen von Strom, und da gibt es nur zwei
Grundzustände: Strom fließt oder er fließt nicht. Beim Aufbau des Zahlen-
systems habe ich dagegen zehn Grundzustände, nämlich die Ziffern von Null
bis Neun, aus denen sich alles andere zusammensetzt. So viele Möglichkeiten
1.4 Binäre Arithmetik 39

besitzt der Computer nicht, er muss seine gesamte Arithmetik, all seine Rech-
nungen, zusammensetzen aus den erwähnten zwei Grundzuständen. In Ziffern
übersetzt heißt das: ihm stehen keine neun Ziffern zur Verfügung, sondern nur
zwei, und damit muss er auskommen.
Sehen wir uns also an, wie man die natürlichen Zahlen computerfreundlich
darstellen kann. Die Darstellung, nach der ich suche, beruht auf nur zwei Zif-
fern, und deshalb spricht man oft vom Zweiersystem oder auch Binärsystem;
andere bevorzugen den Namen Dualsystem. Es spielt aber keine Rolle, wie Sie
es nennen. Wichtig ist nur dass es darum geht, sämtliche natürlichen Zahlen
mit Hilfe von nur zwei Ziffern darzustellen. Dabei ist es sinnvoll, sich erst
einmal zu überlegen, wie eigentlich das vertraute Dezimalsystem aufgebaut
ist.
Die Bedeutung der Dezimalzahl 13271 dürfte Ihnen vertraut sein. Offenbar
hat die 1 ganz vorne einen völlig anderen Wert als die 1 ganz hinten, denn sie
steht in Wahrheit für 10000, während die hintere 1 einfach nur für sich selbst
steht. Die Wertigkeit einer Ziffer hängt also davon ab, an welcher Stelle der
Zahl sie steht, und je weiter man nach links kommt, desto höher steigt man
in der Zehnerpotenz. Im Falle meiner Beispielzahl gilt:

13271 = 1 · 104 + 3 · 103 + 2 · 102 + 7 · 101 + 1 · 100 .

Nach diesem Prinzip kann ich jetzt genau aufschreiben, was man unter einer
Dezimalzahl versteht: es ist eine Folge von endlich vielen Ziffern zwischen
0 und 9, und der tatsächliche Zahlenwert jeder Ziffer hängt davon ab, an
welcher Stelle der Zahl die Ziffer steht. Je weiter links, desto höher ist die
Zehnerpotenz, mit der man die Ziffer multiplizieren muss.

Dezimalzahlen
Eine (n + 1)-stellige Dezimalzahl ist eine Folge von n + 1 Ziffern

an an−1 . . . a1 a0 ,

die alle zwischen 0 und 9 liegen. Der dezimale Wert dieser Zahl beträgt

an · 10n + an−1 · 10n−1 + · · · + a1 · 10 + a0 .

Zwei Worte dazu. In dieser Definition steht nur, dass sich eine Dezimalzahl
aus den Ziffern 0 bis 9 zusammensetzt, und dass man die wirkliche Bedeutung
jeder Ziffer nur sehen kann, wenn man darauf achtet, an welcher Stelle der
Zahl sie steht. Natürlich klingt das verbal etwas gefälliger als formelmäßig,
aber in der Informatik muss man nun mal ab und zu mit Formeln umgehen.
Vielleicht verwirrt Sie übrigens der Umstand, dass die Zahl (n + 1)-stellig
ist. Das liegt einfach nur daran, dass ich bei der Nummer 0 mit dem Zählen
anfange und bei n wieder aufhöre. Im Falle meiner Beispielzahl 13271 habe ich
offenbar fünf Stellen, also ist n + 1 = 5 und damit n = 4. Die Ziffern lauten
40 1 Grundlagen

dann a0 = 1, a1 = 7, a2 = 2, a3 = 3 und a4 = 1. Sie sehen, die allgemeine


Definition passt genau zum Beispiel. Wäre ja auch schlimm, wenns anders
wäre.
Das ist ja alles schon mal ganz schön, aber die Dezimalzahlen kennen Sie
schon ziemlich lange, und eigentlich wollte ich auf die Binärzahlen hinaus. Sie
sind aber genauso aufgebaut wie die Dezimalzahlen, nur eben mit zwei Ziffern
anstatt zehn. Eine typische Binärzahl lautet beispielsweise 101112 , wobei die
kleine Zwei bedeutet, dass es sich um eine Zahl zur Basis 2 handelt. Wäre das
eine Dezimalzahl, also eine Zahl zur Basis 10, dann hätte sie die Bedeutung
1 · 104 + 0 · 103 + 1 · 102 + 1 · 101 + 1 · 100 , wie wir es eben besprochen haben.
Es ist aber keine Dezimalzahl, sondern soll eine Binärzahl sein, und bei den
Binärzahlen spielt die Zahl 2 die gleiche Rolle wie bei den Dezimalzahlen
die 10. Ich muss daher nur die Basiszahl austauschen und aus der 10 eine 2
machen, um die eigentliche Bedeutung der Binärzahl 101112 vor mir zu sehen.
Ihr dezimaler Wert kann nur 1 · 24 + 0 · 23 + 1 · 22 + ·21 + 1 · 20 sein, denn ich
musste überall die Basis 10 gegen die Basis 2 austauschen. Rechnet man das
aus, so ergibt sich die Dezimalzahl 23, weshalb also die Binärzahl 101112 der
Dezimalzahl 23 entspricht.
Jetzt dürften Sie so weit sein, die Definition der Binärzahlen zu verstehen.
Sie sieht fast genauso aus wie Definition der Dezimalzahlen, nur dass ich auf
die richtigen Ziffern achten muss: bisher hatte ich 0 bis 9, jetzt habe ich nur
noch 0 und 1. Und auch beim dezimalen Wert wird sich etwas ändern.

Binärzahlen
Eine (n + 1)-stellige Binärzahl oder auch Dualzahl ist eine Folge von n + 1
Ziffern
an an−1 . . . a1 a0 ,
die alle entweder gleich 0 oder gleich 1 sind. Der dezimale Wert dieser Zahl
beträgt
an · 2n + an−1 · 2n−1 + · · · + a1 · 2 + a0 .

Werfen Sie einen Blick auf den dezimalen Wert einer solchen Binärzahl.
Bei den Dezimalzahlen musste ich jede Ziffer mit der passenden Zehnerpo-
tenz multiplizieren, um den richtigen Wert zu erhalten, weil ich nun einmal
über zehn Ziffern verfügte. Bei meinen Binärzahlen habe ich aber nur noch
zwei Ziffern, und deshalb bleibt mir nichts anderes übrig, als zu Zweierpo-
tenzen überzugehen. Da Sie vermutlich mit dieser seltsamen Art von Zahlen
noch selten etwas zu tun hatten, zeige ich Ihnen noch ein paar Beispiele. Um
Verwechslungen zu vermeiden, werde ich alle Binärzahlen mit dem Index 2
versehen, damit klar ist, dass es sich um Zahlen zur Basis zwei handelt.
Wie lautet nun der dezimale Wert von 1010102 ? Die Zahl besteht aus sechs
Ziffern, daher ist die höchste Potenz 25 . Damit ergibt sich:

1010102 = 1 · 25 + 0 · 24 + 1 · 23 + 0 · 22 + 1 · 21 + 0 · 20 = 32 + 8 + 2 = 42.
1.4 Binäre Arithmetik 41

1010102 ist also die binäre Darstellung der vertrauten Zahl 42, ist doch eigent-
lich ganz einfach. Immerhin musste ich hier noch die Stellen der Binärzahl
zählen, damit ich wusste, mit welcher Zweierpotenz ich anfangen muss. Et-
was einfacher hat man es, wenn man bei der Umrechnung von rechts nach
links vorgeht und nicht wie eben von links nach rechts. Sie wissen ja, dass
Sie bei 1 anfangen müssen und sich dann immer entlang der Zweierpotenzen
hochhangeln sollen. Das bedeutet zum Beispiel:

110111012 = 1 + 4 + 8 + 16 + 64 + 128 = 221,

denn ganz rechts finden Sie eine binäre 1, die einer vertrauten 1 entspricht.
Nach einer 0 kommt dann wieder eine 1, also gibt es keine Zweierstelle, sondern
erst wieder eine Viererstelle, der sich eine Achterstelle und eine Sechzehnerstel-
le anschließen. Die Zweiunddreißigerstelle wird dann wieder ausgelassen, und
ganz links versammeln sich eine Vierundsechzigerstelle und eine Einhundert-
achtundzwanzigerstelle. Auf die gleiche Weise können Sie dann zum Beispiel
10001012 = 1 + 4 + 64 = 69 rechnen.
Das wird ein recht langer Abschnitt, und Sie sollten unterwegs schon wis-
sen, an welchen Beispielen Sie üben können. Falls Sie schon ein wenig rechnen
wollen, wäre das der richtige Zeitpunkt für die Aufgabe 1.12.

1.4.3 Umrechnungsverfahren

Sie wissen nun also, was man unter einer Binärzahl versteht und wie man eine
Binärzahl in eine Dezimalzahl umrechnet. Daraus entstehen aber sofort zwei
Probleme. Der Computer soll schließlich mit allen natürlichen Zahlen rechnen
können, und das setzt voraus, dass es auch zu jeder natürlichen Zahl eine
binäre Darstellung gibt. Die Frage ist also: kann man jede natürliche Zahl
als Binärzahl darstellen? Und falls man das kann: wie findet man konkret die
passende Binärzahl? Es nützt dem Rechner schließlich gar nichts, wenn er
weiß, dass es eine binäre Darstellung gibt, aber keine Ahnung hat, wie er sie
finden soll. Beide Probleme will ich jetzt auf einmal lösen, indem ich Ihnen ein
Umrechnungsverfahren zeige, das Dezimalzahlen in Binärzahlen umrechnet.
Sehen wir uns das zunächst an einem Beispiel an.
Ich untersuche die Dezimalzahl m = 17. Binärzahlen orientieren sich an
Zweierpotenzen, also suche ich nach der größten Zweierpotenz, die noch in die
17 hineinpasst. Das ist offenbar 16 = 24 . Es bleibt also etwas von m übrig, das
die 16 übersteigt, nämlich genau 17−16 = 1. In die 1 passt natürlich weder die
nächste unter 16 liegende Zweierpotenz 8 noch die 4 noch die 2. Die Binärdar-
stellung hat daher keine Achter-, Vierer- oder Zweierstelle, sondern nur noch
die pure Einserstelle 1. Mit anderen Worten: ich habe herausgefunden, dass

17 = 16 + 1 = 1 · 24 + 0 · 23 + 0 · 22 + 0 · 21 + 1 · 20

gilt, und daraus folgt:


17 = 100012 .
42 1 Grundlagen

So kann man das immer machen. Am Anfang sucht man nach der größten
Zweierpotenz 2n , die die gegebene Zahl m nicht übersteigt. Dann zieht man
2n von m ab und sieht nach, was übrigbleibt. Dabei kann es sein, dass man
schon unter die nächste mögliche Zweierpotenz 2n−1 rutscht, und in diesem
Fall gehört an die entsprechende Stelle eine 0, ansonsten eine 1. Anschließend
muss ich eine Stelle tiefer gehen und testen, ob im Rest noch Platz ist für
2n−2 , und so hangle ich mich immer tiefer, bis ich schließlich bei 20 am Boden
angekommen bin. Wichtig dabei ist nur, dass man in jedem Schritt darauf
achtet, das bisher Erreichte von m abzuziehen und nachzusehen, ob die nächste
Zweierpotenz noch hineinpasst. Dieses Verfahren werde ich jetzt in ein präziser
beschriebenes Verfahren fassen.
Gegeben ist also eine Dezimalzahl m, die ich in ihre Binärdarstellung
an an−1 ...a1 a0 umrechnen will. Dazu suche ich die größte Zweierpotenz, die
m nicht übersteigt, das heißt:

2n ≤ m < 2n+1 .

Da dann m < 2n+1 gilt, ist 2n tatsächlich die größte Zweierpotenz, die in
m hineinpasst. Also setze ich an = 1. Anschließend berechne ich m − 2n .
Die vorher errechnete Zweierpotenz wird nun also von m abgezogen, damit
ich feststellen kann, welche Zweierpotenzen noch in den Rest passen. Falls
m − 2n ≥ 2n−1 ist, passt die nächstkleinere Zweierpotenz in den Rest. Also
gibt es eine 2n−1 erstelle, und ich setze an−1 = 1. Falls dagegen m − 2n < 2n−1
ist, passt die nächstkleinere Zweierpotenz eben nicht in den Rest. Also set-
ze ich an−1 = 0, denn es gibt keine besetzte 2n−1 erstelle. So oder so, die
2n−1 erstelle ist nun entweder mit einer 0 abgetan oder mit einer 1 gefüllt,
und ich kann mich der nächsten Stelle widmen, die zu 2n−2 gehört. Zu diesem
Zweck berechne ich m − 2n − an−1 · 2n−1 . Von dem alten Rest muss ich also
die neue Stelle abziehen, um den neuen Rest zu erhalten. Falls an−1 = 0 war,
muss ich hier natürlich gar nichts tun, und ansonsten muss ich genau die 2n−1
abziehen, die im alten Rest noch enthalten waren, damit ich mich jetzt um
die kleineren Zweierpotenzen kümmern kann. Ich gehe dabei nach dem glei-
chen Prinzip vor wie in Schritt 2. Falls m − 2n − an−1 2n−1 ≥ 2n−2 ist, passt
die nächstkleinere Zweierpotenz in den Rest. Also gibt es eine 2n−2 erstelle,
und ich setze an−2 = 1. Falls dagegen m − 2n − an−1 2n−1 < 2n−2 ist, passt
die nächstkleinere Zweierpotenz nicht in den Rest. Also setze ich an−2 = 0.
Wiederholen Sie das Verfahren, bis a0 berechnet ist, dann steht die Binärdar-
stellung der Dezimalzahl m vor Ihnen.

Umrechnung einer Dezimalzahl in eine Binärzahl


Das folgende Verfahren rechnet eine Dezimalzahl m in ihre Binärdarstellung
an an−1 ...a1 a0 um.
• Man suche die größte Zweierpotenz, die m nicht übersteigt, das heißt:

2n ≤ m < 2n+1 .
1.4 Binäre Arithmetik 43

Man setze an = 1.
• Man berechne m − 2n . Falls m − 2n ≥ 2n−1 ist, setze man an−1 = 1. Falls
m − 2n < 2n−1 ist, setze man an−1 = 0.
• Man berechne m − 2n − an−1 · 2n−1 . Falls m − 2n − an−1 2n−1 ≥ 2n−2
ist, setze man an−2 = 1. Falls m − 2n − an−1 2n−1 < 2n−2 ist, setze man
an−2 = 0.
• Man wiederhole das Verfahren, bis a0 berechnet ist.

Ich sage es noch einmal: dieses Verfahren ist nichts weiter als die präzisere
Beschreibung dessen, was ich vorhin schon gemacht habe. Man sieht einfach
nach, ob die nächstkleinere Zweierpotenz noch in das hineinpasst, was von
der Zahl noch übrig geblieben ist. Falls ja, bekommt die Binärdarstellung an
der passenden Stelle eine 1, falls nein gibt es eine 0. Ich weiß, auch das ist
ungewohnt und neu, und deshalb kann eine weitere Beispielrechnung nicht
schaden.
Ich will die Dezimalzahl m = 42 in eine Binärzahl umwandeln. Das hat
den Vorteil, dass ich die umgekehrte Richtung vorhin schon beschritten habe
und deshalb leicht kontrollieren kann, ob mein Ergebnis stimmt. Ich werde
dabei nach den eben beschriebenen Schritten vorgehen.
• Die größte Zweierpotenz, die m nicht übersteigt, ist 25 = 32, also ist n = 5
und a5 = 1. Das bedeutet, dass m eine Zweiunddreißigestelle hat.
• Nun muss ich sehen, was noch hineinpasst, wenn ich 32 abgezogen habe.
Es gilt: m − 25 = 42 − 32 = 10, und ich muss mich hier nur noch um den
Rest 10 kümmern. Die nächstkleinere Zweierpotenz lautet 24 = 16, und
wegen 10 < 16 existiert keine Sechzehnerstelle, das heißt: a4 = 0.
• Der neue Wert a4 · 24 muss jetzt von der 10 abgezogen werden, aber da
ich keine Sechzehnerstelle hatte, ist auch nichts zu tun. Folglich ist m −
25 − a4 24 = 10. Es geht jetzt aber um die Zweierpotenz 23 = 8, und die
liegt ganz sicher unterhalb von 10, passt also in das hinein, was von der
Zahl noch übriggeblieben ist. Daher gibt es eine Achterstelle, und ich muss
a3 = 1 setzen.
• Weiteres Subtrahieren ist angesagt. Von 10 ziehe ich die Zweierpotenz
23 = 8 ab, und das ergibt:

m − 25 − a4 24 − a3 23 = 2.

Ich glaube, ich kann ab jetzt auf die exakte Durchführung des Rituals
verzichten. Wenn der Rest genau 2 ist, dann gibt es natürlich keine Vie-
rerstelle, sondern nur noch eine Zweierstelle, und auch auf die Einserstelle
kann und muss ich verzichten, denn mit der Zweierstelle ist bereits alles
erledigt. Das bedeutet:

a2 = 0, a1 = 1, a0 = 0.
44 1 Grundlagen

Damit habe ich alle Ziffern der Binärdarstellung gefunden und muss sie nur
noch aufschreiben, angefangen mit a5 = 1 an der Spitze. Insgesamt ergibt sich
also:
42 = 1010102 ,
was auch mit meiner alten Rechnung übereinstimmt.
Diese Methode funktioniert immer. Sie werden mir allerdings zustimmen,
dass es beispielsweise kein reines Vergnügen ist, die größte Zweierpotenz zu
finden, die 1896268482366748 nicht übersteigt, und dass überhaupt das gan-
ze Verfahren doch ein wenig zur Schwerfälligkeit neigt. Und noch ein Grund
spricht gegen die Methode. Sie werden sich erinnern, dass Sie auf Ihrer Tasta-
tur Dezimalzahlen eingeben und der Computer dann mit Binärzahlen rechnet.
Er muss also irgendwie die eingegebenen Dezimalzahlen in Binärzahlen um-
rechnen, und genau dieses Problem beackere ich ja gerade. Aber kann er das
auf die Weise, die ich Ihnen vorgestellt habe? Er kann es nicht. Das beschrie-
bene Umrechnungsverfahren ist voll von dezimalen Rechnungen, andauernd
werden irgendwelche Dezimalzahlen von anderen Dezimalzahlen abgezogen,
werden wieder andere Dezimalzahlen miteinander verglichen, und das wird
Ihr Computer beim besten Willen nicht auf die Reihe bekommen. Um die-
ses Verfahren durchführen zu können, müssten Sie ihm erst einmal alle vor-
kommenden Zahlen in binärer Form zur Verfügung stellen, und das heißt,
Sie müssten die Umrechnung erledigen, bevor Sie die Umrechnung erledigen.
Klingt nicht gut und wäre auch nicht gut; wir müssen uns nach einem besseren
Umrechnungsverfahren umsehen.
Ich werde Ihnen jetzt eine deutlich einfachere Umrechnungmethode zeigen.
Das Prinzip sehen wir uns erst einmal am Beispiel der Dezimalzahl m =
247 an. Was passiert, wenn ich dieses m durch 10 teile und dabei den Rest
aufschreibe? Dann ist
247 : 10 = 24 Rest 7.
Das Divisionsergebnis 24 teile ich wieder mit Rest durch 10 und erhalte

24 : 10 = 2 Rest 4.

Wenn ich dann zum Schluss auch dieses Divisionsergebnis durch 10 teile, so
ergibt sich
2 : 10 = 0 Rest 2.
Was fällt Ihnen auf, wenn Sie die Divisionsreste von unten nach oben betrach-
ten? Sie ergeben nebeneinander geschrieben wieder genau die Zahl 247. Man
erhält also die Dezimalstellen einer Zahl, indem man immer wieder mit Rest
durch 10 teilt und anschließend die Reste in umgekehrter Reihenfolge auf-
schreibt. Und bei der Binärdarstellung ist das nicht anders, nur dass Sie hier
natürlich nicht durch 10 teilen, sondern durch die Basiszahl der Binärzahlen,
also durch 2.
Schön herausgefunden, aber bisher ist das nur eine Vermutung, die auf
einem Analogieschluss von Dezimalzahlen auf Binärzahlen beruht. Sehen wir
1.4 Binäre Arithmetik 45

uns erst einmal an einem Beispiel an, ob das auch funktioniert. Dazu betrachte
ich die Dezimalzahl m = 13. Im Folgenden schreibe ich auf, was passiert, wenn
man andauernd durch 2 teilt, die Reste aufschreibt und dann das jeweilige
Divisionsergebnis wieder durch 2 teilt. Dann gilt:
13 : 2 = 6 Rest 1
6 : 2 = 3 Rest 0
3 : 2 = 1 Rest 1
1 : 2 = 0 Rest 1.
Schreibt man nun die Reste in umgekehrter Reihenfolge als Binärzahl auf, so
ergibt sich 11012 = 8 + 4 + 1 = 13, also ist alles in bester Ordnung.
Das gibt Anlass zur Hoffnung, und wie stimmen vermutlich darin überein,
dass diese Methode doch etwas einfacher ist als die erste. Man muss nur oft
genug durch zwei teilen und die Divisionsreste richtig herum aufschreiben. Ich
formuliere das jetzt wieder als ein allgemeingültiges Verfahren.

Umrechnung einer Dezimalzahl in eine Binärzahl


Das folgende Verfahren rechnet eine Dezimalzahl m in ihre Binärdarstellung
an an−1 ...a1 a0 um.
• Man dividiere m durch 2 und notiere den Rest bei der Division.
• Man dividiere das Divisionsergebnis durch 2 und notiere wieder den Rest
bei dieser Division.
• Man wiederhole den letzten Schritt, bis als Divisionsergebnis der Wert 0
auftritt.
• Man schreibe die aufgetretenen Reste in der umgekehrten Reihenfolge ihres
Auftretens als Binärzahl.
Die ermittelte Binärzahl ist dann die binäre Darstellung von m.

Vielleicht sollte ich noch ein Wort über das Abbruchkriterium verlieren:
sobald das Divisionsergebnis eine Null liefert, soll ich aufhören. Was würde
geschehen, wenn ich diese Vorschrift ignoriere? Im nächsten Schritt würde ich
das alte Divisionsergebnis durch 2 teilen, und da ich vorher bereits eine Null
ermittelt habe, wäre das Ergebnis schlicht 0 Rest 0. Und auch alle weiteren
Schritte können mir nur immer mehr Nullen liefern. Die Reste muss ich dann
aber in der umgekehrten Reihenfolge ihres Auftretens notieren, und das heißt,
dass ich auf diese Weise nur eine Unmenge führender Nullen produziere, von
denen es in Politik und Wirtschaft schon genug gibt, und die meine Binärzahl
sicher nicht verbessern. Deshalb kann ich das Verfahren beenden, sobald das
Divisionsergebnis eine Null liefert.
Nach dem Notieren eines Verfahrens sollte man immer noch ein Beispiel
rechnen, damit man sich an das Verfahren gewöhnt. Zum Glück ist die Re-
chenmethode ziemlich übersichtlich, sodass ich hier auf erklärende Kommen-
tare verzichten kann. Ich werde jetzt also m = 157 in seine Binärdarstellung
umrechnen. Es gilt:
46 1 Grundlagen

157 : 2 = 78 Rest1
78 : 2 = 39 Rest0
39 : 2 = 19 Rest1
19 : 2 = 9 Rest1
9 : 2 = 4 Rest1
4 : 2 = 2 Rest0
2 : 2 = 1 Rest0
1 : 2 = 0 Rest1.
Folglich ist 157 = 100111012 .
Sie sollen hier aber nicht nur hübsche Rechenmethoden kennenlernen, son-
dern auch wissen, warum sie funktionieren. Es nützt Ihnen später schließlich
auch nichts, wenn Sie in einer Firma wissen, mit welchem Programm man
die Datensätze sortiert und welche Arbeitsschritte dabei durchgeführt werden
müssen, aber nichts darüber wissen, nach welchem Prinzip Ihre Programme
funktionieren: dann wären Sie beim ersten Problem verloren, weil Sie nicht
wüssten, wo Sie anfangen sollen, nach der Problemlösung zu suchen.
Zum Glück ist die Begründung für das Divisionsverfahren leicht einzuse-
hen. Aus dem ersten Verfahren wissen wir, dass man m auf jeden Fall als
Binärzahl schreiben kann, also als Summe von Zweierpotenzen

m = an · 2n + an−1 · 2n−1 + · · · + a1 · 2 + a0 ,

wobei an an−1 ...a1 a0 die Binärdarstellung von m ist. Bei der ersten Division
durch 2 erhält man also:

m : 2 = (an · 2n + an−1 · 2n−1 + · · · + a1 · 2 + a0 ) : 2


= an · 2n−1 + an−1 · 2n−2 + · · · + a1 Rest a0 ,

denn jeden einzelnen Summanden kann ich auf Grund der vorhandenen Zwei-
erpotenzen durch 2 dividieren, indem ich einfach den Exponenten um 1 her-
untersetze - nur der Summand a0 hat keine Zweierpotenz bei sich, bei der
man irgendetwas heruntersetzen könnte, und deshalb ist er der Rest bei der
Division.
Bei der zweiten Division muss ich nun das Divisionsergebnis durch 2 teilen.
Das liefert:

(an · 2n−1 + an−1 · 2n−2 + · · · + a1 ) : 2


= an · 2n−2 + an−1 · 2n−3 + · · · + a2 Rest a1

mit dem gleichen Argument wie eben. Die erste Division hat mir als Rest a0
geliefert, also die niedrigste Binärstelle von m. Die zweite Division hat dann
als Rest zu a1 geführt, also zur nächsten Binärstelle von m. Setzt man nun
das Verfahren fort, so erhält man natürlich durch andauernes Dividieren als
Reste alle weiteren Binärstellen, bis man zum Schluss bei an angekommen
ist. Daher bekomme ich die gesamte Binärdarstellung von m, indem ich alle
1.4 Binäre Arithmetik 47

aufgetretenen Reste in der umgekehrten Reihenfolge ihres Auftretens von links


nach rechts aufschreibe.
Ein schönes Verfahren, ausgesprochen einfach durchzuführen, so viel ist
wahr. Erinnern Sie sich aber an das Problem, das ich oben angesprochen hat-
te: kann der Computer die dringend notwendige Umrechnung von dezimalen
in binäre Zahlen auf diese übersichtliche Art durchführen? Wieder eine der
Enttäuschungen, die das Leben gelegentlich bereit hält; natürlich kann er es
nicht, denn es wird ja eine Dezimalzahl nach der anderen durch 2 geteilt.
Da der Rechner nur mit Binärzahlen rechnen kann, müsste er auch hier die
Umrechnung schon vorgenommen haben, bevor er sie vornehmen kann. Die
bisher besprochenen Umrechnungsverfahren sind sehr praktisch beim Umrech-
nen von Hand, dem Computer nützen sie gar nichts.
Jetzt können Sie die Aufgabe 1.13 lösen.

1.4.4 Addition

Der letzte Abschnitt endete etwas trostlos, aber das ist kein Grund aufzuge-
ben. Ein Umrechnungsverfahren, das auch der Computer versteht, wird sich
aus den Grundrechenarten ergeben, über die ich hier sprechen will. Es reicht
nämlich nicht, Dezimalzahlen in Binärzahlen umrechnen zu können, das Re-
chenwerk des Computers soll schließlich auch in der Lage sein, konkrete Rech-
nungen durchzuführen, sonst haben alle unsere Bemühungen keinen Sinn.
Fangen wir mit der Addition an. Sie ist extrem einfach, wenn es darum
geht, nur die binären Ziffern zu addieren, denn davon gibt es nur zwei. Indem
Sie die entsprechenden dezimalen Rechnungen als Vergleich heran ziehen, fin-
den Sie:

02 + 02 = 02 , 02 + 12 = 12 , 12 + 02 = 12 , 12 + 12 = 102 .

Das letzte Ergebnis gibt auch schon einen Hinweis auf Additionen von mehr-
stelligen binären Zahlen, denn man kann hier eine Analogie zum Addieren
von dezimalen Zahlen feststellen: die Summe von 12 und 12 lässt sich nicht
mehr mit Hilfe einer einzigen Ziffer ausdrücken, und daher geht man zu zwei-
stelligen Zahlen über, wie man das im dezimalen Bereich zum Beispiel bei
5 + 5 auch machen würde. So etwas nennt man einen Übertrag in die nächste
Stelle, wobei hier der Übertrag schon bei der Addition von zwei Einsen ent-
steht. Überraschend ist das aber eigentlich nicnt; ein Übertrag entsteht dann,
wenn beim Zusammenzählen die Basiszahl erreicht ist, und das ist bei binären
Zahlen nun mal die 2. Sobald ich also die Zahl 2 aufschreiben müsste, bin ich
gezwungen, zu einem Übertrag Zuflucht zu nehmen, weil in meinem binären
System die Ziffer 2 nicht existiert.
Das klingt alles ganz ähnlich wie bei den Dezimalzahlen, nur eben mit der
Basis 2 statt mit der Basis 10. Deshalb funktioniert das schriftliche Addieren
auch ganz genauso wie im Falle von Dezimalzahlen: Man schreibt die binären
Zahlen untereinander und addiert dann der Reihe nach von rechts nach links,
48 1 Grundlagen

wobei man Überträge jeweils in die nächste Stelle mitnimmt. Das Beste wird
sein, ich zeige Ihnen das an Beispielen. Zunächst möchte ich die beiden binären
Zahlen 100012 und 10112 addieren. Dazu schreibe ich sie rechtsbündig unter-
einander und fange von rechts nach links mit dem Addieren an.
100 01
+ 1 01111

111 00
Sie sehen, was passiert. In der Einserstelle ist 12 + 12 = 102 , also erhält das
Ergebnis in der Einserstelle eine 0, und den Übertrag 1 schleppe ich mit in
die Zweierstelle. Nun habe ich in der Zweierstelle die Addition 12 + 12 + 02 ,
denn den Übertrag muss man mitaddieren, und das ergibt wieder 102 . Somit
schreibe ich in die Zweierstelle des Ergebnisses wieder eine 0 und belaste die
Viererstelle mit einem neuen Übertrag 1. So langsam bekommen wir Routine.
In der Viererstelle ergibt sich die Addition 12 + 02 + 02 , was genau 12 ergibt
und mir für die Achterstelle jeden Übertrag erspart. Sowohl in der Achter-
als auch in der Sechzehnerstelle ist dann nur eine binäre 12 auf eine binäre
02 zu addieren mit dem jeweils gleichen Ergebnis 12 . Insgesamt komme ich
auf das Ergebnis 111002 , und aus Sicherheitsgründen sollte ich nachsehen,
ob das überhaupt stimmt. Das ist aber kein Problem. Aus 100012 = 17 und
10112 = 11 folgt sofort 100012 + 10112 = 17 + 11 = 28 = 111002 , denn
111002 = 16 + 8 + 4 = 28. Es passt also alles zusammen.
Ein Beispiel könnte reiner Zufall sein, und außerdem kann ein wenig Übung
in einem neuen Verfahren nicht schaden. Ich addiere jetzt also die beiden
binären Zahlen 1110112 und 111012 . Nach dem eben besprochenen Schema
ergibt das die folgende Rechnung.
11 1 0 11
+ 1111111011

101 1 0 00
Hier geschieht auch nichts anderes als vorher. Bei der Addition der Einser-,
Zweier- und Viererstellen erhalte ich jeweils das Ergebnis 102 , also eine 0 in
der Ergebniszahl und eine 1 im Übertrag. Bei der Achterstelle müssen Sie ein
wenig aufpassen, denn hier tritt die Addition 12 + 12 + 12 auf, die natürlich
zu dem Resultat 112 führt, also zu einer 1 in der Ergebniszahl und einer
1 im Übertrag. Das gleiche passiert in der Sechzehnerstelle, während dann
in der Zweiundreißigerstelle die Addition 12 + 12 Sie zu dem Ergebnis 102
bringt. Noch ein kurzer Blick auf die Kotrollrechnung: es gilt 1110112 = 59
und 111012 = 29, also 1110112 + 111012 = 59 + 29 = 88 = 10110002 , denn
10110002 = 64 + 16 + 8 = 88. Es ist nichts schief gegangen.
Und dabei geht auch nie etwas schief, das ist genau die Methode, mit der
man binäre Zahlen addiert und die auch der Computer bei seinen Rechenope-
rationen anwendet. Das dürfte schon eine allgemeine Regel wert sein.
1.4 Binäre Arithmetik 49

Addition binärer Zahlen


Man addiert binäre Zahlen, indem man sie untereinander schreibt, von rechts
nach links stellenweise addiert, die Einerziffer der Addition in die Ergebnis-
zahl schreibt und eventuell auftretende Überträge in die jeweils nächste Stelle
übernimmt.

Kürzer gesagt: man addiert binär genauso wie dezimal, nur eben zur Basis
2.

1.4.5 Subtraktion und Zweierkomplement

Auch die binäre Subtraktion bietet auf den ersten Blick keine Überraschun-
gen, allerdings wird es hier zu einem zweiten Blick kommen. Zunächst einmal
kann man sich auf den Standpunkt stellen, dass auch hier das von den Dezi-
malzahlen gewohnte Verfahren analog zur Addition verwendet werden kann,
und das funktioniert auch problemlos. Ich zeige Ihnen das an einem kleinen
Beispiel, indem ich 110012 − 10102 berechne. Wie üblich schreibt man die
beiden Zahlen untereinander und rechnet von rechts nach links.
11 0 01
− 111011 0

1 1 11

Sie müssen dabei immer nur darauf achten, alles ganz genauso zu machen
wie bei dezimalen Zahlen - nur mit dem Unterschied, dass die Basis hier 2
lautet und nicht 10. In der Einserstelle wollen sie von 0 auf 1 zählen, was
keinerlei Schwierigkeiten macht und zum Resultat 1 führt. In der Zweierstelle
sieht es schon schlechter aus, Sie müssen von 1 auf 0 zählen, und das geht
natürlich nicht. Die 0 wird daher als 102 interpretiert, die 1, die Ihnen zu
dieser 102 oben fehlt, wird als Übertrag unten in die Viererstelle geschrieben,
und in der Ergebiszahl haben Sie eine 1, da 12 + 12 = 102 gilt; so hätten sie
das bei vergleichbaren Verhältnissen im Dezimalsystem auch gemacht. Diese
Situation wiederholt sich auf Grund des Übertrages in der Viererspalte: das
Zählen von der 1 auf die 0 liefert Ihnen einen Übertrag in die Achterstelle und
in der Ergebniszahl wieder eine 1. Jetzt sehe ich mir die Achterstelle an, und
da sieht die Sache anders aus. Durch den Übertrag, den mir die Viererstelle
eingebrockt hat, steht jetzt bei der unteren Zahl 12 + 12 = 102 , und von
dieser 102 aus muss ich hochzählen auf 12 . Das geht nur dann, wenn diese
binäre 12 eigentlich eine 112 ist und ich mir wieder einen Übertrag für die
Sechzehnerstelle aufhalse. Hier habe ich dann nur noch von der 1 auf die 1
zu zählen, was ich mir genausogut sparen kann. Der übliche Test sollte auch
beim Subtrahieren nicht fehlen. Es gilt 110012 = 25 und 10102 = 10, also
110012 − 10102 = 25 − 10 = 15, und natürlich gilt 11112 = 8 + 4 + 2 + 1 = 15.
Wieder ist alles gut gegangen.
50 1 Grundlagen

Es ist nicht einzusehen, warum ich beim Subtrahieren gutgläubiger sein


soll als ich es beim Addieren war; ein zweites Beispiel sollte ich schon noch
anführen. Berechnen wir also 1010012 − 111112 .

1 0 1 0 01
− 1 1111111 1

1 0 10

Es gibt nichts Neues unter der Sonne, schon gar nicht beim Subtrahieren.
In der Einserstelle pasiert gar nichts, denn das Hochzählen von der 1 zur 1
liefert für die Ergebniszahl eine 0. Auch die Zweierstelle verhält sich nicht
ungewöhnlich, ich zähle von 1 auf 0 hoch und erhalte um den Preis eines
Übertrags die 1 für die Ergebniszahl. Immerhin etwas neuer ist die Lage bei der
Viererstelle. Wegen des Übertrags habe ich in der unteren Zahl jetzt 12 + 12 =
102 , was ich auf eine 0 hochzählen muss - das liefert natürlich wieder den
Übertrag 1 für die Achterstelle und die 0 für die Ergebniszahl. Der Rest der
Rechnung besteht nur noch aus Altvertrautem, weshalb ich nicht mehr näher
draus eingehe. Und auch der Test kann mich nicht mehr wirklich aus der Ruhe
bringen; es gilt 1010012 = 41 und 111112 = 31, also ist 1010012 − 111112 =
41 − 31 = 10 = 10102 , denn 10102 = 8 + 2 = 10.
Warum hätte es auch nicht funktionieren sollen? Schon bei der Addition
haben die Binärzahlen die Analogie zu den Dezimalzahlen gut vertragen, und
bei der Subtraktion ist es nicht anders.

Subtraktion binärer Zahlen


Man subtrahiert eine kleinere Binärzahl von einer größeren, indem man die
kleinere unter die größere schreibt, von rechts nach links stellenweise subtra-
hiert und eventuell auftretende Überträge in die jeweils nächste Stelle über-
nimmt.

Auch das ist wie bei den Dezimalzahlen und von daher nichts Besonderes.
Ich hatte aber schon angekündigt, dass es einen zweiten Blick auf die Subtrak-
tion geben würde, nachdem der erste Blick sich als etwas überraschungsarm
herausgestellt hat. Diese Art des Subtrahierens hat nämlich zwei Probleme.
Beim manuellen Rechnen neigt man dazu, sich mit den Überträgen zu verhed-
dern, die den meisten Leuten erfahrungsgemäß bei der Addition leichter fallen
als bei der Subtraktion. Das wäre noch nicht weiter schlimm, denn die ganze
binäre Rechnerei soll ja zeigen, wie ein Computer intern rechnet, und der wird
sich schon nicht verheddern. Stimmt. Aber denken Sie daran, dass das Rech-
nen im Computer mit Hilfe von elektronischen Schaltelementen durchgeführt
wird, die man erst einmal bauen muss, und je weniger verschiedene Schaltele-
mente man sich überlegen muss, desto besser. Für die Addition braucht man
auf jeden Fall ein Schaltelement, das bleibt nicht aus. Es wäre aber günstig,
wenn man auch mit dieser einen Art von Schaltelementen auskäme und sich
nicht noch eine neues für die Subtraktion ausdenken müsste. Ich will jetzt also
1.4 Binäre Arithmetik 51

daran gehen, eine Subtraktion durchzuführen, ohne wirklich subtrahieren zu


müssen, nur durch Additionen. Am Beispiel einer dezimalen Rechnung zeige
ich Ihnen, wie man Überträge vermeiden kann, und im binären Fall wird sich
sogar der Vorgang des Subtrahierens selbst verflüchtigen und nur Additionen
zurücklassen. Zunächst einmal das dezimale Beispiel, es geht um 3784 − 1717.
Das zugehörige Rechenschema finden Sie hier, anschließend erkläre ich, was
beim Rechnen eigentlich passiert ist.

9999
− 1717

8282
+ 3784

12066
+ 1

12067
−10000

2067

Erinnern Sie sich daran, dass ich ohne Überträge abziehen wollte? Nun handelt
es sich hier um vierstellige Zahlen, und die einzige vierstellige Dezimalzahl,
von der ich mit Sicherheit jede vierstellige Dezimalzahl ohne jeden Übertrag
subtrahieren kann, ist die 9999. Also rechne ich erst einmal 9999 − 1717. Das
ist natürlich die falsche Subtraktion, also muss ich das Ergebnis irgendwie
korrigieren. Addiert man im nächsten Schritt die gewünschten 3784 und an-
schließend noch eine 1 dazu, so hat man insgesamt die Rechnung

9999 − 1717 + 3784 + 1 = 10000 + 3784 − 1717

durchgeführt. Diese Zahl ist also genau um 10000 größer als die gesuchte,
weshalb ich dann auch am Ende noch diese überschüssigen 10000 abziehe und
damit das gesuchte Ergebnis 2067 erhalte. Hier kommen zwar zwei Subtrak-
tionen vor, aber jede ist harmlos. Die erste erfolgt ohne den leisesten Hauch
eines Übertrags, weil von 9999 subtrahiert wird. Und die zweite kann man
durch das Streichen der führenden Ziffer 1 erledigen, denn Sie müssen hier
nur die Zahl 10000 von einer fünfstelligen Dezimalzahl abziehen. Auf diese
Weise konnte ich subtrahieren, ohne wirklich subtrahieren zu müssen.
Sie sehen, das Prinzip ist ganz einfach: man subtrahiert von der größtmögli-
chen Zahl mit der gleichen Anzahl von Ziffern, um lästige Überträge zu ver-
meiden, und gleicht den dabei verursachten Fehler später durch Addieren und
Streichen einer Eins wieder aus. Wie ich Ihnen gleich zeigen werde, funk-
tioniert das gleiche Prinzip auch bei Binärzahlen mit dem zusätzlichen Vor-
teil, dass man die erste Subtraktion eigentlich überhaupt nicht vornehmen
52 1 Grundlagen

muss. Zunächst wieder ein Beispiel, das die Methode illustriert; ich berechne
110012 − 10102 . Damit ich keine Problem mit der Anzahl der Stellen bekom-
me, schreibe ich die 10102 als 010102 , so haben beide Zahlen fünf Stellen. Nun
haben Sie gerade gelernt, dass man die abzuziehende Zahl von der größtmögli-
chen Zahl mit der gleichen Anzahl von Stellen subtrahieren muss. Bei den
Dezimalzahlen bestand diese Riesenzahl aus lauter Neunen, weil 9 nun mal
die höchste Ziffer im Dezimalsystem ist. Und wie lautet die höchste Ziffer im
Binärsystem? Richtig, das ist die 1, und daher lautet die größtmögliche fünf-
stellige Binärzahl schlicht 111112 . Das Rechenschema ist dann das folgende.
11111
− 01010

10101
+ 11001

101110
+ 1

101111
−100000

1111
Was ist hier geschehen? Zuerst habe ich ganz nach Plan 010102 von 111112
abgezogen, und das ging ganz ohne Übertrag. Auf dieses Zwischenergebnis
habe ich dann die ursprüngliche Zahl 110012 addiert, um mich wieder der
eigentlichen Aufgabe zu nähern. Natürlich habe ich jetzt 111112 zu viel auf
der Rechnung, was ich noch ein wenig verschlimmere, indem ich eine schlichte
12 dazu addiere. Das führt zu einem Überschuss von 111112 + 12 = 1000002 ,
und das ist das Beste, was mir passieren konnte. Sie sehen nämlich, dass
mein bisheriges Ergebnis 1011112 beträgt, eine Zahl mit einer 1 vorne und
fünf weiteren Stellen. Und davon muss ich 1000002 abziehen, ebenfalls eine
1 vorne mit fünf anschließenden Nullen. Nichts könnte also leichter sein, als
diese überschüssige 1000002 abzuziehen, ich muss dafür nur die führende 1
aus bei dem bisherigen Ergebnis 1011112 streichen und finde das Endergebnis
11112
So geht das tatsächlich immer. Die eigentliche Subtraktion erledigen Sie,
indem Sie von der größtmöglichen Zahl mit der passenden Anzahl von Stellen
subtrahieren, also von einer hinreichend langen Ansammlung von Einsen, was
immer ohne Übertrag funktioniert. Dann addieren Sie noch die ursprüngliche
größere Zahl, addieren eine weitere Eins und streichen aus dem entstande-
nen Ergebnis die führende Eins heraus, um schließlich beim Endergebnis zu
landen. Schön und gut, aber noch nicht wirklich überzeugend. Das Ziel war
ja, eine Subtraktion hinzubekommen, ohne subtrahieren zu müssen, nur mit
Additionen. Aber auch wenn es vielleicht nicht so aussieht: dieses Ziel ist
1.4 Binäre Arithmetik 53

schon erreicht. Bei der Rechnung 111112 − 010102 kam beispielsweise 101012
heraus, und jetzt vergleichen Sie einmal 01010 mit 10101. Fällt Ihnen etwas
auf? Natürlich fällt Ihnen etwas auf, man kommt von der ersten Zahl auf
die zweite Zahl, indem man die Nullen durch Einsen und die Einsen durch
Nullen ersetzt. Das kann auch gar nicht anders sein, denn an jeder Stelle der
Subtraktion subtrahieren Sie von einer binären 1, da ich als Ausgangspunkt
der Subtraktion die Zahl 111112 gewählt habe. Nun ist aber 12 − 02 = 12 ,
sodass aus einer Eins eine Null wird, und 12 − 12 = 02 , sodass aus einer
Null eine Eins wird. Die erste vorkommende Subtraktion ist also nur schein-
bar eine echte Subtraktion, in Wahrheit reicht es, wenn Sie Einsen zu Nullen
werden lassen und umgekehrt. Diese Operation ist so wichtig, dass sie einen
eigenen Namen bekommen hat: das Einserkomplement. Und weil im Verlauf
der Rechnung auch noch eine schlichte 12 addiert werden muss, hat man sich
auch dafür einen Namen ausgedacht: die Summe aus Einserkomplement und
12 heißt Zweierkomplement.

Einserkomplement und Zweierkomplement


Das Einserkomplement einer Binärzahl erhält man, indem man die Nullen der
Zahl durch Einsen und ihre Einsen durch Nullen ersetzt. Ihr Zweierkomple-
ment erhält man, indem man zu ihrem Einserkomplement noch 12 addiert.

Damit haben wir schon alles zusammen, um eine subtraktionsfreie“ Sub-



traktion einer kleineren binären Zahl von einer größeren durchführen zu
können. Im Beispiel haben sie gesehen, wie es geht: man bildet erst das Einser-
komplement der abzuziehenden Zahl, addiert dann die ursprüngliche größere
Zahl, und addiert auf diese Summe noch 12 . Anders gesagt: auf die ursprüng-
lich größere Zahl addiert man das Zweierkomplement der kleineren. Das ergibt
ein bißchen zu viel, aber das macht gar nichts, denn den Überschuss kann man
beseitigen, indem man die führende Eins streicht.

Subtraktion binärer Zahlen


Sind a und b Binärzahlen und gilt a ≥ b, so berechnet man a − b folgenderma-
ßen. Falls b weniger Stellen hat als a, ergänzt man b durch führende Nullen
und bildet das Zweierkomplement von b. Dieses Zweierkomplement von b ad-
diert man zu a und streicht von dem Ergebnis die führende Eins. Das Resultat
ist a − b. Ist a < b, so berechnet man b − a und nimmt das Ergebnis negativ.

Die führenden Nullen stellen nicht nur eine große Versuchung dar, sich
zwischendurch kurz über Politiker und Manager zu äußern, sondern sind
auch für die Subtraktion sehr bedeutend. Wenn Sie nämlich vergessen, ei-
ne zu kurz geratene Zahl b mit führenden Nullen auf die passende Stellenzahl
zu bringen, wird das Ihr ganzes Ergebnis ruinieren. Nehmen sie als Beispiel
die Aufgabe 10012 − 12 . Das Einserkomplement der unaufgefüllten 12 lautet
natürlich gerade 02 , und da man darauf noch eine 12 addieren muss, ergibt
sich das Zweierkomplement 12 . Das Verfahren würde also von mir verlangen,
54 1 Grundlagen

10012 + 12 = 10102 auszurechnen und davon die führende Eins zu streichen,


das ergibt 0102 = 102 . Aber dummerweise ist 10012 − 12 = 10002 = 102 ,
und dieser Fehler konnte nur auftreten, weil ich die kleinere Zahl 12 nicht
durch führende Nullen auf die richtige Form 00012 gebracht hatte. In die-
sem Fall funktioniert nämlich wieder alles; das Zweierkomplement lautet
11102 + 1 = 11112 , also hat man die Addition 10012 + 11112 = 110002 , und
das Streichen der führenden Eins ergibt das korrekte Resultat 10002 .
Sie sehen: sobald man eine Grundschaltung für die Addition hat, kann man
die Subtraktion ebenfalls mit Hilfe dieser Additionsschaltung durchführen,
damit ist mein Ziel erreicht. Jetzt fehlen nur noch Multiplikation und Division,
aber die sind schnell erledigt.
Übungen gefällig? Dann sollten Sie sich an Aufgabe 1.14 machen.

1.4.6 Multiplikation und Division

Machen wir es kurz: multiplizieren und dividieren kann man Binärzahlen ge-
nauso wie Dezimalzahlen, nur dass die Ausführung etwas einfacher ist. Um
also beispielsweise 1010112 · 1012 auszurechnen, multipliziert man den ersten
Faktor mit der führenden Ziffer 1 des zweiten und schreibt das Ergebnis auf.
Anschließend multipliziert man den ersten Faktor mit der nächsten Ziffer 0
des zweiten und schreibt das Ergebnis unter das erste Ergebnis, allerdings um
eine Stelle nach rechts verschoben. Und so macht man weiter, bis alle Zif-
fern des zweiten Faktors abgearbeitet sind. Da aber nur Einsen und Nullen
vorkommen können, sind die einzelnen Multiplikationen ausgesprochen leicht:
entweder Sie schreiben den ersten Faktor hin oder eine gleichlange Reihe von
Nullen. Dass man am Ende alle Zwischenergebnisse addieren muss, wird Sie
kaum überraschen. Im Rechenschema sieht das dann so aus:
101011· 101
101011· 101
101011
1010110
000000 oder etwas kürzer
101011
101011
11010111
11010111
Die erste Variante entspricht genau dem, was ich oben beschrieben habe, in
der zweiten Variante habe ich noch ausgenutzt, dass die Multiplikation mit
0 nicht so sehr viel bringt und es genügt, an das vorherige Ergebnis eine 0
zu hängen - das kennen Sie noch aus der Schule, und ich muss nicht viele
Worte darüber verlieren. Wichtig ist aber, dass auch die Multiplikation genau
genommen nur aus Additionen besteht und man deshalb auch hier wieder auf
Additionsschaltungen zurückgreifen kann. Übrigens ist das Multiplizieren mit
Zweierpotenzen besonders angenehm: wenn sie mit 102 = 2 multiplizieren,
dann müssen Sie an den anderen Faktor nur eine 0 anhängen, beim Multipli-
zieren mit 1002 = 4 zwei Nullen und so weiter.
1.4 Binäre Arithmetik 55

Multiplikation binärer Zahlen


Die Multiplikation binärer Zahlen erfolgt analog zum bekannten dezimalen
Verfahren der schriftlichen Multiplikation.

Sie können sich sicher vorstellen, wie es bei der Division aussehen wird:
die ganzzahlige binäre Division unterscheidet sich nicht wesentlich von der
bekannten dezimalen. Wollen Sie also beispielsweise den Dividenden 101012
durch den Divisor 112 teilen, so laufen Sie so lange von links nach rechts
durch den Dividenden, bis Ihr Divisor in ihn hineinpasst. Das ist wesentlich
einfacher als im dezimalen Fall, denn Sie haben nur zwei Ziffern zur Verfügung
statt deren zehn und deshalb passt der Divisor entweder genau einmal in
die betrachtete Zahl oder gar nicht. Sobald man etwas Passendes gefunden
hat, schreibt man den Divisor an die richtige Stelle unter den Dividenden,
subtrahiert und zieht die nächste Stelle herunter. Konkret sieht das dann so
aus:
10101:11=111
11

100
11

11
11

0
Also ist 101012 : 112 = 1112 . Allerdings geht nicht jede Division auf, oft genug
muss man mit einem Rest rechnen, und das ist genau dann der Fall, wenn
keine weiteren Stellen mehr zu finden sind, die man herunterziehen könnte,
und die letzte Subtraktion nicht den Wert 0 ergeben hat. Macht aber auch
nichts, dann schreibt man eben den verbleibenden Rest auf wie im folgenden
Beispiel.
1 0 0 0 1 : 1 0 1 1 = 1 Rest 1 1 0
1011

110
Sie müssen also nichts weiter können als subtrahieren, und da wir ja schon die
Subtraktion auf die Addition zurückgeführt hatten, reduziert sich auch das
Dividieren zum Schluss auf das schlichte Addieren. Aber nicht ganz. Immer-
hin sollen Sie ja auch nachsehen, in welchen Teil des Dividenden der Divisor
passt, und zu diesem Zweck bleibt Ihnen nichts anderes übrig als zwei binäre
Zahlen miteinander zu vergleichen. Wie so etwas geht, werden Sie sich in einer
Übungsaufgabe überlegen.
Auch bei der Division gibt es einen Spezialfall, der sie besonders einfach
werden lässt, nämlich die Zweierpotenzen. Teilen Sie durch 102 = 2, so müssen
56 1 Grundlagen

Sie nur den Dividenden seiner letzten Stelle berauben, die dann zum Divisi-
onsrest wird. Beim Teilen durch 1002 streichen streichen Sie entsprechend die
letzten beiden Stellen des Dividenden und machen sie zum Rest und so weiter.
Daher ist ohne weitere Rechnung beispielsweise 10102 : 102 = 1012 , denn den
Rest 0 muss ich nicht aufschreiben, und 1011012 : 1002 = 10112 Rest 012 .

Division binärer Zahlen


Die Division binärer Zahlen erfolgt analog zum bekannten dezimalen Verfah-
ren der schriftlichen Division.
Zum Selbstrechnen stehen die Aufgabe 1.15 und 1.16 zur Verfügung.

1.4.7 Computerorientierte Umrechnungsverfahren


Das Leben könnte schön und der Abschnitt beendet sein, wenn nicht noch ganz
am Anfang ein wichtiger Punkt offen geblieben wäre. Sie hatten gesehen, dass
die besprochenen Umrechnungsverfahren von Binärzahlen in Dezimalzahlen
und umgekehrt zwar beim Rechnen von Hand funktionieren, den Computer
aber vor eine unüberwindbare Hürde stellen: sie verwenden dezimale Arithme-
tik, und der Computer versteht nun mal nichts anderes als die binäre Arith-
metik. Irgendwie muss er aber in der Lage sein, die Dezimalzahlen, die ich
ihm die Tastatur eingebe, in Binärzahlen umzuwandeln, sonst könnte er nicht
damit rechnen. Ich brauche also ein Verfahren, dass die Umrechnungen ganz
ohne dezimale Rechnungen, auf rein binärer Basis vornimmt. Jetzt haben wir
aber - im Gegensatz zum Anfang dieses Abschnitts - alles zur Verfügung, um
so ein Verfahren zu entwickeln.
Ich gehe zunächst von einer Dezimalzahl aus, die dem Computer beispiels-
weise über die Tastatur eingegeben wird. Diese Dezimalzahl besteht aus ein-
zelnen Ziffern. Da ich irgendwie eine Verbindung zwischen der dezimalen und
der binären Darstellung finden muss, brauche ich in meinem Rechner eine klei-
ne Tabelle, in der die Binärdarstellungen der dezimalen Ziffern 0 bis 9 sowie
der 10 aufgelistet sind. Die Tabelle wird also etwa so aussehen:
0 0000
1 0001
2 0010
3 0011
4 0100
5 0101
6 0110
7 0111
8 1000
9 1001
10 1010
wobei ich mich auf vierstellige Binärzahlen beschränken kann. Diese Tabelle
ist also in meinem Rechner gespeichert. Um nun zum Beispiel die dreistellige
1.4 Binäre Arithmetik 57

Dezimalzahl 247 in eine Binärzahl umzurechnen, fange ich mit der ersten Zif-
fer an und sehe in der Tabelle nach, wie ihre binäre Darstellung lautet: Die
2 hat die Darstellung 102 . Nun war diese 2 aber gar keine reine 2, denn hin-
terher kommt noch mindestens eine weitere Stelle, also war die 2 in Wahrheit
mindestens eine 20. Das schadet gar nichts, ich kann ja problemlos die bisher
erreichte Ziffer 102 mit der Binärzahl 10102 = 10 multiplizieren und erhalte
101002 . Die nächste Ziffer in meiner Dezimalzahl war 4 = 1002 . Die addiere
ich auf die bisher erreichte Binärzahl, was zu 101002 + 1002 = 110002 führt.
Dezimal betrachtet, habe ich jetzt 20 + 4 = 24 gerechnet. Hätte meine Zahl
nur zwei Stellen gehabt, dann wäre ich jetzt fertig. Hat sie aber nicht, eine
Stelle ist noch da. Meine 24 war also gar keine simple 24, sondern eine 240,
muss also wieder mit der dezimalen 10 multipliziert werden. Binär gerech-
net ergibt das 110002 · 10102 = 111100002 . Jetzt bin ich immerhin so weit,
dass ich die 240 binär dargestellt und dazu nicht eine einzige dezimale Rech-
nung vorgenommen habe. Eine Stelle ist aber noch da, nämlich die 7, und
da mir die gespeicherte Tabelle sagt, dass 7 = 1112 gilt, muss ich nur noch
auf das bisher erreichte Ergebnis die 1112 addieren, um zu dem Endergebnis
247 = 111100002 + 1112 = 111101112 zu kommen.
Dieses Verfahren führt immer zum Ziel. Man beginnt bei der führenden Zif-
fer der Dezimalzahl und wandelt sie mit Hilfe der Tabelle in eine Binärzahl um.
Gibt es noch weitere Stellen, so multipliziert man die Ziffer in ihrer binären
Form mit 10102 = 10, um ihre Wertigkeit zu erhöhen, und addiert die fol-
gende Ziffer, natürlich in eine Binärzahl umgewandelt, dazu. Und so macht
man immer weiter, immer wieder mit der binären Entsprechung der dezimalen
10 multiplizierend und die noch folgende Ziffer in ihrer binären Darstellung
addierend, bis alle Ziffern aufgebraucht sind. Sie brauchen dafür neben der
Umrechnungstabelle für die binären Ziffern nur ein Register, also eine kleine
Speicherstelle zur Aufnahme der Zwischenergebnisse und des Endergebnisses,
und schon kann Ihr Rechner jede dezimale Eingabe ohne den leisesten Hauch
einer dezimalen Rechnung in eine Binärzahl umrechnen.

Computerorientierte Umrechnung einer Dezimalzahl in eine Binär-


zahl
Das folgende Verfahren rechnet eine Dezimalzahl m auf der Basis der binären
Arithmetik in ihre Binärdarstellung um.
• Man rechne die führende Ziffer von m mit Hilfe einer computerinternen
Umrechnungstabelle in eine Binärzahl um und speichere diese Binärzahl
in einem Register R.
• Falls keine weitere Ziffer folgt, ist die Umrechnung beendet. Gibt es min-
destens eine weitere Ziffer, so berechne man R · 10102 , addiere darauf die
aus der Tabelle entnommene Binärdarstellung der folgenden Ziffer und
speichere das Ergebnis wieder in R.
• Man wiederhole so lange den letzten Schritt, bis alle Ziffern abgearbeitet
sind.
58 1 Grundlagen

Für m = 234 ist im ersten Schritt R = 102 , im zweiten Schritt R =


102 · 10102 + 112 = 101112 , und im letzten Schritt R = 101112 · 10102 + 1002 =
111010102 , womit dann im Register R genau die Binärdarstellung von 234
steht.
Wieder etwas geschafft, und noch immer ist das nur die halbe Miete. Um
alle Probleme zu lösen, muss ich auch noch eine Binärzahl in eine dezimale
Zahl umrechnen können, ohne dezimale Arithmetik zu verwenden. Das ist
leichter als man denkt und beruht auf dem gleichen Prinzip wie das zweite
Umrechnungsverfahren von dezimaler in binärer Richtung, das ich Ihnen am
Anfang des Abschnitts gezeigt habe. Sehen wir uns noch einmal das Beispiel
der Dezimalzahl m = 247 an. Was passiert, wenn ich dieses m durch 10 teile
und dabei den Rest aufschreibe? Dann ist

247 : 10 = 24 Rest 7.

Das Divisionsergebnis 24 teile ich wieder mit Rest durch 10 und erhalte

24 : 10 = 2 Rest 4.

Wenn ich dann zum Schluss auch dieses Divisionsergebnis durch 10 teile, so
ergibt sich
2 : 10 = 0 Rest 2.
Man erhält also die Dezimalstellen einer Zahl, indem man immer wieder mit
Rest durch 10 teilt und anschließend die Reste in umgekehrter Reihenfolge
aufschreibt. Und niemand kann mich daran hindern, diese Rechnungen, die
ich hier gerade dezimal erledigt habe, auf binärer Basis durchzuführen. Falls
irgendeine Rechnung den Wert 111101112 ergeben hat, sagt mir das eben
besprochene Prinzip, dass ich durch die dezimale 10, also durch die binäre
10102 teilen und den Rest bei dieser Division notieren sollte. Die Division
111101112 : 10102 ergibt nach dem Verfahren, das wir vorhin besprochen
haben genau 110002 Rest 1112 . Nun soll ich das gerade erreichte Ergebnis
wieder durch 10102 teilen, und das liefert mir 102 Rest 1002 . Und wenn ich
zum guten Schluss auch noch dieses Ergebnis der Teilung durch 10102 unter-
werfe, bekomme ich natürlich 02 Rest 102 . Das ist gut, denn ein Blick auf die
in umgekehrter Reihenfolge aufgeschriebenen Reste 102 , 1002 und 1112 zeigt,
dass es sich genau um die Ziffern 2, 4 und 7 handelt: die Dezimalziffern von
247. Mit meiner kleinen Tabelle und rein binärer Arithmeitk habe ich also die
Dezimaldarstellung der gegebenen Zahl 111101112 gefunden.

Computerorientierte Umrechnung einer Binärzahl in eine Dezimal-


zahl
Das folgende Verfahren rechnet eine Binärzahl m auf der Basis der binären
Arithmetik in ihre Dezimaldarstellung um.
• Man teile m durch 10102 und notiere den Rest.
• Man teile das Divisionsergebnis durch 10102 und notiere den neuen Rest.
1.4 Binäre Arithmetik 59

• Man wiederhole den zweiten Schritt, bis als Divisionsergebnis der Wert 02
auftritt.
• Man rechne die Reste mit derUmrechnungstabelle in Dezimalzahlen um
und gebe diese Reste im umgekehrter Reihenfolge aus.

Auch hier noch ein kurzes Beispiel. Für m = 11012 erhält man im ersten
Schritt 11012 : 10102 = 12 Rest 112 . Der zweite Schritt liefert dann 12 :
10102 = 02 Rest 12 . Die beiden Reste lauten 12 = 1 und 112 = 3, also handelt
es sich um die Dezimalzahl 13.
Übungsbeispiele finden Sie in Aufgabe 1.17.

1.4.8 Hexadezimalzahlen

Noch eine Kleinigkeit, dann haben Sie die Rechnerarithmetik hinter sich. Wie
Sie sehen konnten, sind binäre Zahlen recht praktisch, da sie nur mit Nullen
und Einsen arbeiten, aber aus dem gleichen Grund können sie ziemlich schnell
ziemlich lang werden. Denken Sie beispielsweise an den Arbeitsspeicher, über
den wir im letzten Abschnitt gesprochen hatten. Jede Speicherzelle hat ei-
ne eigene Adresse, und jede Adresse ist natürlich eine Nummer, die binär
dargestellt werden muss, sonst könnte der Rechner nicht mit ihr umgehen.
Nimmt man nur mal ein Kilobyte Arbeitsspeicher an, so laufen die Nummern
dezimal gerechnet von 0 bis 1023, denn jedes Byte bekommt eine laufende
Nummer. Das sieht noch nicht so schlimm aus, aber binär heißt das: von 02
bis 11111111112 , und das ist schon nicht so leicht zu lesen. Nicht vergessen:
das war nur ein Kilobyte, ein heutiger Arbeitsspeicher kann schon mal locker
ein Gigabyte an Arbeitsspeicher vorweisen, also 230 Byte - können Sie ohne zu
zögern mit dreißigstelligen Binärzahlen umgehen? Manchmal besteht außer-
dem die Notwendigkeit, sich einen Speicherauszug ausdrucken zu lassen, und
der Speicher besteht nun mal aus einer Unmenge von Einsen und Nullen, kei-
ne sehr augenfreundliche Lektüre. Um so etwas absolut Unlesbares wenigstens
ein wenig lesbarer zu machen, hat man die Hexadezimalzahlen entwickelt, auf
deutsch die Sechzehnerzahlen.
Die Idee ist einfach genug. Teilt man eine Binärzahl auf in Viererpäck-
chen, dann kann jedes Viererpäckchen jeden Wert zwischen 00002 und 11112
annehmen, dezimal gerechnet zwischen 0 und 15. Wie Sie unschwer feststellen
können, sind das für jedes Viererpäckchen 16 mögliche Werte, weshalb man
ja auch von Hexadezimalzahlen spricht. Im Sinne einer einfachen Darstellung
braucht man also für jeden Wert zwischen 0 und 15 ein eigenes Zeichen, da-
mit man sie leicht auseinander halten kann. Nun gut, für die Ziffern von 0
bis 9 muss ich da nicht lange suchen und nehme selbstvetständlich die Ziffern
von 0 bis 9. Nur die Zahlen von 10 bis 15 sollten noch mit Zeichen versehen
werden, und dafür verwendet man die ersten fünf Buchstaben des Alphabets.
Man schreibt also A = 10, B = 11, C = 12, D = 13, E = 14 und F = 15.
Damit wird beispielsweise 11111111112 = 11 1111 11112 = 3F F16 : zuerst teilt
60 1 Grundlagen

man die Zahl von rechts nach links in Viererblöcke auf und dann setzt man
diese Viererblöcke in die hexadezimalen Ziffern um. Der vorderste Block hat
nur zwei Stellen, aber das schadet gar nichts, denn 112 = 00112 = 3 = 316 .
Weiterhin ist 11112 = 15 = F16 , und so entsteht die Hexadezimalzahl 3F F16 .
Sie werden zugeben, dass man drei gültige Stellen leichter lesen kann als acht.

Hexadezimalzahlen und Binärzahlen


Man erhält den hexadezimalen Wert einer Binärzahl, indem man sie von rechts
nach links in Vierergruppen zusammenfasst und die hexadezimalen Werte
dieser Vierergruppen aufschreibt. Umgekehrt erhält man die Binärdarstellung
einer Hexadezimalzahl, indem man die einzelnen hexadezimalen Ziffern in
binäre Vierergruppen auflöst.

Zwei Beispiele sollen das Ganze noch abrunden. Zunächst bestimme ich
die Hexadezimaldarstellung von 110011111012 . Die Vierergruppen von rechts
nach links lauten 1101, 0111 und 0110, wobei ich ganz vorne eigentlich
nur eine Deiergruppe 110 habe, aber das Hinzufügen von führenden Nul-
len hat noch nie einer Zahl geschadet. Nun gilt aber 11012 = 13 = D16 ,
0111 = 7 = 716 und 0110 = 6 = 616 . Daraus folgt, wenn man wieder die
richtige Reihenfolge herstellt, 110011111012 = 67D16 . Es ist ziemlich klar,
dass die hexadezimale Darstellung etwas übersichtlicher ist und daher beim
Lesen von Speicherauszügen in aller Regel bevorzugt wird. Nun gehe ich um-
gekehrt vor und suche die Binärdarstellung der Hexadezimalzahl 4F A16 . Ich
muss jede hexadezimale Ziffer in eine binäre Vierergruppe auflösen. Es gilt:
A16 = 10 = 10102 , F16 = 15 = 11112 und 416 = 4 = 0100. Damit folgt:
4F A16 = 0100111110102 = 100111110102 .
Genug der Arithmetik, im nächsten Abschnitt werden Sie sehen, wie man
die Rechenoperationen konkret im Computer realisiert.

Übungen

1.12. Berechnen Sie die dezimalen Werte der folgenden Binärzahlen.


(a) m = 1101100112
(b) m = 111100012

1.13. Berechnen Sie auf zwei verschiedene Arten die Binärdarstellung von
m = 278.

1.14. Führen Sie die folgenden binären Rechenvorgänge durch. Subtraktionen


sind dabei mit Hilfe des Zweierkomplements anzugehen.
(a) 11010102 + 101011102
(b) 10102 + 01101102
(c) 111011012 − 1010012
(d) 11012 − 111012
1.5 Logische Schaltungen und Addierer 61

1.15. Führen Sie die folgenden binären Rechenvorgänge durch.


(a) 1101102 · 10112
(b) 101001001112 · 1002
(c) 11011011012 : 11012
(d) 10000012 : 1002
1.16. Entwickeln Sie ein Verfahren, das zwei binäre Zahlen der Größe nach
vergleicht und feststellt, welche die größere ist. Beschreiben Sie das Verfahren
verbal.
1.17. Führen Sie eine computerorientierte Umrechnung von einer Dezimalzahl
in eine Binärzahl bzw. umgekehrt durch.
(a) Dezimalzahl m = 238
(a) Binärzahl m = 1100112
1.18. Führen Sie eine Umrechnung von einer Binärzahl in eine Hexadezimal-
zahl bzw. umgekehrt durch.
(a) Binärzahl 1101100112
(b) Hexadezimalzahl ABC16
1.19. Eine Oktalzahl ist eine Folge aus n + 1 Ziffern an an−1 ...a1 a0 , wobei jede
der Ziffern zwischen 0 und 7 liegt. Wie kann man auf sehr einfache Weise sie
Umrechnung von Binärzahlen in Oktalzahlen oder umgekehrt vornehmen?

1.5 Logische Schaltungen und Addierer


Die binäre Arithmetik in allen Ehren, doch noch fehlt etwas sehr Wichtiges.
Sie wissen jetzt zwar, wie man grundsätzlich so rechnet, als wäre man ein
Computer, aber wie wird das nun eigentlich auf der Basis von elektrischem
Strom im Rechenwerk realisiert? Auch Einsen und Nullen kann kein Com-
puter der Welt direkt verstehen, er versteht nur den Strom, der fließt oder
es bleiben lässt. Wir müssen uns also ein paar Gedanken darüber machen,
wie man die Rechenoperationen aus dem letzten Abschnitt konkret mit Hilfe
von elektronischen Schaltungen umsetzen kann. Glücklicherweise haben Sie
bei der binären Arithmetik gelernt, dass im Grunde jede Rechenoperation
auf Additionen zurück geführt werden kann, und deshalb werde ich mich hier
darauf beschränken, eine Art von Addierwerk zu bauen, das in der Lage sein
soll, binäre Zahlen zu addieren. Wie man dann die anderen Grundrechenarten
aus diesem Addierwerk zusammensetzt, folgt direkt aus den Überlegungen des
letzten Abschnitts.
Das wesentliche Hilfsmittel bei der Realisierung der Addition sind die lo-
gischen Schaltungen oder auch logischen Gatter. Sie beruhen auf nur drei
sehr einfachen logischen Konstruktionen, weshalb man auch drei verschiede-
ne Grundschaltungen unterscheidet. Fangen wir vorsichtig mit der ersten an,
schwer ist keine von ihnen.
62 1 Grundlagen

1.5.1 Die UND-Schaltung

Aus dem täglichen Sprachgebrauch ist Ihnen die Verwendung des Wortes
und“ mehr als vertraut, und genau diesen Sprachgebrauch setzt die so ge-

nannte UND-Schaltung oder auch AND-Schaltung um. Denken Sie immer dar-
an, dass es hier um Einsen und Nullen geht, also um fließenden oder nicht flie-
ßenden Strom. Hat man nun beispielsweise zwei binäre Inputs, also vielleicht
zwei Nullen oder auch eine Null und eine Eins, so möchte man darauf auf ein-
fache Weise einen binären Output machen, und eine Möglichkeit dazu bietet
die UND-Schaltung. Sie liefert genau dann eine 1 im Output, wenn alle Inputs
auf 1 stehen; hat man auch nur einen auf 0 stehenden Input, so ergibt sich am
Ende der UND-Schaltung gnadenlos eine 0. Physikalisch betrachtet, kann sie
durch eine Reihenschaltung oder auch Serienschaltung wie in Abbildung 1.10
realisiert werden.

A B

Abb. 1.10. Reihenschaltung

Sie sehen eine Stromquelle, die durch die beiden senkrechten Linien ver-
schiedener Länge symbolisiert wird. Das seltsame runde Ding rechts ist das
gängige Symbol für einen Stromverbraucher, also eine Glühbirne, ein PC oder
sonst irgendetwas, das Ihre Stronmrechnung hochtreibt. Offenbar kann nur
dann Strom fließen, wenn die beiden Schalter A und B geschlossen sind, an-
sonsten wird der Stromfluss rüde unterbrochen und der Stromverbraucher
wird Ihre Rechnung nicht belasten. Stellt man sich einen Schalter als eine Art
Relais vor, dann kann der Schalter geschlossen werden, wenn er duch einen
magnetisierten Eisenkern nach unten gezogen wird, und dieser Eisenkern wird
durch fließenden Strom magnetisiert - das haben Sie schon bei der Aufgabe
1.5 gesehen. Ein existierender Inputstrom führt also zu einem geschlossenen
Schalter, anders gesagt: steht der Input auf 1, so wird der Schalter geschlossen.
Steht er dagegen auf 0, wird niemand mehr den Schalter nach unten ziehen,
er bleibt offen und der betrachtete Stromkreis kann nicht geschlossen werden.
Natürlich wird so etwas heute nicht mehr über elektromechanische Relais
geregelt, sondern über vollelektronische Schaltelemente, aber darauf kommt
es hier gar nicht an. Wichtig ist, dass Sie sehen, wie Input und Output zusam-
menhängen: stehen beide Inputs auf 1, ist der Outputstromkreis geschlossen,
1.5 Logische Schaltungen und Addierer 63

steht also abenfalls auf 1. Steht auch nur einer der Inputs auf 0, ist der Out-
putstromkreis unterbrochen und steht daher auf 0. Die UND-Schaltung liefert
also genau dann eine 1, wenn alle Inputs auf 0 stehen, ansonsten liefert sie eine
triste 0. Das Formelsymbol hat man der Aussagenlogik entnommen, für zwei
Inputs schreibt man A∧B, für drei Inputs A, B und C entsprechend A∧B ∧C.
Ich will mich hier nicht über die Rechenregeln für das UND-Symbol ∧ verbrei-
ten, das gehört in Ihre Vorlesungen zur Algebra, Diskreten Mathematik oder
auch Digitaltechnik. Wir sehen uns lieber an, wie sich die UND-Schaltung in
einer konkreten Tabelle ausdrückt. Schwer ist das nicht, man muss nur alle
möglichen Input-Kombinationen und ihren jeweiligen Output ordentlich in ei-
ner Tabelle zusammenfassen. Bei drei Inputs sieht das dann folgendermaßen
aus.
A B C A∧B∧C
0 0 0 0
0 0 1 0
0 1 0 0
0 1 1 0
1 0 0 0
1 0 1 0
1 1 0 0
1 1 1 1
Sie sehen auch hier wieder: der Output liefert Ihnen genau dann eine 1, wenn
alle Inputs auf 0 standen. Beachten Sie übrigens, wie ich die möglichen Input-
werte in der Tabelle eingetragen habe. Da es sich um drei Inputs handelt, habe
ich es mit dreistelligen binären Zahlen zu tun, und deshalb habe ich einfach
die dreistelligen binären Zahlen in die Zeilen der Tabelle hinein geschrieben.
Von 0002 bis 1112 , also von 0 bis 7, gibt es aber genau acht Zahlen, daher hat
die Tabelle auch acht Zeilen.

A
& A B
B

Abb. 1.11. Symbol der UND-Schaltung

Sollten Sie jedesmal, wenn Sie eine UND-Schaltung in ein etwas kompli-
zierteres Schaltsystem einzeichnen müssen, das komplette Bild aus Abbildung
1.10 abmalen, dann würden Sie sich mit Recht beschweren. Die Beschwerde
kann aber gar nicht erst aufkommen, denn man hat sich ein graphisches Sym-
bol überlegt, das die Einzeichnung von UND-Schaltungen in Schaltbilder zur
leichten Übung werden lässt; Sie sehen dieses für sich sprechende Symbol in
Abbildung 1.11.
64 1 Grundlagen

UND-Schaltung
Die UND-Schaltung bestimmt aus mehreren binären Inputs einen binären
Output. Der Output ist genau dann 1, wenn alle Inputs auf 1 stehen; hat
auch nur ein Input den Wert 0, so liefert die UND-Schaltung den Output 0.
Die UND-Verknüpfung von zwei Inputs A und B wird mit dem Symbol A ∧ B
symbolisiert.

1.5.2 Die ODER-Schaltung

Das Wort oder“ hat in der deutschen Sprache zwei Bedeutungen, auch wenn

man sich das nicht immer klar macht. Der Straßenräuber, der Ihnen auf der B-
Ebene der Frankfurter Konstablerwache den freundlichen Satz Geld her oder

ich schieße“ zuraunt, meint genau genommen entweder-oder“, denn sobald

Sie Ihre Brieftasche herausrücken, sollte man zumindest hoffen, dass er Sie
ungeschoren lässt. Im Zusammenhang mit der Informatik im Allgemeinen und
der Logik sowie den Schaltungen im Besonderen ist etwas anderes gemeint,
aber ich bin ja auch kein Straßenräuber. Unser Oder ist ein Oder-Auch, ein
einschließendes Oder, sodass mit A oder B“ immer gemeint ist: A oder B

oder beides.
Nach dieser Vorrede ist vielleicht schon klar, was man unter einer ODER-
Schaltung bzw. OR-Schaltung zu verstehen hat. Sie liefert genau dann den
Output 1, wenn mindestens einer der beteiligten Inputs den Wert 1 hat; nur
wenn alle Inputs auf 0 stehen, wird sie den Output 0 liefern. Physikalisch be-
trachtet, kann sie durch eine Parallelschaltung wie in Abbildung 1.12 realisiert
werden.

Abb. 1.12. Parallelschaltung

Im Gegensatz zur UND-Schaltung ist die ODER-Schaltung nicht allzu


kleinlich. Wenn einer der beiden Schalter offen ist, kann der Strom noch immer
durch die andere Leitung fließen, sodass der Stromkreis geschlossen bleibt.
Bei mehr als zwei Schaltern ist das natürlich genauso: sobald auch nur ein
Schalter geschlossen ist, also mindestens ein Input den Wert 1 hat, wird die
1.5 Logische Schaltungen und Addierer 65

ODER-Schaltung den Output 1 liefern, und so war es ja auch geplant. Erst


bei durchgängig geöffneten Schaltern, also bei einer Belegung aller Inputs
mit Nullen, erhalten Sie den Output 0. Das Formelsymbol hat man wieder
der Aussagenlogik entnommen, für zwei Inputs schreibt man A ∨ B, für drei
Inputs A, B und C entsprechend A ∨ B ∨ C und so weiter.
Wie sieht das nun in einer Tabelle aus? Ganz einfach. Nehmen wir wie
im Falle der UND-Schaltung wieder drei Inputs A, B und C an, so ist A ∨
B ∨ C genau dann 0, wenn sowohl A = 0 als auch B = 0 als auch C = 0
gilt, in allen anderen Fällen kommt 1 heraus. An der Menge der möglichen
Inputkombinationen ändert sich gar nichts, es sind immer noch die gleichen
acht Möglichkeiten wie bei der Tabelle für die UND-Schaltung. Damit ergibt
sich die folgende Tabelle.

A B C A∨B∨C
0 0 0 0
0 0 1 1
0 1 0 1
0 1 1 1
1 0 0 1
1 0 1 1
1 1 0 1
1 1 1 1

Und auch für die ODER-Schaltung hat sich ein eigenes Symbol durchgesetzt,
das Sie in Abbildung 1.13 bewundern können.

A
1 A B
B

Abb. 1.13. Symbol der ODER-Schaltung

ODER-Schaltung
Die ODER-Schaltung bestimmt aus mehreren binären Inputs einen binären
Output. Der Output ist genau dann 0, wenn alle Inputs auf 0 stehen; hat auch
nur ein Input den Wert 1, so liefert die ODER-Schaltung den Output 1. Die
ODER-Verknüpfung von zwei Inputs A und B wird mit dem Symbol A ∨ B
symbolisiert.
66 1 Grundlagen

1.5.3 Die NICHT-Schaltung


Ein logisches Gatter fehlt noch in der Liste, nämlich die Schaltung für puber-
tierende Jugendliche: was auch immer Sie ihnen sagen, sie wollen ohnehin das
Gegenteil. Für solche Zwecke hat man die NICHT-Schaltung oder auch NOT-
Schaltung erfunden, und sie ist von allen drei logischen Grundschaltungen die
einfachste - aus einer 1 macht sie eine 0, aus einer 0 dagegen eine 1. Übersicht-
licher gehts nicht mehr. Man spricht auch von der Negation eines Inputs und
meint damit, dass sich der Wert des Inputs genau in sein Gegenteil verkehrt.
Für diese Negation sind sogar zwei verschiedene Symbole im Gebrauch; ist A
ein Input für die NICHT-Schaltung, so wird der Output von manchen als A
und von anderen als ¬A bezeichnet. Beides ist gebräuchlich, und Sie können
sich aussuchen, was Ihnen besser gefällt.
Die tabellarische Darstellung der Negation ist ausgesprochen übersichtlich,
da ich nur einen Input und einen Output habe. Das führt zu der folgenden
Tabelle.
A A
0 1
1 0
Es wird niemanden überraschen, dass sich auch für die NICHT-Schaltung
ein Symbol durchgesetzt hat, das ihre Darstellung in größeren Schaltbildern
vereinfacht. In Abbildung 1.14 ist es zu sehen.

A 1 A

Abb. 1.14. Symbol der NICHT-Schaltung

NICHT-Schaltung
Die NICHT-Schaltung bestimmt aus einem binären Input einen binären Out-
put. Der Output ist genau dann 1, wenn der Input auf 0 steht und genau
dann 0, wenn der Input auf 1 steht. Wird ein Input A der NICHT-Schaltung
unterworfen, so schreibt man für den Output A oder auch ¬A.

So viel zu den Grundschaltungen. Jetzt sehen wir uns an, was man damit
anfangen kann.

1.5.4 Der Halbaddierer


Es mag ja ganz nett sein, über die drei Grundschaltungen zu verfügen, aber
mein eigentliches Ziel war es, eine Schaltung aufzubauen, die in einem Rech-
1.5 Logische Schaltungen und Addierer 67

ner die binäre Addition realisiert, denn das ist die Grundoperation für die
gesamte Arithmetik. Von diesem Ziel bin ich aber nicht mehr sehr weit ent-
fernt. Was passiert denn, wenn ich beispielsweise zwei binäre Ziffern addiere?
Es ergibt sich eine Ergebnisziffer und unter Umständen ein Übertrag, den ich
beim Addieren der nächsten beiden Ziffern berücksichtigen muss. Anders ge-
sagt: ich habe zwei Inputs, nämlich die beiden zu addierenden Ziffern, und
zwei Outputs, nämlich die Ergebnisziffer und den Übertrag, und das kann
man, wenn man Spaß daran hat, in einer Tabelle darstellen, genau wie bei
den Tabellen der Grundschaltungen. Das gilt aber nicht nur für die Addition
zweier Binärziffern. Wann immer Sie ein Zuordnung von einem oder mehreren
binären Inputs zu einem oder mehreren binären Outputs haben, können Sie
natürlich eine Tabelle aufbauen, in deren linken Teil Sie alle möglichen Input-
kombinationen auflisten und in deren rechten Teil Sie die jeweiligen Outputs
für die jeweilige Inputkombination aufschreiben. So etwas ist ein Geduldsspiel,
weiter nichts. Tabellen nützen mir aber nichts, ich brauche Schaltungen, die
auf meinen drei Grundschaltungen beruhen. Und schon haben wir ein kleines
Problem: wenn eine Funktionstabelle aus einem oder mehreren Inputs und ei-
nem oder mehreren Outputs gegeben ist, wie kann man dann nur aus den drei
Grundschaltungen eine Schaltung aufbauen, die genau diese Funktionstabelle
realisiert?
Das ist leichter als man vielleicht denkt, ich zeige es Ihnen einmal am
Beispiel des ausschließenden ODER. Die ODER-Schaltung, auf die wir uns
vorhin geeinigt hatten, war bekanntlich ein einschließendes Oder, ein Oder-
Auch. Denkt man an den Straßenräuber in der Konstablerwache, so fällt einem
sofort wieder das alte Entweder-Oder ein, das viel strenger ist als unser altes
Oder-Auch: entweder A oder B kann nur heissen, dass nicht beide gleichzei-
tig etwas zum Ergebnis beitragen können, und das bedeutet, das Ergebnis
ist dann 1, wenn entweder A auf 1 steht oder B auf 1 steht. Sollten beide
gleichzeitig auf 0 oder beide gleichzeitig auf 1 stehen, muss sich der Output
0 ergeben. Man bezeichnet das als ein ausschließendes ODER, abgekürzt als
XOR, wobei das X für eXclusive“ steht, und verwendet dafür das Symbol

A ⊕ B. In einer Tabelle sieht das so aus:
A B A⊕B
0 0 0
0 1 1
1 0 1
1 1 0
Offenbar liefert A ⊕ B genau dann eine 1 wenn entweder A oder B auf 1
steht. Alles klar, aber ich will doch nur die drei Grundschaltungen verwenden
und nicht alle fünf Minuten gezwungen sein, eine weitere Grundschaltung
einzuführen. Folglich muss ich jetzt zusehen, wie ich diese neue Schaltung aus
meinen alten Schaltungen zusammensetze.
Sehen wir uns das XOR etwas genauer an und formulieren die Bedingungen
um, unter denen es den Output 1 liefert. Es ist doch A ⊕ B = 1, genau dann,
68 1 Grundlagen

wenn A = 0 und B = 1 gilt oder wenn A = 1 und B = 0 gilt. Da aber


A = 0 genau dann gilt, wenn man A = 1 hat, bedeutet das: A ⊕ B = 1
gilt genau dann, wenn A = 1 und B = 1 gilt oder wenn A = 1 und B = 1
gilt. Das ist aber praktisch, denn in dieser Beschreibung kommen nur noch
die Schlüsselworte und“, oder“ und die Negation vor, und das waren genau
” ”
meine drei logischen Grundschaltungen. Wenn Sie jetzt nämlich die letzte
Beschreibung in eine formale Schreibweise übersetzen, dann stellen Sie fest,
dass genau dann A ⊕ B = 1 gilt, wenn A ∧ B = 1 oder B ∧ A = 1 gilt. Und
da ich auch ein Symbol für das ODER habe, folgt insgesamt:

A ⊕ B = (A ∧ B) ∨ (B ∧ A).

Sobald man das einmal gefunden hat, kann man es auch leicht nachprüfen,
indem man einfach für die rechte Seite eine Tabelle aufstellt und die Ergeb-
nisspalte mit der von A ⊕ B vergleicht: natürlich sind dann beide Spalten
identisch.
Damit ist das XOR übersetzt in eine Kombination der drei bekannten
und beliebten Grundschaltungen. Üblicherweise setzt man so etwas um in
ein Schaltbild, das den Vorteil einer etwas größeren Anschaulichkeit hat. Das
Schaltbild für das ausschließende ODER sehen Sie in Abbildung 1.15.

A 1
&
B
A B
1
1
&

Abb. 1.15. Schaltbild der XOR-Schaltung

Die beiden Inputs heißen A und B. Von A aus gerät man sowohl in
ein NICHT-Gatter als auch - über eine Abzweigung, die durch einen dicken
schwarzen Punkt markiert wird, - in ein UND-Gatter, und genau das Glei-
che passiert mit B. Der Wert A wird dann über eine UND-Schaltung mit B
kombiniert, was zu dem Zwischenergebnis A ∧ B führt. Analog wird der Wert
B über die zweite UND-Schaltung kombiniert mit A selbst, und das ergibt
natürlich A ∧ B. Das war es auch schon fast, denn diese beiden Zwischener-
gebnisse müssen nur noch in einem letzten ODER-Gatter zusammengefasst
werden, um am Ende das Ergebnis A ⊕ B = (A ∧ B) ∨ (B ∧ A) zu erhalten.
1.5 Logische Schaltungen und Addierer 69

Die XOR-Schaltung
Die XOR-Schaltung bestimmt aus zwei binären Inputs einen binären Output.
Der Output ist genau dann 1, wenn genau ein Input auf 1 steht, wenn also
entweder der eine oder andere Input den Wert 1 hat. Haben beide Inputs
den gleichen Wert, so liefert die XOR-Schaltung den Output 0. Die XOR-
Verknüpfung von zwei Inputs A und B wird mit dem Symbol A ⊕ B symboli-
siert. Sie kann mit Hilfe der drei Grundschaltungen UND, ODER und NICHT
erzeugt werden.

Nun sind wir so weit, dass wir den ersten Addierer bauen können, den
so genannten Halbaddierer. Er leistet nichts weiter als die Addition zweier
Binärziffern und liefert dabei eine Ergebnisziffer und einen Übertrag, beides
kann 0 oder 1 werden. Da es nun um konkrete Ziffern geht, werde ich die
Bezeichnungsweise ändern und die beiden Inputziffern mit x und y bezeichnen,
die Ergebnisziffer r nennen und den Übertrag u. Dann ist also r die Ziffer,
die man beim schriftlichen Addieren unter dem Strich finden würde, und u ist
der Übertrag, der für das weitere Rechnen über den Strich geschrieben wird.
Das ergibt die folgende Tabelle.
x y r u
0 0 0 0
0 1 1 0
1 0 1 0
1 1 0 1

x r
x y
y

u
&

Abb. 1.16. Halbaddierer

Werfen Sie einen Blick auf die beiden Ergebnisspalten. Kommen sie Ihnen
bekannt vor? Ich hoffe doch, denn beide haben wir bereits besprochen. Die Er-
gebnisziffer r entspricht genau der XOR-Verknüpfung der beiden Inputs x und
70 1 Grundlagen

y, während der Übertrag noch einfacher gebaut ist, denn er wird genau dann
1, wenn beide Inputs auf 1 stehen und stellt daher nur die UND-Verknüpfung
von x und y dar. Also ist schlicht r = x ⊕ y und u = x ∧ y. Um das wieder
in ein Schaltbild zu zeichnen, muss ich jetzt natürlich nicht mehr alle Einzel-
heiten der XOR-Schaltung aufmalen, die kennen Sie bereits aus Abbildung
1.15. Es genügt jetzt, das XOR als eine Schaltung zu betrachten, die man
bereits als Bauteil zur Verfügung hat, und diese Schaltung als eine Einheit in
die gesuchte Schaltung des Halbaddieres einzubauen. In Abbildung 1.16 sehen
Sie, was dabei herauskommt.
Die Symbole der Schaltung habe ich bereits alle erklärt, deshalb werde
ich jetzt nicht mehr näher darauf eingehen. In jedem Fall haben wir hier eine
Schaltung zusammengebaut, die auf der Basis von elektrischem Strom mit
Hilfe der drei Grundschaltungen zwei binäre Ziffern addiert und eine Ergeb-
nisziffer sowie einen Übertrag berechnet. Die Schaltung heißt deshalb Halbad-
dierer, weil sie nur die Hälfte der Arbeit erledigt, die man beim Addieren von
Binärzahlen braucht. Wenn Sie nämlich, wie im letzten Abschnitt besprochen,
zwei binäre Zahlen addieren, dann ist es zwar sicher wahr, dass Sie in jeder
Stelle eine Ergebnisziffer und einen Übertrag bekommen werden, aber Sie
müssen immer damit rechnen, dass aus der vorher betrachteten binären Stelle
noch ein Übertrag da ist, den Sie berücksichtigen müssen. Bei Additionen der
Form
11 1 0 11
+ 1111111011

101 1 0 00
wird es ziemlich häufig passieren, dass man drei Ziffern addieren muss und
nicht nur zwei, und genau das kann der Halbaddierer nicht leisten. Es hilft
also nichts, ich werde das Spiel noch etwas weiter treiben und Ihnen zeigen,
wie man einen Volladdierer baut, der dieses Problem löst.

Halbaddierer
Der Halbaddierer bestimmt aus zwei binären Inputs zwei binäre Outputs. Er
addiert zwei Binärziffern x und y und liefert dabei eine Ergebnisziffer r und
einen Übertrag u. Man kann ihn zusammen setzen aus einer XOR-Schaltung
und einer UND-Schaltung; es gilt r = x ⊕ y und u = x ∧ y.

1.5.5 Der Volladdierer

Wir hatten uns bereits geeinigt: bei der Addition zweier mehrstelliger Bi-
närzahlen wird man nicht nur jeweils zwei binäre Ziffern x und y, sondern
leider auch noch einen alten Übertrag u1 aus der vorherigen Stelle addieren
müssen. Diese Addition aus drei Inputs liefert dann eine Ergebnisziffer r und
einen neuen Übertrag u2 , genau wie schon beim Halbaddierer. Die zugehörige
1.5 Logische Schaltungen und Addierer 71

Tabelle ist schnell aufgeschrieben, da Sie die binäre Addition mittlerweile im


Schlaf beherrschen.
x y u1 r u2
0 0 0 0 0
0 0 1 1 0
0 1 0 1 0
0 1 1 0 1
1 0 0 1 0
1 0 1 0 1
1 1 0 0 1
1 1 1 1 1
Jetzt muss ich nur noch genau das Gleiche machen wie vorher bei der XOR-
Schaltung und beim Halbaddierer: ich muss feststellen, wie ich diese Tabelle
in eine vernünftige Schaltung umsetzen kann. Das Prinzip ist aber auch nicht
anders; solange man sich an die immer gleiche Vorgehensweise hält, kann
nichts schiefgehen.
Ich fange mit dem Output r an. Wann steht r auf 1? Offenbar genau dann,
wenn genau ein Input auf 1 steht oder wenn alle drei Inputs auf 1 stehen. In
der zweiten Zeile ist beispielsweise nur u1 = 1, und das heißt, dass sowohl
x = 1 als auch y = 1 gilt. Anders gesagt: genau dann gilt gleichzeitig x = 0
und y = 0 und u1 = 1, wenn x ∧ y ∧ u1 = 1 gilt. In diesem Fall habe ich auf
jeden Fall r = 1. Aber nicht nur in diesem Fall, es könnte ja auch sein, dass
x = 0, y = 1 und u1 = 0 gilt, wie es der dritten Zeile meiner Tabelle entspricht.
Das bedeutet natürlich x = 1, y = 1 und u1 = 1, also ist dieser Fall genau
dann gegeben, wenn x ∧ y ∧ u1 = 1 ist. Sie können sich schon denken, wie es
weiter geht. In der fünften Zeile hat r wieder eine 1 aufzuweisen, und weil hier
x = 1, y = 0 und u1 = 0 gilt, muss x ∧ y ∧ u1 = 1 sein. Damit habe ich die
Fälle erledigt, in denen genau einer der Inputs auf 1 steht. Es bleibt noch die
letzte Zeile der Tabelle, bei der überhaupt alles zu 1 wird, insbesondere auch
x, y und u1 . Diese drei haben aber genau dann gleichzeitig den Wert 1, wenn
x ∧ y ∧ u1 = 1 ist, denn genauso war die UND-Verknüpfung gemacht.
Gefunden habe ich jetzt, dass r genau dann den Wert 1 liefert, wenn
x ∧ y ∧ u1 = 1 oder x ∧ y ∧ u1 = 1 oder x ∧ y ∧ u1 = 1 oder x ∧ y ∧ u1 = 1 gilt.
Und weil wir für solche Zwecke eine ODER-Verknüpfung haben, heißt das:

r = (x ∧ y ∧ u1 ) ∨ (x ∧ y ∧ u1 ) ∨ (x ∧ y ∧ u1 ) ∨ (x ∧ y ∧ u1 )
= (x ∧ y ∧ u1 ) ∨ (x ∧ y ∧ u1 ) ∨ (x ∧ y ∧ u1 ) ∨ (x ∧ y ∧ u1 ).

In der letzten Zeile habe ich dabei nur den ersten Klammerausdruck nach vor-
ne gestellt, damit ich nicht gleich mit so etwas Negativem wie einer Negation
anfangen muss.
Schön ist das nicht, aber nötig, denn jetzt habe ich meine logische Schal-
tung für die Ergebnisziffer schon zusammen. Ich werde sie auch gleich in ein
Schaltbild eintragen, aber das liefert nur noch eine graphische Veranschau-
lichung dieser Formel, die Informationen stecken schon vollständig in dem
72 1 Grundlagen

Ausdruck, den ich aus der Tabelle geschlossen habe. Noch bin ich allerdings
nicht fertig, der Übertrag u2 fehlt mir noch, und er verdient einen genaueren
Blick.
Gehen wir zunächst nach dem gleichen Schema vor wie eben. Der Output
u2 liefert in der vierten, sechsten, siebten und achten Zeile der Tabelle eine 1,
was ich wieder leicht in eine logische Schaltung übersetzen kann. In der vierten
Zeile ist x = 0, y = 1 und u1 = 1, was zu dem Ausdruck x ∧ y ∧ u1 führt.
Nach der gleichen Methode entsprechen die restlichen interessanten Zeilen den
Ausdrücken x ∧ y ∧ u1 , x ∧ y ∧ u1 und x ∧ y ∧ u1 . Daraus folgt, dass
u2 = (x ∧ y ∧ u1 ) ∨ (x ∧ y ∧ u1 ) ∨ (x ∧ y ∧ u1 ) ∨ (x ∧ y ∧ u1 )
gilt. Schon wieder so ein langes Elend. Denken Sie daran: das sollen ja kon-
krete Schaltungen in einem Computer werden, und je weniger Bauteile Sie
benötigen, desto besser für die Kostenrechnung und für Ihren Arbeitsplatz.
Den Übertrag u2 können Sie aber tatsächlich ein wenig kürzer darstellen, mit
einer deutlich einfacheren Schaltung. Noch einmal ziehe ich die Tabelle zu
Rate und sehe mir genauer an, wann u1 = 1 ist. Das gilt nämlich genau dann,
wenn mindestens zwei der Inputs auf 1 stehen, wenn also x ∧ y = 1 oder
x ∧ u1 = 1 oder y ∧ u1 = 1 ist. Sobald eine dieser drei Bedingungen erfüllt
ist, können Sie sich auf keinen Fall in der ersten, zweiten, dritten oder fünften
Zeile der Tabelle befinden, denn dort würde jede der drei UND-Verknüpfun-
gen den Wert 0 ergeben. In der letzten Zeile sind sogar alle drei Bedingungen
auf einmal erfüllt, und in den anderen drei Zeilen jeweils eine, weshalb ich u2
auch durch den wesentlich einfacheren Ausdruck
u2 = (x ∧ y) ∨ (x ∧ u1 ) ∨ (y ∧ u1 )
darstellen kann. Sollten Sie daran zweifeln, dann empfehle ich Ihnen, diesen
Ausdruck einfach in einer Input-Output-Tabelle auszuwerten und das Ergeb-
nis mit u2 zu vergleichen. Spätestens das wird Sie überzeugen.
Nun habe ich sowohl für die Ergebnisziffer r als auch für den Übertrag u2
eine logische Schaltung gefunden und muss die beiden Schaltungen nur noch in
einem Schaltbild darstellen. Das wird natürlich etwas komplizierter ausfallen
als die bisherigen recht übersichtlichen Schaltbilder, denn die Schaltungen sind
nun mal aufwendiger; sie sehen es in Abbildung 1.17 vor sich.
Niemand hat behauptet, dass das Schaltbild des Volladdierers den Schön-
heitspreis des Jahres gewinnt, aber man kann nicht alles haben. Wenn Sie
die Verbindungslinien unter Berücksichtigung der dick eingezeichneten Kno-
tenpunkte verfolgen, werden Sie die Übereinstimmung des Schaltbildes mit
meinen mühsam ermittelten logischen Ausdrücken feststellen, und nur dar-
auf kommt es an. Im unteren Teil des Bildes werden zum Beispiel jeweils zwei
Inputs in einer UND-Schaltung verknüpft, woraufhin die Ergebnisse der UND-
Verknüpfungen in einem ODER-Gatter zusammwen geführt werden: das ist
genau die Schaltung, die mir den Übertrag u2 liefert. Im oberen Teil habe ich
zunächst alle drei Inputs in ein UND-Gatter geführt und den Ausgang die-
ses UND-Gatters zum Input eines ODER-Gatters werden lassen: damit wird
1.5 Logische Schaltungen und Addierer 73

x y u1

&

&

& 1 r
&

&

& 1 u2
&

Abb. 1.17. Volladdierer

x ∧ y ∧ u1 zu einem der vier Inputs des großen ODER-Gatters, und genau-


so verlangt es der logische Ausdruck für r. Danach wird jeder der drei Inputs
x, y und u1 durch ein NICHT-Gatter geschickt und so mit den anderen Inputs
UND-verknüpft, dass die drei Ausdrücke x ∧ y ∧ u1 , x ∧ y ∧ u1 und x ∧ y ∧ u1
entstehen, die sich dann zusammen mit dem vorher erzeugten x ∧ y ∧ u1 in
einem ODER-Gatter vereinigen, das schließlich die Ergebnisziffer r liefert.
So viel Aufwand für die schlichte Notwendigkeit, zwei binäre Ziffern samt
Übertrag zu addieren. Damit haben Sie aber auch den Grundbaustein in der
Hand; Sie hatten gesehen, dass die gesamte binäre Arithmetik sich auf die
Addition zurückführen lässt, und wie man eine Addition von binären Ziffern
umsetzt, habe ich Ihnen gerade gezeigt.

Volladdierer
Der Volladdierer bestimmt aus drei binären Inputs zwei binäre Outputs. Er
addiert zwei Binärziffern x und y zu einem Übertrag u1 und liefert dabei eine
Ergebnisziffer r und einen Übertrag u2 . Es gelten die Formeln:

r = (x ∧ y ∧ u1 ) ∨ (x ∧ y ∧ u1 ) ∨ (x ∧ y ∧ u1 ) ∨ (x ∧ y ∧ u1 )

und
u2 = (x ∧ y) ∨ (x ∧ u1 ) ∨ (y ∧ u1 )
74 1 Grundlagen

Sie sollten doch immerhin einmal sehen, dass man die entwickelten Grund-
bausteine auch zu etwas Sinnvollem zusammensetzen kann. Wie werden denn
jetzt ganze Binärzahlen addiert, wie kommt man von der Beschränkung auf
Ziffern los? Das ist ganz einfach. Nehmen wir an, dass unser Rechner in der
Lage sein soll, vierstellige Binärzahlen zu addieren, so muss ich nur die bei-
den entwickelten Schaltelemente Halbaddierer“ und Volladdierer“ richtig
” ”
zusammenschalten. Zu diesem Zweck bezeichne ich den Halbaddierer mit dem
Symbol HA und den Volladdierer mit dem Symbol VA. Addieren will ich die
beiden vierstelligen Binärzahlen x3 x2 x1 x0 und y3 y2 y1 y0 . Dann ergibt sich das
Addierwerk aus Abbildung 1.18; die Inputs stehen über dem Addierwerk, der
Output r4 r3 r2 r1 r0 setzt sich aus den Ergebnissen zusammen, die von den
einzelnen Additionsschaltungen geliefert werden.

x 3 y3 x 2 y2 x 1 y1 x 0 y0

VA VA VA HA

u3 u2 u1

r4 r3 r2 r1 r0

Abb. 1.18. Additionswerk

Zuerst werden die beiden Einserstellen x0 und y0 addiert, das sind nur zwei
Stellen, also brauche ich dazu nur einen Halbaddierer. Die Ergebnisziffer r0
wird bei den Ergebnissen notiert, der Übertrag u1 wird an die nächste Stelle
weiter gereicht. Für die nächste Addition brauche ich dann auch tatsächlich
einen Volladdierer, denn hier habe ich die beiden Inputziffern x1 , y2 und den
Übertrag u1 der vorherigen Addition zu addieren. Die Ergebnisziffer r1 , die der
Volladdierer liefert, wird wieder bei den Ergebnissen notiert, der Übertrag u2
zur nächsten ziffernweisen Addition weitergegeben. Und so zieht sich das Spiel
durch, ähnlich wie beim Domino, bis das Ende in Gestalt der letzten Stelle
erreicht ist. Hier gibt es natürlich keinen Übertrag mehr, denn es steht keine
weitere Stelle mehr zur Verfügung, mit der gerechnet werden könnte. Ob dann
die Ergebnisziffer r4 tatsächlich zu den Ergebnissen gerechnet wird, hängt
1.5 Logische Schaltungen und Addierer 75

ab von der tatsächlichen Architektur Ihres Rechenwerks: wenn beispielsweise


durchgängig nur mit vier binären Stellen gerechnet wird, haben Sie im Falle
r4 = 1 einen so genannten Überlauf und r4 geht verloren. Man sollte deshalb
immer eine Stelle mehr zur Verfügung haben als man zu brauchen glaubt,
denn für gewöhnlich wird ein Überlauf im Dunkel der Nacht verschwinden.

Addierwerk
Ein Addierwerk setzt sich zusammen aus einem Halbaddierer und mehre-
ren Volladdierern, wobei die Anzahl der Volladdierer von der Anzahl der
gewünschten Stellen der verwendeten binären Zahlen abhängt. Mit ihm ist
es möglich, binäre Zahlen nach den Regeln der binären Arithmetik auf der
Basis der drei logischen Grundschaltungen zu addieren.

Mein Ziel, einen Addierer zu bauen, habe ich jetzt erreicht. Nur eine kleine
Anmerkung noch. Sowohl bei der Konstruktion der XOR-Schaltung als auch
beim Aufbau des Volladdieres haben Sie gesehen, wie man eine Funktionsta-
belle, die die Verteilung der Einsen und Nullen bei den Inputs und Outputs
beschreibt, in ein Schaltbild umsetzen kann. Dieses Verfahren funktioniert
immer, ganz unabhängig vom Aufbau der Tabelle, Sie müssen nur genauso
vorgehen, wie wir das bei den besprochenen Schaltungen gemacht haben. Hat
Ihre Tabelle also einige binäre Inputs x1 , ..., xn und einen binären Output
r, so sollten Sie im ersten Schritt alle Zeilen der Tabelle heraussuchen, die
den Ergebniswert 1 aufzuweisen haben; die anderen interessieren nicht. Dann
sehen Sie in den relevanten Zeilen nach, welche Inputwerte vorhanden sind:
kann ein Input xi mit einer 1 glänzen, so wird er unbesehen übernommen,
ist er dagegen mit einer 0 geschlagen, wird er der Negation unterworfen und
aus xi wird xi . Jede relevante Tabellenzeile erhält anschließend einen logischen
Ausdruck, denn jetzt werden die Inputgrößen durch UND-Verknüpfungen ver-
bunden, wobei Sie natürlich darauf achten müssen, ob xi oder xi zu verwenden
ist. Und zum guten Schluss werden Sie die logischen Ausdrücke, die aus den
einzelnen Zeilen entstanden sind, durch ODER-Verknüpfungen verbinden.
Nehmen wir beispielsweise die Tabelle

x1 x2 x3 r
0 0 0 1
0 0 1 0
0 1 0 0
0 1 1 0
1 0 0 0
1 0 1 1
1 1 0 0
1 1 1 0

mit den drei Inputs x1 , x2 , x3 und dem Output r. Die relevanten Zeilen sind
die erste und die sechste, die entsprechenden logischen Ausdrücke lauten x1 ∧
76 1 Grundlagen

x2 ∧ x3 bzw. x1 ∧ x2 ∧ x3 . Damit ergibt sich für den Output r der Ausdruck


r = (x1 ∧ x2 ∧ x3 ) ∨ (x1 ∧ x2 ∧ x3 ), und schon ist die Sache erledigt.
Sie sehen, dass die resultierenden Ausdrücke immer auf die gleiche Wei-
se aufgebaut sind: sie bestehen aus Klammerausdrücken, die durch ODER-
Schaltungen verknüpft werden, und innerhalb der Klammern gibt es nur Ne-
gationen und UND-Verknüpfungen. Einen Ausdruck dieser Art nennt man
eine disjunktive Normalform; sie entsteht, wenn Sie sich an den Einsen im
Output orientieren. Ich will Ihnen nicht verschweigen, dass man sich auch an
den Nullen orientieren kann und dann eine andere Normalform erhält, die
so genannte konjunktive Normalform. Darum kümmern sich aber die Bücher
über Algebra, Diskrete Mathematik oder auch Digitaltechnik, für unseren Zu-
sammenhang soll die disjunktive Normalform genügen.
Genug geschaltet. Bevor Sie endgültig abschalten und sich der Sportschau
zuwenden, wechsle ich lieber das Thema.

1.5.6 Negative Zahlen

Eine Kleinigkeit noch, dann können wir den Bereich der rechnerinternen
Abläufe verlassen. Bisher habe ich mich in allen Verfahren auf das Rechnen
mit positiven ganzen Zahlen beschränkt. Sie wissen aber so gut wie ich, dass
spätestens auf manchen Kontoauszügen auch negative Zahlen auftauchen, mit
denen man irgendwie umgehen muss. Nun weiß ich nicht, wie Sie auf even-
tuelle negative Beträge auf ihren Auszügen reagieren, aber wie ein Computer
mit negativen Zahlen umgeht, kann ich Ihnen erklären. Sie werden dabei se-
hen, wie wichtig tatsächlich das Zweierkomplement aus dem Abschnitt über
binäre Arithmetik ist und wie praktisch es sein kann, Überläufe einfach zu
ignorieren.
In alten Zeiten hat man es sich manchmal einfach gemacht. Jede Zahl ist
eine Folge von Bits, und wenn sich alle darauf einigen, das vorderste Bit nicht
als Ziffer zu interpretieren, sondern als Vorzeichen, dann ist auch allen gedient.
Man hat also beispielsweise bei Zahlen mit acht Stellen das führende Bit als
Vorzeichen verstanden, eine 1 als Minus, eine 0 als Plus, und die eigentliche
Zahl auf sieben Bits beschränkt. Die Bitfolge 01010101 ist in dieser Darstellung
als +10101012 , also als 85 zu verstehen, während die Bitfolge 11010101 der
Zahl −10101012 , also −85 entspricht. Die größtmögliche ganze Zahl aus acht
Bits ist dann 01111111 = 127, die kleinstmögliche ist 11111111 = −127,
denn die führende 1 beschreibt keine weitere Binärstelle, sondern das negative
Vorzeichen.
So hat man das früher manchmal gemacht, so macht das heute kein Mensch
mehr. Diese Vorgehensweise hat nämlich zwei Nachteile. Erstes kann man hier
die schlichte Zahl 0 auf zwei Arten darstellen, als 00000000 und als 10000000,
wobei ich der Einfachheit halber wieder von Bitfolgen der Länge acht ausgehe.
Was sollte es aber für einen Sinn machen, zwei verschiedene Darstellungen für
so etwas wie eine Null zu haben? Natürlich ist 0 = −0, was durch die verschie-
denen Darstellungsformen zum Ausdruck kommt, aber das ist noch lange kein
1.5 Logische Schaltungen und Addierer 77

Grund, diese simple Tatsache auch noch im Rechenwerk hervorzuheben. Im-


merhin: damit könnte man leben, der zweite Grund ist deutlich gewichtiger.
Erinnern Sie sich noch an das Zweierkomplement? Es spielte eine bedeutende
Rolle bei der binären Subtraktion, und die ganze binäre Arithmetik habe ich
ja nur besprochen, weil der Computer nun mal darauf angewiesen ist. Wenn
man also nach einem Verfahren zur Darstellung negativer Zahlen sucht, dann
wäre es ganz praktisch, diese Darstellung auch ein wenig danach auszurich-
ten, was hinterher mit diesen Zahlen angestellt werden soll. Offenbar haben
aber negative Zahlen etwas mit dem Subtrahieren zu tun, also könnte es nicht
schaden, irgendwie das Zweierkomplement ins Spiel zu bringen.
Auf genau dieser Idee beruht die heute übliche rechnerinterne Darstellung
negativer Zahlen. Ich zeige Ihnen das Prinzip am Beispiel von Bitfolgen der
Länge vier, gehe also davon aus, dass ich meine ganzen Zahlen durch jeweils
vier Bits darstelle. Genau wie bei der eben besprochenen Methode soll das
führende Bit das Vorzeichen beschreiben, die 1 steht für Minus, die 0 steht
für Plus. Die positiven Zahlen fangen also alle mit einer 0 in der vordersten
Stelle an, und danach stehen noch drei Stellen zur Verfügung für die eigentliche
Zahl. So entspricht zum Beispiel die Bitfolge 0110 der Zahl 3 und 0111 der
Zahl 7. Mehr ist im positiven Bereich nicht zu holen, denn die nächste binäre
Zahl wäre 1000, und die hat in der vordersten Stelle leider schon eine 1. So
weit war alles wie gehabt. Die negativen Zahlen stelle ich jetzt nicht mehr
durch einfaches Ändern des Vorzeichenbits dar, sondern durch das vierstellige
Zweierkomplement der entsprechenden positiven Zahl. Falls Sie jetzt etwas
verwirrt blicken, kann ich das verstehen, so ein Satz muss an einem Beispiel
erklärt werden.
Darstellen möchte ich die Zahl −6, und dafür stehen mir vier Bits zur
Verfügung. Die entsprechende positive Zahl lautet 6 mit der vier-Bit-Darstel-
lung 0110. Erinnern Sie sich noch, wie man das Zweierkomplement bestimmt?
Zuerst wandelt man alle Einsen in Nullen um und umgekehrt, und dann ad-
diert man noch eine binäre 1 auf das Resultat. Die Umwandlung ergibt die
Bitfolge 1001, die Addition einer 1 ergibt das Zweierkomplement 1010. Und
schon ist es geschafft, die Bitfolge 1010 ist die rechnerinterne Darstellung der
Zahl −6. Nur um den Unterschied einmal klar zu machen: nach der alten
Methode hätten Sie die Folge 1110 erhalten, es kommt also wirklich etwas
anderes heraus. Es kann nicht schaden, noch ein Beispiel zu sehen, diesmal
auf der Basis von acht binären Stellen. Ich möchte also die Zahl −52 nach
dem Zweierkomplementverfahren binär darstellen. Zunächst entspricht 52 der
Bitfolge 00110100. Das Einserkomplement dieser Bitfolge lautet 11001011,
und durch Hinzuaddieren einer binären 1 finde ich das Zweierkomplement
11001100, womit die Arbeit auch schon getan ist: rechnerintern wird −52
durch die achtstellige Bitfolge 11001100 dargestellt.
Aber worin liegt der Vorteil dieses Verfahrens? Sehen wir uns die beiden
Nachteile des ersten Verfahrens an und überprüfen wir, ob sich etwas gebessert
hat! Zunächst war da die doppelte Darstellung der 0, einmal als 0 und einmal
als −0, und das gibt es jetzt tatsächlich nicht mehr. Um das einzusehen, gehe
78 1 Grundlagen

ich wieder von einer Darstellung durch vier Bits aus, aber das Argument gilt
für jede beliebige Länge. Bei vier Stellen entspricht die 0 der Bitfolge 0000,
das wird keinen überraschen. Das Einserkomplement lautet natürlich 1111,
und jetzt muss ich zur Bestimmung des Zweierkomplements noch eine binäre
1 hinzu addieren. Ich bin aber mit 1111 schon am Ende der mit vier Stel-
len darstellbaren Zahlen angekommen, weiteres Addieren ergibt 10000. Und
hier sehen Sie, wie segensreich sich das Wegfallen der überlaufenden Ziffer
auswirken kann, über die ich im im Zusammenhang mit dem Addierwerk ge-
sprochen hatte. Da mir nun mal nur vier Ziffern zur Verfügung stehen, wird
die führende 1 nicht mehr berücksichtigt; sie kann gar nicht mehr berücksich-
tigt werden, weil jede Zahl einer Bitfolge aus vier Bits entsprechen muss. Also
fällt sie schlicht und einfach weg, und was übrigbleibt, ist die Bitfolge 0000.
Aus 0000 wurde auf diese Weise wieder 0000, zu einer Darstellung auf zwei
Arten ist es nicht gekommen.
Ein Problem ist erledigt. Die Lösung des zweiten Problems ergibt sich fast
von alleine, wie ich Ihnen wieder an einem Beispiel zeigen werde. Wie schon
gewohnt, gehe ich von vierstelligen Bitfolgen aus und möchte die beiden Zahlen
3 und −2 addieren. Die entsprechenden binären Darstellungen lauten 0011,
denn 3 ist positiv, und 1110, denn das Einserkomplement von 0010 lautet
1101, und Addieren von 1 liefert 1110. Jetzt habe ich aber die −2 schon
als Zweierkomplement zur Verfügung, und natürlich ist 3 + (−2) = 3 − 2.
Wie subtrahiert man eine Zahl von der anderen? Richtig, indem man auf
die andere Zahl das Zweierkomplement der einen Zahl addiert. Genau das
kann ich hier tun, ich habe die Bitfolge 0011, die der 3 entspricht, und die
Bitfolge 1110, die der −2 in Zweierkomplementform entspricht, und darf ganz
einfach die beiden Folgen nach den üblichen Regeln addieren. Das ergibt dann
0011 + 1110 = 10001. Wieder muss ich Ihr Gedächtnis bemühen: nach der
Addition des Zweierkomplements habe ich immer die führende 1 gestrichen,
das musste ich von Hand machen. In der Situation des Rechners passiert
das von alleine, denn ich habe hier schon wieder einen Überlauf um eine
Stelle, der nicht berücksichtigt werden kann, also ganz einfach verschwindet.
Die Addition der Zweierkomplementdarstellung der negativen Zahl auf die
positive Zahl führt daher automatisch zum richtigen Ergebnis 0001.
Noch ein Beispiel, damit es deutlicher wird. Ich berechne 6 + (−3) und
verwende wieder vierstellige Bitfolgen. Die 6 hat dann die Darstellung 0110,
die 3 dagegen wird als 0011 geschrieben mit dem Einserkomplement 1100 und
dem Zweierkomplement 1101. Addieren ergibt 0110 + 1101 = 10011, aber
die vorderste Stelle wird von vornherein ignoriert, also habe ich das Ergebnis
0011, das dem dezimalen Ergebnis 3 entspricht.
Indem ich also negative Zahlen durch das Zweierkomplement der entspre-
chenden positiven Zahl darstelle, passt die rechnerinterne Schreibweise ausge-
zeichnet zur binären Arithmetik und man kann problemlos auch mit negativen
Zahlen rechnen. Wir müssen uns nur noch überlegen, welche Zahlenbereiche
mit welcher Stellenzahl abgedeckt werden können. Das ist aber nicht schwer.
Sobald mir n binäre Stellen zur Verfügung stehen, reserviere ich die führende
1.5 Logische Schaltungen und Addierer 79

Stelle für das Vorzeichen und behalte n − 1 Stellen übrig für die eigentliche
Zahl. Die kleinstmögliche nichtnegative Zahl ist natürlich die 00...00, die nur
aus n Nullen besteht und die übliche 0 darstellt. Die größtmögliche lautet
011...11, sie beginnt mit einer 0 für das positive Vorzeichen, der n − 1 Einsen
nachfolgen. Würde man jetzt noch spaßeshalber eine binäre 1 addieren, dann
ergäbe das wegen der Überträge 100...00, eine führende 1 mit n − 1 Nullen,
und das ist nach den Regeln der Binärzahlen genau 2n−1 . Da dieses 2n−1 aber
um 1 größer ist als meine größtmögliche positive Zahl, kann ich im positiven
Bereich höchstens die Zahl 2n−1 − 1 verarbeiten.
Ein klein wenig anders sieht es bei den negativen Zahlen aus. Das Zwei-
erkomplement von 00...001 lautet 11...111, also entspricht −1 der Bitfolge
11...111. Das Zweierkomplement von 00...010 lautet 11...110, also entspricht
−2 der Bitfolge 11...110. Sie sehen, dass ich beim Übergang von −1 zu −2 in
der Bitfolge gerade 1 abziehen musste, und genauso geht das auch weiter: der
−3 entspricht die Folge 11...101, der −4 die Folge 11...100 und so weiter. Und
wo hört das auf? Das Zweierkomplement der größten darstellbaren positiven
Zahl 011...111 ist 100...001, weshalb −(2n−1 − 1) durch die Bitfolge 100...001
dargestellt wird. Aber offenbar habe ich hier noch eine Zahl in Reserve: ich
habe angefangen bei 11...111 und habe mich herunter gehangelt bis 10...001
Einen Schritt kann ich jetzt noch machen, und zwar den auf 10...000, eine
führende 1 mit n − 1 Nullen. Und da wir, dezimal gerechnet, bei der −1 ge-
startet waren und hinab gestiegen sind bis zur −(2n−1 − 1), entspricht diese
letzte Bitfolge 10...000 der nächstkleineren Dezimalzahl −2n−1 . Man kann da-
her mit der Zweierkomplementmethode durch n binäre Stellen die negativen
Zahlen bis zu −2n−1 darstellen. Insgesamt heißt das: mit n binären Stellen
kann man die ganzen Zahlen m mit der Eigenschaft −2n−1 ≤ m ≤ 2n−1 − 1
im Rechner abbilden. Haben Sie also zum Beispiel acht gültige Binärstellen,
so existieren für Ihren Computer die ganzen Zahlen zwischen −128 und 127.
Geht man über zu zwei Bytes oder sechzehn Bits, so stehen schon die Zahlen
zwischen −215 und 215 − 1 zur Verfügung, also alles zwischen −32768 und
32767.

Darstellung ganzer Zahlen mit Vorzeichen


Stehen zur rechnerinternen Darstellung ganzer Zahlen n Stellen zur Verfü-
gung, so stellt man eine positive Zahl dar, indem man an die führende Stelle
eine 0 setzt und mit den restlichen n − 1 Stellen die Binärdarstellung der Zahl
realisiert. Eine negative Zahl stellt man durch das Zweierkomplement der
entsprechenden positiven Zahl dar. Diese Methode erlaubt die Anwendung
der binären Arithmetik auch für negative Zahlen.
Mit n Stellen kann man auf diese Weise die ganzen Zahlen m mit der
Eigenschaft −2n−1 ≤ m ≤ 2n−1 − 1 im Rechner abbilden.

Über das Innenleben des Rechners habe ich jetzt lange genug gesprochen,
es wird Zeit, das Grundlagenkapitel zu beenden. Im nächsten Kapitel werde
80 1 Grundlagen

ich Ihnen einiges über Algorithmen und ihre Umsetzung in der Programmier-
sprache C berichten.

Übungen
1.20. Weisen Sie mit Hilfe von Funktionstabellen die Beziehungen A ∧ B =
A ∨ B, A ∨ B = A ∧ B und A = A nach.
1.21. Untersuchen Sie, ob es möglich ist, mit nur zwei logischen Grundschal-
tungen auszukommen anstatt, wie dargestellt, mit drei Grundschaltungen.
1.22. Stellen Sie für r = (x ⊕ y) ⊕ z eine Funktionstabelle auf. Vergleichen
Sie die Ergebnisse mit denen der Ergebnisziffer beim Volladdierer. Wie kann
man mit Hilfe von (x ⊕ y) ⊕ z das Schaltbild des Volladdierers vereinfachen?
1.23. Setzen Sie die Schaltung aus Abbildung 1.19 in eine Tabelle um.

x y z

&
1
&
r
&

Abb. 1.19. Schaltung

1.24. Setzen Sie die folgende Tabelle mit den Inputs x, y und dem Output r
in eine logische Schaltung um.
x y r
0 0 1
0 1 0
1 0 0
1 1 1
Wie hängt diese Schaltung mit der XOR-Schaltung zusammen?
2
Strukturierte Programmierung mit C

Leben heißt immer, Entscheidungen treffen“



Jean Luc Picard, Captain der Enterprise

Klaus Kinski hat einmal in einem Interview gesagt, Sprache sei das Gefähr-
lichste, was es überhaupt gebe. Obwohl das eine ziemlich gewagte Behauptung
ist, die andere Gefahrenquellen wie Lawinen, Waldbrände oder Finanzämter
stark vernachlässigt, ist doch etwas Wahres daran. Egal welche Sprache Sie
einsetzen, die Gefahr von Missverständnissen lauert hinter jedem Satz. Sie
können beispielsweise mit einem aus zwei Worten bestehenden Satz einem
Chinesen sagen, dass Sie ihn gerne etwas fragen würden. Sprechen Sie aber
den gleichen Satz aus und betonen nur einen einzigen Vokal ein klein wenig
anders, dann haben Sie auf einmal gefragt, ob Sie ihn küssen dürfen, und je
nach Laune und Statur Ihres chinesischen Gegenübers kann seine Reaktion
die Wahrheit von Kinskis Meinung deutlich illustrieren.
So etwas passiert aber nicht nur bei ostaiatischen Sprachen, es kommt
auch in unseren Breiten vor, sowohl beim Sprechen als auch beim Program-
mieren. Als ich vor langer Zeit Programmierer bei einer großen Softwarefirma
mit drei Buchstaben war, hatte ich auch gelegentlich mit Kunden zu tun, die
die von der Firma entwickelte Software einsetzen wollten. Natürlich kann es -
gerade bei großen Systemen - vorkommen, dass auch fehlerhafte Programme
an die Kunden ausgeliefert werden, und genau das hatte man sich bei diesem
Kunden geleistet. War ja auch nicht so schlimm, schließlich gab es einen te-
lefonischen Notdienst, der leider an diesem Tag gerade von mir besetzt war.
Ich erklärte dem Kunden also am Telefon, welche Zeile er in das Programm
einfügen musste, damit es klaglos funktionierte; es war wirklich nur eine Zeile,
die einen so genannten else-Zweig regelte. Was immer das auch sein mag, in
jedem Fall ist else“ ein englisches Wort mit der deutschen Bedeutung an-
” ”
sonsten“, und der Kunde sollte einfach das, was ich ihm diktierte, wörtlich
in sein Programm hineinschreiben. Das hat er auch gemacht. Anschließend
versuchte er das Ganze zum Laufen zu bringen und verkündete mir verzwei-
felt, es gebe eine ganz seltsame Fehlermeldung, das Wort else“ werde nicht

verstanden. Nun war die Verzweiflung ganz auf meiner Seite, denn wenn eine
Programmiersprache das Wort else“ vorsieht, dann sollte dieses Wort, an der

richtigen Stelle platziert, zu keinem Fehler führen. Hatte er es denn wirklich
82 2 Strukturierte Programmierung mit C

genau an die Stelle geschrieben, wo es hingehörte? Es las mir noch einmal vor,
alles in Ordnung. So ging es eine ganze Weile hin und her, ich wusste keinen
Rat mehr, bis dem Kunden auf einmal die Erleuchtung kam: Ja, hätte ich

vielleicht else hinten mit e schreiben müssen?“
Ein Programmierer. Ein Chefprogrammierer sogar, der nicht wusste, wie
man ein unzählige Male benutztes Wort einer Programmiersprache schreibt,
und dem man im konstant freundlichen Ton erklären musste, dass es eben
nicht els, sondern else heißt. Das war kein reines Vergnügen, aber vor allem
zeigt es, wie wichtig es ist, gerade beim Programmieren sprachlich präzise zu
bleiben. Genau wie in der chinesischen Sprache kann ein falsch - oder vielleicht
gar nicht - gesetzter Vokal, ein auf bestimmte Weise falsch geschriebenes Wort
zum Zusammenbruch oder auch zu falschen Resultaten führen. Damit Ihnen
so etwas nicht passiert, werde ich mich in diesem und dem nächsten Kapitel
mit zwei Programmiersprachen befassen. Dieses zweite Kapitel ist erst mal
der Sprache C gewidmet, aber nicht nur: ich will Ihnen hier einerseits eine
Einführung in das Programmieren mit C geben und Sie andererseits ein we-
nig in das algorithmische Denken einführen. Schließlich nützt es Ihnen rein
gar nichts, wenn Sie genau wissen, wie man in C welche Worte schreibt und
sie aneinandersetzen darf, Sie aber keine Ahnung haben, mit welchen Metho-
den man bestimmte Probleme löst. Das ist wie in jeder anderen Sprache auch;
Rechtschreibung ist wichtig, Grammatik auch, aber sie sind nur Mittel zum
Zweck, und der Zweck sind die Inhalte, die Sie zum Ausdruck bringen wollen.
Deshalb beginne ich dieses Kapitel mit ein paar grundsätzlichen Bemerkun-
gen über Algorithmen und werde dann mit Ihnen die ersten Schritte mit der
Sprache C unternehmen. Für eine Weile werden wir uns mit den üblichen
Kontrollstrukturen Sequenz, Auswahl und Wiederholung beschäftigen, wor-
aufhin ich versuchen werde, Ihnen Zeiger, verkettete Listen und Funktionen
nahe zu bringen. Auch einige Worte zur Dateiverarbeitung werden nicht feh-
len, und unterwegs werden Ihnen als graphische Beschreibungsmittel immer
wieder Struktogramme und Programmablaufpläne begegnen.

2.1 Einführung

Es steht außer Frage, dass die elektronische Datenverarbeitung mit Hilfe so


genannter Programme erfolgt, darüber hatten wir schon gelegentlich gespro-
chen. Dennoch ist das nur die halbe Wahrheit. Mit Hilfe eines Programms
bilden Sie im Computer etwas ab, was Sie oder vielleicht auch Ihre Kollegen
sich vorher ausgedacht haben, ein Verfahren, das irgendetwas bewirken soll.
Ohne Verfahren kein Programm, denn ohne Verfahren wüssten Sie überhaupt
nicht, was Sie programmieren sollten. Und da Verfahren zu einfach klingt, hat
man sich die Bezeichnung Algorithmus ausgedacht.
2.1 Einführung 83

2.1.1 Algorithmen

Was ist ein Algorithmus? Die Formulierungen gehen hier auseinander, mach-
mal auch die Meinungen, aber das ist für unsere Zwecke nicht weiter schlimm.
Wir hatten uns darauf geeinigt, dass ein Algorithmus das Verfahren ist, das
dem konkreten Programm zugrunde liegt, und die Programme werden in
aller Regel auf einem Computer ausgeführt. Also dient der Computer der
Ausführung von Algorithmen, wobei das Programm die Rolle des Vermittlers
zwischen dem abstrakten Algorithmus und dem konkreten Computer spielt:
man kann dem Rechner nicht auf Deutsch erklären, was er jetzt zu tun hat,
dazu braucht man eine klar definierte und auf die Bedürfnisse der Maschi-
ne abgestimmte Sprache. Wenn Sie aber eine Verfahrensanleitung, also den
Algorithmus, übersetzen können sollen in die Anweisungen einer Program-
miersprache, dann darf dieser Algorithmus sich nicht in den Gefilden philo-
sophischer Großzügigkeit bewegen, sondern muss klar und exakt sein. Das ist
nicht alles. Kein Computer der Welt kann unendlich lange laufen, also darf
kein Algorithmus der Welt unendlich lang sein. Und das heißt, dass sowohl
die Anzahl der Verfahrensvorschriften als auch die Anzahl der konkret durch-
zuführenden Einzelschritte nicht unendlich groß werden darf. Sie dürfen also
keine Anweisung der Form Solange 1 = 1 ist, gib die Zahl 17 am Bildschirm

aus“ in Ihren Algorithmus schreiben, denn damit hätten Sie zwar nur eine
einzige Verfahrensvorschrift, diese einzige Verfahrensvorschrift müsste jedoch
unendlich oft durchgeführt werden, und das ist offenbar nicht möglich.
Fast haben wir es schon, nur noch eine Kleinigkeit fehlt. Wie kommt man
auf die Idee, sich einen Algorithmus zu überlegen? Sicher nicht aus heiterem
Himmel, weil heute Mittwoch oder gar Donnerstag ist. Ein Algorithmus dient
immer der Lösung eines Problems, einer Aufgabe, sonst wäre er sinnlos. Sobald
Sie also ein Problem haben und dieses Problem mit Hilfe endlich vieler klarer
Verfahrensvorschriften lösen wollen, die sich in endlich vielen Einzelschritten
durchführen lassen, können Sie mit Fug und Recht von einem Algorithmus
sprechen.

Algorithmus
Ein Algorithmus ist eine endliche Menge von eindeutig festgelegten Verfah-
rensvorschriften zur Lösung eines Problems durch eine in der Regel endliche
Menge von Einzelschritten.

Man kann die Auffassung vertreten, die Menge der Einzelschritte dürfte
auch unendlich groß werden; es gibt auch mathematische Verfahren, die ei-
ne unendliche Anzahl von Wiederholungen verlangen und übereinstimmend
als Algorithmen bezeichnet werden. Sobald Sie solche Verfahren aber kon-
kret anwenden wollen, werden Sie gezwungen sein, nach einer gewissen Zeit
aufzuhören - möglichst noch vor dem Ende des Universums, weil sonst Ihre
berechneten Ergebnisse zu nichts mehr zu gebrauchen sind und somit auch
kein Problem lösen können. Für uns hat ein Algorithmus deshalb in jeder
84 2 Strukturierte Programmierung mit C

Hinsicht endlich zu sein, auch wenn das in der Mathematik manchmal anders
aussieht.
Übrigens klingt das Wort Algorithmus zwar sehr griechisch, kommt aber
aus der arabischen Sprache und lehnt sich an den Namen des arabischen Ma-
thematikers und Astronomen Mohamed ibn Musa al-Hwarizmi aus dem neun-
ten Jahrhundert an, der zwar sicher keine Computerprogramme geschrieben,
aber immerhin als erster arabischer Mathematiker das Ihnen vertraute De-
zimalsystem benutzt und systematisch beschrieben hat. Es handelt sich also
um ein reines Kunstwort, und das ist auch in Ordnung, denn schließlich ist
ein Algorithmus auch etwas Künstliches.

2.1.2 Programmiersprachen

Führt man nun einen Algorithmus aus, so spricht man von einem Prozess, und
der Ausführende ist der Prozessor. Wer dabei die Rolle des Prozessors spielt,
hängt ganz vom Einzelfall ab. Soll zum Beispiel meine Tochter zwei natürli-
che Zahlen auf dem Papier addieren, dann ist sie der Prozessor. Soll dagegen
eine Addition auf dem Computer vorgenommen werden, wird man je nach
Betrachtungsweise den Computer oder seine Zentraleinheit oder gar nur das
Rechenwerk als den Prozessor bezeichnen - in der Regel natürlich die schon im
letzten Kapitel erwähnte CPU. Beim Ausführen eines etwas komplizierteren
Algorithmus zum Sortieren einer großen Menge von Datensätzen kann Ihnen
kein menschlicher Prozessor mehr die Ausführung des Algorithmus abneh-
men, dieser Prozess muss auf einer Maschine laufen. Es dürfte klar sein, dass
wir uns im Folgenden nur mit Algorithmen befassen werden, die als Prozes-
sor einen Computer verwenden. Immerhin gibt es ja auch viele Algorithmen,
die nur mit Hilfe eines Computers sinnvoll zur Ausführung gebracht werden
können. Über die Gründe habe ich mich schon am Anfang des ersten Kapitels
ausgelassen, als es um die Vorteile der maschinellen Datenverarbeitung ging:
der Computer hat eine ausgesprochen hohe Verarbeitungsgeschwindigkeit, er
ist in aller Regel wesentlich zuverlässiger als ein menschlicher Prozessor, seine
Speicherkapazität ist gerade bei der Verarbeitung von Massendaten eher an-
gemessen als ein menschliches Kurzzeitgedächtnis, und selbstverständlich ist
er auch oft genug die kostengünstigste Lösung.
Nun entsteht aber, wenn man maschinell ausführbare Algorithmen haben
möchte, ein kleines Problem. Der Prozessor muss nämlich den Algorithmus
in irgendeiner Weise interpretieren können, und dazu gehören immer zwei
Dinge: erstens muss er überhaupt einmal jeden Schritt des Algorithmus ver-
stehen, und zweitens muss er auch in der Lage sein, jede verlangte Opera-
tion auszuführen. Wie schon erwähnt, können Sie dem Computer nicht ein-
fach erzählen, was er machen soll, auch wenn das Captain Jean Luc Picard
auf der Brücke des Raumschiffs Enterprise immer wieder mit gekonnter Ele-
ganz vorführt, Sie müssen Ihren Algorithmus in eine Form bringen, die der
Computer versteht. Und damit sind wir bei der Programmierung und den
Programmiersprachen. Der Algorithmus selbst ist zwar völlig unabhängig von
2.1 Einführung 85

einer verwendeten Programmiersprache, um ihn aber dem Computer verständ-


lich zu machen, braucht man eine Formulierung in einer Programmiersprache,
möglichst auch noch in einer, die der verwendete Computer versteht. Es wird
Ihnen also nichts anderes übrig bleiben, als Ihren Algorithmus in einer Pro-
grammiersprache als Programm zu formulieren und dann dem Computer zur
Kenntnis zu geben.
Welche Programiersprache Sie dabei verwenden, hängt von Ihrem Ge-
schmack und von der Sachlage ab. Zunächst einmal unterscheidet man zwi-
schen zwei Klassen von Programmiersprachen: den maschinennahen Program-
miersprachen und den höheren Programmiersprachen. Warum maschinennahe
Sprachen so heißen, wie sie heißen, ist leicht zu verstehen. Jede Anweisung ei-
ner maschinennahen Sprache kann entweder direkt von der CPU als ein Befehl
oder doch wenigstens als eine sehr kleine Folge von Befehlen an die Maschine
verstanden werden. Deshalb können die in einer maschinennahen Sprache er-
laubten Anweisungen auch nur sehr einfach sein, sonst wäre die CPU mit der
direkten Umsetzung schlicht überfordert. Was aber der CPU die Sache leicht
macht, erschwert sie dem Programmierer: einen komplizierten Algorithmus
mit kleinen und einfachen Anweisungen umzusetzen, führt offenbar zu langen
und unübersichtlichen Programmen. Aus diesem Grund hat man die höheren
Programmiersprachen erfunden, die es dem Programmierer leicht machen und
der CPU etwas schwerer. Sie sind in aller Regel an einem sehr einfachen Eng-
lisch orientiert, was die Verständlichkeit für einen menschlichen Leser deutlich
erhöht, und vor allem fassen sie in ihren Befehlen etliche kleine maschinennahe
Befehle zusammen. Anders gesagt: was Sie in einer maschinennahen Sprache
erst mühsam von Hand zusammenprogrammieren müssen, wird Ihnen von ei-
ner höheren Sprache als fertiges Paket frei Haus geliefert. Natürlich macht das
die Programmierung einfacher und reduziert die Fehleranfälligkeit. Standard-
beispiele für maschinennahe Sprachen sind die Assembler-Sprachen, und auch
an höheren Programmiersprachen herrscht kein Mangel, ich nenne hier nur
FORTRAN, COBOL, Basic, Pascal, C und Java, ohne jeden Anspruch auf
Vollständigkeit. Zwei davon werde ich hier besprechen, nämlich C und Java.

Programmiersprachen
Algorithmen, die auf einem Computer ausgeführt werden sollen, müssen mit
Hilfe einer Programmiersprache als Programme geschrieben werden. Man un-
terscheidet zwischen den maschinennahen Programmiersprachen, bei denen
jede Anweisung direkt entweder in einen einzigen maschinenverständlichen
Befehl oder in eine kleine Folge solcher Befehle umgesetzt werden kann, und
den höheren Programmiersprachen, die stärker an den Bedürfnissen des Pro-
grammierers ausgerichtet sind und meistens auf einem einfachen englischen
Vokabular beruhen.

Denken Sie immer daran: der Algorithmus, Ihre Verfahrensbeschreibung


ist die Grundlage der Verarbeitung, das Programmieren setzt die Existenz
eines Algorithmus voraus, der in eine Programmiersprache umgesetzt wer-
86 2 Strukturierte Programmierung mit C

den soll. Wenn Sie also ein Problem mit Hilfe eines Programms lösen wollen,
dann ist es immer zu empfehlen, in drei Schritten vorzugehen. Zuerst soll-
ten Sie sich darüber klar werden, um welches Problem es eigentlich geht. Das
klingt wesentlich trivialer als es ist, erinnern Sie sich nur einmal an die Ge-
schichte über den unnötigen Computer, den John von Neumann verhindert
hat. Oft genug wird wild in die Gegend hinein programmiert, ohne dass sich
jemand Rechenschaft darüber ablegt, welche Ziele genau erreicht, welche kon-
kreten Probleme gelöst werden sollen; deshalb ist die Problemdefinition, in
der das anzugehende Problem so genau wie möglich beschrieben wird, von
großer Bedeutung. Ist das Problem einmal definiert, so können Sie daran ge-
hen, einen Algorithmus zur Lösung des Problems zu entwerfen - man spricht
dann vom Programmentwurf. Darin wird also noch nicht das konkrete Pro-
gramm geschrieben, sondern es werden die Verfahrensschritte festgelegt, die
anschließend in der Programmierung in eine Programmiersprache umgesetzt
werden.
Zugegeben: bei kleinen Problemen kann man die ersten beiden Teile dieses
Ablaufs, also die Problemdefinition und den Programmentwurf, mehr oder
weniger im Kopf erledigen, aber darüber nachdenken sollte man auf jeden
Fall, und ich werde auch bei den kleinen Programmen, die wir uns hier anse-
hen, immer wieder darauf zurückkommen. Bei großen Systemen ist allerdings
ein strukturiertes Vorgehen unverzichtbar, wenn man nicht ganz schnell in
gewaltige Schwierigkeiten kommen will; das Fach Software Engineering ist die
Antwort der Informatiker auf das wilde Programmieren der informatischen
Frühzeit.
Eine kleine Einschränkung muss ich noch machen. Im Prinzip ist tatsäch-
lich der Algorithmus unabhängig von der Programmiersprache, in die er an-
schließend umgesetzt wird, oder er sollte es wenigstens sein. Wie die mei-
sten hohen Prinzipien ist allerdings auch dieses Unabhängigkeitsprinzip nicht
vollständig und konsequent durchführbar. Verschiedene Programmiersprachen
sind nun mal auf verschiedene Anwendungsgebiete zugeschnitten, und das
kann ein Algorithmus nicht einfach ignorieren. Wie ich schon im ersten Kapi-
tel berichtet hatte, ist die Sprache COBOL für betriebswirtschaftliche Anwen-
dungen gedacht, während FORTRAN einen naturwissenschaftlich-technischen
Hintergrund hat. Niemand käme auf die Idee, eineSatellitenflugbahn mit ei-
nem COBOL-Programm berechnen zu lassen, und genauso unsinnig wäre es,
ein System zur Lohnbuchhaltung in FORTRAN programmieren zu wollen.
Da Sie aber in der Regel schon beim Programmentwurf, wenn es erst mal
um den reinen Algorithmus geht, schon wissen, in welcher Programmierspra-
che Ihr Algorithmus umgesetzt werden soll, liegt es nahe, auf die Eigenheiten
der Programmiersprache Rücksicht zu nehmen. Sie werden beispielsweise im
Laufe dieses Kapitels lernen, was ein Zeiger ist und wie man mit so etwas
in C umgeht. COBOL dagegen hat keine Zeiger. Würden Sie in einem Algo-
rithmus, der für COBOL gedacht ist, irgendwelche Zeiger verwenden? Was
immer ein Zeiger auch sein mag, sein Einsatz in einem Algorithmus mit der
Zielsprache COBOL wäre völlig sinnlos, weil Sie dann Ihren mühevoll er-
2.1 Einführung 87

stellten Algorithmus überhaupt nicht in der gewählten Programmiersprache


formulieren könnten. Welche Methoden und Verfahren Sie also in einem Al-
gorithmus einsetzen, wird davon abhängen, welche Methoden und Verfahren
von der gewählten Programmiersprache unterstützt werden.

Programmerstellung
Bei der Erstellung eines Programms zur Lösung eines Problems sollte man
schrittweise vorgehen. Eine mögliche Einteilung sieht drei Schritte vor:
• Problemdefinition,
• Programmentwurf,
• Programmierung.
Der im Programmentwurf entwickelte Algorithmus sollte sich an den Gege-
benheiten der für die Programmierung verwendeten Programmiersprache ori-
entieren, damit er in dieser Sprache realisiert werden kann.

2.1.3 Software

Ein Computer ist eine konkrete Maschine, die aus einer Menge verschiedener
Einzelteile besteht, aus physisch vorhandenen Komponenten. Die Gesamtheit
dieser technischen Geräte und Einrichtungen bezeichnet man gerne als Hard-
ware, ein englisches Wort für Eisenwaren. Das kann aber nicht alles sein. Wir
hatten uns gerade darauf geeinigt, dass ein Computer neben seiner Funktion
als Staubfänger vor allem der Ausführung von Algorithmen dient, indem er
als Prozessor für Programme zur Verfügung steht. Ein Algorithmus ist kein
physikalischer Gegenstand. Man kann zwar das zugehörige Programm auf eine
Festplatte speichern, aber die zur Speicherung verwendeten Festplattensekto-
ren wird kaum jemand mit dem Programm gleichsetzen wollen. Algorithmen
und Programme sind und bleiben etwas Abstraktes, eine geistige Konstruk-
tion, und ganz sicher keine Eisenwaren. Aus diesem Grund hat sich für die in
einem Computer einsetzbaren Programme die Bezeichnung Software durch-
gesetzt, um den Gegensatz zur Hardware zu dokumentieren und deutlich zu
machen, dass zum Betrieb eines Computers eben etwas mehr nötig ist als die
anfassbaren Komponenten.
Nun gibt es aber verschiedene Arten von Software, und man neigt dazu,
sie in zwei Klassen zu unterscheiden: auf der einen Seite haben Sie die System-
software, auf der anderen die Anwendungssoftware. Ohne ein gewisses Maß an
Systemsoftware kann kein Rechner existieren. Irgend jemand muss sich darum
kümmern, dass sich Benutzer anmelden können und dass Programme, wie Sie
es bei der Beschreibung der Computerarchitektur gesehen haben, geladen und
ausgeführt werden können. Man braucht eine Software, die die Verwaltung der
Dateien organisiert, die dafür sorgt, dass die Steuerung der Prozesse reibungs-
los abläuft und sich mehrere Prozesse nicht gegenseitig stören - all das erledigt
das Betriebssystem, es verwaltet die Ressourcen ihres Rechners und erspart es
88 2 Strukturierte Programmierung mit C

Ihnen, sich selbst direkt mit der Hardware herumschlagen zu müssen. Ob das
nun ein Windows-Dialekt oder Linux oder sonst etwas ist: die grundsätzlichen
Aufgaben des Betriebssystems sind immer gleich, und kein Computerbenutzer
kann darauf verzichten.
Das Betriebssystem ist wohl der wichtigste Teil der Systemsoftware, aber
nicht der einzige. Dazu gehören auch noch Dienstprogramme und Werkzeu-
ge, die dem Benutzer und vor allem dem Entwickler von Software das Leben
erleichtern sollen. Sie werden gleich C-Programme schreiben müssen - womit
eigentlich? Dazu brauchen Sie irgendeinen Editor, mit dessen Hilfe Sie eine
Datei erstellen können, Sie werden auch einen so genannten Compiler benöti-
gen, der Ihr Programm überhaupt erst in eine für den Computer verständliche
Form bringt, und vielleicht legen Sie auch Wert auf einen Internetbrowser,
damit Sie im Internet nach den möglichen Ursachen für Fehler in Ihren Pro-
grammen suchen können. All das gehört nicht mehr zum Betriebssystem, aber
auf jeden Fall zur Systemsoftware, denn Programme dieser Art erlauben es,
mit Ihrem System zu arbeiten.
Die Systemsoftware ist also die Voraussetzung, ohne die nichts geht. Und
was soll mit ihrer Hilfe eigentlich gehen? Natürlich die Programmierung, auf
die will ich ja hier hinaus. Und die Programme, die Sie erstellen, die Pro-
gramme, die Sie schreiben, damit sie jemand anwendet, fallen unter den Be-
griff Anwendungssoftware. Ob das nun ein Abrechnungsprogramm ist oder
ein Programm zur Berechnung von Wettervorhersagen oder sonst irgendeine
Anwendung: es handelt sich in jedem Fall um Anwendungssoftware.

Softwarearten
Unter Software versteht man die Menge der Programme, die auf einem Com-
puter eingesetzt werden können. Man unterteilt sie in Systemsoftware und
Anwendungssoftware, wobei die Systemsoftware noch einmal unterteilt wird
in das Betriebssystem sowie systemnahe Dienstprogramme und Werkzeuge.

Sie konnten in dem Abschnitt über die historische Entwicklung der Compu-
ter sehen, dass man Software und natürlich auch Anwendungssoftware schon
seit langer Zeit entwickelt. Was man heute darunter versteht, konnte natürlich
erst mit der Erfindung der Programmiersprachen seinen Anfang nehmen; seit
es FORTRAN gibt, haben die Software-Entwickler munter vor sich hin pro-
grammiert. Das führte aber zu Problemen, weil jeder seinen eigenen Stil ent-
wickelte und sich unter Software-Entwicklern damals wie heute ein leichter
Hang zu einer Nach mir die Sintflut“-Mentalität ausbildete: solange der Ent-

wickler sein Programm verstand, war alles in Ordnung, die Nachwelt hat kei-
nen interessiert. Insbesondere wurden mit Begeisterung wilde Sprünge in Pro-
grammen ausgeführt, die dazu führten, dass ein Programm eben noch einen
bestimmten Befehl ausführte und sich dann infolge eines Sprungbefehls plötz-
lich an einer völlig anderen Stelle befand. Es erinnerte, falls die Sprungbe-
reitschaft stark ausgeprägt war, streckenweise an manche Gesprächspartner,
die einen Satz anfangen, sich mitten im Satz unterbrechen, dann den unter-
2.1 Einführung 89

brechenden Satz wieder durch einen anderen Einschub zerhacken und dieses
Spiel so lange betreiben, bis nicht mehr die geringste Hoffnung besteht, zu
dem eigentlich angefangenen Satz zurückzufinden.
Sie können sich vorstellen, dass Programme dieser Art unter Umständen
leichte Qualitätsprobleme aufwiesen, weshalb man sich ab den späten sechziger
Jahren bemüht hat, zu einem besseren Stil zu finden. Nach einem grundle-
genden Artikel des holländischen Informatikers Edsger Dijkstra, in dem er zu
recht die Sprunganweisung verdammte, ging man langsam über zum Prinzip
der strukturierten Programmierung, über die ich auch in diesem Kapitel vor
allem reden will. Ihr Grundgedanke ist einfach genug: um Himmels Willen
niemals einen Sprungbefehl verwenden und alle Programmanweisungen schön
der Reihe nach erledigen. Eine Anweisung in einem Programm kann man nur
erreichen, wenn man die vorherige Anweisung erledigt hat, und sobald man
mit einer Anweisung fertig ist, geht man zur nächsten über, ohne wie ein
Känguruh durch den Programmtext zu hüpfen. Da es aber manchmal - und
sogar ziemlich häufig - nötig ist, einem Programm eine gewissen Flexinilität zu
erlauben, wurden in der strukturierten Programmierung bestimmte Kontroll-
strukturen eingeführt, mit deren Hilfe man die Abarbeitungsreihenfolge der
Programmanweisungen beeinflussen kann. Was man darunter im Einzelnen
versteht, werden Sie gleich sehen, wenn es um konkrete C-Programme geht;
für den Moment will ich nur sagen, dass man in der strukturierten Program-
mierung die Kontrollstrukturen Sequenz, Auswahl und Wiederholung kennt.
Sie werden Ihnen ab jetzt immer wieder begegnen.

Strukturierte Programmierung
Ein Programm entspricht dann den Regeln der strukturierten Programmie-
rung, wenn es keinerlei Sprunganweisungen enthält und die Steuerung des
Programmablaufs mit Hilfe der Kontrollstrukturen Sequenz, Auswahl und
Wiederholung erfolgt.

Genug der grauen Theorie. Im nächsten Abschnitt sehen wir uns die ersten
C-Programme an.

Übungen

2.1. Untersuchen Sie die Wirkungsweise des folgenden Algorithmus, der mit
einer ganzen Zahl a arbeitet. Welchen Wert hat die Zahl a nach der Abarbei-
tung des Algorithmus? Könnte man den Algorithmus auch einfacher formu-
lieren?
Falls a > 0 ist
dann gehe folgendermaßen vor:
Falls a < 0 ist, setze a = 1
ansonsten setze a = 2
90 2 Strukturierte Programmierung mit C

Beachten Sie, dass die eingerückten Zeilen eine Gesamtheit bilden, sodass
also im Fall a > 0 beide Anweisungen nach dem Doppelpunkt ausgeführt
werden müssen.

2.2. Gegeben seien die folgenden beiden Algorithmen zur Beschreibung des
Verhaltens eines Autofahrers an einer Straßenkreuzung. Wie interpretieren
Sie die verschiedenen Einrückungen? In welcher Situation unterscheiden sich
Algorithmus 1 und Algorithmus 2? Welcher Algorithmus erscheint Ihnen
zweckmäßiger?
Algorithmus 1:
Falls die Ampelanlage funktioniert
dann gehe folgendermaßen vor:
falls die Ampel rot oder gelb anzeigt
dann bleibe stehen
ansonsten fahre

Algorithmus 2:
Falls die Ampelanlage funktioniert
dann gehe folgendermaßen vor:
falls die Ampel rot oder gelb anzeigt
dann bleibe stehen
ansonsten fahre

2.3. Das folgende Programm in einer nicht existierenden Programmiersprache


enthält mehrere Sprungbefehle, die sich auf die jeweils voran gestellten Zei-
lennummern beziehen. Stellen Sie fest, zu welchem Problem dieses Programm
führt.
01 x=1
02 y=2
03 gehe zu Zeile 07
04 berechne 2x
05 berechne x+y
06 gehe zu Zeile 03
07 gehe zu Zeile 04

2.2 Erste C-Programme

Die Programmiersprache C dient der strukturierten Programmierung, nicht


mehr und nicht weniger. Als man C entwickelt hat, dachte noch kein Mensch
an so etwas wie Objektorientierung, und deshalb ist C von der objektorien-
tierten Programmierung meilenweit entfernt. Das ist kein Nachteil für eine
Programmiersprache, sofern sie für Aufgaben gedacht ist, die man mit struk-
turierter Programmierung erledigen kann und die nichts weniger brauchen als
2.2 Erste C-Programme 91

irgendwelche Objekte - solche Aufgaben gibt es noch immer massenweise, wes-


halb auch nicht zu erwarten ist, dass das klassische C bald sein seliges Ende
erreicht und unwiderruflich den objektorientierten Sprachen weichen muss.

2.2.1 Die Entwicklung von C

Wie es zur Entwicklung der Programmiersprache C kam, ist schnell erzählt.


Ab 1969 arbeiteten Ken Thomson und Dennis Ritchie in den recht berühm-
ten AT&T-Bell-Laboratories ein neues Betriebssystem mit dem Namen UN-
IX, von dem man auch heute noch hin und wieder etwas hört. Ein Werkzeug
dabei war die bereits vorhandene Programmiersprache BCPL, die Thomson
weiter entwickelte und mit dem sprechenden Namen B versah. Dabei han-
delte es sich um ein reines Werkzeug zur Systemprogrammierung, das - grob
gesprochen - nicht zwischen einer ganzen Zahl und einer Nähmaschine unter-
scheiden konnte; B kannte nichts anderes als so genannte Maschinenworte, die
man zur Systemprogrammierung brauchte, und für eine Programmiersprache,
mit der man vielleicht auch Anwendungssoftware erstellen wollte, war das kein
Zustand. Dass B zusätzlich noch sehr große Nähe zu Assembler aufwies und
daher nicht übermäßig benutzerfreundlich war, machte die Sache nicht besser.
Thomson machte sich daher an eine Weiterentwicklung von B, die er kon-
sequenterweise C nannte. Mit C war es dann möglich, sich an die Prinzipien
der strukturierten Programmierung zu halten, und darüber hinaus war C eine
typisierte Sprache: es machte jetzt durchaus einen Unterschied, ob man mit
ganzen Zahlen oder Kommazahlen hantierte, ob man Buchstaben verarbeiten
wollte oder numerische Werte, denn C konnte diese verschiedenen Datentypen
deutlich voneinander unterscheiden. Einerseits war und ist C also eine Spra-
che zur Entwicklung von Anwendungssoftware, andererseits haben Thomson
und Ritchie bereits 1973 das Betriebssystem UNIX fast vollständig mit Hilfe
von C entwickelt, woraus man schließen muss, dass C auch zum Schreiben
von Systemsoftware mehr als geeignet ist. Inzwischen ist C schon lange ei-
ne eigenständige Programmiersprache, keineswegs mehr abhängig von UNIX,
sondern auf den verschiedensten Rechnertypen unter den verschiedensten Be-
triebssystemen einsetzbar, vom schlichten PC bis zum Supercomputer. Sie
werden zugeben, dass es nicht schaden kan, sich ein wenig mit einer so weit
verbreiteten Sprache auszukennen.
Die erste Standardisierung von C fand 1978 statt, als B. Kerninghan und
Ritchie ihr Buch The C Programming Language“ heraus brachten, auch heu-

te noch ein Standardbuch, mit dem man weltweit C lernt. Einen offiziellen
Standard gab es dann zehn Jahre später mit dem so genannten ANSI-C des
American National Standard Institute, abgekürzt ANSI.

Die Entwicklung von C


Die Programmiersprache C wurde in den frühen siebziger Jahren des zwan-
zigsten Jahrhunderts im Zusammenhang mit dem Betriebssystem UNIX ent-
92 2 Strukturierte Programmierung mit C

wickelt. Sie dient einerseits der Anwendungsprogrammierung, wird aber an-


dererseits auch in der Systemprogrammierung, vor allem in Bezug auf UNIX
eingesetzt.

2.2.2 Ein erstes Programm

Irgendwann muss man ja mal anfangen, sich an die konkreten Programme zu


wagen. Ich will dabei nichts überstürzen und beginne mit einer Variante des
Programms, das Sie in so ziemlich jedem C-Buch finden werden. Im Original
von Kerninghan und Ritchie gibt es die Worte Hello world“ auf dem Bild-

schirm aus; da wir uns hier aber nicht in Amerika befinden, ziehe ich es vor,
Sie mit einem schlichten Guten Morgen“ zu begrüßen. Das Programm sieht

dann folgendermaßen aus.
/* gruss.c Guten Morgen */

#include<stdio.h>

main( )
{
printf("Guten Morgen\n");
}
Allzu schön ist das sicher nicht, aber es zählen ja auch die inneren Werte.
Sehen wir uns einmal an, wie dieses Programm aufgebaut ist. Es beginnt mit
irgendwelchen Informationen, die zwischen /* und */ eingeschlossen sind. Sol-
che Informationen sind immer unproblematisch, denn an /* und */ können Sie
erkennen, dass es sich um Kommentare handelt. In einen Kommentar können
Sie schreiben, was Sie wollen, er dient nur der Information des Lesers und wird
vom eigentlichen Programm völlig ignoriert. Er kann sich übrigens auch über
mehrere Zeilen erstrecken, das ist ganz egal, Hauptsache, er wird zwischen
/* und */ gesteckt. Diesem Kommentar entnehme ich, dass das Programm
gruss.c heißt und irgendetwas mit dem Ausdruck Guten Morgen“ zu tun hat.

Anschließend sehen Sie eine Leerzeile, woraus Sie schon schließen können,
dass man in ein C-Programm jederzeit ohne Folgen Leerzeilen einfügen darf;
die sind dem Programm ähnlich gleichgültig wie Kommentare. Die nachfolgen-
de Anweisung #include<stdio.h> verschiebe ich kurz auf später, lassen Sie
mich erst auf main() eingehen. Wenn man es genau nimmt, handelt es sich hier
um eine Funktion, aber Funktionen werden wir erst später besprechen, also
beschränke ich mich erst einmal auf den Hinweis, dass mit main() das Haupt-
programm eingeleitet wird. Mein C-Programm beginnt seine Ausführung am
Anfang dessen, was ich unter main() abgelegt habe, und deshalb muss in
jedem C-Programm auch so ein main() vorkommen, sonst wüsste das Pro-
gramm nicht, wo es überhaupt anfangen soll. Versuchen Sie einmal, einfach nur
main anstatt main() zu schreiben - beim Schreiben selbst wird das natürlich
noch keine Probleme machen; wenn es aber darum geht, das Programm zum
2.2 Erste C-Programme 93

Laufen zu bringen, werden Sie die Folgen spüren. Vielleicht kennen Sie das
noch aus der Mathematik: main() ist nun mal eine Funktion, und eine Funk-
tion hat immer etwas, was man in sie einsetzen kann, weshalb man ja auch
meistens f (x) oder so ähnlich für Funktionen schreibt. Und in C erkennt man
eine Funktion eben an der Existenz der beiden Klammern, ganz gleich ob
dazwischen etwas steht oder nicht. Jede Funktion muss also als Zusatz die
Klammern ( ) mit sich schleppen, sonst gibt es Ärger.
Was nun in der main()-Funktion, also im eigentlichen Hauptprogramm,
passiert, finden Sie innerhalb der Mengenklammern { und }. Die geöffnete
Klammer bezeichnet den Anfang des Anweisungsteils der main()-Funktion,
hier stehen sämtliche Anweisungen, aus denen die Funktion besteht. Im Falle
des riesigen Programms gruss.c habe ich nur eine Anweisung zu bieten, sie
heißt printf("Guten Morgen\n") und ist im Vergleich zur darüber stehen-
den Zeile etwas nach rechts eingerückt. Das ist Absicht. In der Regel verwen-
det man eine einfache Einrückungstechnik, um so etwas wie eine Hierarchie
abzubilden: die Anweisung printf("Guten Morgen\n") gehört zur main( )-
Funktion, sie ist nichts weiter als ein Teil von main(), weshalb man sie eben
ein wenig nach rechts rückt. Das ist nicht zwingend notwendig, Sie können
auch jede Zeile wieder ganz vorne anfangen lassen, aber es dient der Über-
sichtlichkeit und trägt dazu bei, ein Programm anständig zu strukturieren -
schließlich sollen wir hier strukturiert programmieren. Natürlich hat die An-
weisung printf("Guten Morgen\n") auch eine Bedeutung, printf() steht
für formatiertes Drucken oder formatierte Ausgabe, und das heißt einfach nur,
dass alles, was in der Klammer nach printf() steht, nach bestimmten Re-
geln ausgegeben wird. Beachten Sie übrigens, dass es sich bei printf() um
eine vordefinierte Funktion handelt, und Funktionen schreibt man üblicher-
weise immer mit dem Zusatz () auf, damit der Leser das Kommando an den
Klammern sofort als Funktion erkennt.
In Fall meines kleinen Programms wird also die Zeichenkette Guten Mor-

gen“ ausgegeben und danach in eine neue Zeile gewechselt, denn das so ge-
nannte Steuerzeichen \n ist ein Zeilentrenner, der bewirkt, dass das Programm
nach der Ausgabe in eine neue Zeile wechselt. Ohne den Zeilentrenner bleiben
Sie in der zuerst ausgegebenen Zeile bis ans Ende Ihrer Tage. Man hätte also
die gleiche Ausgabe auch mit dem folgenden Programm erreichen können:
/* gruss.c Guten Morgen */

#include<stdio.h>

main( )
{
printf("Guten ");
printf("Morgen");
printf("\n");
}
94 2 Strukturierte Programmierung mit C

Die ersten beiden Ausgabekommandos bringen hier nämlich alles in ei-


ne Zeile, und erst danach wird durch die Verwendung des Zeilentrenners für
eine neue Zeile gesorgt. Sie können hier auch noch eine weitere wichtige Ei-
genart von C-Programmen deutlich sehen: eine Anweisung muss immer mit
einem Strichpunkt abgeschlossen werden, sonst kommen Sie in Schwierigkei-
ten. Wenn ich im Rahmen des Textes Anweisungen aufschreibe, dann lasse
ich den Strichpunkt immer weg, weil es beim Lesen eines Satzes nur verwirrt,
aber im Programm dürfen Sie ihn nicht vergessen.
Dass am Ende des Programms, also am Ende der main()-Funktion, dann
die schließende Klammer steht, hatte ich schon gesagt. Ein Punkt ist aber noch
offen: was soll dieses ominöse #include<stdio.h> bedeuten? Ganz einfach. C
in seiner Grundform kennt überhaupt keine Möglichkeiten zur Ausgabe von
Daten auf dem Bildschirm oder gar zur Dateneingabe über die Tastatur. Die
printf()-Anweisung kann man also nicht einfach so verwenden, Sie müssen
vorher dafür sorgen, dass Ihr C-Programm diese Anweisung auch versteht,
und genau das passiert mit #include<stdio.h>. Die Datei stdio.h ist eine
so genannte Header-Datei, in der bestimmte Ein- und Ausgabefunktionen zu
finden sind, die Ihnen ohne diese Datei nicht zur Verfügung stünden. Durch
den Befehl #include<stdio.h> ist es angenehmerweise möglich, alle in der
Header-Datei angegebenen Funktionen auch in dem Programm zu benutzen,
das den Befehl zum Einbinden der Header-Datei enthält. Weitere Header-
Dateien werden Ihnen im Lauf der Zeit noch begegnen.

main()-Funktion und Ausgabebefehl


Jedes C-Programm muss eine main()-Funktion enthalten, wobei darauf zu
achten ist, dass sie nicht nur als main, sondern durch Angabe von Klammern
als main() definiert wird. Die Ausgabe am Bildschirm wird durch printf()
geregelt; der printf()-Befehl ist in der Header-Datei stdio.h zu finden, die
durch die Anweisung #include<stdio.h> in das aktuelle Programm einge-
bunden wird.

Das erste C-Programm ist nun geschrieben. Und jetzt? Irgendwie müssen
Sie den Computer dazu bringen, etwas damit anzustellen, und das geschieht
mit Hilfe spezieller Programme.
Das erste der Hilfsprogramme haben Sie wohl bereits benutzt: den Pro-
grammtext tippen Sie mit irgendeinem Editor und speichern ihn dann unter
einem selbstgewählten Namen als Datei ab. Ich hatte mich hier für den Na-
men gruss.c entschieden. Der eigentliche Dateiname gruss ist dabei ziemlich
egal, bei der Endung haben Sie allerdings keine Wahlfreiheit: ein C-Programm
muss die Endnung .c“ aufweisen, sonst erkennt der Compiler nicht, dass Sie

ihm ein C-Programm präsentieren. Und damit haben wir schon das zwei-
te unbedingt nötige Hilfsprogramm gefunden: den Compiler. Da Ihre CPU
nicht in der Lage ist, den in den Editor geschriebenen Quellcode, also das
eigentliche C-Programmm, zu verstehen, muss jemand dieses C-Programm
nehmen und in eine maschinenverständliche Sprache übersetzen. Das kann
2.2 Erste C-Programme 95

natürlich nur dann gut gehen, wenn Ihr Programm einwandfrei den Regeln
der C-Grammatik entsprochen hat, da darf man sich keine Schlampereien
erlauben. Ich als menschlicher Leser verstehe wahrscheinlich auch die Auf-
forderung printd("Guten Morgen\n"), der Compiler als Vertreter äußerster
Genauigkeit versteht sie nicht. In einem ersten Schritt muss er also die syntak-
tische Korrektheit Ihres Programms überprüfen, das er in der vorher angeleg-
ten Quelldatei findet, und das heißt, der Compiler geht Ihren Programmtext
Zeichen für Zeichen, Wort für Wort durch und stellt fest, ob Sie sich auch an
die Regeln der C-Grammatik gehalten haben. Werden diese Regeln irgendwo
durchbrochen, so wird er Ihnen die Regelverstöße in Form von Fehlermel-
dungen um die Ohren hauen und sich weigern, das vorliegende Programm in
lauffähigen Maschinencode zu übersetzen - wie sollte er auch, da er aufgrund
der fehlerhaften Grammatik gar nicht wissen kann, was Sie eigentlich von ihm
wollen. In diesem Fall liegt es also an Ihnen, noch einmal das Quellprogramm
mit Hilfe des Editors zu bearbeiten und die angezeigten Fehler auszumerzen.
Sobald Sie das geschafft haben, wird der nächste Compilerlauf anzeigen, dass
Ihr Programm fehlerfrei ist und deshalb lauffähiger Maschinencode erzeugt
wurde. Sollten Sie beispielsweise mit einem PC auf Windows-Basis arbeiten,
so hat der Compiler eine Datei namens gruss.exe erzeugt, wobei die Endung
exe eine Abkürzung für executable, also ausführbar darstellt und somit bereits
das ausführbare Programm vorliegt.
Manchmal ist das Leben aber auch einfacher. Es gibt recht komfortable
Entwicklungsumgebungen, die Ihnen die Arbeit des ständigen Wechsels zwi-
schen Editor, Compiler und Ausführung abnehmen, weil alle diese Kompo-
nenten in der Entwicklungsumgebung integriert sind. Sie werden dann also
beispielsweise im Rahmen einer graphischen Benutzeroberfläche Ihren Pro-
grammtext schreiben, ihn mit einem Mausklick - natürlich auf der richtigen
Schaltfläche - abspeichern, mit einem weiteren Mausklick die Übersetzung
durch den Compiler anwerfen und schließlich, sofern die Übersetzung fehler-
frei funktioniert hat, durch einen letzten Mausklick das Programm laufen las-
sen und die originelle Begrüßung Guten Morgen“ bewundern. Das kann man

so machen und es ist bequem, aber man muss es nicht. Genauso gut können
Sie das Editieren beispielsweise mit dem DOS-Editor erledigen, den Compiler
durch einen Befehl vom DOS-Fenster aus aufrufen und dann vom gleichen
Fenster aus durch Eingabe des Befehls gruss dafür sorgen, dass die ausführ-
bare Datei gruss.exe zur Anwendung kommt und Ihr Programm abläuft. Wie
Sie im Einzelnen vorgehen, hängt dabei von Ihrem persönlichen Geschmack
und den Möglichkeiten Ihres C-Compilers ab.
Noch zwei Worte zum Compiler und zum Kompilieren. Bei großen Pro-
grammen kann und wird es vorkommen, dass sie aus getrennten Modulen
bestehen, aus Teilprogrammen, die auch in getrennten Dateien abgelegt wer-
den, weil zum Beispiel mehrere Entwickler daran arbeiten und man Durch-
einander vermeiden will. Die einzelnen Module kann man dann zwar separat
übersetzen in maschinenlesbaren Code, aber damit das gesamte Programm
zur Verfügung steht, müssen Sie diese Module noch zu einem Ganzen zusam-
96 2 Strukturierte Programmierung mit C

menfügen. Das geschieht durch den Aufruf eines Linkers, der die nach dem
Kompilieren vorliegenden Teile zu einem lauffähigen Programm zuammen-
bindet und die Voraussetzungen dafür schafft, dass alle diese Teile koordiniert
zusammenarbeiten. Wie er das im Einzelnen macht, braucht uns hier nicht zu
interessieren, so riesig werden unsere Programme nicht werden.
Der Linker wird also nach dem Compiler aktiv. Am Anfang des Compilie-
rens dagegen muss der Präprozessor seiner Arbeit nachgehen, den ich schon
einmal, wenn auch ohne Namensnennung, erwähnt hatte. Erinnern Sie sich
noch an die Header-Dateien? Die Anweisung #include<...> ist eine Anwei-
sung an den Präprozessor, sich die entsprechende Datei zu besorgen und für
die Dauer der Übersetzung in den Quellcode hinein zu kopieren. Auf diese Wei-
se sorgt der Präprozessor dafür, dass alle Informationen aus der angegebenen
Header-Datei auch in Ihrem Programm verfügbar sind und Sie beispielsweise
die Ein- und Ausgabefunktionen aus der Datei stdio.h ungestört einsetzen
können.

Der Entwicklungsprozess
Der Programmtext wird mit Hilfe eines Editors geschrieben und in einer Da-
tei mit der Endung .c“ gespeichert. Anschließend überprüft der Compiler die

syntaktische Korrektheit des Programms und übersetzt es im Falle einer er-
folgreichen Überprüfung in maschinenlesbaren Code, der dann zur Ausführung
gelangen kann. Zu Beginn des Compilerlaufs werden vom Präprozessor die
angegebenen Header-Dateien in das Programm eingefügt. Besteht das Pro-
gramm aus mehreren separaten Modulen, so müssen die erzeugten Codeteile
mit einem Linker zusammen gebunden werden.

2.2.3 Variablen
Das war nun ziemlich viel Erklärung für ein Programm, das nicht allzu viel Lei-
stung erbringt; es sollte doch möglich sein, etwas mehr von einem C-Programm
zu verlangen als eine schlichte Begrüßung. Ein etwas leistungsfähigeres Pro-
gramm sehen wir uns jetzt an.
/* rechnen.c -- summe berechnen */

#include <stdio.h>

main()
{
int x;
int y;
int z;
x=3; y=4;
z=x+y;
printf("%d",z);
}
2.2 Erste C-Programme 97

Das Programm fängt ähnlich an wie das vorherige; über Kommentare,


Header-Dateien und die main()-Funktion brauche ich nicht mehr zu äußern.
Innerhalb der main()-Funktion finden sie jetzt aber etwas Neues, und das
sind die so genannten Variablen. Es werden hier drei Variablen namens x, y
und z angelegt, die alle mit dem Vorsatz int geschlagen sind. Die Bedeutung
ist ganz einfach. Ich will hier nichts anderes als eine Addition ganzer Zahlen
durchführen, also brauche ich Variablen, die ganze Zahlen repräsentieren. Das
teile ich dem Compiler mit, indem ich so etwas wie int x schreibe und ihm
dadurch klar mache, dass x eine Variable vom Typ Integer sein soll. Zu jeder
Variable gehört also ein Datentyp, der angibt, um was für eine Art von Varia-
ble es sich handeln soll, in diesem Fall um eine Integer-Variable. Sobald der
Compiler auf so eine Anweisung stößt, wird er im Arbeitsspeicher des Rech-
ners ein wenig Speicherplatz reservieren - eben so viel, dass eine ganze Zahl
hineinpasst. Auf dieses kleine Stückchen Speicherplatz kann dann mein Pro-
gramm sorglos zugreifen, wann immer es die Bezeichnung x verwendet. Dass
Variablen nichts anderes sind als Anwartschaften auf etwas Speicherplatz, hat
Konsequenzen. Sie haben im ersten Kapitel gesehen, dass der Arbeitsspeicher
in Speicherzellen aufgeteilt ist, und deshalb wird die Größe beispielsweise ei-
ner ganzen Zahl abhängen von der Größe der Speicherzellen, in denen sie
abgelegt werden soll. Wie man so etwas ausrechnet, haben Sie im Abschnitt
über logische Schaltungen und Addierer gesehen. Hat man beispielsweise zwei
Byte, also 16 Bits, für eine Zahl zur Verfügung, so kann man ganze Zahlen
zwischen −215 = −32768 und 215 − 1 = 32767 in einer Variable vom Typ
int ablegen, bei vier Byte, also 32 Bits, liegt der Bereich schon zwischen
−231 = −2147483648 und 231 − 1 = 2147483647.
Das ist Ihnen nicht neu und soll nur zeigen, dass ich Sie im letzten Kapi-
tel nicht zu meinem persönlichen Vergnügen mit Zahlendarstellungen gequält
habe. Wie groß nun die ganzen Zahlen auf Ihrem eigenen C-Compiler werden
können, weiß ich nicht, das hängt von Ihrem Compiler, Ihrem Computer und
Ihrem Betriebssystem ab. Oft genug hat man eine Länge von vier Byte, und
das ist doch immerhin schon etwas.
Natürlich gibt es nicht nur Variablen vom Datentyp int, in späteren Bei-
spielen werden Sie noch andere Typen sehen. In meinem kleinen Programm
rechnen habe ich drei int-Variablen angegeben, und dieses Angeben nennt
man normalerweise Deklarieren. Sie können jede Variable einzeln deklarieren,
was ich aber nicht empfehle, denn es spart Zeit und Platz, die Deklaration
typweise zu erledigen. An Stelle der drei separaten Deklarationen hätte ich
nämlich auch kürzer int x, y, z; schreiben können: gleicher Effekt, weni-
ger Platz und Schreibarbeit.
Nun werden diese Variablen nicht nur deklariert, mit ihnen passiert auch
etwas: endlich habe ich einmal ein Programm, das konkret rechnet. Nachdem
x der Wert 3 und y der Wert 4 mit den Kommandos x=4 bzw. y=3 zugewie-
sen wurde, berechnet das Programm die Summe aus x und y und weist sie
der Variablen z zu. Das Kommando z=x+y zerfällt also bei seiner Ausführung
in zwei Teile: zuerst wird die Summe aus den beiden Variablen x und y be-
98 2 Strukturierte Programmierung mit C

rechnet, und dann wird das Ergebnis der Variablen z zugewiesen. Wie Sie
sehen, erfolgt die Zuweisung eines Wertes an eine Variable mit einem schlich-
ten Gleichheitszeichen; nachdem diese Zuweisung erfolgt ist, ist z mit dem
Wert 7 belegt.
Schon mal nicht schlecht, aber der Benutzer des Programms wird ein ge-
wisses Interesse daran haben, die Ergebnisse seiner Rechnung auch zu se-
hen, und dafür gibt es schließlich die printf()-Anweisung. Hier lautet sie
printf("%d",z), und das sieht einigermaßen abschreckend aus. Nur Geduld,
das ist halb so wild. Wie ich Ihnen erzählt habe, stößt unser printf() eine
formatierte Ausgabe an, und irgendwie muss man dem Kommando die nötigen
Formatanweisungen zukommen lassen. Ausgeben wird es immer eine Zeichen-
kette; die Frage ist nur, wie sich diese Zeichenkette zusammensetzt. Während
die Zeichenkette selbst durch Anführungszeichen begrenzt wird, finden Sie in
ihr die etwas seltsam anmutende Angabe %d - ein so genanntes Formatele-
ment, mit dem man steuert, wie die Ausgabe der Variablen stattfinden soll.
Formatelemente fangen immer mit % an und geben an, wo und wie die nachfol-
genden Argumente in die Ausgabe einzufügen sind. Da ich in die Zeichenkette
nur das Formatelement %d geschrieben habe, wird auch nur die anschließend
aufgeführte ganzzahlige Variable z am Bildschirm ausgegeben, und das passt
auch gut zusammen, denn %d benennt immer die Ausgabe eines ganzzahligen
Wertes. Wie müsste man formatieren, wenn man zum Beispiel sagen wollte
Die Summe lautet 7“? Nichts einfacher als das. Man packt die auszuge-

bende Zeichenkette wieder in Anführungszeichen, und an die Stelle, an der
die Variable vorkommen müsste, schreibt man das entsprechende Formatele-
ment. Konkret heißt das dann: printf("Die Summe lautet %d",z). Bei der
Ausführung dieses Befehls wird an der passenden Stelle die Variable z un-
ter Verwendung des Formatelementes ausgegeben. Das setzt natürlich voraus,
dass Variable und Formatelement zueinander passen: Sie können beispielswei-
se eine reelle Zahl, bei der Stellen nach dem Komma vorkommen, nicht mit
einer Formatelement für ganze Zahlen ausgeben.
Und wie kann man dafür sorgen, dass die gesamte Rechenaufgabe samt
Ergebnis zur Ausgabe kommt? Auch nicht schwer. Wenn Sie das Kommando
printf("Die Summe aus %d und %d lautet %d",x,y,z);
in Ihr Programm aufnehmen, wird es Ihnen die Ausgabe
Die Summe aus 3 und 4 lautet 7
liefern. Für jede auszugebende Variable müssen Sie also ein passendes For-
matelement in Ihre Zeichenkette einfügen. Die Variablen werden dann in der
Reihenfolge ihres Auftretens mit den Formatelementen verglichen, und wenn
alles zusammen passt, kann problemlos die Ausgabe erfolgen. Die auszuge-
benden Variablen nennt man auch Argumente.

Variablen und Ausgabe


Eine Variable gibt eine Speicherstelle im Arbeitsspeicher an. Jede Variable
muss einen Datentyp haben; eine ganzzahlige Variable wird beispielsweise
durch die Angabe int vor dem Variablennamen als ganzzahlig gekennzeich-
2.2 Erste C-Programme 99

net. Die Größe der entsprechenden Speicherstelle und damit auch die Größe
der verwendbaren Zahlen hängt vom Compiler, vom Computer und vom Be-
triebssystem ab.
Die Ausgabe von Variablen erfolgt mit der printf()-Anweisung. Ausgege-
ben wird immer eine Zeichenkette, zu jedem auszugebenden Argument muss
es in der Zeichenkette ein entsprechendes Formatelement geben.

2.2.4 Eingabe von der Tastatur

Zugegeben: die Rechenaufgabe 3 + 4 = 7 hätten sie wahrscheinlich auch noch


ohne einen C-Compiler lösen können - vermutlich sogar schneller. Aber was
ist mit 3275789 + 7648242 oder noch schlimmeren Zahlen? Zu Fuß macht
das keinen rechten Spaß mehr, aber das bisherige Programm rechnen.c ist
dafür nicht geeignet, es sei denn, Sie ändern die Belegungen der Variablen
x und y im Programmtext und starten anschließend wieder den Compiler.
Bei einem so kleinen Programm wäre das notfalls, wenn einem nichts anderes
einfällt, noch vertretbar, bei einem größeren käme kein Mensch auf so eine
umständliche Idee. Man braucht vielmehr eine Möglichkeit, den Wert einer
Variablen während der Laufzeit des Programms zu beeinflussen, und diese
Möglichkeit liefert der scanf()-Befehl. Sehen Sich sich einmal an, wie das
Rechenprogramm unter Einsatz dieser Anweisung aussieht.
/* rechnen.c -- summe berechnen */

#include <stdio.h>

main()
{
int x, y, z;
scanf("&d",&x);
scanf("&d",&y);
z=x+y;
printf("Die Summe aus %d und %d lautet %d",x,y,z);
}
Mit dem Kommando scanf("%d",&x) machen Sie dem Compiler zweier-
lei klar. Erstens erklären Sie ihm, dass jetzt gleich eine Eingabe erfolgen soll,
und zwar eine Eingabe einer ganzen Zahl: das kann er schon an der Format-
angabe %d erkennen, die Ihnen bereits bei der formatierten Ausgabe begegnet
ist. Und zweitens soll diese Eingabe in die Variable x hineingeschrieben wer-
den, was durch die Angabe von &x ausgedrückt wird. Vielleicht wundern Sie
sich ein wenig darüber, was wohl dieses seltsame & vor dem Variablennamen
soll, und da wundern Sie sich nicht alleine. C regelt das ein wenig umständ-
lich. Sie müssen dem Compiler nämlich direkt mitteilen, an welcher Stelle des
Arbeitsspeichers er die Eingabe unterbringen soll, die pure Angabe des Va-
riablennamens alleine reicht nicht aus. Wie Sie später noch sehen werden, ist
100 2 Strukturierte Programmierung mit C

das aber keine große Sache, denn die zu einer Variablen x gehörende konkrete
Speicherstelle kann man ganz einfach mit &x ansprechen, weshalb dann auch
im scanf()-Kommando jede Variable mit dem &-Zeichen versehen sein muss.
Lassen Sie jetzt das Programm laufen, so wird es gleich nach dem Start an-
halten und eine Eingabe über die Tastatur erwarten, die Sie ihm auch gönnen
sollten; schenken Sie Ihrem Programm also eine ganze Zahl und schließen Sie
diese Eingabe mit der Return-Taste ab. Kaum haben Sie Ihre Arbeit getan,
müssen Sie gleich noch mal an die Arbeit, denn schließlich hat Ihr Programm
zwei Eingabekommandos zu bieten. Erst wenn Sie sowohl x als auch y ein-
gegeben haben, kann die Berechnung stattfinden und das Ergebnis ausgege-
ben werden. Achten Sie dabei unbedingt darauf, dass Sie auch wirklich ganze
Zahlen eingeben, also eine Eingabe vornehmen, die zum Kommando passt; an-
dernfalls müssen sie damit rechnen, dass das Programm dummerweise nicht
abbricht, sondern mit völlig sinnlosen Werten weiter rechnet.
Guter Stil ist das aber immer noch nicht. Der Benutzer des Programms
wird zweimal mit einem blinkenden Cursor konfrontiert und hat nicht die lei-
seste Ahnung, was er nun machen soll, weil es ihm keiner erklärt hat. Bevor
Sie von einem Anwender Ihres Programms also eine Eingabe verlangen, soll-
ten Sie ihm freundlicherweise mitteilen, was für eine Eingabe Ihnen zu Ihrem
Glück noch fehlt. In unserem Fall könnte das so aussehen:
/* rechnen.c -- summe berechnen */

#include <stdio.h>

main()
{
int x, y, z;
printf("Geben Sie bitte eine ganze Zahl ein:\n");
scanf("&d",&x);
printf("Geben Sie bitte noch eine ganze Zahl ein:\n");
scanf("&d",&y);
z=x+y;
printf("Die Summe aus %d und %d lautet %d",x,y,z);
}
Jetzt erst weiß der Anwender, wie er mit dem Programm umgehen soll,
und alle sind zufrieden. Alle, bis auf mich, denn noch immer ist die Ein-
gabe etwas umständlich programmiert. Wenn man schon bei der Ausga-
be mehrere Variablen in einem Kommando ausgeben kann, dann sollte et-
was Ähnliches auch bei der Eingabe möglich sein. Das ist auch so. An-
statt sich mit einem scanf()-Kommando pro Variable zu plagen, können
Sie auch schlicht das eine Kommando scanf("%d %d",&x,&y) verwenden,
und schon werden beide Variablen von der Tastatur eingelesen. Natürlich
müssen Sie in diesem Fall auch Ihren auffordernden Text in so etwas wie
printf("Geben Sie bitte zwei ganze Zahlen ein:\n") umändern. Da-
gegen muss der Benutzer Ihres Programms darauf achten, entweder zwei ganze
2.2 Erste C-Programme 101

Zahlen, getrennt durch ein Leerzeichen, einzugeben und dann die Eingabe mit
der Return-Taste abzuschließen, oder aber genau wie vorhin vorzugehen und
nach jeder der beiden Zahlen die Return-Taste zu drücken. Beides funktioniert
und liefert das richtige Ergebnis.

Das scanf()-Kommando
Die formatierte Eingabe von der Tastatur erfolgt mit Hilfe des scanf()-
Kommandos. Es benötigt eine Formatangabe wie z.B. "&d", in der festgelegt
wird, welche Art von Variablen jetzt eingelesen werden soll, und eine anschlie-
ßende Liste von Variablen, wobei vor jeden Variablennamen das &-Zeichen
gesetzt werden muss, da auf diese Weise direkt die Arbeitsspeicheradresse der
jeweiligen Variablen angesprochen wird. Sollen mehrere Variablen eingelesen
werden, so muss die Formatangabe der Anzahl und den Datentypen der Va-
riablen angepasst sein.

Das scanf()-Kommando wird uns ab jetzt noch häufiger begegnen, zum


Beispiel gleich im nächsten Teil.

2.2.5 Arithmetik

Sie können jetzt über die Tastatur ein- und auf dem Bildschirm wieder aus-
geben, immerhin. Was Sie bisher nur sehr eingeschränkt können, ist rechnen,
aber das wird sich gleich ändern. Wie jede Programmiersprache, die sich der
strukturierten Programmierung verschrieben hat, verfügt selbstverständlich
auch C über die nötigen Grundrechenarten, und man schreibt sie ziemlich
genauso auf, wie man sich das vorstellt. Dazu ein kleines Beispielprogramm.

/* grundrechenarten.c -- grundrechenarten anwenden */

#include <stdio.h>

main()
{
int x, y, a, b, c, d;
printf("Geben Sie bitte zwei ganze Zahlen ein:\n");
scanf("&d &d",&x, &y);
a=x+y;
b=x-y;
c=x*y;
d=x/y;
printf("Die Werte lauten: %d, %d, %d und %d\n",a,b,c,d);
}

Sie sehen, wie es funktioniert. Plus, Minus, Mal und Durch symbolisiert
man durch +, -, * und /, und mehr steckt nicht dahinter. Ein Problem sollte
102 2 Strukturierte Programmierung mit C

man allerdings nicht unterschätzen. Sobald Sie das Programm laufen lassen
und die Eingaben x = 5 und y = 9 von sich geben, dann werden Sie zwar die
korrekten Werte a = 14, b = −4 und c = 45 erhalten, aber zur allgemeinen
Freude wird d = 0 sein. Wie? 5/9 = 0? Ja, manchmal schon, wenn man
nämlich den Fehler macht, nur mit ganzen Zahlen zu rechnen. Natürlich ist
5/9 = 0, 555.., aber dieses Ergebnis können Sie der ganzzahligen Variablen d
auf keinen Fall zumuten, die nimmt nur an, was sie kennt: ganze Zahlen. Und
der ganzzahlige Anteil von 0, 555.. ist nun mal schlicht 0, weshalb auf d das
Ergebnis 0 stehen wird.
Dagegen muss man etwas tun. Damit Sie auch mit reellen Zahlen rechnen,
mit Kommazahlen umgehen können, hat man den Datentyp float erfunden.
Er sorgt dafür, dass die Nachkomastellen nicht einfach abgeschnitten, sondern
tatsächlich registriert und auf Wunsch weiter verarbeitet werden. Also sollte
es doch genügen, jetzt die Variable d als float-Variable zu deklarieren, um
alle Probleme mit der Division zu lösen. Das Programm würde dann folgen-
dermaßen aussehen.
/* grundrechenarten.c -- grundrechenarten anwenden */

#include <stdio.h>

main()
{
int x, y, a, b, c;
float d;
printf("Geben Sie bitte zwei ganze Zahlen ein:\n");
scanf("&d &d",&x, &y);
a=x+y;
b=x-y;
c=x*y;
d=x/y;
printf("Die Werte lauten: %d, %d, %d und %f\n",a,b,c,d);
}
Die Variable d ist aus der Liste der ganzzahligen Variablen verschwunden
und taucht als eine float-Variable wieder auf, also als eine schlichte reelle
oder rationale Zahl, die auch mit Nachkommastellen geschlagen sein kann.
Und da die Formatangabe %d für ganze Zahlen reserviert ist, muss sich auch
bei der Ausgabe etwas ändern: als letztes Formatelement taucht jetzt %f auf,
was offenbar bedeutet, dass hier eine float-Variable ausgegeben werden soll.
So weit, so gut. Lassen Sie jetzt das Programm laufen mit den Eingabewerten
x = 5 und y = 9, dann erhalten Sie die Ausgabe:
Die Werte lauten 14, -4, 45 und 0.000000
Hat sich ja richtig gelohnt. Die Variable d wird zwar tatsächlich als Komma-
zahl ausgegeben, wobei die Beobachtung, dass man genau genommen einen
Punkt und kein Komma verwendet, nicht unter den Tisch fallen sollte - aber
der Wert selbst hat sich nicht verbessert. Das ist auch kein Wunder. Durch
2.2 Erste C-Programme 103

den Befehl d=x/y; wurde erst einmal der Quotient aus x und y berechnet und
dann das Ergebnis der float-Variablen d zugewiesen, aber da beim puren
Rechnen immer noch niemand anderes beteiligt war als zwei int-Variablen,
wurde auch nur ganzzahlig gerechnet. Und bei der ganzzahligen Division von
5 durch 9 ergibt sich nun mal 0, denn alle Reste werden abgeschnitten. Somit
konnte d keinen anderen Wert bekommen als eine nette dezimale 0.
Solange also nur ganzzahlige Variablen an einer Rechnung beteiligt sind,
wird auch nur ganzzahig gerechnet, unabhängig vom Typ der Ergebnisvaria-
blen. Um die Rechnung genauer durchführen zu können, muss auch schon in
den eigentlichen Operationen, beim tatsächlichen Rechnen eine float-Zahl
beteiligt sein; eine reicht schon, dann wird alles auf float umgerechnet. Im
folgenden Programm sehen Sie, wie man das machen kann.
/* division.c -- dividieren ganzer zahlen */

#include <stdio.h>

main()
{
int x, y;
float d;
printf("Geben Sie bitte zwei ganze Zahlen ein:\n");
scanf("&d &d",&x, &y);
d=(x*1.0)/y;
printf("Das Ergebnis lautet: %f\n",d);
}
Indem Sie x mit der Kommazahl 1.0 multiplizieren, erfüllen Sie die Bedin-
gung, dass eine float-Zahl an der Rechnung beteiligt sein muss, und als Er-
gebnis werden Sie dann 0.555556 erhalten. Sie können aber auch, wenn Sie die
Datentypen nicht durcheinander bringen wollen, zwei weitere float-Variablen
a und b einführen, die Eingaben für x und y in a und b zwischenspeichern
und dann mit diesen Variablen weiter rechnen. Nach der Eingabe Ihrer ganz-
zahligen Variablen müssten Sie also die Kommandos a=x; b=y vorsehen und
dann d=a/b rechnen lassen - auch das funktioniert. Das alles ist natürlich nur
dann nötig, wenn Sie als Eingangswerte ganze Zahlen haben: rechnen Sie von
Anfang an mit float-Variablen, werden alle Verrenkungen unnötitg, sofern
Sie bei der Eingabe auf die richtigen Formate achten.
/* divisionreell.c -- dividieren von kommazahlen */

#include <stdio.h>

main()
{
float x,y;
float d;
printf("Geben Sie bitte zwei reelle Zahlen ein:\n");
104 2 Strukturierte Programmierung mit C

scanf("&f &f",&x, &y);


d=x/y;
printf("Das Ergebnis lautet: %f\n",d);
}
Sollten Sie übrigens den umgekehrten Wunsch verspüren, die Nachkomma-
stellen einer float-Zahl verschwinden zu lassen, dann ist nichts einfacher als
das: weisen Sie den Inhalt der float-Variablen einer int-Variablen zu und die
Sache hat sich. Für eine float-Variable z, die mit 1.234 belegt ist, und eine
int-Variable x würde also die Wertzuweisung x=z dazu führen, dass auf x der
Wert 1 zu finden ist, da die Nachkommastellen von z abgeschnitten wurden.
Noch eine kurze Warnung: vermeiden Sie Divisionen durch null. Die gehen
nicht nur in der Mathematik schief, sondern auch beim Programmieren. Der
Compiler ist nicht schlau genug, bei den Anweisungen
int x,y,z=0; y=x/z;
schon während der Syntaxprüfung zu bemerken, dass Sie hier Unsinn rech-
nen wollen - wie sollte er auch, die Befehle sind ja korrekt aufgeschrieben.
Erst wenn das Programm läuft, wird festgestellt, dass die anbefohlene Rech-
nung gar nicht durchführbar ist, und das Programm wird mit einer Fehler-
meldung abbrechen. Aber noch eine andere Kleinigkeit sollte Ihnen an dieser
Programmzeile auffallen: man kann eine Variable schon bei ihrer Deklarati-
on mit Leben füllen, indem man ihr sofort einen Wert zuweist. So etwas wie
int z=0 nennt man Initialisierung.

Grundrechenarten
Die vier Grundrechenarten werden mit Hilfe der Operatoren +, -, * und /
durchgeführt. Die Division ganzer Zahlen erfolgt ganzzahlig, also unter Ab-
schneiden der Nachkommastellen. Um mit Kommazahlen rechnen zu können,
verwendet man den Datentyp float.

2.2.6 Datentypen

Sie sehen: welchen Datentyp ich verwende, hängt stark vom Problem ab, das
ich gerade angehe. Im Folgenden habe ich Ihnen einmal die Datentypen auf-
gelistet, mit denen man es üblicherweise zu tun hat.
Zuerst haben wir da den Datentyp int, eine Abkürzung für Integer, al-
so für ganze Zahlen. Sofern Sie Ihre ganzen Zahlen auf zwei Byte beschränkt
sind, können Sie mit diesem Datentyp die ganzen Zahlen zwischen −32768 und
32767 darstellen, wie ich es Ihnen am Ende des fünften Abschnittes im ersten
Kapitel gezeigt habe. Sollten Sie über vier Byte verfügen, läuft der Zahlenbe-
reich im Datentyp int von −231 bis 231 − 1. Sie können übrigens leicht heraus
finden, ob Ihre ganzen Zahlen bei 32767 enden: schreiben Sie ein C-Programm,
das in eine int-Variable x den Wert 32767 einlieft und dann y = x+1 setzt,
wobei natürlich auch y eine Integer-Variable sein muss. Falls die ganzen Zah-
len noch weiter gehen, wird auf y der Wert 32768 stehen, und alles ist gut.
2.2 Erste C-Programme 105

Falls aber nicht, werden Sie feststellen, dass die Variable y mit dem Wert
−32768 versehen ist. Warum? Verrate ich nicht, das sollen Sie sich in einer
Übungsaufgabe selbst überlegen. In jedem Fall kann man mit int-Variablen
die besprochenen Grundrechenarten auf die besprochene Weise durchführen,
und noch eine mehr: bei der Division haben Sie gesehen, dass zwangsläufig die
Nachkommastellen abgeschnitten wurden. Nun gibt es aber für ganze Zahlen
die Division mit Rest, zum Beispiel ist 5/3 = 1 Rest 2 und 11/4 = 2 Rest 3.
Die Division selbst liefert immer das ganzzahlige Divisionsergebnis. Den Rest
erhält man dagegen mit der modulo-Operation %. Hat man also beispielsweise
die int-Variablen x=11 und y=4, so ergibt x % y den Wert 3, weil der Rest
bei der Division 11/4 eben genau 3 ist.
Sollten Sie genau wissen, dass Ihre ganzen Zahlen nicht übermäßig lang
werden, so können Sie durch den Einsatz des Datentyps short oder auch
short int - das ist genau das Gleiche - ein wenig Speicherplatz sparen. Zahlen
vom Typ short haben nur halb so viel Platz zur Verfügung wie Zahlen vom
Typ int; bei meinen zweibytigen ganzen Zahlen heißt das also, dass eine
Deklaration wie short x oder short int y zu ganzen Zahlen mit einer Länge
von einem Byte, also acht Bits, führt. Wie im ersten Kapitel besprochen, folgt
daraus: der Bereich an ganzen Zahlen, den der Datentyp short abdeckt liegt
zwischen −27 und 27 − 1, also zwischen −128 und 127 - kein großer Bereich,
sondern eher ein kurzer, aber was soll man erwarten, wenn schon die Zahlen
selbst so wenig Platz haben. Davon abgesehen, sind die möglichen Operationen
die gleichen wie gerade beim Datentyp int.
Während short-Zahlen die verkürzte Variante von int-Zahlen darstellen,
haben Sie in den long-Zahlen das genaue Gegenteil vor sich: eine long-Zahl
oder auch long int-Zahl kann im Arbeitsspeicher doppelt so viel Platz für
sich beanspruchen wir eine gewöhnliche int-Zahl, in dem von mir angenomme-
nen Fall der zweibytigen int-Zahlen also immerhin vier Byte. Da vier Byte
bekanntlich 32 Bits entsprechen, können Sie im Datentyp long die ganzen
Zahlen von −231 bis 231 − 1 darstellen, nicht mehr und nicht weniger. Für
die üblichen ganzzahligen Anwendungen sollte das eigentlich genügen, denn
immerhin sind das die Zahlen zwischen −2 147 483 648 und 2 147 483 647,
das ist gar nicht so wenig. Eine mit long x oder auch long int x deklarierte
Variable können Sie also eine ganze Weile mit großen ganzen Zahlen plagen,
ehe etwas Unangenehmes passiert.
Erwähnen sollte ich noch kurz die vorzeichenlosen ganzen Zahlen, die sich
im Datentyp unsigned int versammeln. Sie sind dann zu empfehlen, wenn
Sie garantieren können, dass in Ihrem Programm keine negativen Zahlen auf-
treten werden und Sie gleichzeitig etwas Platz im Arbeitsspeicher einsparen
wollen. Da eine Zahl vom Typ unsigned int genauso vorzeichenlos ist wie sie
heißt, wird sie grundsätzlich als positive Zahl gerechnet, weshalb man für sie
kein Vorzeichenbit braucht. Das ist praktisch, denn bei einer Breite von zwei
Byte, also 16 Bits, stehen in diesem Fall alle 16 Bits zur Speicherung der puren
Zahl bereit, und das bedeutet, dass die größte vorzeichenlose Zahl, die Sie mit
zwei Byte darstellen können, der Bitfolge 11111111 11111111 entspricht, also
106 2 Strukturierte Programmierung mit C

der Zahl 216 − 1 = 65535. Der durch den Datentyp unsigned int abdeckbare
Zahlenbereich liegt also zwischen 0 und 65535. Aber Vorsicht: auch hier gibt
es die üblichen Probleme, sobald Sie den zulässigen Bereich verlassen. Was
passiert mit den folgenden drei Programmzeilen?
unsigned int x, y;
x=65535;
y=x+1;
Eingepackt in ein korrektes Hauptprogramm, würde nach Ablauf dieses Pro-
grammstücks auf der Variablen y der Wert 0 stehen. Den Grund werden Sie
sich im Rahmen einer Übungsaufgabe selbst überlegen.
Ich weiß: eine Auflistung und Analyse der verschiedenen Datentypen wirkt
ähnlich spannend wie die gesammelten Werke des Deutschen Wetterdienstes
von 1978. Das täuscht aber. Während man mit den alten Wetterberichten nun
wirklich nichts mehr anfangen kann, sind die Datentypen nun mal unverzicht-
bar, um verschiedene Arten von Daten verarbeiten zu können. Etwas Geduld
noch, es dauert auch nicht mehr lange.
Wie würden Sie beispielsweise Zeichen verarbeiten ohne einen passenden
Datentyp? C stellt dafür den Datentyp char zur Verfügung, für den man ge-
rade mal ein ganzes Byte, also acht Bits braucht. Das würde schon die binäre
Darstellung von 256 Zeichen erlauben, aber um rechnerintern den Datentyp
char von den ganzen Zahlen nicht allzu sehr unterscheiden zu müssen, hat
man sich dafür entschieden, auch hier ein Vorzeichenbit zu verwenden. Als
Zahlen betrachtet, haben die Zeichen daher einen Bereich von −128 bis 127,
und nur die Werte von 0 bis 127 werden verwendet zur Darstellung von Zei-
chen. Damit auch alle Programmierer die gleichen Zeichen mit den gleichen
binären Verschlüsselungen belegen, gibt es den so genannten ASCII-Code, ei-
ne Abkürzung für American Standard Code for Information Interchange, der
die Nummern der einzelnen Zeichen festlegt. Die Ziffern 0 bis 9 haben, als
Zeichen betrachtet, beispielsweise die Nummern 48 bis 57, die Großbuchsta-
ben liegen zwischen 65 und 90, die Kleinbuchstaben zwischen 97 und 122. Der
Witz ist aber der, dass Sie eine char-Variable jederzeit wie eine int-Variable
behandeln können und umgekehrt. Nehmen wir zum Beispiel das folgende
Programm.
/* zeichen.c -- Zeichen und Zahlen */

#include <stdio.h>

main()
{
char x,y;
x=65; y=x+1;
printf("\n Der Wert lautet %c %c",x,y);
}
Die Variablen x und y sind als char-Variablen deklariert, aber trotzdem
wird x mit dem Wert 65 belegt. Damit erhält x das Zeichen mit der Nummer
2.2 Erste C-Programme 107

65, und das ist genau das große A. Wegen y=x+1 wird auf y das Zeichen mit der
Nummer 66 stehen: das große B. genauso gut hätte ich auch x=’A’; y=x+1;
schreiben können, das Wechselspiel zwischen int und char funktioniert in bei-
den Richtungen. Wenn man aber einer char-Variablen auch wirklich explizit
ein Zeichen zuweisen will, dann muss es in Hochkommas eingeschlossen sein,
also nicht nur x=A, sondern korrekt x=’A’. Das Formatelement für Zeichen
lautet übrigens %c, Sie konnten es schon im Beispielprogramm sehen.
Verlässt man den Bereich der ganzen Zahlen, so kommt man, wie Sie es
schon bei der Arithmetik gesehen haben, schnell zu den Kommazahlen. Da
aber erstens beim Programmieren kein Komma verwendet wird, sondern ein
Punkt, und man zweitens noch ausdrücken will, dass dieser Dezimalpunkt mal
hier, mal da stehen kann und seine Position nicht starr festgelegt ist, spricht
man hier von Gleitpunktzahlen. Den zugehörigen Datentyp float haben Sie
schon in vorherigen Beispielen bewundern können. Will man beispielsweise
die Zahl 123, 452867 in einer float-Variablen abspeichern, so wird man die
Programmzeilen
float x;
x=123.452867;
verwenden oder das Ganze direkt in dem einen Befehl float x = 123.452867
erledigen, der die Deklaration und die erste Wertbelegung verbindet. Intern
sieht die Zahl allerdings anders aus, da wird eine Exponentialschreibweise ver-
wendet. Aus der Potenzrechnug wissen Sie, dass 123, 452867 = 1, 23452867·102
gilt, und dem entspricht auch die Speicherungsmethode für float-Zahlen.
Sie werden aufgeteilt in eine Mantisse, das ist in meinem Beispiel die Zahl
1.23452867, einen Exponenten, das ist bei uns der Wert 2, und natürlich wie
üblich ein Vorzeichenbit. Dass man auf diese Weise ausgesprochen große Zah-
len darstellen kann, dürfte klar sein: Sie müssen ja nur einen etwas größeren
Exponenten verwenden wie zum Beispiel 17, und schon haben Sie eine Zahl
in der Größenordnung von 1017 dargestellt. Standardmäßig haben Sie in der
Mantisse eine Genauigkeit von sechs Stellen, während der Zehnerexponent in
aller Regel zweistellig bleiben muss.
Diese Exponentialdarstellung kann man auch direkt zur Angabe einer Zahl
verwenden, beispielsweise durch float x=3.553421e12. Damit wird die Zahl
3, 553421 · 1012 in der float-Variablen x gespeichert. In welcher Weise Sie die
Zahlen dann ausgeben, hängt von Ihrem Formatelement ab. Die Anweisung
printf("Die Zahl lautet %f",x);
wird natürlich die float-Zahl x in der üblichen Form mit Vor- und Nachkom-
mastellen ausgeben. Anders sieht es aus bei dem Ausgabekommando
printf("Die Zahl lautet %e",x);
denn das Formatelement %e sorgt für die Ausgabe in der Exponentialschreib-
weise.
Auch Gleitpunktzahlen kann man etwas genauer machen. Wollen Sie von
sechs auf zwölf Stellen gehen, so müssen sie Variablen vom Typ double ein-
setzen, sind sogar 24 Stellen erwünscht, ist der Datentyp long double zu
108 2 Strukturierte Programmierung mit C

empfehlen. Ansonsten verhalten sich die Zahlen dieser Typen genauso wie
float-Zahlen, weshalb ich mich nicht weiter darüber auslasse.
Noch ein paar Worte zu den Formatelementen für die verschiedenen Da-
tentypen, dann gehen wir sofort zu den Kontrollstrukturen über. Sie hatten
bereits gesehen, dass ganze Zahlen schlicht die Formatangabe %d zu schätzen
wissen, Gleitpunktzahlen je nach Darstellungsart auf %f oder %e warten und
Zeichen sich mit %c zufrieden geben. Man kann das aber noch etwas genauer
steuern. Wollen Sie beispielsweise sicher sein, dass Ihre ganze Zahl mit min-
destens sechs Stellen ausgegeben wird, dann verwenden Sie einfach das For-
matelement %6d; das kann hilfreich sein bei der Erstellung von Tabellen mit
Hilfe eines C-Programms. Entsprechendes gilt für Variablen vom Typ long,
die sehnlichst auf das Formatelement %ld warten. Interessant ist noch, wie Sie
das Formatelement %f exakter kontrollieren können. Sie können nämlich einer-
seits festlegen, wie viele Zeichen minedstens insgesamt für die Zahl ausgegeben
werden sollen: mit %6f würde eine float-Zahl beispielsweise eine Ausgabe-
breite von garantiert sechs Stellen aufweisen. Und andererseits können Sie
auch die Stellen nach dem Dezimalpunkt angeben, indem Sie zum Beispiel
%.2f schreiben und damit klar machen, das zwei Zeichen hinter dem De-
zimalpunkt erwünscht sind. Es wäre bedauerlich, beide Möglichkeiten nicht
kombinieren zu können, und natürlich ist auch das möglich. Mit einer Angabe
wie %6.2f weisen Sie an, dass die Ausgabe Ihrer float-Variablen mindestens
sechs Stellen breit sein soll, davon zwei Stellen nach dem Dezimalpunkt.

Datentypen
Zur Verarbeitung unterschiedlicher Zahlenarten stellt C verschiedene Daten-
typen bereit. Man unterscheider dabei zwischen Typen zur Verarbeitung von
ganzen Zahlen, von Zeichen und von Gleitpunktzahlen, wobei sich verschie-
dene Datentypen zu den ganzen Zahlen bzw. zu den Gleitpunktzahlen vor
allem in der Größe des darstellbaren Zahlenbereichs unterscheiden. Wichtige
Datentypen sind: int, long, short, unsigned int, char, float, double und
long double.
Zur Ausgabe von Variablen werden verschiedene Formatelemente einge-
setzt. Sie beginnen immer mit dem %-Zeichen und steuern die Ausgabe der
Variablen auf dem Bildschirm.

Übungen

2.4. Schreiben Sie ein C-Programm, das den Benutzer dazu auffordert, drei
ganze Zahlen einzugeben, diese drei Zahlen einliest und dann das Volumen des
Quaders ausrechnet, dessen Seitenlängen diese Zahlen darstellen. Natürlich
soll am Ende ein Antwortsatz ausgegeben werden.

2.5. Ändern Sie das Programm aus Aufgabe 2.4 so um, dass nicht mehr ganz-
zahlige Eingaben erwartet werden, sondern Gleitpunktzahlen. Testen Sie bei
2.3 Kontrollstrukturen 109

der Ausgabe der Ergebnisse verschiedene Möglichkeiten zur Angabe von For-
matelementen wie z.B. %f, %8.3f oder %e.

2.6. Schreiben Sie ein C-Programm, das folgendes leistet. Der Benutzer wird
aufgefordert, den Radius eines Kreises einzugeben, natürlich als Gleitpunkt-
zahl. Daraufhin berechnet das Programm den Umfang und die Fläche des
Kreises und gibt sie aus. Anschließend wird der Benutzer aufgefordert, den
Umfang eines weiteren Kreises einzugeben, und das Programm gibt den Ra-
dius und die Fläche des neuen Kreises aus.

2.7. Schreiben Sie ein C-Programm, mit dem man das Zusammenspiel der
beiden Datentypen int und char testen kann. Der Benutzer soll eine ganze
Zahl eingeben können und das Programm das Zeichen mit der entsprechenden
Nummer ausgeben.

2.8. Gegeben sei ein Rechner, auf dem int-Vaiablen zwei Byte im Arbeits-
speicher belegen. In eine int-Variable x wird der Wert 32767 eingelesen und
dann die Anweisung y = x+1 für eine weitere int-Variable y durchgeführt.
Warum hat dann y den Wert −32768?

2.9. Gegeben sei ein Rechner, auf dem int-Vaiablen zwei Byte im Arbeitsspei-
cher belegen. In eine unsigned int-Variable x wird der Wert 65535 eingelesen
und dann die Anweisung y = x+1 für eine weitere unsigned int-Variable y
durchgeführt. Warum hat dann y den Wert 0?

2.3 Kontrollstrukturen

Im letzten Abschnitt habe ich Sie sehr intensiv mit Details plagen müssen.
Leider war daa unvermeidbar, denn bevor man an die richtige und ernsthafte
Programmierung gehen kann, muss man wissen, mit welchen grundsätzlichen
Strukturen man es in der jeweiligen Programmiersprache zu tun hat. Das ist
jetzt aber erst mal erledigt; Sie wissen Bescheid über Variablen und Arith-
metik, über Datentypen und Formatelemente, und jetzt können wir loslegen
mit den zentralen Grundbausteinen der strukturierten Programmierung: den
Kontrollstrukturen. Im Prinzip sind sie immer gleich, ob Sie nun C einsetzen
oder Pascal, Java oder FORTRAN, Sie werden an den drei Kontrollstruk-
turen Sequenz, Auswahl und Wiederholung nie vorbei kommen. Das ist aber
auch gut so, denn sie sind sehr mächtige Hilfsmittel der Programmierung, und
davon abgesehen sind sie gar nicht schwer zu verstehen.
In diesem Abschnitt werden ich Ihnen die drei Kontrollstrukturen vorstel-
len und Ihnen zeigen, wie man sie einerseits in C umsetzt und andererseits
mit den graphischen Beschreibungsmethoden der Struktogramme und der Pro-
grammablaufpläne unabhängig von einer Programmiersprache darstellen kann.
110 2 Strukturierte Programmierung mit C

2.3.1 Sequenz

Am einfachsten ist die schlichte Sequenz zu verstehen. Dass jedes Programm


aus verschiedenen Anweisungen besteht, haben Sie natürlich schon längst mit-
bekommen, und eine Abfolge aus einzelnen Schritten, aus einzelnen Anweisun-
gen, die schön brav nacheinander auszuführen sind, nennt man eine Sequenz.
Zu jedem Zeitpunkt der Verarbeitung wird genau ein Schritt ausgeführt, wo-
bei jeder Schritt auch genau einmal ausgeführt wird, nichts wird ausgelassen,
nichts wiederholt. Sie können es sich vorstellen wie das vollständige Auslöffeln
einer Suppe: zu jedem Zeitpunkt wird genau ein Löffel gegessen, keiner wird
ausgelassen, denn Sie essen Ihre Suppe brav auf, und es wird auch keiner dop-
pelt gegessen, zumindest hoffe ich das. Was die Programmsequenz von der
Suppe unterscheidet, ist der Umstand, dass es im Programm eine klar defi-
nierte Reihenfolge der Abarbeitung gibt, die schon beim Aufschreiben einer
Sequenz deutlich wird; bei der Suppe ist das offenbar anders.
Ein kleines Beispiel einer Sequenz gibt das folgende Programm, das eine
Währungsumrechnung vornimmt. Nach dem Kurs des heutigen Tages ent-
spricht ein Euro genau einem US-Dollar und 31 US-Cent. Das Umrechnungs-
programm lautet dann folgendermaen.
/* geld.c -- waehrungen umrechnen */

#include <stdio.h>

main()
{
float enachd=1.31;
float euro, dollar;
printf("Geben Sie einen Euro-Betrag ein:\n");
scanf("%f",&euro);
dollar = euro * enachd;
printf("%6.2f Euro sind %6.2f Dollar\n",euro,dollar);
}
Das Programm ist wohl ziemlich selbsterklärend. Der eingegebene Euro-
Betrag wird mit dem Umrechnungsfaktor multipliziert, und anschließend wer-
den der Euro-Betrag und der Dollar-Betrag mit zwei Stellen nach dem De-
zimalpunkt ausgegeben. Eine klassische Sequenz, denn eines wird nach dem
anderen gemacht, alles passiert genau einmal, nichts wird ausgelassen, nichts
wiederholt.
Programmiertechnisch ist das kaum ein weiteres Wort wert. Da aber nicht
alle Programmierer die gleiche Programmiersprache verwenden, kann es nicht
schaden, so etwas auch anders darstellen zu können, unabhängig von einer
konkreten Programmiersprache, und die beiden beliebtesten Hilfsmittel zu
diesem Zweck sind Struktogramme und Programmablaufpläne.
Struktogramme wurden 1973 von Nassi und Shneiderman als Methode
zur Strukturierung vorgeschlagen, weshalb man sie auch ab und zu als Nassi-
2.3 Kontrollstrukturen 111

Shneiderman-Diagramme bezeichnet, und erfreuen sich mehr als 30 Jahre nach


ihrer Geburt heute noch großer Beliebtheit. Jeder Verarbeitungsschritt wird
in einem schlichten rechteckigen Kasten dargestellt, mehrere Verarbeitungs-
schritte zusammen ergeben einen Block. Den grundsätzlichen Aufbau sehen
Sie in Abbildung 2.1.

Abb. 2.1. Struktogramm

Hier werden drei Anweisungen in einem Block zusammengefasst, mehr


gibt ein so elementares Struktogramm nicht her. Auch mein kleines Programm
geld.c sieht als Struktogramm nicht aufregender aus als vorher; in Abbildung
2.2 können sie es bewundern

Abb. 2.2. Struktogramm zur Währungsumrechnung

Wie Sie sehen, bildet das Struktogramm genau die einzelnen Schritte mei-
nes Programms ab, aber man kann es auch umgekehrt sehen: das Struk-
112 2 Strukturierte Programmierung mit C

togramm liefert den Algorithmus für mein Programm, sozusagen den Pro-
grammentwurf, und als Programmierer habe ich nichts anderes mehr zu tun
als diesen Algorithmus in ein korrektes C-Programm zu übersetzen. Ob man
dabei im Struktogramm schon dem Programmierer die Datentypen für die
einzelnen Variablen vorgibt, ist ein wenig Geschmackssache; ich habe es hier
unterlassen. Vergessen Sie nicht: das Struktogramm ist unabhängig von der
Programmiersprache oder sollte es wenigstens sein, Datentypen aber können
sehr abhängig sein von der verwendeten Programmiersprache, also ist es si-
cher nicht sinnlos, in einem Struktogramm die Datentypen nicht zu konkret
werden zu lassen.
Mehr Struktogrammtechnik gibt eine schlichte Sequenz nicht her, also
können wir uns gleich ansehen, wie das Ganze in Form eines Programmab-
laufplanes, abgekürzt PAP, aussieht. Im Falle einer Sequenz besteht ein PAP
meistens nur aus zwei Arten von Elementen: aus Parallelogrammen, die eine
Ein- oder Ausgabe symbolisieren, und aus Rechtecken, mit denen Verarbei-
tungsschritte gekennzeichnet werden. Na gut, ein Start- und ein Endesymbol
sind auch noch oft dabei, aber viele Leute lassen das auch einfach weg. Wie
man diese Symbole kombiniert, sehen Sie in Abbildung 2.3

Start
geld

setze
enachd=1.31

Ausgabe:
Eingabeaufforderung
für euro

einlesen euro

dollar = euro * enachd

Ausgabe
euro und dollar

Ende
geld

Abb. 2.3. Programmablaufplan zur Währungsumrechnung


2.3 Kontrollstrukturen 113

Es passiert genau das Gleiche wie schon im Struktogramm, nur eben an-
ders dargestellt. Die einzelnen Schritte, sei es nun eine Ein-/Ausgabe oder
eine Verarbeitung, werden durch Pfeile miteinander verbunden, damit kein
Zweifel über die Reihenfolge entstehen kann, und in die Elemente werden die
jeweils nötigen Anweisungen geschrieben. Mehr steckt nicht dahinter, und viel
schwieriger wird es nicht.
Bevor ich mit der Auswahl zur nächsten Kontrollstruktur übergehe, soll-
te ich noch eines kurz erwähnen. Den Umrechnungsfaktor enachd hatte ich
ganz schlicht als Variable definiert und gleich bei der Deklaration mit einem
Wert versehen. Nun sollte aber eigentlich so ein Umrechnungsfaktor, wenn
er einmal festgelegt ist, nicht andauernd wieder geändert werden; auf irgend
etwas muss sich der Mensch ja verlassen können, auch wenn der europäische
Stabilitätspakt nicht das Papier wert ist, auf dem man ihn gedruckt hat. Um
eine nicht mehr allzu variable Variable auch in C darstellen zu können, gibt
es das Konzept der Konstanten. Anstatt innerhalb der main()-Funktion eine
Variable enachd zu definieren, hätten Sie auch durch eine Präprozessoranwei-
sung dem Compiler mitteilen können, dass er jetzt unf für alle Zeiten unter
dem Namen ENACHD die Zahl 1.31 verstehen soll. Das funktioniert ähnlich wie
schon bei der #include-Anweisung mit dem Zeichen #, und das Programm
lautet dann wie folgt:
/* geld.c -- waehrungen umrechnen */

#include <stdio.h>
#define ENACHD = 1.31

main()
{
float euro, dollar;
printf("Geben Sie einen Euro-Betrag ein:\n");
scanf("%f",&euro);
dollar = euro * ENACHD;
printf("%6.2f Euro sind %6.2f Dollar\n",euro,dollar);
}
Es hat sich eingebürgert, Konstanten mit Großbuchstaben zu schreiben,
das ist aber reine Konvention und vom Compiler nicht vorgeschrieben. Wenn
Sie sich aber dafür entscheiden, eine Konstante mit Großbuchstaben zu be-
zeichnen, dann müssen Sie das auch durchhalten und dürfen unterwegs bei-
spielsweise nicht mehr enachd anstatt ENACHD schreiben, denn das würde Ih-
nen der Compiler doch sehr übel nehmen.

Die Sequenz
Unter einer Sequenz versteht man eine Abfolge aus einzelnen Schritten, die
nacheinander auszuführen sind. Zu jedem Zeitpunkt der Verarbeitung wird
genau ein Schritt ausgeführt, wobei jeder Schritt genau einmal ausgeführt
wird.
114 2 Strukturierte Programmierung mit C

Eine Sequenz lässt sich sowohl mit Hilfe eines Struktogramms als auch
eines Programmablaufplanes darstellen; im Struktogramm kann man die An-
weisungen einer Sequenz zu einem Block zusammenfassen, im Programmab-
laufplan wird die Verarbeitungsreihenfolge durch Pfeile zwischen den einzelnen
Elementen symbolisiert.

2.3.2 Auswahl

Nichts gegen Sequenzen, aber das Leben hat nun mal nicht immer so einen or-
dentlichen Ablauf, wie das eine schlichte Sequenz gerne hätte. Oft genug muss
man Entscheidungen treffen, die dann den gesamten weiteren Lebensweg be-
einflussen, und so etwas passiert natürlich nicht nur bei Captain Picard auf
der Enterprise, sondern auch bei Programmen. Was ich Ihnen bisher gezeigt
habe, war zu unflexibel, zu starr, da der Ablauf ohne jede Variationsmöglich-
keit vorgegeben war. Sie brauchen sich nur einmal mein altes Beispiel der
Gehaltsabrechnung und Lohnbuchhaltung ins Gedächtnis zu rufen: je nach
Steuerklasse wird die Berechnung des Nettogehalts verschieden ablaufen, also
kann man sie nicht mit Hilfe einer starren Sequenz in ein Programm fassen.
Dieses Problem löst das Prinzip der Auswahl, die die Ausführung eines
Schrittes von einer bestimmten Bedingung abhängig macht. Das kennen Sie
aus dem richtigen Leben - wenn Sie beispielsweise für die nächste Klausur
lernen, dann haben Sie gute Chancen, sie zu bestehen, wenn nicht, dann
eben nicht. Es ist Ihre Entscheidung, und ähnliche Entscheidungen beein-
flussen auch den Ablauf eines Programms. Schon das schlichte Umrechnungs-
programm, das wir vorhin entwickelt haben, liefert hier ein Beispiel. Vielleicht
will der Benutzer ja gar nicht mehr umrechnen, weil ihm gerade eingefallen
ist, dass er dringen den Rasen mähen muss. Für solche Fälle sollte man ihm
eine gewissen Entscheidungsfreiheit lassen, die das folgende Programm garan-
tiert.
/* geld.c -- waehrungen umrechnen */

#include <stdio.h>
#define ENACHD 1.31

main()
{
float euro, dollar;
char antwort;
printf("Wollen Sie Euro in Dollar umrechnen?\n");
scanf("%c",&antwort);
if (antwort == ’j’)
{
printf("Geben Sie einen Euro-Betrag ein:\n");
scanf("%f",&euro);
2.3 Kontrollstrukturen 115

dollar = euro * ENACHD;


printf("%6.2f Euro sind %6.2f Dollar\n",euro,dollar);
}
}
Der Text erklärt sich fast von selbst. Das Programm fragt den Anwender,
ob er tatsächlich Euro in Dollar umrechnen möchte, und erwartet dann die
Eingabe seiner Antwort von der Tastatur. Zu diesem Zweck habe ich eine
char-Variable antwort definiert, die mit einem scanf()-Komamndo gefüllt
wird; achten Sie dabei vor allem auf das benötigte Formatelement und die An-
gabe des &-Zeichens vor dem Variablennamen. Danach beginnt die eigentliche
Auswahl. Mit dem Ausdruck if (antwort == ’j’) wird abgefragt, ob der
Benutzer das Zeichen j oder irgendetwas anderes eingegeben hat. Nur wenn er
sich wirklich mit dem Buchstaben j geäußert hat, wird der nach der Abfrage
stehende Block ausgeführt. Und so geht das immer. Es wird eine Bedingung
abgefragt, und falls diese Bedingung erfüllt ist, werden bestimmte Aktionen
durchgeführt, falls nicht, dann eben nicht. Die Abfrage der Bedingung er-
folgt stets nach dem Schema if (Bedingung). Sie beginnt also immer mit
dem Schlüsselwort if, an das sich dann, in Klammern gesetzt, die Bedingung
anschließt, die Sie überprüfen wollen. Vielleicht ist Ihnen aufgefallen, dass
bei der Abfrage aus meinem Beispielprogramm eine Seltsamkeit auftaucht:
offenbar wird hier gefragt, ob die Variable antwort mit dem Zeichen ’j’ be-
legt ist, aber die Bedingung lautet eben nicht ganz einfach antwort = ’j’,
sondern antwort ==’j’, mit zwei Gleichheitszeichen anstatt einem. Das hat
einen einfachen Grund. In C ist das gewöhnliche Gleichheitszeichen mit der
Wertzuweisung verbunden, weshalb so etwas wie antwort = ’j’ der Varia-
blen antwort das Zeichen ’j’ zuweisen würde anstatt danach zu fragen, ob
es schon da ist. Da Wertzuweisungen nun einmal etwas völlig anderes sind als
Vergleiche und Abfragen, hat man für die Abfrage, ob ein Teil gleich einem
anderen ist, das doppelte Gleichheitszeichen erfunden.
Und was sollen die Klammern nach der Abfrage? Nichts einfacher als das.
Die komplette Auswahl hat in dieser Form den Aufbau
if (Bedingung) Aktion;
Falls also die Bedingung erfüllt ist, soll die nächstfolgende Aktion ausgeführt
werden, und mehr nicht. Nun will ich aber gleich ein paar Dinge erledigt
wissen, falls der Benutzer wirklich umrechnen will, und nicht nur die eine An-
weisung, die direkt auf die Abfrage folgt. Damit der Compiler das erkennt,
fasse ich die Anweisungen, die ich als Einheit betrachten will, durch den Ein-
satz der Mengenklammern { und } zu einem Block zusammen, den dann der
Compiler tatsächlich als Einheit erkennt und akzeptiert. Sobald sie also meh-
rere Anweisungen durch die Mengenklammern als einen Block deklarieren,
wird auch der gesamte Block ausgeführt, falls die Bedingung erfüllt ist. Ist
das erledigt, geht das Programm einfach zur nächsten Anweisung über. Ist
die Bedingung dagegen nicht erfüllt, so wird das Programm bei der ersten
Anweisung nach dem bewussten Block weitergeführt. Ein kleines Beispiel soll
das verdeutlichen.
116 2 Strukturierte Programmierung mit C

/* beispiel.c */

#include <stdio.h>

main()
{
int x,y=-1;
printf("Bitte eine ganze Zahl x eingeben:\n");
scanf("%d",&x);
if (x > 0)
y=1;
printf("Der Wert fuer y lautet %d",y);
}
}
Die eingelesene int-Variable x wird gefragt, ob sie größer als 0 ist; in diesem
Fall soll y auf 1 gesetzt werden. Die anschließende Ausgabe von y findet in
jedem Fall statt, egal ob x größer oder kleiner als 0 ist, denn ich habe nach der
if-Abfrage nichts eingeklammert, sodass die Ausgabeanweisung nicht mehr
zum if-Block gehört. Nur ihr Ergebnis hängt natürlich vom x-Wert ab: war
x positiv, so wird für y der Wert 1 ausgegeben, war x negativ oder 0, so gibt
das Programm für y den vordefinierten Wert −1 aus. Sie sehen an diesem Bei-
spiel übrigens, dass es nicht nur Abfragen auf Gleichheit gibt, man kann auch
anderes testen. Hier habe ich den >-Vergleich angewendet, dazu stehen auch
noch die Vergleichsmöglichkeiten < und != zur Verfügung, wobei der letzte
Vergleich als ungleich“ zu verstehen ist, sowie die Mischformen <= und >=.

Der gesante if-Block lässt sich sehr angenehm durch ein graphisches Ele-
ment in einem Struktogramm darstellen; Sie sehen seinen Aufbau in Abbil-
dung 2.4

Abb. 2.4. Struktogramm zur Auswahl

Es wird eine Bedingung gestellt und überprüft, ob sie gültig ist. Falls sie
erfüllt ist, geht das Struktogramm in den Ja“-Zweig und führt die Anwei-

2.3 Kontrollstrukturen 117

sungen aus, die sich dort versammeln, falls nicht, wird am Ende des Auswahl-
Blocks weiter gemacht, wobei das seltsame Zeichen ∅ symbolisiert, dass nichts
geschehen soll. Mein kleines Beispielprogramm hat dann das Struktogramm
aus Abbildung 2.5.

Abb. 2.5. Auswahlblock in einem Struktogramm

Sie können mit bloßem Auge sehen, was los ist: nach dem Einlesen von x
wird mit der Abfrage nach dem Vorzeichen von x der if-Block angegangen.
Sobald er auf die eine oder andere Weise abgearbeitet ist, kann die Verar-
beitung einfach bei der nächsten Anweisung nach dem if-Block fortgeführt
werden.
Auch in einem Programmablaufplan kann man eine Auswahl darstellen,
das ist nicht schwerer als mit einem Struktogramm. Ich zeige es Ihnen gleich
an der Umsetzung meines kleinen Beispielprogramms in Abbildung 2.6. Es ist
natürlich Geschmackssache, aber ich persönlich finde das Struktogramm sau-
berer und übersichtlicher. In einem PAP wird die Abfrage durch eine Raute
symbolisiert, und von der Raute aus zweigen die Möglichkeiten ab, die sich
am Ende in dem kleinen Kreis wieder treffen: ist die Bedingung erfüllt, geht
man in den ja“-Zweig, falls nicht, geht man in den “nein“-Zweig, und am

Ende wird alles in dem durch einen Kreis dargestellten Sammelpunkt zusam-
mengeführt. Durch dieses Hin- und Herzweigen geht ein wenig an Struktur
verloren, das strenge Denken in einer ordentlichen Abfolge, auf das man in
der strukturierten Programmierung so großen Wert legt, verliert sich ein we-
nig in den Verzweigungen. Das kann in einem Struktogramm nicht passieren,
weil Sie dort stur Block für Block vorgehen und Verzweigungen klar einem
bestimmten Block zugeordnet sind.
Wie dem auch sei, bisher habe ich Ihnen nur die einfachste Form der
Auswahl vorgestellt. Im Allgemeinen wird man nicht nur eine Folge von An-
weisungen haben, die bei erfüllter Bedingung zu erledigen sind, sondern noch
118 2 Strukturierte Programmierung mit C

Start
beispiel

setze y= -1

einlesen x

x>0 ? ja

nein y= -1

Ausgabe y

Ende
beispiel

Abb. 2.6. Auswahl in einem Programmablaufplan

eine andere, die dann zum Einsatz kommt, wenn die Bedingung nicht erfüllt
ist. Es könnte ja beispielsweise sein, dass mein Umrechenprogramm entweder
von Euro in Dollar oder aber, je nach Benutzerwunsch, von Dollar in Euro
umrechnet. Das realisiert das nächste Programm.
/* geld.c -- waehrungen umrechnen */

#include <stdio.h>
#define ENACHD 1.31

main()
{
float euro, dollar;
int antwort;
printf("Geben Sie eine 1 ein, wenn Sie
Euro in Dollar umrechnen wollen\n");
printf("Geben Sie eine 2 ein, wenn Sie
Dollar in Euro umrechnen wollen\n");
scanf("%d",&antwort);
2.3 Kontrollstrukturen 119

if (antwort == 1)
{
printf("Geben Sie einen Euro-Betrag ein:\n");
scanf("%f",&euro);
dollar = euro * ENACHD;
printf("%6.2f Euro sind %6.2f Dollar\n",euro,dollar);
}
else
{
printf("Geben Sie einen Dollar-Betrag ein:\n");
scanf("%f",&dollar);
euro = dollar / ENACHD;
printf("%6.2f Dollar sind %6.2f Euro\n",dollar,euro);
}
printf("Ende der Umrechnung\n");
}
Der Benutzer wird gefragt, was er möchte: Euro in Dollar umrechnen oder
Dollar in Euro. Je nach Eingabe wird dann die eine oder die andere Ver-
arbeitung angestoßen, wobei ich mir die Einführung eines weiteren Umrech-
nungsfaktors erspart habe und beim Umrechnen von Dollar nach Euro ein-
fach durch den bekannten Faktor ENACHD teile. Im Deutschen würde man das
Ganze so formulieren: Falls die Bedingung erfüllt ist, mache dies, ansonsten

mache das“. Und dieses ansonsten“ heißt sowohl im Englischen als auch in

der Sprache C else“ - das berühmte else, mit dem der Chefprogrammierer

seine Probleme hatte.
Dass man auch diese etwas bessere Auswahl mit einem Struktogramm
darstellen kann, wird keinen überraschen, und dass sie wie in Abbildung 2.7
aussieht, wahrscheinlich auch nicht.

Abb. 2.7. Auswahlblock in einem Struktogramm

Der bisher leere nein“-Zweig wird jetzt mit Anweisungen gefüllt, mehr

steckt nicht dahinter. Nicht anders sieht es aus bei der Darstellung im Pro-
grammablaufplan: bisher lief der nein“-Zweig einfach nur auf den Sammel-

punkt zu, jetzt tauchen dort noch Anweisungen auf.
120 2 Strukturierte Programmierung mit C

nein Bedingung ja

Anweisungen Anweisungen

Abb. 2.8. Auswahl in einem Programmablaufplan

Geändert hat sich nicht viel. Wie zuvor wird eine Bedingung abgefragt,
und je nach Antwort geht man in den einen oder in den anderen Zweig. So-
bald die jeweiligen Anweisungen ausgeführt sind, geht es am Sammelpunkt
weiter, egal ob die Bedingung erfüllt war oder nicht. Um es noch einmal zu
sagen: die Anweisungen müssen nicht nur in Form eines einzelnen Befehls vor-
liegen, sondern können natürlich auch aus einer ganzen Sequenz von Befehlen
bestehen.
Die Darstellung meines kleinen Euro-Dollar/Dollar-Euro-Programms als
Struktogramm und als PAP werde ich Ihnen als Übungsaufgabe überlas-
sen und mich hier lieber noch der einen oder anderen Verbesserung unserer
Auswahl-Struktur widmen. Zunächst müssen wir uns mit einer kleinen Un-
klarheit befassen, die auf der Möglichkeit der Schachtelung von if-Abfragen
beruht. Sie können innerhalb eines if-Blocks und natürlich auch im else-
Block noch weitere if-Abfragen starten, was dann unter Umständen zu einer
Situation wie der folgenden führt.
if (x > 0)
if (y > 0)
z = y;
else
z = x;
Es ist hier klar, dass z=y gesetzt werden soll, sofern y>0 gilt. Aber worauf
bezieht sich der else-Zweig: auf das erste if oder auf das zweite? Das macht
einen gewaltigen Unterschied, denn im ersten Fall würde man z=x setzen,
wenn nicht x>0 gilt, und im zweiten wäre z=x zu setzen, falls erstens x>0
gilt und darüber hinaus noch die Bedingung y>0 nicht gilt. Die Lösung des
Problems ist ganz einfach. Der else-Zweig wird, sofern Sie keine Klammern
setzen, immer dem letzten if zugeordnet, für das noch kein else existiert. In
meinem kleinen Beispiel stellt das else also die Alternative zu der Bedingung
y > 0 dar und nicht zu x > 0. In einem Struktogramm könnte dieses Inter-
2.3 Kontrollstrukturen 121

pretationsproblem erst gar nicht auftreten, da die strenge Blockstruktur von


vornherein alles klar werden lässt, wie Sie in Abbildung 2.9 sehen.

Abb. 2.9. Geschachtelte Auswahl

Sollten Sie aber das Bedürfnis haben, von dieser festen Regel abzuwei-
chen, dann kann Ihnen leicht geholfen werden: Sie müssen nur Klammern an
die richtige Stelle setzen. Was passiert zum Beispiel bei dem folgenden Pro-
grammstück?
if (x > 0)
{
if (y > 0)
z = y;
}
else
z = x;
Hier ist die Lage wieder ganz anders. Falls nämlich x>0 gilt, wird der einge-
klammerte Block ausgeführt; der else-Zweig befindet sich außerhalb dieses
Blocks und kann daher auf keinen Fall zu dem inneren if gehören, das in den
Block eingeschlossen ist. Daher gehört dieses else zum äußeren if, und wir
haben die Situation aus Abbildung 2.10.
Sobald also in einem if-Zweig mehrere Anweisungen vorkommen, sollte
man sie unbedingt in Mengenklammern setzen, selbst wenn es nach der oben
angesprochenen Regel vielleicht gar nicht nötig wäre: es steigert in jedem
Fall die Verständlichkeit des Programms. Dass Sie zusätzlich auch noch die
Anweisungen, die inhaltlich zusammen gehören und deshalb innerhalb eines
Blocks stehen, auch noch einrücken sollten, ist aus den bisher behandelten
Beispielprogrammen hoffentlich ebenfalls deutlich geworden; dem Compiler
ist die Einrückung zwar völlig egal, aber auch sie lässt Ihre Programme für
den menschlichen Leser wesentlich deutlicher werden.
Eine Schachtelung von if-Abfragen hat übrigens auch mein Umrechnungs-
programm dringend nötig. Werfen Sie noch einmal einen Blick darauf: wenn
122 2 Strukturierte Programmierung mit C

Abb. 2.10. Geschachtelte Auswahl

der Benutzer eine 1 eingibt, wird von Euro nach Dollar umgerechnet, und wenn
er eine 2 eingibt von Dollar nach Euro, so viel ist wahr. Kommt er allerdings
auf die Idee, eine 3, 17 oder sonst irgendeinen Blödsinn in die Tastatur zu
hauen, dann wird das Programm ebenfalls von Dollar nach Euro umrechnen,
da es die zweite Eingabemöglichkeit überhaupt nicht abfragt. Wann immer Sie
etwas von 1 Verschiedenes eingeben, wird die Umrechnung von Dollar nach
Euro vorgenommen, und der Benutzer fragt sich, was er falsch gemacht hat. In
diesem Fall war aber nicht der Benutzer schuld - obwohl er das nach Meinung
der Programmierer fast immer ist -, sondern eindeutig der Programmierer,
der besser das folgende Programmstück für seine Abfragen verwenden sollte.
if (antwort == 1)
{
printf("Geben Sie einen Euro-Betrag ein:\n");
scanf("%f",&euro);
dollar = euro * ENACHD;
printf("%6.2f Euro sind %6.2f Dollar\n",euro,dollar);
}
else
{
if (antwort == 2)
{
printf("Geben Sie einen Dollar-Betrag ein:\n");
scanf("%f",&dollar);
euro = dollar / ENACHD;
printf("%6.2f Dollar sind %6.2f Euro\n",dollar,euro);
}
else
printf("Fehlerhafte Eingabe\n");
}
Erst jetzt können Sie garantieren, dass die Umrechnung genau bei der Ein-
2.3 Kontrollstrukturen 123

gabe der Zahlen 1 oder 2 stattfindet, bei allen anderen Eingaben gibt es eine
Fehlermeldung.
Bleiben wir noch einen Moment bei der Schachtelung von if-Abfragen.
Damit die Sache nicht zu kompliziert wird, hat man sich für übersichtliche
Fälle eine recht praktische Konstruktion ausgedacht, die auf der oben erwähn-
ten Regel beruht: die Anwendung von else if. Das folgende Programmstück
zeigt Ihnen, was damit gemeint ist.
if (Bedingung 1)
Anweisungen 1
else if (Bedingung 2)
Anweisungen 2
else if (Bedingung 3)
Anweisungen 3
else
Anweisungen 4
Es erklärt sich tatsächlich fast von selbst. Eine Bedingung nach der anderen
wird abgearbeitet und ausgewertet, und sobald eine Bedingung zutrifft, wer-
den die zu dieser Bedingung gehörenden Anweisungen ausgeführt - die müssen
Sie natürlich, falls es mehrere sind, wieder durch Mengenklammern zu einem
Block zusammenfassen. Ist also beispielsweise Bedingung 2 erfüllt, Bedingung
1 aber nicht, so wird das Programm nicht in den ersten Anweisungsteil ge-
hen, aber sicher in den zweiten und dort die entsprechenden Anweisungen
ausführen. Was in den nachfolgenden else-Zweigen steht, interessiert dann
keinen mehr. Es kann aber vorkommen, dass keine der Bedingungen erfüllt
ist, und auch dafür ist vorgesorgt: Durchlaufen Sie erfolglos alle angegebe-
nen Bedingungen, ohne auch nur ein einziges Mal auf Zustimmung zu stoßen,
werden einfach die Anweisungen aus dem letzten else-Teil durchgeführt.
Eins fehlt uns noch zum Glück, dann ist das if erledigt. Bisher konnte ich
immer nur eine schlichte Bedingung wie z.B. x>0 abfragen. Das Leben ist aber
manchmal komplizierter und verlangt von mir, Bedingungen miteinander zu
verknüpfen und abzufragen, ob gleichzeitig x>0 und x<17 gilt. Kein Problem,
dafür gibt es die logischen Verknüpfungen, die Ihnen schon im ersten Kapi-
tel begegnet sind. Die Schreibweise ist allerdings etwas gewöhnungsbedürf-
tig: für und“ schreibt man &&, für oder“ schreibt man ||, und für nicht“
” ” ”
schreibt man !. Die eben angeführte Anfrage würde also if (x>0 && x<17)
lauten. Will man abfragen, ob eine Zahl a ungleich einer anderen Zahl b ist,
gibt es sogar zwei Möglichkeiten. Einmal können sie das bereits besprochene
Ungleichheitszeichen nehmen und die Abfrage als if (a != b) fomulieren,
Sie können aber auch die Bedingung a==b verneinen und die Formulierung
!(a==b) vorziehen. Beides wird dann bejaht, wenn a und b zwei verschiedene
Zahlen sind.

Auswahlstrukturen mit der if-Anweisung


Auswahlen zwischen mehereren Möglichkeiten kann man mit Hilfe der if-
Anweisung realisieren. Sie hat die grundsätzliche Form
124 2 Strukturierte Programmierung mit C

if (Bedingung)
Anweisungen 1
else
Anweisungen 2
Mehrere if-Abfragen können ineinander geschachtelt werden, wobei auf die
korrekte Zuordnung der jeweiligen else-Zweige zu den if-Abfragen zu achten
ist. In übersichtlichen Situationen kann man die Verschachtelung mit Hilfe der
else if-Konstruktion vornehmen.
Die Bedingungen, die abgefragt werden, können durch &&, || und ! ver-
knüpft werden.

Man kann sich aber Situationen vorstellen, in denen das Schachteln von if-
Abfragen zu kompliziert und unübersichtlich wird. Manchmal ist es einfacher,
so genannte Fallunterscheidungen zu treffen, die Sie in C mit einem einfachen
Sprachmittel realisieren können. Nehmen wir beispielsweise an, Ihr Programm
hat eine ganzzahlige Variable nummer, von der man hofft, dass sie zwischen 1
und 7 liegt. Ist das der Fall, dann soll die Nummer als Wochentagsnummer
interpretiert und der Name des entsprechenden Tages ausgegeben werden. Ist
das aber nicht der Fall, sollte das Programm eine entsprechende Fehlermel-
dung liefern. Nichts kann Sie daran hindern, dieses Problem mit mehrfach
geschachtelten if-Abfragen zu lösen. Es geht aber auch etwas übersichtlicher,
wie das folgende Programmstück zeigt.
switch(nummer)
{
case 1: printf("Montag"); break;
case 2: printf("Dienstag"); break;
case 3: printf("Mittwoch"); break;
case 4: printf("Donnerstag"); break;
case 5: printf("Freitag"); break;
case 6: printf("Samstag"); break;
case 7: printf("Sonntag"); break;
default: printf("kein Wochentag");
}
Die Variable nummer ist mit irgendeiner ganzen Zahl belegt. Handelt es sich da-
bei um eine der Zahlen von 1 bis 7, dann werden die Anweisungen ausgeführt,
die direkt hinter der entsprechenden Zahl nach der case-Anweisung steht. Die
Belegung von nummer mit dem Wert 5 führt also dazu, dass das switch()-
Kommando die nach den case-Anweisungen stehenden Zahlen so lange mit
der Zahl 5 vergleicht, bis festgestellt wird, dass tatsächlich eine Übereinstim-
mung existiert und die nachfolgenden Anweisungen ausgeführt werden. So
weit, so gut, aber was soll dieses ewige break nach den Ausgabekommandos?
Das ist eine klare Schwäche von C, an der beispielsweise die Programmierspra-
che Pascal nicht leidet: sobald Sie die Anweisungen nach der entsprechenden
case-Marke erledigt haben, wird alles nachstehende auch noch abgearbeitet,
als ob es nie eine Fallunterscheidung gegeben hätte - es sei denn, man setzt
2.3 Kontrollstrukturen 125

eben dieses ominöse break-Kommando. Das break-lose Programmstück


int nummer;
scanf("%d",&nummer);
switch(nummer)
{
case 1: printf("Montag");
case 2: printf("Dienstag");
case 3: printf("Mittwoch");
case 4: printf("Donnerstag");
case 5: printf("Freitag");
case 6: printf("Samstag");
case 7: printf("Sonntag");
default: printf("kein Wochentag");
}
würde zwar im Falle nummer=5 ganz richtig die Ausgabe freitag liefern, aber
sich von den danach stehenden Anweisungen nicht abschrecken lassen und
auch noch Samstag, Sonntag und kein Wochentag ausgeben, was sicher nicht
im Sinne des Programmierers liegt. Die Anweisung break sorgt dafür, dass der
aktuelle Block beendet und verlassen wird, ganz egal was noch anstehen mag;
das bedeutet hier, dass nach der Erledigung der gewünschten Verarbeitung
die Fallunterscheidung beendet wird.
Hinter den Sinn der default-Anweisung sind Sie sicher schon selbst ge-
kommen: falls die switch()-Anweisung keinen der geforderten Fälle liefern
kann, tritt automatisch der default-Fall in Kraft und die zu ihm gehören-
den Anweisungen werden ausgeführt. Es ist übrigens nicht nötig, dass der
switch()-Anweisung eine fertige ganze Zahl mitgegeben wird. Sie können
auch eine Formel als Input für das switch() verwenden, einen arithmeti-
schen Ausdruck wie zum Beispiel i*j+5, Hauptsache das Resultat ist eine
ganze Zahl. Und nicht einmal das darf man ganz wörtlich nehmen, denn Sie
haben ja gelernt, dass Variablen vom Datentyp char intern behandelt werden
wie ganze Zahlen, weshalb das switch()-Kommando auch char-Variablen ak-
zeptiert. Beispiel gefällig? Lässt sich machen.
char nummer;
scanf("%c",&nummer);
switch(nummer)
{
case ’1’: printf("Montag");
case ’2’: printf("Dienstag");
case ’3’: printf("Mittwoch");
case ’4’: printf("Donnerstag");
case ’5’: printf("Freitag");
case ’6’: printf("Samstag");
case ’7’: printf("Sonntag");
default: printf("kein Wochentag");
}
126 2 Strukturierte Programmierung mit C

Selbstverständlich ist nummer jetzt keine Zahl mehr, sondern ein Zeichen, und
daher wird auch nicht mehr nach den Zahlen 1 bis 7 gefragt, sondern nach
den Zeichen ’1’ bis ’7’, das ist der ganze Unterschied. Beachten Sie übrigens,
dass nach der case-Marke keine zu berechnenden Ausdrücke oder Variablen
stehen dürfen; hier brauchen Sie wirklich einen konstanten Wert.
Auch für die Fallunterscheidung gibt es ein Struktogramm, Sie sehen es in
Abbildung 2.11.

Abb. 2.11. Fallunterscheidung als Struktogramm

Hier werden vier Fälle unterschieden, die dann zu vier möglichen Aktionen
führen. Trifft keiner der vier Fälle ein, wird der sonst“-Zweig aktiviert und

die zugehörigen Aktionen kommen zur Anwendung.
Während also die Fallunterscheidung im Rahmen der Struktogramme ein
eigenes graphisches Beschreibungmittel besitzt, muss man sie beim Einsatz
eines Programmablaufplanes sozusagen zu Fuß herstellen durch eine leichte
Variation der bereits besprochenen Abfrage. Das sieht dann so aus wie in
Abbildung 2.12. Der Einfachheit halber habe ich in diesem PAP nur zwei
Fälle sowie den sonst“-Fall unterschieden; der Programmablaufplan dürfte

ansonsten selbsterklärend sein.

Die Fallunterscheidung
Ist ein Ausdruck gegeben, der ganzzahlige Werte liefert, und sollen für ver-
schiedene Werte verschiedene Aktionen ausgeführt werden, so verwendet
man die switch() - case-Konstruktion, um eine Fallunterscheidung durch-
zuführen. Wird bei der Auswertung des Ausdrucks einer der nach case ste-
henden Werte gefunden, werden die nachstehenden Anweisungen ausgeführt.
Es ist daher nötig, die zu einem Wert gehörenden Anweisungen mit der break-
Anweisung abzuschließen, um den switch()-Block an der richtigen Stelle zu
verlassen. Falls keine der zu den case-Marken gehörenden Konstanten aus
dem Ausdruck ermittelt wird, tritt automatisch der default-Fall in Kraft
und die zu ihm gehörenden Anweisungen werden ausgeführt.
Fallunterscheidungen lassen sich sowohl in Struktogrammen als auch in
Programmablaufplänen darstellen.
2.3 Kontrollstrukturen 127

Fall 1 Fall ? sonst

Fall 2

Aktion 1 Aktion 2 Aktion

Abb. 2.12. Fallunterscheidung im Programmablaufplan

Sie merken schon, der Abschnitt über Kontrollstrukturen ist nicht ganz
kurz, und es kann nicht schaden, zwischendurch ein wenig zu üben. Dazu
geben Ihnen unter anderem die Aufgaben 2.11 und 2.12 Gelegenheit.

2.3.3 Wiederholung als nicht abweisende Schleife

Die letzte der drei klassischen Kontrollstrukturen finden Sie jeden Tag auch
ohne Computer im täglichen Leben. Manche Dinge wiederholen sich eben
immer und immer wieder, man steht morgens auf, geht ins Bad, frühstückt
oder auch nicht und verlässt die Wohnung. Diesen Ablauf müssen Sie offenbar
nicht jeden Tag neu lernen, mit der Zeit funktioniert das alles ohne weiteres
Nachdenken automatisch, und auf dem gleichen Prinzip der Wiederholung
oder auch Iteration beruht auch die Kontrollstruktur, über die ich jetzt reden
will. Sie tritt in drei verschiedenen Varianten auf, die eigentlich alle das Gleiche
machen, aber eben auf verschiedene Weisen aufgeschrieben werden.
Beginnen wir mit der ersten Variante. Ich komme noch einmal zurück auf
mein Umrechnungsprogramm und bitte Sie, sich in die Lage eines Geldwechs-
lers zu versetzen, der irgendwo auf Teneriffa in seiner Wechselstube sitzt und
den lieben langen Tag Dollars in Euro umtauscht. Das ist ohnehin schon kein
reines Vergnügen, also sollte man ihm das Leben nicht noch schwerer machen
als nötig. Sofern er die Umrechnung nämlich computergestützt durchführt,
muss er nach dem bisherigen Stand der Dinge bei jedem neuen Kunden das
Umrechnungsprogramm neu starten, den ganzen Tag lang. Sinnvoller wäre es
auf jeden Fall, ein etwas besser konzipiertes Umrechnungsprogramm einmal
am Tag, bei Dienstbeginn zu starten und dann mit Hilfe dieses einen Pro-
gramms immer wieder das Gleiche zu tun, nämlich Umrechnungen von Dollars
128 2 Strukturierte Programmierung mit C

in Euro vorzunehmen. Anders gesagt: der Mann braucht ein Programm, das
Wiederholungen zulässt, und das werde ich ihm mit dem folgenden Programm
liefern. Es löst nicht nur das Problem des Wechselstubenbesitzers , sondern
führt auch gleich noch die so genannte do-Schleife in das Geschehen ein.
/* geld.c -- waehrungen umrechnen */

#include <stdio.h>
#define DNACHE 0.76336

main()
{
float euro, dollar;
char antwort;
do
{
printf("Geben Sie einen Dollar-Betrag ein:\n");
scanf("%f",&dollar);
getchar();
euro = dollar * DNACHE;
printf("%6.2f Dollar sind %6.2f Euro\n", dollar, euro);
printf("Weitere Umrechnung?(j/n) \n");
scanf("%c",&antwort);
getchar();
}
while(antwort == ’j’ || antwort == ’J’);
printf("Feierabend\n");
}
Nach der Definition der nötigen Variablen wird mit dem Schlüsselwort do
angezeigt, dass jetzt eine Iteration beginnt, eine Schleife. Alles was innerhalb
des Schleifenrumpfes steht, also in dem Block, der sich zwischen do und while
befindet, wird so lange ausgeführt, wie die in der while-Anweisung angegebe-
ne Bedingung es erlaubt. Das Programm wird daher auf jeden Fall mindestens
einmal die Anweisungen im Schleifenrumpf ausführen, denn am Anfang der
ganzen Sache kann es nicht wissen, ob am Ende vielleicht eine Weiterführung
abgelehnt wird. Mein Umrechnungsprogramm wird also klaglos mindestens
eine Umrechnung erledigen und dann nachfragen, ob eine weitere Umrech-
nung erlaubt ist. Der Benutzer hat jetzt die Möglichkeit, sich zu äußern: will
er eine weitere Umrechnung, so muss er ein j oder ein J eingeben. Damit
sind alle Anweisungen aus dem Schleifenrumpf angearbeitet, und das Pro-
gramm überprüft, ob Bedingung while-Bedingung tatsächlich zutrifft. Falls
ja, beginnt die Schleife wieder von vorn und der Benutzer kann eine weitere
Umrechnung erledigen, falls nein, wird der Laden für heute geschlossen und
das Programm gibt nur noch die Aufforderung zum Feierabend aus, den es
dann sofort antritt. Beachten Sie übrigens, dass das Schlüsselwort do nicht
mit einem Semikolon abgeschlossen wird! Das ist auch ganz wichtig, denn die
2.3 Kontrollstrukturen 129

do-Anweisung ist erst dann beendet, wenn der Anweisungsblock feststeht und
die Durchführungsbedingung aufgeschrieben ist. Würden Sie nach dem do ein
Semikolon setzen, so würde der Compiler überhaupt nicht erkennen, dass der
nachfogende Block und die angegebenen Bedingungen etwas mit diesem do zu
tun haben, und Ihnen eine Fehlermeldung liefern.
Einen neuen Befehl muss ich aber noch erklären. Wozu dieses getchar()?
Eigentlich wird doch die Eingabe bereits mit scanf() erledigt. So ist es auch
- fast, nicht ganz. Welche Tasten drücken Sie, wenn Sie beispielsweise Ihr j
als Antwort eintippen? Erstens den Buchstaben j und zweitens die Return-
Taste. Beides zusammen wird im Puffer gespeichert und nur der eingegebene
Buchstabe wird direkt dem Programm übergeben und in die Variable antwort
geschrieben. Auch das Drücken der Return-Taste wurde aber als ein Zeichen
dem Puffer einverleibt, und beim nächsten Eingabevorgang glaubt das Pro-
gramm dann, eben dieses Return-Zeichen wäre das aktuell einzulesende Zei-
chen, denn es steht ja noch im Puffer. Diesem Problem können Sie begegnen,
indem Sie direkt nach dem scanf()-Kommando das getchar() Kommando
absetzen. Es hat nicht viel zu tun, nur ein Zeichen einzulesen und es nirgend-
wohin zu schreiben, aber durch seinen Einsatz wird das Return-Zeichen aus
dem Puffer gelesen und die nächsten Eingaben kommen wieder ungehindert
an ihr Ziel. Im Umgang mit int-Variablen können Sie sich diesen Umstand
ersparen, weshalb ich es auch bei C-Programmen meistens vorziehe, die Abfra-
gen mit einer int-Variable vorzunehmen, den Wert 1 als ja“ zu interpretieren

und den Wert 0 als nein“.

Man nennt eine Schleife dieser Art eine nicht abweisende Schleife, weil
in jedem Fall mindestens ein Schleifendurchlauf stattfinden wird, denn die
Bedingung wird erst am Ende der Schleife geprüft. Diese Bedingung ist übri-
gens eine Durchführungsbedingung, das heißt es kommt genau dann zu einem
weiteren Schleifendurchlauf, wenn die Bedingung erfüllt ist. Andere Program-
miersprachen wie zum Beispiel Pascal machen es genau anders herum und
verwenden eine Abbruchbedingung; die Entwickler von C waren anscheinend
eher Vertreter des positiven Denkens.

Abb. 2.13. Nicht abweisende Schleife als Struktogramm


130 2 Strukturierte Programmierung mit C

Natürlich gibt es zur nicht abweisenden Schleife auch die Möglichkeit einer
Darstellung im Struktogramm. In Abbildung 2.13 besteht der Schleifenrumpf
aus drei Anweisungen, die auf jeden Fall einmal durchgeführt werden und
deren weitere Durchführung dann davon abhängt, ob die am Ende geprüfte
Durchführungsbedingung erfüllt ist. Falls ja, fängt alles von vorne an, falls
nein, ist die Schleife beendet und es geht nach dem Schleifenblock weiter.
Man kann an dem Struktogramm übrigens deutlich sehen, warum man diese
Schleifenart auch als fußgesteuerte Schleife bezeichnet, denn die alles entschei-
dende Durchführungsbedingung steht nun mal unten, am Fuß der Schleife.
Auch ein Programmablaufplan erlaubt die Darstellung einer Iteration
in Form einer nicht abweisenden Schleife, wie Sie in Abbildung 2.14 sehen
können.

Anweisungen

Bedingung ? Ja

Nein

Abb. 2.14. Nicht abweisende Schleife im Programmablaufplan

Sehr überraschend ist das Bild sicher nicht. Es werden irgendwelche An-
weisungen ausgeführt, danach wird abgefragt, ob eine bestimmte Bedingung
erfüllt ist, und wenn das der Fall ist, fängt man wieder mit der Ausführung
der eben erwähnten Anweisungen an. Was mich an dieser Darstellungsweise
stört, ist eine leichte Tendenz zur nicht strukturierten Programmierung. Der
PAP sagt mir eindeutig und klar, dass ich nach der Abfrage der Bedingung
gegebenenfalls an eine bestimmte Stelle des Ablaufs zu springen habe, und
Sprünge wollte man doch eigentlich vermeiden. Sicher: die Sprünge gehen im-
mer an den Anfang des Schleifenrumpfes und richten daher kein Unheil an,
aber mit den gleichen graphischen Möglichkeiten kann ich auch an irgendeine
beliebige Stelle meines Algorithmus springen, was ich nun wirklich auf keinen
Fall tun sollte. Mit Struktogrammen ist so etwas von vornherein unmöglich.
2.3 Kontrollstrukturen 131

Sehen wir uns noch ein kleines Beispiel für eine nicht abweisende Schleife
in C an, bevor ich Ihnen die nächste Schleifenart vorstelle. Ich will die Summe
der ersten soundsoviel natürlichen Zahlen ausrechnen. Zu diesem Zweck lasse
ich den Benutzer eine int-Variable n mit Leben füllen und berechne dann die
Summe der ersten n natürlichen Zahlen, angefangen bei 1.
/* summe.c -- summe der ersten n zahlen */

#include <stdio.h>

main()
{
int n, i=1, s=0;
do
{
printf("Eine natuerliche Zahl eingeben.\n");
scanf("%d",&n);
if (n <= 0)
printf("%d ist keine natuerliche Zahl, bitte nochmal:\n",n);
}
while (n <= 0);

do
{
s = s+i;
i = i+1;
}
while(i <= n);
printf("Die Summe lautet %d\n", s);
}
Dieses Programm ist sicher ein paar Worte wert. Mein Ziel ist es, die Sum-
me der ersten soundsoviel natürlichen Zahlen zu berechnen, und damit das
auch funktioniert, muss ich erst mal eine natürliche Zahl zur Verfügung haben.
Selbstverständlich verwende ich dafür eine int-Variable, aber das schließt lei-
der keine negativen Zahlen aus, also stelle ich mit Hilfe der ersten do-Schleife
sicher, dass es sich bei n auch wirklich um eine positive Zahl handelt: gibt
der Benutzer eine negative Zahl oder die Null ein, so geht ihm das Programm
so lange mit der ewig gleichen Meldung, er solle doch bitte eine natürliche
Zahl eingeben, auf die Nerven, bis er entweder endlich eine positive Zahl her-
ausrückt oder mit der Axt auf den Computer haut. Daran schließt sich die
eigentliche Berechnung der Summe an, die Sie sich genau ansehen sollten,
denn auf diese Weise pflegt man oft Summenberechnungen zu programmie-
ren. Am Anfang des Programms habe ich die Variablen i und s gleich mit
den Werten i=1 und s=0 initialisiert, das heißt: sie sofort bei der Deklaration
mit diesen Werten versehen. Auf s soll am Ende die gesuchte Summe stehen,
132 2 Strukturierte Programmierung mit C

während i eine Art Hilfsvariable darstellt, auf die im Laufe des Hochaddierens
die einzelnen Summanden geschrieben werden.
Was passiert nun, wenn die Schleife startet? Einmal läuft sie mindestens,
da es sich um eine nicht abweisende Schleife handelt. Im ersten Schleifen-
durchlauf wird zunächst die Anweisung s=s+i durchgeführt. Das ist nicht
mathematisch zu verstehen, sondern wie üblich eine schlichte Wertzuweisung:
der Variablen s wird die Summe aus dem zugewiesen, was derzeit auf s und
aus i steht. Da mit beiden Variablen bisher noch nichts passiert war, hat s
vor dieser Wertzuweisung den Inhalt 0 und i den Inhalt 1. Also wird s die
Summe 0+1 zugewiesen, weshalb nach der ersten Wertzuweisung auf s der
Wert 1 steht. in der nächsten Anweisung soll dann der Variablen i der um 1
erhöhte bisherige Wert von i zugewiesen werden. Die mathematisch verstan-
dene Gleichung i = i + 1 wäre natürlich übelster Blödsinn: eine Zahl kann
nicht die gleiche bleiben, wenn man sie um 1 erhöht. Das ist aber auch mit
der Anweisung i=i+1 nicht gemeint, hier soll nur der Variablen i der um 1
erhöhte bisherige Wert der Variablen i zugewiesen werden. Mit anderen Wor-
ten: durch diese Anweisung wird i einfach nur um 1 erhöht und hat also nach
dem ersten Schleifendurchlauf den Wert 2.
Nehmen wir jetzt einma an, ich hätte am Anfang für n den Wert 3 eingege-
ben. Dann wird offenbar die Bedingung i<=n locker erfüllt, und das Programm
geht in den nächsten Schleifendurchlauf. Nun steht aber auf s der Wert 1 und
auf i der Wert 2, also wird die erste Wertzuweisung im Schleifenrumpf dazu
führen, dass auf s jetzt der Wert 1 + 2 = 3 steht. Daraufhin wird wieder i
um 1 erhöht, erhält also den Wert 3 und kann noch immer den Vergleichstest
mit der Variablen n bestehen, denn auch die steht auf 3. Somit kommt es zu
einem letzten Aufbäumen meiner do-Schleife, noch einmal werden die Anwei-
sungen ihres Schleifenrumpfes durchlaufen. Was dabei passiert, ist wohl klar:
der aktuelle Wert von i wird auf den aktuellen Wert von s addiert, das er-
gibt 3 + 3 = 6, und anschließend wird i traditionsgemäß um 1 erhöht. Damit
verläßt i allerdings den grünen Bereich der Schleifenbedingung, denn kaum
einer wird behaupten wollen, dass 4 ≤ 3 gilt. Die do-Schleife ist also beendet
und die Summe kann ausgegeben werden.
Wie schon gesagt: Schleifen dieser Art findet man häufiger, wenn es darum
geht, irgendwelche Summen aus vielen Summanden oder Produkte aus vie-
len Faktoren auszurechnen; deshalb habe ich sie so genau besprochen. Viele
finden übrigens die Schreibweise i=i+1 ausgesprochen schwerfällig und ver-
wenden lieber die kompaktere Anweisung i++. Sie bedeutet so ziemlich das
Gleiche, man muss nur etwas aufpassen, weil es neben i++ auch noch das
ähnlich aussehende ++i gibt und beide keineswegs immer die gleiche Wir-
kung haben. Natürlich zählen beide die Variable i um 1 hoch: das nennt man
Inkrementierung von i. Solange es einfach nur darum geht, den Ausdruck
i=i+1 etwas zu verkürzen, können Sie bedenkenlos sowohl i++ als auch ++i
schreiben. Genauer aufpassen müssen Sie, wenn Sie i inkrementieren und den
erhöhten Wert einer anderen Variablen zuweisen wollen. Nehmen wir beispiels-
weise eine weitere int-Variable m zur Hand und gehen wir davon aus, dass auf
2.3 Kontrollstrukturen 133

i der Wert 17 steht. Die Anweisung


m=i++;
bewirkt dann, dass zuerst der aktuelle Wert von i auf die Variable m geschrie-
ben und danach erst i inkrementiert, das heißt um 1 erhöht wird. Als Resultat
dieser Anweisung hat also i den Wert 18, m dagegen den Wert 17. Anders läuft
die Sache bei der Anweisung
m=++i;
Hier wird zuerst die Variable i inkrementiert und anschließend der aktuali-
sierte Wert von i der Variablen m zugewiesen, sodass am Ende beide Variablen
sich am Wert 18 erfreuen können. Sie sehen vielleicht ein, dass hier höchste
Vorsicht geboten ist und ich es vorziehe, zur Erzielung des ersten Effekts die
Anweisungen
m=i; i=i+1;
und für den zweiten Effekt
i=i+1; m=i;
zu schreiben, aber das bleibt natürlich jedem selbst überlassen. Der Vollständig-
keit halber muss ich allerdings noch erwähnen, dass es neben der Inkrementie-
rung auch noch die Dekrementierung einer Variablen gibt, bei der die Variable
um 1 erniedrigt wird. Wie Sie sich vermutlich denken können, hat man dabei,
wenn es um die Variable i geht, die Wahl zwischen den beiden Varianten i--
und --i mit den gleichen Bedeutungsunterschieden wie bei der Inkrementie-
rung.
Es kann nicht schaden, das Programm zur Summenbildung einmal kom-
plett als Struktogramm zu formulieren; dazu haben Sie in den Übungsaufga-
ben Gelegenheit.

do-Schleife
Die do-Schleife, auch nicht abweisende Schleife oder fußgesteuerte Schleife
genannt, erlaubt es, einen Anweisungsblock wiederholt durchführen zu las-
sen, sofern bestimmte Durchführungsbedingungen erfüllt sind. In C lautet
das Schema der do-Schleife:
do
{
Anweisungen
}
while(Bedingungen);
Die Anweisungen im so genannten Schleifenrumpf werden in jedem Fall min-
destens einmal ausgeführt.

2.3.4 Wiederholung als abweisende Schleife

Ich wende mich jetzt dem zweiten Schleifentyp zu: der while-Schleife, auch
abweisende Schleife genannt. Damit die Sache übersichtlich bleibt, program-
miere ich die eben betrachtete Summenbildung noch einmal, nur mit dem
134 2 Strukturierte Programmierung mit C

anderen Schleifentyp. Auch hier handelt es sich nur um eine Iteration, nur
eben anders aufgezogen.
/* summeneu.c -- summe der ersten n zahlen */

#include <stdio.h>

main()
{
int n, i=1, s=0;
do
{
printf("Eine natuerliche Zahl eingeben.\n");
scanf("%d",&n);
if (n <= 0)
printf("%d ist keine natuerliche Zahl, bitte nochmal:\n",n);
}
while (n <= 0);

while(i <= n)
{
s = s+i;
i++;
}
printf("Die Summe lautet %d\n", s);
}
So groß ist der Unterschied gar nicht. Die Eingabeschleife habe ich gelassen,
wie sie war, denn man sollte auf jeden Fall mindestens einmal eine Eingabe
machen können, weshalb eine nicht abweisende Schleife den Vorzug verdient.
Aber bei der Summenberechnung sieht es jetzt doch etwas anders aus. Die
Bedingung i<=n wird jetzt ganz am Anfang der Schleife abgefragt, sodass
Sie nur dann in den Schleifenrumpf gelangen können, wenn die Bedingung
auch wirklich von Anfang an erfüllt ist. Im Unterschied zur nicht abweisenden
Schleife, die Sie auf alle Fälle mindestens einmal durchlaufen müssen, könn-
te Ihnen daher diese abweisende Schleife unter Umständen gleich zu Beginn
den Zutritt verweigern: wenn die Bedingung nicht erfüllt ist, geht gar nichts.
Während also die Durchführungsbedingung der do-Schleife eher die Rolle ei-
nes Rausschmeißers in der Disco spielt, der Sie erst einmal ins Haus lässt,
Sie aber dann jederzeit wieder vor die Tür setzen kann, hat die Bedingung
der while-Schleife die Bedeutung eines Türstehers, der Sie gleich vor der Tür
stehen lässt, wenn ihm Ihre Frisur nicht gefällt.
Die unterschiedliche Bedeutung der Durchführungsbedingung wird auch
an der Darstellung im Struktogramm deutlich.
Überrascht es irgend jemanden, dass man die abweisende Schleife auch
gerne als kopfgesteuerte Schleife bezeichnet, weil die Bedingung sich nun mal
wie der Kopf oben befindet? Ich glaube kaum. Keine sehr glückliche Bezeich-
2.3 Kontrollstrukturen 135

Abb. 2.15. Abweisende Schleife als Struktogramm

nung nach meinem Geschmack, und man kann sich nur glücklich schätzen,
dass die Schleifenbedingungen eigentlich immer ganz oben oder ganz unten
stehen, sodass nur Kopf und Fuß zur Namensgebung herangezogen wurden.
Die Darstellung im Programmablaufplan hat wieder etwas Sprunghaftes an
sich, was vom Struktogramm ganz und gar vermieden wird. Trotzdem will ich
sie Ihnen nicht vorenthalten, Sie finden den entsprechende PAP-Ausschnitt in
Abbildung 2.16. Bevor ich aber zur dritten und letzten Schleifenart übergehe,
sollten wir uns noch kurz überkgen, was in meinem neuen Summenprogramm
wohl geschieht, wenn durch einen dummen Zufall die Variable n doch nicht
mit einem positiven Wert belegt ist. Ganz einfach: der Türsteher in Form der
Durchführungsbedingung wird Sie am Zutritt zum Schleifenrumpf hindern, es
wird keine weitere Berechnung durchgeführt und die Summenvariable s bleibt
da stehen, wo sie von Anfang an war, nämlich bei dem Wert 0. Ist ja auch
nicht sehr überraschend, denn wenn man nichts addiert, dann ist die Summe
eben 0. Bei dieser Programmvariante, die eine abweisende Schleife verwendet,
hätte ich also eigentlich auf die Anfangsschleife zur Steuerung einer vernünf-
tigen Eingabe verzichten können. Um aber sicher zu gehen, dass tatsächlich
etwas zur Verarbeitung vorhanden ist, konnte auch die Eingabeschleife nichts
schaden.

while-Schleife
Die while-Schleife, auch abweisende Schleife oder kopfgesteuerte Schleife ge-
nannt, erlaubt es, einen Anweisungsblock wiederholt durchführen zu lassen,
sofern bestimmte Durchführungsbedingungen erfüllt sind. In C lautet das
Schema der while-Schleife:
while(Bedingungen)
{
Anweisungen
}
Die Anweisungen im so genannten Schleifenrumpf werden nur dann aus-
136 2 Strukturierte Programmierung mit C

Nein Bedingung ?

Ja

Anweisungen

Abb. 2.16. Abweisende Schleife im Programmablaufplan

geführt, wenn die Durchführungsbedingungen erfüllt sind; sind die Bedingun-


gen nicht erfüllt, wird der Anweisungsblock nicht ausgeführt.

2.3.5 Wiederholung als Zählschleife

Eine Schleife habe ich noch im Gepäck, auch wenn sie nichts umwerfend Neu-
es bringt: die Zählscheife oder auch for-Schleife. Sehen wir uns zunächst ein-
mal an, wie man das Summenprogramm mit Hilfe einer Zählschleife erledigen
kann. Ich beschränke mich dabei jetzt auf die Schleife zur eigentlichen Sum-
menberechnung, der Rest bleibt für den Moment gleich.
for(i=1; i<=n; i++)
{
s = s+i;
}
Was ist hier los? Gar nicht so viel. Inerhalb der Klammer der for-Schleife
finden Sie den Schleifenkopf, der angibt, unter welchen Bedingungen die An-
weisungen im Schleifenrumpf ausgeführt werden sollen. Die Variable i wird
mit dem Wert 1 initialisiert, und die Ausführungsbedingung, die Sie nach dem
ersten Strichpunkt finden, besagt, dass die Anweisungen durchgeführt werden
sollen, solange dieses i kleiner oder gleich n ist. Und auch die Inkrementie-
rung von i wird gleich im Schleifenkopf gesteuert: mit der Anweisung i++
nach dem zweiten Strichpunkt machen Sie dem Programm klar, dass nach je-
dem Schleifendurchlauf die Variable i um 1 erhöht werden soll. Auf Deutsch
könnte man den Schleifenkopf also etwa so fomulieren: Führe von i = 1 bis

2.3 Kontrollstrukturen 137

i = n die folgenden Anweisungen aus und erhöhe nach jedem Durchlauf die
Variable i um 1.“ Im Schleifenrumpf darf ich dann selbstverständlich auf die
Inkrementierung von i verzichten, denn die wird ja schon automatisch durch
die Inkrementierungsanweisung im Schleifenkopf durchgeführt.
Beachten Sie, dass man die Zeile for(i=1; i<=n; i++) um Himmels Wil-
len nicht mit einem Semikolon abschließen darf. Wenn Sie nämlich
for(i=1; i<=n; i++);
{
s = s+i;
}
schreiben, dann wird die Schleife abgeschlossen, noch ehe sie begonnen hat,
da das Semikolon das Ende der Anweisung angibt. Sie würden also in der
Schleife brav die Variable i bis n hochzählen und sonst gar nichts tun, denn
das nachfolgende s=s+i würde dann überhaupt nicht mehr als zur Schleife
zugehörig erkannt.
Viel Neues liefert das nicht. Im Grunde handelt es sich nur um eine andere
Schreibweise für die abweisende Schleife, denn auch hier kann es vorkommen,
dass man erst gar nicht zu den Anweisungen des Schleifenrumpfes gelangt:
wenn in meinem kleinen Beispiel auf n ein negativer Wert steht, wird die
Druchführungsbedingung i<=n schon am Anfang verletzt und die Schleife wird
nicht durchlaufen. Da hier nur eine Variante der abweisenden Schleife vorliegt,
gibt es auch in Struktogrammen kein eigenes Beschreibungsmittel für die for-
Schleife, man verwendet einfach die Darstellung der abweisenden Schleife. Nur
bei der Formulierung der Bedingung, von der die Durchführung abhängt, gibt
es verschiedene Möglichkeiten. Ich empfehle in solchen Fällen eine Darstellung
wie in Abbildung 2.17.

for-Schleife
i = 1 (1) n

s=s+i

Abb. 2.17. Zählschleife als Struktogramm

Sie sehen hier die eben in C notierte for-Schleife als Struktogrammblock


vor sich. Mit der Schreibweise i=1(1)n ist gemeint, dass die Variable i bei 1
startet und dann in Einserschritten inkrementiert wird, bis am Ende der Wert
der Variablen n erreicht ist. Natürlich ist es auch möglich, größere Schrittwei-
ten zu wählen oder irgendwelche Variablen abwärts zu zählen wie zum Beispiel
in den Schleifenköpfen
for(i=12; i <= 38; i=i+2)
138 2 Strukturierte Programmierung mit C

oder
for(i=12; i > 0; i--)
die vermutlich selbsterklärend sind: der erste Schleifenkopf verlangt, so lan-
ge von 12 an hochzuzählen, bis 38 überschritten ist, wobei die Zählung in
Zweierschritten erfolgt, also 12, 14, 16, .... Und im zweiten Schleifenkopf wird
einfach ab 12 abwärts gezählt, solange nur die Bedingung i>0 gilt. Sobald
also i den Wert 0 hat, ist die Schleife beendet. Wer kurze und unverständ-
liche Schreibweisen mag, kann übrigens die Anweisung i=i+2 auch ersetzen
durch i+=2. Das ist nur eine abkürzende Schreibweise für eben dieses i=i+2,
die oft verwendet wird, aber nicht gerade ein Muster an sprachlicher Eleganz
darstellt. Entsprechend können Sie die Dekrementierung i-- natürlich auch
ersetzen durch i-=1.
Es kommt aber noch besser, denn die for-Schleife ist von bemerkenswer-
ter Flexibilität, sofern man bereit ist, ein wenig komplizierter zu denken. Was
halten Sie beispielsweise von der folgenden Schleife?
for(s=0, i=1; i <= n; s = s+i, i++);
Auf den ersten Blick sieht sie etwas abschreckend aus, aber das scheint nur so;
in Wahrheit ist sie einfach nur ziemlich raffiniert. Die Initialisierung bezieht
sich jetzt nicht mehr nur auf die eine Variable i, sondern auch noch auf s, was
macht das schon. Man kann also durch Einsatz eines Kommas verschiedene
Befehle zu einer Einheit zusammenfassen. Die Durchführungsbedingung ist
noch die gleiche wie vorher bei der vertrauten Summenbildung: so lange soll
dir Schleife durchlaufen werden wie i <= n gilt. Und nach jedem Schleifen-
durchlauf wird jetzt neben der Inkrementierung von i auch noch die Variable
s um den Wert von i erhöht. Was passiert im Schleifenrumpf? Gar nichts,
es ist ja auch keiner da, direkt nach der for-Anweisung steht ein Semikolon.
Da Sie mit dem Initialwert i=1 starten und dann die Schleife so lange laufen
lassen wie i <= n gilt, wird in jedem Schleifendurchlauf der jeweils aktuelle
Wert von i auf s addiert - also erst 1, dann 2, dann 3 und so weiter, bis n
erreicht ist. Ohne den Einsatz eines Schleifenrumpfes können Sie also auch in
einer Schleife echte Verarbeitungen durchführen lassen, falls Sie den Schleifen-
kopf etwas raffinierter gestalten. Empfehlen würde ich es nicht, die Lesbarkeit
von Programmen wird auf diese Weise nicht gerade gesteigert.
Sollten Sie das Bedürfnis verspüren, eine Schleife immer und immer wie-
der bis zum Ende des Universums oder doch wenigstens der Stromversorgung
laufen zu lassen, dann ist auch das mit einer for-Schleife möglich, indem Sie
einfach eine Schleife schreiben, deren Schleifenkopf im Leser ein starkes Gefühl
der Verlassenheit erweckt, nämlich:
for( ; ; )
{
Anweisungen
}
Es wird kein Initialwert vergeben, keine Durchführungsbedingung festgelegt
und keine wie auch immer gestaltete Inkrementierung oder Dekrementie-
rung verlangt. Mit einem Wort: es ist alles egal, und deshalb wird so eine
2.3 Kontrollstrukturen 139

Schleife auch nie enden. Das liegt daran, dass sie im Normalfall dann ihr
natürliches Ende findet, wenn der Ausdruck nach dem ersten Semikolon, die
Durchführungsbedingung, falsch wird. Ist keine Durchführungsbedingung da,
kann sie auch nicht falsch werden, und die Sache läuft bis zum jüngsten Tag.
Wenn man schon Wert auf so eine Endlosschleife legt, sollte man sie aber aus
Gründen der besseren Lesbarkeit wohl eher mit einer while-Schleife program-
mieren, zum Beispiel durch
while(1==1)
{
Anweisungen
}
Kaum jemand wird nämlich bestreiten wollen, dass die Zahl 1 sich selbst gleich
ist; also ist die Bedingung immer erfüllt und die Schleife läuft endlos.
Solange das Absicht ist, mag eine Endlosschleife ja in Ordnung sein. Sie
kann aber leicht auch ganz ungeplant entstehen, wenn man beim Zusam-
menspiel von Inkrementierung oder Dekrementierung nicht aufpasst. An der
Schleife
for(i=0; i<1; i--)
{
printf("Neuer Schleifendurchlauf\n");
}
werden Sie viel Freude haben, vor allem sehr lange, denn die Durchführungs-
bedingung i<1 wird nie verletzt sein, und Ihre Schleife wird Sie ein Leben
lang begleiten. Solche Effekte sollten Sie besser vermeiden.
Noch eine Bemerkung, dann erlöse ich sie von der Zählschleife, wenn auch
nur, um sie gleich wieder zur Verarbeitung einer neuen Datenstruktur einzu-
setzen. Sowohl in C++, der objektorientierten Erweiterung von C, als aucb
in Java, das ich Ihnen im nächsten Kapitel zeige, besteht die Möglichkeit, ei-
ne Laufvariable wie mein viel verwendetes i erst direkt im Schleifenkopf zu
deklarieren, also etwas zu schreiben wie
for (int i=1; i<10; i++)
und dafür vorher im Programm die Variable i nicht zu definieren. Im übli-
chen, gewöhnlichen C geht so etwas nicht, Sie können im Schleifenkopf keine
Variablen definieren, sondern nur bereits definierte benutzen. Aber immerhin
kann man so etwas wie lokale Variablen einsetzen, wie Sie im folgenden Bei-
spiel sehen.
for(i=0; i<10; i++ )
{
int n=i*i;
printf("Zahl und Quadratzahl: %d %d\n", i, n);
}
Was diese Schleife macht, ist wohl klar: sie gibt für die Zahlen von 0 bis
9 die zugehörigen Quadratzahlen aus. Aber was passiert, wenn Sie nach der
Schleife noch das Kommando printf("%d",n) durchführen wollen? Das wird
Ihnen der Compiler um die Ohren hauen, denn die Variable n haben Sie inner-
140 2 Strukturierte Programmierung mit C

halb der Blocks der for-Schleife definiert, und außerhalb dieses Blocks kennt
diese Variable kein Mensch. Bisher haben wir die Variablen immer ganz am
Anfang des Hauptprogramms definiert, weshalb man sie auch im gesamten
Hauptprogramm kannte. Sobald Sie aber eine Variable innerhalb eines Blocks
vereinbaren, existiert diese Variable auch nur genau in dem Block, in dem sie
vereinbart wurde. Das gilt nicht nur für for-Schleifen, sondern ganz allgemein
für jeden beliebigen Block. So wird beispielsweise das Programmstück
if(i=17)
{
int n=i*i;
printf("Zahl und Quadratzahl: %d %d\n", i, n);
}
printf("%d",n);
Ihren Compiler zu einer Fehlermeldung provozieren, weil die Variable n in-
nerhalb des if-Blocks definiert war und deshalb außerhalb dieses Blocks auf
wenig Begeisterung stößt.
Nun ist es aber genug mit den syntaktischen Einzelheiten zu den Schleifen.

for-Schleife
Die for-Schleife, eine spezielle Form der abweisenden Schleife, die man auch
Zählschleife nennt, erlaubt es, einen Anweisungsblock wiederholt durchführen
zu lassen, sofern bestimmte Durchführungsbedingungen erfüllt sind. In C lau-
tet das Schema der for-Schleife:
for(Ausdruck1; Ausdruck2; Ausdruck3)
{
Anweisungen
}
In der Regel wird in Ausdruck 1 der Schleifenanfangswert gesetzt, also die In-
itialisierung vorgenommen. In Ausdruck 2 wird festgesetzt, unter welcher Be-
dingung die Anweisungen im Schleifenrumpf ausgeführt werden sollen, meist
in Form eines Schleifenendwertes. Ausdruck 3 regelt die Veränderung an einer
oder mehreren Schleifenvariablen, mit deren Hilfe die Ausführungsbedingung
aus Ausdruck 2 untersucht wird.

2.3.6 Felder

Ein klassisches Anwendungsgebiet der for-Schleife ist die Verarbeitung von so


genannten Feldern. Das hat leider nichts zu tun mit einem netten Spaziergang
in noch netterer Begleitung durch schöne blühende Felder, sondern ist nur ein
poetischer Name für eine bestimmte Art, Daten zu verwalten. Manche nennen
ein Feld auch lieber einen Vektor oder gar auf Englisch einen array, um jede
landwirtschaftlich-romantische Assoziation zu vermeiden - gemeint ist mit all
diesen Worten dasselbe. Stellen Sie sich beispielsweise vor, Sie müssen auf
der Zugspitze zu jeder Stunde des Tages die Temperatur messen, damit der
Deutsche Wetterdienst genügend Daten für seine Wettervorhersagen hat. Eine
2.3 Kontrollstrukturen 141

einzelne Temperaturmessung entspricht einer einzigen float-Variablen, in der


man eine reelle Zahl speichern kann. Aber soll man denn für 24 verschiedene
Temperaturen pro Tag wirklich 24 verschiedene Variablen anlegen? Das wäre
erstens ausgesprochen schwerfällig aufzuschreiben und brächte außerdem Pro-
bleme bei der weiteren Verarbeitung. Besser ist es, so etwas in einer einzigen
Definition zu erledigen, und dazu braucht man ein Feld.
Das Prinzip ist einfach. Während Sie für eine float-Variable beispielswei-
se die Anweisung float temperatur verwenden würden, definiert man ein
ganzes Feld von Variablen durch die Anweisung
float temperatur[24];
Die Variable heisst immer noch temperatur, aber sie kann nicht mehr nur
eine einzige reelle Zahl aufnehmen, sondern gleich 24. Anders gesagt: das Feld
temperatur hat 24 reelle Komponenten. Natürlich könnten es auch 17, 38 oder
nur zwei Komponenten sein, je nachdem, welche Zahl Sie in die eckigen Klam-
mern schreiben. Wichtig ist nur, dass in einem Feld mehrere Komponenten des
immer gleichen Typs auftreten, wobei die Anzahl durch den Ausdruck in den
eckigen Klammern festgelegt wird. Und das geht nicht nur für den Datentyp
float, auf den kommt es hier gar nicht an. Sobald Sie eine Variable irgend-
eines Datentyps anlegen und hinter den Variablennamen in eckige Klammern
einen konstanten Ausdruck schreiben, haben Sie ein Feld Ihres verwendeten
Datentyps definiert mit der Länge, die der Ausdruck angibt. Sie könnten also
auch
float temperatur[2*12];
oder gar
float temperatur[MONATE];
schreiben, falls Sie vorher die Konstante MONATE mit einer #define-Anweisung
definiert haben; beides würde der Compiler verstehen. Variablen dürfen Sie
zur Angabe der Länge allerdings nicht verwenden, die Länge eines Feldes muss
feststehen.
Und was soll jetzt das Ganze? Sehen Sie sich einmal das nächste Programm
an.
/* zugspitze.c -- temperaturmessung */

#include <stdio.h>
#define MAX 24

main()
{
int i;
float temperatur[MAX];
for (i=0; i<MAX; i++)
{
printf("Temperatur in Stunde %d angeben: \n",i);
scanf("%f",&temperatur[i]);
}
142 2 Strukturierte Programmierung mit C

printf("Kontrollausgabe\n");
for(i=0; i<MAX; i++)
{
printf("Temperatur in Stunde %d betraegt %6.2f\n",
i,temperatur[i]);
}
Mit diesen wenigen Zeilen haben Sie die gesamte Temperaturverarbeitung
erledigt. Es wird ein Feld temperatur angelegt, das aus 24 reellen Zahlen be-
steht. In der for-Schleife werden die 24 benötigten Temperaturwerte in das
Feld eingegeben, aber hier ist ein wenig Vorsicht geboten: im Gegensatz zu den
meisten Menschen beginnt die Programmiersprache C das Zählen bei 0, nicht
bei 1. Es geht hier also nicht um die Nummern 1 bis 24, sondern eben um die
Nummern von 0 bis 23 - etwas gewöhnungsbedürftig, aber nicht zu ändern.
Jede einzelne Feldkomponente können Sie durch die Angabe ihrer Nummer
direkt ansprechen, eine ist so gut wie die andere. Durch die Anweisung
scanf("%f",&temperatur[i]);
wird daher Ihre Tatstatureingabe direkt in die Feldkomponente temperatur[i]
übertragen, und da Sie das von i=0 bis i=23 machen, wird am Ende der ersten
Schleife das gesamte Feld mit den Temperaturwerten gefüllt sein. Die zweite
Schleife funktioniert genauso. Wieder durchlaufen Sie das gesamte Feld mit all
seinen 24 Komponenten, nur dass Sie jetzt nichts mehr in das Feld schreiben,
sondern die dort stehenden Werte am Bildschirm ausgeben. Stellen Sie sich
vor, das hätten Sie mit 24 einzelnen float-Variablen erledigen müssen; der
Programmieraufwand wäre deutlich höher geworden.
Ich vermute, das Prinzip ist klar geworden. Indem Sie ein Feld eines be-
stimmten Datentyps mit einer gewissen festen Länge vereinbaren, stehen Ih-
nen mit einem Schlag mehrere Komponenten dieses Datentyps zur Verfügung.
Geht es zum Beispiel um das int-Feld x der Länge 10, so müssen Sie es mit
dem Kommando
int x[10];
anlegen und können anschließend auf die 10 Zahlen x[0],x[1],...,x[9] zu-
greifen. Dass man das bei Feldern einer gewissen Länge am besten mit einer
for-Schleife macht, bedarf kaum noch der Erwähnung.
Nehmen wir als weiteres Beispiel ein Unternehmen, das drei Produkte her-
stellt. Jedes Produkt hat einen gewissen Stückpreis, der sich je nach Marktlage
von Tag zu Tag ändern kann. Und von jedem Produkt wird täglich eine be-
stimmte Anzahl von Exemplaren verkauft - wie viele, weiß man erst, wenn der
Tag vorbei ist. Um am Ende eines langen Arbeitstages den erreichten Umsatz
zu bestimmen, wird man natürlich die jeweils verkauften Exemplare mit dem
jeweiligen Stückpreis multiplizieren und dann die Teilumsätze addieren. In
einem C-Programm sieht das folgendermaßen aus.
/* umsatz.C -- umsaetze berechnen */

#include <stdio.h>
#define MAX 3
2.3 Kontrollstrukturen 143

main()
{
float preis[MAX], verkauf[MAX];
float umsatz=0;
int i;
for (i=0; i < MAX; i++)
{
printf("Stueckpreis von Produkt %d eingeben\n",i+1);
scanf("%f",&preis[i]);
printf("Verkaufsmenge fuer Produkt %d eingeben\n",i+1);
scanf("%f",&verkauf[i]);
}
for (i=0; i < MAX; i++)
{
umsatz = umsatz + preis[i]*verkauf[i];
}
printf("Der Umsatz betraegt heute %7.2f Euro\n",umsatz);
}
Viel muss ich dazu nicht mehr sagen. Für die Stückpreise und die Ver-
kaufszahlen wird je ein Feld angelegt, danach werden beide Felder in einer
for-Schleife gefüllt und anschließend der Umsatz nach dem gleichen Verfah-
ren berechnet, mit dem ich auch die Summe der ersten natürlichen Zahlen
ausgerechnet hatte - nur dass jetzt der Reihe nach Feldkomponenten verar-
beitet werden. Beachten Sie in der ersten for-Schleife die Ausgabe: ich habe
hier immer i+1 als Nummer ausgegeben anstatt nur i. Das liegt natürlich
daran, dass die beiden Felder von 0 bis 2 nummeriert sind und nicht von 1 bis
3, der Benutzer aber sicher nicht den Preis des nullten Produkts eingeben will,
sondern den des ersten. Also habe ich bei der Ausgabe die laufende Nummer
immer um 1 erhöht.
Natürlich hätte man dieses kleine Rechenprogramm auch ohne Felder hin-
bekommen. Stellen Sie sich aber vor, das Unternehme hat 123 Produkte an-
statt nur drei. Dann muss ich in meinem Programm nur die Konstante MAX
auf 123 setzen und kann den Rest so lassen wie er ist. Bei einem feldfreien
Programm hätten Sie mit Sicherheit einen wesentlich höheren Änderungsauf-
wand.
Neben dem Einsatz bei der Lösung solcher und ähnlicher Probleme können
Sie Felder aber auch noch zur Verarbeitung von Zeichenketten einsetzen,
die wir bisher fast völlig ignoriert haben. Zwar haben meine Programm im-
mer mal wieder Zeichenketten ausgegeben, zum Beispiel mit der Anweisung
printf("Text"), die die Zeichenkette Text“ ausgibt, aber es war bisher nicht

möglich, eine Zeichenkette direkt von der Tastatur einzugeben. Das wird sich
jetzt ändern, denn was ist eine Zeichenkette anderes als eine Aneinanderrei-
hung einzelner Zeichen - also ein Feld aus Zeichen? Wollen Sie beispielsweise
eine Zeichenkette der Länge 10 definieren, dann genügt die Vereinbarung
144 2 Strukturierte Programmierung mit C

char kette[10];
und die Sache ist erledigt. Die Eingabe von der Tastatur könneen Sie dann
anstoßen durch die Anweisung
scanf("%s",kette);
wobei das Formatelement "%s" für das englische Wort String“, also Zeichen-

kette steht. Seltsamerweise können Sie bei solchen Zeichenketten das &-Symbol
vor dem Namen weglassen und einfach scanf("%s",kette) schreiben - das
ist kein Druckfehler, das funktioniert, und warum das gar nicht so seltsam ist,
erkläre ich Ihnen im Abschnitt über Zeiger. Als zusätzliche Eingabemöglich-
keit steht auch noch das Kommando gets(kette) zur Verfügung, das die
gleichen Resultate liefert. Und um die ganze Sache noch ein wenig zu verein-
fachen, können Sie sogar zwei Zeichenketten durch ein schlichtes Kommando
miteinander vergleichen. Sind nämlich zwei Zeichenketten kette1 und kette2
gegeben, so ermittelt das Kommando
strcmp(kette1,kette2);
welche der beiden Zeichenketten alphabetisch vorne liegt, indem es einen ganz-
zahligen Wert an das Programm liefert: ist der Wert kleiner als 0, so liegt
kette1 vor kette2, ist er größer als 0, liegt kette1 nach kette2, und ist
er am Ende sogar gleich 0, sind beide Strings gleich. Mit dem folgenden Pro-
grammstück können Sie also leicht den alphabetischen Vergleich durchführen.
int i;
i = strcmp(kette1,kette2);
if (i < 0)
printf("%s liegt vor %s\n", kette1, kette2);
else if (i > 0)
printf("%s liegt nach %s\n", kette1, kette2);
else
printf("%s ist gleich %s\n", kette1, kette2);
Wie immer ist auch ein wenig Vorsicht geboten. Die Werte eines jeden Fel-
des werden im Arbeitsspeicher brav hintereinander abgelegt, in unmittelbar
aufeinander folgenden Speicherplätzen, wobei das nullte Element die niedrig-
ste Adresse hat. Das wäre noch nicht weiter schlimm. Leider hat der Compiler
aber ein recht gutes Gedächtnis und merkt sich, dass er an dieser Stelle ein
Feld abgelegt hat, in dem Elemente eines bestimmten Datentyps stehen. Soll-
ten Sie jetzt auf die Idee kommen, zu viele Elemente einzugeben oder gar
mehr Elemente aus Ihrem Feld zu lesen, als es überhaupt aufzuweisen hat,
dann wird der Compiler das zum Zeitpunkt des Kompilierens absolut nicht
als Fehler zur Kenntnnis nehmen, sondern so tun, als wäre alles in Ordnung.
Das Programmstück
int x[3], i;
for (i=0;i<3;i++)
x[i] = i;
for (i=0;i<4;i++)
printf("%d",x[i]);
wird der C-Compiler daher als richtig akzeptieren und auch klaglos zur
2.3 Kontrollstrukturen 145

Ausführung bringen, obwohl Sie hier aus einem Feld der Länge drei ganze vier
Elemente auslesen. Und was passiert beim vierten Element? Da der Compiler
weiß, dass in diesem Feld ganze Zahlen stehen sollen und er auch weiß, dass
eine ganze Zahl beispielsweise zwei Byte lang ist, wird er einfach die nächsten
zwei Byte nach dem Feldende als ganze Zahl interpretieren und ausgeben.
Was dabei heraus kommt, weiß kein Mensch, weil Sie ja nicht wissen können,
was im Arbeitsspeicher direkt hinter Ihrem Feld steht. Verlassen Sie sich also
bei der Einhaltung der Feldlänge um Himmels Willen nie auf den Compiler,
der kümmert sich darum überhaupt nicht. Wann immer Sie ein Feld benut-
zen, müssen Sie selbst dafür sorgen, dass Sie nicht über die Feldlänge hinaus
laufen, sonst kommen Sie in leichte Schwierigkeiten.
Noch etwas mehr Vorsicht ist beim Einsatz von Zeichenketten geboten.
Erstens kann es nicht schaden, in solchen Fällen die Headerdatei string.h
einzubinden, in der die Kommandos zur Verarbeitung von Zeichenketten zu
finden sind; das geht mit der Präprozessoranweisung #include <string.h>.
Vor allem sollten Sie aber darauf achten, in eine Zeichenkette immer ein Zei-
chen weniger einzulesen, als die vereinbarte Länge eigentlich zulässt. Ist also
kette eine char-Feld der Länge 10, dann ist sehr zu empfehlen, in kette nur
eine Zeichenkette aus höchstens 9 Zeichen einzulesen, denn das letzte Zeichen
einer Zeichenkette wird automatisch mit dem Stringendezeichen ’\0’ belegt,
einem Sonderzeichen, das klar macht, dass hier das Ende der Fahnenstan-
ge erreicht ist. Wenn Sie mehr als diese Höchstzahl von Zeichen eingeben,
dann kann das zu Schwierigkeiten beim Einsatz von Kommandos wie strcmp
führen und die Ergebnisse Ihrer Stringverarbeitung stark verfälschen. Ande-
rerseits macht es die Existenz des Stringendezeichens auch leicht, das Ende
einer Zeichenkette zu finden: Sie müssen nur nach dem Zeichen ’\0’ fragen,
und sobald es gefunden ist, ist der String beendet.

Felder
Ein Feld besteht aus einer festen Anzahl von Komponenten eines einzigen
Typs. Jede Komponente kann man ansprechen durch Angabe der Nummer
im Feld, wobei die Nummerierung bei 0 beginnt. Soll also ein Feld x aus 10
int-Komponenten angelegt werden, so schreibt man
int x[10];
und hat damit die 10 ganzen Zahlen x[0], x[1],...,x[9] zur Verfügung.
Die Verarbeitung von Feldern erfolgt häufig mit Hilfe von for-Schleifen. Der
Programmierer muss selbst auf die Einhaltung der Feldlänge achten, da der
C-Compiler auch Verarbeitungen über die Feldgrenze hinaus akzeptiert.
Mit Hilfe von char-Feldern kann man Zeichenketten verarbeiten. Dabei ist
darauf zu achten, dass das letzte Zeichen automatisch das Stringendezeichen
’\0’ ist. Zum Vergleich von Zeichenketten steht das Kommando strcmp zur
Verfügung.

Schon ganz gut, aber nicht gut genug. Bisher musste ich mich auf so ge-
nannte eindimensionale Felder beschränken, in denen die Komponenten ein-
146 2 Strukturierte Programmierung mit C

fach durchnummeriert waren. Es geht auch etwas komplexer, und manchmal


ist das auch nötig. Wie soll man zum Beispiel mit so einem simplen Feld ein
Schachbrett modellieren, das nun mal nicht nur in eine Richtung geht, sondern
in zwei? Die Antwort ist einfach genug: wenn eindimensionale Felder nicht
mehr ausreichen, versucht man es eben mit zweidimensionalen. Wollen Sie
beispielsweise jeder Position auf dem Schachbrett mit Hilfe eines Buchstabens
die darauf stehende Figur zuweisen, dann brauchen Sie das zweidimensionale
Feld
char schachbrett[8][8];
das aus 8 · 8 = 64 Elementen vom Typ char besteht. Wie bei einem eindimen-
sionalen Feld kann man die einzelnen Komponenten direkt durch die Angabe
der Nummern ansprechen, wobei Sie wieder darauf achten müssen, dass die
Nummerierung bei 0 anfängt und im Fall meines Schachbretts in beiden Rich-
tungen bei 7 aufhört. Um einen Läufer der Komponente mit den Nummern 0
und 3 zuzuweisen, müsste ich also zum Beispiel das Kommando
schachbrett[0][3]=’L’;
absetzen, für eine Dame in der Komponente mit den Nummern 2 und 4 tut
es das Kommando
schachbrett[2][4]=’D’;
Die Speicherung eines solchen zweidimensionalen Feldes im Arbeitsspeicher
erfolgt wieder schön der Reihe nach: an vorderster Stelle steht das Element
schachbrett[0][0], dann kommt die komplette nullte Zeile, also die Kom-
ponenten schachbrett[0][1] bis schachbrett[0][7], und dann geht es zei-
lenweise weiter mit schachbrett[1][0] bis hin zu schachbrett[1][7], und
so weiter.
Aber wie kann man so ein zweidimensionales Feld vernünftig verarbeiten?
Das wesentliche Hilfsmittel bei der Verarbeitung der ordinären eindimensio-
nalen Felder war die for-Schleife, und bei den zweidimensionalen Feldern ist
es die geschachtelte for-Schleife. Was das ist, sehen wir uns im nächsten Pro-
gramm an. Ich gehe dabei davon aus, dass Sie eine ungefähre Vorstellung
davon haben, was eine Matrix ist, nämlich nichts anderes als ein rechteckiges
Schema aus Zahlen wie zum Beispiel
   
12 56
oder auch .
34 78
Sie werden Ihnen in der Mathematik begegnen, falls das nicht schon längst
passiert ist. Für unsere Zwecke ist es nur wichtig, dass es sich genau wie beim
Schachbrett offenbar um zweidimensionale Größen handelt, und dass man mit
Ihnen auch ein wenig rechnen kann, indem man beispielsweise die zueinander
passenden Komponenten der Matrizen addiert. Bei meinen Beispielmatrizen
heißt das:      
12 56 6 8
+ = .
34 78 10 12
Das folgende Programm wird jetzt zwei Matrizen mit zwei Zeilen und Spalten
mit Zahlen füllen, anschließend die beiden Matrizen addieren und das Ergebnis
2.3 Kontrollstrukturen 147

am Ende ausgeben.
/* matrizen -- addiert matrizen */

#include <stdio.h>
#define DIM 2
main()

{
int i, j;
int A[DIM][DIM], B[DIM][DIM], C[DIM][DIM];
printf("Eingabe der Matrizen A und B\n");
for (i=0; i<DIM; i++)
for (j=0; j<DIM; j++)
{
printf("Geben Sie A[%d][%d] ein: \n", i, j);
scanf("%d", &A[i][j]);
printf("Geben Sie B[%d][%d] ein: \n", i, j);
scanf("%d", &B[i][j]);
}
for (i=0; i<DIM; i++)
for (j=0; j<DIM; j++)
C[i][j]=A[i][j] + B[i][j];
for (i=0; i<DIM; i++)
{
for (j=0; j<DIM; j++)
printf("%4d", C[i][j]);
printf("\n");
}
}
Noch Fragen? Na ja, vielleicht schon, und deshalb werde ich jetzt noch ein
wenig über dieses Programm reden. Zuerst sollten Sie aber einen Blick auf
Abbildung 2.18 werfen, in der das Verfahren zur Matrizenaddition in einem
Struktogramm formuliert wird - dann kann ich nämlich beides auf einmal
erklären.
Warum eine Konstante DIM zur Angabe der Dimension festgelegt wird,
hatten wir schon einmal besprochen: auf diese Weise kann ich durch eine
Änderung an einer einzigen Stelle durchgängig größere oder kleinere Matri-
zen verwenden. Interessant wird es erst, wenn ich die Matrizenwerte einlese,
denn hier tritt zum ersten Mal eine geschachtelte for-Schleife auf. Die erste
for-Anweisung lässt i von 0 bis DIM-1 laufen. Wenn nun i einen dieser Werte
annimmt, kommt der Schleifenrumpf ins Spiel, denn für jedes i soll noch eine
weitere Schleife gestartet werden, in der aber die Variable j von 0 bis DIM-1
läuft. Ist also i am Anfang mit 0 belegt, so läuft j von 0 bis DIM-1, und es wer-
den die Werte von A[0][0] und B[0][0] bis A[0][DIM-1] und B[0][DIM-1]
eingelesen. Im nächsten Schleifendurchlauf der äußeren Schleife steht dann i
148 2 Strukturierte Programmierung mit C

Abb. 2.18. Struktogramm zur Matrizenaddition

auf 1, und wieder wird j von 0 bis DIM-1 laufen, weshalb dann die Werte von
A[1][0] und B[1][0] bis A[1][DIM-1] und B[1][DIM-1] eingelesen werden.
Die doppelte for-Schleife arbeitet also das zweidimensionale Feld zeilenweise
ab; für jedes i, das in der äußeren Schleife auftritt, wird die gesamte innere
Schleife durchlaufen.
Genauso steht es auch im Struktogramm. Der Schleifenrumpf der äußeren
Schleife, die von i=0 bis i=DIM-1 läuft, enthält eine weitere Zählschleife mit
der Schleifenvariablen j, und das heißt, dass für jedes i zwischen 0 und DIM-1
die innere, von j abhängige Schleife durchlaufen werden muss.
Von jetzt an ist alles Routine, denn wenn man einmal eine geschachtelte
Schleife verstanden hat, funktionieren alle weiteren nach dem gleichen Prinzip.
In der nächsten geschachtelten Schleife werden wieder alle möglichen i,j-
Kombinationen abgeklappert und die nötigen Additionen vorgenommen. In
der dritten Doppelschleife tritt dann noch etwas Neues auf. Zwar wird wieder
2.3 Kontrollstrukturen 149

für jedes angesprochene i eine komplette j-Schleife angestoßen, aber inner-


halb des Schleifenrumpfes der äußeren Schleife findet noch ein Zeilenwechsel
statt. Da also in der äußeren Schleife nicht nur die innere for-Schleife gestar-
tet wird, sondern auch noch die Anweisung zum Zeilenwechsel hinzukommt,
habe ich um die Kommandos, die zum Schleifenrumpf der i-Schleife gehören,
Mengenklammern gesetzt, damit sie als zusammengehörender Block erkannt
werden. Und wozu der Zeilenwechsel, ginge es nicht auch ohne? Nein. Ohne
die Anweisung printf("\n") würde das Programm die gesamte Matrix C in
einer einzigen Zeile ausgeben, was sicher nicht im Sinne des Erfinders liegt.
Die Variable i steht hier schließlich für die Zeilennummer, also muss nach der
Ausgabe einer Zeile, am Ende jedes Schleifendurchlaufes der äußeren Schleife,
auch für einen Zeilenwechsel gesorgt werden.
Sie müssen es nicht bei zweidimensionalen Feldern bewenden lassen,
natürlich sind auch drei- und mehrdimensionale Strukturen möglich, die auf
analoge Weise aufgebaut werden. Ich will es jetzt aber mit den Feldern gut
sein lassen und damit die Besprechung der grundsätzlichen Kontrollstrukturen
beenden.

Zweidimensionale Felder
Ein zweidimensionales Feld enspricht der Anordnung eines Schachbretts. Mit
dem Kommando
int x[3][2];
wird zum Beispiel ein zweidimensionales Feld mit drei Zeilen und zwei Spalten
angelegt, das aus den ganzzahligen Komponenten
x[0][0], x[0][1]
x[1][0], x[1][1]
x[2][0], x[2][1]
besteht. Zweidimensionale Felder eigenen sich zur Bearbeitung mit Hilfe ge-
schachtelter for-Schleifen. Auch die Verwendung drei- und höherdimensiona-
ler Felder ist möglich.
Die Verarbeitung höherdimensionaler Felder durch geschachtelte Schleifen
lässt sich problemlos mit Hilfe von Struktogrammen darstellen. Dabei wird
im Schleifenrumpf einer äußeren Schleife eine innere Schleife definiert.

2.3.7 continue und break

Manche Dinge sollte man besser nicht erwähnen, aber ich will mir nicht vor-
werfen müssen, ich hätte Ihnen etwas Wichtiges verschwiegen. C sieht eine
Möglichkeit vor, die ordentliche Abarbeitung einer Iteration zu durchbrechen
und mehr oder weniger wild durch die Gegend zu springen, und zwar in Form
der Kommandos continue und break. Sehen wir uns das an einem einfachen
Beispiel an.
150 2 Strukturierte Programmierung mit C

int i=0;
while (i<10)
{
i++;
printf("%d\n", i);
if (i == 5)
continue;
printf("%d\n",i*i);
}
Die Schleife gibt für die Zahlen von 1 bis 10 sowohl die Zahl selbst als auch
ihr Quadrat aus. Hat i aber den Wert 5, so wird das Kommando continue
abgesetzt, und das bedeutet, dass der gesamte Rest des Schleifenrumpfs über-
sprungen wird und die Verarbeitung mit einem neuen Schleifendurchlauf be-
ginnt. Für die Zahl 5 wird also nur die Zahl selbst ausgegeben und nicht das
Quadrat. Mit continue können Sie auf diese Weise einen Schleifendurchlauf
unterbrechen und sofort mit dem nächsten Durchlauf weiter machen.
Noch schlimmer wird es, wenn Sie break anstatt continue schreiben. Der
Name sagt es schon: die Schleife wird dann nicht mit einem neuen Durchlauf
weiter geführt, sondern schlicht abgebrochen. In meinem Beispielprogramm
würde das Programm also nach der Ausgabe der Zahl 5 die Schleife beenden.
Mit strukturierter Programmierung hat dieses Herausspringen aus der
Schleife nicht mehr viel zu tun, und ich rate deshalb dringend davon ab. Was
immer Sie mit break und continue erreichen wollen, können Sie auch durch
eine anständige Formulierung der Durchführungsbedingung und des Schlei-
fenrumpfes auf die Reihe bekommen, ohne durch die Gegend zu springen.
Erzählen musste ich es Ihnen aber trotzdem, damit Sie notfalls wissen, wie
die beiden Kommandos funktionieren.

continue und break


Der Einsatz des continue-Kommandos in einer Schleife führt dazu, dass
der aktuelle Schleifendurchlauf abgebrochen und mit dem nächsten Schleifen-
durchlauf weitergemacht wird. Der Einsatz von break in einer Schleife führt
dagegen zum kompletten Abbruch der Schleife.

Übungen

2.10. Formulieren Sie einen Algorithmus in Form eines Struktogramms sowie


eines Ablaufplans, der je nach Auswahl des Benutzers einen Eurobetrag in
einen Dollarbetrag oder einen Dollarbetrag in einen Eurobetrag umrechnet.
2.11. Schreiben Sie ein C-Programm sowie ein zugehöriges Struktogramm,
das Folgendes leistet.
Es ist - nach einem stark vereinfachten Verfahren - die Steuer auf das
monatliche Einkommen auszurechnen. Das Einkommen ist über die Tastatur
einzugeben. Die ersten 600 Euro sind steuerfrei. Die nächsten 600 Euro werden
2.3 Kontrollstrukturen 151

mit 30 Prozent besteuert und der Rest mit 40 Prozent. Beträgt allerdings das
gesamte monatliche Einkommen mehr als 15000 Euro, so sind statt 40 Prozent
50 Prozent zu veranschlagen.

2.12. Formulieren Sie einen Algorithmus in Form eines Struktogramms sowie


eines Ablaufplans, der für eine natürliche Zahl n die Summe der ersten n
natürlichen Zahlen berechnet.

2.13. Eine lineare Gleichung hat die Form ax + b = 0 mit den bekannten
Koeffizienten a und b und der Unbekannten x. Schreiben Sie ein C-Programm,
das, falls möglich, nach Eingabe der beiden Koeffizienten a und b die Lösung
x = − ab ausrechnet und ausgibt. Achten Sie dabei auch auf die Sonderfälle, die
entstehen können, wenn der eine oder andere Koeffizient den Wert 0 annimmt.

2.14. Schreiben Sie ein C-Programm einschließlich Struktogramm, das Fol-


gendes leistet.
In einem Labor sind Messreihen zu verarbeiten. Aus Erfahrung weiß man,
dass keine Werte anfallen, die über 1000 bzw. unter −1000 liegen. Dem Benut-
zer liegt nun eine Liste von Messwerten vor, die er in den Computer eingeben
soll, wobei die Eingabe beendet ist, sobald ein Wert über 1000 oder unter
−1000 eingegeben wird. Es ist dabei zu beachten, dass dieser letzte Wert
nicht mehr zur eigentlichen Messreihe gehört, sondern nur dem Abbruch der
Eingabe dient. Das Programm soll dann folgende Größen berechnen und aus-
geben:
• die Summe aller positiven Werte und den Durchschnitt dieser Werte;
• die Summe aller negativen Werte und den Durchschnitt dieser Werte;
• die Summe aller Werte und den Durchschnitt aller Werte.

2.15. Erweitern Sie das Programm aus Aufgabe 2.13 in der Form, dass es
eine beliebige Anzahl von linearen Gleichungen lösen kann. Vor jeder neuen
Gleichung soll der Benutzer gefragt werden, ob er eine weitere Gleichung lösen
will. Bei einer positiven Antwort wird eine Gleichung eingegeben und gelöst,
bei einer negativen wird das Programm beendet. Formulieren Sie ebenfalls
das zugehörige Struktogramm.

2.16. Schreiben Sie ein C-Programm, das eine beliebige Anzahl von quadra-
tischen Gleichungen löst.
Nach dem Programmstart wird der Benutzer aufgefordert, die Koeffizien-
ten p und q der Gleichung x2 + px + q = 0 einzugeben. Sobald diese Eingabe
erfolgt ist, berechnet das Programm sämtliche reellen Lösungen der Gleichung
und gibt sie aus. Falls keine reellen Lösungen existieren, soll das Programm
nur die reelle Unlösbarkeit ausgeben. Nach dieser Ausgabe wird der Benutzer
gefragt, ob er eine weitere Gleichung gelöst haben möchte. Gibt er j oder J
ein, so beginnt das Spiel von Neuem, gibt er etwas anderes ein, verabschiedet
sich das Programm.
Die Lösungsformel für quadratische Gleichungen lautet
152 2 Strukturierte Programmierung mit C

p p2
x1,2 =− ± − q.
2 4
Die Lösungen sind genau dann reell, wenn der Wurzelinhalt nicht kleiner als
0 ist. Die Wurzel aus einer double-Zahl x berechnet man mit dem Komman-
do sqrt(x). Damit Sie auf dieses Kommando zugreifen können, sollten Sie
die Header-Datei math.h mit der Präprozessoranweisung #include <math.h>
einbinden.

2.17. Schreiben Sie ein C-Programm, das als Eingabe eine Zeichenkette er-
wartet und dann feststellt, wie oft der Buchstabe a in dieser Zeichenkette
vorkommt.

2.18. Schreiben Sie ein C-Programm, das Folgendes leistet.


Der Benutzer wird zum Programmstart aufgefordert, die Einträge einer
Matrix aus drei Zeilen und drei Spalten einzugeben. Nach der Eingabe dieser
Zahlen berechnet das Programm sowohl die kleinste als auch die größte ein-
gebene Zahl und gibt beide Werte aus. Anschließend wird die gesamte Matrix
ausgegeben.

2.4 Zeiger und dynamische Datenstrukturen


Erinnern Sie sich für einen Moment an das Längenproblem, das im Zusam-
menhang mit den Feldern aufgetreten ist: ein Feld hat eine feste Länge, die
man auch besser nicht überschreiten sollte, und wenn man durch einen dum-
men Zufall mehr Einträge verarbeiten muss als das Feld hergibt, dann gerät
man in Schwierigkeiten. Das liegt daran, dass ein Feld, so wie es nun mal ge-
baut ist, eine recht statische Struktur hat. Sie dürfen eine bestimmte, ein- für
allemal festgelegte Anzahl von Elementen in das Feld packen, natürlich auch
weniger, aber nicht mehr. Und auch wenn Sie bescheiden sind und ein Feld,
in das eigentlich 100 Einträge passen, nur mit 10 Einträgen belasten, spricht
das sicher für Ihre Genügsamkeit, aber gut ist das nicht, denn auf diese Weise
verschwenden Sie Speicherplatz. Schließlich haben Sie ja beim Anlegen des
Feldes den Platz für die 100 Einträge reserviert, und sie dann nicht zu nützen
ist übelste Vergeudung.
Weiß man genau, wie lang oder breit eine bestimmte Datenmenge werden
soll, so spricht nichts gegen statische Datenstrukturen. Oft genug hat man
aber vor dem Programmstart nicht die leiseste Ahnung, was im Verlauf des
Prgramms alles auf einen zukommen kann und muss dafür sorgen, dass das
Programm flexibel auf einen unerwarteten Anstieg der Datenmengen reagiert.
Dazu sind statische Datenstrukturen nicht zu gebrauchen, man braucht so
etwas wie eine dynamische Datenstruktur. Was das nun eigentlich ist, werde
ich Ihnen in diesem Abschnitt zeigen.
2.4 Zeiger und dynamische Datenstrukturen 153

2.4.1 Zeiger

Der zentrale Begriff im Zusammenhang mit dynamischen Datenstrukturen ist


der Begriff des Zeigers. Um ihn zu verstehen, müssen Sie sich auf den Aufbau
des Arbeitsspeichers besinnen, den wir im ersten Kapitel besprochen haben.
Kurz gesagt, besteht er aus einer Folge gleichlanger Speicherzellen; in jeder
Speicherzelle oder auch Speicherstelle kann man ein so genanntes Wort ab-
legen. Wie groß so eine Speicherzelle ist, kann man nicht allgemein sagen,
es hängt vom einzelnen Rechner ab. Das kann mir aber auch völlig egal sein,
denn mich interessiert hier vor allem die Tatsache, dass jede Speicherzelle eine
bestimmte Adresse hat, über die man sie ansprechen kann, ähnlich wie eine
Hausnummer in einer bestimmten Straße. Und genau wie Sie einem auswärti-
gen Besucher sagen können, dass Herr Meier im Haus mit der Nummer 17
wohnt, kann man den Inhalt einer Speicherzelle finden und verwenden, indem
man die Adresse der Zelle kennt und über die Adresse auf die Zelle zugreift.
Im ersten Kapitel hatte ich Ihnen schon erzählt, dass eine Adresse eine
ganze Zahl ist, eine schlichte Nummer, die mindestens 0 ist und höchstens
so groß sei kann wie die gesamte Anzahl der im Arbeitsspeicher vorhandenen
Speicherzellen.

Speicherstelle 0
Speicherstelle 1
Speicherstelle 2
Speicherstelle 3
Speicherstelle 4
Speicherstelle 5

Abb. 2.19. Aufbau des Arbeitsspeichers

Wenn Sie nun also die einfache Variablendeklaration int x vornehmen, so


wird im Arbeitsspeicher ein hinreichend großer freier Platz gesucht, in den eine
Variable vom Typ int hinein passt. Wo das sein wird, muss mich zunächst
nicht kümmern, aber in jedem Fall wird es eine Adresse für diese Variable
geben, eine Nummer, die aussagt, in welcher Speicherzelle meine Variable
ihre vorläufige Heimat gefunden hat - oder zumindest doch, wo die Variable
anfängt, denn es ist ja möglich, dass man für eine längere Variable mehrere
Speicherzellen belegen muss. Liegt also beispielsweise meine Variable x in
der 1717-ten Speicherzelle, dann hat sie die Adresse 1717 und ist über diese
Hausnummer eindeutig zu identifizieren. Genau genommen sind die Adressen
154 2 Strukturierte Programmierung mit C

natürlich hexadezimale Zahlen und keine dezimalen wie in meinem Beispiel,


aber das ist für den Augenblick nicht weiter wichtig.
Wichtig ist dagegen, dass es eine eindeutige Zuordnung zwischen dem Va-
riablennamen und der konkreten Adresse im Arbeitsspeicher gibt; hat man
einmal die Adresse, so findet man auch die Variable wieder. Was passiert al-
so, wenn ich meine Variable x definiere und ihr den Wert 38 zuweise? Ganz
genau: zuerst wird der Variablen eine Adresse im Arbeitsspeicher zugeord-
net, also eine feste Position im Speicher, bei der sie später zu finden sein
wird, falls sie irgend jemand suchen sollte. Diese Zuordnung machen nicht
Sie als Programmierer; wahrscheinlich wäre Ihnen eine solche Aktion in den
bisher besprochenen Programmen schon einmal aufgefallen. Das übernimmt
selbstverständlich der C-Compiler, der sein Geld auch irgendwie verdienen
muss. Sobald er also eine Speicheradresse festgelegt hat, wird er mithilfe die-
ser Adresse den Anfang des nötigen Speicherplatzes ausfindig machen. Da ich
davon ausgehe, dass es sich um die Speicherstelle 1717 handelt, wird der Wert
von x, also die Zahl 38, in dem Speicherbereich ab der Speicherstelle mit der
Nummer 1717 abgelegt - es kann sich dabei um mehrere Speicherzellen han-
deln, falls der eingesetzte Datentyp mehrere Speicherzellen verlangt, aber auf
jeden Fall wird der Speicherbereich für meine Variable x an der Stelle 1717
beginnen. Sobald nun umgekehrt irgendeine Programmanweisung auf meine
Variable x zugreifen will, wird die Speicherstelle 1717 in ihrer Ruhe gestört
und der dort abgelegte Wert ausgelesen.
Nun ist es eine Eigenart von C, dass man nicht nur die Variablen verwen-
den, sondern auch auf ihre Adressen im Arbeitsspeicher zugreifen kann. Sie
haben sogar schon mehrfach gesehen, wie das funktioniert; bei der Einführung
des scanf()-Kommandos hatte ich es erwähnt. Die Adresse einer Variablen
x bekomme ich mit dem Adressoperator &, das heißt: &x ist die Adresse der
Speicherzelle, an der sich x im Arbeitsspeicher aufhält. Egal wie die Variable
heißt, ihre Adresse im Arbeitsspeicher bekommen Sie immer, indem Sie das
&-Zeichen vor den Variablennamen schreiben. Das scanf()-Kommando ver-
langt beispielsweise die Angabe einer konkreten Speicheradresse, in der der
eingelesene Wert landen soll, und daher musste ich bei allen Eingaben den
Adressoperator & verwenden.
Aber was soll das alles? Was könnte es mir für meinen weiteren Lebensweg
nützen, wenn ich die Adresse einer bestimmten Variable kenne? Die Antwort
liegt in dem Problem der statischen und dynamischen Datenstrukuren, das
ich zu Anfang angeschnitten habe. Sie wissen nun, dass man auch mit den
Adressen im Arbeitsspeicher innerhalb eines C-Programms umgehen kann.
Wenn man es nun fertig bringt, von einer Variable aus direkt auf die nächste
gewünschte Variable verweisen zu können, indem man ihre Adresse angibt,
dann könnte das auf dem Weg zu einer wirklich dynamischen Datenstruktur
hilfreich sein. Stellen Sie sich beispielsweise eine Schnitzeljagd vor, die schon
so manchen Kindergeburtstag gerettet hat. Es gibt mehrere Stationen, wobei
die Kinder nicht wissen, wie viele Stationen abzuklappern sind, und an jeder
Station finden sie sowohl irgendwelche Süßigkeiten, um sie bei Laune zu hal-
2.4 Zeiger und dynamische Datenstrukturen 155

ten, als auch einen Hinweis darauf, wo die nächste Station zu finden sein wird.
Die Süßigkeiten entsprechen den eigentlichen Datenwerten, die Sie irgendwo
abspeichern wollen. Aber da Sie genau wie die Kinder nicht wissen werden,
wie viele Datenelemente noch kommen, machen Sie es sich leicht und sorgen
erst einmal dafür, dass wenigstens ein nächster Wert verarbeitet werden kann
- so wie die Geburtstagsgäste, die nach der ersten Station erst einmal die
zweite Station finden wollen. Wenn es mir also möglich ist, von einer Varia-
blen aus auf eine nächste zu deuten, auf sie zu zeigen, dann sollte ich auch in
der Lage sein, quer durch den Arbeitsspeicher eine Schmitzeljagd auf meine
abgelegten Datenelemente zu veranstalten. Und wie zeigt man auf eine Varia-
ble? Natürlich indem man auf ihre Adresse im Arbeitsspeicher zeigt, und hier
schließt sich der Kreis. Die Möglichkeit, mit Adressen zu hantieren, erlaubt es
mir, eine Datenschnitzeljagd durchzuführen, weil ich auf die Adressen zeigen
und mich so von einer freundlichen Speicherstelle zur anderen hangeln kann.
Hier sollte Sie ein ungutes Gefühl beschleichen. Können wir denn schon auf
eine bestimmte Speicherstelle zeigen? Eigentlich nicht. Der Adressoperator &
erlaubt es mir nur, die Adresse einer existierenden Variablen anzusprechen,
aber ich habe noch keinen Datentyp zur Verfügung gestellt, den man als einen
Zeigertypen bezeichnen könnte. Das wird sich jetzt ändern.
Die Vereinbarung einer Variablen als Zeiger erfolgt ganz einfach mit dem
Zeichen *. Soll also p eine Zeigervariable sein, die auf eine Speicherzelle für
int-Variablen zeigt, so setzt man die Definition
int *p;
ab. Damit wird p ein Zeiger, stellt also eine Adresse für eine int-Variable
zur Verfügung - anders gesagt: p zeigt auf eine int-Variable. Ein Zeigertyp ist
also nichts anderes als ein Datentyp, mit dessen Hilfe man Adressen darstellen
kann. Und weil Zeiger in der englischen Sprache pointer heißt, benennt man
Zeigervariablen häufig mit dem Buchstaben p. Noch einmal: die Zeigervariable
beheimatet die Adresse einer bestimmten Variable, nicht aber deren Wert.
Denken Sie noch einmal an die int-Variable x, die an der Speicherstelle 1717
gelandet ist und den Wert 38 hat. Da meine Zeigervariable p dazu da ist, auf
Variablen vom Typ int zu zeigen, also Adressen von int-Variablen aufnehmen
kann, ist die Wertzuweisung
p = &x;
ausgesprochen sinnvoll. Nach ihrer Ausführung finden Sie in der Variablen
p die Adresse der Variablen x - nicht etwa ihren Inhalt. Aber auch auf den
Inhalt können Sie von der Zeigervariablen aus zugreifen, indem Sie den zu &
umgekehrten Operator einsetzen: ist p nämlich ein Zeiger auf die Variable x,
dann bezeichnet *p den Inhalt der Speicherstelle, auf die der Zeiger p zeigt.
Zurück zu meinem alten Beispiel. Ich hatte x mit dem Wert 38 belegt,
und die Variable x stand an der Speicherstelle 1717. Folglich finden Sie in p
die Adresse von x, also 1717, und *p ist gleich dem Inhalt der angezeigten
Speicherstelle, also gleich 38. Die Variable p selbst steht natürlich auch wie-
der irgendwo im Speicher, aber ganz sicher nicht an der Speicherstelle 1717,
denn dort befindet sich schon x. Aber ob Sie beispielsweise printf("%d",x)
156 2 Strukturierte Programmierung mit C

Speicherstelle 1234
1717 Speicherstelle 1235

38 Speicherstelle 1717
Speicherstelle 1718

Abb. 2.20. Speicherplatz für Variable und Zeiger

oder printf("%d",*p) schreiben, ist völlig egal; in beiden Fällen wird 38


ausgegeben, weil p auf x zeigt und x den Wert 38 hat. Im Speicher sieht das
dann etwa so aus wie in Abbildung 2.20, in der Sie einen kleinen Speicheraus-
schnitt sehen. Die Variable p steht hier an der Speicherstelle 1235 und enthält
nichts weiter als die Adresse der Variablen x, denn die habe ich ihr ja mit
dem Kommando p = &x zugewiesen. Dagegen steht x selbst wie ein Fels in
der Brandung immer noch an der Speicherzelle 1717, und p zeigt auf genau
diese Stelle.

p 38

Abb. 2.21. Zeiger auf eine Variable

Um klar zu machen, dass eine Zeigervariable auch wirklich auf eine Spei-
cherstelle zeigt, verwendet man oft eine Darstellungsweise wie in Abbildung
2.21. Die Variable p zeigt auf eine Speicherstelle, und der Inhalt dieser Spei-
cherstelle ist die Zahl 38.
Sehen wir uns die Auswirkungen von Zeigern noch einmal an einem klei-
nen Beispiel an. Ich definiere zwei float-Zeiger namens p1 und p2, also zwei
Zeigervariablen, die jeweils auf eine Speicherstelle zur Aufnahme einer reellen
Zahl zeigen. Das Kommando lautet:
float *p1, *p2;
Nun kann ich die Speicherstellen mit Leben füllen, indem ich ihnen konkrete
Werte zuweise, zum Beispiel
2.4 Zeiger und dynamische Datenstrukturen 157

*p1 = 18.23; *p2 = 234.198;


An den Speicherstellen, auf die p1 und p2 zeigen, befinden sich jetzt also die
reellen Zahlen 18.23 bzw. 234.198, was auch in Abbildung 2.22 dargestellt
wird.

p1 18.23

p2 234.198

Abb. 2.22. Zwei Zeiger

Nun werde ich ein wenig Durcheinander veranstalten und zwei verschiede-
ne Arten von Wertzuweisungen ausprobieren; der Ordnung halber führe ich
jeweils noch einmal die Zeigerdeklaration mit auf. Was passiert also bei der
Durchführung des folgenden Programmstücks?
float *p1, *p2;
*p1 = 18.23; *p2 = 234.198;
*p1 = *p2;
Die ersten beiden Zeilen haben wir schon besprochen. In der dritten Zeile wird
mit den Inhalten der Speicherzellen gearbeitet, auf die p1 und p2 zeigen; man
sagt auch, dass sie diese Speicherstellen referenzieren. Der von p1 referenzier-
ten Speicherstelle soll der Inhalt der Speicherstelle zugewiesen werden, auf
die mein Zeiger p2 treu und brav verweist. Also wird nach der Durchführung
dieses Kommandos p1 zwar noch auf die gleiche Speicherstelle zeigen wie vor-
her, aber in dieser Zelle steht jetzt der Wert 234.198, der auch gleichzeitig an
der Stelle steht, auf die p2 zeigt. Diese Situation ist auf der linken Seite von
Abbildung 2.23 zu sehen.

p1 234.198 p1 18.23

p2 234.198 p2 234.198

Abb. 2.23. Zwei Zeiger nach den Zuweisungen

Ganz anders präsentiert sich die Lage nach den folgenden Kommandos.
158 2 Strukturierte Programmierung mit C

float *p1, *p2;


*p1 = 18.23; *p2 = 234.198;
p1 = p2;
Die einzige Änderung habe ich in der dritten Zeile vorgenommen, wo jetzt
der *-Operator nicht mehr vorkommt. Das bedeute, dass der Zeigervariablen
p1 jetzt der Wert der Zeigervariablen p2 zugewiesen wird. In p2 steht aber
ein Verweis auf eine Speicherstelle, und zwar auf die Speicherstelle, in der die
reelle Zahl 234.198 abgelegt ist. Dieser Verweis wird jetzt auch der Zeiger-
variablen p1 zugewiesen, sodass p1 die gleiche Speicherzelle anzeigt wie p2
und deshalb auch hier *p1=234.198 gilt. Trotzdem ist das nicht die gleiche
Situation wie vorher. Während in der ersten Fassung zwei verschiedene Spei-
cherstellen existierten, die von je einem Zeiger referenziert wurden, aber beide
den gleichen Inhalt hatten, gibt es jetzt nach wie vor die alten Speicherstellen
mit den beiden alten Werten 18.23 und 234.198, aber nur noch eine Refe-
renzierung: sowohl p1 als auch p2 zeigen auf die eine Speicherstelle, die den
Wert 234.198 mit sich herum schleppt. Die andere Speicherstelle, in der 18.23
vor sich hin vegetiert, existiert zwar noch, fühlt sich aber vermutlich ziemlich
einsam, weil sich kein Zeiger mehr um sie kümmert. Es kann also im Zuge des
Zeigereinsatzes vorkommen, dass Speicherinhalte verloren im Arbeitsspeicher
stehen, da sie nicht mehr referenziert werden und keiner mehr weiß, an wel-
cher Stelle des Speichers sie sich befinden. Diese Situation finden Sie auf der
rechten Seite von Abbildung 2.23. Bei Spielen dieser Art kann übrigens ein
wenig Vorsicht nicht schaden. Da Sie zwei Zeiger auf reelle Zahlen deklarieren,
ihnen aber noch keine konkrete Adresse zuweisen, bevor Sie auf *p1 und *p2
Zahlenwerte schicken, sollte Ihnen Ihr C-Compiler eine Warnung zukommen
lassen, dass Ihre Zeiger nicht ganz korrekt benutzt werden. Da hat er schon
recht, aber funktionieren wird es trotzdem, auch wenn es nicht ganz der reinen
Lehre entspricht. Wie man dieser Warnung entgeht, zeige ich Ihnen später im
Zusammenhang mit der Speicherreservierung bei Strukturtypen.
So viel zu den Zeigervariablen. Erwähnen sollte ich wohl noch zwei Dinge.
Zunächst einmal kann man das Spiel auch beliebig weiter treiben und Zeiger
auch auf Zeiger zeigen lassen. Wie bitte? Ganz einfach. Nehmen Sie beispiels-
weise die Anweisung
int **p;
Wegen des doppelten Sterns ist dann p ein Zeiger auf eine gesternte Variable,
und die gesternten Variablen sind nun einmal die Zeiger. Also ist p ein Zeiger
auf einen Zeiger auf eine int-Variable. Hat man beispielsweise noch die beiden
Variablen
int *q, i;
definiert, so sind deshalb die Wertzuweisungen
q=&i; p=&q;
sinnvolle Anweisungen: auf q steht dann die Adresse von i und auf p steht
die Adresse von q. Umgekehrt können Sie dann natürlich auch
q=*p; i=*q;
2.4 Zeiger und dynamische Datenstrukturen 159

ansetzen; auch in diesem Fall passt wieder alles zusammen. Und um sich den
Zwischenschritt mit der Variablen q zu sparen, darf man auch direkt so etwas
wie
printf("%d\n" **p);
schreiben. In diesem Fall wird durch den doppelten Sternoperator erst nach-
gesehen, was an der Stelle steht, auf die p selbst zeigt, und da p auf einen
Zeiger zeigt, wird durch den zweiten Stern auf den Inhalt der Speicherstelle
zugegriffen, auf die *p zeigt.
Auf die Gefahr hin, Sie etwas zu verwirren, muss ich noch einen weiteren
Punkt anführen. Sie haben wohl schon gemerkt, dass der Adressoperator &
und der Sternoperator * sich gegenseitig aufheben. Ist zum Beispiel i eine
int-Variable und q ein Zeiger auf eine int-Variable, so kann man die Adresse
von i durch die Anweisung q=&i in der Variablen q unterbringen, während die
Anweisung printf("%d",*q) den Wert von i wieder ausgibt. Das geht auch
direkt, beide Operatoren kann man kombinieren, und da sie sich gegenseitig
aufheben, passiert dann auch nicht allzu viel. Unter *&i ist beispielsweise zu
verstehen, dass die Adresse von i genommen und dann nachgesehen wird,
welcher Inhalt sich wohl unter dieser Adresse verbirgt. Aber natürlich ist da
genau der Wert von i, denn davon bin ich ja ausgegangen. Also ist *&i nichts
anderes als i selbst; lassen Sie sich von solchen kombinierten Operatoren nicht
hinters Licht führen. Dass die beiden Operatoren sich gegenseitig aufheben,
erkennen Sie auch an ihrer verbalen Beschreibung: mit & wird referenziert,
mit * wird dereferenziert. Das sind aber nur Namen, Bezeichnungen für das
Wechselspiel zwischen den Variablen und den Zeigern auf die Variablen.

Zeiger
Ein Zeiger ist eine Variable, die auf eine bestimmte Stelle im Arbeitsspeicher
verweist. Durch die Anweisung
int *p;
wird beispielsweise eine Zeigervariable p definiert, die auf eine Zelle im Ar-
beitsspeicher zeigt, an der eine int-Zahl angelegt werden kann. Umgekehrt
wird bei einer Variablen x eines beliebigen Typs durch &x die Adresse von x
im Hauptspeicher angegeben. Durch p=&x wird die Variable x mit dem Zeiger
p referenziert. Umgekehrt wird der Zeiger p durch x=*p dereferenziert.

2.4.2 Noch einmal Felder

Ein wenig exotisch kann einem dieses Hantieren mit Zeigern schon vorkom-
men, zumal auch nicht jede Programmiersprache ihre Programmierer mit Zei-
gern traktiert. C hat sie, Pascal hat sie auch, Cobol dagegen sind Zeiger fremd,
und Java ist ein ganz besonderer Fall. Oberflächlich betrachtet kann Java mit
Zeigern rein gar nichts anfangen - aber wenn man genauer hinsieht, stellt man
fest, dass gerade Java fast nichts anderes kennt als Zeiger. Dazu später, im
nächsten Kapitel, mehr. Für den Moment will ich Ihnen erst einmal zeigen,
160 2 Strukturierte Programmierung mit C

warum Zeiger bei Licht betrachtet doch keine so exotische Konstruktion sind,
und am besten sieht man das ein, indem man sich klar macht, wo sie über-
all benutzt werden. Natürlich bei den dynamischen Datenstrukturen; auf die
komme ich im nächsten Teil. Aber auch bei den bereits besprochenen Feldern,
und darüber möchte ich jetzt ein wenig reden.
Auf den ersten Blick ist kein Zusammenhang zwischen einem Feld und
einem Zeiger zu erkennen. Das täuscht aber. Erstens kann man Felder aus
Zeigern bilden und zweitens ist ein Feld, sobald man genauer hinsieht, von
einem Zeiger kaum noch zu unterscheiden. Sehen wir uns zuerst einmal die
Felder aus Zeigern an.
Dazu ist gar nicht so viel zu sagen. Wenn Sie wissen, dass Sie nicht nur
einen Zeiger auf eine int-Variable brauchen werden, sondern deren 12, dann
können Sie das ganz einfach mit der Deklaration
int *vektor [12];
dem Compiler bekannt geben. Damit wird ein Feld vektor aus 12 Komponen-
ten angelegt, wobei jede einzelne Komponente ihrerseits ein Zeiger auf eine
Speicherstelle ist, die eine ganze Zahl aufnehmen kann. Haben Sie beispiels-
weise ein Feld aus x aus 12 ganzen Zahlen zur Verfügung, dann wird Ihnen
die for-Schleife
for (i=0; i<12; i++)
vektor[i] = &x[i];
in jede Komponente des Feldes vektor die Adresse der int-Variablen x[i]
schreiben. Anders gesagt: Felder können natürlich jede Art von Datenelemen-
ten aufnehmen, insbesondere auch Zeiger auf andere Daten.
Ein wenig aufpassen müssen Sie dabei aber schon. Sie könnten ja auch auf
die Idee kommen, Ihr Feld aus Zeigern mit dem Kommando
int (*vektor) [12];
anzulegen, und das bedeutet etwas völlig anderes. Hier wird nicht ein Feld aus
12 Zeigern angelegt, denn offenbar bezieht sich der Stern nur auf den Namen
vektor und nicht auf die Kombination aus Namen und eckigen Klammern.
Also definieren Sie hier nur eine einzige Zeigervariable namens vektor, die
auf einen Vektor aus 12 Elementen zeigt. Was passiert also beim Durchlaufen
des folgenden Programmstücks?
int (*vektor)[3];
int x[3],i;
x[0]=0; x[1]=1; x[2]=2;
vektor = &x;
for (i=0; i<3; i++)
printf("%d \n", (*vektor)[i]);
Die Variable vektor ist jetzt ein Zeiger auf ein dreielementiges Feld aus ganzen
Zahlen, und wie es der Zufall will, ist x genauso ein Feld. Daher kann ich die
Adresse von x sinnvollerweise der Zeigervariablen vektor zuweisen. Wenn ich
nun auf die Inhalte dieses Feldes ganzer Zahlen zugreifen will, ohne weiterhin
den Feldnamen selbst zu verwenden, geht das ganz leicht: schließlich ist das
Feld der Inhalt der Speicherstellen, auf die vektor verweist, es muss also über
2.4 Zeiger und dynamische Datenstrukturen 161

*vektor ansprechbar sein. Und genau das ist der Fall; mit (*vektor)[i]
erhalten Sie exakt wieder x[i] zurück. Die Klammern um vektor sind dabei
leider nötig, da ohne diese runden Klammern die eckigen Klammern [ und
] eine höhere Priorität haben als der Sternoperator und das System daher
nicht erkennt, dass jetzt die i-te Komponente des Feldes genommen werden
soll, das der Zeiger vektor anzeigt.
Wichtiger als solche etwas gefährlichen Konstruktionen, bei denen man
auf jede Klammer achten muss, ist die Tatsache, dass ein Feld intern fast
das Gleiche ist wie ein Zeiger. Der Name des Feldes bezeichnet nämlich in
Wahrheit einen Zeiger, der die Adresse des ersten Feldelementes aufnimmt,
und ab dieser Adresse wird dann weiter gezählt. Haben Sie also ein int-Feld
namens x, dann bezeichnen x selbst und &x[0] genau das Gleiche, und zwar
die Anfangsadresse des Feldes. Das wird auch konsequent durchgehalten. Sie
können beispielsweise eine Zeigervariable p definieren, die zum Zeigen auf int-
Variablen geeignet ist, und dann problemlos die Zuweisung p=x vornehmen.
Hier wird nur ein Zeiger dem anderen zugewiesen, das haben wir schon vorher
gemacht und auch da ist es gut gegangen. Natürlich geht es auch hier gut, p
zeigt auf den Anfang des Feldes, also auf die Speicherzelle von x[0], und weil
p ja ein int-zeiger war, sind *p - der Inhalt der Speicherstelle, auf die p zeigt
- und x[0] - die ganze Zahl am Anfang des Feldes x - identisch.
Dieses Verfahren erklärt, woher der Compiler weiß, wie er die Feldelemente
abzuspeichern hat und wie er sie durch Angabe des Feldnamens wiederfindet.
Mit dem Feldnamen wird eben eine Zeigervariable bezeichnet, mit der man auf
eine bestimmte Speicheradresse zeigen kann, und sobald man diese Anfangs-
adresse gefunden hat, liegt der Rest einfach dahinter. Für Programmierer,
die keine Felder mögen und beim Anblick der eckigen Klammern zu Magen-
schmerzen neigen, hat das entschiedene Vorteile, denn Sie können mit Hilfe
von Zeigern eine Feldverarbeitung programmieren, ohne auch nur ein einziges
Feld explizit zu definieren. Wie das geht, können Sie sich fast schon selbst
denken, trotzdem zeige ich es Ihnen hier an einem kleinen Beispiel, in dem ich
zwar ein Feld definiere, aber dann nie wieder eckige Klammern verwende.
int feld[3],i;
int *p;
p = feld;
for (i=0; i<3; i++)
*(p+i) = i*i;
for (i=0; i<3; i++)
printf("%d \n", *(p+i));
Zuerst wird ein Feld feld angelegt und dann dem Zeiger p die Startadres-
se dieses Feldes zugewiesen - das darf ich ohne Adressoperator machen, denn
der Name des Feldes ist ja selbst ein Zeiger. Danach arbeite ich fast nur noch
mit dem Zeiger. Innerhalb der for-Schleife wird der durch p+i bezeichne-
ten Speicherstelle der Wert i2 zugewiesen, aber was in aller Welt soll dieses
p+i bedeuten? Das ist eine praktische Sache. Der Zeiger p selbst zeigt auf eine
Speicherstelle zur Aufnahme einer ganzen Zahl. Die nächste Speicherstelle, die
162 2 Strukturierte Programmierung mit C

eine ganze Zahl aufnehmen kann, darf ich dann mit dem Zeiger p+1 anspre-
chen, wieder die nächste mit p+2 und so weiter. In meiner for-Schleife wird
also der Wert i2 auf drei hintereinander liegende Speicherstellen geschrieben,
deren erste genau der Anfang meines Feldes war. Kurz gesagt: auf genau die
zur Aufnahme des Feldes reservierten Speicherstellen habe ich meine Werte
geschrieben, und damit habe ich das Feld - ganz wie vorausgesagt - gefüllt,
ohne so etwas wie eckige Klammern zu verwenden. Der Zeiger p+i zeigt ge-
nau auf die Speicherstelle, in der feld[i] seinen Platz findet, und wenn Sie
das Ganze dereferenzieren, dann liefern *(p+i) auf der einen und feld[i] auf
der anderen Seite die gleichen ganzen Zahlen. Umgekehrt kann man diese Ent-
sprechung natürlich auch betrachten: der Zeiger p+i und die Adressenangabe
&feld[i] meinen genau dasselbe.
Eine Zeigervariable kann also sowohl erhöht als auch erniedrigt werden.
Dabei gelten die gleichen Inkrementierungs- und Dekrementierungsmethoden
wie bei den ganzen Zahlen; anstatt p+1 können Sie auch p++ schreiben, und
p-1 dürfen Sie jederzeit durch p-- ersetzen. Schreiben Sie also
float *p, x;
x = *p++;
so wird, wie Sie das auch bei den ganzen Zahlen schon gesehen haben, zuerst
der float-Wert, auf den p zeigt, der Variablen x zugewiesen und danach der
Zeiger p inkrementiert, weshalb er genau eine Speicherstelle weiter zeigt als
vorher. Entscheiden Sie sich dagegen für
float *p, x;
x = *++p;
dann wird zuerst der Zeiger hochgezählt, und der Variablen x wird die re-
elle Zahl zugewiesen, die an der um eins hochgezählten Speicherstelle steht.
Natürlich ist das nur dann sinnvoll, wenn Sie auch sicher sein können, dass
dort wirklich eine reelle Zahl steht, ansonsten kommen Sie in Schwierigkeiten.
Ich hätte in meinem kleinen Beispiel übrigens auch auf den zusätzlichen
Zeiger p verzichten können, denn Feldnamen sind schließlich auch Zeiger. Also
können sie auch schlicht das Programmstück
int feld[3],i;
for (i=0; i<3; i++)
*(feld+i) = i*i;
for (i=0; i<3; i++)
printf("%d \n", *(feld+i));
einsetzen und kommen zum gleichen Resultat. Wer sich mit den eckigen Klam-
mern und den Feldern gar nicht anfreunden kann, der darf auch vollständig
auf sie verzichten und feld einfach als einen Zeiger int *feld definieren, der
auf eine ganze Zahl zeigt. Da im Verlauf des Programms nur auf Adressen
Bezug genommen wird und nicht auf Feldnummern, ist das gar kein Problem.
Ich verschweige Ihnen meine Meinung nicht: von solchen Spielereien hal-
te ich in der praktischen Programmierung nicht viel, solange man nicht aus
inhaltlichen Gründen gezwungen ist, direkt auf Speicherstellen zuzugreifen.
Wenn Sie mit einem Feld hantieren wollen, dann sollten Sie auch ein Feld ver-
2.4 Zeiger und dynamische Datenstrukturen 163

wenden und nicht mit Speicheradressen herumspielen; je transparenter und


verständlicher ein Programm ist, desto besser. Dass das Hin- und Hersprin-
gen zwischen Feldern und Zeigern trotzdem funktioniert, auch wenn es im
Rahmen der strukturierten Programmierung vielleicht nicht im Übermaß ein-
gesetzt werden sollte, erklärt aber immerhin das seltsame Phänomen, das bei
der Eingabe von Zeichenketten über die Tastatur aufgetreten ist. Sie erin-
nern sich: um eine Zeichenkette kette von der Tastatur einzulesen, brauchte
ich nicht wie üblich den Adressoperator & einzusetzen, sondern durfte direkt
scanf("%s",kette) schreiben. Das muss Sie jetzt nicht mehr wundern. Das
scanf()-Kommando verlangt nun mal die Adressen der Speicherstellen, auf
die es die Eingaben schreiben soll, und der Name eines Feldes entspricht schon
einem Zeiger, also einer Adressenangabe. Wozu also noch mit einem Adress-
operator den Compiler verwirren? Wenn er eine Adresse haben will, genügt
die Angabe des Feldnamens; da Zeichenketten als char-Felder realisiert sind,
kann das scanf()-Kommando auch ohne Adressoperator eine Zeichenkette
von der Tastatur einlesen.
Natürlich können Sie auch beim Einsatz von Zeichenketten ganz auf die
eckigen Klammern verzichten, wie Sie es oben schon bei den Feldern ganzer
Zahlen gesehen haben. Anstatt also eine Zeichenkette mit char kette[10]
anzugeben, können Sie auch char *kette schreiben und dann auf die Möglich-
keiten der Zeigerverarbeitung zurückgreifen, die ich Ihnen hier gezeigt habe.

Felder und Zeiger


Der Name eines Feldes ist immer auch ein Zeiger auf die Adresse des Feldele-
ments, mit dem das Feld beginnt. Ist daher ein Feld beispielsweise durch
int feld[10];
definiert, so entspricht feld der Adresse &feld[0], und *feld entspricht dem
Inhalt feld[0].
Für Zeigervariablen existiert eine eigene Arithmetik. Ist p ein Zeiger auf
Elemente eines bestimmten Datentyps, so kann man mit p+1 die nächst-
folgende Speicherstelle ansprechen, die ein Element dieses Datentyps auf-
nehmen kann. Weiterhin existieren die Inkrementierungs- und Dekrementie-
rungsmöglichkeiten p++, ++p, p-- und --p.

2.4.3 Verkettete Listen


Jetzt habe ich fast alles zusammen, um Ihnen eine zentrale Anwendung der
Zeigertechnik vorzuführen: die dynamischen Datenstrukturen in Form einer
verketteten Liste. Nur eine Kleinigkeit brauche ich noch, ohne die nichts funk-
tioniert, und das ist der Strukturtyp struct. Sein Grundprinzip ist einfach
genug. Stellen Sie sich die Datenverarbeitung eines Autohauses vor, in dem
natürlich Autodaten anfallen. Der Einfachheit halber gehe ich davon aus, dass
ein Auto vollständig durch seine Marke, seine Farbe und seinen Hubraum be-
schrieben wird. Folglich brauche ich eine Datenstruktur, die diese drei Merk-
male als eine Einheit zusammenfasst und dann ein Auto als Element der
164 2 Strukturierte Programmierung mit C

Datenstruktur ablegen kann. Das folgende Programmstück zeigt, wie es geht.


main()
{
struct Auto
{
char marke[10];
char farbe[10];
int hubraum;
};
struct Auto x;
printf("Marke eingeben\n");
scanf("%s",x.marke);
printf("Farbe eingeben\n");
scanf("%s",x.farbe);
printf("Hubraum eingeben\n");
scanf("%d",&x.hubraum);
printf("Das Auto ist ein %s, seine Farbe ist %s, \n",
x.marke, x.farbe);
printf("sein Hubraum betraegt %d ccm.\n",x.hubraum);
}
Ich habe hier einen Strukturtyp namens Auto definiert und damit einen eige-
nen Datentyp erfunden. Jedes Element dieses Datentyps hat drei Komponen-
ten: eine Zeichenkette marke, eine Zeichenkette farbe und eine ganze Zahl
hubraum. Beachten Sie übrigens, dass nach der Mengenklammer, mit der ich
die Definition des Strukturtyps abschließe, noch ein Strichpunkt steht: er be-
zeichnet das Ende der Definition, ohne ihn kommen Sie in Schwierigkeiten.
Wenn ich nun ein Element meines selbstgebackenen Datentyps Auto anle-
ge, dann werde ich seine Marke, seine Farbe und seinen Hubraum festlegen
müssen. Genau das geschieht in meinem kleinen Beispielprogramm. Mit dem
Kommando
struct Auto x;
wird eine Variable x angelegt, die dem Strukturtyp Auto angehört. Nun hat
aber diese Variable drei Komponenten, und irgendwie muss ich auf diese drei
Komponenten zugreifen können. Das geschieht mit Hilfe des so genannten
Punktoperators. Er macht genau das, was sein Name sagt: um auf die Kom-
ponente marke der Variablen x Zugriff zu bekommen, schreibe ich einfach
x.marke und schon steht mir die Marke von x zur Verfügung. Sie müssen
also nur Variablenname.Komponentenname angeben, um mit einer einzelnen
Komponente einer Strukturvariablen arbeiten zu können.
Variablen eines Strukturtyps können Sie auch sofort nach der Definition
des Strukturtyps selbst anlegen. So würde zum Beispiel das Kommando
struct Auto
{
char marke[10];
char farbe[10];
2.4 Zeiger und dynamische Datenstrukturen 165

int hubraum;
} x, y;
klaglos zwei Variablen des Typs Auto liefern, die Sie dann über den Punkt-
operator wieder mit Daten für die einzelnen Komponenten beliefern könnten.
Ob das eine sinnvolle Konstruktion darstellt, ist eine ganz andere Frage. Ich
selbst ziehe es vor, erst einen Datentyp wie zum Beispiel Auto zu definie-
ren und mich anschließend in einem davon getrennten Arbeitsgang um die
zugehörigen Variablen zu kümmern.

Strukturtypen
Einen Strukturtyp definiert man durch
struct Typenname
{
Komponenten
}
Variablen eines Strukturtyps werden dann durch die Anweisung
struct Typenname Variablenname;
angelegt. Jede Variable eines Strukturtyps hat so viele Komponenten wie
der Strukturtyp angibt. Auf einzelne Komponenten einer solchen Varia-
ble kann man mit Hilfe der Punktnotation zugreifen, das heißt durch
Variablenname.Komponentenname.

So etwas wie Strukturtypen wird Ihnen im nächsten Kapitel, wenn es um


die objektorientierte Programmierung geht, in Gestalt der Klassen wieder be-
gegnen. Für den Moment brauche ich sie zu einem ganz anderen Zweck: ich will
jetzt daran gehen, eine verkettete Liste zu programmieren. Was das bedeuten
soll, hatte ich schon kurz erwähnt. Während ein Feld zwar mehrere Elemen-
te des gleichen Typs aufnehmen kann, aber die Anzahl der Elemente starr
festliegt, soll die verkettete Liste die Aufnahme einer beliebigen Anzahl von
Elementen erlauben, ohne dass ich mich vor dem eigentlichen Programmlauf
schon darauf festlegen muss, wie viele Elemente nun tatsächlich des Weges
kommen werden. Im Gegensatz zu der statischen Datenstruktur des Feldes
soll meine verkettete Liste also dynamisch sein, und das bedeutet, dass die
Länge der Liste sich während der Laufzeit des Programms beliebig verändern
kann. Das ist eine ausgesprochen praktische Sache, denn bei vielen Anwen-
dungen weiß man einfach vor dem Programmstart nicht, was und vor allem
wie viel einem so passieren kann im Laufe eines langen Tages; da kommt eine
dynamische Datenstruktur gerade recht.
Es wird Sie nicht überraschen, dass verkettete Listen ganz wesentlich auf
dem Einsatz von Zeigern beruhen, sonst hätte ich mich wohl nicht so lange
über Zeiger ausgelassen. Man muss nur die Zeiger kombinieren mit einem pas-
send gewählten Strukturtyp und schon ist der Grundbaustein der verkettenen
Liste erstellt. Will ich beispielsweise eine Liste aus ganzen Zahlen aufbauen,
so hilft mir dabei der folgende Strukturtyp knoten ganz ungemein.
166 2 Strukturierte Programmierung mit C

struct knoten
{
int wert;
struct knoten *nachfolger;
}
Was soll das nun bedeuten? Ein Element des Typs knoten besteht aus zwei
Komponenten: der int-Komponente wert, in der die ganze Zahl abgelegt wird,
um die es eigentlich geht, und einem Zeiger nachfolger, der auf eine weitere
Variable vom Typ knoten verweisen kann. Ein Knoten hat also einerseits einen
Wert, kann aber andererseits auch auf einen weiteren Knoten verweisen, und
genau das macht seinen besonderen Reiz aus. Denken Sie an die Schnitzeljagd,
die ich schon einmal als Beispiel für dynamische Datenstrukturen angeführt
habe. An jeder Station finden die Kinder irgend etwas Süßes, das entspricht
meiner int-Variablen wert. Und sie stoßen auch auf einen Hinweis, wo sie
die nächste Süßigkeit finden werden, und das entspricht meiner Zeigervaria-
blen nachfolger. Indem die zweite Komponente eines Kontenelementes auf
einen weiteren Knoten verweist, kann ich jeder Variablen vom Typ knoten
einen Nachfolgerknoten zuordnen, der dann wieder einen Nachfolgerknoten
bekommt, dem ich dann seinerseits einen Nachfolgerknoten spendiere - und
so weiter, solange ich Lust dazu habe. Dynamischer geht’s nicht mehr. Um
das noch etwas deutlicher zu machen, sehen Sie sich einmal Abbildung 2.24
an.

null
wert wert wert

Abb. 2.24. Vorwärts verkettete Liste

Sie sehen hier nichts anderes als eine kleine Liste aus drei Knoten. Der
erste, also der vorderste Knoten hat als erste Komponente einen Wert und
als zweite Komponente einen Zeiger auf einen weiteren Knoten, genau wie
ich es in meinem Strukturtyp definiert hatte. Beim zweiten Knoten sieht es
nicht anders aus, nur dass er mit seiner Zeigerkomponente auf den dritten
Knoten zeigt. Aber beim dritten Knoten habe ich ein kleines Problem: er
hat einen Wert, das ist nicht weiter schlimm, und er hat, da er nun mal ein
Knoten ist, auch einen Zeiger - aber wohin? Da meine Liste aus Abbildung
2.24 nur drei Elemente hat, gibt es nichts, worauf dieser Zeiger zeigen könnte.
Schadet nichts, die Entwickler von C haben für solche Fälle vorgesorgt. Wenn
es nichts gibt, worauf ein Zeiger sinnvollerweise verweisen kann, dann lässt
man ihn eben ins Nichts zeigen, und das programmtechnische Nichts ist der
2.4 Zeiger und dynamische Datenstrukturen 167

NULL-Zeiger. Wie man im Einzelnen mit ihm umgeht, werden Sie gleich im
nächsten Beispielprogramm sehen. Es baut eine verkettete Liste auf und gibt
die Elemente dieser Liste anschließend wieder am Bildschirm aus. Sie werden
beim Lesen des Programmtextes vermutlich nicht alles gleich verstehen; es
kann sogar passieren, dass Sie recht wenig verstehen, weil die Umsetzung des
Konzeptes der Liste in ein konkretes C-Programm doch etwas schwieriger ist
als das Aufzeichnen eines Bildes. Machen Sie sich nichts daraus, ich werde
gleich nach dem Programmtext erklären, wie das Programm funktioniert.
/* liste.c -- verkettete liste */

#include <stdio.h>
#include <stdlib.h>

main()
{
struct knoten
{
int wert;
struct knoten *nachfolger;
};

struct knoten *kopf, *laufknoten, *hilfe;


int i;
char antwort;
/* liste aufbauen */
printf("Erste Zahl eingeben\n");
scanf("%d",&i);
getchar();
laufknoten = (struct knoten *) malloc(sizeof(struct knoten));
(*laufknoten).wert = i;
(*laufknoten).nachfolger = NULL;
kopf = laufknoten;
printf("Weitere Werte eingeben?(j/n)\n");
scanf("%c",&antwort);
getchar();
while (antwort == ’j’ || antwort == ’J’)
{
printf("Neue Zahl eingeben\n");
scanf("%d",&i);
getchar();
hilfe = (struct knoten *) malloc(sizeof(struct knoten));
(*hilfe).wert = i;
(*hilfe).nachfolger = NULL;
(*laufknoten).nachfolger = hilfe;
laufknoten = hilfe;
168 2 Strukturierte Programmierung mit C

printf("Weitere Werte eingeben?(j/n)\n");


scanf("%c",&antwort);
getchar();
}
/* liste ausgeben */
laufknoten = kopf;
while(laufknoten != NULL)
{
printf("%d\n",(*laufknoten).wert);
laufknoten = (*laufknoten).nachfolger;
}
}
Das ist nun sicher kein ganz triviales Programm mehr, und ich will es
jetzt Schritt für Schritt mit Ihnen durchgehen. Am Anfang wird die übliche
Header-Datei stdio.h eingebunden, die Sie für die Ein- und Ausgabe brau-
chen. Danach habe ich allerdings noch die zusätzliche Header-Datei stdlib.h
bemüht, auf die ich hier nicht verzichten kann; den Grund werden Sie gleich
sehen. Im eigentlichen Programm wird dann zunächst der Strukturtyp knoten
definiert, womit ich Ihnen nichts Neues erzähle, und es werden drei Zeiger-
variablen auf Knoten angelegt. Bis hin zum getchar()-Aufruf ist dann alles
Standard, ich habe zwei weitere Variablen angelegt und dafür gesorgt, dass
die int-Variable i von der Tatstatur eingelesen wird. Erst jetzt kommt etwas
wirklich Neues in Gestalt der Zeile
laufknoten = (struct knoten *) malloc(sizeof(struct knoten));
Das sieht gar nicht schön aus, aber man muss sich nur daran gewöhnen. Die
Variable laufknoten ist ein Zeiger auf ein Element meines Datentyps knoten
und zeigt bisher einfach nur irgendwohin. Nun will ich aber konkrete Kno-
ten anlegen, die Liste soll ja mit Leben gefüllt werden, und deshalb muss der
Compiler wissen, wie groß der benötigte Speicher sein soll. Bei einm selbst-
definierten Datentyp wie beispielsweise meinem Strukturtyp knoten müssen
Sie das dem Compiler explizit mitteilen, sonst erweist er sich beim Reser-
vieren des Speichers als ziemlich hilflos. Die Reservierung von Speicherplatz
erfolgt mit dem malloc()-Kommando, das man üblicherweise in der schon
erwähnten Header-Datei stdlib.h findet, die ich aus eben diesem Grund
auch einbinden musste. Dabei steht malloc() übrigens für memory alloca-

tion“. Aber wenn ich schon Speicherplatz reservieren muss, dann sollte ich
auch wissen, wie viel Platz ich eigentlich brauche. Und woher soll ich das
wissen? Es handelt sich hier um einen Knoten, ich habe nicht die leiseste
Ahnung, wie viel Platz er für sich in Anspruch nimmt. Muss ich auch nicht
wissen, zu irgendetwas muss auch der C-Compiler gut sein. Die Größe einer
Datenstruktur liefert mir die sizeof()-Funktion, der ich nur den in Frage ste-
henden Datentyp mitteilen muss, damit sie mir seinen Speicherbedarf liefert.
Mit malloc(sizeof(struct knoten)) wird also genügend Speicherplatz zur
Verfügung gestellt, um einen Knoten aufzunehmen, und gleichzeitig gibt dieses
malloc()-Kommando an das Programm einen Zeiger auf eben diese Stelle im
2.4 Zeiger und dynamische Datenstrukturen 169

Speicher, die es gerade reserviert hat. Nun hat man es leider so eingerichtet,
dass dieser Zeiger eine Art typenloser Zeiger ist, der nicht so recht weiß, worauf
er eigentlich zeigt. Daher kann ich nicht einfach dem Zeiger laufknoten das
Ergebnis des malloc-Aufrufes zuweisen, denn laufknoten ist ein Zeiger, der
sehr genau weiß wozu er gehört: ich muss erst noch dem Compiler sagen, dass
der Zeiger auf die reservierte Speicherstelle zu einem Element meines Struktur-
typs knoten gehört. Das erledigt der Klammerausdruck (struct knoten *)
vor dem malloc()-Aufruf. Mit malloc(sizeof(struct knoten)) wird also
der nötige Speicherplatz angelegt und dem Programm ein Zeiger auf die pas-
sende Speicherstelle übermittelt, der allerdings noch etwas unspezifisch ist.
Erst durch den so genannten cast-Operator (struct knoten *) wird dann
klargestellt, dass es sich tatsächlich um einen Zeiger auf eine Speicherstelle
für meinen Strukturtyp knoten handelt.
Damit kann man übrigens auch das kleine Problem mit der Speicherzuwei-
sung lösen, auf das wir im Zusammenhang mit Zeigern gestoßen sind. Erinnern
Sie sich? Wenn Sie so etwas wie
float *p;
*p=1.234;
von Ihrem C-Compiler verlangen, dann kann es vorkommen, dass er Sie warnt,
weil Sie zwar eine Zeigervariable deklariert, aber noch keinen konkreten Spei-
cherplatz für die zugewiesene reelle Zahl reserviert haben. Jetzt wissen Sie,
wie man eine solche Reservierung vornimmt. Indem Sie
float *p;
p=(float *) malloc(sizeof(float));
*p=1.234;
schreiben, haben Sie den nötigen Speicherplatz reserviert, und die anschlie-
ßende Wertzuweisung geht ohne Gemäkel durch.
Das aber nur am Rande. Wie Sie sehen konnten, besteht der Sinn dieser
ganzen komplizierten malloc()-Zeile nur darin, dass laufknoten jetzt wirk-
lich und wahrhaftig auf eine real existierende Speicherstelle zeigt, in der ich
einen Knoten unterbringen kann. Mit *laufknoten kann ich somit auf den
Inhalt des Knotens zugreifen, und daher wird der aktuelle Knoten mit den
beiden Kommandos
(*laufknoten).wert = i;
und
(*laufknoten).nachfolger = NULL;
gefüllt. Der eigentliche Zahlenwert wird also von i in den Knoten übertra-
gen, und der Nachfolgeknoten ist das pure Nichts - was soll es auch sonst
sein, bisher habe ich ja nur einen einzigen Knoten gefüllt, und zwar den er-
sten Kniten meiner Liste. Da ich mir irgendiwe merken muss, wo meine Liste
anfängt, weise ich diesen ersten Knoten der Variablen kopf zu. Damit habe
ich zwar immer nur noch einen einzigen Knoten, aber auf ihn verweisen jetzt
zwei Zeiger, kopf und laufknoten.
Seine Einzigartigkeit wird sich aber gleich ändern, denn jetzt mache ich
mich daran, mit Hilfe einer Schleife die Liste um weitere Knoten zu ergänzen.
170 2 Strukturierte Programmierung mit C

Wann immer der Benutzer eine positive Antwort auf die Frage nach weite-
ren Eingaben gibt, wird ein neuer Knoten erzeugt. Zu diesem Zweck wird
mit dem gleichen Verfahren wie eben dem Knotenzeiger hilfe eine passen-
de Speicherstelle zugewiesen, seine Wertkomponente wird gefüllt, und da er
der neueste Knoten ist, wird sein Nachfolger wieder auf NULL gesetzt. Und
nun stellen Sie sich einmal vor, wir befinden uns im ersten Schleifendurch-
lauf. Der neue Knoten *hilfe wurde gerade gefüllt und soll natürlich den
Nachfolger meines alten ersten Knotens darstellen. Aber der erste Knoten
heißt *kopf bzw. *laufknoten, für den Moment ist das noch das Glei-
che. Also muss ich meinem Laufknoten sagen, dass der gerade neu erzeugte
Knoten sein Nachfolger sein soll, und genau das mache ich mit der Anwei-
sung (*laufknoten).nachfolger = hilfe. Hier passt alles zusammen. Die
nachfolger-Komponente meines Laufknotens ist ein Zeiger auf einen Knoten,
und deshalb kann ich ihr in Gestalt von hilfe einen solchen Zeiger zuweisen.
Jetzt ist der neu erstellte Knoten tatsächlich der Nachfolgeknoten meines er-
sten Knotens. Es werden aber vermutlich noch ein paar mehr Knoten dazu
kommen, weshalb ich dafür sorgen muss, dass der Laufknoten tatsächlich der
gerade aktuelle Knoten ist, der sozusagen durch das Programm läuft - daher
der eigenartige Name. Indem ich also das Kommando laufknoten = hilfe
absetze, wird der neu erstellte Knoten zum Laufknoten, und die Schleife kann
bei Bedarf von vorne anfangen.
Was passiert nun in einem eventuellen zweiten Schleifendurchgang? Wieder
wird ein neuer Knoten gebildet und mit Leben gefüllt. Die Laufknotenvariable
ist nun, verursacht durch die letzte Zuweisung des ersten Schleifendurchlaufs
ein Zeiger auf den zweiten Knoten, und damit wird dafür gesorgt, dass der
neue Knoten Nachfolger des zweiten Knotens, also der dritte Knoten ist. Und
wieder wird laufknoten = hilfe den neu erstellten Knoten zum Laufknoten
machen, wie sich das gehört.
Sie sehen, durch das Zusammenspiel von Laufknoten und Hilfsknoten wird
am Ende eines jeden Schleifendurchlaufs der Laufknoten den gerade aktu-
ellen Knoten bezeichnen, während der Knoten *kopf immer gleich bleibt
und den Anfang der Liste kennzeichnet. Das ist auch gut so, denn irgend-
einen Einstiegspunkt in meine verkettete Liste muss ich schließlich haben,
wenn ich jemals auf die Idee kommen sollte, meine Listenelemente auszu-
geben. Genau das geschieht in der zweiten Schleife. Durch die Zuweisung
laufknoten = kopf wird der Laufknoten auf den Listenanfang gesetzt, und
dann geht die Schleife so lange durch die Liste, bis der gerade ausgegebene
Knoten keinen Nachfolger mehr hat und somit die Liste ihr Ende erreicht
haben muss. Das Hochzählen zum nächsten Knoten geschieht mit der Anwei-
sung laufknoten = (*laufknoten).nachfolger, denn auf diese Weise wird
der Nachfolger des gerade aktuell gewesenen Knotens zum neuen aktuellen
Knoten gemacht.
Ein kleines Bild, ein etwas längeres Programm, und dann so viele Erklärun-
gen! Das Konzept der dynamischen Datenstrukturen im Allgemeinen und der
verketteten Liste im Besonderen ist nun mal gerade für einen Anfänger in
2.4 Zeiger und dynamische Datenstrukturen 171

der Programmierung recht ungewohnt, deshalb kann eine etwas ausführliche-


re Erläuterung hier wohl nicht schaden. Diese Art von Liste heißt übrigens
deshalb eine vorwärts verkettete Liste, weil man vom Listenkopf ausgeht und
dann nach vorne schreitet, indem man immer einen Nachfolger hinzu fügt. Es
gibt auch rückwärts verkettete und sogar doppelt verkettete Listen, aber man
muss es ja nicht übertreiben.

Vorwärts verkettete Listen


Eine vorwärts verkettete Liste wird mithilfe eines Strukturtyps und mithil-
fe von Zeigern auf diesen Strukturtyp realisiert. Den Zeigern muss durch die
malloc()-Anweisung explizit Speicherplatz zugewiesen werden. Jede Varia-
ble des Strukturtyps hat mindestens eine Komponente zur Aufnahme der
eigentlichen Datenelemente und eine Komponente, die einen Zeiger auf den
Strukturtyp selbst aufnimmt, der auf das nächste Element der Liste zeigt.
Man baut die Liste durch den Einsatz einer Schleife auf, wobei in jedem
Schleifendurchlauf ein neues Listenelement angelegt wird. Dabei muss das
Programm dafür sorgen, dass am Ende eines Durchlaufes das neu angeleg-
te Listenelement dem aktuell verwendeten Listenelement in der Schleife ent-
spricht. Auch die Listenausgabe erfolgt über eine Schleife, die beendet wird,
sobald das aktuelle Listenelement keinen Nachfolger mehr hat.

Sie können jetzt also eine Liste aufbauen und wieder ausgeben. War’s das
schon? Natürlich nicht. Zum guten Schluss dieses Abschnitts zeige ich Ihnen
noch, wie man einen Knoten in eine Liste einfügt und wie man einen aus der
Liste herauslöscht. Beginnen wir mit dem Löschen. Das Prinzip sehen Sie in
Abbildung 2.25. Ich gehe hier davon aus, dass der zweite Knoten aus der Liste
entfernt werden soll. Nichts könnte leichter sein als das: ich muss dann nur dem
ersten Knoten sagen, dass nicht mehr der zweite Knoten sein Nachfolger sein
soll, sondern gleich der dritte. Dann zeigt nämlich der Nachfolgerzeiger des
ersten Knotens direkt auf den dritten und der zweite ist nicht mehr erreichbar.
Er existiert zwar noch irgendwo im Arbeitsspeicher, aber es gibt keinen Zeiger
mehr, der auf ihn deuten würde, weshalb er für meine verkettete Liste schlicht
nicht mehr da ist.

null
wert wert wert

Abb. 2.25. Löschen aus einer Liste


172 2 Strukturierte Programmierung mit C

Das Prinzip ist also einfach, und die Realisierung in C ist auch nicht viel
schwerer, zumal Sie jetzt schon etwas Übung in der Behandlung verketteter
Listen haben.
/*Element loeschen*/
printf("Zu loeschenden Wert eingeben\n");
scanf("%d",&n);
laufknoten = kopf;
hilfe = (*laufknoten).nachfolger;
if ((*kopf).wert ==n)
{
free(kopf);
kopf = hilfe;
}
else
{
while((*hilfe).wert != n && (*hilfe).nachfolger != NULL)
{
laufknoten = hilfe;
hilfe = (*hilfe).nachfolger;
}
if ((*hilfe).wert == n)
{
(*laufknoten).nachfolger = (*hilfe).nachfolger;
free(hilfe);
}
else
printf("Element nicht vorhanden\n");
}
In diesem Programmstück soll ein Knoten aus der Liste gelöscht werden, des-
sen Wert n beträgt. Irgendwo vorher müssen Sie natürlich eine int-Variable n
definieren, das habe ich mir hier erspart. Nach dem Prinzip aus Abbildung 2.25
muss ich den Nachfolgerzeiger des Vorgängers des gesuchten Knotens auf den
Nachfolger eben dieses gesuchten Knotens umbiegen. Also ist es sinnvoll, sich
nicht nur einen aktuellen Knoten zu merken, sondern deren zwei - sonst habe
ich vielleicht irgendwann den Wert n zwar gefunden, aber seinen Vorgänger
verloren. Es gibt daher zwei laufende Knoten: den guten alten laufknoten
und einen weiteren Knoten namens hilfe. Am Anfang frage ich nach, ob der
gesuchte Wert schon im Startknoten zu finden ist. Sollte das der Fall sein,
dann muss ich nur den Kopf der Liste um einen Knoten verschieben, und
genau das leistet das Kommando kopf = hilfe. Damit der vom bisherigen
Kopf belegte Speicherplatz nicht sinnlos in der Gegend herum steht, gebe ich
diesen Platz aber vorher mit dem Kommando free(kopf) wieder frei. Sollte
der gesuchte Wert aber nicht gleich am Anfang der Liste zu finden sein, sieht
es etwas anders aus; in diesem Fall muss ich so lange durch die Liste wan-
dern, bis ich entweder den Wert gefunden oder das Ende der Liste erreicht
2.4 Zeiger und dynamische Datenstrukturen 173

habe. Das Durchwandern der Liste geschieht in der while-Schleife. Solange


der Wert des Hilfeknotens nicht dem gesuchten Wert entspricht und gleichzei-
tig der Hilfeknoten nicht schon der letzte Knoten ist, gehe ich zum nächsten
Knoten über. Bedenken Sie dabei, dass der Hilfeknoten immer der Nachfolger
des Laufknotens sein soll, und genau das erreiche ich im Verlauf der Schleife
dadurch, dass ich zuerst dem Laufknoten die Adresse seines Nachfolgers, des
Hilfeknotens, zuweise, und danach dem Hilfeknoten die Adresse seines eigenen
Nachfolgers. Damit sind beide Knoten einen Schritt nach vorne gegangen und
der Laufknoten ist nach wie vor der Vorgänger des Hilfeknotens.
Nun wird irgendwann die Schleife beendet sein. Das Schleifenende kann
erreicht sein, weil ich endlich den gesuchten Wert gefunden habe; dann muss
ich den entsprechenden Knoten löschen. Da der Hilfeknoten mein Orientie-
rungspunkt war, wird also der Vorgänger von hilfe - das war laufknoten -
einen neuen Nachfolger bekommen, nämlich den alten Nachfolger von hilfe.
Damit ist der Hilfeknoten selbst überhaupt nicht mehr in der Liste vorhan-
den, denn ich konnte ihn nur als den Nachfolger des Laufknotens erreichen,
und der zeigt jetzt auf einen anderen Nachfolgerknoten. Dass ich danach den
inzwischen sinnlos gewordenen Speicherplatz, auf den eben noch hilfe zeigte,
durch free(hilfe) freigebe, brauche ich kaum zu erwähnen. Es könnte aber
auch sein, dass ich das Schleifenende erreicht habe, weil ich die Liste erfolglos
durchlaufen habe. Dann bleibt mir nichts anderes übrig als meinen Misserfolg
einzugestehen und die Sache zu beenden.
Schon ist das Löschen aus der Liste erledigt. Aber Vorsicht: diese Art des
Löschens beseitigt nur den ersten Knoten, dessen Wert dem gesuchten Wert
entspricht; sollen alle Knotgen dieser Art entfernt werden, muss man etwas
genauer hinsehen. Das werden Sie als Übungsaufgabe erledigen.

null
wert wert wert

wert

Abb. 2.26. Einfügen in eine Liste

Kommen wir zum letzten Punkt, dem Einfügen eines Elementes in eine
vorhandene Liste. Die prinzipielle Vorgehensweise zeigt Abbildung 2.26. Zwi-
schen dem zweiten und dem dritten Knoten soll hier ein weiterer Knoten
174 2 Strukturierte Programmierung mit C

eingefügt werden, und Sie sehen schon, wie man das macht: ich muss nur den
neuen Knoten zum Nachfolger des zweiten Knotens erklären und dann den al-
ten dritten Knoten zum Nachfolger des neuen. Um das in einem C-Programm
zu realisieren, laufe ich so lange durch die Liste, bis ich die gewünschte Stelle
gefunden habe, nach der eingefügt werden soll, und biege dann wie in Ab-
bildung 2.26 die Zeiger um. Werfen wir also einen Blick auf das zugehörige
Programmstück.
/*Element einfuegen*/
printf("Nach welcher Stelle soll eingefuegt werden? \n");
scanf("%d",&n);
printf("Welcher Wert soll eingefuegt werden? \n");
scanf("%d",&m);
laufknoten = kopf;
while((*laufknoten).wert != n && (*laufknoten).nachfolger != NULL)
laufknoten = (*laufknoten).nachfolger;
if ((*laufknoten).wert == n)
{
hilfe = (struct knoten *) malloc(sizeof(struct knoten));
(*hilfe).wert = m;
(*hilfe).nachfolger = (*laufknoten).nachfolger;
(*laufknoten).nachfolger = hilfe;
}
else
printf("Kein passendes Element gefunden\n");
Nach dem Knoten mit dem Wert n soll ein Knoten mit dem Wert m ein-
gefügt werden. Zu diesem Zweck laufe ich durch die Liste und halte mit Hilfe
der while-Schleife nach einem passenden Knoten Ausschau. Ist er gefunden,
so wird ein neuer Knoten mit Speicherplatz versehen und wie gewünscht so-
wohl mit einem Wert als auch mit einem Nachfolger versehen: der Nachfolger
des neuen Knotens muss der bisherige Nachfolger des laufenden Knotens sein,
hinter dem der neue Knoten eingefügt werden soll. Und schließlich muss der
neue Knoten noch zum Nachfolger des Laufknotens erklärt werden, denn nach
dem Laufknoten sollte er ja schließlich stehen.

Löschen aus Listen und Einfügen in Listen


Man löscht ein Element aus einer verketteten Liste, indem man so lange durch
die Liste läuft, bis man das gesuchte Element gefunden hat, und dann den
Nachfolger des gesuchten Knotens zum Nachfolger des Vorgängers des gesuch-
ten Knotens macht. Dabei sollte man darauf achten, nicht mehr benötigten
Platz im Arbeitsspeicher mit Hilfe des free()-Kommandos wieder freizuge-
ben.
Man fügt ein Element in eine Liste ein, indem man so lange durch die
Liste läuft, bis man den Knoten gefunden hat, nach dem eingefügt werden
soll. Anschließend erzeugt man einen neuen Knoten, macht den Nachfolger
2.4 Zeiger und dynamische Datenstrukturen 175

des gefundenen Knotens zum Nachfolger des neuen Knotens und den neuen
Knoten selbst zum Nachfolger des gefundenen Knotens.

Jetzt haben wir die üblichen Methoden zum Umgang mit verketteten Li-
sten besprochen. Damit die einzelnen Programmstücke aber nicht so vereinzelt
in der Gegend herumstehen, zeige ich Ihnen einmal ein etwas größeres Pro-
gramm, das zeigt, wie man notfalls die Einzelteile zusammenfügen kann. Im
nächsten Abschnitt wird das dann noch einmal geschehen, nur etwas besser
mit Hilfe von Funktionen, aber für den Moment ist das folgende Programm
besser als gar nichts.
/* liste.c -- verkettete liste */

#include <stdio.h>
#include <stdlib.h>

main()
{
struct knoten
{
int wert;
struct knoten *nachfolger;
};

struct knoten *kopf, *laufknoten, *hilfe;


int aktion, i, n, m;
char antwort;
printf("Listenverarbeitung\n");
printf("==================\n");
do
{
printf("Wollen Sie \n");
printf(" - eine Liste aufbauen? --> 1 eingeben\n");
printf(" - die Liste ausgeben? --> 2 eingeben\n");
printf(" - aus der Liste loeschen? --> 3 eingeben\n");
printf(" - in die Liste einfuegen? --> 4 eingeben\n");
printf(" - aufhoeren? --> 5 eingeben\n");
scanf("%d",&aktion);
switch(aktion)
{
case 1:

/* liste aufbauen */
printf("Erste Zahl eingeben\n");
scanf("%d",&i);
getchar();
176 2 Strukturierte Programmierung mit C

laufknoten = (struct knoten *)


malloc(sizeof(struct knoten));
(*laufknoten).wert = i;
(*laufknoten).nachfolger = NULL;
kopf = laufknoten;
printf("Weitere Werte eingeben?(j/n)\n");
scanf("%c",&antwort);
getchar();
while (antwort == ’j’ || antwort == ’J’)
{
printf("Neue Zahl eingeben\n");
scanf("%d",&i);
getchar();
hilfe = (struct knoten *)
malloc(sizeof(struct knoten));
(*hilfe).wert = i;
(*hilfe).nachfolger = NULL;
(*laufknoten).nachfolger = hilfe;
laufknoten = hilfe;
printf("Weitere Werte eingeben?(j/n)\n");
scanf("%c",&antwort);
getchar();
}
break;

case 2:

/* liste ausgeben */
laufknoten = kopf;
while(laufknoten != NULL)
{
printf("%d\n",(*laufknoten).wert);
laufknoten = (*laufknoten).nachfolger;
}
break;

case 3:

/*loeschen*/
printf("Zu loeschenden Wert eingeben\n");
scanf("%d",&n);
laufknoten = kopf;
hilfe = (*laufknoten).nachfolger;
if ((*kopf).wert ==n)
kopf = hilfe;
2.4 Zeiger und dynamische Datenstrukturen 177

else
{
while((*hilfe).wert != n && (*hilfe).nachfolger != NULL)
{
laufknoten = hilfe;
hilfe = (*hilfe).nachfolger;
}
if ((*hilfe).wert == n)
(*laufknoten).nachfolger = (*hilfe).nachfolger;
else
printf("Element nicht vorhanden\n");
}
break;

case 4:

/*Element einfuegen*/
printf("Nach welcher Stelle soll eingefuegt werden? \n");
scanf("%d",&n);
printf("Welcher Wert soll eingefuegt werden? \n");
scanf("%d",&m);
laufknoten = kopf;
while((*laufknoten).wert != n &&
(*laufknoten).nachfolger != NULL)
laufknoten = (*laufknoten).nachfolger;
if ((*laufknoten).wert == n)
{
hilfe = (struct knoten *)
malloc(sizeof(struct knoten));
(*hilfe).wert = m;
(*hilfe).nachfolger = (*laufknoten).nachfolger;
(*laufknoten).nachfolger = hilfe;
}
else
printf("Kein passendes Element gefunden\n");
break;

default:

/*beenden*/
printf("Ende der Listenverarbeitung\n");
}
}
while(aktion ==1 || aktion ==2 || aktion == 3 || aktion == 4);
}
178 2 Strukturierte Programmierung mit C

Tut mir leid, das musste jetzt sein. Nichts in diesem Programm war neu,
alles hatten wir vorher besprochen. Es gibt zu Beginn eine Steuerung der wei-
teren Abläufe, bei der der Benutzer seinen aktuellen Wunsch mitteilt, und
danach wird so lange mit der Liste gearbeitet, bis er die Lust verliert und
sagt, dass er aufhören möchte. Die einzelnen Verfahren der Listenverarbei-
tung habe ich dabei den vorher besprochenen Programmstücken entnommen,
und die Gesamtsteuerung des Ablaufs beruht auf einer schlichten do-Schleife,
kombiniert mit switch() und case. Trotzdem sollte Sie vielleicht ein ungutes
Gefühl beschleichen. Muss man denn wirklich alles in einem großen Haufen von
Anweisungen unterbringen, die alle ohne Unterschied in der main()-Funktion
stehen? Nein, das muss man nicht, und wie man das etwas besser machen
kann, zeige ich Ihnen im nächsten Abschnitt.

Übungen

2.19. Schreiben Sie ein C-Programm, das Folgendes leistet. Es werden eine
int-Variable n und ein int-Zeiger p angelegt. Der Variablen n wird der Wert
17, dem Zeiger p wird die Adresse der Variablen n zugewiesen. Anschließend
wird der Zeiger einmal dekrementiert. Nun sollen der von p angezeigten Spei-
cherstelle und den nachfolgenden drei Speicherstellen die Zahlen 1, 2, 3 und 4
zugewiesen werden, was mit Hilfe einer Schleife und der Inkrementierung der
Zeigervariablen erledigt werden kann. Am Ende wird die Variable n ausgege-
ben. Welchen Wert hat jetzt n? Erklären Sie diesen Wert.

2.20. Schreiben Sie ein C-Programm, das eine Zeichenkette aus zehn Zeichen
von der Tastatur einliest, diese Zeichenkette wieder ausgibt und feststellt, ob
der Buchstabe a oder der Buchstabe e häufiger in der Zeichenkette vorkommt.
Dabei ist die Verarbeitung mit Zeigern durchzuführen.

2.21. Ändern Sie das Programm zur Verabeitung einer veketteten Liste so,
dass das Einfügen eines neuen Knotens nicht mehr nach einem zu suchenden
Knoten, sondern vor einem zu suchenden Knoten erfolgt.

2.22. Ändern Sie das Programm zur Verabeitung einer veketteten Liste so,
dass nicht mehr nur der erste Knoten, dessen Wert dem gesuchten Wert ent-
spricht, entfernt wird, sondern alle Knoten dieser Art entfernt werden.

2.23. Schreiben Sie ein C-Programm, das es erlaubt, eine Flugroute einzu-
geben und am Bildschirm wieder auszugeben. Die Länge der Flugroute soll
dabei nicht von vornherein bekannt sein, sodass der Benutzer nach jedem ein-
gegebenen Flughafen gefragt wird, ob er einen weiteren Flughafen eingeben
möchte, und bei einer positiven Antwort die Möglichkeit erhält, einen weiteren
Flughafen einzugeben. Nach der letzten Eingabe soll die gesamte Flugroute
am Bildschirm ausgegeben werden. Die Flugroute soll in Form einer verkette-
ten Liste programmiert werden. Die Flughäfen sind wie üblich als dreistellige
Kürzel wie FRA (für Frankfurt) oder MUN (für München) anzugeben.
2.5 Funktionen und Rekursion 179

2.24. Erweitern Sie das Programm aus Aufgabe 2.23 zu einer kompletten
Flugroutenverwaltung. Der Benutzer soll zum Programmstart aufgefordert
werden, eine Flugroute anzulegen, auszugeben, einen Flughafen in eine Route
einzufügen oder aus einer Route zu löschen. Je nach Benutzereingabe soll
die entsprechende Aktion erfolgen und dann nach einer neuen Aktion gefragt
werden.

2.5 Funktionen und Rekursion

Ich habe schon am Ende des letzten Abschnittes erähnt, dass es keine sehr
schlaue Idee ist, immer die gesamte Verarbeitung der main()-Funktion auf-
zubürden. Sie wird dadruch recht schnell unübersichtlich und schwer verständ-
lich, vor allem dann, wenn Sie auch noch gezwungen sind, eine etwas kompli-
ziertere Verarbeitung mit Hilfe von switch() und case zu steuern. In Wahr-
heit macht das natürlich kein Mensch, weder in der Programmierung noch
im richtigen Leben. Stellen Sie sich beispielsweise vor, Captain Jean Luc Pi-
card vom Föderationsraumschiff Enterprise würde ständig von einer Konsole
zur anderen hüpfen und alles selbst machen: den dabei entstehenden Schlin-
gerkurs der Enterprise würde ich gerne im nächsten Star-Treck-Film sehen,
ganz zu schweigen von eventuellen Problemen bei Treffen mit den Romula-
nern. Und wenn schon Captain Picard sich eher darauf beschränkt, Anwei-
sungen zu geben, die Detailarbeit der Besatzung zu überlassen und am Ende
die Ergebnisse der Mitarbeiter in Empfang zu nehmen, dann sollte sich auch
die main()-Funktion nicht lumpen lassen. Schließlich hat man sie main()-
Funktion genannt, weil sie die Hauptstelle des gesamten Programms sein soll,
also gewissermaßen der Captain, der die ganze Sache leitet, die Aufgaben ver-
teilt und den Überblick behält - nichts Schlimmeres als ein Chef, der sich in
jede Einzelheit einmischt.
Die Rolle des Captains soll also main() spielen. Und wer macht die Arbeit?
Kein Problem, dafür gibt es die anderen Funktionen, und wie man so eine
Funktion programmiert, werde ich Ihnen jetzt zeigen.

2.5.1 Funktionen

Algorithmen neigen schnell zur Unübersichtlichkeit. Sobald ein Algorithmus


bzw. das zugehörige C-Programm Gefahr läuft, zu unübersichtlich und un-
verständlich zu werden, sollte man den Algorithmus bzw. das Programm auf-
teilen in kleinere Teilalgorithmen, die nicht alle das ganze Problem lösen, um
das es in dem Programm geht, sondern nur jeweils ein kleines Teilproblem.
Man bemüht sich daher, einen großen dicken Algorithmus aufzuteilen in ei-
nige oder sogar viele kleine dünne Algorithmen, die leicht beherrschbar und
programmierbar sind, wobei der Hauptalgorithmus nichts weiter zu tun hat
als die Teilalgorithmen an die Arbeit zu schicken und dann ihre Ergebnisse
180 2 Strukturierte Programmierung mit C

entgegenzunehmen. Man spricht hier normalerweise nicht von Teilalgorith-


men, das Wort ist wohl zu sperrig, sondern von Modulen, und der Prozess der
Aufteilung eines großen Algorithmus in viele kleine Module wird als Modu-
larisierung bezeichnet. In einem modularisierten C-Programm stehen in der
main()-Funktion also nur noch die Aufrufe der einzelnen Module, in denen
die eigentliche Arbeit gemacht wird.
Es kann aber auch sein, dass eine Folge von Aktionen immer und immer
wieder im Verlauf eines Algorithmus durchgeführt werden muss, mal an die-
ser Stelle, mal an jener. Im Rahmen einer technischen Anwendung könnte
zum Beispiel hin und wieder das Bedürfnis bestehen, eine lineare oder gar
eine quadratische Gleichung zu lösen, und zwar an sehr verschiedenen Stellen
des Programms. Auch zu diesem Zweck sind offenbar Module gut einsetz-
bar: man programmiert einmal das Modul zur Lösung der einen oder anderen
Gleichungsart, und wann immer im Programm eine solche Gleichung gelöst
werden muss, ruft man nur noch das entsprechende Modul auf.
So oder so, um die Modularisierung kommt man nicht herum. In der Pro-
grammiersprache C werden Module durch so genannten Funktionen realisiert,
die Ihnen sogar schon mehrfach begegnet sind - schließlich habe ich schon oft
von der main()-Funktion gesprochen, aber auch scanf() oder printf() sind
nichts anderes als Funktionen. Klar, die kennen Sie schon, der C-Compiler
stellt sie zur Verfügung, und deshalb unterscheidet man auch zwischen vor-
definierten Funktionen, die Sie schlicht anwenden können, und den selbstde-
finierten Funktionen, die Sie dem Compiler erst einmal begreiflich machen
müssen. Und wie definiert man eine Funktion? Nichts könnte einfacher sein;
das nächste Programm, in dem ich wieder einmal die Summe der ersten so-
undsoviel natürlichen Zahlen ausrechne, zeigt Ihnen, wie es geht.
/* summe.c -- summenfunktion */

#include <stdio.h>

int summe(int m); /*Prototyp*/

main()
{
int n,s;
printf("n eingeben\n");
scanf("%d",&n);
s = summe(n);
printf("Die Summe der Zahlen von 1 bis %d lautet %d\n",n,s);
printf("Und von 1 bis 17 kommt %d heraus\n", summe(17));
}

int summe(int m) /*Definition der Funktion*/


{
int i,sum=0;
2.5 Funktionen und Rekursion 181

for(i=1;i<=m;i++)
sum = sum + i;
return sum;
}
Sehen wir uns zunächst das Hauptprogramm an, die main()-Funktion.
Dort wird eine ganzzahlige Variable n von der Tatstatur aus mit Leben gefüllt,
und dann wird dieses n in der Anweisung s=summe(n) eingesetzt. Das sollte Sie
an etwas erinnern, was Sie auf jeden Fall in der Schule gelernt haben: genau,
das sieht aus wie eine Funktion. Die Funktion selbst heißt summe, der Input
ist n, und was dabei herauskommt, wird der Variablen s zugewiesen. Haben
Sie sich nicht wenigstens ab und zu gefagt, wozu dieses eigenartige y = f (x),
mit dem man Sie in der Schule geplagt hat, gut sein soll? Spätestens jetzt
wissen Sie es, denn die Funktionen, die C so dringend verlangt, sind auch
nichts anderes als die in Ihrer Schulzeit gelernten Funktionen.
Damit der Aufruf s=summe(n) auch gut geht, müssen Sie dem Compiler
natürlich irgendwo klar machen, wie diese Summenfunktion funktioniert, wie
man also zu einem gegebenen Input den passenden Output berechnet. Das
pasiert nach Abschluss der main()-Funktion. In den bisherigen Programmen
war nach der letzten Klammer der main()-Funktion alles zu Ende und wir
konnten nach Hause gehen. Jetzt sieht das anders aus, denn die eigentliche
Definition der Funktionen, die innerhalb des Hauptprogramms main() ein-
gesetzt werden, erfolgt üblicherweise erst nach dem Programmtext für das
Hauptprogramm. Dort definiere ich die Funktion summe durch Angabe eines
Funktionskopfes und eines Funktionsrumpfes, ähnlich wie bei den Schleifen.
Im Funktionskopf werden drei Dinge festgelegt: wie heißt die Funktion, von
welcher Art müssen die Inputs der Funktion sein und welche Art von Out-
puts liefert sie? In diesem Beispiel heißt die Funktion summe, sie verlangt nach
einem ganzzahligen Input und liefert einen ganzzahligen Output. Auf diese
Weise kann man den Kopf jeder Funktion durch Angabe des Funktionsna-
mens und von Input- und Outputtypen festlegen. Denken Sie dabei daran,
nicht nur den Datentyp des Inputs mitzugeben, sondern auch den Input selbst
mit einem konkreten Namen zu versehen; in meinem Bespiel habe ich den
Input als m bezeichnet.
Mit diesem m wird dann im Funktionsrumpf gearbeitet, denn ich will ja
eine bestimmte Summe ausrechnen. Die eigentliche Summenberechnung er-
folgt genauso, wie ich es Ihnen schon im Zusammenhang mit Schleifen gezeigt
habe, darauf muss ich hier nicht mehr näher eingehen. Dass zur Berechnung
der Summe noch zwei Variablen i und sum benötigt werden, ist weder zu
vermeiden noch schlimm, aber achten Sie darauf, wo diese Variablen definiert
werden: die Definition von i und sum erfolgt im Funktionsrumpf, und deshalb
kennt das Programm sie auch nur so lange, wie es sich in der Bearbeitung
der Funktion aufhält. Außerhalb dieser Funktion hat kein Mensch jemals et-
was von i oder sum gehört, sie existieren nur innerhalb des Funktionsrumpfes.
Deshalb bezeichnet man solche Variablen, die nur innerhalb eines bestimmten
Teils eines Programms existieren, gerne als lokale Variablen. Dagegen sind vor
182 2 Strukturierte Programmierung mit C

dem Hauptprogramm main() definierte Variablen immer und überall gültig,


auch in jeder anderen Funktion des Programms, weshalb man sie auch als
globale Variablen bezeichnet.
Mithilfe meiner beiden lokalen Variablen i und sum wird also die gewünsch-
te Summe innerhalb der Funktion ausgerechnet. Nun mag es ja schön sein für
die Funktion, das Ergebnis der Summenberechnung zu kennen, aber eigentlich
wollte ich die Ergebnisse auch sehen, und ich bekomme meine Informationen
in aller Regel vom Hauptprogramm main(). Daher muss die Funktion ihre
Ergebnisse irgendwie an die aufrufende Stelle, in diesem Fall an das Haupt-
programm, zurückgeben, ganz wie Sie beim Tennis einen zu Ihnen geschlagenen
Ball wieder zum Aufschlagenden zurückspielen. Und wie nennen das die Ten-
nisspieler? Sie sprechen von einem Return“, der Ihnen in meinem kleinen

Beispielprogramm ebenfalls begegnet. Mit dem Kommando return sum wird
der Wert der Variablen sum an die aufrufende Stelle zurückgegeben, und das
heißt, dass der Funktionsaufruf summe(n) als Ergebnis genau das erhält, was
am Ende der Funktionsbearbeitung in der Variablen sum steht.
Aber gibt es da nicht noch ein kleines Problem? Immerhin hatte ich im
Hauptprogramm die Anweisung s=summe(n) abgesetzt, und in der Funktion
wird gar nicht die Summe der natürlichen Zahlen bis n berechnet, sondern
bis m. Das sieht aus wie ein Widerspruch, ist aber gar keiner und funktioniert
wie in der guten alten Schulmathematik. Die Funktionsdefinition y = 2x + 3
sagt zum Beispiel nur aus, dass zu jedem x, das Ihnen über den Weg laufen
könnte, der Wert 2x + 3 ausgerechnet werden und dass das Ergebnis dann
y heißen soll. Die Definition sagt nichts darüber aus, welches konkrete x ich
eine halbe Stunde später bei der Hand nehme und in die Formel der Funk-
tionsdefinition einsetze, zum Beispiel y(2) = 7 oder y(5) = 13. Der Input in
der Funktionsdefinition ist also nur ein abstrakter Input, der noch nicht mit
einem konkreten Wert belegt ist; wirklich gerechnet wird erst, wenn ich einen
wirklichen Inputwert zur Verfügung habe.
Und genauso läuft es auch bei den Funktionen in der C-Programmierung.
Man spricht hier gar nicht erst von Variablen, um keine Missversändnisse
aufkommen zu lassen, sondern von Parametern. Mein m ist also ein Input-
parameter, noch keine mit Leben gefüllte Variable, sondern eine abstrakte
Inputgröße, nur dazu geschaffen, den Rechenweg zu beschreiben. Es spielt al-
so die Rolle des x in der obigen Funktionsdefinition y = 2x + 3. Da es sich
somit um eine rein formale Größe handelt, nennt man diesen Inputparameter
meistens Formalparameter. Aber rein formale Größen sollte man irgendwann
konkretisieren, und das geschieht durch den konkreten Aufruf der Funktion,
in meinem Beispiel durch summe(n) oder durch summe(17). Durch so einen
Aufruf werden der Funktion so genannte Aktualparameter übergeben, die der
Funktion mitteilen, mit welchen konkreten Werten sie jetzt rechnen soll. Beim
Aufruf der Funktion werden dann die Formalparameter durch die Werte der
angegebenen Aktualparameter ersetzt. Sollen Sie also summe(n) berechnen
und steht n auf dem Wert 12, so wird bei der Abarbeitung der Funktion nicht
mehr mit einem abstrakten m, sondern mit der konkreten Zahl 12 gerechnet.
2.5 Funktionen und Rekursion 183

Und genauso bei dem Aufruf summe(17); hier wird im Verlauf der Funktion
die Zahl 17 eingesetzt, wo in der Funktionsdefinition der Parameter m stand.
Formalparameter werden also in der Beschreibung der Funktion gebraucht,
Aktualparameter dagegen sind die Parameter, die ich bei einem konkreten
Aufruf der Funktion als echten Input mitgebe. Das hat Konsequenzen. Mein
Formalparameter m ist nur innerhalb der Funktion bekannt, denn nur dort
kann ich mit ihm umgehen. Würden Sie jetzt außerhalb der Funktion summe()
mit diesem Parameter hantieren wollen, dann würde Ihnen der Compiler mit
einigem Unverständnis begegnen, denn außerhalb der Funktion kennt er dieses
m nicht. Ein Grund zur Vorsicht, aber auch zur Hoffnung. Denn gerade weil
der Formalparameter außerhalb der Funktion eine völlig unbekannte Größe
ist, dürfen Sie auch anderswo eine gleichlautende Variable anlegen: die Haupt-
funktion main() kennt keinen Parameter m und hat noch nie etwas von ihm
gehört. Sie dürfen deshalb im Hauptprogramm jederzeit eine gleichnamige
Variable definieren; da man den Parameter im Hauptprogramm nicht kennt,
kann das nicht zu Konflikten führen.
So viel zu den Parametern. Ein Wort noch zum return-Kommando, mit
dem Sie das Ergebnis der Funktion an die aufrufende Stelle zurückgeben.
In meinem Beispielprogramm habe ich eine ordentliche Variable zurückge-
geben, aber so einfach muss man es dem Compiler gar nicht machen. Nach
dem Schlüsselwort return kann ein beliebiger Ausdruck stehen wie zum Bei-
spiel return a*b oder return x+y; in diesem Fall wird das Ergebnis dieses
Ausdrucks der aufrufenden Stelle als Resultat geliefert. Angenehm wäre es
allerdings, wenn das Ergebnis, das Sie ausrechnen, zum angegebenen Rückga-
betyp passt. Packen Sie gar kein return oder ein return ohne nachfolgenden
Ausdruck in Ihre Funktion, so wird die Funktion ohne Ergebnisrückmeldung
beendet; man kann nicht alles haben.
Über eine Kleinigkeit habe ich noch nicht gesprochen. Warum beginnt
denn mein Programm mit der Ankündigung
int summe(int m); /*Prototyp*/
die einfach nur so herum steht und die später aufgeschriebene Funktion vorweg
nimmt? Ganz einfach. In der Hauptfunktion main() wird die selbstdefinierte
Funktion summe() aufgerufen, ohne dass das Programm schon wissen könnte,
worum es sich bei der Summenfunktion handelt: die wird ja erst anschließend
definiert. Es könnte den Compiler in Schwierigkeiten bringen, mit etwas han-
tieren zu müssen, was erst später definiert wird, und um ihm diese Probleme
zu ersparen, setzt man vor die Hauptfunktion für jede selbstdefinierte Funk-
tion einen Prototypen. Die Funktion muss vor ihrem ersten Aufruf zumindest
im Prinzip dem Compiler bekannt sein, was man durch die Angabe des Proto-
typen auch leicht bewerkstelligen kann. Die Prototypangabe für meine Sum-
menfunktion teilt dem Compiler nur mit, dass gleich eine Funktion mit dem
Namen summe() vorkommen wird, die als Input eine ganze Zahl erwartet und
als Output wieder eine ganze Zahl liefert; mehr muss der Compiler am Anfang
gar nicht wissen. Natürlich müssen die Angaben im Prototyp mit der eigent-
lichen Funktionsdefinition übereinstimmen, sonst gibt es Ärger. Hier haben
184 2 Strukturierte Programmierung mit C

Sie aber ein wenig künstlerische Freiheit: die nötige Übereinstimmung bezieht
sich auf die Input- und Outputtypen, nicht auf irgendwelche Variablennamen;
die kann man sogar weglassen. Sie hätten also als Prototyp genauso gut
int summe(int k); /*Prototyp*/
oder gar
int summe(int); /*Prototyp*/
schreiben können, beides geht problemlos durch.
Nach so vielen Erklärungen erst einmal wieder ein kleines Beispielpro-
gramm. Sicher kann eine kleine Funktion zum Potenzieren nicht schaden. Das
Programm sieht dann folgendermaßen aus.
#include <stdio.h>
#include <stdlib.h>

float potenz(float x, int m); /*Prototyp*/

main()
{
int n;
float a;
printf("Basis eingeben\n");
scanf("%f",&a);
printf("Exponent eingeben\n");
scanf("%d",&n);
printf("%f hoch %d = %f\n",a,n,potenz(a,n));
}

float potenz(float x, int m) /*Definition der Funktion*/


{
int i;
float p=1;
for(i=1;i<=m;i++)
p = p*x;
return p;
}
Dazu muss ich wohl nichts Nennenswertes mehr erklären, das Programm hat
den gleichen prinzipiellen Aufbau wie das Summenprogramm, nur dass Sie
hier zwei Formalparameter in der Funktion sehen statt nur einen wie bis-
her. Das war Absicht. Eine Funktion kann beliebige viele Formalparameter
mit sich herum schleppen; solange Sie sie im Funktionsaufruf durch geeignete
Aktualparameter mit Leben füllen, spricht dagegen gar nichts.
Auf die Berechnung von Potenzen komme ich gleich noch einmal zurück.
Vorher möchte ich noch ein Wort zu den verwendeten Datentypen sagen. Die
üblichen Datentypen hatten wir ja am Anfang des Kapitels besprochen, und
natürlich kann man sie alle in jeder beliebigen Funktion verwenden. Aber was
macht man, wenn man gar keine Daten zurückgeben will? Auch dafür wurde
2.5 Funktionen und Rekursion 185

gesorgt. Soll eine Funktion keine Rückgabewerte liefern, so können Sie leich-
ten Herzens den Rückgabetyp void verwenden, der nur aussagt, dass nichts
zurückgegeben werden soll. Auch dazu wieder ein Beispiel.
void linie(int n)
{
int i;
for (i=0; i<n; i++)
printf("_");
printf("\n");
}
Diese Funkion macht nichts anderes als eine Linie auf dem Bildschirm aus-
zugeben, und wenn Sie sie aus dem Hauptprogramm heraus mit linie(17)
aufrufen, dann wird sie eine Linie ausgeben, die aus 17 kleinen Unterstrichen
bestehet. Ein Rückgabetyp ist hier offenbar nicht nötig, es wird kein Wert
bestimmt, den das Hauptprogramm gern zur Verfügung hätte. Deshalb hat
die Funktion auch den Rückgabetyp void, der aussagt, dass sie keine Wer-
te an die aufrufende Stelle zurückgibt. Aber auch um eine nicht vorhandene
Eingabe zu kennzeichnen, können Sie void verwenden. Meine Linienfunktion
könnte ja beispielsweise immer die Aufgabe haben, eine Linie aus konstant 17
Unterstrichen zu zeichnen, was den Formalparameter n völlig sinnlos werden
ließe. In diesem Fall definiert man die Funktion einfach als
void linie(void)
und legt in der Funktionsdefinition die Länge der Linie ein- für allemal fest.
Der Aufruf erfolgt dann durch linie().
Die Angabe von void ist übrigens nicht gleichbedeutend mit dem schlich-
ten Weglassen eines Ausgabetyps: das führt nämlich standardmäßig dazu, dass
der Compiler Ihrer Funktion den Ausgabetyp int zuordnet. Bei der Sum-
menfunktion hätte ich also den Ausgabetyp auch weglassen können, aber zu
empfehlen ist so etwas nicht, da es die Lesbarkeit des Programms nicht ge-
rade steigert. Anders sieht es aus bei den Eingabetypen. Sie haben es schon
bei der Definition der main()-Funktion gesehen, dass man hier einfach nichts
zwischen die Klammern schreiben muss, damit der Compiler auch nichts er-
wartet. Alternativ könnten Sie aber auch main(void) schreiben, das würde
auf das Gleiche hinauslaufen.
Sie haben lange kein Struktogramm mehr gesehen, es wird mal wieder
Zeit. Auch der Aufruf einer Funktion kann in einem Struktogramm dargestellt
werden, wie Sie in Abbildung 2.27 sehen.
Natürlich müssen Sie dann dafür sorgen, dass Ihre Summenfunktion in
Form eines weiteren Struktogramms vorhanden ist, sonst läuft der Aufruf der
Funktion ins Leere.

Selbstdefinierte Funktionen
Eine selbstdefinierte Funktion besteht aus dem Funktionskopf und dem Funk-
tionsrumpf. Im Funktionskopf werden die Inputparameter - die so genannten
Formalparameter - und die Outputparameter einschließlich der zugehörigen
186 2 Strukturierte Programmierung mit C

Abb. 2.27. Funktionsaufruf

Datentypen festgelegt. Im Funktionsrumpf wird die eigentliche Funktionsde-


finition, also die Definition der vorzunehmenden Verarbeitungsschritte vorge-
nommen. Die Funktion kann über das return-Kommando Ergebnisse an die
aufrufende Stelle zurückgeben. Weder die Formalparameter noch die lokalen
Variablen der Funktion sind dem aufrufenden Block bekannt.
Um anzugeben, dass eine Funktion keine Ausgabe liefert, verwendet man
den Datentyp void. Er kommt auch zum Einsatz, wenn eine Funktion keine
Inputparameter hat.

2.5.2 Vordefinierte Funktionen

Auch vordefinierte Funktionen musste irgendwann jemand programmieren;


vom Himmel fallen sie nicht. Der Vorteil ist nur der, dass sie bereits existie-
ren und durch Einbindung der passenden Header-Datei sofort zur Verfügung
stehen, ohne dass Sie sich noch über ihre Programmierung Gedanken machen
müssen. Beispiele solcher vordefinierten Funktionen haben Sie mit scanf()
oder printf() schon gesehen. Von besonderer Bedeutung sind noch die ma-
thematischen Standardfunktionen, ohne die Sie oft vor großen Problemen ste-
hen würden: stellen Sie sich nur vor, Sie müssten selbst die Berechnung von
sin x oder von ex programmieren. Da ist es doch angenehmer, auf die vor-
handenen Funktionen zurückgreifen zu können. Sobald Sie die Header-Datei
math.h eingebunden haben, lassen sich eine Unmenge mathematischer Funk-
tionen problemlos verwenden, und um welche es sich handelt, habe ich in der
folgenden Tabelle aufgelistet.
2.5 Funktionen und Rekursion 187

Funktionsprototyp Mathematische Funktion


double acos(double) arccos x
double asin(double) arcsin x
double atan(double) arctan x
double ceil(double) Aufrunden
double cos(double) cos x
double cosh(double) cosh x
double exp(double) ex
double fabs(double) |x|
double floor(double) Abrunden
double log(double) ln x
double log10(double) log10 x
double pow(double,double) xy
double sin(double) sin x
double sinh(double) sinh
√ x
double sqrt(double) x
double tan(double) tan x
double tanh(double) tanh x

Damit keine Unklarheiten aufkommen können, sollte ich noch sagen, dass der
Aufruf pow(x,y) den Wert xy ausrechnet und nicht umgekehrt. Und wenn
Sie den Absolutbetrag einer ganzzahligen Variablen i ausrechnen wollen, dann
geht das mit dem Aufruf abs(i). Dazu sollten Sie allerdings die Header-Datei
stdlib.h einbinden, denn abs() steht nicht in math.h.

Vordefinierte Funktionen
Zum Einsatz vordefinierter Funktionen ist die Einbindung der passenden
Header-Dateien nötig. Die meisten mathematischen Funktionen benötigen die
Header-Datei math.h, einige Funktionen finden sich auch in der stdlib.h.

2.5.3 call by value und call by reference

Auch wenn es einiges an Übereinstimmung zwischen den C-Funktionen und


den altbekannten mathematischen Funktionen gibt, so sind sie doch nicht
völlig identisch. Das merkt man ganz schnell, wenn es um die Änderung des
Wertes eines Aktualparameters geht, den Sie mit Hilfe einer Funktion irgend-
wie verändern wollen. Was wird Ihrer Meinung nach in dem folgenden Pro-
gramm passieren?
#include <stdio.h>

void aendern(int);

main()
{
int n=17;
188 2 Strukturierte Programmierung mit C

aendern(n);
printf("%d\n",n);
}

void aendern(int m)
{
m = 2*m;
}
Man sollte doch denken, dass der Aktualparameter n auf den Formalpara-
meter m geladen und dann verdoppelt wird, sodass als Ergebnis der Geschichte
am Bildschirm die stolze Zahl 34 erscheint. Stimmt aber nicht, der Bildschirm
zeigt Ihnen nur eine 17, geändert hat sich an n gar nichts. Wie kann das sein?
Das liegt an der Art, wie in C die Aktualparameter an die Formalparameter
übergeben werden, und wenn man das weiß, geht auch nichts mehr schief.
Bei dem Aufruf aendern(n) übergibt das Hauptprogramm an die Funktion
keineswegs n selbst, sodass also die Funktion aendern() auf die Speicherstelle
zugreifen könnte, die n aufgenommen hat. Weit gefehlt, die Funktion erhält
nur eine lokale Kopie von n, die zwar den gleichen Wert hat, aber weiß der
Himmel wo im Speicher steht. Es wird also nicht mit n selbst gearbeitet, son-
dern nur mit dem Wert von n, und die Adresse der Variablen n wird nicht
angegriffen. Deshalb nennt man dieses Verfahren der Parameterübergabe auch
call by value: die Funktion hat nur Zugriff auf den Wert des Aktualparame-
ters, nicht auf die Speicherstelle, in der der Aktualparameter steht. Also wird
zwar brav der Wert 17 innerhalb der Funktion verdoppelt und auf die Varia-
ble m geschrieben, aber meine ursprüngliche Variable n interessiert das kein
bisschen, da sie an einer völlig anderen Speicherstelle steht.

call by value
Übergibt man einer Funktion einen Eingabeparameter, so wird in der Funk-
tion eine lokale Kopie des Parameters angelegt, der dann der Wert des Ein-
gabeparameters zugewiesen wird. Änderungen, die innerhalb der Funktion an
einem Formalparameter vorgenommen werden, sind deshalb nach der Rück-
kehr zur aufrufenden Stelle wieder hinfällig. Man nennt diese Methode des
Funktionsaufrufs call by value.

Das ist schlecht, aber es ist nun mal so. Von Haus aus kennt C eben nur
den call by value. Trotzdem ist es möglich, auch mit den echten Variablen zu
arbeiten, wenn man seine Zuflucht bei den Adressen und Zeigern sucht, die wir
im letzten Abschnitt besprochen hatten. Sehen sie sich einmal die geänderte
Variante des Änderungsprogramms an.
#include <stdio.h>

void aendern(int *);

main()
2.5 Funktionen und Rekursion 189

{
int n=17;
aendern(&n);
printf("%d\n",n);
}

void aendern(int *m)


{
*m = *m * 2;
}
Hier wird nicht mehr der Wert von n selbst übergeben, sondern die Adres-
se von n, und das macht einen gewaltigen Unterschied. Natürlich wird beim
Aufruf der Funktion wie immer eine lokale Kopie des übergebenen Aktual-
parameters angelegt. Das heißt aber jetzt, dass die Adresse von n an irgend-
einer Speicherstelle abgelegt wird, die mich eigentlich gar nicht interessieren
muss. Meine Funktion erhält als Parameter einen Zeiger auf eine bestimmte
Speicherstelle, aber der Inhalt der Speicherstelle ist diesmal nicht selbst der
übergebene Parameter. In m befindet sich die Speicheradresse der Zahl, die Sie
vorher in der Variablen n abgelegt hatten. Dass Veränderungen an m selbst we-
gen des Prinzips call by value“ nicht an die aufrufende Stelle zurückgegeben

werden können, interessiert jetzt überhaupt nicht mehr, denn ich nehme eine
Änderung vor an der Speicherstelle, auf die m zeigt - und diese Speicherstelle
war eben kein Parameter. Um also an einem übergebenen Parameter etwas
ändern zu können, was dann auch an der aufrufenden Stelle seine Wirkung
zeigt, darf man nicht nur eine Variable selbst übergeben, sondern muss die
Adresse der Variablen an die Funktion übergeben, die dann selbst natürlich
einen oder mehrere Zeiger der passenden Art als Formalparameter aufweisen
muss.
Man nennt das Verfahren, Adressen als Parameter zu übergeben, call by
reference, also Aufruf durch Referenz. Er erlaubt es, in einer Funktion Ände-
rungen an den Eingabeparametern vorzunehmen, die dann nach der Rückkehr
zur aufrufenden Stelle auch beibehalten werden. Diese Methode ist besonders
beliebt, wenn man zwei Variablen vertauschen muss. Vor allem bei der Anwen-
dung von Sortierverfahren kommt es immer wieder vor, dass der Wert einer
Variablen einer anderen Variablen zugewiesen werden muss und umgekehrt.
Das funktioniert so wie im nächsten Programm.
#include <stdio.h>

void tauschen(int*, int*);

main()
{
int n=17, k=19;
tauschen(&n,&k);
printf("n=%d, k=%d\n",n,k);
190 2 Strukturierte Programmierung mit C

void tauschen(int *m,int *j)


{
int hilfe;
hilfe = *m;
*m = *j;
*j = hilfe;
}
Auch hier wird an den Inputparametern rein gar nichts geändert, denn
übergeben habe ich die Zeiger auf n und auf k, die nach wie vor auf die glei-
chen Speicherstellen zeigen. Nur stehen in diesen Speicherstellen jetzt eben
andere Werte als vorher. Beachten Sie übrigens, wie ich vertauscht habe. Man
findet oft die Meinung, dass zum Vertauschen auch die beiden Zeilen
*m = *j;
*j = *m;
reichen würden, aber das ist leider ein Irrtum. Auf *m würden Sie damit
zwar den Wert von *j schreiben, aber damit haben Sie den alten Wert von
*m komplett verloren und würden mit der nächsten Anweisung nur *j durch
sich selbst überschreiben. Deshalb habe ich eine ganzzahlige Hilfsvariable ein-
geführt, die den Wert von *m rettet und dann diesen Wert der Speicherstelle
zuweist, auf die mein Zeiger j weist. Auf diese Weise zeigen zwar die Zeiger
noch immer auf die gleichen Stellen im Speicher, aber die Werte, die an diesen
Stellen stehen, wurden gegeneinander ausgetauscht.

call by reference
Übergibt man einer Funktion die Adresse eines Parameter, so wird in der
Funktion eine lokale Kopie dieser Adressvariablen angelegt. An der Speicher-
stelle, auf die diese Adressvariable zeigt, können dann Änderungen innerhalb
der Funktion vorgenommen werden, die nach der Rückkehr zur aufrufenden
Stelle gültig bleiben. Man nennt diese Methode des Funktionsaufrufs call by
reference.

2.5.4 Rekursion

Es war der bekannte Freiherr von Münchhausen, der den erfolgreichen Versuch
unternahm, sich selbst an seinem eigenen Schopf aus dem Sumpf zu ziehen.
Ihm ist das damals erstaunlicherweise gelungen - zumindest hat er das be-
hauptet - , nur hat es ihm seither niemand nachmachen können. Zumindest
nicht im richtigen Leben. In der Programmierung gibt es nämlich etwas ganz
Ähnliches in Gestalt der Rekursion.
Die Idee ist einfach. Manche Funktionen lassen sich so schreiben, dass man
sie berechnen kann, indem man auf frühere Berechnungsergebnisse zurück-
greift. Denken Sie einmal kurz zurück an die Summenfunktion, die man ma-
thematisch schreiben kann als summe(n) = 1 + 2 + · · · + n. Nun ist aber
2.5 Funktionen und Rekursion 191

summe(2) = 1 + 2 = summe(1) + 2, summe(3) = (1 + 2) + 3 = summe(2) + 3,


summe(4) = (1 + 2 + 3) + 4 = summe(3) + 4, und offenbar gilt allgemein:

summe(n) = (1 + 2 + · · · + (n − 1)) + n = summe(n − 1) + n.

Ich kann also die Summenfunktion mit dem Eingabeparameter n zurückführen


auf die Summenfunktion mit dem Eingabeparameter n − 1, indem ich auf
summe(n − 1) noch den Wert n addiere.
Matheamtisch ist das alles kein Problem, aber was nützt es mir für die Pro-
grammierung? Ziemlich viel. Genau das gleiche Prinzip können Sie nämlich
auf die konkrete Programmierung von Funktionen in C anwenden. im nächsten
Beispielprogramm sehen Sie, wie man das macht.
int summe(int);

main()
{
int n=5,s;
s=summe(n);
printf("Die gesuchte Summe lautet %d\n",s);
}
int summe(int m)
{
if (m<1)
return 0;
else if (m==1)
return 1;
else
return m + summe(m-1);
}
Das Programm soll nichts weiter tun als die Summe der ersten fünf natürlichen
Zahlen auszurechnen. Zu diesem Zweck definiere ich wieder eine Summenfunk-
tion, die jetzt allerdings etwas anders aussieht als vorher. Zunächst wird in
der Funktion abgeprüft, ob m kleiner als 1 ist - in diesem Fall wird das Er-
gebnis 0 zurückgegeben - oder ob m gleich 1 ist - in diesem Fall wird mir das
korrekte Ergebnis 1 geliefert. Und falls m größer als 1 ist, wird die Summe für
m berechnet, indem ich auf die Summe für m-1 einfach noch m addiere. Das ist
genau wie ich es oben gezeigt habe; man kann die Summe der Zahlen bis m
zurückführen auf die Summe der Zahlen bis m-1, sofern man auf diese kleinere
Summe noch m addiert.
So etwas funktioniert, eine Funktion kann sich selbst aufrufen. Man nennt
das eine rekursive Funktion und bringt damit nur zum Ausdruck, dass sich
die Funktion selbst aufruft. Aber führt das nicht in Schwierigkeiten, muss
man nicht damit rechnen, dass die Funktion sich immer und immer wieder
selbst aufruft bis in alle Ewigkeit? Das kann passieren, wenn man nicht auf-
passt. Aber in meiner Summenfunktion habe ich natürlich aufgepasst, und
192 2 Strukturierte Programmierung mit C

ich zeige Ihnen jetzt an einem konkreten Funktionsaufruf, worauf man ach-
ten muss. Gehen wir einmal davon aus, dass ein Hauptprogramm den Aufruf
summe(3) tätigt. Dann wird also summe() mit dem Inputparameter 3 auf-
gerufen und es wird m=3 gesetzt. Natürlich gilt dann weder m<1 noch m==1,
also versucht die Funktion, den Rückgabewert auszurechnen, indem sie auf m
selbst den Wert summe(m-1) - also summe(2) - addiert. So einfach geht das
aber nicht, denn summe() ist nun mal eine Funktion, weshalb man summe(2)
nur durch einen entsprechenden Funktionsaufruf erhalten kann. Daher bleibt
meine zuerst aufgerufene Funktion summe(3) erst einmal stehen, wo sie ist,
ruft ihrerseits summe(2) auf und wartet auf das Ergebnis.
Jetzt startet also erneut die Summenfunktion, nur eben mit dem Inputpa-
rameter 2. Beachten Sie, dass summe(3) noch längst nicht beendet ist, sondern
nur dumm herumsteht und wartet, bis summe(2) ein Ergebnis liefert. Die neu
aufgerufene Funktion summe(2) wird also nicht vom Hauptprogramm aufgeru-
fen, sondern von der vorher aufgerufenen Funktion summe(3). Und was macht
jetzt summe(2)? Genau das, was die Funktionsdefinition vorsieht. Der Formal-
parameter m ist jetzt mit dem Wert 2 belegt, der weder kleiner noch gleich
1 ist. Also versucht summe(2) seine Arbeit zu tun und ein Ergebnis zu lie-
fern, stellt aber fest, dass er zu diesem Zweck 2+summe(1) auszurechnen hat.
Was bleibt also anderes übrig als ein erneuter Aufruf der Summenfunktion,
diesmal mit dem Aktualparameter 1? Nicht vergessen: um summe(2) auszu-
rechnen, wurde schon summe(3) unterbrochen, und jetzt muss auch summe(2)
so lange stillstehen, bis summe(1) ein Ergebnis geliefert hat.
Das geht jetzt aber schnell. mit dem neuen Eingabeparameter startet ei-
ne weitere Ausgabe der Summenfunktion, aber sie wird sehr schnell feststel-
len, dass jetzt endlich die Bedingung m==1 erfükllt ist. Also liefert summe(1)
den Rückgabewert 1 an die aufrufende Stelle und ist beendet - und die auf-
rufende Stelle war nun mal die Funktion summe(2). Sie hat sehnlichst auf
diesen Rückgabewert gewartet, denn erst jetzt kann sie ihre Arbeit beenden
und 2+summe(1) ausrechnen. Das ergibt den Wert 3, den nun summe(2) als
Rückgabewert an die aufrufende Stelle zurück gibt; dann ist auch summe(2)
beendet. Erst jetzt kommt wieder summe(3) ins Spiel. Die zuerst aufgerufe-
ne Funktion musste die Beendigung von summe(2) abwarten, denn ohne das
Ergebnis von summe(2) konnte sie nicht weiterrechnen. Na gut, jetzt hat sie
endlich das Ergebnis, und die Rechnung 3+summe(2) liefert für summe(3) das
Resultat 6, das sie stolz ihrer aufrufenden Stelle übergeben kann. Nur diese
eine allererste Ausgabe der Summenfunktion liefert ihr Ergebnis direkt an
das aufrufende Hauptprogramm, alle anderen mussten sich damit zufrieden
geben, ihre Resultate der übergeordneten Fassung von summe() abzuliefern.
Wenn man sich einmal daran gewöhnt hat, ist das alles halb so wild. In
Abbildung 2.28 sehen Sie eine graphische Darstellung dieses Ablaufes.
Die Funktion ruft sich so lange selbst auf, bis eine Situation erreicht ist, in
der die Durchführung der Funktion keinen weiteren Aufruf mehr benötigt: das
war der Fall, sobald mein Eingabeparameter den Wert 1 hatte. Auf so etwas
müssen Sie auch immer achten, das muss sichergestellt sein. Ihre Rekursion
2.5 Funktionen und Rekursion 193

summe(3)

Falls 3<1 dann...


Falls 3==1 dann..
Sonst berechne 3 +summe(2)

Falls 2<1 dann...


Falls 2==1 dann..
Sonst berechne 2 +summe(1)

Falls 1<1 dann...


Falls 1==1 dann ist das Ergebnis 1
sonst...

Ergebnis=3
Ergebnis=6

Abb. 2.28. Ablauf bei einer rekursiven Funktion

muss irgendwann zu einer Situation führen, in der die Rekursion beendet wird,
sonst geraten Sie in eine Art Endlosschleife.

Abb. 2.29. Rekursive Funktion im Struktogramm

Eine rekursive Funktion hat also zwei Merkmale. Erstens ruft sie sich bei
ihrer Durchführung selbst auf und zweitens sollte die Rekursion irgendwann
daduch beendet werden, dass kein weiterer rekursiver Aufruf mehr nötig ist,
sondern nur noch eine einigermaßen einfache Operation durchgeführt wird.
Im Grunde ist das ganz analog zur Schleife, bei der man auch darauf ach-
ten muss, dass die Schleife nicht endlos wiederholt wird, sondern durch eine
194 2 Strukturierte Programmierung mit C

passende Durchführungsbedingung irgendwann zu einem Ende findet. Im Ge-


gensatz zu einer Schleife verursacht eine rekursive Funktion allerdings einen
beträchtlichen Verwaltungsaufwand. Bedenken Sie, dass die Funktion bei je-
dem rekursiven Aufruf stillsteht und sich selbst aufruft, und der aktuelle Stand
der aufrufenden Funktion muss schließlich irgendwo festgehalten werden: an-
sonsten könnten Sie, sobald beispielsweise das Ergebnis von summe(2) vor-
liegt, gar nicht mit summe(3) fortfahren. Je tiefer die Rekursion, desto höher
natürlich der interne Verwaltungsaufwand. Sie als Programmierer haben da-
mit zwar nichts zu tun, das macht der Compiler schon alleine. Aber dennoch
kostet es Rechenzeit, und je mehr internen Verwaltungsaufwand Ihre rekursi-
ve Funktion verursacht, desto höher der Anteil der Verwaltungsaufgaben an
der zur Verfügung stehenden Rechenzeit.
Man kann daher die Empfehlung aussprechen, sich lieber eine passende
Schleife auszudenken anstatt eine Rekursion anzusetzen. Theoretisch ist das
sogar immer möglich: jedes Problem, das mit Hilfe einer rekursiven Funktion
gelöst werden kann, hat auch eine Lösung, die nur auf den drei Kontrollstruk-
turen Sequenz, Auswahl und Wiederholung beruht. Das ist eine allgemeine
Aussage, die man beweisen kann. Sie nützt nur manchmal nicht viel, weil
manche Probleme so sehr nach einer Rekursion schreien, dass man es nicht
mehr überhören kann. Die Summenfunktion wird man normalerweise mit Hil-
fe einer Schleife programmieren, aber bei anderen Funktionen kommt man
um die Rekursion nicht so leicht herum. Standardbeispiele dafür sind die Be-
rechnung von Permutationen oder die Lösung des Problems der Türme von
Hanoi - aber das würde jetzt zu weit führen; so etwas bespricht man eher in
Veranstaltungen zu Algorithmen und Datenstrukturen.

Rekursion
Eine Funktion heißt rekursiv, falls sie sich selbst aufruft. Bei rekursiven Funk-
tionen ist darauf zu achten, dass irgendwann eine Situation entsteht, in der
kein weiterer rekursiver Aufruf der Funktion erforderlich ist, da ansonsten
unendlich viele Selbstaufrufe erfolgen. Der Sinn einer rekursiven Funktion
besteht oft darin, ein Problem, das von einem Eingabeparameter abhängt,
zurückzuführen auf das gleiche Problem mit einem leichter zu verarbeitenden
Eingabeparameter.
Rekursive Funktionen führen bei ihrem Ablauf zu einem hohen internen
Verwaltungsaufwand, da stets bei jedem Funktionsaufruf die Situation der
aufrufenden Stelle festgehalten werden muss. Daher ist es oft zu empfehlen,
anstatt einer Rekursion die drei Kontrollstrukturen Sequenz, Auswahl und
Wiederholung einzusetzen, denn jedes Problem, das mit Hilfe einer rekursiven
Funktion gelöst werden kann, hat auch eine Lösung, die nur auf diesen drei
Kontrollstrukturen beruht.
2.6 Dateien 195

Übungen

2.25. Schreiben Sie das Programm zur Lösung quadratischer Gleichungen so


um, dass das eigentliche Lösen der Gleichung in einer Funktion stattfindet
und das Hauptprogramm nur noch dazu dient, die Parameter p und q der
quadratischen Gleichung x2 +px+q = 0 einzugeben und dann an die Funktion
weiterzugeben.

2.26. Schreiben Sie das Programm zur Erstellung und Verwaltung der Flug-
routen aus Aufgabe 2.24 so um, dass das Aufbauen der Liste, das Ausgeben
der Liste, das Löschen aus der Liste und das Einfügen in die Liste in jeweils
einer Funktion stattfindet.

2.27. Schreiben Sie ein C-Programm, das ein Feld aus ganzen Zahlen einliest
und dann die Zahl mit dem größten Betrag aus diesem Feld herausfindet. Das
Finden der betragsmäßig größten Zahl soll in einer eigenen Funktion erfolgen,
die als Eingabeparameter ein Feld ganzer Zahlen erwartet.

2.28. Die Fakultät einer Zahl n ∈ IN ist definiert durch n! = 1 · 2 · 3 · · · n, also


als das Produkt der ersten n natürlichen Zahlen. Folglich gilt n! = n · (n −
1)!. Schreiben Sie eine rekursive Funktion zur Berechnung der Fakultät einer
ganzzahligen Variablen n und testen Sie diese Funktion durch ein geeignetes
Hauptprogramm.

2.29. Für a ∈ IR und n ∈ IN ist an definiert durch an = a · a · · · a, wobei


a n-mal als Faktor vorkommt. Folglich gilt an = a · an−1 . Schreiben Sie eine
rekursive Funktion zur Berechnung von an und testen Sie diese Funktion durch
ein geeignetes Hauptprogramm.

2.6 Dateien

Sicher hatten Sie schon einmal mit Dateien zu tun, vielleicht ohne sich
darüber großartige Gedanken zu machen. Meistens ist das auch gar nicht
nötig. Wenn Sie ein Word-Dokument zusammenstellen und abspeichern oder
ein C-Programm schreiben und abspeichern, dann haben Sie in jedem Fall
eine Datei erstellt, denn eine Datei ist nichts anderes als eine Folge von Bytes,
die auf irgendeinem Festspeicher abgelegt ist. Natürlich sollte eine Datei für
den Benutzer auch wieder erreichbar sein, sonst hätte er sich das Abspeichern
gleich sparen können.
Die bisher betrachteten C-Programme haben zwar brauchbare Ergebnisse
ausgespuckt, aber sobald das Programm sein Ende erreicht hatte, waren die
Ergebnisse für immer verloren, da ich sie in keiner Datei gespeichert hatte.
Umgekehrt musste ich alle Eingaben von Hand über die Tastatur vornehmen,
weil ich über keine Möglichkeit verfügte, sie von einer Datei einzulesen. Das
wird sich jetzt ändern, der Zugriff auf Dateien von einem C-Programm aus
196 2 Strukturierte Programmierung mit C

ist nicht weiter kompliziert. Sie sollten nur immer an eines denken: bevor
Sie mit einer Datei arbeiten können, müssen Sie sie öffnen, und wenn die
Arbeit beendet ist, muss die Datei auch wieder geschlossen werden; das ist
nicht anders als bei einem Tresor. Eine ungeöffnete Datei wird sich jedem
Bearbeitungsversuch entziehen. Wie so etwas geht, zeigt das folgende Beispiel.
/* speichern.c -- Zahlen speichern */

#include <stdio.h>

main()
{
int i,x;
FILE *datei;
datei = fopen("daten.dat","w");
if (datei == NULL)
printf("Dateierstellung fehlgeschlagen\n");
else
{
for (i=0;i<3;i++)
{
printf("Eingabe einer Zahl\n");
scanf("%d",&x);
fprintf(datei,"%d\n",x);
}
fclose(datei);
}
}
Das sieht schlimmer aus als es ist. Um mit einer Datei arbeiten zu können,
muss man dem Programm die Datei erst einmal bekannt machen. Zu diesem
Zweck verwendet man den vordefinierten Datentyp FILE, mit dessen Hilfe Da-
teien einem Programm angekündigt werden können. Die Großschreibung von
FILE ist übrigens keine Frage des persönlichen Geschmacks, sondern wird vom
Compiler verlangt. Damit Sie nun eine Datei in Ihrem Programm verwenden
können, brauchen Sie einen Zeiger auf eine FILE-Variable, und den habe ich
mit FILE *datei angelegt. Dieser Zeiger sollte auf eine Datei zeigen, aber
woher bekomme ich meine konkrete Datei, in der meine Daten stehen? Die
liefert mit der fopen()-Befehl. Wie Sie der Zeile
datei = fopen("daten.dat","w");
gibt das Kommando fopen() einen Zeiger auf eine FILE-Variable zurück,
sonst wäre die Wertzuweisung an datei ziemlich sinnlos. Interessant ist aber,
welche Inputparameter fopen() verlangt: Sie müssen bei der Dateiöffnung
zwei Zeichenketten übergeben, die einerseits den tatsächlichen Namen der Da-
tei und andererseits die Art des Zugriffs beschreiben. In die erste Zeichenkette
schreiben Sie den Namen der Datei, um die es sich handelt; das Beispielpro-
gramm soll also mit der Datei daten.dat hantieren. Und die zweite Zeichen-
2.6 Dateien 197

kette sagt Ihnen, auf welche Weise Sie auf diese Datei zugreifen dürfen. Mit
der Angabe "w" wird beispielsweise festgelegt, dass auf die in Frage stehende
Datei nur schreibend zugegriffen werden darf: Sie dürfen auf die Datei schrei-
ben und sonst dürfen Sie gar nichts. Hier sollten Sie ein wenig Vorsicht walten
lassen. Wenn Sie sich nämlich für die Zugriffsart "w" entscheiden, dann wird
entweder - falls noch keine Datei dieses Namens existiert - eine neue Datei
angelegt, auf die Sie dann schreiben können, oder - falls es schon eine gibt -
eine bereits existierende Datei dieses Namens wird komplett mit den neuen
Daten überschrieben.
Auf andere Zugriffsarten komme ich gleich zu sprechen, zunächst zurück
zum Programm. Das fopen()-Kommando hat also eine Datei mit dem Na-
men daten.dat angelegt und als beschreibbar qualifiziert. Der Zeiger datei
zeigt jetzt auf eine FILE-Variable, mit der ich auf diese Datei daten.dat
zugreifen kann, aber nur, wenn nichts schiefgegangen ist. Sollte das fopen()-
Kommando fehlerhaft verlaufen sein, weil die Datei aus welchen Gründen
auch immer nicht angelegt werden konnte, dann gibt es keinen Dateizeiger,
der zurückgegeben werden könnte. In diesem Fall wird fopen() den bekann-
ten und beliebten NULL-Zeiger liefern, und das erklärt die Abfrage des Zeigers
datei auf den Wert NULL. Falls datei auf NULL zeigt, hat das Anlegen der
Datei nicht funktioniert und jede weitere Verarbeitung wäre sinnlos.
Nun gehe ich davon aus, dass ich die Datei anlegen konnte, und will nichts
weiter tun als drei ganze Zahlen in meine Datei zu schreiben. In der for-
Schleife wird jeweils eine Zahl von der Tastatur eingelesen und dann mit Hilfe
von fprintf() der Datei übergeben. An der Zeile
fprintf(datei,"%d\n",x);
können Sie sehen, dass fprintf() genauso funktioniert wie das vertraute
printf(), es muss nur zusätzlich noch ein Zeiger auf eine Datei als erster Pa-
rameter mitgegeben werden. Daher wird in meinem Programm die ganze Zahl
x im richtigen Format in die Datei daten.dat geschrieben und anschließend
die Zeile gewechselt, damit die nächste Zahl eine neue Zeile bekommt.
Am Ende der Verarbeitung wird die Datei mit fclose() geschlossen, und
das war’s schon. Auf diese Weise können Sie immer Ihre Datenelemente in eine
Datei schreiben. Nun kann es aber sein, dass Sie schon eine Datei haben und
ein paar Elemente an sie anhängen wollen. Kein Problem, dazu brauchen Sie
nur den zweiten Parameter des fopen()-Kommandos auf "a" anstatt "w" zu
setzen, und schon wird nichts mehr überschrieben, sondern brav angehängt.
Dabei steht übrigens "a" für append und "w" für write. Und für ganz Gründ-
liche, zu denen Sie und ich doch sicher gehören, steht noch der Parameterwert
"a+" zur Verfügung, der im Falle einer existierenden Datei die Daten anhängt
und die Datei neu erzeugt, falls sie noch nicht existiert. Was das Schreiben
angeht, sind Sie damit auf der sicheren Seite: was vorhanden ist, wird nicht
überschrieben, und was noch nicht vorhanden ist, wird neu angelegt.
Nun kann ich ja viel erzählen, bis ein Tag lang ist, aber man sollte die Er-
gebnisse auch irgendwie überprüfen können. Wie sehe ich denn, ob das alles
gut gegangen ist? Nichts leichter als das. Sie können beispielsweise direkt mit
198 2 Strukturierte Programmierung mit C

einem Editor die erzeugte Datei ansehen und werden dann feststellen, dass
tatsächlich drei Zahlen in der Datei stehen, wobei jede Zahl eine eigene Zeile
hat. Nur sollten Sie dafür wissen, wo Sie Ihre Datei eigentlich suchen sollen.
Wird einfach nur ein Dateiname angegeben, so befindet sich die erzeugte Da-
tei in genau dem Verzeichnis oder Ordner, in dem Sie die ganze Zeit arbeiten,
also in Ihrem C-Arbeitsverzeichnis - wobei ich davon ausgehe, dass Sie Ihren
heimischen PC gut genug kennen, um zum Beispiel über den Windows Explo-
rer einen bestimmten Ordner zu finden. Aber was macht man, wenn die Datei
an eine ganz andere Stelle soll? Auch nicht schwer. Jede Datei hat einen Pfad,
der angibt, an welcher Stelle sie sich befindet. Nehmen wie an, Sie haben ein
Verzeichnis test angelegt, das direkt auf der Platte C: liegt, dann hat dieses
Verzeichnis unter Windows die Adresse C:\test. Und wenn jetzt meine Da-
tei daten.dat in dieses Verzeichnis geschrieben werden soll, dann tue ich ihr
den Gefallen und gebe im fopen()-Kommando einfach den kompletten Pfad
C:\test\daten.dat anstelle des schlichten Dateinamens daten.dat an. Aber
Vorsicht! Den so genannten Backslash \ akzeptiert der C-Compiler nicht als
passendes Zeichen bei der Pfadangabe, Sie müssen einen doppelten Backslash
verwenden. Die korrekte Syntax wäre hier also
fopen("C:\\test\\daten.dat","w");
und schon sitzt Ihre Datei im Verzeichnis test.
Nicht schlecht, aber das reicht noch nicht. Wenn die Datei schon mit ei-
nem C-Programm erstellt worden ist, dann sollte es auch möglich sein, sie mit
einem C-Programm zu lesen. Und natürlich ist das möglich, wie das folgende
Programm zeigt.
#include <stdio.h>

main()
{
int i,x=0;
FILE *datei;
datei = fopen("daten.dat","r");
if (datei == NULL)
printf("Fehler beim Oeffnen der Datei\n");
else
{
for (i=0;i<3;i++)
{
fscanf(datei,"%d",&x);
printf("%d\n",x);
}
fclose(datei);
}
}
Hier muss ich wohl nicht mehr viel sagen. Da die Datei jetzt zum Lesen geöff-
net werden soll, lautet der zweite Parameter für das fopen()-Kommando
2.6 Dateien 199

natürlich "r" für read. Alternativ hätten Sie auch "r+" nehmen können, dann
wird die Datei sowohl zum Lesen als auch zum Schreiben geöffnet, sofern sie
schon existiert. Von der Verwendung des Parameterwertes "w+" rate ich in
diesem Zusammenheng eher ab, denn der erzeugt eine Datei zwar zum Lesen
und Schreiben, aber einer vorhandene Datei wird gnadenlos überschrieben
und die Daten gehen schon beim Anlegen verloren.
Dass in der for-Schleife mit einem fscanf()-Kommando die Daten aus
der Datei heraus gelesen werden, wird Sie wohl nicht überraschen; es funktio-
niert genauso wie das vertraute scanf(), nur dass zusätzlich noch ein Zeiger
auf eine Datei als erster Parameter mitgegeben werden muss. Und mehr pas-
siert mit der Datei auch gar nicht, die eingelesenen Daten werden am Bild-
schirm ausgegeben, und dann wird die Datei mit fclose(datei) geschlossen.
Aber woher soll ich denn wissen, dass genau drei Zahlen in meiner Datei
sind? Immerhin könnte ja das Beschreiben der Datei schon lange zurück liegen
und keiner mehr wissen, wie viele Elemente im Lauf der Zeit dazu gekommen
sind. Auch das ist aber kein Problem. Sie können mit einer Bedingungsschleife
durch die Einträge der Datei laufen und bei jedem Durchlauf abfragen, ob das
Ende der Datei erreicht ist. Dazu dient das feof()-Kommando, das ich Ihnen
im nächsten Programmstück vorführe, mit dem die for-Schleife des vorheri-
gen Programms ersetzt werden kann.
while (feof(datei) ==0)
{
fscanf(datei,"%d",&x);
printf("%d\n",x);
}
Das ist schon alles. Das Kommando feof() steht für file end of file, testet,
ob das Ende der Datei erreicht ist, und liefert zu diesem Zweck eine ganze
Zahl. Ist das Ergebnis 0, so haben Sie das Ende der Datei noch nicht erreicht,
kommt dagegen eine von 0 verschiedene Zahl heraus, so befinden Sie sich am
Dateiende. Deshalb überprüfe ich in meiner Bedingung, ob feof(datei) den
Wert 0 zurück gibt. Falls ja, gibt es noch etwas zu tun, falls nicht, ist alles er-
ledigt. Aber Vorsicht: das Programm, mit dem ich die Datei gefüllt hatte, hat
an jede Zahl noch einen Zeilenwechsel angehängt, und das kann dazu führen,
dass feof() das Dateiende erst einen Schritt zu spät erkennt und damit ein
Schleifendurchlauf zu viel durchgeführt wird. Um so etwas zu vermeiden, soll-
ten Sie beispielsweise in Ihrem Schreibprogramm dafür sorgen, dass die letzte
in die Datei geschriebene Zahl mit keinem Zeilenwechsel mehr belastet wird.
Sie können übrigens die Durchführungsbedingung der while-Schleife auch et-
was kürzer formulieren. Da der Wert 0 von C immer als das logische falsch“

interpretiert wird, während jede von 0 verschiedene Zahl als wahr“ betrachtet

wird, genügt schon die Angabe von while(!(feof(datei)). Ob das allerdings
zur Klarheit und Lesbarkeit eines Programms beiträgt, sei dahingestelt.
Das alles funktioniert nicht nur mit ganzen Zahlen, sondern auch mit reel-
len Zahlen, Zeichenketten oder anderen Datenelementen; Sie müssen nur die
nötigen Änderungen bei der Ein- und Ausgabe vornehmen. Natürlich gibt es
200 2 Strukturierte Programmierung mit C

noch weitere Möglichkeiten, in C mit Dateien umzugehen, aber das Wesentli-


che wissen Sie jetzt. Für den Moment haben wir auch genug über Dateien und
ihre Verarbeitung gesprochen; im nächsten Kapitel werden Sie ihnen wieder
begegnen.

Dateien in C
Um eine Datei in einem C-Programm zu verarbeiten, muss im Programm ein
Zeiger auf eine FILE-Variable definiert sein, dem das Ergebnis eines fopen()-
Kommandos zugewiesen werden kann. Im fopen()-Kommando legt man Na-
men und Pfad der Datei sowie die Zugriffsart auf die Datei fest. In die Datei
schreiben und aus ihr lesen kann man mit den Kommandos fprintf() und
fscanf(). Um bei einer existierenden Datei festzustellen, wann das Dateiende
erreicht ist, setzt man das Kommando feof() ein.

Übungen

2.30. Schreiben Sie ein C-Programm, das eine beliebige Anzahl von Zeichen-
ketten von der Tastatur einliest, in eine Datei schreibt und anschließend aus
der Datei wieder am Bildschirm ausgibt.

2.31. Schreiben Sie ein weiteres Programm, das die in Aufgabe 2.30 bearbei-
tete Datei verwendet, neue Zeichenketten an das bisherige Dateiende anhängt
und dann alle Zeichenketten aus der Datei wieder am Bildschirm ausgibt.
3
Objektorientierte Programmierung mit Java

Was wir hinterlassen, ist nicht so wichtig wie die Art,



wie wir gelebt haben.“
Jean Luc Picard, Captain der Enterprise

Einen Minigolfplatz zu besuchen ist normalerweise kein großes Risiko, wenn


man davon absieht, dass ältere Spieler sich beim Bücken über die Bahn leicht
das Kreuz verrenken können. Anders sieht es aus, sobald Sie minigolfinteres-
sierte Kinder im passenden Alter haben: vielleicht kommen sie auf die Idee,
bei Turnieren mitzuspielen, an Meisterschaften teilzunehmen, um Pokale zu
spielen - und so die Wochenenden ihrer Eltern zu strapazieren, denn ein we-
nig Begleitung kann nicht schaden. Immerhin ist das auch für die Eltern eine
neue Erfahrung, denn wem würde schon von alleine einfallen, sich tagelang auf
einem Minigolfplatz aufzuhalten und Dutzenden von Kindern und Jugendli-
chen dabei zuzusehen, wie sie beispielsweise für die Deutsche Meisterschaft
trainieren? Hätten Sie gewusst, dass für einen trainierenden Minigolfer das
eigentliche Objekt der Begierde der Ball ist? Nein, natürlich nicht, so etwas
lernen nur die Spieler selbst und die Eltern der Spieler, wobei Sie einmal ra-
ten dürfen, zu welcher der beiden Kategorien ich gehöre. Während man beim
üblichen Golf andauernd den Schläger wechselt, mit dem man dann aber auch
unverdrossen auf den gleichen Ball einschlägt, ist es beim Minigolf genau um-
gekehrt: ein Schläger ist mehr oder weniger so gut wie der andere, aber von
Bahn zu Bahn wird mal dieser, mal jener Ball zum Einsatz kommen. Und
glauben Sie ja nicht, dass man einen Ball einfach nur schlägt. Er muss warm
gehalten werden oder heiss oder kalt, abhängig von der Bahn, auf der er ge-
spielt wird, und von den Wind- und Temperaturverhältnissen.
Damit sind wir schon mitten in dem Thema, um das es mir hier gehen
wird. Ich hatte es schon gesagt: der Ball ist das Objekt der Begierde eines
Minigolfers, und dieses Minigolfobjekt spielt bei einem Turnier im Grunde
genommen eine sehr ähnliche Rolle wie die Objekte, mit denen Sie es in der
objektorientierten Programmierung zu tun haben. Natürlich können Sie nicht
nur auf einen Ball schlagen, dazu gibt es auch Sandsäcke, Nägel oder Spar-
ringspartner, aber es geht ja gar nicht primär um die Aktivität des Schlagens,
sondern um den Ball, der geschlagen werden muss. Natürlich kann man außer
einem Ball auch noch ganz andere Dinge wärmen wie zum Beispiel das Essen
von gestern oder die Freundin von heute, aber das Wärmen an sich ist nicht
202 3 Objektorientierte Programmierung mit Java

das Hauptproblem des Minigolfers, sondern das Wärmen seines Balls, wenn er
denn einen warmen Ball für die aktuelle Bahn brauchen sollte. All diese Akti-
vitäten rund um den Ball dienen nur dem Zweck, diesen Ball, dieses Objekt in
den Zustand zu bringen, der von ihm erwartet wird. Im Mittelpunkt steht das
Objekt, steht der Ball, und nicht das allgemeine Schlagen oder das allgemeine
Wärmen und Abkühlen. Alles was passiert, ist auf das Objekt bezogen, und
das ist auch klar, denn beim Spielen nützt Ihnen eine aufgewärmte Suppe gar
nichts, falls Sie einen aufgewärmten Ball brauchen.
Bei der objektorientierten Programmierung sieht es nicht anders aus.
Während wir uns bisher auf das sogenannte prozedurale Programmieren kon-
zentriert haben, bei dem die Aktivität, der Ablauf, die Prozedur im Mittel-
punkt des Interesses steht, kommt jetzt noch etwas hinzu. Jede Aktivität,
jeder Vorgang hat einen direkten Bezug zu einem Objekt, das nun in den Fo-
kus rückt und meine Aufmerksamkeit beansprucht. Selbstverständlich müssen
die Aktivitäten selbst noch immer programmiert werden, und das geschieht
noch immer nach den Methoden der strukturierten Programmierung, wie Sie
es am Beispiel von C gelernt haben. Aber alles, was geschieht, wird bei der
objektorientierten Programmierung einen Bezug zu einem Objekt haben, wird
seine Eigenschaften ändern oder schlicht dafür sorgen, dass mit dem Objekt
etwas passiert - so wie das Schlagen des Balles dafür sorgt, dass der Ball
über die Bahn und möglichst ins Loch rollt: der Ball soll rollen und nicht der
Sandsack oder gar der Sparringspartner.
Wie Sie mit Minigolfobjekten richtig umgehen, damit Sie für eine Run-
de aus 18 Bahnen nur 22 Schläge brauchen, kann ich Ihnen nicht erklären,
dazu fragen Sie am besten Ihren örtlichen Minigolfverein oder notfalls auch
Ihren Arzt oder Apotheker. Ich kann Ihnen nur zeigen, wie man objektori-
entierte Programmierung betreibt, und genau darüber werde ich jetzt eine
Weile am Beispiel der Programmiersprache Java reden. Warum gerade Java?
Dafür gibt es mehrere Gründe. Java ist eine ausgesprochen moderne Sprache,
die gerade im Internetbereich weite Anwendungen findet, und es kann nicht
schaden, eine sich immer stärker verbreitende Programmiersprache zu lernen.
Die Objektorientierung selbst können Sie natürlich auch anhand von C++
angehen, der objektorientierten Weiterentwicklung von C. Aber erstens soll-
ten Sie während Ihrer Grundausbildung nicht nur mit einer Sprache und ihrer
Weiterentwicklung konfrontiert werden, sondern besser gleich zwei Sprachen
lernen, und zweitens macht es gar keinen so großen Unterschied, denn wer mit
Java umgehen kann, lernt auch ganz schnell C++ und umgekehrt.
Sehen wir uns also an, was Java zu bieten hat. Zuerst werden ich Ihnen
zeigen, wie man mit Java strukturiert programmieren kann, dann werden Sie
Klassen, Objekte und Methoden genauer kennen lernen. Nach der Klärung
dieser Grundbegriffe können wir uns dann an Vererbung und Polymorphie
wagen, Sie werden einiges über Interfaces und Exceptions lernen, und nach
einem Ausflug in die Welt der Streams werde ich Ihnen noch ein wenig über
graphische Benutzeroberflächen erzählen.
3.1 Strukturierte Programmierung mit Java 203

3.1 Strukturierte Programmierung mit Java


Java ist eine objektorientierte und plattformunabhängige Programmierspra-
che: na gut, das kann jeder sagen, es ist auf den ersten Blick sicher nicht klarer
als die klassische Selbstbeschreibung des alten Mephisto, er sei ein Teil von
jener Kraft, die stets das Böse will und stets das Gute schafft. Aber Faust
war auch nicht dumm, und mit ihm stelle ich jetzt die Frage, was denn wohl
mit diesem Rätselwort gemeint sei. Was Objektorientierung bedeutet, habe
ich schon in der Einleitung angedeutet, im Laufe der Zeit wird das noch kla-
rer werden, denn darüber werde ich schließlich die ganze Zeit reden. Auf die
Plattformunabhängigkeit komme ich gleich zu sprechen, erst möchte ich aber
noch meine Behauptung von vorhin untermauern, Java sei eine ausgesprochen
moderne Programmiersprache. Wie ist Java eigentlich entstanden?

3.1.1 Die Entwicklung von Java

Das Internet gibt es schon sehr viel länger als die Programmiersprache Java,
und in seinen Anfängen hatte Java auch rein gar nichts mit dem Internet zu
tun. Das diente nämlich in grauer Vorzeit, als man in den sechziger Jahren
des zwanzigsten Jahrhunderts begann, sich über eine weitläufige Vernetzung
Gedanken zu machen, zu nichts anderem als zum Austausch von Informa-
tionen, vor allem unter Wissenschaftlern. Kein Gedanke an farbige Bildchen,
an graphische Benutzeroberflächen oder gar an Chaträume, das kam erst viel
später. Deshalb erblickte auch das World Wide Web 1990 in einer wissen-
schaftlichen Institution, dem CERN-Forschungslabor in Genf, das Licht der
Welt, und noch immer hatte man vor allem den Informationsaustausch un-
ter Wissenschaftlern im Sinn. Von Java sprach damals noch keiner außer den
Geographen und den Touristen, die schon mal die Insel Java besucht hat-
ten. Kein Wunder, denn die Programmiersprache Java gab es noch nicht, ihre
Entwicklung begann 1990 bei Sum Microsystems.
Aber um Himmels Willen nicht im Zusammenhang mit dem Internet, wen
hat das damals schon interessiert? Was die Forschungsgruppe Green Project“

von Sun Microsystems im Sinn hatte, war die Entwicklung einer Program-
miersprache zur Entwicklung von Software für so etwas wie Haushaltsgeräte,
interaktives Fernsehen oder Telefonanlagen. Man wollte also Gegenstände des
täglichen Lebens mit ein wenig maschineller Intelligenz ausstatten und über-
sah dabei, dass kaum ein Mensch sich mit seinem Elektroherd unterhalten
möchte. Oder haben Sie das Bedürfnis, einen interaktiven Kühlschrank zu be-
sitzen, der Ihnen mit irgendwelchen Fehlermeldungen auf die Nerven fällt, weil
keine Milch mehr im Haus ist? Vielleicht steht ja die Milch im Keller, weil die
Katze keine zu kalte Milch mag, oder mein Arzt hat bei mir eine Milchaller-
gie festgestellt - Umstände, die kein noch so gut programmierter Kühlschrank
wissen kann. Es ist daher kein Wunder, dass man mit diesem Projekt wegen
fehlender Marktaussichten nicht so recht weiter kam. Vielleicht lag es auch
ein wenig an dem sperrigen Namen OAK der Programmiersprache, von dem
204 3 Objektorientierte Programmierung mit Java

die einen sagen, er stehe für Object Application Kernel, die anderen aber be-
haupten, der Entwickler James Gosling habe zum falschen Zeitpunkt etwas
zu lang auf die Eiche vor seinem Bürofenster geschaut, und Eiche heißt auf
Englisch nun mal Oak.
Wie dem auch sei, als das World Wide Web seinen Siegeszug antrat, stell-
te sich schnell heraus, dass die für völlig andere Zwecke konzipierte Sprache
OAK gut für Web-Einsätze geeignet war, und ab diesem Zeitpunkt kannte
die Entwicklung keine Grenzen mehr. Den Namen musste man ändern, nicht
nur, weil niemand eine Eiche auf seinem Rechner installieren will, sondern
auch, weil er dummerweise schon an eine andere Programmiersprache verge-
ben war. Nun geht ja das Gerücht, dass Programmierer den ganzen Tag über
literweise Kaffee trinken, wenn ich auch zugeben muss, dass zu meiner Zeit
als aktiver Programmierer die besten Tage und auch die besten Programme
eher von dem einen oder anderen Bier begleitet waren. Aber das Gerücht hält
sich hartnäckig, und weil Java erstens der Name einer indonesischen Insel ist,
von der Kaffee exportiert wird, und zweitens auch noch im Amerikanischen
als Synonym für Kaffee verwendet wird, sollen sich die Entwickler für den
Namen Java“ entschieden haben. Schon im Mai 1995 hat Sun sein Java als

neue Programmiersprache der Welt vorgestellt, und seither hat es sich immer
weiter durchgesetzt. Ein Grund liegt sicher in seinem günstigen Preis: Java
kostet nichts und kann direkt von Sun über das Internet auf den heimischen
Computer geladen werden. Aber die Gratisabgabe würde nichts helfen, wenn
das Produkt selbst zu nichts zu gebrauchen wäre, und einige der Möglichkeiten
von Java werden Sie auf den folgenden Seiten sehen.

Die Programmiersprache Java


Ab 1990 wurde bei Sun Microsystems an der Entwicklung der Programmier-
sprache OAK gearbeitet, die zur Entwicklung von Software für Haushalts-
geräte dienen sollte. Als dieses Projekt wegen mangelnder Marktaussichten
scheiterte, wurde die Sprache unter dem neuen Namen Java seit 1995 zur
Programmierung im Internetbereich eingesetzt.

3.1.2 Compiler und Interpreter

Bevor ich Ihnen das erste kleine Java-Programm zeigen kann, muss ich mich
noch kurz über den Unterschied zwischen einem Compiler und einem Inter-
preter äußern, denn Java arbeitet beim Bearbeiten des Programmtextes etwas
anders als das inzwischen vertraute C. Wie Sie sich sicher erinnern oder auf
jeden Fall nachlesen können, wird ein C-Programm, sobald Sie es mühevoll
in den Editor geschrieben und als Datei gespeichert haben, vom Compiler
auf seine korrekte Syntax überprüft. Liegen grammatische Fehler vor, weil
Sie beispielsweise els statt else geschrieben haben, so wird er Sie mit Hilfe
von Fehlermeldungen darauf aufmerksam machen. Falls aber alles korrekt ist,
wird Ihr Quellprogramm direkt in die Maschinensprache übersetzt, damit es
3.1 Strukturierte Programmierung mit Java 205

nach dem Compilerlauf sofort ausführbar ist. Resultat dieser Aktivitäten ist
dann zum Beispiel unter dem Betriebssystem Windows eine exe-Datei, wobei
exe“ eine Abkürzung für executable, also ausführbar darstellt. Ist eine exe-

Datei einmal erstellt, kann man sie immer wieder verwenden, ohne sich noch
mit weiteren Compilierungsvorgängen plagen zu müssen, denn das Programm
liegt ja schon in Maschinensprache vor, also in einer Form, die Ihr Computer
direkt versteht.
Der wesentliche Vorteil dieses Verfahrens liegt in der Ausführungsge-
schwindigkeit. Das einmal übersetzte Programm hat eine direkt von der Ma-
schine verwendbare Form und kann daher ohne weitere Umwege auch direkt
von der Maschine in Aktionen umgesetzt werden; man bewegt sich also in
dem übersetzten Programm bereits auf der Ebene von Nullen und Einsen,
die der Computer so dringend zum Leben braucht. Daraus folgt aber auch
schon ein Nachteil des Compilerverfahrens: gerade weil die Übersetzung eine
ausführbare Datei liefert, die der jeweilige Computer direkt und ohne Um-
schweife verwenden kann, muss die ausführbare Datei den Gegebenheiten die-
ses einen Computers genau angepasst sein. Ein handelsüblicher PC, der mit
einem Windows-Betriebssystem versehen ist, hat ein anderes Innenleben als
ein Großrechner, aber Unterschiede gibt es nicht nur auf der Ebene der Hard-
ware. Auch wenn Sie einen Rechner unter Windows betreiben und einen an-
deren mit dem Betriebssystem UNIX bestücken, müssen Sie davon ausgehen,
dass die internen Abläufe sich unterscheiden, sonst wären die Betriebssysteme
schließlich nicht verschieden. Also werden auch die ausführbaren Dateien des
einen Systems, die den Gegebenheiten dieses Systems angepasst waren, auf
einem anderen System nicht einsetzbar sein, weil dort andere Gegebenheiten
herrschen. Mit einem Wort: das übersetzte Programm ist plattformabhängig,
wobei man unter Plattform die Maschine und das Betriebssystem versteht,
die das Programm zum Laufen bringen sollen. Sie können nicht ein unter
Windows übersetztes C-Programm in Form einer exe-Datei auf eine UNIX-
Maschine transportieren und dann glauben, dass es auch dort laufen wird;
man muss damit rechnen, dass die ausführbare Datei der einen Plattform für
die andere Plattform nichts taugt.
Trotz dieses Nachteils gibt es eine Unmenge von Programmiersprachen,
die nach dem Compilerprinzip arbeiten; ich nenne hier nur die Beispiele C,
Pascal, COBOL und FORTRAN. Aber es gibt eben auch eine andere Me-
thode, Programmtext umzusetzen, und das ist das Interpreterprinzip. Noch
einmal: ein Compiler liest die Quelldatei und wandelt sie ein- für allemal in
eine ausführbare Datei um. Der Interpreter macht in gewisser Weise das ge-
naue Gegenteil, indem er die Quelldatei interpretiert, und das heißt, dass er
die benötigten Programmteile erst zur Laufzeit in Maschinencode umwan-
delt. Es wird also nicht auf einen Schlag alles übersetzt, sondern während des
Programmlaufs schön der Reihe nach gerade das übersetzt, was man zum wei-
teren Lauf des Programms braucht. Natürlich wird dadurch das Programm
insgesamt etwas langsamer laufen, da das Übersetzen und das Durchführen
miteinander kombiniert werden. Auf der anderen Seite kann die Entwicklungs-
206 3 Objektorientierte Programmierung mit Java

phase des Programms, also der Vorgang des Programmierens selbst, schneller
vorangehen als beim Einsatz des Compilerprinzips, denn ein ganzes Programm
immer wieder zu übersetzen ist gerade in der Testphase ein mühseliger und
zeitraubender Vorgang, der beim Einsatz des Interpreterprinzips entfällt.
Was hat das nun alles mit Java zu tun? Verwendet Java das Compilerprin-
zip oder arbeitet es nach dem interpretierenden Verfahren? Keins von beiden,
oder besser gesagt: beide der Reihe nach. Da Java plattformunabhängig sein
soll, kann es nicht auf dem reinen Compilerprinzip basieren, und da auch Java-
Programme eine vertretbare Laufzeit vorweisen können sollen, darf es nicht
nur auf das Interpreterprinzip zurückgreifen. Man hat sich deshalb für eine
Mischform entschieden, die es fertigbringt, die Vorteile beider Formen in sich
zu vereinen und die Nachteile wenigstens nicht mit voller Wucht auf sich zu
ziehen.
Wie üblich schreiben Sie ein Java-Programm mit Hilfe eines Editors, der
auch einer Programmierumgebung entstammen kann, und speichern Ihr Quell-
programm in einer Datei ab. Diese Datei wird dann dem Java-Compiler über-
geben, der das Quellprogramm kompiliert, also übersetzt - so weit gibt es
keinen Unterschied zum Compilerprinzip. Der Java-Compiler wird Ihr Pro-
gramm aber, sofern es syntaktisch in Ordnung ist, nicht direkt in Maschinen-
code übersetzen, weil es dann nicht die geringste Chance auf Plattformun-
abhängigkeit gäbe, sondern eine Zwischenform erzeugen, nicht mehr Quellco-
de und noch nicht Maschinencode: den so genannten Bytecode. Der Bytecode,
der aus dem Compilerlauf entsteht, ist also kein ausführbares Programm, das
ist sein Nachteil. Aber egal auf welcher Maschine, unter welchem Betriebssy-
stem auch immer Sie Ihr Javaprogramm dem Compiler überantworten: der
entstehende Bytecode wird immer der gleiche sein, die Kompilierung verläuft
völlig unabhängig von der umgebenden Plattform. Das und nichts anderes ist
gemeint, wenn man Java als plattformunabhängige Sprache bezeichnet.
Aber natürlich hat das auch einen Haken. Das Ergebnis des Kompilierens
ist der plattformunabhängige Bytecode, schön und gut. Und wie mache ich
aus diesem Bytecode jetzt ein lauffähiges Programm? Auch nicht schwer. Da
der Bytecode nun mal nicht auf die Gegebenheiten jeder Maschine angepasst
sein kann, schaltet man eine Art gedachter Maschine zwischen den Bytecode
und die eigentliche physisch vorhandene Maschine, und diese gedachte Ma-
schine ist in der Lage, den Bytecode zu interpretieren und auszuführen. Das
klingt unübersichtlicher als es ist. Bei jeder Installation von Java wird auch
die virtuelle Maschine von Java automatisch mitgeliefert, die Java Virtual
Machine, die ohne zu zögern die Rolle der erwähnten gedachten Maschine
übernimmt. Diese Java Virtual Machine, abgekürzt JVM, schnappt sich den
Bytecode und behandelt ihn dann genauso, wie ich es oben im Zusammen-
hang mit dem Interpreterprinzip beschrieben habe, das heißt: sie wandelt ihn
in Maschinencode um und führt ihn aus. Zuerst wird also der Quellcode nach
einer Syntaxprüfung übersetzt - das ist der Anteil des Compilerprinzips. Und
dann wird das Resultat des Übersetzens, der Bytecode, von der JVM inter-
pretiert und ausgeführt - hier setzt sich das Interpreterprinzip durch. Der
3.1 Strukturierte Programmierung mit Java 207

übliche Nachteil der geringeren Geschwindigkeit kommt natürlich auch hier


zum Tragen, aber das ist nicht ganz so wild. Schließlich wird das Programm
vorher kompiliert, und erst dann wird ein bereits vorkompiliertes Programm
der Interpretation ausgesetzt. Das ist zwar etwas langsamer als ein Ablauf
eines komplett kompilierten Programms, aber aufgrund der Vorkompilierung
immer noch erheblich schneller als es ein konventioneller Interpreterlauf sein
könnte.
Damit keine Missverständnisse aufkommen: die JVM ist tatsächlich eine
virtuelle Maschine, also ein Stück Software, das dazu da ist, Bytecode zu in-
terpretieren und zur Ausführung zu bringen. Selbstverständlich kann die JVM
selbst nicht mehr plattformunabhängig sein, denn sie muss den plattformun-
abhängigen Bytecode mit den Gegebenheiten des vorliegenden Systems in Ein-
klang bringen. Sie ist sozusagen der Vermittler, die Schnittstelle zwischen dem
plattformunabhängigen Compiler und dem zugrunde liegenden System Ihres
physisch vorhandenen Computers. Deshalb ist der Satz, Java sei plattformun-
abhängig, auch nur die halbe Wahrheit. Der Compiler produziert plattformun-
abhängigen Bytecode, den man problemlos von einer Maschine auf die andere
transportieren kann; so viel ist wahr. Aber die JVM ist plattformabhängig, auf
einem Windows-Rechner werden Sie eine andere Interpretation brauchen als
auf einem UNIX-Rechner; auf dieser Ebene muss die Plattformabhängigkeit
wieder zuschlagen.

Programmtext

Java-Compiler

Bytecode

JVM

Ausführung

Abb. 3.1. Java-Compiler und JVM

Einsatz von Java-Programmen


Ein Java-Programm wird mit Hilfe eines Compilers in plattformunabhängigen
208 3 Objektorientierte Programmierung mit Java

Bytecode übersetzt. Dieser Bytecode wird dann von der plattformabhängigen


Java Virtual Machine (JVM) interpretiert und zur Ausführung gebracht.

3.1.3 Erste Schritte

Vor dem ersten Schritt sollte man sich überlegen, in welche Richtung man lau-
fen will. Es gibt verschiedene Arten von Java-Programmen, und je weiter Java
sich entwickelt, desto mehr wird sich das Ganze noch auffächern. Hier werde
ich nur über Applikationen reden, die Grundform aller Java-Programme. Da-
hinter steckt nichts Aufregendes, eine Applikation ist nur ein ganz normales
in Java geschriebenes Programm, eine Anwendung eben, die man nicht viel
anders behandelt als ein C-Programm. Davon abgesehen gibt es auch noch
so etwas wie Applets, die man normalerweise nur im Zusammenhang mit ei-
nem Internet-Browser angeht - aber wie gesagt: mich interessieren hier die
Applikationen. Wir schreiben also ganz normale Programme.
Das erste dieser Programme sehen Sie hier vor sich; es soll wieder einmal
nichts anderes tun als Sie mit einem freundlichen Guten Morgen“ zu be-

grüßen.
//Gruss.java --- gibt Guten Morgen aus
class Gruss
{
public static void main(String[] args)
{
System.out.println("Guten Morgen");
}
}
Es gibt den einen oder anderen Unterschied zu dem entsprechenden Programm
in C, aber so gewichtig sind sie nicht. Zunächst fällt auf, dass das Programm
mit einer Art von Klassendeklaration beginnt, was in C bisher nicht vorkam.
Daran sehen Sie schon den ersten Hinweis auf die vielbeschworene Objektori-
entierung: Objekte und Klassen haben eine sehr enge Beziehung, auf die ich
später noch zu sprechen kommen werde. Da Java eine ausgesprochen streng
objektorientierte Sprache ist, muss alles, was geschieht, in eine Klasse einge-
bettet sein, die einen Namen braucht. Üblicherweise lässt man den Namen der
Klasse mit einem Großbuchstaben beginnen, aber das ist nur Konvention und
keine zwingende Vorschrift. Die Klasse, um die es hier geht, heißt also Gruss,
und das gleiche Wort Gruss finden Sie auch gleich in der ersten Zeile des Pro-
grammtextes, die Sie wahrscheinlich schon als Kommentarzeile erkannt haben.
Solange ein Kommentar nur eine Zeile lang sein soll, genügt es, ihn mit den
beiden Schrägstrichen // beginnen zu lassen, aber wirklich nur bei einzeiligen
Kommentaren. Sie haben allerdings auch die Möglichkeit, einen Kommentar
über mehrere Zeilen laufen zu lassen, indem Sie den Kommentaranfang mit
/* und das Kommentarende mit */ kennzeichnen. Wo haben Sie das schon
3.1 Strukturierte Programmierung mit Java 209

mal gesehen? Richtig, bei den Kommentaren in der Sprache C, schon zeigen
sich die ersten Ähnlichkeiten.
Jedes C-Programm musste ich in einer Datei mit der Dateiendung .c ab-
speichern. Es liegt nahe, dass man Java-Programme in Dateien speichert, die
auf .java enden, und genau das ist auch der Fall. Aber Vorsicht: die Klas-
se heißt Gruss, und deshalb sollte die Datein auch den Namen Gruss.java
tragen. Man kann von dieser Regel abweichen, sollte es aber möglichst ver-
meiden; was bei einer Abweichung passiert, erkläre ich Ihnen später. Ich gehe
jetzt erst einmal davon aus, dass Sie Ihr Programm, also Ihre Klasse Gruss,
in der Datei Gruss.java abspeichern. Innerhalb der anschließenden Mengen-
klammern wird dann die Klasse implementiert, also die eigentliche Ablauflogik
festgelegt. Der eigentliche Inhalt des Programms beginnt mit der Zeile
public static void main(String[] args)
Auch hier fällt Ihnen sicher die Ähnlichkeit zu C auf, wo ich in jedem Pro-
gramm eine main()-Funktion haben musste. Analog dazu braucht auch jede
Java-Applikation eine main()-Funktion, nur dass man sie in Java nicht mehr
eine Funktion nennt, sondern, wie in der objektorientierten Programmierung
üblich, eine Methode. Lassen Sie sich von dem Namen nicht verwirren, eine
Methode ist beim derzeitigen Stand der Dinge nichts anderes als eine Funkti-
on. Wichtig ist, dass jede Java-Applikation eine main()-Methode haben muss,
die die Rolle des Hauptprogramms spielt, weil ansonsten der Compiler keinen
Einstiegspunkt findet. Deshalb ist main() auch immer die erste Funktion, die
beim Start einer Applikation aufgerufen wird.
Dummerweise steht im Kopf der main()-Methode auch noch ziemlich viel
anderes Zeug, was einen beim ersten Hinschauen schon ein wenig irritieren
kann. Machen Sie sich nichts daraus, das steht da immer, und wenn Sie es
ein paar Mal geschrieben haben, beherrschen Sie den Kopf von main() im
Schlaf. Trotzdem sollten Sie eine Vorstellung davon haben, was Sie da eigent-
lich schreiben. Da wäre erst einmal das Schlüsselwort public. Es bedeutet ge-
nau das, was seine Übersetzung sagt, nämlich öffentlich“. Jede Java-Methode

kann man als öffentlich oder nicht-öffentlich, also privat, kennzeichnen, aber
Sie müssen nur einmal kurz nachdenken, um festzustellen, dass eine Einstiegs-
methode wie main(), die man auf jeden Fall anläuft, auf keinen Fall nicht-
öffentlich sein darf: schließlich muss irgendwer das Programm starten und
benutzen, und wenn schon die Startmethode in irgendeiner Weise privat, also
nicht jedem zugänglich wäre, dann könnte der Start ganz schnell schief gehen.
Natürlich zeige ich Ihnen später genauer, wie man so etwas steuert; für den
Moment genügt die Information, dass man eine als public gekennzeichne-
te Methode immer verwenden darf und dass die main()-Methode immer als
public qualifiziert sein muss, damit der Einstieg in das Programm gelingt.
Wir kommen voran. Das nächste Schlüsselwort lautet static, und auch
das wird Ihnen auf den folgenden Seiten noch öfter begegnen. Eine Methode
kann durch die Angabe von static als statische Methode oder auch Klas-
senmethode gekennzeichnet werden, aber das hilft Ihnen jetzt wahrscheinlich
auch nicht viel weiter. Hier muss ich ein wenig vorgreifen, um Sie nicht schon
210 3 Objektorientierte Programmierung mit Java

wieder auf die Zukunft zu vertrösten. Ich hatte bereits erwähnt, dass Klassen
und Objekte eng miteinander zusammenhängen: eine Klasse wird sich als eine
Menge gleichartiger Objekte herausstellen, mit denen man dies und das an-
stellen kann. Sie hatten auch schon ein Beispiel gesehen, nämlich die Klasse
der Minigolfbälle, die man schlagen, wärmen und abkühlen kann. Nun sind
diese Aktivitäten nichts anderes als die Methoden, die man auf einen Mini-
golfball üblicherweise anwendet, ohne einen Minigolfball sind sie sinnlos. Sol-
che Methoden, die ein Objekt brauchen, um überhaupt ausgeführt werden zu
können, sind gewissermaßen die normalen Methoden eines objektorientierten
Programms. Man kann sich aber auch andere Methoden vorstellen, denen die
Objekte völlig egal sind. Wenn Sie beispielsweise die Klasse der Minigolfbälle
mit einem Ballkoffer identifizieren, der alle ihre Bälle enthält, dann werden
Sie irgendwann den Koffer öffnen. Dabei ist es dem Koffer völlig egal, ob er
mit Bällen überfüllt oder so leer ist wie der Kopf meines Lieblingsnachbarn:
öffnen können Sie ihn auf alle Fälle. Die Einstiegsmethode des Kofferöffnens
ist also nicht an die Existenz eines Balls in diesem Koffer gebunden, kann sie
auch gar nicht sein, denn sonst wäre es nie möglich, einen Ball in einem bisher
leeren Koffer zu deponieren. Und solche Methoden, die kein existierendes Ob-
jekt brauchen, sondern schon funktionieren, wenn man nur die Klasse selbst
hat, nennt man Klassenmethoden oder auch statische Methoden.
Offenbar muss die main()-Methode statisch sein. Beim Einstieg in die
Applikation kann ich ja noch gar kein Objekt haben, denn alle Aktivitäten
sollen schließlich innerhalb der Applikation stattfinden. Also habe ich keine
andere Wahl, als die main()-Methode als statisch zu qualifizieren.
Einfacher ist das nächste Schlüsselwort void zu erklären, denn das kennen
Sie schon. Die main()-Methode kann und darf keinen Rückgabewert haben,
und das regelt man durch die Angabe des leeren Datentyps void.
Innerhalb der runden Klammern werden einer Funktion, wie Sie im letz-
ten Kapitel gelernt haben, die Formalparameter mitgegeben. Im Gegensatz
zur main()-Funktion in C, die auf Parameter keinen Wert legt, muss die
main()-Methode in Java einen Formalparameter haben, den ich hier als args
bezeichnet habe. Dieses args steht für arguments“ und soll ein Feld aus

Zeichenketten sein: Zeichenketten werden in Java als Strings bezeichnet und
gehören dem Datentyp String an, und durch die Angabe von String[] macht
man klar, dass es sich nicht nur um einen einzigen String handelt, sondern um
ein ganzes Feld von Zeichenketten. Weglassen dürfen Sie diesen Formalpara-
meter nicht. Der Compiler würde es zwar zulassen und so tun als hätten Sie
ein fehlerfreies Programm, aber beim Ablauf des Programms würde die JVM
die main()-Methode nicht als die richtige main()-Methode erkennen, weil die
nun mal den beschriebenen Formalparameter braucht. Wozu er gut ist, zeige
ich Ihnen gleich, sobald wir das Zusammenspiel von Kompilierung und Ablauf
des Programms besprechen.
Der Aufbau des Kopfes der main()-Methode ist sicher etwas gewöhnungs-
bedürftig, aber nicht weiter schlimm. Hat man ihn zweimal aufgeschrieben,
kennt man ihn auswendig, und da er immer gleich ist, werden Sie sich darüber
3.1 Strukturierte Programmierung mit Java 211

sehr schnell keine Gedanken mehr machen. Das Programm selbst hat dann nur
eine einzige Anweisung, die der Ausgabe des Guten Morgen“-Grußes dient.

Mit System.out.println() geben Sie den String, der in der runden Klam-
mer angegeben ist, am Bildschirm aus und wechseln anschließend vorsorglich
die Zeile. Warum das Kommando so seltsam heißt, werden Sie verstehen,
wenn wir über Streams zur Eingabe und Ausgabe reden; für den Augenblick
genügt es zu wissen, dass die Standardausgabe auf dem Bildschirm mit diesem
System.out.println() erfolgt.
Programmiert ist der Gruß nun, und was jetzt? Jetzt muss er kompiliert
werden, und zu diesem Zweck brauchen Sie einen lauffähigen Java-Compiler
auf Ihrem Computer. Sun Microsystems selbst stellt beispielsweise das Java
Development Kit, abgekürzt JDK, zur Verfügung, ein Werkzeug zur Erstel-
lung von Java-Programmen, das Sie kostenlos über das Internet herunterladen
können und das sowohl den Java-Compiler als auch die JVM enthält. Dieses
JDK hat keine graphische Benutzeroberfläche, von der aus Sie Kompilierung
und Ablauf per Mausklick starten könnten. Sie müssen daher die entsprechen-
den Komandos von Hand eingeben, und das heißt, dass Sie bei Einsatz eines
Windows-Rechners die DOS-Oberfläche bemühen müssen und bei Verwen-
dung des Betriebssystems UNIX ein Kommando über die so genannte Shell
abgeben werden. Das Komando zum Kompilieren lautet in unserem Fall
javac Gruss.java
und nach diesem Schema kompiliert man Java-Programme immer. Mit javac
rufen Sie den Java-Compiler auf, der die nachstehend angegebene Datei in
Bytecode übersetzen soll. Vergessen Sie nie, die Endung java aufzuschreiben:
man sollte zwar denken, dass ein Java-Compiler schlau genug ist, nur nach
Dateien mit der Endung java zu suchen, aber von alleine schafft er das nicht.
Falls Ihr Programm syntaktisch korrekt war, wird die Übersetzung erfolg-
reich sein und den gewünschten Bytecode liefern. Die zugehörige Datei heißt
auch Gruss, trägt aber nicht die Endung java, sondern class. Sobald also Ihr
Programm erfolgreich übersetzt worden ist, sollte sich in Ihrem aktuellen Ar-
beitsverzeichnis die Datei Gruss.class finden, die Sie anschließend ausführen
können. Das JDK-Kommando zur Ausführung von erzeugtem Bytecode lautet
schlicht java, und im Gegensatz zu javac verlangt es keine Endungsangabe.
Es reicht also völlig aus, auf der DOS-Oberfläche oder der UNIX-Shell das
Kommando java Gruss loszuwerden, und schon sollte der freundliche Gruß
Guten Morgen“ auf dem Bildschirm erscheinen.

Noch ein paar Worte zu dem einen oder anderen offenen Punkt. Ich hat-
te erwähnt, dass der Name Ihrer Datei, in der Sie die Klasse abspeichern,
dem Klassennamen entsprechen sollte, sodass aus der Klasse Gruss die Datei
Gruss.java wird. Sie können die Datei auch anders nennen, kommen dann
aber unter Umständen in Namensprobleme, denn die durch das Kompilieren
entstandene class-Datei trägt in jedem Fall den Namen der Klasse. Wenn
Sie also Ihre Klasse Gruss zum Beispiel in der Datei Test.java abspeichern,
dann erzeugt der Compilerlauf trotzdem die Klassendatei Gruss.class und
nicht etwa Test.class. Bei Verwendung des klassischen JDK ist das nicht
212 3 Objektorientierte Programmierung mit Java

weiter schlimm, sofern man das weiß. Sobald Sie aber eine komfortable Ent-
wicklungsumgebung einsetzen, bei der Sie Kompilierung und Ausführung per
Mausklick starten können, ist die Gefahr groß, dass das System durcheinan-
der kommt und eine Datei Test.class sucht, die es natürlich nicht finden
kann. Es ist daher empfehlenswert, an der Regel Klassenname = Dateiname
festzuhalten.
Und wie war das nun mit dem Formalparameter der main()-Methode?
Meistens kann er Ihnen völlig egal sein; nur wenn Sie das Bedürfnis haben,
beim Start des Programms noch irgendwelche Strings mitzugeben, kommt er
zum Einsatz. Das folgende Programm zeigt, wie.
//Gruss1.java --- gibt Benutzereingaben aus
class Gruss1
{
public static void main(String[] args)
{
System.out.println(args[0]);
System.out.println(args[1]);
}
}
Nach der problemlosen Übersetzung starten Sie dieses Programm beispiels-
weise mit dem Kommando java Gruss1 ding dong und werden feststellen,
dass am Bildschirm die beiden Zeichenketten ding“ und dong“ ausgegeben
” ”
werden. Der Parameter von main() ist also in der Lage, Zeichenketten aufzu-
nehmen, die der Anwender beim Start des Programms als Aktualparameter
mitgegeben hat, und diese Zeichenketten dann bei Bedarf zu verarbeiten. So
etwas wird nur selten gebraucht, denn Eingaben sollten Sie während des Pro-
grammablaufs vornehmen und nicht schon vor dem eigentlichen Programm-
start.

Aufbau eines Java-Programms


Jedes Java-Programm muss in einer Klasse enthalten sein, die durch
class Klassenname deklariert wird. Der Einstieg in eine Applikation findet
immer über die main()-Methode statt, die sowohl public als auch static
sein muss, also für jeden zugänglich ist und eine Klassenmethode darstellt, die
man ohne existierende Objekte verwenden kann. Die Ausgabe am Bildschirm
findet mit dem Kommando System.out.println() statt, das Zeichenketten
am Bildschirm ausgibt.
Der Programmtext wird in einer Datei mit der Endung java abgespei-
chert, deren Name mit dem der Klasse übereinstimmt. Verwendet man das
Java Development Kit (JDK) von Sun Microsystems, so erfolgt die Kompi-
lierung durch das Kommando javac Dateiname.java, die Ausführung durch
java Dateiname.
3.1 Strukturierte Programmierung mit Java 213

3.1.4 Standardeingabe

Bevor ich zu den Kontrollstrukturen komme, die dann allerdings sehr schnell
erledigt sein werden, sollten wir uns kurz mit der Eingabe von der Tastatur
befassen, der so genannten Standardeingabe. Sie erinnern sich: in C fand sie mit
Hilfe des Kommandos scanf() statt, das auf Formatelemente angewiesen war.
Solche Formatelemente braucht Java glücklicherweise nicht, aber dafür ist die
Tastatureingabe in Java mit anderen Umständlichkeiten gesegnet. Sehen Sie
sich einmal das folgende Programm an, das eine ganze Zahl von der Tastatur
erwartet, um sie anschließend wieder am Bildschirm auszugeben.
import java.io.*;

class Eingeben
{
public static void main(String[ ] args)
{
int i=0;
BufferedReader in = new BufferedReader(
new InputStreamReader(System.in));

System.out.println("Bitte eine ganze Zahl eingeben");


try
{
String Eingabe = in.readLine();
Integer x = new Integer(Eingabe);
i = x.intValue();
}
catch(Exception e)
{
System.out.println("Unzulaessige Eingabe");
}
System.out.println("Die Zahl lautet " + i);
}
}
Ich gebe es zu: das sieht schon etwas komplizierter aus. Wenn man genauer
hinsieht, wird es dann wieder ganz einfach, aber man kann kaum leugnen,
dass die Tastatureingabe in Java etwas gewöhnungsbedürftig ist. Fangen wir
vorsichtig an.
Dass man etwas einbinden muss, um mit der Ein- und Ausgabe zurechtzu-
kommen, sind Sie von C her gewöhnt. Dort war es die Header-Datei stdio.h,
in Java bindet man keine Dateien ein, sondern ganze Pakete, so genannte
packages, in denen sich normalerweise viele praktische Klassen tummeln. Sol-
che Pakete kann man auch selbst anlegen und seine Programme in passender
Weise auf sie verteilen, aber ich werde mich hier darauf beschränken, vordefi-
nierte Pakete in ein eigenes Programm zu integrieren. Durch die Angabe von
214 3 Objektorientierte Programmierung mit Java

import java.io.* wird in mein Programm nämlich alles eingebunden, was


das Package java.io zu bieten hat, also alle Klassen dieses Pakets. Es reicht
leider nicht, nur import java.io zu schreiben und damit das gesamte Pa-
ket einzubinden, aber durch die Angabe des Sterns hinter dem Paketnamen
erwischen Sie trotzdem alles.
Anschließend wird die Klasse mit dem Namen Eingeben deklariert und die
main()-Methode angegangen, in der zunächst einmal der Benutzer aufgefor-
dert wird, eine ganze Zahl einzugeben. So weit war das noch nicht aufregend.
Die ganze Zahl soll am Ende der int-Variablen i zugewiesen werden, weshalb
ich diese Variable gleich deklariere und ihr sicherheitshalber einen Initialwert
zuweise für den Fall, dass die Eingabe schief geht. Sie sehen daran, dass man
ganze Zahlen genau wie in C mithilfe des Datentyps int deklariert.
Aber jetzt kommt etwas Neues. Durch die Angabe
BufferedReader = in
wird eine Variable in deklariert, die der Klasse BufferedReader angehören
soll - genauso wie ein Minigolfball zu einem bestimmten Ballkoffer gehört.
Und woher kommt diese seltsame Klasse? Ganz einfach: es muss ja irgendet-
was gebracht haben, das Paket java.io mit all seinen Klassen einzubinden,
und eine dieser Klassen ist eben die vordefinierte Klasse BufferedReader.
Sie brauchen sie nicht selbst zu definieren, sie gehört zu den vielen Klassen,
die Java zur Verfügung stellt und mit denen Sie nach Herzenslust arbeiten
können. Etliches von dem, was Sie für die Eingabe brauchen, wird in der
Klasse BufferedReader bereitgestellt - man muss nur wissen, wie man dar-
auf zugreift, und das zeige ich Ihnen jetzt.
Die Variable in - oder genauer: das Objekt in - wird noch in der gleichen
Zeile exakter spezifiziert. Durch die Anweisung
BufferedReader in = new BufferedReader(
new InputStreamReader(System.in));
wird festgelegt, wo die Daten zum Einlesen her kommen sollen. Man muss
das zur Anwendung gar nicht so genau wissen, da es immer das Gleiche ist,
aber einen ungefähren Überblick sollten Sie schon haben. Ein Buffer ist ein
Puffer, und ein BufferedReader ist nichts anderes als ein Objekt zur Pufferung
bestimmter Eingaben. Und was gepuffert werden soll, steht auf der rechten
Seite des Gleichheitszeichens. Unter System.in versteht Java grundsätzlch die
Standardeingabe, also die Tastatur; das ist so vordefiniert und nicht zu ändern.
Jede Eingabe in die Tastatur liefert einen Bytestrom, den ich aber gerne in eine
Zeichenkette umwandeln würde, damit mein Programm damit arbeiten kann.
Deshalb wird alles, was von der Tastatur kommt, einem InputStreamReader
überantwortet, dessen Aufgabe darin besteht, aus dem Bytestrom einen Strom
aus characters zu machen, also ein Feld aus einzelnen Zeichen. Das ist schon
besser als so ein paar hingeworfene Bytes, aber noch nicht gut genug. Damit
man sich nicht mit Feldern aus einzelnen Zeichen plagen muss, kann man ein
solches Feld auch noch puffern, indem man es dem BufferedReader aussetzt,
durch dessen Mitwirkung dann das Zeichenfeld zu einer schönen Zeichenkette,
also einem Element vonm Typ String wird.
3.1 Strukturierte Programmierung mit Java 215

Das ist umständlich, aber immer das Gleiche. Das Objekt in gehört zur
Klasse BufferedReader und ist durch die Angaben nach dem Gleichheitszei-
chen so gebaut worden, dass es in der Lage ist, eine Eingabe von der Tastatur
als eine Zeichenkette, also einen String, zu verstehen. Damit dieser String auch
wirklich den Weg von der Tastatur zu meinem Java-Programm findet, muss
ich nur noch das Kommando
String Eingabe = in.readLine();
absetzen, und schon wird in die Zeichenkette Eingabe das eingelesen, was ich
in die Tasten gehauen habe.
Ich weiß, was Sie jetzt denken, und Sie haben recht. Hätte man nicht ein
Kommando stricken können, das den ganzen Kram auf einmal erledigt und
dem Programmierer die Arbeit abnimmt? Hätte man, hat man aber nicht.
Damit müssen Sie leben, und es ist auch gar nicht so schlimm, weil jede Ein-
gabe nach diesem Muster funktioniert. Wann immer Sie eine Zeichenkette
von der Tastatur in Ihr Programm transportieren wollen, definieren Sie ein
BufferedReader-Objekt auf die angegebene Weise und schicken dann die dort
gepufferte Eingabe über readline() an einen String. Ob das Objekt dabei
in oder ding oder sonstwie heißt, ist natürlich ganz egal, Hauptsache es ent-
stammt der Klasse BufferedReader und ruft die readLine()-Methode auf.
Bei dem Objektnamen ding hätten Sie also den Aufruf
String Eingabe = ding.readLine();
abzusetzen, und alles ist in Ordnung.
Man macht das einmal von Hand und kopiert es dann, wenn man es wieder
braucht. Und auch an das unvertraute try in Verbindung mit catch werden
Sie sich schnell gewöhnen, denn das ist etwas ausgesprochen Praktisches. Was
passiert, wenn Sie in einem C-Programm eine ganze Zahl eingeben sollen,
aber statt dessen einen Buchstaben präsentieren? Sie müssen damit rechnen,
dass das Programm abbricht. Um diesen etwas brachialen Abgang zu verhin-
dern, hat man sich für Java eine Art Versuchskonzept ausgedacht, und weil
versuchen“ auf Englisch try“ heißt, ist so das try-Kommando entstanden.
” ”
Sie sehen, dass nach der try-Anweisung ein Block mit einer Mengenklammer
geöffnet und irgendwann wieder geschlossen wird. Mit try wird nun getestet,
ob die Anweisungen in diesem Block ausgeführt werden können. Falls ja, ist
alles in Ordnung und die Anweisungen werden tatsächlich ausgeführt. Falls
nein, bricht aber das Programm nicht ab, sondern fängt einfach den Fehler
ab - und weil fangen“ auf Englisch catch“ heißt, wird der abfangende Block
” ”
der catch-Block genannt. Er wird nur dann ausgeführt, wenn im try-Block
etwas nicht funktioniert hat. Über die Formulierung catch(Exception e)
brauchen Sie sich jetzt noch keine Gedanken zu machen, man kann sie immer
zum Start eines catch-Blockes verwenden. Sie dürfen nur, wenn es um Ein-
gabe und Ausgabe geht und Sie Klassen aus dem Paket java.io einsetzen,
nicht die try-catch-Konstruktion vergessen, denn ohne try und catch spielt
der Java-Compiler bei Ein- und Ausgaben nicht mit.
Das mag umständlich aussehen, ist aber in Wahrheit sehr praktisch. Denn
was passiert, wenn Sie in einem C-Programm eine ganze Zahl als Eingabe
216 3 Objektorientierte Programmierung mit Java

erwarten und dummerweise einen Buchstaben eingeben? Das Programm wird


sich verabschieden, weil es damit nicht zurecht kommt. In Java kann so etwas
nicht vorkommen, die Reaktion des Programms auf falsche Eingaben können
Sie mit Hilfe des catch-Blockes selbst bestimmen, und sobald der catch-Block
abgearbeitet ist, macht das Programm einfach mit den nächsten Anweisungen
weiter.
Die Anweisungen im catch-Block meiner Klasse Eingeben sind hier nicht
der Rede wert; es wird nur ausgegeben, dass die Eingabe unzulässig war.
Anders sieht das im try-Block aus, denn hier findet die eigentliche Verarbei-
tung statt. Mit der readLine()-Methode konnte ich meine Tastatureingabe
als Zeichenkette in dem String Eingabe speichern. Anders geht das nicht, alle
Tastatureingaben werden bestenfalls als Zeichenkette angesehen, die man ei-
nem String zuweisen kann. Aber ich wollte doch eine ganze Zahl haben, und
eine Zeichenkette ist keine ganze Zahl. Die nächsten beiden Programmzeilen
sind dafür da, mit Hilfe der vordefinierten Klasse Integer aus der Zeichen-
kette eine Zahl zu machen. Sie können mit der Anweisung
Integer x = new Integer(Eingabe);
dafür sorgen, dass der String Eingabe umgesetzt wird in ein Objekt x der
Klasse Integer, das fast eine normale ganze Zahl ist - allerdings nicht ganz,
denn ganze Zahlen haben einfach den Datentyp int. Was es genau mit dieser
Hüllenklasse integer auf sich hst, werde ich Ihnen im Abschnitt über Klas-
sen, Methoden und Objekte erzählen. Im Augenblick ist nur wichtig, dass Sie
einen String in ein Integer-Objekt verwandeln und aus diesem Objekt den
eigentlichen Inhalt, den Wert der ganzen Zahl, herausholen können, indem
Sie mit i=x.intValue() den tatsächlichen Wert von x der int-Variablen i
zuweisen. Natürlich kann das nur gut gehen, wenn in der Zeichenkette, die
Sie eingegeben haben, auch tatsächlich eine ganze Zahl steht und nicht etwa
irgendein Buchstabe. Aber genau für dieses Problem ist ja die try-catch-
Konstruktion da: wenn es bei der Umwandlung in eine ganze Zahl irgendein
Problem gibt, tritt automatisch der catch-Block in Kraft und die Sache hat
sich.

Tastatureingabe in Java
Die Eingabe von der Tastatur wir mithilfe eines Objekts der vordefinierten
Klasse BufferedReader vorgenommen. Von der Standardeingabe System.in
wird die Eingabe einem InputStreamReader übertragen, dessen Aufgabe dar-
in besteht, aus dem Bytestrom einen Strom aus characters zu machen, also
ein Feld aus einzelnen Zeichen. Dieses Feld wird dem BufferedReader-Objekt
übergeben und kann dann einer String-Variablen zugewiesen werden, wobei
die Methode readLine() angewendet wird.
Um die Ein- und Ausgabeoperationen vornehmen zu können, muss das
Paket java.io durch das Kommando import java.io.* eingebunden wer-
den. Weiterhin müssen mögliche Fehler durch den Einsatz der try-catch-
Konstruktion aufgefangen werden.
3.1 Strukturierte Programmierung mit Java 217

Bei der Eingabe ganzer Zahlen muss der eingegebene String in eine ganze
Zahl umgewandelt werden. Das geschieht mit Hilfe der vordefinierten Klasse
Integer und der Methode intValue().

Viele Erklärungen für ein kurzes Programm, ich weiß. Genau genommen
haben wir aber das Schlimmste jetzt schon hinter uns; ab jetzt wird es weni-
ger um Details und mehr um Konzepte gehen. Die Eingabe muss ich trotzdem
noch zu Ende erklären, denn bisher können Sie nur ganze Zahlen eingeben,
und es gibt ja auch noch Zeichenketten und reelle Zahlen. Das ist jetzt aber
nicht mehr schwierig. Zeichenketten können Sie schon eingeben, denn jede
Tastatureingabe, die Sie über die Klasse BufferedReader an Ihr Programm
schicken, wird ja sowieso ein String. In diesem Fall lautet also der try-Block
nur
try
{
String Eingabe = in.readLine();
}
Und schon steht auf Eingabe Ihre Eingabe. Auch bei reellen Zahlen ist es
nicht viel schwerer. Der nötige Datentyp heißt double, die zugehörige Hüllen-
klasse entsprechend Double, und somit komme ich zu dem try-Block:
try
{
String Eingabe = in.readLine();
Double x = new Double(Eingabe);
a = x.doubleValue();
}
Dabei müssen Sie natürlich vorher eine Deklaration double a=0 vorgenom-
men haben, damit Ihre Eingabe auch in einer reellen Variablen landet.

Eingabe von Zeichenketten und reellen Zahlen


Die Eingabe von Zeichenketten erfolgt direkt über die readLine()-Methode
der Klasse BufferedReader. Bei der Eingabe reeller Zahlen muss der ein-
gegebene String in eine reelle Zahl umgewandelt werden. Das geschieht mit
Hilfe der vordefinierten Klasse Double, der Methode doubleValue() und dem
Datentyp double.

3.1.5 Kontrollstrukturen
Nach all diesen neuen und ungewohnten Details, mit denen ich Sie eben plagen
musste, kommen wir jetzt zu etwas Altvertrautem, nämlich den Kontrollstruk-
turen Sequenz, Auswahl und Wiederholung. Ihre Realisierung in C hatte ich
ausführlich besprochen, und es gibt, was Java betrifft, eine gute Nachricht:
man programmiert die Kontrollstrukturen in Java genauso wie Sie es von C
her kennen. Sequenzen haben Sie ohnehin schon gesehen, jede Abfolge einzel-
ner Kommandos ist eine Sequenz, und man schreibt sie einfach nacheinander
218 3 Objektorientierte Programmierung mit Java

hin. Aber auch eine Auswahl funktioniert schlicht ganz genauso wie in C,
weshalb ich auch nicht viele Worte darüber verlieren will. Sie können auch in
Java die einfache Form
if (Bedingung) Aktion
einsetzen, Sie können mit
if (Bedingung)
Aktion 1
else
Aktion 2
eine Alternative ins Spiel bringen, und niemand hindert Sie daran, auch die
gute alte Konstruktion
if (Bedingung 1)
Anweisungen 1
else if (Bedingung 2)
Anweisungen 2
else if (Bedingung 3)
Anweisungen 3
else
Anweisungen 4
anzuwenden. Im Vergleich zu C haben Sie in Java allerdings eine deutliche
Verbesserung beim Formulieren von Bedingungen. Java kennt nämlich den
primitiven Datentyp boolean, der seinen Variablen nur die Ausprägungen
true oder false erlaubt. Sie dürfen daher so etwas wie das Folgende schrei-
ben.
boolean weiter;
if (x>0)
weiter = true;
else
weiter = false;
Mit der boolschen Variablen weiter können Sie dann über die Wahrheitswerte
den weiteren Ablauf des Programms steuern.
Und falls Sie schon nervös geworden sind: auch das alte switch-case hat
man in Java wie in C aufgebaut. Die gesamte Auswahl funktioniert einfach
genau wie in C, und mehr ist dazu nicht zu sagen. Doch, eines schon, aber
das bezieht sich nicht auf die Auswahl an sich, sondern auf die Bedingungs-
abfrage. Auch die macht man fast durchgängig so wie in C, Sie können also
wie gewohnt auf Gleicheit oder Ungleichheit, größer oder kleiner abfragen und
Ihre Bedingungen mit && und || verknüpfen. Nur bei Strings gibt es einen
Unterschied. Um zwei Strings miteinander zu vergleichen, sollten Sie nicht das
gewohnte == einsetzen, denn das geht nur selten gut. Dafür gibt es wieder mal
eine vordefinierte Methode namens equals(), die nichts anderes macht als
zwei Zeichenketten miteinander zu vergleichen. Haben Sie also zwei Strings
kette1 und kette2, die Sie auf Gleichheit testen wollen, dann lautet die Ab-
frage:
3.1 Strukturierte Programmierung mit Java 219

if (kette1.equals(kette2))
und leider nicht if (kette1 == kette2).

Auswahl
Die Auswahl funktioniert in Java mit genau den gleichen if-, if-else- und
switch-case-Konstruktionen wie in C. Ausnahme ist die Abfrage auf die
Gleichheit zweier Strings, die mit der Methode equals() durchgeführt werden
muss.

Damit ist die Auswahl auch schon erledigt und wir können zur Wiederho-
lung übergehen, zur Iteration. Wundert es irgend jemanden, dass Sie schon
alles Nötige über die Iteration wissen? Man führt sie in Java genauso durch
wie schon in C, es gibt eine do-Schleife, eine while-Schleife und eine for-
Schleife, mit denen Sie auf die vertraute Weise umgehen können. Nur bei der
for-Schleife sollte ich noch auf eine Erweiterung hinweisen, die in C nicht
möglich ist. Sehen Sie sich einmal das folgende kleine Programmstück zur Be-
rechnung der Summe der ersten zehn natürlichen Zahlen an.
int summe = 0, i;
for(i=1; i<=10; i++)
summe = summe + i;
Ist das C oder Java? Keine Ahnung, es könnte beides sein. Aber wie sieht es
mit dem folgenden Programmstück aus?
int summe = 0;
for(int i=1; i<=10; i++)
summe = summe + i;
Der Unterschied ist nicht groß, aber deutlich. In Java (und übrigens auch in
C++) ist es möglich, die Laufvariable der for-Schleife erst im Schleifenkopf
selbst zu deklarieren und nicht schon vorher. Das sorgt für etwas mehr Ord-
nung im Programmablauf, denn eine Laufvariable gehört zu einer Zählschleife
und sollte genau genommen außerhalb dieser Schleife keine Existenzberechti-
gung mehr haben. Wenn Sie nämlich in der ersten Variante nach der Schleife
den Wert von i ausgeben lassen, dann wird das problemlos erledigt, weil i
nicht innerhalb der Schleife definiert wurde und daher auch außerhalb der
Schleife bekannt ist. In der zweiten Variante ist das anders. Ein nachfolgendes
Kommando wie System.out.println(i) führt schon beim Kompilieren zu
einem Fehler, denn die Laufvariable i wurde im Schleifenkopf definiert und
ist deshalb nur innerhalb der Schleife bekannt.

Schleifen
Java kennt wie C und C++ die do-Schleife, die while-Schleife und die for-
Schleife, die syntaktisch so aufgebaut sind wie in C. Bei der for-Schleife kann
man Schleifenvariablen innerhalb des Schleifenkopfes deklarieren, was dazu
führt, dass sie nur innerhalb der Schleife bekannt sind und außerhalb der
Schleife nicht verwendet werden können.
220 3 Objektorientierte Programmierung mit Java

Sehen wir uns einmal ein komplettes Java-Programm an: die Dollar-Euro-
Umrechnung, die Sie schon in der Sprache C gesehen haben.
import java.io.*;
class Umrechnen
{
// Umrechnen.java -- waehrungen umrechnen
final static double DNACHE=0.76336;

public static void main(String[] args)


{
double euro=0, dollar=0;
String antwort="j", eingabe;
BufferedReader in = new BufferedReader(
new InputStreamReader(System.in));
do
{
System.out.println("Bitte Dollar-Betrag eingeben:");
try
{
eingabe = in.readLine();
Double x = new Double(eingabe);
dollar = x.doubleValue();
}
catch(Exception e)
{
System.out.println("Fehlerhafte Eingabe");
}
euro = dollar * DNACHE;
System.out.println(dollar + " Dollar sind "
+ euro + " Euro");
System.out.println("Weitere Umrechnung?(j/n)");
try
{
antwort = in.readLine();
}
catch(Exception e)
{
System.out.println("Fehlerhafte Eingabe");
}
}
while(antwort.equals("j") || antwort.equals("J"));
System.out.println("Feierabend");
}
}
3.1 Strukturierte Programmierung mit Java 221

An diesem Programm finden Sie kaum etwas Neues, was Ihnen zeigen soll-
te, wie weit Sie schon in der Sprache Java fortgeschritten sind. Es wird eine
Klasse Umrechnen deklariert, in die alle Klassen des Pakets java.io einge-
bunden werden sollen. Gleich zu Beginn der Klasse habe ich - analog zu C,
aber mit einem anderen Befehl - eine Konstante definiert, und Konstanten
kennzeichnet man in Java mit dem Schlüsselwort final: ist ja auch passend,
denn eine Konstante soll ihren Wert nicht mehr ändern, hat also bereits ih-
ren finalen“ Wert angenommen. Da meine Variable DNACHE an kein Objekt

der Welt gebunden ist, muss sie, wie Sie das schon bei der main()-Methode
gesehen haben, statisch sein.
Nach dem Anlegen der Konstante beginnt meine Hauptmethode, zu der
ich nicht viel sagen muss, denn die Logik entspricht dem Umrechnungspro-
gramm aus dem C-Kapitel, und die Java-Sprachelemente haben wir schon
besprochen. Nur eines ist hier neu, nämlich ein Detail bei der Ausgabe am
Bildschirm. Das Kommando System.out.println() gibt grundsätzlich Zei-
chenketten aus und hat den Vorteil, dass es auszugebende Zahlen automatisch
in Zeichenketten umwandelt. Wenn Sie aber mehrere Zeichenketten in einem
Kommando ausgeben wollen, dann brauchen Sie sie nur mit Hilfe eines +-
Zeichens aneinanderzuhängen, und schon ist alles erledigt. Genau das habe
ich in der Programmzeile
System.out.println(dollar + " Dollar sind " + euro + " Euro");
gemacht.
Eine weitere Sicherheitsmaßnahme kann man in das Programm noch ein-
bauen. Wenn Sie das Bedürfnis haben, Ihr Programm abbrechen zu lassen,
sobald ein Eingabefehler vorgekommen ist, weil der Benutzer es dann nicht
besser verdient hat, können Sie das durch den Einbau eines Abbruchkomman-
dos in den catch-Block garantieren. Er lautet dann:
catch(Exception e)
{
System.out.println("Fehlerhafte Eingabe");
System.exit(0);
}
Mit dem Kommando System.exit(0) sorgen Sie für eine geregelte Beendi-
gung des Programms, die genau an der Stelle durchgeführt wird, an der Sie sie
einprogrammiert haben - also entscheidet nicht der Compiler darüber, wann
das Programm abbricht, sondern Sie, der Programmierer.
Sie haben den letzten Beispielen entnehmen können, dass die Arithmetik in
Java nicht anders behandelt wird als in C: die Operatoren sind die gleichen wie
immer, da gibt es keinen Unterschied. Na ja, fast keinen. Eine Besonderheit
stellt die Division durch null dar, von der Sie hoffentlich gelernt haben, dass sie
streng verboten ist. Ist sie ja auch, aber Java geht auf etwas eigenartige Weise
mit diesem Verbot um. Haben Sie beispielsweise eine double-Variable x an-
gelegt und setzen das Kommando x=0.0/0.0 ab, so wird weder der Compiler
mäkeln noch das Programm zur Laufzeit abbrechen. Ausgeben der Variablen
x am Bildschirm liefert dann allerdings keine Zahl, welche sollte das auch sein,
222 3 Objektorientierte Programmierung mit Java

sondern den Wert“ NaN. Dieses NaN steht für Not a Number“, auf Deutsch
” ”
Keine Zahl“. Ich kann der JVM da kaum widersprechen, natürlich ergibt sich

bei der Division durch null keine Zahl, aber sollte man bei einer verbotenen
Operation eine Variable, in der eigentlich eine Zahl zu stehen hat, einfach so
mit dem Wert NaN belasten? Ohne weitere Fehlermeldung? Vielleicht sind Sie
anderer Meinung, mir erscheint das wenig sinnvoll, aber es ist nun mal, wie
es ist. Das gleiche Phänomen tritt übrigens auf, wenn Sie eine Quadratwurzel
aus einer negativen Zahl ziehen wollen.
Immerhin ist Java bei den Divisionsanomalien einigermaßen konsequent.
Das Teilen von null duch null ist ein undefinierter Ausdruck, also ganz sicher
keine Zahl. Ein anderes Ergenis erhalten Sie, wenn Sie x=1.0/0.0 ausrech-
nen lassen wollen: auch das ergibt sicher keine Zahl, aber auch nicht NaN,
sondern schlicht Infinity, also die Unendlichkeit. Sobald also eine von null
verschiedene Zahl durch null geteilt wird, gibt Java das Ergebnis Infinity
oder -Infinity aus, je nach Vorzeichen des Zählers. Eine schlichte Fehler-
meldung hätte es auch getan, aber mich hat ja keiner gefragt.
Und um die Verwirrung vollständig zu machen, behandelt Java ganzzahlige
Divisionen durch null wieder ganz anders. Sollten Sie jemals einer ganzzahli-
gen Variablen so etwas wie 0/0 oder auch 1/0 zuweisen, wird ihr Programm
tatsächlich mit einer Art Fehlermeldung abbrechen, indem es eine so genann-
te ArithmeticException am Bildschirm ausgibt und Ihnen mitteilt, dass Sie
gerade versucht haben, durch null zu teilen. Nicht vergessen: das passiert nur
bei ganzzahligen Rechnungen; reellwertige Rechnungen führen zu den Selt-
samkeiten, die ich weiter oben beschrieben hatte.

NaN und Infinity


Teilt man im Bereich der double Zahlen null durch null, so ergibt sich das
Ergebnis NaN, ebenso beim Ziehen einer Wurzel aus einer negativen Zahl.
Teilt man im Bereich der double Zahlen eine von null verschiedene Zahl duch
null, so ergibt sich das Ergebnis -Infinity oder Infinity. Das ganzzahlige
Dividieren durch null führt dagegen zu einem Programmabbruch.

Die Kontrollstrukturen sowie die Tücken der Ein- und Ausgabe haben wir
damit erledigt. Im nächsten Abschnitt werde ich Ihnen einiges über Klassen,
Objekte und Methoden erzählen.

Übungen

3.1. Schreiben Sie ein Java-Programm, das je nach Auswahl des Benutzers
einen Eurobetrag in einen Dollarbetrag oder einen Dollarbetrag in einen Eu-
robetrag umrechnet.

3.2. Schreiben Sie ein Java-Programm, das Folgendes leistet.


Es ist - nach einem stark vereinfachten Verfahren - die Steuer auf das
monatliche Einkommen auszurechnen. Das Einkommen ist über die Tastatur
3.1 Strukturierte Programmierung mit Java 223

einzugeben. Die ersten 600 Euro sind steuerfrei. Die nächsten 600 Euro werden
mit 30 Prozent besteuert und der Rest mit 40 Prozent. Beträgt allerdings das
gesamte monatliche Einkommen mehr als 15000 Euro, so sind statt 40 Prozent
50 Prozent zu veranschlagen.
3.3. Eine lineare Gleichung hat die Form ax + b = 0 mit den bekannten Koef-
fizienten a und b und der Unbekannten x. Schreiben Sie ein Java-Programm,
das, falls möglich, nach Eingabe der beiden Koeffizienten a und b die Lösung
x = − ab ausrechnet und ausgibt. Achten Sie dabei auch auf die Sonderfälle, die
entstehen können, wenn der eine oder andere Koeffizient den Wert 0 annimmt.
3.4. Erweitern Sie das Programm aus Aufgabe 3.3 in der Form, dass es eine
beliebige Anzahl von linearen Gleichungen lösen kann. Vor jeder neuen Glei-
chung soll der Benutzer gefragt werden, ob er eine weitere Gleichung lösen
will. Bei einer positiven Antwort wird eine Gleichung eingegeben und gelöst,
bei einer negativen wird das Programm beendet.
3.5. Schreiben Sie ein Java-Programm, das eine beliebige Anzahl von quadra-
tischen Gleichungen löst.
Nach dem Programmstart wird der Benutzer aufgefordert, die Koeffizien-
ten p und q der Gleichung x2 + px + q = 0 einzugeben. Sobald diese Eingabe
erfolgt ist, berechnet das Programm sämtliche reellen Lösungen der Gleichung
und gibt sie aus. Falls keine reellen Lösungen existieren, soll das Programm
nur die reelle Unlösbarkeit ausgeben. Nach dieser Ausgabe wird der Benutzer
gefragt, ob er eine weitere Gleichung gelöst haben möchte. Gibt er j oder J
ein, so beginnt das Spiel von Neuem. Gibt er etwas anderes ein, verabschiedet
sich das Programm.
Die Lösungsformel für quadratische Gleichungen lautet

p p2
x1,2 = − ± − q.
2 4
Die Lösungen sind genau dann reell, wenn der Wurzelinhalt nicht kleiner als 0
ist. Die Wurzel aus einer double-Zahl x berechnet√man mit dem Kommando
Math.sqrt(x), das als Ergebnis die double-Zahl x liefert.
3.6. Schreiben Sie ein Java-Programm, das Folgendes leistet.
In einem Labor sind Messreihen zu verarbeiten. Aus Erfahrung weiß man,
dass keine Werte anfallen, die über 1000 bzw. unter −1000 liegen. Dem Benut-
zer liegt nun eine Liste von Messwerten vor, die er in den Computer eingeben
soll, wobei die Eingabe beendet ist, sobald ein Wert über 1000 oder unter
−1000 eingegeben wird. Es ist dabei zu beachten, dass dieser letzte Wert
nicht mehr zur eigentlichen Messreihe gehört, sondern nur dem Abbruch der
Eingabe dient. Das Programm soll dann folgende Größen berechnen und aus-
geben:
• die Summe aller positiven Werte und den Durchschnitt dieser Werte;
• die Summe aller negativen Werte und den Durchschnitt dieser Werte;
• die Summe aller Werte und den Durchschnitt aller Werte.
224 3 Objektorientierte Programmierung mit Java

3.2 Klassen, Objekte und Methoden


Wie Sie schon im letzten Abschnitt gesehen haben, spielt sich in Java alles
innerhalb von Klassen ab - kein Datenelement, keine Aktivität kann sich die-
sem Prinzip entziehen. Ezwas einer Klasse Ähnliches haben Sie bereits im
Zusammenhang mit C kennen gelernt, nämlich die Strukturtypen, von denen
Klassen einiges übernommen haben. Eine Klasse ist nämlich eine abstrakte
Beschreibung eines Objekts mit seinen Daten und Methoden, und das heißt,
in einer Klassendeklaration wird in aller Regel beschrieben, wie die zukünf-
tigen Objekte oder auch Instanzen aussehen sollen , die ich verarbeiten will,
und was man mit ihnen anstellen kann. Dass man Eigenschaften von Elemen-
ten zu einer Einheit zusammenfasst, ist nichts Neues, das haben wir schon
bei den C-Strukturtypen gemacht. Aber dass man auch noch die möglichen
Aktionen gleich mit definiert, ist ein völlig neues Konzept mit weit reichenden
Konsequenzen, und genau das ist ein wesentlicher Punkt der Objektorientie-
rung. In einer einzigen Struktur, eben der Klasse, werden sowohl die Daten
als auch die Operationen vereinigt, die man auf diese Daten anwenden kann
- ein Konzept, das man oft als Kapselung, auf Englisch encapsulation, be-
zeichnet. Dadurch wird sicher gestellt, dass nur die Operationen, die in einer
Klasse definiert wurden, die Daten eines Objekts dieser Klasse ändern können,
sodass also der Zugriff auf Objekte einer Klasse ausschließlich mit Hilfe der
vordefinierten Aktionen, der so genannten Methoden dieser Klasse erfolgen
soll.
Mir ist schon klar, wie abstrakt sich das anhört, aber gerade im Zusammen-
hang mit objektorientiertem Design muss man sich an ein wenig Abstraktion
gewöhnen. Nicht nervös werden, im Folgenden zeige ich Ihnen, was das alles
mit so etwas Konkretem wie Minigolfbällen zu tun hat.

3.2.1 Klassen und Objekte

Ein Minigolfball ist ohne Frage ein Objekt, und dieses Objekt hat bestimmte
Eigenschaften, die man aufschreiben kann. Er hat eine Marke, eine Farbe und
ein Gewicht, wie jeder andere Körper. Und dazu weist er noch eine bestimmte
Geschwindigkeit auf, denn der eine Ball läuft auf einer Bahn etwas schneller,
der andere etwas langsamer, je nachdem wie das Ballmaterial zu der Bahn
passt. Jeder Ball hat diese Eigenschaften, also kann ich eine Klasse Ball
bilden, die mir genau diesen Sachverhalt in eine Javaklasse übersetzt. Die
Klasse lautet dann:
class Ball
{
String marke;
String farbe;
int gewicht;
String geschwindigkeit;
}
3.2 Klassen, Objekte und Methoden 225

Der Name der Klasse fängt mit einem Großbuchstaben an, und das ist kein
Zufall: Klassennamen sollten Sie immer mit einem Großbuchstaben beginnen
lassen, das ist zwar nicht zwingend, aber eine weit verbreitete Konvention. In
jedem Fall ist diese Klasse zunächst nicht viel anderes als ein Beispiel eines
Strukturtyps, wie Sie ihn aus C kennen. Es wird also ein eigener Datentyp
definiert, mit dessen Elementen Sie hinterher nach Belieben hantieren können.
Aber was sind das für Elemente? Die Vermutung liegt nahe, dass es sich
hier um die schon lange angekündigten Objekte handelt, und genauso ist es
auch. Objekte der gleichen Art, die man durch die gleichen Eigenschaften
beschreiben kann, werden in einer Klasse zusammengefasst; die Klasse ist
sozusagen der große Topf, in dem alle Objekte der gleichen Art schwimmen.
Man nennt deshalb die Objekte auch Instanzen der Klasse.
Die Eigenschaften der Objekte der Klasse Ball habe ich schon aufgelistet:
ein Ball wird beschrieben durch Marke, Farbe, Gewicht und Geschwindigkeit.
Im Programm habe ich das ausgedrückt durch vier so genannte Memberva-
riablen oder auch Attribute, deren einzige Rolle es ist, die nötigen Eigenschaf-
ten der Objekte aufzunehmen. Ich musste daher die vier Membervariablen
marke vom Typ String, farbe vom Typ String, gewicht vom Typ int und
schließlich geschwindigkeit vom Typ String anlegen: ohne Variablen keine
Eigenschaften, wo sollte ich die Eigenschaften sonst hinschreiben?
Nun habe ich also die Eigenschaften der Klasse Ball festgelegt, nach denen
sich in Zukunft jeder Ball richten muss. Die Klasse ist aber nur ein Sammel-
behälter, ein Container, in den jeder Ball hineinpasst; durch das Anlegen der
Klasse wird noch kein einzigews Objekt kreiert. Nur weil Sie einen Ballkoffer
kaufen, wird sich noch kein einziger Ball in den Koffer verirren, es sei denn,
Sie kaufen einen und legen ihn hinein. Bei Objekten ist das nicht anders, man
muss sie erst anlegen, bevor es sie gibt. Ein Objekt der Klasse Ball können
Sie nun durch
Ball meinBall;
deklarieren, aber damit existiert es genau genommen immer noch nicht, son-
dern Sie teilen dem Compiler nur mit, dass meinBall ein Objekt der Klas-
se Ball werden soll. Das ist wie in der Sprache C, als wir noch zusätzlich
den nötigen Speicherplatz anlegen mussten, nur dass es hier deutlich einfa-
cher funktioniert. Ist das Objekt meinBall erst einmal deklariert, genügt das
Kommando
meinBall = new Ball();
um das gewünschte Objekt tatsächlich mit dem benötigten Speicherplatz zu
versehen und damit das eigentliche Anlegen zu erledigen. Der erste Teil ist
also die Deklaration, in der ein Objekt angekündigt wird, der zweite dagegen
die echte Definition, in der das Objekt konkret erzeugt wird. Sie können aber
auch alles in einem Schritt erledigen, indem Sie gleich
Ball meinBall = new Ball();
schreiben, und auf diese Weise Deklaration und Definition auf einmal hinter
sich bringen. Dass der Objektname hier mit einem kleinen Buchstaben be-
226 3 Objektorientierte Programmierung mit Java

ginnt, ist übrigens kein Zufall: das sollten Sie immer so machen, auch wenn
es sich nur um eine Konvention handelt.
Nehmen wir also an, Sie haben erstens die Klasse Ball deklariert und zwei-
tens das konkrete Ballobjekt meinBall angelegt. Dann ist das bisher nichts
weiter als ein Ball ohne Eigenschaften, und damit er nicht das gleiche Schick-
sal erleidet wie Musils berühmter Mann ohne Eigenschaften, sollten wir ihm
schnellstens ein paar Eigenschaften verleihen. Das geht ganz einfach mit Hilfe
der Punktnotation, die Sie schon von den C-Strukturtypen her kennen. Das
Ballobjekt heißt meinBall, die erste Membervariable heißt marke. Wenn es
sich nun um einen Ball der Marke Nifo“ handeln soll (die gibt es wirklich,

fragen Sie mal meine Tochter Anna), dann können Sie dem Objekt diese Ei-
genschaft mit dem Kommando
meinBall.marke="Nifo";
zuweisen. Natürlich funktioniert das mit den anderen Attributen nicht anders,
sodass Sie durch die Angaben
meinBall.marke="Nifo";
meinBall.farbe="blaurot";
meinBall.gewicht="40";
meinBall.geschwindigkeit="mittelschnell";
einen blauroten Nifo-Ball definiert haben, der 40 Gramm wiegt und üblicher-
weise eher mittelschnell über die Bahn rollt.
Schön und gut, aber was nützen mir diese Angaben, wenn ich Sie zwar
in die Klasse hinein bringe, aber nie mehr wieder heraus? Das kann nicht
passieren, auch davor bewahrt mich die Punktnotation. Will ich beispielsweise
die Farbe meines Balles meinBall wissen, brauche ich nur das Kommando
System.out.println(meinBall.farbe);
abzusetzen, und schon wird der entsprechende String, also die Membervariable
farbe von meinBall, am Bildschirm ausgegeben.

Anlegen von Klassen und Objekten


Eine Klasse wird deklariert durch die Angabe eines Klassennamens und von
Membervariablen oder auch Attributen in der Form
class Klassenname
{
Membervariable 1;
Mambervariable 2;
.......
}
Dabei sollte der Klassenname mit einem Großbuchstaben anfangen, was aber
nicht zwingend ist.
Ein Objekt der Klasse wird deklariert durch
Klassenname objektname;
und vollständig definiert, also mit Speicherplatz versehen, durch
objektname = new Klassenname();
Beide Arbeitsgänge lassen sich in dem einen Kommando
3.2 Klassen, Objekte und Methoden 227

Klassenname objektname = new Klassenname();


vereinigen. Man bezeichnet ein Objekt einer Klasse auch als eine Instanz der
Klasse.
Auf die Membervariablen eines angelegten Objektes kann man mit der
Punktnotation objektname.Membervariable zugreifen.

Sie wissen nun, wie man Objekte einer bestimmten Klasse anlegt. Erin-
nern Sie sich noch, dass man in C den angeforderten Speicherplatz wieder
freigeben musste? Das ist einer der Vorteile von Java: um die Freigabe von
Speicherplatz müssen Sie sich überhaupt nicht kümmern, das macht die JVM
ganz von alleine. In regelmäßigen Abständen läuft im Hintergrund der so ge-
nannte Garbage Collector, also ein Müllsammler, der alle jemals angelegten
Objekte daraufhin untersucht, ob irgendeine Variable des Programms noch
etwas mit dem konkreten Objekt zu tun hat. Ist das nicht der Fall, wird der
Speicherplatz freigegeben. Und das alles läuft automatisch, ohne Ihr Zutun,
weshalb Sie zwar Ihre Objekte von Hand anlegen, Ihren Müll aber nicht selbst
beseitigen müssen.

3.2.2 Methoden

Das war noch lange nicht alles, was ich Ihnen über Klassen und Objekte zu
sagen habe. Um weiterzukommen, muss ich aber erst einmal einen neuen Be-
griff einführen: die Methode, deren Name schon vorher ab und zu gefallen ist.
Eine Methode ist nichts anderes als eine Funktion, und sie dient dazu, die Ak-
tivitäten zu beschreiben, die mit meinen Objekten möglich sind. Was kann ich
zum Beispiel mit meinen Ballobjekten machen? Es handelt sich nicht um die
echten Bälle, sondern nur um eine Ballverwaltung im Computer, das dürfen
Sie nicht vergessen. Trotzdem wäre es sinnvoll, zu jedem Ball unbürokratisch
eine Beschreibung ausgeben oder seine Farbe ändern zu können. Kein Pro-
blem, wird sofort erledigt, indem ich zwei Methoden definiere, die mir diese
Arbeit abnehmen. Damit Sie sehen, wie eine Methode in eine Klasse integriert
wird, zeige ich Ihnen hier die komplette um die beiden Methoden erweiterte
Klasse.
class Ball
{
String marke;
String farbe;
int gewicht;
String geschwindigkeit;

String beschreiben()
{
return "Der Ball ist ein " + marke + ", seine Farbe ist "
+ farbe + ", sein Gewicht betraegt " + gewicht
+ " Gramm, und er ist " + geschwindigkeit + ".";
228 3 Objektorientierte Programmierung mit Java

void farbeAendern(String farb_par)


{
farbe = farb_par;
}
}
Eigentlich passiert auch hier nicht viel Neues. Methoden sind Funktionen,
die man auf ein Objekt anwendet. Die Methode beschreiben() verlangt kei-
nen Parameter, da sie sich nur auf den aktuellen Ball und sonst auf gar nichts
bezieht; also ist ihre Parameterliste leer. Dagegen soll sie eine den Ball be-
schreibende Zeichenkette ausgeben, braucht also den Rückgabetyp String.
Im Methodenrumpf wird dann angegeben, was die Methode zu tun hat: sie
soll eine Beschreibung des Balles liefern, um den es gerade geht, indem sie sei-
ne Eigenschaften in einem Satz zusammenfasst und an die aufrufende Stelle
zurückgibt. Beachten Sie dabei, dass ich hier nicht die Punktnotation wie zum
Beispiel meinBall.marke verwendet, sondern einfach nur marke geschrieben
habe. Das muss auch so sein, denn ich habe ja noch gar kein Objekt an-
gelegt. Zur Zeit beschreibe ich noch die Klasse mit ihren Membervariablen
und ihren Methoden, Objekte der Klasse existieren noch nicht. So etwas wie
return marke bedeutet also nur: wenn für irgendein Objekt später mal diese
Methode aufgerufen werden sollte, dann gib bitte die Farbe des Balles zurück,
um den es dann gehen wird.
Genauso sieht es bei der zweiten Methode aus. Sie hat in ihrer Parame-
terliste einen Formalparameter namens farb_par, und die Methode soll nur
den Farbwert eines Ballobjektes umsetzen auf die im Parameter mitgegebene
Farbe. Diese Methode liefert nichts zurück an die aufrufende Stelle, sie ändert
nur etwas in einer Membervariablen, und daher hat sie den bekannten und
beliebten Rückgabetyp void, mit dem Sie dem Compiler verraten, dass nichts
zurückgegeben werden soll.
Vielleicht beschleicht Sie mit der Zeit ein leichtes Unbehagen über diese
Klasse. Ich hatte doch gesagt, dass man immer eine main()-Methode braucht,
wo ist die denn geblieben? Da haben Sie schon recht, eine main()-Methode
ist weit und breit nicht zu sehen, und diese Klasse bekommt auch keine. Eine
Klasse der vorgestellten Art bildet einen Rahmen, ein Muster, nach dem sich
alle Objekte zu richten haben, aber sie ist kein Programm, das man ausführen
kann, eben weil es keine main()-Methode gibt. In der Klasse Ball lege ich die
Struktur meiner Balldaten - das sind die Membervariablen - und die mit den
Bällen möglichen Operationen - das sind die Methoden - ab. Ich führe aber
noch keine konkrete Verarbeitung durch, ich sage nur, wie Verarbeitungen
auszusehen haben, wenn sie irgendwann einmal durchgeführt werden. Trotz-
dem können und sollen Sie diese Klasse mit javac Ball.java kompilieren
und feststellen, dass die Übersetzung in Bytecode einwandfrei funktioniert,
sofern Sie sich nicht verschrieben haben. Aber der Versuch, ein Programm
3.2 Klassen, Objekte und Methoden 229

mit dem Kommando java Ball laufen zu lassen, wird schmählich scheitern,
weil die JVM keine main()-Methode finden kann.
Auch wenn Ihnen das jetzt umständlich erscheint: genau darin liegt ein
Vorteil der objektorientierten Programmierung. Man sagt in einer Klasse, wie
die Daten aussehen und wie man mit ihnen umgehen kann, und in einer an-
deren Klasse geht man dann mit den konkreten Objekten um - und zwar
ganz genauso, wie es die Deklaration der ersten Klasse vorschreibt. Um also
jetzt Balldaten konkret verarbeiten zu können, schreibe ich eine neue Klasse
BallTest, in der die so schmerzlich vermisste main()-Methode zu finden ist.
class BallTest
{
public static void main(String[] args)
{
Ball meinBall = new Ball();
meinBall.marke = "Nifo";
meinBall.farbe = "blaurot";
meinBall.gewicht = 100;
meinBall.geschwindigkeit = "mittel";
System.out.println(meinBall.beschreiben());
meinBall.farbeAendern("rot");
System.out.println(meinBall.beschreiben());
}
}
Die Klasse BallTest nützt die Programmierung der Klasse Ball scham-
los aus, und das soll sie auch. Zuerst wird ein Objekt meinBall der Klasse
Ball angelegt. Dann wird dieses Objekt mit Hilfe der Punktnotation mit Le-
ben gefüllt. Anschließend wird System.out.println() aufgerufen, mit dem
man Strings ausgibt, und die Beschreibungsmethode meiner Ballklasse hat
Strings erzeugt. Die Methode beschreiben() kann ich für das konkrete Ob-
jekt meinBall aufrufen, indem ich die Punktnotation verwende; der Aufruf
meinBall.beschreiben() liefert mir den beschreibenden String, der dann
dem Ausgabekommando System.out.println() übergeben wird. Die glei-
che Punktnotation verwende ich beim Ändern der Ballfarbe. Die Methode
farbeAendern() verlangt einen Parameter, also werde ich meinen Ball in Rot
umlackieren lassen. Da farbeAendern() keinen Rückgabewert liefert, kann
ich es einfach so aufrufen und mich darauf verlassen, dass die vorher allge-
mein programmierte Methode jetzt, da sie auf ein konkretes Objekt meinBall
angewendet wird, die Farbe korrekt von blaurot in rot ändern wird. Die an-
schließende erneute Ausgabe der Ballbeschreibung dient der Kontrolle und
zeigt Ihnen, dass die Farbe tatsächlich geändert wurde.
Nach diesem Prinzip lauft es eigentlich immer. Sie definieren innerhalb
einer grundlegenden Klasse, wie Ihre Datenstrukturen aussehen und welche
Operationen damit erlaubt sein sollen. Wer auch immer dann mit Ihrer Ball-
klasse und ihren Objekten hantieren will, muss sich nach dem richten, was
Sie vorher in der Klasse Ball festgelegt haben. Jede weitere Verarbeitung von
230 3 Objektorientierte Programmierung mit Java

Ballobjekten muss mit den Methoden auskommen, die in der Ballklasse zur
Verfügung gestellt werden. Das ist einerseits eine Einschränkung, weil man
sich an dem vorhandenen Methodenbestand orientieren muss. Andererseits
ist es aber auch ein ungeheurer Vorteil, denn wenn diese Methoden anständig
dokumentiert sind, muss man sich für weitere Entwicklungen nicht mehr dar-
um kümmern, wie sie programmiert sind: der Entwickler muss dann nur noch
wissen, welche Parameter sie brauchen und was sie liefern; ihr Innenleben
interessiert nicht mehr. Gerade das macht die objektorientierte Programmie-
rung für größere Projekte so interessant. Ein Entwickler programmiert eine
bestimmte Klasse, legt die Datenstrukturen und Methoden fest und doku-
mentiert Input- und Outputverhalten der Methoden. Jeder andere Entwickler
kann dann auf diese Klasse, ihre Objekte und ihre Methoden zurückgreifen,
ohne sich Gedanken darüber machen zu müssen, wie die Methoden nun im
Einzelnen programmiert sind.

Methoden
Eine Methode ist eine Funktion, die - außer bei statischen Methoden - immer
für ein bestimmtes Objekt ausgeführt wird. Sie wird innerhalb der Klassen-
deklaration festgelegt und kann dann mithilfe der Punktnotation für jedes
Objekt dieser Klasse ausgeführt werden.
Innerhalb einer Klasse legt man also die Datenstruktur und die erlaubten
Methoden fest. Von anderen Klassen und insbesondere auch von der main()-
Methode aus kann dann mit Hilfe der Methoden mit den konkreten Objekten
der ursprünglichen Klasse gearbeitet werden.

Eine Ausnahme bilden die statischen Methoden, denen alle Objekte dieser
Welt völlig egal sein können. Die eben besprochenen Methoden sind nicht-
statisch, und das heißt, dass sie grundsätzlich nur im Zusammenhang mit
einem Objekt durchgeführt werden können. Wie schon im Zusammenhang
mit der main()-Methode besprochen, sind statische Methoden dagegen Klas-
senmethoden, die für die ganze Klasse gültig sind und nicht von der Existenz
eines Objektes abhängen. Sobald Sie eine Methode daher als statisch defi-
nieren, hat sie mit den Objekten Ihrer Klasse nichts mehr zu tun und wird
deshalb auch durch
Klassenname.methodenname()
aus anderen Klassen aufgerufen. Ein Beispiel dafür haben Sie schon in Aufga-
be 3.5 in Gestalt von Math.sqrt(x) gesehen. Ein weiteres Beispiel zeige ich
Ihnen jetzt, und gleichzeitig werde ich Sie damit von allen Umständlichkeiten
mit der Tastatureingabe befreien, über die Sie sich im letzten Abschnitt so
geärgert haben.
3.2 Klassen, Objekte und Methoden 231

import java.io.*;

class Eingabe
{
static BufferedReader in = new BufferedReader(
new InputStreamReader(System.in));
public static double lesenreell()
{
boolean nochmal;
double a=0;
do
{
nochmal = false;
try
{
String Eingabe = in.readLine();
Double x = new Double(Eingabe);
a = x.doubleValue();
}
catch(Exception e)
{
System.out.println("Unzulaessige Eingabe");
System.out.println("Neuer Versuch");
nochmal = true;
a = 0;
}
}
while (nochmal == true);
return a;
}

public static int lesenint()


{
try
{
String Eingabe = in.readLine();
Integer x = new Integer(Eingabe);
int a = x.intValue();
return a;
}
catch(Exception e)
{
System.out.println("Unzulaessige Eingabe");
return 0;
232 3 Objektorientierte Programmierung mit Java

}
}

public static String lesenstring()


{
try
{
String Eingabe = in.readLine();
return Eingabe;
}
catch(Exception e)
{
System.out.println("Unzulaessige Eingabe");
return "";
}
}
}
Die Klasse Eingabe selbst bietet programmiertechnisch nichts Neues.
Erwähnenswert ist höchstens, dass in der Methode lesenreell() eine Si-
cherheitsschleife eingebaut wurde, die garantiert, dass bei unpassender Ein-
gabe das Ganze noch einmal von vorne anfängt, und dass alle vorkommenden
Methoden genau wie die main()-Methode sowohl mit static als auch mit
public qualifiziert wurden. Und das muss auch so sein, denn jeder soll sie
benutzen können, und sie sollen nicht von einzelnen Objekten abhängig sein.
Wenn Sie nun diese Eingabeklasse in Ihrem Arbeitsverzeichnis abspeichern
und kompilieren, dann können Sie jetzt und in alle Zukunft auf die Schererei-
en mit der Tastatureingabe verzichten. Wollen Sie beispielsweise in irgendeiner
anderen Klasse eine ganze Zahl i über die Tastatur eingeben, dann schreiben
Sie einfach nur
i = Eingabe.lesenint();
und schon wird alles erledigt. Das ist genau der Vorteil, den ich oben schon
erwähnt hatte: man macht die Programmierarbeit ein- für allemal, legt die
Methoden in einer bestimmten Klasse ab, und dann können andere Klassen
auf die bereits programmierten Methoden zugreifen.

Statische Methoden
Statische Methoden werden mit dem Schlüsselwort static gekennzeich-
net und hängen nicht an einem Objekt, sondern sind Klassenmetho-
den, also für die gesamte Klasse gültig. Man kann sie aufrufen durch
Klassenname.methodenname().

3.2.3 Konstruktoren und set/get-Methoden


Meine Ballklasse funktioniert zwar schon einwandfrei, aber so ganz gefällt sie
mir noch nicht. Die Zuweisung über die Punktnotation ist recht schwerfällig
3.2 Klassen, Objekte und Methoden 233

und verstößt außerdem gegen das oben beschriebene Prinzip, auf die Objek-
te nur mit Hilfe der klar definierten Methoden zuzugreifen. Ein wesentliches
Hilfsmittel zur Behebung dieses Problem sind die Konstruktoren. Tatsächlich
haben Sie sogar schon mal einen aufgerufen, ohne es zu merken, denn bei
jedem Aufruf von new kommt der Konstruktor zum Tragen. Es handelt sich
dabei um eine spezielle Methode, die direkt beim Erzeugen des Objekts auf-
gerufen wird, und wo haben wir das schon gemacht? Richtig, bei dem Aufruf
Ball meinBall = new Ball();
Hier wird eine Methode Ball() aufgerufen, und diese Methode ist der Kon-
struktor, der ein Objekt konkret konstruiert. Ich hatte doch aber gar keine
Methode Ball() in meiner Ballklasse definiert, wie kann das funktionieren?
Ganz einfach. Solange Sie gar keinen Konstruktor programmieren, wird beim
Anlegen eines Objekts automatisch der so genannte Standardkonstruktor auf-
gerufen, den man nicht eigens programmieren muss, weil er immer das Gleiche
macht. Er reserviert nur für das gewünschte Objekt den nötigen Speicherplatz
und tut sonst gar nichts.
Sobald Sie aber schon beim Anlegen des Objekts die eine oder andere Ak-
tivität durchführen möchten, müssen Sie einen Konstruktor in Ihrer Klasse
programmieren. Sehr üblich ist es, dem Konstruktor als Parameter die Werte
für die Membervariablen mitzugeben, damit ein angelegtes Objekt auch gleich
mit Leben gefüllt ist. Sie können aber auch einen parameterfreien Konstruktor
verwenden und im entsprechenden Methodenrumpf dann für die notwendigen
Eingaben sorgen. Im Folgenden habe ich die Klasse Ball um zwei Konstruk-
toren ergänzt.
class Ball
{
String marke;
String farbe;
int gewicht;
String geschwindigkeit;

Ball(String marke_par, String farbe_par, int gewicht_par,


String geschwindigkeit_par)
{
marke = marke_par;
farbe = farbe_par;
gewicht = gewicht_par;
geschwindigkeit = geschwindigkeit_par;
}

Ball()
{
System.out.println("Marke eingeben");
marke = Eingabe.lesenstring();
System.out.println("Farbe eingeben");
234 3 Objektorientierte Programmierung mit Java

farbe = Eingabe.lesenstring();
System.out.println("Gewicht eingeben");
gewicht = Eingabe.lesenint();
System.out.println("Geschwindigkeit eingeben");
geschwindigkeit = Eingabe.lesenstring();
}

String beschreiben()
{
return "Der Ball ist ein " + marke + ", seine Farbe ist "
+ farbe + ", sein Gewicht betraegt " + gewicht
+ " Gramm, und er ist " + geschwindigkeit + ".";
}

void farbeAendern(String farb_par)


{
farbe = farb_par;
}
}
Ich habe hier zwei Konstruktoren namens Ball() definiert, einen mit und
eine ohne Parameter. Das ist auch ganz in Ordnung; Sie können beliebig viele
Konstruktoren definieren, solange sie sich in ihren Parameterlisten unterschei-
den. Der Name des Konstruktors muss dem Namen der Klasse entsprechen,
deshalb heißen beide Konstruktoren auch Ball(). Und ein Konstruktor darf
niemals einen Rückgabetyp haben - nicht nur, dass er keinen Rı̈ckgabewert
hat, das könnte man durch void regeln, er hat nicht einmal einen Rückgabe-
typ, und deshalb steht auch weder ein Klassenname noch sonst ein Datentyp
vor Ball().
Mit diesen beiden Konstruktoren kann ich nun in der main()-Methode
arbeiten. Das Kommando
Ball neuerBall = new("Deutschmann", "gruen", 60, "eher schnell");
legt dann beispielsweise ein Ballobjekt namens neuerBall an, dessen Mem-
bervariablen direkt beim Anlegen mit den angegebenen Werten gefüllt werden,
sodass Sie sich im Hauptprogramm nicht mehr mit der Punktnotation plagen
müssen. Dagegen führt der Aufruf
Ball schoenerBall = new Ball();
jetzt nicht mehr nur dazu, dass einfach nur der nötige Speicherplatz für
schoenerBall angelegt wird: da bei diesem Anlegevorgang mein parameterlo-
ser Konstruktor aufgerufen wird, erscheinen sofort beim Anlegen des Objekts
die Eingabeaufforderungen am Bildschirm, denen dann die nötigen Eingabe-
vorgänge folgen. Ich hoffe übrigens, dass Sie spätestens jetzt sehen, wie sinnvoll
die Programmierung der Klasse Eingabe war.
Dass man zwei und mehrere Konstruktoren gleichen Namens definieren
kann und dabei nicht in Schwierigkeiten kommt, solange die Parameterlisten
sich unterscheiden, ist nichts Ungewöhnliches und wird Ihnen gleich bei den
3.2 Klassen, Objekte und Methoden 235

anderen Methoden auch begegnen. Man spricht dabei von der Überladung von
Konstruktoren und meint nur, dass mehrere Konstruktoren, die verschieden
funktionieren, den gleichen Namen tragen.

Konstruktoren
Ein Konstruktor ist eine spezielle Methode, die direkt beim Erzeugen des
Objekts aufgerufen wird. Ihr Name muss dem Klassennamen entsprechen.
Die Methode hat keinen Rückgabetyp.
Es ist möglich, mehrere verschiedene Konstruktoren einer Klasse zu defi-
nieren, die sich in ihren Parameterlisten unterscheiden müssen. Diesen Vor-
gang nennt man Überladen.

Sobald Sie übrigens einmal einen expliziten Konstruktor in Ihrer Klasse


definiert haben, ist der automatisch ansprechbare Standardkonstruktor nicht
mehr vorhanden; den müssen Sie dann bei Bedarf von Hand neu programmie-
ren, was nicht weiter schwer ist, da er ja nichts tut.
Noch ein Wort zu der Bezeichnung der Parameter sowohl im Konstruktor
als auch in anderen Methoden. Ich habe im obigen Beispiel die Namen der
Membervariablen immer mit dem Zusatz _par versehen, wenn ich einen Para-
meter zu einer bestimmten Membervariablen definiert habe. So etwas ist auch
ziemlich unvermeidbar, obwohl man ja vielleicht lieber die Parameter genauso
nennen würde wie die Membervariablen selbst, aber dann Wertzuweisungen
wie
marke = marke;
erhalten würde, was jeden Compiler überfordert. Trotzdem gibt es einen Aus-
weg aus dieser schwerfälligen Bezeichnungsweise. Sobald Sie sich in der De-
finition eines Objekts befinden, können Sie das aktuelle Objekt als dieses

Objekt“ ansprechen, und da dieses“ auf Englisch this“ heißt, gibt es in Java
” ”
das Schlüsselwort this. Den erste Konstruktor der Ballklasse kann ich dann
unter Einsatz von this folgendermaßen formulieren.
Ball(String marke, String farbe, int gewicht,
String geschwindigkeit)
{
this.marke = marke;
this.farbe = farbe;
this.gewicht = gewicht;
this.geschwindigkeit = geschwindigkeit;
}
Sie sehen, dass die Schwerfälligkeit in den Parameternamen verschwunden ist,
aber nichts im Leben ist umsonst: die Membervariablen musste ich mit dem
Zusatz this präzisieren, um deutlich zu machen, dass zum Beispiel die Varia-
ble marke dieses Objekts - also this.marke - mit dem Wert des Parameters
marke belegt wird. Wer es mag, kann es verwenden, ich selbst benutze es eher
selten. Der Einsatz von this ist natürlich nicht auf Konstruktoren beschränkt,
236 3 Objektorientierte Programmierung mit Java

sondern überall da möglich, wo Sie vor einem ähnlichen Namensproblem ste-


hen wie hier geschildert.
Ein anderes Verfahren zum Füllen und Ansehen von Membervariablen
sind die so genannten Setter und Getter - zwei schreckliche Kunstworte zur
Bezeichnung eines sehr einfachen Vorgangs. Was ich in meinem parameter-
beladenen Konstruktor für alle Attribute auf einmal erledigt habe, kann ich
auch für jedes Attribut einzeln machen, und bei solchen Methoden spricht
man von Setter-Methoden, weil man den Wert des Attributs setzt. Ein Bei-
spiel für meine Ballklasse wäre
void setMarke(String marke_par)
{
marke = marke_par;
}
Über ihre Funktionsweise muss ich keine Worte verlieren, man kann sie so-
wohl zum erstmaligen Besetzen des Attributs marke als auch zum Ändern
benutzen. Sobald Sie für jede Membervariable eine set-Methode definiert ha-
ben, können Sie selbstverständlich auf die Methode farbeAendern() leichten
Herzens verzichten, denn ihre Funktion wird von setFarbe() übernommen.
Wie die entsprechenden Getter aussehen, mit denen man die Werte der
Attribute lesen kann, dürfte jetzt ziemlich klar sein. Als Beispiel zeige ich Ih-
nen die get-Methode für die Marke.
String getMarke()
{
return marke;
}
Daran ist nichts Aufregendes. Aber wozu macht man sich die Mühe, solche
Methoden zu schreiben, wo man doch auch die Punktnotation einsetzen könn-
te? Das hat etwas mit den Grundprinzipien der Objektorientierung zu tun.
Eine Klasse beschreibt die Datenstruktur und die Methoden, mit denen man
auf die Daten zugreifen kann. Damit eine möglichst große Ordnung herrscht,
soll auf die Daten aber tatsächlich nur mit den bereitgestellten Methoden zu-
gegriffen werden und sonst überhaupt nicht. Ein Programmierer, dem Sie die
Ballklasse zur Verfügung stellen, soll mit der internen Programmierung Ihrer
Klasse nichts zu tun haben, sondern muss nur wissen, welche Schnittstellen
zwischen Ihrer Klasse Ball und der Außenwelt bestehen und wie man da-
mit umgeht. Und solche Schnittstellen werden nun mal über die Methoden
realisiert, weshalb ein Datenzugriff grundsätzlich über Methoden geschehen
sollte.
Von Bedeutung wird das besonders dann, wenn Sie Ihre Membervariablen
ein wenig geheim halten, indem Sie sie als privat erklären. Indem Sie nicht
mehr nur String marke, sondern genauer private String marke beim An-
legen der Membervariable marke schreiben, qualifizieren Sie die Memberva-
riable marke als privat, und das heißt, dass dieses Element nur in der Klasse
selbst sichtbar ist. Innerhalb der Klassendefinition können Sie ungehindert auf
marke zugreifen, aber außerhalb geht gar nichts, weshalb auch insbesondere
3.2 Klassen, Objekte und Methoden 237

die Punktnotation nicht funktionieren kann. Keine andere Klasse, kein Haupt-
programm findet den Weg zu der Membervariablen marke, nur die Klasse Ball
selbst darf damit umgehen. Das macht aber gar nichts, denn in dieser Klasse
Ball habe ich einen Getter und einen Setter für marke definiert, die natürlich
nicht auch noch als private gekennzeichnet sein dürfen.
Methoden können nämlich auf die gleiche Weise qualifiziert werden wie
Membervariablen. Eine private-Methode kann nur innerhalb der Klasse auf-
gerufen werden, was natürlich wenig Sinn macht, weil Methoden in aller Regel
dafür da sind, von außen für ein bestimmtes Objekt aufgerufen zu werden.
Also wird man gerade Getter und Setter für jeden zugänglich machen, und
den dazu nötigen Qualifizierer haben Sie schon ab und zu gesehen: es ist das
Schlüsselwort public. Eine public-Methode kann jeder ungehindert benut-
zen, auf eine public-Membervariable kann jeder ungehindert zugreifen. Da die
grundsätzliche Idee darin besteht, auf die Daten nur über Methoden zuzugrei-
fen, kennzeichnet man Membervariablen oft als private, Methoden dagegen
als public.
Und was ist los, wenn ich gar keinen Qualifizierer verwende, wie ich es aus
Faulheit bisher meistens getan habe? In diesem Fall kann jedes Programm,
jede Klasse, die sich im gleichen Verzeichnis befindet wie meine Klasse Ball,
auf die Membervariable oder auf die Methode zugreifen. Bei den kleinen Pro-
grammen, die ich hier vorstelle, spielt sich ohnehin alles in einem Verzeichnis
ab, und daher ist es ziemlich egal, ob ich nun public verwende oder gar nichts.
Bei größeren Programmsystemen dagegen werden verschiedene Klassen oft in
verschiedenen Verzeichnissen liegen, und da sollten Sie schon genau aufpassen
und genaue Qualifizierungen vornehmen.

Umgang mit Membervariablen


Um klar definierte Schnittstellen zu haben, füllt, verändert und betrachtet
man die Inhalte von Membervariablen oft mit Settern und Gettern, also mit
Methoden, die nur das Belegen und Auslesen der Membervariablen erledigen.
Sowohl Membervariablen als auch Methoden kann man durch die Angabe
von private oder public qualifizieren. Ein private-Element ist nur innerhalb
der jeweiligen Klasse sichtbar und ansprechbar, ein public-Element kann von
jeder Stelle aus angesprochen werden. Qualifiziert man ein Element gar nicht,
so ist es in dem Verzeichnis, in dem seine Klasse definiert ist, ansprechbar,
von einem anderen Verzeichnis aus nicht.

3.2.4 Überladen von Methoden

Was den Konstruktoren recht ist, wird den allgemeinen Methoden billig sein:
mehrere Methoden können den gleichen Namen besitzen, aber verschiedene
Verarbeitungen durchführen. Die Methode wird nämlich in Java nicht nur
durch ihren Namen identifiziert, sondern auch durch ihre Parameterliste, also
durch Anzahl und Typen der mitgegebenen Parameter, was man auch gern
als Signatur bezeichnet. Verschiedene Methoden einer Klasse können also den
238 3 Objektorientierte Programmierung mit Java

gleichen Namen haben, wenn sie sich im Aufbau ihrer Parameterliste unter-
scheiden. Die Technik, verschiedene Methoden mit gleichem Namen, aber ver-
schiedenen Parameterlisten zu schreiben, bezeichnet man als Überladen von
Methoden.
Das klingt zunächst vielleicht sinnlos, hat aber durchaus seinen Sinn, da
es auf diese Weise möglich ist, für eine Funktionalität immer den gleichen
Methodennamen zu verwenden, auch wenn verschiedene Parameterlisten ver-
wendet werden sollen. Aber Vorsicht: unterscheiden sich zwei Methoden nur
nach ihrem Rückgabetyp, während sie in Namen und Parameterlisten über-
einstimmen, so macht der Compiler mit Sicherheit Ärger in Form einer Fehler-
meldung. Verschiedene Rückgabetypen reichen für das Überladen nicht aus.
Sehen wir uns ein Beispiel an.
class Test
{
public int mult(int x, int y)
{
return x * y;
}

public int mult(int x, int y, int z)


{
return x * y * z;
}

public double mult(double x, double y)


{
return x * y;
}
}
In einem Hauptprogramm könnte dann stehen:
public static void main(String[] args)
{
Test ding = new Test();
int m,n,k;
double a,b;
System.out.println(ding.mult(a,b));
System.out.println(ding.mult(m,n));
System.out.println(ding.mult(m,n,k));
}
Java bemerkt von alleine, welche der verschiedenen mult()-Methoden ver-
wendet werden sollen, da an der Liste der aktuell verwendeten Parameter
abgelesen werden kann, um welche der drei Multiplikationsmethoden es sich
handeln muss. Sie müssen daher nicht für verschiedene Arten der Multiplika-
tion sich jeweils einen neuen Methodennamen ausdenken und dann mühsam
beim Stricken des Hauptprogramms immer wieder darauf achten, dass Sie die
3.2 Klassen, Objekte und Methoden 239

richtige Methode auswählen: Sie können sich ganz entspannt zurücklehnen,


jede vorkommende Multiplikation auf die gleiche Weise mit mult() anspre-
chen und sich darauf verlassen, dass Ihr Java die richtige Methode aussuchen
wird.

Überladen von Methoden


Verschiedene Methoden einer Klasse können den gleichen Namen haben, wenn
sie sich in ihrer Signatur unterscheiden. Das erlaubt es, verschiedene Aktio-
nen, die im Einzelnen von der Art der mitgegebenen Parameter abhängen,
unter dem gleichen Namen durchzuführen. Man nennt dieses Verfahren das
Überladen einer Methode.

In C++ geht man sogar noch etwas weiter und erlaubt das Überladen von
Operatoren. Sie können als beispielsweise dem +- oder dem *-Zeichen eine neue
Bedeutung verleihen, indem Sie es als Vektoraddition oder als Skalarprodukt
von Vektoren interpretieren. Darin liegt aber die Gefahr, als Programmierer
recht schnell durcheinander zu kommen, und deshalb gibt es in Java keine
Überladung von Operatoren.

3.2.5 Felder und Vektoren

Natürlich verfügt Java auch über Felder, wie sollte es das nicht! Sie sind
recht ähnlich aufgebaut wie in C, weshalb ich hier auch nur kurz darüber
sprechen will. Nehmen wir beispielsweise an, Sie wollen ein Feld der Länge
10 aus ganzen Zahlen aufbauen, dann werden Sie dem Compiler erst einmal
durch die Anweisung int[] feld oder int feld[] mitteilen, dass es sich
bei feld um eine Feld aus ganzen Zahlen handeln soll; auf die Länge lassen
Sie sich noch nicht ein. Bei dieser Deklaration ist übrigens die erste Variante
eher die übliche, und ich werde sie ab jetzt auch durchgängig verwenden.
Das eigentliche Definieren, das Erzeugen des Feldes erfolgt dann im nächsten
Schritt durch
feld = new int[10];
Erst jetzt können Sie das Feld verwenden und auf die übliche Weise mit Hilfe
von for-Schleifen bearbeiten. Natürlich hätten Sie auch gleich
int[] feld = new int[10]
schreiben und damit alles in einem Schritt erledigen können; in diesem Fall
werden Deklaration und Definition verbunden.
Der Zugriff erfolgt wie üblich durch die Angabe der laufenden Nummer,
also feld[0], feld[9] oder auch feld[n], sofern n eine ganze Zahl ist, die
zwischen 0 und 9 liegt. Java ist nämlich in bezug auf Feldgrenzen sehr viel
nachtragender als C. Erinnern Sie sich daran, wie nachlässig C mit dem Über-
schreiten der Feldgrenzen umgeht? Das wollten die Entwickler von Java besser
machen, und es ist ihnen auch gelungen. Sobald Sie nämlich auf die Stel-
le feld[10] oder irgendeine andere laufende Nummer über 10 oder unter 0
240 3 Objektorientierte Programmierung mit Java

zugreifen wollen, wird Ihnen die JVM auf die Finger hauen und eine so ge-
nannte ArrayIndexOutOfBounds-Exception erzeugen. Wenn Sie sich vorher
nicht darum gekümmert haben, bricht Ihr Programm dann mit einer Feh-
lermeldung ab. Das können Sie durch Einsatz der try-catch-Konstruktion
vermeiden; wie so etwas im Einzelnen geht, sehen Sie im Abschnitt über Aus-
nahmebehandlung. Es sollte aber erst gar nicht vorkommen, denn wenn Sie
Ihre Programme ordentlich schreiben, wird es keine Grenzüberschreitungen
bei Feldern geben.
Dass Sie auch wie in C mehrdimensionale Felder definieren können, dürfte
Sie nicht überraschen, und auch die Form des Deklarierens und Definierens
durch
double[][] matrix = new double[10][10];
für eine Matrix mit zehn Zeilen und zehn Spalten aus insgesamt 100 reellen
Zahlen wird keinen vor Begeisterung schreien lassen. Wichtiger ist die Frage,
wie man Felder mit echten Objekten anlegt, und da gibt es tatsächlich einen
Unterschied.
Die Datentypen int, double oder boolean sind so genannte primitive Da-
tentypen, sie sind keine Klassen; zu einem solchen Typ gehörende Elemente
sind einfach da, sobald Sie so etwas wie int n schreiben. Wie Sie wissen, sieht
das bei Objekten anders aus, denn die muss man noch durch einen Konstruk-
toraufruf konkret erzeugen, damit der benötigte Speicherplatz zur Verfügung
steht. Wenn Sie nun ein Feld aus Minigolfbällen definieren wollen, dann wer-
den Sie natürlich auch erst mal zu dem Kommando
Ball[] baelle = new Ball[10];
greifen, und das ist auch ganz richtig. Es reicht aber nicht. Da jeder Eintrag
in meinem Feld baelle ein eigenes Ballobjekt ist, verlangt auch jeder Eintrag
einen eigenen Konstruktoraufruf, sonst sind die Objekte, aus denen sich mein
Feld zusammensetzt, überhaupt nicht definiert. Steht also beispielsweise der
Standardkonstruktor für die Klasse Ball zur Verfügung, dann wird das Ball-
feld erst nach dem folgenden Programmstück vollständig angelegt sein.
Ball[] baelle = new Ball[10];
for(int i=0;i<10;i++)
baelle[i]=new Ball();
Sollten Sie übrigens im Zuge eines langen Programms vergessen haben, wie
lang ein bestimmtes Feld ist, so schadet das gar nichts. Die Länge des Feldes
baelle wird Ihnen über baelle.length direkt von Java geliefert.

Felder
Ein Feld über einem primitiven Datentyp wird beispielsweise durch
int[] feld = new int[laenge];
angelegt, wobei laenge eine positive ganze Zahl sein muss. Mehrdimensio-
nale Felder werden durch die entsprechende Angabe mehrerer Dimensionen
definiert. Sollen die Feldinhalte Objekte sein, so muss in einer Schleife jedes
Feldelement durch einen Konstruktoraufruf definiert werden.
3.2 Klassen, Objekte und Methoden 241

An die Schwachstelle der Felder können Sie sich sicher noch aus dem C-
Kapitel erinnern. Bevor Sie damit arbeiten können, müssen Sie seine Länge
festlegen, und das bedeutet, dass es sich um alles andere als eine dynamische
Datenstruktur handelt. Zum Glück stellt Java in Gestalt der Klasse Vector
eine hauseigene Lösung dieses Problems bereit. Ein Element der Klasse Vector
ist im Grunde genommen nichts anderes als ein Feld, nur dass es sich nicht
so kleinlich wie ein Feld gleich am Anfang auf eine Feldlänge festlegen muss.
Für das nächste Beispielprogramm setze ich wieder die Klasse Ball voraus,
bei der der parameterlose Konstruktor für die Eingabe der Membervariablen
sorgt. Eine beliebige Anzahl von Bällen können Sie dann mit der folgenden
Klasse verarbeiten.
import java.util.Vector;

class Ballverarbeitung
{
public static void main(String[] args)
{
String antwort;
Vector baelle = new Vector();
boolean abbrechen = false;
do
{
Ball neuerBall = new Ball();
baelle.addElement(neuerBall);
System.out.println("Wollen Sie die Daten eines
weiteren Balles eingeben?(j/n)");
antwort = Eingabe.lesenstring();
}
while(antwort.equals("j") || antwort.equals("J"));
for (int i=0; i < baelle.size(); i=i+1)
{
String b = ((Ball)baelle.elementAt(i)).beschreiben();
System.out.println("Ball " + i + ":" + b);
}
}
}
Die Klasse Vector steht nicht einfach so zur Verfügung, man muss sie aus
dem Paket java.util importieren, und genau das geschieht in der ersten Zei-
le. Im Hauptprogramm wird dann ein Vektor namens baelle angelegt. Sie
sehen, dass ich dem Vektor nicht mitteilen kann, welcher Art seine Einträge
sein werden: das unterscheidet Vektoren massiv von Feldern und wird Konse-
quenzen haben. In einer Schleife erzeuge ich dann bei jedem Schleifendurchlauf
ein neues Ballobjekt und schreibe es in den Vektor baelle hinein. Ich gebe
es zu: der Befehl dazu ist nicht der Schönste; man kann hier nicht einfach die
von den Feldern so vertraute Schreibweise einsetzen, sondern muss tatsächlich
242 3 Objektorientierte Programmierung mit Java

die Methode addElement() verwenden. Die Klasse Vector ist nun mal vor-
definiert, also bin ich auch auf ihre vordefinierten Methoden angewiesen, und
die Methode addElement() fügt das Objekt neuerBall dem Vektor baelle
hinzu.
Sobald ich mit dem Füllen des Vektors fertig bin, gebe ich ihn wieder
aus. Die Länge des Vektors musste ich nirgendwo festlegen, also kann ich
auch nicht wissen, wie viele Bälle der Benutzer im Verlauf des Programms
eingegeben hat, aber das macht gar nichts. In Vector gibt es die Methode
size(), die automatisch die Länge des Vektors liefert und hier im Kopf meiner
for-Schleife zur Ausgabe eingesetzt wird. Die Kommandos im Schleifenrumpf
sehen allerdings auf den ersten Blick etwas verwirrend aus. Der zweite Blick
erklärt aber, worum es geht. Auf das i-te Element von baelle kann ich leider
nicht mit baelle[i] zugreifen, sondern nur mit der vordefinierten Methode
baelle.elementAt(i); daran müssen Sie sich gewöhnen. Nun konnte ich aber
beim Anlegen des Vektors nicht festlegen, welche Art von Objekten in diesen
Vektor kommen sollen. Theoretisch, wenn ich schlampig programmiert hätte,
könnten da neben Ballobjekten auch Schläger oder Schiedsrichter stehen. Ich
muss daher dem Programm klarmachen, dass es sich hier wirklich um Ball-
objekte handelt und sonst um gar nichts. Das Mittel dazu haben Sie schon
einmal in C gesehen: so etwas macht man mit einem Typecast, der in Java
genauso funktioniert wie in C. Durch (Ball)baelle.elementAt(i) machen
Sie dem Programm klar, dass es alles was die Methode elementAt() aus dem
Vektor baelle heraus holt, als ein Ballobjekt zu verstehen hat. Von alleine
kann das die JVM nicht wissen, Sie müssen es ihr auf jeden Fall mitteilen.
Nun ist also (Ball)baelle.elementAt(i) ein Ballobjekt, auf das ich die
Methode beschreiben() meiner Klasse Ball anwenden kann. Sie liefert mir
einen String zurück, den ich in der Variable b zwischenspeichere und dann
mit dem nächsten Kommando am Bildschirm ausgebe.
Machen Sie sich bitte klar, dass Java mit der Klasse Vector eine sehr
komfortable dynamische Datenstruktur zur Verfügung stellt, mit der sich
wesentlich leichter umgehen lässt als mit selbstprogrammierten verketteten
Listen. Alle Methoden werden von Java geliefert, in den Vektor dürfen Sie
schreiben, was Sie wollen, und die Länge müssen Sie beim Programmie-
ren noch nicht festlegen: was will der Mensch mehr? Zwei Dinge will er.
Erstens ist für eine komfortable Verarbeitung noch etwas mehr nötig als
die beiden Methoden elementAt() und addElement(), und zum Glück ist
Java in dieser Hinsicht nicht kleinlich. Ich gebe Ihnen hier nur ein paar
Beispiele. Sollten Sie das Bedürfnis haben, an einer bestimmten Stelle des
Vektors baelle ein bestimmtes Objekt dieserBall einzufügen, so genügt
die Methode insertElementAt(dieserBall,i), wobei die int-Variable i
angibt, an welcher Stelle des Vektors Ihr Objekt stehen soll. Alle ab der
Position i stehenden Elemente werden dann um je einen Platz nach hin-
ten verschoben, ohne dass Sie sich noch darum kümmern müssten. Sie
können aber auch einfach ein Objekt an die i-te Stelle des Vektors setzen
und das vorher dort stehende Objekt gnadenlos überschreiben, indem Sie
3.2 Klassen, Objekte und Methoden 243

setElementAt(dieserBall,i) einsetzen. Ein Objekt aus dem Vektor löschen


können Sie mit removeElement(dieserBall): dann wird der Vektor nach
dem mitgegebenen Objekt durchsucht, und falls es vorhanden ist, wird das
Objekt aus dem Vektor entfernt. In diesem Fall liefert die Methode den Rück-
gabewert true, andernfalls eben false. Wenn Sie nur wissen, an welcher Stelle
Sie etwas löschen wollen, ist das auch in Ordnung, denn dafür gibt es die Me-
thode removeElementAt(i), die wieder eine ganze Zahl als Parameter zur
Angabe der Position im Vektor braucht.
Das sind noch längst nicht alle Methoden der Klasse Vector, aber ich will
es jetzt damit gut sein lassen. Es gibt so viele vordefinierte Klassen mit so
vielen vordefinierten Methoden, dass man nicht alle in einem einzigen Ka-
pitel auflisten kann; so etwas finden Sie aber oft in der Hilfefunktion einer
Java-Entwicklungsumgebung oder in der Online-Dokumentation von Sun Mi-
crosystems selbst, die zu jeder vordefinierten Klasse auch jede vordefinierte
Methode auflistet.
Ich hatte zwei Dinge angekündigt, die man von Vektoren verlangen kann.
Über das erste habe ich gerade gesprochen, das zweite ist der schlichte Wunsch,
in einem Vektor alles abzulegen, was einem so in den Sinn kommt. Das geht
aber leider nicht ganz. In einen Vektor können Sie nur echte Objekte eintragen,
und da die Elemente der primitiven Datentypen int, double und boolean nun
einmal nicht als Objekte gelten, kann man sie nicht in einen Vektor eintragen;
ein Vektor aus schlichten int-Zahlen wird nicht akzeptiert. Das ist schlecht,
aber auch wieder nicht so schlecht, denn auch dieses Problem hat eine Lösung,
die ich Ihnen gleich zeigen werde.

Die Klasse Vector


Die Klasse Vector aus dem Paket java.util bietet eine komfortable vorde-
finierte dynamische Datenstruktur. Ein Vektor kann beliebig viele Objekte
beliebiger Klassen aufnehmen, wobei es oft sinvoll ist, sich auf Objekte ei-
ner Klasse zu beschränken. Mit Hilfe vordefinierter Methoden kann man die
Einträge eines Vektors bearbeiten und das Aufbauen und Ausgeben der Vek-
toreinträge sowie das Einfügen in den Vektor und das Löschen aus dem Vektor
auf bequeme Weise durchführen.
Einträge eines Vektors müssen stets Objekte sein; Elemente primitiver
Datentypen sind nicht erlaubt.

3.2.6 Hüllenklassen
Ich hatte versprochen, dass das zuletzt geschilderte Problem mit dem Füllen
von Vektoren eine Lösung hat, und hier ist sie schon in Form der bereits
früher angesprochenen Hüllenklassen oder auch Wrapperklassen. Zu jedem
primitiven Datentyp gibt es eine Hüllenklasse, mit der Sie die Elemente des
Datentyps in Objekte verwandeln können, die dann beispielsweise in einem
Vektor untergebracht werden dürfen. Es gibt also beispielsweise die Hüllen-
klassen Integer, Double und Boolean zur Umwandlung ganzer Zahlen, reeller
244 3 Objektorientierte Programmierung mit Java

Zahlen und von Wahrheitswerten. Und was nützen sie mir? Ganz einfach. Sie
können eine int-Variable i leicht in ein Integer-Objekt verwandeln, indem
Sie beispielsweise den Konstruktoraufruf
Integer izahl = new Integer(i);
verwenden. Dann ist izahl ein ordnungsgemäßes Objekt, das problemlos
Platz in einem Vektor findet und natürlich auch wieder in eine schlichte int-
Zahl zurückverwandelt werden kann. Dazu müssen Sie nur mit dem Komman-
do i = izahl.intValue() die Methode intValue() einsetzen, und schon
steht auf i der eigentliche Wert Ihres Integer-Objekts. Schon klar, das ist
ein wenig umständlich, aber das ist nun mal der Preis, den man für eine
einigermaßen strenge Objektorientierung bezahlen muss.
Auch eine reelle Zahl x wird auf diese Weise problemlos in ein Objekt ver-
wandelt, indem Sie den analogen Konstruktoraufruf
Double xzahl = new Double(x);
einsetzen, und umgekehrt hilft Ihnen die Methode doubleValue() wieder
dabei, aus einem Double-Objekt eine schlichte reelle Zahl zu machen. Bei
der Behandlung der Tastatureingabe haben Sie übrigens noch einen weite-
ren Konstruktor einer Hüllenklasse kennengelernt. Anstatt direkt eine re-
elle Zahl mitzugeben, können Sie auch einen String als Parameter verwen-
den, der dann natürlich so aussehen muss, wie die entsprechende Zahl. Statt
new Integer(17) geht also auch new Integer("17"), und neben dem ver-
trauten Aufruf new Double(1.234) spricht auch nichts gegen die Fassung
new Double("1.234"). Bei der Tastatureingabe war das von Bedeutung, weil
die entsprechenden Methoden nun mal immer Strings liefern und man eine
Möglichkeit braucht, die Strings in Zahlen umzuwandeln.

Hüllenklassen
Zu jedem primitiven Datentyp gibt es eine Hüllenklasse, mit der man die
Elemente des Datentyps in Objekte verwandeln kann. Durch entsprechende
Konstruktorenaufrufe wird aus einem Element eines Datentyps oder aus einem
passenden String ein Objekt konstruiert. Mit Hilfe vordefinierter Methoden
der Hüllenklassen kann man aus den Objekten die Elemente der Datentypen
rekonstruieren.

3.2.7 Zeiger

Vielleicht haben Sie schon mit Freude festgestellt, dass bisher keine Zeiger
aufgetreten sind. Diese Freude werde ich Ihnen sofort verderben, denn auch
wenn es nicht danach aussah: in diesem Java-Kapitel sind schon ungeheuer
viele Zeiger vorgekommen, ohne dass Sie es bemerkt haben. Ein Indiz dafür
war beispielsweise die zweistufige Prozedur zum Anlegen von Objekten, erst
die Deklaration, dann die endgültige Definition. Was für einen Sinn soll das
haben, warum macht man nicht gleich alles auf einmal? Weil Java intern
andauernd mit Zeigern und Adressen arbeitet, aber freundlich genug ist, den
Benutzer nichts davon merken zu lassen.
3.2 Klassen, Objekte und Methoden 245

Wenn Sie also die Klasse Ball angelegt haben und dann im Rahmen der
main()-Methode die Deklaration
Ball meinBall;
vornehmen, dann wird, wie schon besprochen, noch kein Speicherplatz für Ihr
Objekt reserviert, aber doch immerhin schon der Platz für einen Zeiger auf
das noch zu bauende Objekt. Die Variable meinBall ist also eigentlich nur
ein Zeiger auf ein Objekt, nicht das Objekt selbst. Sobald Sie dann
meinBall = new Ball();
oder einen anderen Konstruktoraufruf absetzen, wird der Speicherplatz für
das eigentliche Objekt erzeugt, und die vorher deklarierte Variable zeigt auf
eben diese Speicherstelle. Das läuft also genau genommen wie bei den Zeigern
in C, nur mit einem wichtigen Unterschied: Sie kriegen es nicht mit. Von
all diesen Verzeigerungen zwischen den Variablen und den Objekten selbst
merken Sie rein gar nichts, weil es automatisch und immer passiert, völlig
ohne Ihr Zutun. Java erlaubt im Gegensatz zu C oder C++ keine direkten
Zugriffe auf Speicheradressen, weil die Entwickler von Java sich die damit
verbundenen Probleme nicht antun wollten. Obwohl also alles mit Hilfe von
Zeigern abläuft, braucht Sie das nicht zu stören, weil Sie in aller Regel die
deklarierte Variable mit dem erzeugten Objekt identifizieren können - mit
dem Hintergrundwissen, dass das nicht so ganz stimmt.
Welche Konsequenzen das hat, sehen wir uns einmal am Beispiel der
Vektoren an. Stellen Sie sich vor, ich habe irgendeine Klasse Ding ange-
legt, die nur den Standardkonstruktor hat, dafür aber noch eine Methode
datenEingeben() zum Füllen ihrer Membervariablen aufweist. Nun will ich
einen Vektor dinger mit Objekten der Klasse Ding füllen und mache das fol-
gendermaßen.
String antwort;
Vector dinger = new Vector();
boolean abbrechen = false;
Ding neuesDing = new Ding();
do
{
neuesDing.datenEingeben();
dinger.addElement(neuesDing);
System.out.println("Wollen Sie die Daten eines
weiteren Dinges eingeben?(j/n)");
antwort = Eingabe.lesenstring();
}
while(antwort.equals("j") || antwort.equals("J"));
Gar nicht so schlecht auf den ersten Blick, aber leider falsch. Was wird
passieren, wenn Sie drei Dingobjekte in Ihren Vektor packen und danach die
Einträge des Vektors wieder ausgeben lassen? Sie werdem am Bildschirm drei-
mal die Daten des gleichen Dingobjektes sehen, und zwar des letzten, das
Sie eingegeben haben. Sie haben zwar ein Objekt neuesDing angelegt, aber
durch das einmalige Anlegen wurde auch nur einmal der für ein Objekt nötige
246 3 Objektorientierte Programmierung mit Java

Speicherplatz reserviert, und es gibt auch nur einen Zeiger, nämlich die Va-
riable neuesDing, der auf diese Speicherstelle zeigt. Bei jedem Durchlaufen
der Schleife werden dann die Membervariablen des Dingobjektes neu gefüllt -
nur sind es leider die Membervariablen des immer gleichen Objektes, die da
überschrieben werden, und nicht die Attribute eines jeweils neuen Objektes.
Sie füllen das immer gleiche Objekt, das an der immer gleichen Adresse steht,
mit immer wieder neuen Werten und überschreiben damit die alten, sodass
am Ende die Variable neuesDing natürlich immer noch auf die gleiche Stelle
zeigt, auf der die zuletzt eingegebenen Werte stehen.
Und der Vektor? Der hat überhaupt keine Wahl, er muss nehmen, was er
bekommt. Bei jedem Aufruf von addElement() wird auch dem Vektor selbst
nicht das gesamte Objekt eingefügt, sondern ein Zeiger auf die Speicherstelle,
an der das Objekt steht. In neuesDing steht nun aber leider immer die gleiche
Adresse der immer gleichen Speicherstelle, und in jedem Schleifendurchlauf
wird dieses neuesDing dem Vektor als nächstes Objekt zugewiesen. Also wird
nach Ablauf der Schleife jeder Eintrag des Vektors einen Zeiger auf die gleiche
Speicherstelle enthalten, und weil in dem immer gleichen Objekt die zuletzt
eingegebenen Werte stehen, habe ich einen Vektor, in dem ziemlich oft das
Gleiche steht.
Sie werden zugeben, dass das etwas ungünstig und sicher nicht wünschens-
wert ist. Der Fehler liegt darin, nur ein Objekt zu produzieren und dann dar-
auf zu vertrauen, dass durch das immer neue Belegen dieses Objektes und
das Einfügen in den Vektor schon irgendwie neue Objekte geschaffen werden.
Daraus wird aber nichts, und wenn man das nicht weiß, läuft man ganz schnell
in die Falle. Sobald Sie aber das Problem durchschaut haben, ist seine Lösung
sehr einfach: schaffen Sie den Speicherplatz dort, wo Sie ihn brauchen, also
innerhalb der Schleife.
String antwort;
Vector dinger = new Vector();
boolean abbrechen = false;
do
{
Ding neuesDing = new Ding();
neuesDing.datenEingeben();
dinger.addElement(neuesDing);
System.out.println("Wollen Sie die Daten eines
weiteren Dinges eingeben?(j/n)");
antwort = Eingabe.lesenstring();
}
while(antwort.equals("j") || antwort.equals("J"));
Indem Sie den Konstruktoraufruf innerhalb der Schleife vornehmen, wird
jedesmal ein neues Objekt geschaffen und Sie gehen all den geschilderten Pro-
blemen elegant aus dem Weg. Manchmal muss man eben etwas über den
Hintergrund wissen, um im Vordergund keine Fehler zu machen.
3.2 Klassen, Objekte und Methoden 247

Objektvariablen als Zeiger


Beim Deklarieren eines Objektes wird nur ein Zeiger auf das eigentliche Objekt
erzeugt, der nach dem Definieren des Objektes, also der konkreten Instanz, auf
das eigentliche Objekt zeigt. Der Java-Programmierer kann aber im Gegensatz
zum C-Programmierer nicht selbst in die Speicherverwaltung eingreifen. Auch
in einem Vektor werden nur Verweise auf die Speicherstellen abgespeichert,
an denen die einzutragenden Objekte abgelegt sind.

3.2.8 Die Klasse String

Jede Objektvariable ist also genau genommen nur ein Zeiger auf ein im Ar-
beitsspeicher abgelegtes Objekt, auch wenn man diesen feinsinnigen Unter-
schied nur selten mitbekommt. Eine Klasse, bei der es ab und zu sehr hilfreich
sein kann, sich an den Charakter von Objektvariablen zu erinnern, ist die Klas-
se String. Jede Zeichenkette ist ein Objekt der vorderfinierten Klasse String
und daher genau den Gesetzmäßigkeiten unterworfen, von denen ich gerade
gesprochen habe. Sie können einen String anlegen, indem Sie zunächst einmal
eine passenden Objektvariable wie String s deklarieren und dann den String
entweder sofort initialisieren wie zum Beispiel durch String s = "Kette"
oder anschließend einen Wert zuweisen, also beispielsweise
String s;
s = "Kette";
Ein Konstruktoraufruf ist hier also nicht nötig, aber natürlich möglich, denn
auch ein Anlegen durch
String s = new String("Kette");
ist problemlos machbar.
Was passiert nun, wenn Sie zwei Stringobjekte mit dem gleichen Wert in-
itialisieren? Haben Sie beispielsweise
String s1 = "Kette", s2 = "Kette";
abgesetzt, so wird die konstante Zeichenkette "Kette" nur einmal als Objekt
im Speicher abgelegt, und die beiden Objektvariablen s1 und s2 verweisen auf
das gleiche Objekt, auf die gleiche Stelle im Arbeitsspeicher. Der Vergleich
if (s1 == s2)
würde daher das Ergebnis true liefern, da es sich nur um ein einziges Objekt
handelt, auf das beide Objektvariablen zeigen. Nun kommt noch eine dritte
Objektvariable hinzu, die ich mit
String s3 = new String("Kette");
anlege. Bei s3 findet aber keine Initialisierung statt, die dem Compiler schon
während des Übersetzens bekannt ist, sondern ein Aufruf des Konstruktors,
und so ein Konstruktoraufruf kann nur zur Laufzeit des Programms erfolgen.
Während die Belegungen von s1 und s2 schon zum Zeitpunkt des Übersetzens
feststanden und es daher ausreichte, ein einziges Objekt für beide Objektva-
riablen anzulegen, ist das gleiche Verfahren für s3 nicht möglich. Sein Wert
248 3 Objektorientierte Programmierung mit Java

ist erst zur Laufzeit des Programms bekannt, und deshalb muss für s3 ein
eigener Speicherplatz angelegt werden.
Das hat Konsequenzen. Wenn Sie jetzt mit der Abfrage
if (s1 == s3)
die Gleichheit der beiden Strings s1 und s3 feststellen wollen, werden Sie
Schiffbruch erleiden. Mit == testen Sie nämlich, ob es sich um die gleichen
konkreten Objekte im Arbeitsspeicher handelt, ob also der Zeiger von s1 und
der Zeiger von s3 auf die gleiche Stelle im Arbeitsspeicher zeigen. Und weil
das nicht der Fall ist, liefert if (s1 == s3) das Ergebnis false, auch wenn
beide Strings den gleichen Inhalt haben.
Schön ist das nicht, aber auch nicht weiter schlimm, denn die Lösung
dieses Problems kennen Sie schon. Um zwei Strings auf inhaltliche Gleichheit
zu überprüfen, verwendet man eben nicht ==, sondern die Methode equals(),
mit der man genau den gewünschten Effekt erzielen kann. Die Abfrage
if (s1.equals(s3))
hat dann auch das erwartete Resultat true. Natürlich ist equals() nicht die
einzige Methode der Klasse String, da gibt es noch viele andere. Praktisch ist
zum Beispiel compareTo(), mit der Sie Zeichenketten alphapetisch vergleichen
können. Die Abfrage
s.compareTo(t)
für zwei Strings s und t liefert den Wert 0, wenn beide Strings inhaltsgleich
sind, den Wert −1, wenn s alphabetisch vor t liegt, und den Wert 1, wenn s
alphabetisch nach t liegt.

Strings
Zeichenketten sind Objekte der Klasse String. Werden verschiedene Objekt-
variablen mit der gleichen Zeichenkette initialisiert, so wird für sie nur ein
Objekt im Arbsitsspeicher abgelegt, auf das alle diese Variablen verweisen.
Für jede Stringvariable, die erst zur Laufzeit mit einem Wert belegt wird,
wird ein eigenes Objekt im Arbeitsspeicher angelegt. Da die Operation == die
Gleichheit von Objekten im Arbeitsspeicher abfragt, kann man normalerweise
die inhaltliche Gleichheit von Strings nicht mit == testen; zu diesem Zweck
gibt es die Methode equals().
Mit der Methode compareTo() kann man testen, ob ein String alphabe-
tisch vor oder hinter einem anderen liegt oder ob beide gleich sind. Weitere
Methoden kann man den bekannten Online-Dokumentationen entnehmen.

3.2.9 Methodenaufrufe

Auch ein klassisches Problem im Zusammenhang mit Methodenaufrufen ver-


steht man besser, wenn man gelegentlich an die intern verwendeten Zeiger
denkt. Erinnern Sie sich an den Unterschied zwischen dem call by value und
dem call by reference im Zusammenhang mit den C-Funktionen? Um Ände-
rungen an den übergebenen Parametern auch an der aufrufenden Stelle zu
spüren, musste ich mich mit Zeigern und Adressoperatoren herumschlagen
3.2 Klassen, Objekte und Methoden 249

und sowohl den *- als auch den &-Operator an der richtigen Stelle einsetzen.
Solche Operatoren habe ich in Java nicht, und deshalb müssen wir uns einmal
ansehen, wie eine Java-Methode mit ihren Parametern umgeht.
Zunächst die schlechte Nachricht: nichts könnte Java fremder sein als ein
call by reference, es kennt nur den call by value. Wenn Sie also einer Methode
einen Aktualparameter übergeben, dann wird zum Gebrauch in der Methode
eine lokale Kopie dieses Parameters erzeugt, mit der die Methode arbeiten
kann. Am eigentlich übergebenen Parameter ändert die Methode überhaupt
nichts. Die folgende Klasse Ding hat beispielsweise eine Methode aendern(),
die von einem ganzzahligen Parameter n abhängt.
class Ding
{
String eintrag;
void aendern(int n)
{
n = n*n;
}
}
Aber sobald Sie in einem Hauptprogramm die Zeilen
Ding ding = new Ding();
int m = 17;
ding.aendern(m);
zur Ausführung bringen, wird es der Variablen m völlig egal sein, was die
Methode aendern(m) gerne mit ihr anstelle würde; aendern() arbeitet mit
einer lokalen Kopie von m und nicht mit m selbst, weshalb nach dem Aufruf
aendern(m) die Variable m immer noch den Wert 17 haben wird und nicht
etwa 289.
Schon wieder ein Problem, das nach einer Lösung schreit. Alles kann man
nicht haben im Leben, und so ist es auch hier: an der Nicht-Änderbarkeit von
Elementen primitiver Datentypen können Sie nichts drehen. Sie brauchen sich
nur daran zu erinnern, dass es in C nötig war, auf die Speicheradressen der
Variablen zuzugreifen, und das in Verbindung zu bringen mit der Tatsache,
dass Sie in Java keinen Zugriff auf die Speicheradressen haben, um einzusehen,
dass Sie damit nicht weiterkommen. Aber so schlimm ist es nun auch wieder
nicht; sofern Sie mit Objekten umgehen wollen und nicht mehr mit primitiven
Datentypen, gibt es einen Weg, das reine call by value zu umgehen, sofern Sie
Ihre Klassen anständig mit set- und get-Methoden versehen haben. Nehmen
wir als Beispiel die Klasse
class GanzeZahl
{
int zahl;
void setZahl(int i)
{
zahl = i;
}
250 3 Objektorientierte Programmierung mit Java

int getZahl()
{
return zahl;
}
}
Sie ist streng nach Vorschrift aufgebaut und erlaubt daher auch eine Art
Simulation des call by reference, wie Sie gleich sehen werden. In einer anderen
Klasse könnte zum Beispiel das Folgende stehen.
class Ding
{
String eintrag;
void aendern(GanzeZahl z)
{
int i = z.getZahl();
z.setZahl(i*i) ;
}
}
Es ist kaum zu glauben, aber das funktioniert. In Ihrem Hauptprogramm
können Sie jetzt beispielsweise
Ding ding = new Ding();
GanzeZahl x = new GanzeZahl();
x.setZahl(10);
ding.aendern(x);
System.out.println(x.getZahl());
einfügen und werden feststellen, dass der Zahlenwert von x tatsächlich auf 100
steht.

x Objekt

lokale Kopie

Abb. 3.2. Parameteränderung in Java

Aber wie konnte das funktionieren? Noch einmal: ich habe mein Objekt
x als Parameter an die Methode aendern() der Klasse Ding übergeben, und
obwohl Java doch nur zu einem call by value in der Lage ist, wurde der In-
halt des Objektes x geändert. Das sieht mysteriöser aus, als es ist; bei Licht
3.2 Klassen, Objekte und Methoden 251

betrachtet fuktioniert die Sache genauso wie schon der call by reference in C.
Beim Methodenaufruf ding.aendern(x) wird der Methode eine lokale Kopie
von x übergeben, die ich in Abbildung 3.2 als z bezeichnet habe. Aber dieses
z ist ja keine Kopie des eigentlichen Datenobjekts, denn die Objektvariable
x war nur ein Zeiger auf die Speicherstelle, an der das Objekt abgelegt ist.
In z befindet sich also ebenfalls ein Zeiger auf das gleiche Datenobjekt wie
vorher, denn ich habe nur ein Objeklt dieser Art, an dem sich bisher noch
nichts geändert hat. Nun wird aber der Zahleninhalt dieses Objekts auf die
int-Variable i übertragen, und anschließend mit der setZahl()-Methode ein
neuer Zahlenwert gesetzt. An dem Parameter z selbst ändert sich dadurch
rein gar nichts, denn z zeigt immer noch auf eine bestimmte Speicherstelle
im Arbeitsspeicher, an der sich meine abgelegten Daten befinden. Aber durch
den Aufruf der set-Methode wird an eben dieser Speicherstelle eine Änderung
vorgenommen: wo vorher noch 10 stand, wird jetzt eine 100 notiert. Da das
Objekt selbst nicht als lokale Kopie vorliegt, sondern der aufgerufene Set-
ter direkt auf das korrekte Objekt zugreift, wird diese Änderung auch dann
bestehen bleiben, wenn Sie die Änderungsmethode wieder verlassen.
Eine Änderung der übergebenen Parameter ist also möglich, sofern es sich
dabei um Objekte handlet, deren Klassen über anständige Getter und Setter
verfügen. Da die Hüllenklassen leider keine Methoden zum Ändern ihrer Werte
haben, lässt sich das mit Objekten der Klassen Integer oder Double nicht
machen - man kann eben nicht immer gewinnen. Aus diesem Grund hatte ich
je gerade die Klasse GanzeZahl geschrieben, die diesen Mangel wieder behebt.

Änderung von Parameterwerten


An eine Methode übergebene Parameter werden immer nach dem Prinzip
call by value“ behandelt. Es wird also eine lokale Kopie angelegt, mit der

innerhalb der Methode gearbeitet wird, weshalb in der Methode an dem Pa-
rameter vorgenommene Änderungen nach dem Verlassen der Methode nicht
mehr wirksam sind. Werden Objekte als Parameter übergeben und stellt die
Klasse des Parameterobjektes Methoden zum Ändern der Membervariablen
zur Verfügung, so kann man durch Anwendung der Methoden auch innerhalb
der Methode Änderungen am übergebenen Objekt vornehmen.

3.2.10 Rekursion

Über rekursive Methodenaufrufe muss ich kaum etwas sagen, außer dass sie
genauso funktionieren, wie Sie es von C her kennen. Sehen Sie sich einmal die
folgende Klasse Rekursion an.
class Rekursion
{
int summe(int n)
{
if (n==1)
252 3 Objektorientierte Programmierung mit Java

return 1;
else
return n+summe(n-1);
}
}
Wie von den rekursiven Funktionen in C gewohnt, ruft sich summe() selbst
auf, und zwar mit einem um 1 reduzierten Parameterwert, damit eine Chan-
ce besteht, sich der Abbruchbedingung zu nähern. Falls Sie also mit einer
natürlichen Zahl die Methode summe() starten, wird alles gut gehen; bei einer
negativen Zahl geraten Sie natürlich in eine Endlosschleife, weil Ihr Einga-
beparameter nie den Wert 1 erreichen kann. Mit einer weiteren if-Abfrage
können Sie das aber ohne Probleme abfangen.

Rekursive Methoden
Der Einsatz von Methoden, die sich selbst aufrufen und daher rekursiv sind,
ist in Java problemlos möglich und erfolgt analog zum Einsatz rekursiver
Funktionen in der Sprache C.

Übungen

3.7. Schreiben Sie ein Programm zur Lösung linearer Gleichungen ax + b = 0


in der Art, dass es in Form von Klassen und Methoden aufgebaut ist. Gehen
Sie dabei folgendermaßen vor. Schreiben Sie eine Klasse Linear, die folgendes
enthält:
• zwei Membervariablen a, b, die zur Aufnahme der Koeffizienten der linea-
ren Gleichung dienen;
• einen Konstruktor mit dem Namen Linear, der die zwei Formalparameter
a_par, b_par erwartet und die Werte dieser Parameter seinen Memberva-
riablen a und b übergibt;
• eine Methode loesen(), die das Lösungsverfahren in Form der Formel
x = − ab enthält und in Verbindung mit einem Objekt der Klasse Linear
aufgerufen werden kann und muss.
Schreiben Sie weiterhin eine Klasse LinearTest, in der die Methode main()
(also das Hauptprogramm) steht. In dieser Klasse programmieren Sie den
Ablauf des Dialogs mit dem Benutzer, also die Eingabeaufforderung für die
Koeffizienten der Gleichung. Die Klasse LinearTest muß natürlich Objekte
der Klasse Linear verwenden.
3.8. Schreiben Sie ein Programm zur Lösung einer beliebigen Zahl quadrati-
scher Gleichungen x2 + px + q = 0 in der Art, dass es in Form von Klassen
und Methoden aufgebaut ist. Gehen Sie dabei folgendermaßen vor. Schreiben
Sie eine Klasse Gleichung, die Folgendes enthält:
• zwei Membervariablen p, q, die zur Aufnahme der Koeffizienten der qua-
dratischen Gleichung dienen;
3.2 Klassen, Objekte und Methoden 253

• einen Konstruktor mit dem Namen Gleichung, der die zwei Formalpa-
rameter p_par, q_par erwartet und die Werte dieser Parameter seinen
Membervariablen p und q übergibt;
• eine Methode loesen(), die das Lösungsverfahren in Form der p, q-Formel
enthält und in Verbindung mit einem Objekt der Klasse Gleichung auf-
gerufen werden kann und muss.
Schreiben Sie weiterhin eine Klasse GleichungTest, in der die Methode
main() (also das Hauptprogramm) steht. In dieser Klasse programmieren Sie
den Ablauf des Dialogs mit dem Benutzer, also die Eingabeaufforderung für
die Koeffizienten der Gleichungen und eine Schleife, damit eine beliebige An-
zahl von Gleichungen gelöst werden kann. Die Klasse GleichungTest muss
natürlich Objekte der Klasse Gleichung verwenden.

3.9. Schreiben Sie eine Klasse Rechteck mit den beiden Membervariablen
breite und hoehe, einer get- und einer set-Methode für beide Membervaria-
blen, einer Methode flaeche(), in der die Fläche eines gegebenen Rechtecks
berechnet wird, und zwei verschiedenen Konstruktoren Ihrer Wahl. Testen
Sie Ihre Klasse mit Hilfe einer Klasse RechteckTest, die die main()-Methode
enthält.

3.10. Schreiben Sie eine Klasse Feld zur Verarbeitung von Feldern ganzer
Zahlen, die folgendes enthält:
• eine Membervariable laenge, die zur Aufnahme der Länge des Feldes
dient;
• eine Membervariable a, mit der ein Feld aus ganzen Zahlen deklariert wird,
dessen konkrete Länge allerdings erst im Konstruktor festgelegt wird;
• einen Konstruktor mit dem Namen Feld, der den entsprechenden Formal-
parameter laenge_par erwartet, die Werte dieses Parameters der Mem-
bervariablen laenge übergibt und ein Feld der Länge laenge anlegt;
• Methoden zum Anlegen und Ausgeben eines Feldes;
• eine Methode istSortiert(), mit der festgestellt werden kann, ob das
Feld aufsteigend sortiert ist, wobei der Rückgabewert der Methode vom
Typ boolean sein soll;
• eine Methode sucheMaximum(), die den größten Eintrag der Folge zurück-
gibt.
Schreiben Sie weiterhin eine Klasse FeldTest, in der die Methode main() (also
das Hauptprogramm) steht. In dieser Klasse programmieren Sie den Ablauf
des Dialogs mit dem Benutzer, also die Eingabeaufforderung für die Einträge
des Feldes sowie die Aufrufe der Methoden für das Feld.

3.11. Schreiben Sie eine Klasse, mit der die Flugroutenverwaltung aus Auf-
gabe 2.24 realisiert wird. Alle Aktionen wie ausgeben oder löschen sind als
Methoden einer Klasse Flugroute zu programmieren, die Sie als einen Vek-
tor aus Strings realisieren sollen.
254 3 Objektorientierte Programmierung mit Java

3.12. Erweitern Sie die Klasse Ding aus dem letzten Abschnitt um eine Metho-
de tauschen(GanzeZahl zahl1, GanzeZahl zahl2), die die Inhalte zweier
Objekte der Klasse GanzeZahl miteinander vertauscht.

3.3 Vererbung
Das Konzept der Vererbung gehört zu den wichtigsten Konzepten in der ge-
samten objektorientierten Programmierung. Die Grundidee ist einfach genug:
wenn man schon irgendwelche Datenfelder oder auch Funktionalitäten in Form
von Methoden einer Klasse abgelegt hat, dann sollte man bei einer irgendwie
ähnlichen Klasse - was immer Ähnlichkeit hier auch bedeuten mag - nicht
noch einmal das Gleiche machen müssen, sondern die bereits geleistete Arbeit
ohne Probleme übernehmen können. Die Realisierung dieser einfachen Idee ist
das beliebte Prinzip der Vererbung, das leicht klar wird, wenn Sie sich vom
testamentarischen Erbbegriff entfernen. Sobald nämlich über ein Testament
oder im Rahmen der gesetzlichen Erbfolge etwas zu vererben ist, muss man
leider davon ausgehen, dass der sogenannte Erblasser nichts mehr davon hat,
während die Erben die neuen und alleinigen Nutznießer sind. Diese Art der
Übertragung hat nicht nur Jean Luc Picard als nicht übermäßig wichtig be-
zeichnet, sie ist auch in der objektorierentierten Programmierung gar nicht
mit dem Begriff Vererbung gemeint; es hätte ja auch wenig Sinn, wenn eine
Klasse alle ihre Attribute und Methoden an eine andere abgibt und sie dann
selbst nicht mehr benutzen kann. Sie müssen sich vielmehr am biologischen
Vererbungsbegriff anlehnen, um zu verstehen, worum es geht: ein Kind erbt
gewisse Eigenschaften seiner Eltern, ohne dass die Eltern diese Eigenschaften
verlieren. Wenn ich einmal davon absehe, dass Kinder ihre positiven Eigen-
schaften natürlich von den Eltern haben, während ihre Schlechtigkeiten ohne
Zweifel auf schädlliche Umwelteinflüsse zurückzuführen sind, funktioniert auf
genau diese Weise auch die Vererbung in Java. Man lässt eine Klasse von einer
anderen erben, indem man die neue Klasse, also sozusagen die Kindklasse, als
Kind der alten Klasse markiert und damit festlegt, dass sie die Memberva-
riablen und die Methoden der alten Klasse verwenden kann als wären es ihre
eigenen. Die abgeleitete Kindklasse erbt in diesem Sinne die Eigenschaften,
also die Membervariablen und die Methoden, der alten Elternklasse, und man
muss den ganzen Kram für die Kindklasse nicht noch einmal neu definieren.
Wie das im Einzelnen vor sich geht, werde ich Ihnen jetzt zeigen.

3.3.1 Erweitern einer Klasse

Kinder sind keine Klone. Ihre Nachkommen werden zwar zwangsläufig Eigen-
heiten ihrer Vorfahren an sich haben und vielleicht einen lebenslangen vergeb-
lichen Kampf ausfechten, um sie los zu werden, aber in jedem Fall werden Sie
auch sehr eigene Eigenschaften entwickeln und ihre Probleme vielleicht mit
Methoden lösen, auf die ihre Eltern nie gekommen wären. Ob diese Methoden
3.3 Vererbung 255

besser oder schlechter sind als die alten, ist eine ganz andere Frage, mich in-
teressiert hier nur, dass es sich um neue Methoden handelt, die die vererbten
Kenntnisse und Gewohnheiten übersteigen.
Die Bildung erbender Klassen in Java läuft nicht anders. Einerseits über-
nimmt so eine erbende Klasse die Attribute und Methoden der vererbenden
Klasse, andererseits hätte die ganze Erberei wenig Sinn, wenn die neue Klas-
se ein genauer Klon der alten Klasse wäre: es müssen neue Aspekte hinzu
kommen, neue Attribute oder neue Methoden oder beides. Die erbende Klas-
se wird daher die alte Klasse erweitern, sie übernimmt das Altbekannte und
schafft zusätzlich Neues. Nun muss man diesen Erweiterungsvorgang aber
irgendwie dem Java-Compiler verständlich machen, und da erweitern“ auf

Englisch extend“ heißt, liegt es nahe, eine erbende Klasse mit dem Schlüssel-

wort extends auszuzeichnen. Die neue Klasse erweitert die Klasse, deren Ei-
genschaften und Methoden sie erbt. Eine Klasse erweitert also eine andere,
indem sie die Eigenschaften der alten Klasse übernimmt und zusätzlich noch
weitere Membervariablen oder auch Methoden einfügt, die sich aus den spe-
ziellen Zielen der neuen Klasse ergeben. Das Ganze passiert natürlich nicht
zum Spaß oder um eine neue Syntax auszuprobieren, sondern hat das Ziel,
die Möglichkeiten der alten Klasse auch in einer neuen Klasse auszunutzen,
ohne die Programmierung wiederholen zu müssen.
Anstatt aber ständig die unklaren Formulierungen alte Klasse“ und neue
” ”
Klasse“ oder vererbende Klasse“ und erbende Klasse“ zu gebrauchen, spicht
” ”
man üblicherweise von Superklassen und Subklassen. Eine Superklasse ist also
in unserem Kontext keine besonders tolle Klasse, sondern einfach nur eine
Klasse, die ihre Attribute und Methoden an eine andere Klasse vererbt - eben
an die Subklasse. Synonyme für Subklasse sind die Begriffe Unterklasse und
abgeleitete Klasse, während man anstelle von Superklasse auch oft Oberklas-
se oder auch Basisklasse sagt. Eine Subklasse wird also von einer Basisklasse
abgeleitet und ist dann eine Unterklasse dieser Basisklasse, während die Basis-
klasse die Superklasse der Subklasse ist. Verwirrt? Macht nichts, nach einem
Beispiel wird gleich alles viel klarer.
Es ist nicht ungewöhnlich, dass in einem Unternehmen oder auch von
Privatleuten Personen verwaltet werden müssen. Falls Sie beispielsweise Ihr
Adressbuch nicht mehr finden oder überhaupt keines haben, weil Sie es ja
ohnehin nie finden würden, liegt es nahe, die Daten der für Sie relevanten
Personen maschinell zu verwalten, denn den PC werden Sie wohl nicht so
schnell verlegen wie ein Adressbuch. In Java geschieht das natürlich mit Hilfe
einer Klasse, die ich im folgenden aufliste.
class Person
{
String name;
String vorname;
int telefon;
String adresse;
256 3 Objektorientierte Programmierung mit Java

public void datenAusgeben()


{
System.out.println("Name: " + name);
//...
}
public void datenEingeben()
{
System.out.println("Namen eingeben: ");
name = Eingabe.lesenstring();
//...
}
//...weitere Methoden zum Aendern und Loeschen
}
Betrachten Sie diese Klasse nicht ale eine vollständige Beschreibung einer
Person; ich habe mich hier auf die Attribute name, vorname, telefon und
adresse beschränkt, die im richtigen Leben wohl kaum ausreichen würden.
Da Sie mit den Daten auch etwas anfangen wollen, sind auch die wichtigsten
Methoden angedeutet: man soll Daten ein- und ausgeben können und nach
Bedarf in der Lage sein, Datensätze zu ändern und zu löschen. Wie diese
Methoden im Einzelnen programmiert sind, ist für unsere Zwecke nicht weiter
wichtig, denn erstens sind das sehr einfache Operationen, die Sie inzwischen
im Schlaf beherrschen, und zweitens kommt es mir hier vor allem auf den
Mechanismus des Erweiterns und Erbens an.
Der ist jetzt nämlich ganz einfach. Stellen Sie sich vor, die Klasse Person
gehört zur Datenverarbeitung einer Firma, die auf diese Weise die üblichen
einer Person zugehörigen Daten ablegt und verarbeitet. Nun gibt es aber be-
stimmte Personen, die für Ihre Firma nicht einfach nur Personen mit einem
Namen, einer Telefonnummer und einer Adresse sind, sondern beispielswei-
se Mitarbeiter oder Kunden. Niemand bestreitet, dass auch ein Mitarbeiter
einen Namen und eine Adresse hat, aber das reicht einfach nicht, man braucht
noch einiges an mitarbeiterspezifischen Daten und Methoden, die in der all-
gemeinen Personenverwaltung nicht vorgesehen sind. Und hier kommt nun
die Vererbung ins Spiel. Die folgende Klasse Mitarbeiter zeigt Ihnen, wie es
geht.
class Mitarbeiter extends Person
{
String funktion;
String familienstand;
int kinderzahl;
int steuerklasse;
double bruttogehalt;
//... weitere Attribute

public double nettoBerechnen()


{
3.3 Vererbung 257

//...
}

//...weitere Methoden
}
Ein Mitarbeiter hat irgendeine Funktion in der Firma, aber vor allem brau-
chen Sie Daten für die Buchhaltung, damit man sein Gehalt korrekt berechnen
und auszahlen kann. Diese Berechnung muss dann irgendwo durchgeführt wer-
den, und was wäre ein besserer Platz für die Durchführung der Berechnung
als eine Methode? Die für den Mitarbeiter so dringend benötigte Methode
nettoBerechnen() ist für die allgemeine Personenverwaltung ohne jede Be-
deutung: Gehälter bekommen nur die Mitarbeiter. Dass man noch weitere
Methoden brauchen wird wie zum Beispiel die Berechnung der Sozialversiche-
rungsbeiträge oder das Durcken von Überweisungsträgern, brauche ich kaum
zu erwähnen.
Wie hängen nun die Klassen Person und Mitarbeiter zusammen? Mit
der Klasse Person wird eine Klasse zur Verwaltung beliebiger Personen defi-
niert. Indem ich die Klasse Mitarbeiter durch
class Mitarbeiter extends Person
deklariert habe, erweitert die Klasse Mitarbeiter meine allgemeine Klasse
Person, sodass also Person die Basisklasse der Subklasse Mitarbeiter wird.
Durch die schlichte Angabe, dass die Subklasse die Basisklasse erweitert - was
ich mit Hilfe des Schlüsselwortes extends angegeben habe - wird klargestellt,
dass jedes Objekt der Klasse Mitarbeiter auch automatisch und ganz von
alleine ein Objekt der Klasse Person ist. Jedes Objekt der abgeleiteten Klasse
ist automatisch auch ein Objekt der Basisklasse und darf daher auch auf die
Membervariablen und Methoden der Basisklasse zugreifen. In einem Haupt-
programm kann ich daher ein Objekt
Mitarbeiter knecht = new Mitarbeiter();
anlegen. Das neue Objekt knecht hat dann neben den Membervariablen und
Methoden, die in der Klasse Mitarbeiter abgelegt sind, auch Zugang zu den
entsprechenden Größen der Klasse Person, denn schließlich ist jeder Mitar-
beiter auch eine Person. Deshalb kann man also neben den selbstverständlich
möglichen Aufrufen wie
knecht.nettoBerechnen();
auch die Methoden von Person aufrufen wie zum Beispiel
knecht.datenAusgeben();
oder
knecht.datenEingeben();
Da die Subklasse Mitarbeiter die Basisklasse Person erweitert, wird der
Mitarbeiter knecht problemlos als Person erkannt und datenAusgeben() gibt
seine personenbezogenen Daten aus, sofern man sie vorher entsprechend defi-
niert hat. Aber natürlich kann diese Methode datenAusgeben() auch nur das
tun, wozu sie programmiert wurde, also ausschließlich die personenbezogenen
Daten ausgeben; sie ist ja immerhin eine Methode der Klasse Person, in der
258 3 Objektorientierte Programmierung mit Java

man überhaupt keine Mitarbeiterdaten kennt. Das ist ein wenig schade, denn
bei einem Mitarbeiter wäre es sinnvoll, auch solche Daten wie die Steuerklas-
se ausgeben zu können, die von der Person-Methode datenAusgeben() nicht
erfasst werden. Nicht verzagen, auch dieses Problem hat eine einfache Lösung,
auf die ich gleich zu sprechen kommen werde.
Zunächst muss ich aber noch einen wichtigen Unterschied zwischen Java
und C++ erwähnen, den man am besten wieder anhand der beiden eben er-
stellten Klassen einsehen kann. Selbstverständlich ist jeder Mitarbeiter auch
eine Person, deshalb ist ja auch die Klasse Mitarbeiter eine Erweiterung der
Klasse Person. Aber mit dem gleichen Recht könnte ein Mitarbeiter nicht
nur als Person betrachtet werden, sondern auch zusätzlich von einer Klas-
se Kostenstelle abgeleitet werden, da er auf jeden Fall Kosten verursachen
wird und die eine oder andere Membervariable oder Methode einer Kosten-
stelle auch für Mitarbeiter nützlich sein mag. Nun gut, warum nicht, Kinder
erben ja auch von beiden Eltern und nicht nur von einem Elternteil. Was aber
in der Biologie funktioniert, kann in der Programmierung zu Schwierigkei-
ten führen. Sie brauchen sich nur vorzustellen, dass die beiden Basisklassen
Person und Kostenstelle eine Membervariable gleichen Namens und glei-
chen Typs aufweisen und ein Objekt der abgeleiteten Klasse Mitarbeiter
auf genau diese Membervariable zugreifen will. Wie ist die dann zu verstehen,
ist es eine Eigenschaft des Mitarbeiters als Person oder als Kostenstelle? Das
geplagte Mitarbeiterobjekt wird sich in dieser Situation ähnlich schwer ent-
scheiden können wie ein Scheidungskind, und um ihm diese Lage zu ersparen,
hat man sie in Java schlicht verboten. Die Mehrfachvererbung, bei der eine
Subklasse von mehreren Basisklassen abgeleitet wird, gibt es in Java nicht; es
ist einer Java-Klasse nicht möglich, von mehr als zwei Klassen gleichzeitig zu
erben, wie es z.B. in C++ jederzeit erlaubt ist, aber auch immer wieder zu
Problemen führt. Eine abgeleitete Klasse kann nur eine einzige Superklasse
haben, während eine Superklasse natürlich massenweise Subklassen aufweisen
darf. Eine Klassendeklaration der Art
Mitarbeiter extends Person, Kostenstelle
ist daher unmöglich; hinter extends kann immer nur der Name einer einzigen
Klasse stehen

Erweitern einer Klasse


Eine Subklasse kann von einer Oberklasse erben, was mit dem Schlüsselwort
extends bei der Deklaration der Subklasse ausgedrückt wird. Auf diese Weise
ist jedes Objekt der Subklasse auch gleichzeitig ein Objekt der Superklasse
und kann mit den in der Superklasse definierten Membervariablen und Me-
thoden arbeiten. Die Mehrfachvererbung, bei der eine Subklasse von mehreren
Basisklassen abgeleitet wird, gibt es in Java nicht.

Auch wenn in unseren Breiten die Einkindfamilie immer üblicher wird,


müssen Sie sich in der objektorientierten Programmierung nicht auf ein Kind
beschränken. Eine Subklasse darf zwar nur von einer Basisklasse abgeleitet
3.3 Vererbung 259

werden, aber umgekehrt kann eine Basisklasse eine Unmenge von Subklassen
haben, die sich alle von der gleichen Basisklasse ableiten. Schon meine Per-
sonenklasse eignet sich für weitere Erweiterungen ungemein gut, denn Ihre
Firma wird neben Mitarbeitern vermutlich auch noch mit Kunden und Liefe-
ranten zu tun haben. Obwohl man in manchen Geschäften einen gegenteiligen
Eindruck bekommen könnte, sind auch Kunden Menschen und damit Per-
sonen, und von Lieferanten hört man ein ähnliches Gerücht. Somit sind die
Deklarationen
class Kunde extends Person
und
class Lieferant extends Person
sinnvolle Erweiterungen der Klasse Person, sofern Sie die neuen abgeleiteten
Klassen mit weiteren Methoden zur spezifischen Verarbeitung der Kunden-
und Lieferantendaten versehen. Sie werden gleich sehen, was das für Konse-
quenzen hat.

3.3.2 Konstruktoren und Zuweisungen

Wie Sie wissen, gibt es zu jeder Klasse den einen oder anderen Konstruktor,
und wenn Sie keinen definieren, dann wird der Standardkonstruktor heran-
gezogen. Meine Klasse Person und die von ihr abgeleiteten Klassen kamen
mit dem Standardkonstruktor aus und haben auf weitere Konstruktoren ver-
zichten können, aber das ist ein wenig schade, denn auf diese Weise ist ein
Zusammenspiel zwischen den Konstruktoren der Basisklasse und der Subklas-
sen unmöglich: was nicht da ist, kann nicht zusammenarbeiten. Sehen wir uns
deshalb einmal zwei neue Klassen an; zunächst die Basisklasse.
class Test
{
String inhalt;
Test(String inhalt_par)
{
inhalt = inhalt_par;
}
}
Eine kleine und übersichtliche Klasse, mit der man nicht viel anfangen kann,
aber immerhin hat sie einen expliziten Konstruktor, und alles weitere kann
die folgende Subklasse übernehmen.
class TestErbe extends Test
{
TestErbe(String inhalt_par)
{
super(inhalt_par);
System.out.println("Inhalt wurde gefuellt");
}
}
260 3 Objektorientierte Programmierung mit Java

Hier tritt etwas Neues auf, nämlich das Schlüsselwort super. Indem Sie in
einem Konstruktor einer abgeleiteten Klasse super() aufrufen, wird der Kon-
struktor der Basisklasse in Aktion gesetzt, der dann natürlich die passenden
Parameter braucht. Ich musste daher meinem super()-Aufruf einen String-
Parameter mitgeben und habe damit dafür gesorgt, dass die Membervariable
inhalt mit diesem String-Parameter gefüllt wird. Nicht vergessen: TestErbe
erbt von Test und besitzt daher ebenfalls die Membervariable inhalt, die
durch den Aufruf des Konstruktors von Test mit einem Wert belegt wird.
Und so geht das tatsächlich immer. Da ein Objekt einer Subklasse im-
mer auch gleichzeitig Objekt der Basisklasse ist, sind die Anweisungen im
Konstruktor der Basisklasse auch sinnvolle Anweisungen für ein Objekt der
Subklasse. Um nicht alles zweimal programmieren zu müssen, können sie des-
halb im Konstruktor der Subklasse den Konstruktor der Basisklasse durch
die schlichte Anwendung von super() aufrufen. Sie müssen dabei nur darauf
achten, dass der super()-Aufruf die richtigen Parameter bekommt, denn er
entspricht einem Aufruf eines Konstruktors der Basisklasse. Das funktioniert
natürlich auch, wenn Ihre Basisklasse mit zwei oder mehr Konstruktoren ge-
segnet ist: hat sie beispielsweise einen parameterlosen Konstruktor und einen,
der unbedingt einen String als Parameter braucht, dann können Sie in der
Subklasse ohne Probleme sowohl super() ohne jeden Parameter aufrufen als
auch super("Ding"), denn schließlich ist "Ding" eine Zeichenkette. Sie soll-
ten nur darauf achten, dass Sie in einem Konstruktor der Subklasse auch nur
einen super()-Aufruf tätigen, da ein Objekt nur einmal angelegt wird.

Konstruktoraufrufe
Eine Subklasse kann in ihrem Konstruktor einen Konstruktor ihrer Basisklas-
se durch super(Parameterliste) aufrufen. Die Parameterliste von super()
muss dabei der Parameterliste eines Konstruktors der Basisklasse entsprechen.

Das super()-Kommando wird uns später beim Überschreiben von Metho-


den noch einmal begegnen. Jetzt will ich erst einmal auf die Tatsache zurück-
kommen, dass man zu einer Basisklasse mehrere Subklassen bilden kann, wie
ich das schon bei der Basisklasse Person mit den Subklassen Mitarbeiter,
Kunde und Lieferant gemacht habe. Keine besondere Sache, werden Sie den-
ken, und Sie haben damit auch recht, aber es hat eine recht praktische Konse-
quenz. Jedes Objekt jeder abgeleiteten Klasse ist auch automatisch ein Objekt
der Klasse Person, kann also die Methoden von Person anwenden. Nun kann
es Ihnen ja passieren, dass Sie die personenbezogenen Daten aller Mitarbeiter,
Kunden und Lieferanten vor Augen haben müssen, weil Sie Weihnachtskarten
schreiben wollen oder allen Beteiligten mitteilen wollen, dass Ihr Laden wegen
eines Lottogewinns des Besitzers geschlossen wird. In diesem Fall können Sie
einfach alle Kunden, Lieferanten und Mitarbeiter als Personen betrachten und
ignorieren, dass sie auch noch zusätzliche Attribute und Methoden haben. Das
kann man beispielsweise mit einem Feld aus Personenobjekten realisieren, also
durch
3.3 Vererbung 261

Person[] leute = new Person[10];


Jedem Eintrag dieses Feldes können Sie nun ein Personenobjekt zuordnen,
aber da jedes Objekt von Mitarbeiter, Kunde oder Lieferant gleichzeitig
auch zur Klasse Person gehört, stört es leute[0] oder ein anderes Feldele-
ment überhaupt nicht, wenn ihm ein Objekt einer der abgeleiteten Klassen
zugewiesen wird. Sie dürfen also klaglos so etwas wie
leute[0] = new Mitarbeiter();
oder auch
leute[i] = new Kunde();
schreiben, wobei natürlich i eine passend belegte int-Variable sein muss.
Was bringt Ihnen das? Eine Menge, weil jetzt alle Objekte, die irgendet-
was mit Personen zu tun haben, in einer einzigen Datenstruktur, nämlich mei-
nem Feld leute, abgelegt und somit einer einheitlichen Verarbeitung zugäng-
lich sind. Was könnte Sie jetzt noch daran hindern, in einer for-Schleife
for(int i=0; i<10; i++)
leute[i].datenAusgeben();
das gesamte Feld einheitlich zu verarbeiten und alle Personendaten auszuge-
ben? Nichts außer vielleicht einem abstürzenden Computer oder einem klin-
gelnden Telefon. Es braucht sie jetzt nicht mehr zu interessieren, ob die Leute
ursprünglich Mitarbeiter, Kunden oder Lieferanten waren: in Ihrem Feld sind
alle nur noch Personen und daher nur noch den für Personen gültigen Me-
thoden unterworfen. Einen Nachteil hat es aber. Auch wenn zum Beispiel
leute[0] ursprünglich einmal ein Mitarbeiter war, werden Sie die mitarbei-
terspezifischen Daten und Methoden für die Feldelemente nicht verwenden
können. Ein Methodenaufruf wie leute[0].nettoBerechnen() ist sinnlos
und führt zu einer Fehlermeldung des Compilers, weil in leute nun einmal
schichte Personen stehen und nichts anderes. Aber bei Licht betrachtet macht
das nichts, denn diese Art der Verarbeitung werden Sie ja genau dann anwen-
den, wenn es darum geht, den gemeinsamen Kern verschiedener Subklassen
zu verwenden und eben nicht die spezifischen Einzelheiten von Mitarbeitern
oder Kunden zu beachten - der Weihnachtskarte kann das Nettogehalt egal
sein, sie braucht nur den Namen und die Adresse.

Zuweisung
Jedes Objekt einer Subklasse kann einer Objektvariablen der zugehörigen Ba-
sisklasse zugewiesen werden. Diese Objektvariable behandelt dann das Ob-
jekt der Subklasse als ein Objekt der Basisklasse und kann nur die Methoden
der Basisklasse verwenden. Auf diese Weise ist es möglich, für Objekte ver-
schiedener Subklassen einer Basisklasse Verarbeitungen, die in Methoden der
Basisklasse abgelegt sind, einheitlich durchzuführen.

3.3.3 Zugriffsattribute
Bisher bin ich mit den Membervariablen und Methoden, die ich an eine Sub-
klasse vererben wollte, recht sorglos umgegangen und habe sie entweder gar
262 3 Objektorientierte Programmierung mit Java

nicht oder gleich als public qualifiziert, weshalb jeder auf sie zugreifen konn-
te. Ich hatte Ihnen aber schon berichtet, dass man Membervariablen eigent-
lich eher als private qualifizieren und dann über entsprechende public-
Methoden auf sie zugreifen sollte. Schon wahr, aber nicht alles Wahre ist
immer praktisch anwendbar. Im Zusammenhang mit Vererbung sollten Sie
bei private-Qzualifizierungen etwas Vorsicht walten lassen, wie das folgende
Beispiel zeigt.
class Oben
{
private int inhalt;
public void setInhalt(int i)
{
inhalt = i;
}
public int getInhalt()
{
return inhalt;
}
}
Die Klasse Oben dient nur zur Verdeutlichung eines Problems, dshalb erspare
ich Ihnen und mir alle unnötigen Attribute und Methoden. Sie ist mit einem
private-Attribut und zwei public-Methoden schulmäßig aufgebaut und soll-
te also keine Probleme aufwerfen, aber manchmal freut man sich eben zu früh:
sobald Sie die Klasse erweitern, fangen die Probleme schon an.
class Unten extends Oben
{
void ausgeben()
{
System.out.println(inhalt*inhalt);
}
}
Die Klasse Unten wird von Oben abgeleitet, hat also Zugriff auf alles, was
Oben zu bieten hat - sollte man zumindest denken. Stimmt aber nicht. Da die
Membervariable inhalt in Oben als private deklariert war, kann tatsächlich
nur die Klasse Oben direkt auf inhalt zugreifen. Es nützt gar nichts, dass
jedes Objekt von Unten auch gleichzeitig ein Objekt von Oben wäre, denn
Sie kommen erst gar nicht so weit, Objekte der Klasse Unten anzulegen. Da
in Unten versucht wird, auf ein privates Element einer anderen Klasse zuzu-
greifen, haut Ihnen der Compiler schon beim Übersetzen eine Fehlermeldung
um die Ohren und wird Ihre Klasse Unten wegen unerlaubten Zugriffs nicht
erstellen.
Es gibt hier also einen Widerspruch zwischen dem Bedürfnis, die Mem-
bervariablen durch private zu schützen und dem Bedürfnis, Vererbungen
vorzunehmen. Glücklicherweise ist dieses Problem auch schon den Entwick-
lern von Java aufgefallen, und sie haben eine einfache Lösung dafür parat:
3.3 Vererbung 263

die Qualifizierung als protected. Sobald Sie eine Membervariable oder eine
Methode als protected bezeichnen, kann man sowohl innerhalb der Klas-
se, in der sie steht, als auch in jeder abgeleiteten Klasse auf sie zugreifen -
und sonst nicht. Der Qualifizierer protected übernimmt also die Rolle von
private für jede Klasse, die nicht von meiner gerade zur Diskussion stehen-
den Klasse abgeleitet ist, und er macht meine Daten und Methoden für alle
abgeleiteten Klassen zugänglich. Die Membervariable inhalt aus der Klasse
Oben muss deshalb nur als protected gekennzeichnet werden, und schon ist
das Problem gelöst.
Damit haben Sie schon vier Zugriffsattribute kennengelernt. Mit public ist
alles zugänglich, mit private dürfen Sie sich nur in der definierenden Klasse
aufhalten, mit protected sorgen Sie für Zugänglichkeit in allen abgeleiteten
Klassen, und mit dem leeren Zugriffsattribut stehen Ihre Membervariablen
und Methoden im aktuellen Verzeichnis jedem offen. Auch das fünfte und letz-
te Zugriffsattribut final haben Sie schon im Zusammenhang mit Konstanten
gesehen. Welche Zugriffsrechte und -einschränkungen damit verbunden sind,
zeige ich Ihnen im nächsten Abschnitt, der sich mit dem Überschreiben von
Methoden beschäftigt.

Das Zugriffsattribut protected


Wird eine Membervariable oder eine Methode mit protected gekennzeichnet,
so ist das Element in seiner eigenen Klasse und in jeder von dieser Klasse
abgeleiteten Klasse zugänglich. Dagegen kann auf private-Elemente nicht in
abgeleiteten Klassen zugegriffen werden.

Ich sollte vielleicht noch erwähnen, dass man auch ganze Klassen qualifi-
zieren kann; so ist zum Beispiel eine als public gekennzeichnete Klasse für
jeden und von überallher benutzbar. Und mithilfe einer public-Klasse können
Sie die alte Regel, dass man in einer Datei nur eine Klasse abspeichern sollte,
aushebeln: in einer Datei darf nur eine einzige als public qualifizierte Klasse
stehen, die anderen haben dann in der Regel überhaupt keinen Qualifizierer.
Aus Gründen der Übersichtlichkeit und Ordnung rate ich von diesem Verfah-
ren ab und empfehle Ihnen, auch weiterhin eine Datei pro Klasse anzulegen.
Falls Sie dazu aber keine Lust haben, müssen Sie wenigstens darauf achten,
dass der Name der Datei dem Namen der public-Klasse entspricht. In der Re-
gel wird man dann die Klasse, in der die main()-Methode steht, als public
bezeichnen und alle anderen Klassen, die in der main()-Methode benutzt wer-
den, nicht näher qualifizieren, damit sie in der gleichen Datei Platz haben. Ob
das die Übersichtlichkeit erhöht, überlasse ich Ihrem eigenen Urteil.

3.3.4 Überschreiben von Methoden und Polymorphie

Die Polymorphie ist im Gegensatz zur Polygamie kein strafbarer Tatbestand


und auch keine ansteckende Krankheit, sondern etwas sehr Praktisches im
Zusammenhang mit der objektorientierten Programmierung. Gemeint ist die
264 3 Objektorientierte Programmierung mit Java

Fähigkeit eines Objekts, die jeweils richtige Methodenauswahl zu treffen, wenn


mehrere Methoden zur Verfügung stehen. Sobald nämlich ein Objekt eine Me-
thode aufruft, gibt es mehrere Möglichkeiten, wo sich diese Methode befinden
kann: die Klasse des Objekts kann die Methode selbst definiert haben, die
Methode kann aus der Superklasse der gerade aktuellen Klasse übernommen,
also geerbt worden sein, oder aber die aktuelle Klasse hat eine gleichnami-
ge Methode einer Superklasse überschrieben. Die ersten beiden Möglichkeiten
kennen Sie schon, die nächste werde ich jetzt erklären.
Eigentlich geht es um etwas ganz Einfaches. Eine Methode einer Basisklas-
se hat selbstverständlich einen Namen, und man könnte auf die Idee kommen,
diesen Methodennamen, die in der Basisklasse bereits verwendet wurde, in
einer Subklasse wieder zu verwenden - und zwar, falls gewünscht, mit ganz
anderen Funktionalitäten der Methode. Hat zum Beispiel die Basisklasse die
Methode ding(), so kann ich auch in der Subklasse eine neue Methode ding()
definieren, die einen völlig anderen Methodenrumpf haben kann als die gleich-
namige Methode der Basisklasse. Rufe ich also für ein Objekt x der Basisklasse
die Methode x.ding() auf und anschließend für ein Objekt y der abgeleiteten
Klasse die Methode y.ding(), so heißen die Methoden zwar gleich, aber es
sind zwei verschiedene Methoden damit gemeint - einmal die in der Basis-
klasse definierte, einmal die in der Subklasse beschriebene. Eine Bedingung
gibt es aber, sonst funktioniert das Ganze nicht: die Parameterlisten der bei-
den gleichnamigen Methoden müssen genau die gleiche Struktur haben, die
Methode muss in der Subklasse die gleiche Signatur aufweisen wie in der Ba-
sisklasse. Wenn nun ein Objekt der Subklasse die Methode ding() aufruft,
dann erkennt Java automatisch, dass es die Methode der Subklasse verwen-
den will, da es sich ja selbst in der Subklasse befindet. Hat man dagegen ein
Objekt der Basisklasse, das ebenfalls die Methode ding() aufruft, so wird auf
jeden Fall die Methode der Basisklasse genommen. Dieses automatische Er-
kennen der richtigen Methode, je nach aufrufendem Objekt, bezeichnet man
als Polymorphie. Sie stellt sicher, dass Sie für abgeleitete Klassen ein spezielles
Verhalten festlegen, aber dennoch im Hauptprogramm den Methodenaufruf
immer gleich halten können. Man sagt, dass die Methode aus der abgeleiteten
Klasse die ursprünglich gegebene Methode der Basisklasse überschreibt.
Im folgenden Beispiel wird die Ausgabe einer Dezimalzahl auf verschiede-
ne Weisen vorgenommen. Wie üblich beginne ich mit der Basisklasse.
class ZahlAusgabe
{
protected double zahl = 876543.21;

public void drucke()


{
System.out.println(zahl);
}
}
Von ZahlAusgabe sollen nun zwei Klassen abgeleitet werden. Die eine soll die
3.3 Vererbung 265

Zahl auf die englische Weise ausgeben, die andere auf die vertraute deutsche.
class ZahlAusgabeEnglisch extends ZahlAusgabe
{
public void drucke()
{
System.out.println("876,543.21");
}
}

class ZahlAusgabeDeutsch extends ZahlAusgabe


{
public void drucke()
{
System.out.println("876.543,21");
}
}
Die Klassen ZahlAusgabeEnglisch und ZahlAusgabeDeutsch sind beide von
der Klasse ZahlAusgabe abgeleitet. In beiden Klassen wird die Methode
drucke(), die sie schon in der Basisklasse finden, überschrieben: in der er-
sten abgeleiteten Klasse wird die Dezimalzahl auf die englische Weise ausge-
geben, also mit Dezimalpunkt und dem Komma als optischem Trennzeichen,
in der zweiten abgeleiteten Klasse wird sie auf die mehr oder weniger alt-
deutsche Weise ausgegeben, also mit Dezimalkomma und dem Punkt als op-
tischem Trennzeichen. In allen drei Klassen heißt die Methode gleich, nämlich
drucke(), aber überall macht sie etwas anderes.
In einem Hauptprogramm kann das dann beispielsweise auf die folgende
Weise ausgenutzt werden.
ZahlAusgabe[] zahlen = new ZahlAusgabe[2];
zahlen[0] = new ZahlAusgabeEnglisch();
zahlen[1] = new ZahlAusgabeDeutsch();
for(int i=0; i<2; i++)
zahlen[i].drucke();
Das Feld zahlen besteht aus zwei Einträgen, die beide der Klasse ZahlAusgabe
angehören. Da aber ZahlAusgabeEnglisch und ZahlAusgabeDeutsch Sub-
klassen der Basisklasse ZahlAusgabe sind, ist jedes Objekt dieser beiden Klas-
sen automatisch auch Objekt von ZahlAusgabe und kann somit in dem Feld
zahlen gespeichert werden. Wenn Sie nun dieses Feld durchlaufen und für je-
des vorkommende Objekt die Methode drucke() aufrufen, dann erkennt die
JVM, dass es sich um Objekte der entsprechenden Subklassen handelt, und
nach dem Prinzip der Polymorphie werden die jeweils passenden Methoden
aus den Klassen ZahlAusgabeEnglisch bzw. ZahlAusgabeDeutsch verwen-
det. Mit einem immer gleichen Aufruf erreicht man also, dass verschiedene
Verarbeitungen vorgenommen werden, sofern die verwendeten Methoden in
den abgeleiteten Klassen überschrieben worden sind.
266 3 Objektorientierte Programmierung mit Java

Noch einmal: das Überschreiben von Methoden setzt dreierlei voraus. Er-
stens muss es eine Basisklasse und eine Subklasse geben, zweitens muss eine
Methode in der Basisklasse existieren, und drittens muss in der Subklasse ei-
ne Methode mit der gleichen Signatur vorhanden sein, also eine Methode mit
dem gleichen Namen und der gleichen Struktur der Parameterliste und des
Rückgabetyps. Nur dann kann das Überschreiben und damit die Polymorphie
funktionieren. Falls Sie in einer Subklasse eine Methode mit gleichem Namen
wie in der Basisklasse, aber mit einer anderen Parameterliste definieren, dann
findet kein Überschreiben statt. In diesem Fall erkennt die JVM, dass das
aufrufende Objekt der Subklasse ja eigentlich auch ein Objekt der Basisklasse
ist, und Sie haben es mit einem ganz normalen Überladen der Methode aus
der Basisklasse zu tun.

Überschreiben von Methoden


Eine Methode einer Basisklasse kann in einer Subklasse überschrieben wer-
den. Die überschreibende Methode der Subklasse muss mit der Methode der
Basisklasse im Namen, in der Parameterliste und im Rückgabetyp überein-
stimmen. Beim Aufruf der Methode für ein bestimmtes Objekt erkennt die
JVM, welche Methode verwendet werden muss, je nachdem, ob das zugehörige
Objekt zur Bssisklasse oder zu Subklasse gehört. Diese Fähigkeit, die richtige
Methodenauswahl zu treffen, nennt man Polymorphie.

Wenn man genau hinsieht, dann hat das Überschreiben von Methoden
nicht nur Vorteile. Was ist denn, wenn Sie das Bedürfnis haben, manchmal
die Methode der Basisklasse anzuwenden und manchmal die der Subklasse?
Nach dem Polymorphieprinzip wird für ein Objekt der Subklasse grundsätz-
lich auch die Methode aus der Subklasse genommen, sofern eine existiert, und
an die entsprechende Methode der Basisklasse ist nicht mehr zu denken. Das
sieht aber nur so aus, in Wahrheit haben die Java-Entwickler auch dieses Pro-
blem schon im Voraus gelöst, und zwar ähnlich wie bei den Konstruktoren.
Sie können bei der Definition einer Methode einer Subklasse auf die Metho-
den der Basisklasse zurückgreifen, auch auf die gleichnamigen. Nehmen wir
wieder die Ausgabeklassen von oben. Vielleicht wollen Sie ja neben der engli-
schen Ausgabe auch noch die Standardausgabe aus der Klasse ZahlAusgabe
am Bildschirm sehen. In diesem Fall würde die Methode drucke() der Klasse
ZahlAusgabeEnglisch so aussehen:
public void drucke()
{
super.drucke();
System.out.println("876,543.21");
}
Durch die Angabe von super.drucke() wird die Methode drucke() der Su-
perklasse, also der Basisklasse, aufgerufen, und das ist ja genau die, die ich
zusätzlich haben wollte. Sie können also auch innnerhalb der Subklasse durch
3.3 Vererbung 267

Einsatz von super.methodenname() auf die gleichnamigen Methoden der Ba-


sisklasse zugreifen.
Das alles ist offenbar nur dann nötig, wenn Sie Vererbungen und Ablei-
tungen überhaupt zulassen wollen. Es kann auch vorkommen, dass man keine
Methoden überschrieben haben will und zu einer Klasse keine Erben zulassen
möchte, weil alle Methoden schon so sind, wie sie sein sollen und eine weitere
Modifikation der Klassen nicht erwünscht ist. Anders gesagt: eine Methode
oder eine Klasse hat vielleicht schon ihren Endzustand, ihren finalen Zustand
erreicht, an dem nichts mehr zu tun ist, und schon haben wir das passende
Schlüsselwort gefunden, das Sie schon von der Definition konstanter Werte
kennen. Mit dem Qualifizierer final vor einer Methode erreichen Sie, dass
diese Methode nicht mehr überschrieben werden kann. Ich könnte also meine
Klasse ZahlAusgabe ein wenig ändern, indem ich die Methode drucke() als
final kennzeichne.
public final void drucke()
{
System.out.println(zahl);
}
Dann werden allerdings die abgeleiteten Klassen ZahlAusgabeEnglisch und
ZahlAusgabeDeutsch einigermaßen sinnlos, da ich ihre eigenene Methoden
drucke() nicht mehr definieren kann: sobald eine Methode als final qualifi-
ziert ist, kann sie von keiner abgeleiteten Klasse mehr überschrieben werden.
Der Compiler würde beim Übersetzen der abgeleiteten Klassen Fehlermeldun-
gen liefern, weil er merkt, dass hier eine Methode überschrieben werden soll,
die ihr Endstadium schon erreicht hat und Änderungen nicht mehr zugänglich
ist.
Immerhin ist dann trotzdem noch eine Erweiterung von ZahlAusgabe
möglich, nur das Überschreiben der Methode drucke() habe ich auf diese
Weise verhindert. Sie können auch noch einen Schritt weiter gehen und dafür
sorgen, dass die ganze Klasse keine Erweiterungen mehr erlaubt. Mit einer
Klassendeklaration der Art
final class ZahlAusgabe
unterbinden Sie jede Erbschaft von der Klasse ZahlAusgabe und enterben da-
mit sozusagen jede denkbare Klasse. Eine Deklaration
class Irgendwas extends ZahlAusgabe
führt dann schon bei der Übersetzung von Irgendwas zu einer Fehlermel-
dung, denn nichts darf mehr von ZahlAusgabe abgeleitet werden, und wer es
dennoch versucht, wird mit einer Fehlermeldung bestraft.
Das Schlüsselwort super kann auch noch ein weiteres Problem lösen, das
ich Ihnen bisher verschwiegen habe. Es kann jederzeit passieren, dass eine
Membervariable einer Subklasse den gleichen Namen trägt und zu allem Übel
auch noch dem gleichen Datentyp oder der gleichen Klasse angehört wie eine
Membervariable der Basisklasse. In diesem Fall entsteht in der abgeleiteten
Klasse ein heilloses Durcheinander, da Sie innerhalb der Subklasse schlicht
nicht wissen können, um wen es jetzt eigentlich gehen soll. Das Chaos ist aber
268 3 Objektorientierte Programmierung mit Java

beherrschbar, denn das entsprechende Element der Basisklasse wird ganz ein-
fach nicht an die Subklasse vererbt, weil ein Element dieses Namens ja schon
von sich aus in der Subklasse existiert, sodass es bei Vererbung zu einer Kolli-
sion käme. Wenn Sie aber trotzdem das Bedürfnis haben, auf das entsprechen-
de Element der Basisklasse zuzugreifen, dann ist das ohne weiteres möglich,
indem Sie wieder einmal das Schlüsselwort super benutzen. Hat eine Basis-
klasse beispielsweise die Membervariable ding und taucht eine gleichnamige
Membervariable in einer von dieser Klasse abgeleiteten Subklasse auf, so wird
innerhalb der Subklasse die entsprechende Variable der Basisklasse verborgen
- sie liegt sozusagen im Schatten, da sie nicht vererbt wird, und daher nennt
man sie auch verdeckte Variable oder auf Englisch shadowed variable. Will man
sie aber dennoch verwenden, so genügt es, sie als super.ding anzusprechen.
Da sich super immer auf die jeweilige Superklasse bezieht, hat man damit
auch den Zugriff auf die gleichnamige Variable der Basisklasse realisiert.

super und final


Will man in einer überschreibenden Methode einer Subklasse die gleich-
namige Methode der Basisklasse verwenden, so erfolgt der Aufruf mit
super.methodenname().
Mit der Qualifizierung einer Methode als final verhindert man, dass sie in
einer abgeleiteten Klasse überschrieben werden kann. Mit der Qualifizierung
einer ganzen Klasse als final verhindert man, dass von dieser Klasse eine
weitere Klasse abgeleitet werden kann.

3.3.5 Erweitern von Subklassen

Dass Java keine Mehrfachvererbung im obigen Sinne kennt, darf man nicht
verwechseln mit der Möglichkeit, von einer bereits abgeleitete Klasse noch ein-
mal eine weitere Klasse abzuleiten: das ist nämlich jederzeit möglich. So kann
beispielsweise die Klasse Mitarbeiter die abgeleiteten Klassen Arbeiter und
Angestellter besitzen, solange das Tarifrecht diese eigenartige Zweiteilung
noch zulässt. Das führt dann zu den folgenden Klassen.
class Arbeiter extends Mitarbeiter
{
//....
}
und
class Angestellter extends Mitarbeiter
{
//...
}
Wie auch immer die weiteren Membervariablen und Methoden dieser Klassen
aussehen mögen: in jedem Fall ist jedes Objekt der Klasse Arbeiter automa-
tisch als Objekt der Klasse Mitarbeiter und damit auch als Person-Objekt
3.3 Vererbung 269

ausgewiesen, kann also von den dortigen Membervariablen und Methoden


Gebrauch machen. Ein Objekt der Klasse Arbeiter hat also auch eine Mem-
bervariable name, auf die dann auch wie üblich zugegriffen werden kann. Mit
anderen Worten: alle Membervariablen und Methoden, die vererbt wurden,
stehen auch ohne Probleme zur Verfügung, sowohl in einer direkten Sub-
klasse als auch in Subklassen von Subklassen. Aber Vorsicht! Das praktische
Schlüsselwort super funktioniert nur einmal, nicht doppelt. Wenn Sie also
eine Methode aus der obersten Basisklasse in einer Subklasse überschreiben
und dann diese überschriebene Methode in einer von der Subklasse abgeleite-
ten Klasse noch einmal überschreiben, dann können Sie von ganz unten nicht
mit so etwas wie super.super.methodenname() auf die Methode der Basis-
klasse zugreifen. Eine Schachtelung von super-Kommandos ist nicht erlaubt,
weder bei der Anwendung auf Methoden noch beim Einsatz von verdeckten
Variablen.

Subklassen von Subklassen


Jede abgeleitete Klasse kann selbst wieder Basisklasse weiterer abgeleiteter
Klassen sein. Dabei ist es im Falle der Überschreibung von Methoden nicht
möglich, das super-Kommando verschachtelt anzuwenden.

3.3.6 Die Klasse Object

Man kann zwar sicher auch ohne die Kenntnis der Klasse Object Java-
Programme schreiben, aber darüber Bescheid zu wissen macht vielleicht den
einen oder anderen Zusammenhang deutlicher. Diese vordefinierte Klasse
Object ist nämlich die Urklasse aller anderen Klassen, sozusagen die Mut-
ter aller Klassen. Jede Klasse, die neu erzeugt wird und keine Unterklasse
einer anderen Klasse ist, besitzt automatisch die Basisklasse Object. Genau
genommen müsste man also bei jeder Klassendefinition schreiben:
class Klassenname extends Object
aber da das ohnehin für jede Klasse gilt, kann man die Angabe von Object
als Basisklasse auch weglassen.
Die pure Existenz dieser Klasse erklärt beispielsweise, warum man in Vek-
toren der Klasse Vector keine Elemente elementarer Datentypen abspeichern
kann. Die Parameterliste der Methode addElement() besteht aus einem ein-
zigen Parameter der Klasse Object, und daher muss man dieser Methode
irgendein Objekt irgendeiner Klasse liefern - es ist ganz egal, welche Klasse,
denn jede ist von Object abgeleitet, sodass ihre Objekte auch ganz von allei-
ne Objekte der Klasse Object sind. Und da eine int-Zahl nun mal zu keiner
Klasse gehört, passt sie auch nicht zur Klasse Object und kann von keinem
Vektor der Welt aufgenommen werden. Ähnlich sieht es aus beim Auslesen
aus dem Vektor mit der Methode elementAt(). Ihr Rückgabetyp ist ganz
schlicht Object, weshalb die JVM auch nicht weiß, welche konkreten Objekte
dieser Vektor enthält. Sie weiß nur, dass er überhaupt Objekte enthält, denn
270 3 Objektorientierte Programmierung mit Java

etwas anderes nimmt er erst gar nicht zur Kenntnis, und daher muss man das
Rückgabeobjekt von elementAt() mit einem Typecast zu dem machen, was
es eigentlich ist.
Sie sehen: manchmal muss man etwas eher Abstraktes wissen, um so kon-
krete Eigenheiten wie die der Klasse Vector zu verstehen, die wir schon im
Abschnitt über Vektoren besprochen hatten. Aber auch die Klasse Object
selbst besitzt einige Methoden, von denen ich hier nur eine besprechen will,
weil sie Ihnen schon ab und zu begegnet ist. Die Methode equals(Object o)
verlangt irgendein Objekt als Input und liefert einen boolschen Wert als Rück-
gabe: ist das Inputobjekt gleich dem aufrufenden Objekt, so liefert sie true,
ansonsten false. In der Urform wird aber nur überprüft, ob das Inputobjekt
o und das Objekt, das die Methode equals() aufruft, physisch identisch sind,
was bedeutet, dass zwei inhaltlich gleiche Objekte, die aber an verschiedenen
Stellen stehen, nicht als gleich gekennzeichnet werden. Gleichheit innerhalb
der Klasse Object bedeutet also, dass zwei Objekte an der gleichen Stelle im
Hauptspeicher stehen. Um inhaltliche Gleichheit zu überprüfen, sollte man in
einer neuen Klasse also eine neue Methode equals() schreiben, die feststellt,
ob zwei Objekte inhaltlich gleich sind, also in ihren Datenfeldern überein-
stimmen. Wie Sie wissen, hat beispielsweise die Klasse String eine solche
Methode.

Die Klasse Object


Jede Klasse wird automatisch von der Basisklasse Object abgeleitet, ohne dass
man extends Object angeben muss. Object besitzt verschiedene Methoden,
unter anderem die Methode equals() zum Test der physischen Gleichheit
zweier Objekte.

3.3.7 Innere Klassen

Ich hatte Ihnen schon berichtet, dass Java keine Mehrfachvererbung zulässt,
um den damit verbundenen Schwierigkeiten aus dem Weg zu gehen. Das war so
und das bleibt so. Aber wie Sie wissen, kann man manchmal auf einem leichten
oder auch mittelschweren Umweg Ziele erreichen, zu denen kein direkter Weg
geführt hätte, und ein solcher Weg zu einer Art Mehrfachvererbung ist der
Einsatz von inneren Klassen. Im nächsten Abschnit, wenn es um Interfaces
geht, werde ich noch einmal auf sie zurückkommen; für den Moment benutze
ich sie als ein Instrument zur Simulation der Mehrfachvererbung.
Eine innere Klasse selbst ist etwas ganz Einfaches, wie Sie am folgenden
Beispiel sehen.
class Aussen
{
int aussenwert = 17;
class Innen
{
3.3 Vererbung 271

int innenwert = 34;


void ausgabe()
{
System.out.println("Aussenwert = " + aussenwert);
System.out.println("Innenwert = " + innenwert);
}
}
void ausgeben()
{
Innen drin = new Innen();
drin.ausgabe();
}
}
Es wird eine Klasse Aussen definiert, die zunächst einmal eine Memberva-
riable aussenwert aufweist. Danach habe ich aber noch eine Klasse namens
Innen angelegt, aber nicht wie sonst als separate Klasse, sondern innerhalb
der Klasse Aussen. Die innere Klasse Innen ist daher ein Teil der Klasse
Aussen, was sie nicht daran hindert, eine vollwertige Klasse zu sein; sie hat
eine Membervariable innenwert und eine Ausgabemethode ausgabe(), an
der man schon eine Eigenheit erkennen kann: die innere Klasse kann als ein
Teil der äußeren Klasse auf alles zugreifen, was die äußere Klasse zu bieten
hat. In der Ausgabemethode von Innen wird nämlich ohne zu Zögern auf
die Membervariable aussenwert der äußeren Klasse Aussen zugegriffen, und
das wird auch immer funktionieren, denn die innere Klasse hat grundsätzlich
Zugriff auf alle Elemente ihrer äußeren Klasse.
Umgekehrt ist das allerdings nicht der Fall. Sobald Sie die Definition der in-
neren Klasse verlassen, haben Sie keinen direkten Zugriff mehr auf die Elemen-
te der inneren Klasse. Aus diesem Grund habe ich in der Methode ausgeben()
der äußeren Klasse das Objekt drin der Klasse Innen kreiert: die Klasse ist
ja vorhanden, also kann ich in der äußeren Klasse Objekte der inneren Klasse
definieren. Und sobald ein Objekt einmal vorhanden ist, steht es mir auch
frei, seine Methoden einzusetzen. Der Methodenaufruf ding.ausgabe() wird
funktionieren, weil ding ein Objekt der Klasse Innen ist und diese Klasse über
eine Methode ausgabe() verfügt. Was dagegen nicht funktionieren würde, ist
ein Ausgabekommando wie
System.out.println("Innenwert = " + innenwert);
außerhalb der inneren Klasse. Ich darf also die Ausgabemethode von Aussen
nicht etwa auf die folgende Weise programmieren.
void ausgeben()
{
System.out.println("Aussenwert = " + aussenwert);
System.out.println("Innenwert = " + innenwert);
}
In diesem Fall unternehme ich nämlich einen Versuch, aus der äußeren Klas-
272 3 Objektorientierte Programmierung mit Java

se direkt auf das Element innenwert der inneren Klasse zuzugreifen, und so
etwas ist streng verboten.
Innere Klassen spielen eine große Rolle bei der so genannten Ereignisbe-
handlung, auf die ich ganz am Ende des Java-Kapitels noch kurz zu sprechen
kommen werde. Hier will ich sie nur benutzen, um doch noch so etwas wie eine
kleine Mehrfachvererbung auf die Reihe zu bekommen, auch wenn sie in Java
eigentlich nicht funkioniert. Wie üblich zeige ich Ihnen die Vorgehensweise an
einem Beispiel. Ich gehe aus von den beiden folgenden übersichtlichen Klas-
sen.
class Person
{
String name;
String adresse;
String getName()
{
return name;
}
void setName(String name_par)
{
name = name_par;
}
String getAdresse()
{
return adresse;
}
void setAdresse(String adresse_par)
{
adresse = adresse_par;
}
}
sowie
class Kosten
{
String name;
int stelle;
String getName()
{
return name;
}
void setName(String name_par)
{
name = name_par;
}
String getStelle()
{
3.3 Vererbung 273

return stelle;
}
void setStelle(int stelle_par)
{
stelle = stelle_par;
}
}
Zu diesen beiden Klassen ist nichts weiter zu sagen, ausser dass mit
Kosten die Kostenstellen verwaltet werden sollen. Nun will ich eine Klasse
Mitarbeiter schreiben, die sowohl auf Person als auch auf Kosten zugreifen
kann, weil ein Mitarbeiter erstens eine Person ist und zweitens auch Kosten
verursacht, also als Kostenstelle betrachtet werden kann. Und obwohl Java ei-
gentlich keine Mehrfachvererbung zulässt, kann ich dieses Ziel erreichen, indem
ich Mitarbeiter selbst von Person ableite und innerhalb von Mitarbeiter
eine innere Klasse definiere, die dann ihrerseits von Kosten alle Attribute und
Methoden erbt. Durch den Einsatz einer inneren Klasse wird also eine indi-
rekte Ableitung von einer zweiten Klasse möglich. Wie das konkret geht, zeigt
die folgende Klasse Mitarbeiter.
class Mitarbeiter extends Person
{
int gehalt;
class Innen extends Kosten
{
int kostenBerechnen()
{
/*Methode zur Berechnung der aufgelaufenen Kosten
des Mitarbeiters*/
}
}
Innen drin = new Innen();
void datenAusgeben()
{
System.out.println("Der Mitarbeiter heisst " + name);
System.out.println("Er wohnt in " + adresse);
System.out.println("Seine Kostenstelle lautet "
+ drin.getName());
System.out.println("Sie hat die Nummer "
+ drin.getStelle());
System.out.println("An Kosten sind f"ur ihn entstanden "
+ drin.kostenBerechnen());
System.out.println("Er verdient " + gehalt + " Euro");
}
void datenEingeben()
{
System.out.println("Name des Mitarbeiters:");
274 3 Objektorientierte Programmierung mit Java

setName(Eingabe.lesenstring());
System.out.println("Adresse des Mitarbeiters:");
setAdresse(Eingabe.lesenstring());
System.out.println("Name seiner Kostenstelle:");
drin.setName(Eingabe.lesenstring());
System.out.println("Nummer seiner Kostenstelle:");
drin.setStelle(Eingabe.lesenint());
System.out.println("Gehalt des Mitarbeiters:");
gehalt = Eingabe.lesenint();
}
}
Sie sehen, dass Mitarbeiter nur von Person erbt, denn von mehr als einer
Klasse kann eine Klasse gar nicht erben. Der Einfachheit halber habe ich mich
darauf beschränkt, dem Mitarbeiter nur noch die Membervariable gehalt zu
verpassen, weil es mir hier auf buchhalterische Einzelheiten nicht ankommt.
Gleich nach dem Gehaltsattribut wird dann eine innere Klasse namens Innen
definiert, die nicht allzu viel tut; ihr gesamter Lebenszweck besteht nur darin,
die Klasse Kosten zu erweitern, damit in Mitarbeiter alle Attribute und Me-
thoden von Kosten zur Verfügung stehen, und die Kosten zu berechnen, die
ein Mitarbeiter verursacht. Das anschießend definierte Objekt drin der Klasse
Innen ist also insbesondere auch ein Objekt der Klasse Kosten, mit dem ich
in meiner Klasse Mitarbeiter hantieren kann. Wann immer es in der Verar-
beitung der Mitarbeiterdaten um seine personenbezogenen Daten geht, kann
ich direkt auf die Membervariablen von Person zugreifen, denn die Klasse
Mitarbeiter hatte ich von Person abgeleitet. Und sobald ich seine kosten-
stellenbezogenen Daten attakieren muss, greife ich auf das Objekt drin der
Klasse Innen zurück, das mir jederzeit zur Verfügung steht und alle Attribu-
te und Methoden der Klasse Kosten frei Haus in meine Klasse Mitarbeiter
liefert.
Durch die Einführung eines Zusatzobjekts einer abgeleiteten inneren Klas-
se kann ich also auf die Attribute und Methoden mehrerer Klassen zugreifen.
Ich habe es sogar erreicht, dass es absolut keine Schwierigkeiten mit der dop-
pelten Belegung des Namens name in den beiden Klassen Person und Kosten
gibt, denn der Zugriff auf Kostenattribute erfolgt immer über die Zwischen-
stufe des Objekts drin. In einem Hauptprogramm merken Sie von den Ver-
renkungen, die ich in der Klasse Mitarbeiter vorgenommen habe, überhaupt
nichts mehr, hier genügt wie üblich das Anlegen eines Objekts, um die beiden
Methoden anwenden zu können.
Mitarbeiter kerl = new Mitarbeiter();
kerl.datenEingeben();
kerl.datenAusgeben();
Dass die Klasse Mitarbeiter ein wenig von hinten durch die Brust ins
Auge konstruiert worden ist, kann bei ihrem Einsatz keiner mehr sehen, und
genau darin liegt ja auch der Vorteil der objektorientierten Programmierung.
Sobald eine Klasse einmal programmiert ist, interessiert nur noch ihre Schnitt-
3.3 Vererbung 275

stelle nach außen: was verlangt sie, was liefert sie? Wie sie das intern erledigt,
braucht nienmanden mehr zu kümmern. Natürlich hätten Sie auch noch die ei-
ne oder andere Methode der Klasse Kosten in Ihrer inneren Klasse Innen über-
schreiben können; auch das fällte dann beim Einsatz der Klasse Mitarbeiter
in einem Hauptprogramm nicht mehr auf.

Innere Klassen
Innerhalb der Definition einer Klasse kann man weitere Klassen definieren, die
als innere Klassen bezeichnet werden. Aus der inneren Klasse heraus hat man
jederzeit Zugriff auf die Elemente der äußeren Klasse, nicht aber umgekehrt.
Innere Klassen kann man einsetzen, um eine Klasse mit den Methoden
und Attributen mehrerer Klassen zu versehen, also eine Art von Mehrvach-
vererbung auch in Java zhu simulieren. Sie spielen ebenfalls eine große Rolle
bei der Verarbeitung von Ereignissen.

Ein Wort noch zu der internen Verwaltung innerer Klassen. Da für jede
Klasse beim Kompilieren eine eigene class-Datei erzeugt wird, muss das auch
für innere Klassen geschehen, allerdings muss man beim Namen etwas aufpas-
sen. Er setzt sich nämlich aus dem Namen der äußeren und der inneren Klasse
zusammen, wobei die einzelnen Namen mit dem $-Zeichen getrennt werden. Im
Beispiel der Klasse Aussen würde die innere Klasse Innen also die class-Datei
Aussen$Innen.class erhalten. Und natürlich kann eine innere Klasse noch
weitere innere Klassen enthalten; Sie dürfen innere Klassen beliebig schach-
teln. Haben Sie zum Beispiel das Bedürfnis, in der Klasse Innen noch eine
weitere Klasse Doedel anzulegen, die welche Funktion auch immer haben mag,
dann ergeben sich beim Kompilieren drei class-Dateien: einmal die class-Datei
der obersten Klasse, also Aussen.class, dann die class-Datei der ersten in-
nneren Klasse, also Aussen$Innen.class, und schließlich die class-Datei der
innersten Klasse, und die heißt natürlich Aussen$Innen$Doedel.class.
So viel zur Vererbung in Java. Im nächsten Abschnitt werde ich Ihnen eine
spezielle Form von Basisklassen vorstellen: die abstrakten Klassen.

Übungen

3.13. Schreiben Sie eine Klasse Person, die nur eine einzige Membervaria-
ble name vom Typ String enthält. Ihr Konstruktor soll eine Zeichenkette
als Parameter verlangen und diese Zeichenkette dem Attribut name zuwei-
sen. Weiterhin soll Person eine Methode getName() enthalten, die den Na-
men zurückgibt. Schreiben Sie weiterhin eine Klasse Schweizer, eine Klasse
Deutscher und eine Klasse Englaender, die alle von der Klasse Person abge-
leitet werden sollen und jeweils noch das Attribut Geburtsort enthalten. Die
jeweiligen Konstruktoren sollen den Konstruktor der Klasse Person aufrufen
und zusätzlich das Attribut Geburtsort füllen, das als zusätzlicher Parameter
dem Konstruktor mitgegeben wird. Die abgeleiteten Klassen haben eine Me-
thode gruessen(), die den jeweils landesüblichen Gruß zurückgibt, und eine
276 3 Objektorientierte Programmierung mit Java

Methode getOrt() mit der offensichtlichen Funktion. Testen Sie die Klassen
in einem Hauptprogramm.
3.14. Erweitern Sie die Klasse Deutscher aus Aufgabe 3.13 durch eine Klasse
Bayer. In Bayer soll die Methode gruessen() der Klasse Deutscher durch
die Rückgabe des bayrischen Grußes pfüat di“ überschrieben werden. Testen

Sie die neue Klasse durch ein Hauptprogramm.
3.15. Schreiben Sie eine Klasse Vektor zur Aufnahme von zweidimensiona-
len Vektoren (x, y). Die Klasse soll als Membervariable ein Feld mit zwei
reellen Einträgen haben und über Methoden zum Setzen und Auslesen der
Koordinaten verfügen sowie über eine Methode zum Addieren von Vektoren.
Schreiben Sie anschließend eine Klasse Komplex zur Verarbeitung komplexer
Zahlen. Da jede komplexe Zahl aus zwei reellen Koordinaten besteht, soll
Komplex von Vektor abgeleitet werden. Zusätzlich soll Komplex eine Methode
zur Multiplikation komplexer Zahlen nach der Formel (x1 + iy1 ) · (x2 + iy2 ) =
x1 x2 − y1 y2 + i(x1 y2 + x2 y1 ) haben.
3.16. Schreiben Sie ein Klasse Vektor1, die die Klasse Vector erweitert. Die
neue Klasse soll eine weitere Methode enthalten, mit der die Reihenfolge der
Einträge im Vektor umgedreht werden kann, sodass also z.B. aus dem Vek-
tor (1, 2, 3) der Vektor (3, 2, 1) wird. Testen Sie die neue Klasse mit einem
geeigneten Hauptprogramm.
3.17. Die reellen Lösungen einer biquadratischen Gleichung x4 + px2 + q = 0
findet man, indem man z = x2 setzt, die quadratische Gleichung z 2 +px+q = 0
löst und anschließend die Wurzeln aus den reellen Lösungen dieser quadra-
tischen Gleichung zieht. Schreiben Sie eine Klasse Biquadrat, mit der Glei-
chungen dieser Art eingegeben und gelöst werden können. Sie soll eine innere
Klasse Quadrat enthalten, in der das Lösen der quadratischen Gleichung erle-
digt wird, mit deren Hilfe dann die Methode loesen() der Klasse Biquadrat
die Lösung der biquadratischen Gleichung bestimmt.

3.4 Abstrakte Klassen und Interfaces


Die Frage, was man genau unter den Begriffen abstrakt“ und konkret“ zu
” ”
verstehen hat, ist schwer zu beantworten. Ist ein Programm etwas Abstraktes,
weil man es nicht wirklich anfassen kann, oder ist es etwas Konkretes, weil
man es übersetzen und zum Laufen bringen, also einiges damit anstellen kann?
Und soll eine Klasse nicht immer etwas Abstraktes sein? Immerhin ist ein Ball
ein Ball, ein rundes Ding, das durch die Gegend rollt, während ein Objekt
meiner alten Klasse Ball sicher nicht rollen kann, sondern bestenfalls eine
Stelle im Arbeitsspeicher meines Computers bezeichnet. Schon wahr. Aber im
Reich der Abstraktionen, zu dessen Bewohnern sicher auch die Java-Klassen
zählen, gibt es ein paar Dinge, die noch etwas abstrakter sind als andere - das
macht ihre Behandlung nicht schwieriger, vielleicht sogar etwas leichter, denn
je konkreter die Dinge werden, desto mehr Tücken können sie haben.
3.4 Abstrakte Klassen und Interfaces 277

3.4.1 Abstrakte Klassen

Sie haben schon an einigen Beispielen im Abschnitt über Vererbung gesehen,


worum es mir hier gehen wird. Ich hatte zum Beispiel eine Klasse Person
definiert, aber die Objekte dieser Klasse haben mich nicht wirklich interes-
siert. In einem Unternehmen gibt es nun mal kaum reine Personen, jeder hat
irgendeine Funktion, die etwas mit dem Unternehmen und seiner Rolle im
Unternehmen zu tun hat. Deshalb hatte ich die Klassen Mitarbeiter, Kunde
und Lieferant eingeführt, alle abgeleitet von der Basisklasse Person, und
zu diesen Klassen gibt es natürlich echte konkrete Objekte: eine Firma ohne
Mitarbeiter, Kunden und Lieferanten wäre wohl doch etwas trostlos. So wie
die Klassen bisher programmiert waren, besteht aber noch immer die Gefahr,
dass sich eine pure Person in mein System einschleicht, von der kein Mensch
weiß, was sie da eigentlich zu suchen hat, weil sie weder zu den Mitarbeitern,
noch zu den Kunden oder den Lieferanten gehört. Im richtigen Leben ist das
meistens ein Fall für das bekannte Schild mit der Aufschrift Zutritt für Un-

befugte verboten“, in der objektorientierten Programmierung dagegen kommt
in solchen Fällen das Konzept der abstrakten Klassen zum Tragen.
Um zu vermeiden, dass ein Objekt der Klasse Person angelegt wird, muss
ich nur diese Klasse als eine abstrakte Klasse kennzeichnen: was abstrakt ist,
kann nicht konkret sein, und deshalb gibt es keine Objekte, keine Instan-
zen einer abstrakten Klasse. Eine abstrakte Klasse ist eine Klasse, zu der es
keine Objekte geben kann, nicht mehr und nicht weniger. Sobald Sie vor das
Schlüsselwort class bei der Klassendefinition noch das Wort abstract schrei-
ben, ist die Klasse als abstrakt gekennzeichnet, womit es unmöglich wird, ein
Objekt dieser Klasse anzulegen. Wenn Sie es doch versuchen, erhalten Sie vom
Compiler eine Fehlermeldung wie:
Class Soundso is an abstract class. It can’t be instantiated.
Pech gehabt!“ würde meine Tochter jetzt sagen, aber das ist gar kein Pech,

sondern Absicht. Eine abstrakte Klasse soll als Muster dienen, als Vorlage, von
der andere konkrete Klassen dann erben können. Im Falle der Klasse Person
sollte die Klassendeklaration also besser
abstract class Person
lauten, denn erst damit wird garantiert, dass keine unbefugten Personen
in meinem System abgelegt werden. Die abgeleiteten Klassen Mitarbeiter,
Kunde und Lieferant dürfen dann natürlich um Himmels Willen nicht mehr
abstrakt sein, sonst könnte ich auch davon keine Objekte anlegen, und das
kann nicht der Sinn der Sache sein. Der Sinn einer abstrakten Klasse liegt
darin, ein Muster zu liefern, nach dem sich die abgeleiteten Klassen zu richten
haben, und gleichzeitig einige Grundfunktionen bereitzustellen, die dann alle
ihre Subklassen übernehmen können.
Aber was ich Ihnen bisher über abstrakte Klassen gezeigt habe, reicht noch
nicht aus, um dieses Ziel zu erfüllen. Wenn Person als abstrakt gekennzeichnet
wird, dann kann ich keine Objekte von Person anlegen, das ist schon mal gut.
Von einer Vorbildfunktion für die abgeleiteten Klassen bin ich aber noch weit
278 3 Objektorientierte Programmierung mit Java

entfernt, denn die können über die Vererbung die Methoden der Basisklas-
se übernehmen, werden aber ansonsten zu nichts gezwungen oder wenigstens
angehalten. Eine abstrakte Klasse soll ein Muster darstellen, einen Rahmen
liefern, dem sich ihre Subklassen anzupassen haben, und dieser Rahmen ist
es, der mir bisher noch fehlt. Kein Problem, ich bin ja noch da, das haben wir
gleich. Sehen wir uns das Ganze einmal an einem Beispiel an.
abstract class Fahrzeug
{
protected String bezeichnung;
protected boolean verkauft;
protected int geschwindigkeit;
public abstract void datenEingeben();
public abstract void beschreiben();
public boolean verkaufen()
{
if (verkauft == true)
return false;
else
{
verkauft = true;
return true;
}
}
}
Die Klasse ist als abstrakt gekennzeichnet, und das ist auch sinnvoll, denn was
soll denn ein Fahrzeug eigentlich sein? Ist es ein Auto, ein Fahrrad, ein LKW
oder ein Kinderwagen? Was immer es auch sein mag, in jedem Fall hat es eine
Bezeichnung und eine Geschwindigkeit, und man kann festhalten, ob es einem
selbst gehört oder bereits an irgendwen verkauft ist - das alles ist unabhängig
von der konkreten Art des Fahrzeugs. Auch die Methode verkaufen() hat
nichts damit zu tun, ob auf meinem Hof ein Gabelstapler steht oder ein Roll-
schuh: bei beiden kann ich überprüfen, ob das Fahrzeug bereits verkauft ist
und den Verkaufsvorgang gegebenenfalls durchführen und vermerken.
Aber ganz anders sieht es aus bei der Eingabe der Fahrzeugdaten und
der Beschreibung eines Fahrzeugs. Ein Motorrad hat andere Attribute als ein
Kinderwagen, deshalb kann ich nicht für beide Fahrzeuge die gleiche Eingabe-
oder Beschreibungsmethode verwenden. Andererseits muss ich ja irgendwie
die Daten eingeben und auch wieder anzeigen können, weshalb sowohl eine
Eingabe- als auch eine Beschreibungsmethode unverzichtbar sind. Ich muss
zwar sagen, dass es diese beiden Methoden gibt, aber ich darf mich auf der
abstrakten Ebene eines Fahrzeuges nicht darauf festlegen, wie sie aussehen sol-
len, denn das weiß ich einfach nicht. Aus diesem Grund habe ich die Methoden
datenEingeben() und beschreiben() ebenfalls als abstrakt gekennzeichnet,
genauso wie die ganze Klasse. Und weil sie abstrakt sind, kann man auch
nicht definieren, was sie eigentlich tun sollen: das soll schließlich erst in den
3.4 Abstrakte Klassen und Interfaces 279

abgeleiteten konkreten Klassen festgelegt werden. Eine abstrakte Methode ist


also nur eine Aufforderung an spätere Subklassen, sie mit Leben zu füllen und
konkret werden zu lassen, sie hat keinen Methodenrumpf und soll auch keinen
haben.
Eine Klasse mit mindestens einer abstrakten Methode muss automatisch
eine abstrakte Klasse sein. Umgekehrt kann es aber auch abstrakte Klassen
ohne eine einzige abstrakte Methode geben; man kann sich darüber streiten,
ob das sinnvoll ist. Vergessen Sie nie, dass eine abstrakte Klasse in aller Re-
gel als Basisklasse gedacht ist, deren Subklassen dann die konkreten Objekte
beheimaten. Für meine Klasse Person könnte ich jetzt die abgeleiteten Sub-
klassen Auto, LKW, Fahrrad und etliche mehr definieren, die dann alle auf die
konkreten Attribute und Methoden von Fahrzeug zugreifen können, aber die
abstrakten Methoden überschreiben müssen. Damit ist sichergestellt, dass al-
le von der abstrakten Klasse abgeleiteten Klassen die gleiche Grundstruktur
haben: die konkreten Methoden der abstrakten Klasse werden von den Sub-
klassen übernommen, die abstrakten Methoden werden von den Subklassen
überschrieben und damit erst konkretisiert. Man neigt daher dazu, die Grund-
funktionalitäten, die allen Objekten aller abgeleiteten Klassen gemeinsam sein
sollen, als konkrete Methoden in eine abstrakte Basisklasse zu schreiben und
alle anderen Methoden, die von Subklasse zu Subklasse verschieden ausge-
staltet sein können, in der abstrakten Basisklasse als abstrakte Methoden zu
kennzeichnen und dann in den einzelnen Subklassen durch Überschreiben mit
Leben zu füllen.

Abstrakte Klassen
Eine abstrakte Klasse dient als Vorlage für andere Klassen, die von ihr abge-
leitet werden. Sie wird durch
abstract class Klassenname
deklariert. Eine abstrakte Klasse kann abstrakte Methoden enthalten, die
ebenfalls durch abstract gekennzeichnet werden und keinen Methodenrumpf
besitzen. Jede Klasse mit mindestens einer abstrakten Methode ist abstrakt.
Sie muss also durch von ihr abgeleitete Klassen konkretisiert werden, die dann
die abstrakten Methoden der abstrakten Basisklasse überschreiben.
Zu einer abstrakten Klasse kann man keine Objekte definieren.

Noch ein kleines Beispiel, damit das Abstrakte konkreter wird.


abstract class Zahl
{
protected int wert;
public void setWert(int wert_par)
{
wert = wert_par;
}
public abstract void ausgeben();
}
280 3 Objektorientierte Programmierung mit Java

Zu dieser Klasse ist nicht viel zu sagen. Sie enthält eine Membervariable wert,
eine konkrete Methode setWert() und eine abstrakte Methode auageben().
Da die Klasse abstrakt ist, kann ich keine konkreten Objekte von ihr erzeugen,
sondern muss erst einmal richtige konkrete Klassen von ihr ableiten. Das Ziel
dieser Subklassen ist dann natürlich, die abstrakte Methode ausgeben() auf
verschiedene Weise zu überschreiben. Eine einfache abgeleitete Klasse ist die
folgende.
class ZahlOhne extends Zahl
{
public void ausgeben()
{
System.out.println(wert);
}
}
Langweiliger kann eine abgeleitete Klasse kaum sein. Zugegeben: sie über-
schreibt die abstrakte Methode ausgeben(), indem sie den Zahlenwert der
Membervariablen von Zahl ausgibt, aber das bißchen Ertrag war die Mühe
kaum wert. Sie ist ja auch nur ein kleines Beispiel für das Überschreiben einer
abstrakten Methode, und auch schon der Klassnname ZahlOhne deutet an,
dass sie ohne nennenswerten Aufwand programmiert ist. Etwas anders sieht
das schon bei der nächsten Subklasse ZahlMit aus.
class ZahlMit extends Zahl
{
public void ausgeben()
{
if(wert < 1000)
System.out.println(wert);
else
{
String s = String.valueOf(wert);
int n = s.length();
StringBuffer t = new StringBuffer(s);
int i=3;
do
{
t.insert(n-i,".");
i=i+3;
}
while(i<n);
System.out.println(t);
}
}
}
Auch die Klasse ZahlMit ist eine Subklasse von Zahl, und daher überschreibt
sie die abstrakte Methode ausgeben(). Wie sie das anstellt, werde ich Ihnen
3.4 Abstrakte Klassen und Interfaces 281

gleich erklären; für den Moment sage ich nur, dass ZahlOhne eine positive gan-
ze Zahl einfach nur mathematisch als Zahl ausgibt wie zum Beispiel 1234567,
während ZahlMit die positiven Zahlen mit je einem Trennungspunkt nach
drei Dezimalstellen ausgibt, also zum Beispiel 1.234.567. Und wie kann man
die beiden Subklassen jetzt einsetzen? Nichts leichter als das, ein Hauptpro-
gramm, das beide Subklassen verwendet, kann beispielsweise folgendermaßen
aussehen.
public static void main(String[] args)
{
Zahl zahlm, zahlo;
zahlo = new ZahlOhne();
zahlm = new ZahlMit();
zahlo.setWert(1234567);
zahlm.setWert(1234567);
zahlo.ausgeben();
zahlm.ausgeben();
}
Das sieht aber eigenartig aus. Hatte ich nicht vorhin erzählt, dass man
zu einer abstrakten Klasse keine Objekte definieren kann? Kann man auch
nicht, und ich mache es hier auch nicht. Erinnern Sie sich an den Unter-
schied zwischen dem Deklarieren und dem Definieren eines Objektes? Durch
Zahl zahlm, zahlo werden zwei Variablen namens zahlm und zahlo ange-
legt, die zunächst einmal nichts weiter sind als Zeigervariablen, die das Pro-
gramm mit Speicheradressen füllen kann. Es sind noch keine konkreten Ob-
jekte, denn die werden erst mit Hilfe von new angelegt, damit die Variablen
auch auf sie zeigen können. Nun ist das Deklarieren einer Variable zu einer
abstrakten Klasse völlig unproblematisch, weil dadurch noch kein konkretes
Objekt angelegt wird, und mehr habe ich in dieser main()-Methode gar nicht
gemacht. Die Anweisung
zahlm = new Zahl();
würde natürlich schief gehen, da ich damit den verzweifelten Versuch unter-
nehmen würde, ein konkretes Objekt einer abstrakten Klasse anzulegen - das
geht nicht und wird nie gehen. Deshalb habe ich es auch gar nicht erst ver-
sucht, sondern mit
zahlm = new ZahlMit();
dafür gesorgt, dass zahlm eine konkrete Instanz der ebenfalls konkreten Klas-
se ZahlMit ist, und das ist erlaubt. Sie dürfen also jederzeit Variablen vom
Typ einer abstrakten Basisklasse deklarieren, solange Sie mit den passenden
new-Kommados dafür sorgen, dass diese Variablen auf Objekte der konkreten
Subklassen zeigen.
Der Rest des Hauptprogramms dürfte klar sein. Ich habe ein Objekt der
Klasse ZahlMit und ein Objekt der Klasse ZahlOhne angelegt, und da beide
Klassen von Zahl abgeleitet sind, kann ich dafür Variablen vom Typ Zahl
verwenden. Für beide Objekte wird dann die Methode ausgeben() aufgeru-
fen. Der Aufruf zahlo.ausgeben() benutzt die Methode von ZahlOhne und
282 3 Objektorientierte Programmierung mit Java

gibt deshalb schlicht 12345467 aus, während der Aufruf zahlm.ausgeben()


natürlich die hauseigene Methode aus ZahlMit für sich in Anspruch nimmt
und ein stolzes 1.234.567 auf den Bildschirm bringt.
Einen kurzen Blick sollten wir noch auf die Programmierung der Metho-
de ausgeben() in ZahlMit werfen. Falls die positive Zahl wert kleiner als
1000 ist, kann man sie einfach so ausgeben, wie sie ist, denn der Trennungs-
punkt spielt erst bei Zahlen ab 1000 aufwärts eine Rolle. Im else-Zweig
kann ich also davon ausgehen, dass meine Zahl nicht kleiner als 1000 ist.
Nun will ich ja Trennungspspunkte einfügen, und man kann nicht erwarten,
dass so etwas bei reinen Zahlen funktioniert. Bei Zeichenketten dagegen soll-
te es damit keine Probleme geben. Deshalb habe ich mit dem Kommando
String s = String.valueOf(wert) einen String s erzeugt, der genau die
ganze Zahl wert aufnimmt. Sie wissen inzwischen auch, wie dieses Komman-
do aufgebaut ist: die Methode valueOf hängt offenbar nicht an einem einzigen
Objekt, sondern an der ganzen Klasse String, ist also eine vordefinierte sta-
tische Methode von String, weshalb ich sie mit String.valueOf(wert) für
den ganzzahligen Parameter wert aufrufen darf. Sie leistet nichts anderes als
die Zahl in eine Zeichenkette umzuwandeln, die dann in der Stringvariablen
s abgelegt wird. Zur weiteren Verabeitung werde ich die Länge des Strings
brauchen, die mir die Methode length() liefert. Beachten Sie dabei, dass
length() keine statische Methode ist, sondern direkt am Objekt s hängt: das
kann ja auch gar nicht anders sein, denn eine Länge der Klasse String wäre
reichlich sinnlos, während jeder einzelne String natürlich seine eigene Länge
hat.
Nun neigt die Klasse String leider zu einer gewissen Starrköpfigkeit und
Inflexibilität. Es gibt beispielsweise keine Möglichkeit, einen String, sobald
er einmal angelegt ist, nachträglich zu ändern. Wenn Sie beispielsweise der
Variablen kette mit String kette = "ding" eine bestimmte Zeichenkette
zuweisen und irgendwann später die weitere Zuweisung kette = "dong" vor-
nehmen, dann wird die neue Zeichenkette keineswegs an die Stelle der alten
geschrieben, sondern es wird ein neues Objekt erzeugt, auf das die alte Va-
riable kette zeigt. Das kostet Speicherplatz und Zeit. Da zudem die Klasse
String nicht über alle Methoden verfügt, die man sich im Umgang mit Zei-
chenketten wünschen würde, geht man oft zur Klasse StringBuffer, also zu
Stringpuffern über, die über einige praktische Methoden verfügen.
Wie Sie sehen können, kann man aus einem String leicht über einen ent-
sprechenden Konstruktoraufruf von StringBuffer einen Stringpuffer erzeu-
gen, was ich hier in Gestalt von t getan habe. Die Methode insert() fügt an
die durch den ersten Parameter angegeben Stelle das Zeichen ein, das Sie sich
im zweiten Parameter wünschen, und schiebt den Rest der Zeichenkette nach
hinten. Nehmen Sie beispielsweise die Zahl 1234567, so wird die Länge der Zei-
chenkette genau 7 sein, und da 7−3 = 4 ist, wird im ersten Schleifendurchlauf
der Punkt an der vierten Stelle von t eingefügt. Das ist auch gut so, denn die
Punkte muss man in Dreierschritten von hinten nach vorne setzen, und nach
dem ersten Durchlauf haben Sie dann auf t das Resultat "1234.567". Falls
3.4 Abstrakte Klassen und Interfaces 283

Sie das gerade verwirrt hat: man fängt bei 0 an zu zählen, also steht der Punkt
jetzt an der vierten Stelle von t. Im nächsten Schritt soll dann ein Punkt an
die erste Stelle von t gesetzt werden, wogegen nichts spricht, weil dann auf t
das Ergebnis "1.234.567" steht. Sobald anschließend i ein weiteres Mal um
3 erhöht wird, übersteigt es die Länge n meiner Zeichenkette, und die Schleife
wird beendet. Am Ende wird dann das Objekt t ausgegeben als wäre es ein
schlichter String; das Kommando System.out.println() arbeitet auch hier.
Dieses Verfahren funktioniert übrigens für beliebige natürliche Zahlen und
sogar auch für negative ganze Zahlen, solange ihre Stellenzahl nicht durch
drei teilbar ist. In den Übungen werden Sie die Methode ein wenig verbessern,
sodass sie für jede ganze Zahl das richtige Ergebnis liefert.
Neben dem Umgang mit abstrakten Klassen lernen Sie hier noch etwas
Wichtiges. Natürlich hat die Klasse StringBuffer noch mehr zu bieten als
nur die eine Methode insert(). Da wäre zum Beispiel die Methode append(),
der Sie einen beliebigen Parameter mitgeben können mit dem Ziel, ihn in einen
String umzuwandeln und diesen String an das aufrufende StringBuffer-
Objekt anzuhängen. Oder die Methode charAt(), die ein ganze Zahl als Pa-
rameter erwartet und dann das Zeichen zurückgibt, das an der angegebenen
Stelle des aufrufenden Objekts steht. Oder auch die Methode setCharAt(),
die eine ganze Zahl und ein char-Element als Parameter haben möchte und
dann an die angegebene Stelle des aufrufenden Objekts das mitgegebene Zei-
chen schreibt. Es ist aber unmöglich, in einem Buch alle Methoden aller K