Grundlagen der Informatik I Tafel -- Manuskript

Werbung
Grundlagen der Informatik I
Tafel -- Manuskript
Studiengang Wirtschaftsingenieurwesen
Wintersemester 2013/2014
Autor:
Prof. Dr.-Ing. habil. Hans-Joachim Böhme
HTW Dresden, Fakultät Informatik/Mathematik
Friedrich-List-Platz 1, 01069 Dresden
E-Mail: [email protected]
Hinweis: Die Rechte am Dokument liegen ausschließlich beim Verfasser.
Inhaltsverzeichnis
1
Grundlegender Überblick................................................................................................... 4
1.1
Was ist Informatik/Informationsverarbeitung ............................................................ 4
1.2
Was ist Information? .................................................................................................. 5
1.3
Was ist ein Bit? .......................................................................................................... 7
1.4
Datei und Speichergrößen .......................................................................................... 9
2
Informationsdarstellung ................................................................................................... 11
2.1
Text........................................................................................................................... 11
2.1.1
Der ASCII-Code............................................................................................... 11
ASCII-Erweiterungen....................................................................................................... 12
ASCII-Probleme............................................................................................................... 13
2.1.2
Unicode ............................................................................................................ 14
2.1.3
UTF-8 ............................................................................................................... 14
2.1.4
Zeichenketten ................................................................................................... 15
2.2
Zahlendarstellung ..................................................................................................... 16
2.2.1
Binärdarstellung ............................................................................................... 16
2.2.2
Hexadezimaldarstellung ................................................................................... 17
2.2.3
Umwandlung zwischen verschiedenen Zahlensystemen ................................. 17
Exkurs: Division mit Rest ................................................................................................ 17
2.2.4
Die Zweierkomplementdarstellung .................................................................. 18
Berechnung des Zweierkomplements wird in der Übung behandelt! .............................. 19
Standardformate für Zweierkomplementzahlen............................................................... 19
2.3
Darstellung logischer Werte..................................................................................... 19
3
Rechnerorganisation – Teil 1 ........................................................................................... 21
3.1
Von-Neumann-Architektur ...................................................................................... 21
John von Neumann........................................................................................................... 21
Nachteile der von-Neumann-Architektur......................................................................... 22
3.2
Merkmale und Komponenten der Von-Neumann - Rechnerarchitektur .................. 23
3.2.1
Zentrale Recheneinheit / Central Processing Unit ........................................... 23
3.2.2
Speicher............................................................................................................ 24
3.2.3
Interne Datenwege / Busse............................................................................... 24
3.2.4
Ein-/Ausgabeeinheit ......................................................................................... 25
3.3
Mainboard ................................................................................................................ 25
3.3.1
Systembus......................................................................................................... 25
3.3.2
BIOS................................................................................................................. 26
3.3.3
CPU .................................................................................................................. 28
Cache Speicher................................................................................................................. 28
Cache Hierarchie .............................................................................................................. 29
Prozessor-Cache ............................................................................................................... 29
Prozessor-Hersteller ......................................................................................................... 30
3.3.4
Arbeitsspeicher / RAM (Random Access Memory) ........................................ 30
Speichertypen ................................................................................................................... 31
Technische Realisierung .................................................................................................. 32
3.3.5
Northbridge ...................................................................................................... 32
3.3.6
Southbridge ...................................................................................................... 33
3.3.7
PCI-Bus ............................................................................................................ 34
3.3.8
Accelerated Graphics Port (AGP) .................................................................... 35
3.3.9
PCI-Express...................................................................................................... 36
3.3.10 IDE (ATA/ATAPI) .......................................................................................... 37
Konfiguration von EIDE-Geräten .................................................................................... 39
1
ATAPI - Advanced Technology Attachment with Packet Interface................................ 39
3.3.11
S-ATA (Serial ATA)........................................................................................ 40
S-ATA II (Serial ATA II)................................................................................................. 41
3.3.12
USB (Universal Serial Bus) ............................................................................. 41
Technischer Überblick ..................................................................................................... 42
Übertragungsgeschwindigkeiten ...................................................................................... 43
3.4
Speicher-Hierarchie.................................................................................................. 44
3.5
Funktionell-struktureller Aufbau der CPU............................................................... 44
3.6
Einteilung von Rechnerarchitekturen nach Flynn.................................................... 50
Harvard-Architektur ................................................................................................................. 51
4
Betriebssysteme................................................................................................................ 53
4.1
Die ISA-Ebene ......................................................................................................... 53
4.2
Assemblerprogrammierung ...................................................................................... 53
Assemblersprache – Maschinensprache und Assembler.................................................. 54
4.3
Von der Hardware zum Betriebssystem................................................................... 56
4.4
Aufgaben des Betriebssystems................................................................................. 57
4.4.1
Dateiverwaltung ............................................................................................... 58
4.4.2
Prozessverwaltung............................................................................................ 63
4.4.3
Speicherverwaltung.......................................................................................... 66
5
Algorithmen und Programmierung .................................................................................. 70
5.1
Einführung................................................................................................................ 70
5.2
Übersicht der Programmiersprachen........................................................................ 71
5.3
Definition von Programmiersprachen ...................................................................... 72
5.4
Klassifikation von Programmiersprachen ................................................................ 72
5.5
Vom Programm zur Maschine ................................................................................. 74
5.5.1
Virtuelle Maschinen ......................................................................................... 74
5.5.2
Interpreter ......................................................................................................... 76
5.6
Programmentwicklung ............................................................................................. 77
5.6.1
Analyse............................................................................................................. 77
5.6.2
Entwurf............................................................................................................. 78
Algorithmen als Lösung einer Spezifikation.................................................................... 80
Terminierung.................................................................................................................... 81
5.6.3
Implementierung .............................................................................................. 81
5.6.4
Test ................................................................................................................... 82
5.6.5
Dokumentation ................................................................................................. 82
5.7
Elementare Aktionen in Programmiersprachen ....................................................... 83
5.8
Programmierumgebungen ........................................................................................ 84
5.9
Datentypen und Variablen........................................................................................ 84
5.9.1
Datentypen ....................................................................................................... 84
5.9.2
Variablen .......................................................................................................... 84
5.9.3
Primitive Datentypen........................................................................................ 86
5.9.4
Zusammengesetzte Datentypen........................................................................ 87
5.9.5
Variablen und Speicher .................................................................................... 89
5.9.6
Deklaration von Variablen ............................................................................... 89
5.9.7
Initialisierung von Variablen............................................................................ 90
5.9.8
Typkorrekte Ausdrücke.................................................................................... 90
5.9.9
Typfehler .......................................................................................................... 91
5.10 Der Kern imperativer Sprachen................................................................................ 91
5.10.1
Zuweisungen .................................................................................................... 92
5.10.2
Kontrollstrukturen ............................................................................................ 93
Die if-Anweisung ............................................................................................................. 94
2
Die if else-Anweisung...................................................................................................... 96
Die switch-Anweisung ..................................................................................................... 96
Die while-Schleife............................................................................................................ 99
Die for-Schleife .............................................................................................................. 101
Die do while-Schleife..................................................................................................... 103
Die break-Anweisung..................................................................................................... 105
Die continue-Anweisung................................................................................................ 106
5.11 Datenstrukturen ...................................................................................................... 106
5.11.1
Stacks ............................................................................................................. 107
Stackoperationen ............................................................................................................ 108
Anwendungsbeispiel für einen Stack ............................................................................. 108
5.11.2
Queues............................................................................................................ 110
Anwendung von Queues ................................................................................................ 110
5.11.3
Listen.............................................................................................................. 111
Einfach verkettete Listen................................................................................................ 111
Doppelt verkettete Listen ............................................................................................... 112
5.12 Bäume..................................................................................................................... 113
Beispiele für Bäume ....................................................................................................... 114
5.12.1
Binärbäume .................................................................................................... 115
5.12.2
Traversierungen.............................................................................................. 117
5.13 Suchalgorithmen..................................................................................................... 117
5.13.1
Lineare Suche................................................................................................. 118
5.13.2
Binäre Suche .................................................................................................. 119
5.14 Komplexität von Algorithmen ............................................................................... 120
5.15 Sortieralgorithmen.................................................................................................. 121
5.15.1
BubbleSort...................................................................................................... 121
3
1 Grundlegender Überblick
1.1 Was ist Informatik/Informationsverarbeitung
Der Begriff Informatik leitet sich von dem Begriff Information her. Er entstand in den
60er Jahren. Informatik ist die Wissenschaft von der maschinellen
Informationsverarbeitung. Die englische Bezeichnung für Informatik ist Computer
Science, also die Wissenschaft, die sich mit Rechnern beschäftigt. Wenn auch die
beiden Begriffe verschiedene Blickrichtungen andeuten, bezeichnen sie dennoch das
Gleiche. Die Spannweite der Disziplin Informatik ist sehr breit, und demzufolge ist
das Gebiet in mehrere Teilgebiete untergliedert.
1. Technische Informatik



Die Technische Informatik beschäftigt sich mit der Hardware, d.h. mit der
Konstruktion von Rechnern, Rechnerkomponenten (Prozessoren und
Speicherchips) und Peripheriegeräten (Druckern, Bildschirmen). Sie stellt
also die Grundlage der maschinellen Informationsverarbeitung bereit.
Die Hardwareentwicklung erfolgt aber immer auch im Hinblick auf das spätere
Anwendungsgebiet (Steuert ein Prozessor eine Raumfähre oder eine XBox?)
Die Grenzen zur Elektrotechnik sind fließend.
2. Praktische Informatik


Im Gegensatz zur Technischen Informatik beschäftigt sich die Praktische
Informatik mit der Software, die auf der Hardware ausgeführt werden soll,
also im weitesten Sinne mit den Programmen, die die Hardware steuern.
Ein klassisches Anwendungsgebiet der Praktischen Informatik ist der
Compilerbau. Ein Compiler übersetzt Programme, die in einer
Programmiersprache (Java, C, C++,...) geschrieben sind, in die stark von den
Eigenheiten des jeweiligen Systems abhängende Maschinensprache
(Assembler).
Ein compiliertes Programm läuft nur auf dem System, für das es compiliert
wurde. Windowsprogramme laufen also nicht unter Unix, ohne neu compiliert
zu werden.
3. Theoretische Informatik


Die Theoretische Informatik beschäftigt sich mit den abstrakten
mathematischen und logischen Grundlagen aller Teilgebiete der Informatik.
Theorie und Praxis sind in der Informatik enger verwoben als in vielen
anderen Diziplinen, theoretische Erkenntnisse sind schneller und direkter
einsetzbar.
Durch die theoretischen Arbeiten auf dem Gebiet der formalen Sprachen und
der Automatentheorie zum Beispiel hat man das Gebiet des Compilerbaus (-> Praktische Informatik) heute sehr gut im Griff. In Anlehnung an die
theoretischen Erkenntnisse sind praktische Werkzeuge entstanden
(Programme), mit denen ein großer Teil des Compilerbaus automatisiert und
vereinfacht werden kann. Früher dauerte die Konstruktion eines einfachen
4
Compilers ca. 25 Bearbeiter-Jahre. Heute erledigen Studenten vergleichbare
Aufgaben im Rahmen eines Praktikums!
(Vorweg zur Frage: "Was nützt mir die Automatentheorie?". Aber dazu später
mehr...)
4. Angewandte Informatik



Die Angewandte Informatik beschäftigt sich mit dem Einsatz von Rechnern
in den verschiedensten Bereichen unseres Lebens.
Es gibt eine enge Verknüpfung zwischen Angewandter Informatik und den
anderen Teilbereichen.
So gilt es z.B. neue Hardware mit erweiterten Möglichkeiten (-->Technische
Informatik) in Verbindung mit aus dem Zusammenspiel von Theoretischer und
Praktischer Informatik entstandenen Werkzeugen einer sinnvollen, neuen
Anwendung zuzuführen (z.B. Organizer, Palmtops oder Tablet-PC's, für die
Lagerhaltung, auf der Baustelle, oder für den Paketboten).
Dies gilt auch für die Anwendung von Computersystemen in speziellen
Fachbereichen der Wissenschaft:
 In der HKI: Anwendung auf alle (v.a. historischen) Quellen. Bessere
Nutzbarmachung von z.B. alten Handschriften durch Text-Markup und
Datenbanken.
 In der Archäologie: Anwendung von Computersystemen zur
Rekonstruktion antiker Bauten z.B. als 3D-Modell.
 ....
1.2 Was ist Information?
Information und Daten


Die verschiedenen Teilbereiche der Informatik beschäftigen sich mit
unterschiedlichen Ebenen der Informationsverarbeitung.
Um zu beschreiben, wie Informationen im Computer verarbeitet werden,
müssen wir uns zunächst auf eine Erklärungsebene festlegen. Wir beginnen
mit der niedrigsten Ebene, der Ebene der Nullen und Einsen.
Wir beschäftigen uns also zunächst damit, wie Informationen im Rechner
durch Nullen und Einsen repräsentiert werden können. Die so repräsentierten
Informationen nennen wir Daten. Die Repräsentation muss derart gewählt
werden, dass man aus den Daten auch wieder die repräsentierte,
ursprüngliche Information zurückgewinnen kann. Diesen Prozess der
Interpretation von Daten nennt man auch Abstraktion.
5

Zur Verdeutlichung:
Wenn man nur eine Folge von Bits betrachtet, kann noch keine Information
daraus gewonnen werden. Eine Folge von Bits oder Bytes hat für sich
genommen keine Bedeutung. Erst wenn man weiß, wie diese Bytes zu
interpretieren sind, erschließt sich deren Bedeutung und damit eine
Information!
Die Bedeutung (oder der Informationsgehalt) von Daten ergibt sich erst aus
der Kenntnis der benutzten Repräsentation.
(--> mp3 im Texteditor? jpg im Texteditor?).
Diese Interpretation von Daten nennt man Abstraktion.
Definitionen




Daten (Singular: Datum) sind eine Folge von digitalen Zeichen (--> Bits).
Eine Information ist ein Datum mit inhaltlicher Bedeutung.
Eine Nachricht ist eine Zeichenfolge, die Informationen vermittelt.
Ein Signal ist die physikalische Darstellung von Nachrichten oder Daten.
Oder anders ausgedrückt:


Information ist der abstrakte Gehalt (Bedeutungsinhalt, Semantik) eines
Dokuments, einer Aussage, o. ä.
Repräsentation ist die äußere Form der Darstellung (konkrete Form)
Kommunikation und Sprache




Information wird übermittelt durch Kommunikation.
Kommunikation bedient sich immer einer Sprache.
Die Information wird in einer Sprache kodiert.
Jede Sprache hat:
 ein Alphabet (der endliche Symbolsatz)
 eine Grammatik (Regeln zur Formung von Zeichenketten, Worten,
Aussagen)
 eine Syntax ("Rechtschreibung" der Worte bzw. Aussagen)
 eine Semantik (Bedeutung des Gesagten)
6
Repräsentation von Daten

Das Alphabet, das der Computer verwendet, um Daten zu repräsentieren
besteht aus Nullen und Einsen, aus Bits.
1.3 Was ist ein Bit?
Ein Bit (aus dem Englischen für binary digit) ist die kleinstmögliche Einheit der
Information. Ein Bit ist die Informationsmenge in einer Antwort auf eine Frage, die
zwei Möglichkeiten zulässt:







ja oder nein
wahr oder falsch
schwarz oder weiß
hell oder dunkel
naß oder trocken
links oder rechts
usw.
Zu einer solchen Frage lässt sich immer eine Codierung festlegen. Da es zwei
mögliche Antworten gibt, reicht ein Code mit zwei Zeichen, ein sogenannter binärer
Code. Man benutzt dazu die Zeichen:

0 oder 1
Eine solche Codierung ist deswegen nötig, weil die Information technisch dargestellt
werden soll. Man bedient sich dabei z.B. elektrischer Ladungen:


0 = ungeladen
1 = geladen
oder elektrischer Spannungen


0 = 0 Volt
1 = 5 Volt
oder Magnetisierungen


0 = unmagnetisiert
1 = magnetisiert
Bitfolgen
Lässt eine Frage mehr als zwei Antworten zu, so enthält die Beantwortung der Frage
mehr als ein Bit an Information.
Die Frage etwa, aus welcher Himmelsrichtung, Nord, Süd, Ost oder West, der Wind
weht, lässt 4 mögliche Antworten zu:

0 0 = Nord
7



0 1 = Ost
1 0 = Süd
1 1 = West
--> Offensichtlich gibt es genau 4 mögliche Folgen von 2 Bit. Mit 2 Bit können wir
also Fragen beantworten, die 4 mögliche Antworten zulassen.
Angenommen, wir lassen als Antworten auf die obige Frage auch noch die
Zwischenrichtungen Nordost, Nordwest, Südost und Südwest zu, haben wir
insgesamt 8 mögliche Antworten.
Mit einem zusätzlichen Bit, also insgesamt mit 3 Bit können wir alle 8 möglichen
Antworten darstellen.








0 0 0 = Nord
0 0 1 = Ost
0 1 0 = Süd
0 1 1 = West
1 0 0 = Nordost
1 0 1 = Nordwest
1 1 0 = Südost
1 1 1 = Südwest
Nibbels, Bytes und Worte
Ein Rechner ist viel besser als der Mensch in der Lage, mit langen Folgen von Bits
umzugehen. Für uns wird das Ganze schnell unübersichtlich:
01001111011000010110110001101100
Übersichtlicher wird das Ganze, wenn wir die Bitfolge oben in kleinere Vierer-Pakete
unterteilen:
0100 1111 0110 0001 0110 1100 0110 1100
Eine solche Gruppe von 4 Bits nennt man auch ein Halb-Byte oder Nibble.
Wenn ein Rechner Daten liest oder schreibt, wenn er mit Daten operiert, gibt er sich
nie mit einzelnen Bits ab. Dies wäre im Endeffekt viel zu langsam. Stattdessen
arbeitet er immer nur mit Gruppen von Bits, entweder mit 8 Bits, 16 Bits, 32 Bits oder
64 Bits. man spricht dann von 8-Bit-Rechnern, 16-Bit-Rechnern, 32-Bit-Rechnern
(z.B. AMD AthlonXP, Intel Pentium 3 ) oder
von 64-Bit-Rechnern (AMD Athlon64, Athlon X2 64, Opteron, Intel Core 2 Duo). In
Wahrheit gibt es aber auch Mischformen: Rechner, die etwa intern mit 32-Bit-Blöcken
rechnen, aber immer nur Blöcke zu 64-Bits lesen oder schreiben.
Stets jedoch ist die Länge eines Bitblocks ein Vielfaches von 8. Eine Gruppe von 8
Bit nennt man daher auch ein Byte.
4 Bits = Halb-Byte / Nibble
8 Bits = Byte
8
2 Bytes = Wort
Für eine Gruppe von 2, 4 oder 8 Bytes sind auch die Begriffe Wort, Doppelwort und
Quadwort in Gebrauch, allerdings ist die Verwendung dieser Begriffe uneinheitlich!
Bei einem 16-Bit-Rechner bezeichnet man eine 16-Bit Größe (2 Bytes) als Wort, ein
Byte ist dann ein Halbwort.
Bei einem 32-Bit-Rechner steht Wort auch für eine Gruppe von 4 Bytes.
1.4 Datei und Speichergrößen
Dateigrößen von weniger als hundert Byte sind äußert selten, meist bewegen sich
die Dateigrößen in Bereichen von Tausenden oder gar Millionen von Bytes.
Es bietet sich daher an, dafür die von Gewichts- und Längenmaßen gewohnten
Präfixe kilo (für tausend) und mega- (für million) zu verwenden.
Andererseits wäre es günstig, beim Umgang mit binären Größen auch die Faktoren
durch Zweierpotenzen 2, 4, 8, 16, ... auszudrücken. Da trifft es sich sehr gut, dass
die Zahl 1000 sehr nahe bei einer Zweierpotenz liegt, nämlich:
Die Abkürzungen für die in der Informatik gebräuchlichen Größenfaktoren sind daher:
Die obigen Maßeinheiten haben sich auch für die Angabe der Größe des
Hauptspeichers und anderer Speichermedien durchgesetzt.
1 GB = 1024 MB RAM
Wer allerdings schon einmal eine Festplatte gekauft hat, wird sich vielleicht
gewundert haben, dass seine schöne neue 250 GB Festplatte de facto nur ca. 232
GB groß ist. Wie dieser Umstand entsteht, zeigt die folgende Rechnung:
9
10
2 Informationsdarstellung
Wie bereits im Vorangegangenen ausgeführt, bezeichnen wir die Repräsentation
von Information durch Nullen und Einsen als Daten.
Im Folgenden wird nun die Darstellung von Texten, logischen Werten, Zahlen und
Programmen durch Daten erläutert.
2.1 Text
Um Texte in einem Rechner darzustellen, codiert man Alphabet und Satzzeichen in
Bitfolgen. Mit einem Alphabet von 26 Kleinbuchstaben, ebenso vielen
Großbuchstaben, einigen Satzzeichen wie etwa Punkt, Komma und Semikolon und
Spezialzeichen wie "+", "&", "%" hat eine normale Tastatur eine Auswahl von knapp
100 Zeichen.
Die Information, wo ein Zeilenumbruch stattfinden oder wo ein Text eingerückt
werden soll, codiert man ebenfalls durch spezielle Zeichen.
Solche Sonderzeichen, dazu gehören das CR-Zeichen (von engl. carriage return =
Wagenrücklauf) und das Tabulatorzeichen Tab, werden nie ausgedruckt, sie haben
beim Ausdrucken lediglich die entsprechende steuernde Wirkung. Sie heißen daher
auch Steuerzeichen oder nicht-druckbare Zeichen.
--> Exkurs zu Codierung:
Eine Codierung ist eine Abbildungsvorschrift (oder Funktion), die jedem Zeichen
eines Zeichenvorrats (Urbildmenge) eindeutig ein Zeichen oder eine Zeichenfolge
aus einem möglicherweise anderen Zeichenvorrat (Bildmenge) zuordnet.
Den Vorgang des Übersetzens eines Zeichens oder einer Zeichenfolge der
Urbildmenge in die Bildmenge bezeichnet man als Codierung oder
Verschlüsselung.
Der umgekehrte Vorgang heißt Decodierung oder Entschlüsselung.
2.1.1 Der ASCII-Code
Für alle oben aufgezählten Zeichen unseres Alphabets, inklusive der Steuerzeichen
kommen wir mit 7 Bits aus, das ergibt 128 verschiedene Möglichkeiten.
Man muss also nur eine Tabelle erstellen, mit der jedem Zeichen ein solcher Bitcode
zugeordnet wird. Dazu nummeriert man die 128 gewählten Zeichen einfach durch
und stellt die Nummer durch 7 Bit binär dar.
Die heute fast ausschließlich gebräuchliche Nummerierung ist die so genannte
ASCII-Codierung. ASCII steht für "American Standard Code for Information
Interchange". Sie berücksichtigt einige Systematiken wie:

die Kleinbuchstaben sind in alphabetischer Reihenfolge
durchnummeriert,
11


die Großbuchstaben sind in alphabetischer Reihenfolge
durchnummeriert,
die Ziffern 0 bis 9 stehen in der natürlichen Reihenfolge.
Hier exemplarisch einige Zeichen mit Ihrem ASCII-Code:
Zeichen
A
B
Z
a
b
z
?
ASCII
65
66
90
97
98
122
63
Zeichen
0
1
9
CR
+
=
ASCII
48
49
57
13
43
45
61
Die 128 ASCII-Zeichen entsprechen den Bytes

0000 0000
bis
0111 1111.
Eine Datei, die nur ASCII-Zeichen enthält, also Bytes, deren erstes Bit 0 ist, nennt
man ASCII-Datei. Oft versteht man unter einer ASCII-Datei auch einfach eine
Textdatei, selbst wenn Codes aus einer ASCII-Erweiterung verwendet werden.
ASCII-Erweiterungen
Bei der ASCII-Codierung werden nur die letzten 7 Bits eines Byte genutzt. Das erste
Bit verwendete man früher als Kontrollbit für die Datenübertragung.
Es wurde auf 0 oder 1 gesetzt, je nachdem ob die Anzahl der 1-en an den übrigen 7
Bitpositionen gerade (even) oder ungerade (odd) war. Die Anzahl der 1-en in dem
gesamten Byte wurde dadurch immer gerade (even parity).
Wenn nun bei der Übertragung ein kleiner Fehler auftrat, d.h. wenn in dem
übertragenen Byte genau ein Bit verfälscht wurde, so konnte der Empfänger dies
daran erkennen, dass die Anzahl der 1-en ungerade war.
Bei der Verwendung des ASCII-Codes zur Speicherung von Texten und auch als
Folge der verbesserten Qualität der Datenübertragung wurde dieses Kontrollbit
überflüssig.
Daher lag es nahe, nun alle 8 Bit zur Zeichenkodierung zu verwenden.
Somit ergab sich ein weiterer verfügbarer Bereich von ASCII 128 bis ASCII 255.
Da der ursprüngliche ASCII-Zeichensatz in den USA entwickelt wurde, enthält er
natürlich keine Umlaute.
Der IBM-PC nutzt diese zusätzlichen freien Codes von 128 - 255 zur Darstellung von
sprachspezifischen Zeichen wie z.B. "ä" (ASCII 132), "ö" (ASCII 148), "ü" (ASCII
129) und einigen Sonderzeichen anderer Sprachen. Dieser erweiterte ASCII-Code
wird "Extended ASCII" oder auch ANSI (American National Standard Institute)
genannt.
Der ANSI-Zeichensatz wurde von der ISO (International Organization for
Standardization) standardisiert unter
ISO 6937-2.
12
Leider ist auch die Auswahl der sprachspezifischen Sonderzeichen bei weitem nicht
ausreichend für die vielfältigen Symbole fremder Schriften. Daher wurden von der
ISO (International Organization for Standardization) verschiedene ASCIIErweiterungen (Codepages) normiert:


ISO 8859-1 = Latin-1 (Erweiterung für Westeuropa)
ISO 8859-2 = Latin-2 (Erweiterung für Osteuropa)
ASCII-Probleme
Werden zwischen zwei Rechnern, die nicht dieselben ASCII-Erweiterungen
einsetzen, Daten ausgetauscht, werden die Zeichen der Erweiterungen nicht korrekt
dargestellt.


Ein Problem ist das z.B. bei Emails, wenn der Sender die deutsche ASCIIErweiterung verwendet, die auch die Umlaute wie ä, ö, ü und das ß umfasst
und der Empfänger die ASCII-Erweiterung für z.B. Portugal, die diese
speziellen Zeichen nicht enthält, dafür aber andere:
Dort, wo eigentlich die deutschen Sonderzeichen der ASCII-Erweiterung
stehen sollten, tauchen dann sonderbare Grafikzeichen oder Symbole auf.
Dieses Phänomen ist wahrscheinlich jedem schon einmal begegnet.
Es könnte auch sein, dass der Sender eine länderspezifische Erweiterung
benutzt (8-Bit breit) und der Empfänger den ASCII-Code (7-Bit breit!).
In diesem Fall geht sogar das komplette 1. Bit jedes Zeichens verloren, weil
der Empfänger dieses als Korrekturbit behandelt.
--> Ein Lösungsansatz ist, die zu sendenden Dateien (z.B. Bilder oder Programme)
vorher in ASCII-konforme Dateien zu konvertieren, d.h. in Dateien, die nur noch aus
"druckbaren ASCII-Zeichen" bestehen. Dies kann z.B. mit dem Tool "UUencode"
vorgenommen werden. Das Gegenstück, das die Datei wieder in ihren Urzustand
versetzt heißt "UUdecode".

Allerdings ist dies heutzutage nicht mehr ganz so problematisch, da man auch
das folgende Verfahren für das Versenden von Dateien per Email verwenden
kann und dies in den meisten Fällen schon tut, ohne davon zu wissen:

MIME (Multipurpose Internet Mail Extensions).
MIME ist ein Standard-Verfahren, das es erlaubt, beliebige Dateien als
Anhang per Mail zu verschicken.
Die Übertragung der Dateien erfolgt binär, also ohne Umwandlung in ASCII.

Das oben für Emails geschilderte Phänomen ist bestimmt jedem auch schon
bei Webseiten begegnet, also bei HTML-Dateien.
Die Lösung für dieses Problem ist die Darstellung der betroffenen
Sonderzeichen (im Deutschen die Umlaute und das 'ß') als sog. Entities, hier
eine kleine Auswahl:




ä = ä
Ä = Ä
ö = ö
Ö = Ö
13

ß = ß
2.1.2 Unicode
Nachdem sich das Hantieren mit diversen, länderspezifischen ASCII-Codepages
unterschiedlicher Bit-Breite (7-Bit US-ASCII und 8-BIT Extended ASCII oder ANSICode) bei der weltweiten Datenübertragung als sehr lästig und unschön erwiesen
hat, entstand in den letzten Jahren ein neuer Standard, der versucht, sämtliche
relevanten Zeichen aus den unterschiedlichsten Kulturkreisen in einem universellen
Code zu vereinigen.
Dieser neue Code heißt Unicode und verwendet eine 16-Bit Codierung, kann also
maximal 65.536 Zeichen darstellen.
Länderspezifische Zeichen wie die deutschen Umlaute gehören ebenso
selbstverständlich zum Unicode-Zeichensatz wie kyrillische, arabische, japanische,
chinesische und tibetanische Schriftzeichen.
Die ersten 128 Zeichen sind identisch mit dem ASCII-Code, die folgenden 128 sind
identisch mit dem ISO-Latin 1-Code.




Herkömmliche Programmiersprachen lassen meist keine Zeichen zu, die über
den ASCII-Code hinausgehen. Java erlaubt als erste der großen
Programmiersprachen die Verwendung beliebiger Unicode-Zeichen.
Unicode ist als UCS (Universal Character Set) bzw. als ISO-10646 vom
Unicode-Konsortium und der ISO standardisiert worden.
In der Definition von UCS geht die ISO allerdings noch einen Schritt weiter als
das Unicode-Konsortium: Es werden sowohl eine 16-Bit Codierung (UCS-2)
als auch eine 31-Bit Codierung (UCS-4) festgelegt.
Die Codes von UCS-2 werden als basic-multilingual plane (BMP)
bezeichnet, beinhalten alle bisher definierten Codes und stimmen mit Unicode
überein.
Codes, die UCS-4 ausnutzen sind für potenzielle zukünftige Erweiterungen
vorgesehen.
Die Speicherung von Texten im Unicode benötigt zwei (UCS-2) bis viermal
(UCS-4) soviel Speicherplatz, wie im ASCII-Code.
Dies liegt daran, dass ein Zeichen im UCS-2 Format mit 16 Bit statt mit 8 Bit
codiert wird, d.h. selbst Zeichen, die auch im alten ASCII-Standard enthalten
sind, werden mit 16-Bit codiert, von denen die ersten 9-Bit Nullen sind, also
keine Information enthalten!
Aus diesem Grund hat sich eine weitere Codierung etabliert, die etwas
ökonomischer mit Speicherplatz umgeht: UTF-8.
2.1.3 UTF-8
UTF steht für UCS Transformation Format, wodurch betont wird, dass es sich
lediglich um eine andere Codierung von UCS bzw. Unicode handelt. Neben UTF-8
existieren unter anderem noch die Formate UTF-7, UTF-16 sowie UTF-32, die aber
nur geringe Bedeutung erlangt haben, weshalb wir uns hier auf die Darstellung von
UTF-8 beschränken.
UTF-8 ist eine Mehrbyte-Codierung, bzw. ein Code von variabler Bit-Breite.
14

ASCII-Zeichen werden mit 1 Byte codiert, in dem das erste Bit immer Null
ist:
0xxx xxxx

Jedes Byte, das mit einer 1 beginnt, gehört zu einem aus mehreren Bytes
bestehenden UTF-8 Code:
110x xxxx 10xx xxxx = 2 Byte Code

Besteht ein UTF-8 Code aus n ≥ 2 Bytes beginnt das erste Byte
(Startbyte) mit n Einsen, gefolgt von einer Null und jedes n-1 folgende Byte
mit der Bitfolge 10 (siehe Beispiel oben). Ein 3-Byte Code sieht also
folgendermaßen aus:
1110 xxxx 10xx xxxx 10xx xxxx
Mit den 16 noch verfügbaren Bits können alle 16-Bit UCS-2 Codes dargestellt
werden.
UTF-8 codierte Dateien sind also voll abwärtskompatibel zur 7-Bit ASCII
Vergangenheit und vergrößern den Umfang von Dateien aus dem amerikanischen
und europäischen Bereich gar nicht oder nur unwesentlich.
Diese Eigenschaften haben dazu geführt, dass diese Codierungsmethode der de
facto Standard bei der Verwendung von Unicode geworden ist. Bei den Webseiten
des Internets wird UTF-8 immer häufiger verwendet - alternativ dazu, können, wie
bereits geschildert, in HTML-Dateien Sonderzeichen durch sog. Entities
umschrieben werden.
2.1.4 Zeichenketten
Zur Codierung eines fortlaufenden Textes fügt man einfach die Codes der einzelnen
Zeichen aneinander. Eine Folge von Textzeichen heißt auch Zeichenkette oder engl.
string. Der Text "WING WS 2010" wird also durch folgende Zeichenfolge
repräsentiert:
W,I,N,G, ,W,S, ,2,0,1,0
Jedes dieser Zeichen, einschließlich der Leerzeichen " " ersetzen wir durch seine
dezimale Nummer in der ASCII-Tabelle und erhalten:
87 73 78 71 32 87 83 32 50 48 49 48
Als binäre Bitfolge sieht das Ganze dann so aus:
01010111 | 01001001 | 01001110 | 01000111 | 0100000 | 01010111
01010011 | 00100000 | 00110010 | 00110000 | 0110001 | 00110000
15
2.2 Zahlendarstellung
Wie alle bisher diskutierten Informationen werden auch Zahlen durch Bitfolgen
dargestellt. Wenn eine Zahl wie z.B. "4711" (und welche andere Zahl könnten wir in
Köln als Beispiel wählen?) mitten in einem Text vorkommt, etwa in einem
Werbeslogan, so wird sie, wie der Rest des Textes, als Folge ihrer ASCIIZeichencodes gespeichert, d.h. als Folge der ASCII-Zeichen für "4", "7", "1" und "1".
Dies wären hier die ASCII-Codes mit den Nummern 52, 55, 49, 49. Eine solche
Darstellung ist aber für Zahlen, mit denen man arithmetische Operationen
durchführen möchte, unpraktisch und verschwendet unnötig Platz (8-Bit pro Ziffer,
das wären hier schon 4 Byte).
Man kann Zahlen viel effizienter durch eine umkehrbar eindeutige (ein-zu-eins)
Zuordnung zwischen Bitfolgen und Zahlen kodieren.
Wenn wir nur Bitfolgen einer festen Länge N betrachten, können wir damit 2 hoch N
viele Zahlen darstellen.
Gebräuchlich sind N = 8, 16, 32 oder 64. Man repräsentiert durch die Bitfolgen der
Länge N dann:



die natürlichen Zahlen von 0 bis 2N - 1
die ganzen Zahlen von -2N-1 bis 2N-1 - 1
ein Intervall der reellen Zahlen mit begrenzter Genauigkeit
2.2.1 Binärdarstellung
Will man nur positive ganze Zahlen (natürliche Zahlen) darstellen, so kann man mit n
Bits Zahlen von 0 bis 2N - 1, das sind 2N viele, abdecken.
Die Zuordnung der Bitfolgen zu den natürlichen Zahlen geschieht so, dass die
Bitfolge der Binärdarstellung der darzustellenden Zahl entspricht. Die natürlichen
Zahlen nennt man in der Informatik auch vorzeichenlose Zahlen (unsigned), und
die Binärdarstellung heißt demzufolge auch vorzeichenlose Darstellung.
Um die Idee der Binärdarstellung zu verstehen, führen wir uns noch einmal das
gebräuchliche Dezimalsystem (Zehnersystem) vor Augen.
Man kann die einzelnen Ziffern einer Dezimalzahl auch als Koeffizienten von
Zehnerpotenzen schreiben. Deutlicher wird das an folgendem Beispiel:
Für das Binärsystem (Zweiersystem) hat man anstelle der Ziffern 0...9 nur die
beiden Ziffern 0 und 1 zur Verfügung, daher stellen die einzelnen Ziffern einer
Binärzahl die Koeffizienten der Potenzen von 2 dar. Die Bitfolge 1101 hat daher
den Zahlenwert:
16
Mit drei Binärziffern können wir die Zahlenwerte von 0 bis 7 darstellen:




Mit 4 Bits können wir analog die 16 Zahlen von 0 bis 15 erfassen,
mit 8 Bits die 256 Zahlen von 0 bis 255,
mit 16 Bits die 65.536 Zahlen von 0 bis 65.535 und
mit 32 Bits die 4.294.967.296 von 0 bis 4.294.967.295
2.2.2 Hexadezimaldarstellung
Neben dem Dezimalsystem und dem Binärsystem ist in der Informatik auch das
Hexadezimalsystem in Gebrauch.
Es handelt sich um ein Zahlensystem zur Basis 16. Man benötigt 16 verschiedene
Ziffern und benutzt dazu 0...9 und A...F.
Um zu verstehen, wieso sich gerade das 16er System anbietet, erinnern wir uns an
die sog. Nibbles oder Halb-Bytes.
Dies sind Bitfolgen der Länge N=4.
Wie wir im letzten Abschnitt gesehen haben, kann man mit 4 Bits 16 verschiedene
Zahlen darstellen:
0000=0
0100=4
1000=8
1100=C
0001=1
0101=5
1001=9
1101=D
0010=2
0110=6
1010=A
1110=E
0011=3
0111=7
1011=B
1111=F
2.2.3 Umwandlung zwischen verschiedenen Zahlensystemen
Die Umwandlung einer Binär- oder Hexadezimalzahl in das Dezimalsystem ist
einfach. Wir müssen nur die Ziffern mit den entsprechenden Potenzen von 2 oder 16
multiplizieren und die Ergebnisse aufsummieren.
Exkurs: Division mit Rest
Bevor wir uns nun der etwas schwierigeren Wandlung von Dezimalzahlen in andere
Zahlensysteme zuwenden, lohnt es sich, eine wichtige zahlentheoretische
Beobachtung vorweg zu schicken:
--> Wenn wir eine natürliche Zahl z durch eine andere natürliche Zahl d≠0 teilen,
erhalten wir einen Quotienten q und einen Rest r. Es gilt dann offensichtlich der
Zusammenhang:

z=qxd+r
mit 0 ≤ r < d
17

In der Informatik bezeichnet man die Operation des exakten Dividierens mit
div und die Operation, die zwei Zahlen den Divisionsrest zuordnet, mit mod.
Beispielsweise gilt:


39 div 8 = 4 und
39 mod 8 = 7
Die Probe ergibt auch hier: 39 = 4 x 8 + 7.
Die obige Gleichung können wir unter Zuhilfenahme der Operatoren div und mod
allgemein schreiben als:
z = (z div d) x d + (z mod d)
2.2.4 Die Zweierkomplementdarstellung
Die Zweierkomplementdarstellung ist die gebräuchliche interne Repräsentation
ganzer positiver und negativer Zahlen. Sie kommt auf sehr einfache Weise zu
Stande.
Wir erläutern Sie zunächst für den Fall N = 4.
Mit 4 Bits kann man einen Bereich von 24 = 16 ganzen Zahlen abdecken. Den
Bereich kann man frei wählen, also z.B. die 16 Zahlen von -8 bis +7.
Man zählt nun von 0 beginnend aufwärts, bis man die obere Grenze +7 erreicht,
anschließend fährt man an der unteren Grenze -8 fort und zählt aufwärts, bis man die
Zahl -1 erreicht hat.
Auf diese Weise erhält man nun folgende Zuordnung von Bitfolgen zu ganzen
Zahlen:
1000 = -8
1001 = -7
1010 = -6
1011 = -5
1100 = -4
1101 = -3
1110 = -2
1111 = -1
0000 = 0
0001 = 1
0010 = 2
0011 = 3
0100 = 4
0101 = 5
0110 = 6
0111 = 7
Jetzt sieht man auch den Grund, wieso der Bereich von -8 bis +7 gewählt wurde und
nicht etwa der Bereich von -7 bis +8:
18


Bei dem mit 0 beginnenden Hochzählen wird bei der achten Bitfolge zum
ersten Mal das erste Bit zu 1. Springt man also ab der 8. Bitfolge in den
negativen Bereich, so hat man die folgende Eigenschaft: Bei den
Zweierkomplementzahlen stellt das erste Bit das Vorzeichen dar.
Das Zweierkomplement hat zudem den Vorteil, dass die '0' nur einmal
vorkommt.
Darstellbar sind mit dem Zweierkomplement die Zahlen von -2N-1 bis 2N-1-1.
Zweierkomplementzahlen werden auch als signed binary numbers oder signed
integers bezeichnet.
Berechnung des Zweierkomplements wird in der Übung behandelt!
Standardformate für Zweierkomplementzahlen
Prinzipiell kann man beliebige Zahlenformate vereinbaren, in der Praxis werden fast
ausschließlich Zahlenformate mit 8, 16, 32 oder 64 Bits eingesetzt. In den meisten
Programmiersprachen gibt es vordefinierte ganzzahlige Datentypen mit
unterschiedlichen Wertebereichen.
Je größer das Format, desto größer ist natürlich der erfasste Zahlenbereich. Einige
typische Formate sind z.B.:
Wertebereich
-27 ... 27-1
oder
-128 ... 127
-215 ... 215-1
oder
-32.768 ... 32.767
-231 ... 231-1
oder
-2.147.483.648 ... 2.147.483.647
-263 ... 263-1
oder
-9.223.372.036.854.775.808 ... 9.223.372.036.854.775.807
Bytes Java
C++
8 Bit
char
byte
16 Bit short int/short
32 Bit
int
64 Bit long
int/long
----
Arithmetische Operationen im Binärsystem werden in den Übungen
ausführlich behandelt!
Gleiches gilt für die Darstellung von rationalen und reellen Zahlen!
2.3 Darstellung logischer Werte
Logische Werte sind die Wahrheitswerte Falsch und Wahr (engl. true oder false).
Sie werden meist durch die Buchstaben T und F abgekürzt.
Auf diesen logischen Werten sind die Booleschen Verknüpfungen NOT (Negation),
19
AND (logisches Und), OR (logisches Oder) und XOR (exklusives Oder) durch die
folgenden Verknüpfungstafeln festgelegt:
NOT
F
T
T
F
OR
F
T
---------------F
T
F
T
T
T
AND
F
T
F
F
F
T
F
T
XOR
F
---------------F
F
T
T
T
T
F
Beispiele:




¬F = T
T AND T = T
F OR F = F
T XOR F = T
Da es nur zwei Wahrheitswerte gibt, könnte man diese durch die beiden möglichen
Werte eines Bits darstellen, z.B. durch 0=F und 1=T.
Da aber ein Byte die kleinste Einheit ist, mit der ein Computer operiert, spendiert man
meist ein ganzes Byte für einen Wahrheitswert. Eine gängige Codierung ist:


F = 0000 0000
T = 1111 1111
20
3 Rechnerorganisation – Teil 1
3.1 Von-Neumann-Architektur
John von Neumann
Computer der ersten Generation waren riesige Geräte. So bestand zum Beispiel der
ENIAC (1943), ein aus Mitteln der amerikanischen Armee finanziertes Projekt, aus
18.000 Vakuumröhren und 1500 Relais, wog 30 Tonnen und verbrauchte 140 KW
an Strom.
Programmiert wurde der ENIAC durch Einstellen von bis zu 6000 mehrstelligen
Schaltern und durch Anschluss unzähliger Buchsen mit einem wahrhaften Wald an
Steckbrückenkabeln.
Im Jahre 1944 legte John von Neumann, ein ehemaliges Mitglied des ENIACProjektes, ein Architektur-Konzept für einen speicherprogrammierten
Universalrechner vor.
Von Neumann fiel auf, dass die Programmierung von Computern mit Unmengen von
Schaltern und Kabeln langsam, mühsam und unflexibel war.
Er erkannte, dass man das Programm mit den Daten im Speicher des Computers
in digitaler Form darstellen konnte, und er erkannte, dass man die schwerfällige,
serielle Dezimalarithmetik des ENIAC, bei der jede Ziffer mit zehn Vakuumröhren (1
On und 9 Off) dargestellt wird, durch parallele Binärarithmetik ablösen kann.
Von-Neumann-Architektur
Sein erster Entwurf ist heute als Von-Neumann-Maschine oder als Von-NeumannArchitektur bekannt.
Wie weitsichtig und visionär sein Konzept war, wird daran deutlich, dass auch
aktuellen Computern noch diese Architektur zugrunde liegt.
Eine Von-Neumann-Maschine weist die folgenden wichtigen Merkmale auf:
1. Ein programmgesteuerter Rechner besteht aus
a. zentraler Recheneinheit (CPU = Central Processing Unit) mit
 einer ALU (Arithmetical Logical Unit) und
 einer Steuereinheit
b. Speicher
c. Ein- und Ausgabe-Einheiten und
d. den internen Datenwegen (Busse)
2. Die Zahlen werden im Rechner binär dargestellt.
3. Die Struktur des Rechners ist unabhängig von dem zu bearbeitenden Problem
(--> Universalrechner). Die verschiedenen Aufgaben werden durch
entsprechende Programme gelöst.
21
4. Programme und von diesen benötigte Daten werden in einem gemeinsamen
Speicher abgelegt. Die Speicherplätze sind gleichlang und werden über
Adressen einzeln angesprochen.
5. Befehle geben nur die Speicheradresse an, wo die Daten abgelegt sind, und
nicht die Daten selbst.
Die bedeutendste Neuerung in der damaligen Zeit war von Neumanns Idee, das
Programm und die Daten zuerst in denselben Speicher zu laden und dann
auszuführen. Bis dahin war das Programm noch hardwaremäßig verschaltet oder
wurde über Lochstreifen schrittweise eingelesen und sofort (streng sequentiell)
bearbeitet. Nun war es möglich:
1. Sprünge einzuführen, sowohl auf vorhergehende wie spätere
Programmsequenzen, und
2. Programmcode während des Programmablaufes zu modifizieren
(allerdings eine sehr riskante, fehleranfällige Möglichkeit!).
Von Neumann erreicht also mit seinem Konzept, daß der Rechner selbstständig
logische Entscheidungen treffen kann.
Damit ist der Übergang vom starren Programmablauf zur flexiblen
Programmsteuerung oder, anders ausgedrückt, von der Rechenmaschine zur
Datenverarbeitungsanlage vollzogen.
Nachteile der von-Neumann-Architektur
Die von Neumann Architektur hat aber auch einige Nachteile:
1. Im Speicher kann man Befehle und Daten anhand des Bitmusters nicht
unterscheiden.
22
2. Im Speicher kann man variable und konstante Daten nicht unterscheiden.
3. Bei falscher Adressierung können Speicherinhalte verändert werden, die nicht
geändert werden dürfen, wie z.B. Befehle und Konstanten (Eine Bitänderung bei
einem Befehl erzeugt einen ganz anderen Befehl!)
4. Da Daten und Befehle im Speicher gehalten werden, wird die Verbindung und
Datenübertragung zwischen CPU und Speicher über den Systembus zum sog.
Von-Neumann-Flaschenhals.
Jeglicher Datenverkehr von und zur CPU wird über den internen Bus abgewickelt,
dessen Transfergeschwindigkeit langsamer ist, als die
Verarbeitungsgeschwindigkeit der CPU.
Dieses Problem versucht man in modernen PC's durch die Verwendung von
schnellem Cache-Speicher, der meist in der CPU integriert ist, abzuschwächen.
3.2 Merkmale und Komponenten der Von-Neumann Rechnerarchitektur
3.2.1 Zentrale Recheneinheit / Central Processing Unit
Die Zentrale Recheneinheit oder CPU stellt das "Gehirn" im System dar. Sie
besteht aus Recheneinheit und Steuereinheit.
Ein geschicktes Zusammenspiel der Steuereinheit mit der Recheneinheit sorgt dafür,
dass nacheinander die Befehle geholt und dann ausgeführt werden.
Für arithmetische Operationen, wie z.B. der Addition, oder logische Operationen, wie
z.B. AND und NOT, ist die Recheneinheit zuständig.
Die Recheneinheit hat eine feste Verarbeitungsbreite, z.B. 8, 16, 32 oder 64 Bit.
Man spricht dann von einem 8, 16, 32 oder 64-Bit System.
Recheneinheit oder ALU
Die Aufgabe der Recheneinheit besteht in der Bearbeitung der Daten, besonders
dem Ausführen von arithmetischen und logischen Operationen.
Steuereinheit
Zur wichtigsten Aufgabe der Steuereinheit gehört die Koordination der zeitlichen
Abläufe im Rechner. Dazu muss die Steuereinheit die Befehle aus dem Speicher
holen, entschlüsseln und deren Ausführung steuern. Die Steuereinheit besteht aus
Befehlsregister, Befehlsdecoder, Speicheradressregister und Befehlszähler.
1. Im Befehlsregister (IR=Instruction Register) befindet sich jeweils der aktuell
zu bearbeitende Befehl. Das Befehlsregister ist ein spezielles CPU-Register,
das vom Anwender nicht adressierbar ist.
2. Der Befehlsdecoder entschlüsselt den Befehl und erzeugt die zur Ausführung
notwendigen Hardware-Steuersignale
3. Im Speicheradressregister (MAR=Memory Adress Register) steht


die Adresse des nächsten auszuführenden Befehls oder
die Adresse eines Datenwortes, falls zur Ausführung eines Befehls ein
Datenwort vom Speicher geholt bzw. in den Speicher gebracht werden
muss
23

Der Befehlszähler (PC=Program Counter) übernimmt den Wert des
Speicheradressregisters und erhöht ihn entsprechend der
Befehlslänge.
Bei einem linearen Befehlsablauf kann das Speicheradressregister
diesen Wert als Adresse für den folgenden Befehl übernehmen; bei
einem Sprung ist die neue Adresse im Befehl selbst angegeben.
3.2.2 Speicher
Der Speicher eines von Neumann Rechners besteht aus einer Vielzahl von
Speicherzellen. Jede Speicherzelle kann ein Bit speichern.
Man faßt jeweils mehrere Zellen zu einer Speicherzeile oder einem Speicherwort
zusammen (z.B. bei einem 32-Bit System ergeben 32 Bit ein Speicherwort)
Um ein Speicherwort in einem Speicher ablegen und auch wiederfinden zu können,
zählt man die Speicherplätze vom Speicheranfang bis zum Ende durch, und zwar
byteweise. Diese Zahlen nennt man Adressen.
Man braucht also so viele Adressen, wie der Speicher Bytes enthält.
3.2.3 Interne Datenwege / Busse
Innerhalb eines von Neumann-Rechners entstehen Daten in verschiedenen
Hardware-Komponenten und werden dann in anderen Komponenten benötigt. Der
Transport dieser Daten erfolgt auf den sog. internen Datenwegen. Diese wollen wir
im Folgenden näher betrachten.
Die wichtigsten Busse sind 1) der Datenbus, 2) der Adressbus und 3) der
Steuerbus
1. Der Datenbus wird normalerweise parallel übertragen, d.h. bei einem 32-Bit
System besteht der Datenbus aus 32-Leitungen.
Der Datenbus überträgt die Daten, die über den Adressbus angefordert werden.
2. Der Adressbus besteht aus den Adressleitungen, deren Anzahl vom
Adressbereich der CPU abhängt.
Ein typischer Wert sind heute 32-Leitungen, um mit 32 Bit bis zu 4 GByte
adressieren zu können.
Über den Adressbus werden nur Adressen übertragen
3. Der Steuerbus ist der Teil des Bussystems, welcher die Steuerung des Busses
bewerkstelligt.
Hierzu zählen unter anderem die Leitungen für die Interrupt-Steuerung,
Buszugriffssteuerung, der Taktung (falls ein Bustakt erforderlich ist), Reset- und
Statusleitungen.
24
3.2.4 Ein-/Ausgabeeinheit
Eingabe-/Ausgabewerk steht im Konzept des Von-Neumann-Rechners für alle die
Komponenten, die den Computer mit "draußen" verbindet.
"Draußen" bedeutet dabei nicht „außerhalb des Gehäuses", sondern "nicht mehr zu
dem eigentlichen, universellen Von Neumann Rechner-Konzept gehörend".
In einem modernen PC verkörpert
der Prozessor das Rechenwerk und die Steuereinheit,
die Northbridge mit dem RAM den Speicher bzw. das Speicherwerk und
der Rest, angefangen mit der Southbridge, bildet das Eingabe/Ausgabewerk.
Eine Grafikkarte, für die allermeisten Rechner notwendiges Bauteil zur
Kommunikation mit dem Benutzer, ist bereits "draußen" – und heutzutage bereits ein
spezialisierter Rechner für sich.
Zu dem Ein-/Ausgabewerk gehören also z.B. der PCI-Bus für Einsteckkarten ebenso
wie die verschiedenen Schnittstellen für Peripheriegeräte und Festplatten.
3.3 Mainboard
Zentraler Bestandteil des Rechners ist das Mainboard (auch: Hauptplatine oder
Motherboard).
Auf dem Mainboard sind die einzelnen Bauteile wie die CPU (Hauptprozessor), der
Arbeitsspeicher (RAM), der Chip mit dem BIOS sowie verschiedene Steckplätze
und Schnittstellenbausteine montiert.


Es bietet Steckplätze für Grafikkarten (AGP oder PCI-Express)
Für PCI-Steckkarten (Soundkarte, Netzwerkkarte usw.)
Außerdem beherbergt das Mainboard den sog. Chipsatz. Der Chipsatz besteht in
der Regel aus Northbridge und Southbridge.
3.3.1 Systembus
Die oben genannten Erweiterungskarten stecken in den bereits erwähnten
Steckplätzen des Mainboards.
25
Diese Steckplätze sind untereinander und mit der Hauptplatine durch einen sog. Bus
(auch Systembus genannt) verbunden.
Hierbei handelt es sich um eine Serie paralleler Datenleitungen, über die Daten
zwischen Prozessor und Peripheriegeräten ausgetauscht werden.
Busse gibt es mit unterschiedlich vielen Leitungen und mit unterschiedlichen
Protokollen (oder Standards) zur Übertragung von Daten über diese Leitungen.
Die Leistungsfähigkeit eines Busses wird an der Anzahl von Bytes gemessen, die
in einer Sekunde übertragen werden können. Moderne Busse schaffen einige
hundert MByte pro Sekunde.
Da an dem Bus viele unabhängig und gleichzeitig arbeitende Peripheriegeräte
angeschlossen sind, muss auch der Buszugang durch einen Controller (den
Buscontroller) geregelt werden.
Die Standards für Technik und Datenübertragung des Busses haben sich in den
letzten Jahren sehr verändert. Einige dieser Standards heißen:





ISA (Industry Standard Architecture)
EISA (Extended ISA)
PCI (Peripheral Component Interconnect)
AGP (Accelerated Graphics Port)
(Ist strenggenommen kein Bus, da man nur genau ein Gerät anschließen
kann)
PCI-Express
Mit den Standards ändern sich auch die Steckplätze. Bis vor kurzem hatten aktuelle
Mainboards einen AGP-Steckplatz für Grafikkarten. Der AGP Steckplatz ist derzeit
aber bereits fast durch seinen leistungsfähigeren Nachkommen PCI-Express x16
ersetzt worden.
3.3.2 BIOS
Das Akronym BIOS steht für Basic Input Output System.
Das BIOS ist heutzutage in einem sogenannten Flash-EEPROM Baustein
gespeichert.
EEPROM steht für Electrically Erasable Programmable Read-Only Memory, d. h.
elektrisch löschbarer, programmierbarer Nur-Lese-Speicher.
Im diesem BIOS-Chip sind grundlegende und sehr elementare Hilfsprogramme
zur Ansteuerung der verschiedenen Hardwarekomponenten wie Tastatur, Maus,
Festplatte und Grafikcontroller abgelegt.
Die Programme werden auch als Interrupts (engl. für Unterbrechungen) bezeichnet,
weil Sie je nach Prioritätenstufe andere gleichzeitig laufende Programme
unterbrechen dürfen.
26
Jeder Tastendruck, jede Mausbewegung, führt zum Aufruf eines Interrupts. Dabei
werden die Parameter des aufgetretenen Ereignisses ermittelt und es wird ggf.
darauf reagiert.
Der Boot-Vorgang
Zusätzlich enthält das BIOS Programme, die nach dem Einschalten des Rechners
ausgeführt werden. Dazu gehört eine Prüfung, welche Geräte angeschlossen sind,
ein Funktionstest des Hauptspeichers und das Laden des Betriebssystems von
Festplatte, Netzwerk, Diskette oder CD-Rom.
Dies ist ein mehrstufiger Prozess


ein Programm im BIOS lädt und startet ein Ladeprogramm (loader)
Dieses Ladeprogramm lädt das eigentliche Betriebssystem.
Diesen Vorgang nennt man booten
(von engl. bootstrapping = Schuh schnüren.
Eine andere Geschichte behauptet, booten stamme aus den Geschichten von Baron
Münchhausen, der sich in der englischen Fassung nicht an den eigenen Haaren,
sondern an den eigenen Schnürsenkeln=boot straps aus dem Sumpf gezogen hat.)
Verbindung zwischen Betriebssystem und Hardware
Das BIOS stellt also (gemeinsam mit den Treibern) gewissermaßen die Brücke
zwischen der angeschlossenen Hardware und dem Betriebssystem dar.
27
3.3.3 CPU
Die CPU (oder auch Prozessor) ist das Kernstück eines Computers. Er dient der
Verarbeitung von Daten, die sich in Form von Bytes im Speicher des Rechners
befinden.
Zwei wesentliche Bestandteile der CPU sind Register und ALU (arithmetical logical
unit).
Die ALU ist eine komplizierte Schaltung, welche die eigentlichen mathematischen
und logischen Operationen ausführen kann.
Register sind extrem schnelle Hilfsspeicherzellen, die direkt mit der ALU verbunden
sind.
Die ALU-Operationen erwarten Ihre Argumente in bestimmten Registern und liefern
ihre Ergebnisse wieder in Registern ab.
Auch der Datentransfer vom Speicher zur CPU läuft durch die Register.
Üblicherweise sind diese Register 32 oder 64 Bit breit. Man spricht dann von einem
32-Bit oder einem 64 Bit Prozessor, meint damit aber mehrere, eigentlich zu
unterscheidende Aspekte:

D: Die Anzahl der Datenbits, die von dem verwendeten Prozessor in einem
arithmetischen Befehl verknüpft werden können.
(Breite der Register)

S: Die Anzahl der Bits, die von einem LOAD- oder STORE-Befehl
gleichzeitig zwischen dem Speicher und einem CPU-Register transportiert
werden können
(s. Datenbus).

A: Die Anzahl der Bits, die zur Adressierung verwendet werden können.
(s. Adressbus)
Cache Speicher
Der Cache-Speicher der CPU ist ein schneller Puffer-Speicher, in welchen ständig
Kopien des Hauptspeichers vorgehalten werden, die möglicherweise als nächstes
von der CPU benötigt werden.
Die Ziele beim Einsatz eines Caches sind eine Verringerung der Zugriffszeit bzw.
eine Verringerung der Anzahl der Zugriffe auf den zu cachenden Speicher.
Ein weiterer eher nebensächlicher Effekt beim Einsatz von Caches ist die verringerte
Bandbreitenanforderung an die nächsthöhere Speicherebene der
Speicherhierarchie. Dadurch, dass oftmals der Großteil der Anfragen vom Cache
beantwortet werden kann (sog. Cache-Hit), sinkt die Anzahl der Zugriffe und damit
die Bandbreitenanforderung an den zu cachenden Speicher. Ein moderner
28
Mikroprozessor ohne Cache würde selbst mit unendlich kleiner Zugriffszeit des
Hauptspeichers dadurch ausgebremst, dass nicht genügend Speicherbandbreite zur
Verfügung steht, weil durch den Wegfall des Caches die Anzahl der Zugriffe auf den
Hauptspeicher und damit die Anforderung an die Speicherbandbreite stark
zunehmen würde.
Bei CPUs kann der Einsatz von Caches somit zum Verringern des Von-NeumannFlaschenhals der Von-Neumann-Architektur beitragen. Die
Ausführungsgeschwindigkeit von Programmen kann dadurch im Mittel enorm
gesteigert werden.
Ein Nachteil von Caches ist das schlecht vorhersagbare Echtzeitverhalten, da die
Ausführungszeit eines Befehls aufgrund von Cache-Misses nicht immer konstant ist.
Cache Hierarchie
Da Cache-Speicher zwar sehr schnell, aber auch sehr teuer ist, ist es (aus
kostengründen) nicht ohne weiteres möglich, einen Cache zu bauen, der gleichzeitig
sowohl groß als auch schnell ist.
Man kann aber mehrere Caches verwenden - z. B. einen kleinen schnellen und
einen großen langsameren Cache (der aber immer noch Größenordnungen schneller
ist als der zu cachende Speicher). Damit kann man die beiden konkurrierenden Ziele
von geringer Zugriffszeit und Cachegröße bei vertretbaren Kosten realisieren.
Existieren mehrere Caches, so bilden diese eine Cache-Hierarchie, die Teil der
Speicherhierarchie ist. Die einzelnen Caches werden als Level-1 bis Level-n
durchnummeriert (kurz: L1, L2 usw.).
Die niedrigste Nummer bezeichnet hierbei den Cache mit der kleinsten Zugriffszeit welcher also als erstes durchsucht wird.
Enthält der L1 Cache die benötigten Daten nicht, so wird der nächste (meist etwas
langsamere, aber größere) Cache (also der L2) durchsucht usw. Das geschieht
solange, bis die Daten entweder in einem Cache-Level gefunden oder alle Caches
ohne Erfolg durchsucht wurden (Cache Miss).
In diesem Fall muss auf den verhältnismäßig langsamen Speicher zugegriffen
werden.
Moderne CPUs haben meist zwei oder drei Cache-Levels. Mehr als drei ist eher
unüblich. Festplatten haben nur einen Cache.
Prozessor-Cache
Bei CPUs kann der Cache direkt im Prozessor integriert, d.h. On-Die (bei aktuellen
Prozessoren bis zu 4MB L2-Cache) oder extern auf der Hauptplatine oder auf der
Prozessorplatine (z.B. beim Pentium II - Slot1) platziert sein.
Je nach Ort des Caches arbeitet dieser mit unterschiedlichen Taktfrequenzen: Der L1
ist fast immer direkt im Prozessor integriert und arbeitet daher mit dem vollen
29
Prozessortakt, also meist mit mehreren Gigahertz.
Ein externer Cache hingegen wird oftmals nur mit einigen hundert Megahertz
getaktet.
Gängige Größen für L1-Caches sind 4 bis 256 KB und für den L2 256 bis 4096 KB.
Bei aktuellen CPUs ist der L1- und der L2-Cache direkt auf dem Prozessor-Kern
(Die) integriert.
Prozessor-Hersteller
Die beiden wichtigsten Prozessor-Hersteller sind Intel (z.B. Pentium 4, Core 2 Duo
u.a.) und AMD (Athlon XP, Athlon64, Athlon64 X2 u.a.).
Jeder Hersteller verwendet mittlerweile einen eigenen Prozessor-Sockel:

Bei Intel ist der aktuellste Sockel zur Zeit der Sockel 775.
In diesen Sockel passen Pentium 4, Pentium D, und Core 2 Duo Prozessoren.

Die aktuellsten Sockel bei AMD sind zur Zeit der Sockel 939 und der neue
Sockel AM2.
In den Sockel 939 passen Athlon64, Athlon64 X2 und Opteron Prozessoren.
Auch für den Nachfolger, den Sockel AM2, gibt es passende Athlon64 und
Athlon64 X2 Modelle.
3.3.4 Arbeitsspeicher / RAM (Random Access Memory)
Im Hauptspeicher/Arbeitsspeicher/RAM eines Computers werden Programme und
Daten abgelegt.
Die Daten werden von den Programmen bearbeitet. Der Inhalt des RAM's ändert
sich ständig - insbesondere dient der Arbeitsspeicher nicht der permanenten
Speicherung von Daten. Fast immer ist er aus Speicherzellen aufgebaut, die ihren
Inhalt beim Abschalten des Computers verlieren.
Der Arbeitsspeicher ist also ein flüchtiges oder volatiles Speichermedium. Beim
nächsten Einschalten werden alle Bits des Arbeitsspeichers auf ihre
Funktionsfähigkeit getestet und dann auf 0 gesetzt.
Die Bits des Arbeitsspeichers sind byteweise organisiert. Jeder Befehl kann
immer nur auf ein ganzes Byte zugreifen, um es zu lesen, zu bearbeiten oder zu
schreiben.
Den 8 Bits, die ein Byte ausmachen, kann noch ein Prüfbit beigegeben sein. Mit
dessen Hilfe überprüft der Rechner ständig den Speicher auf Fehler, die z.B. durch
eine Spannungsschwankung oder einen Defekt entstehen können.
Diese Fehlerüberprüfung funktioniert nach dem selben Prinzip wie das Prüfbit beim
ASCII-Code.
die Speicherhardware setzt und liest das Prüfbit automatisch.
Meist setzt diese das Prüfbit so, dass die Anzahl aller Einsen in dem gespeicherten
Byte zusammen mit dem Prüfbit geradzahlig wird (even parity), daher heißt das
30
Prüfbit auch Parity-Bit.
Wird ein Bit durch einen Fehler oder Defekt verändert, erkennt das die
Speicherhardware daran, daß die Anzahl der Einsen ungerade wird.
Diese sog. ECC-Fehlerkorrektur wird vorwiegend im Server-Bereich eingesetzt.
Die Verwendung von Prüfbits verliert allerdings seit einiger Zeit wegen der höheren
Zuverlässigkeit der Speicherbausteine an Bedeutung.
Speichertypen
Es gibt grundsätzlich zwei Typen von Speicherzellen:


Dynamisches RAM oder DRAMs und
Statisches RAM oder SRAMs.
DRAMs speichern die elektrische Ladung in Kondensatoren, die periodisch wieder
aufgefrischt werden müssen, da sich die gespeicherte Information aufgrund von
Leckströmen mit der Zeit verflüchtigt.
Aus diesem Grund sind DRAMs langsamer (aber dafür billiger) als SRAMs und
werden hauptsächlich für den Hauptspeicher verwendet.
SRAMs haben die Fähigkeit, ihre Inhalte zu behalten, solange der Strom fließt:
Sekunden, Minuten, Stunden oder gar Tage. SRAMs sind sehr schnell, die typischen
zugriffszeiten betragen nur wenige Nanosekungen. Aus diesem Grund sind werden
sie häufig als Cache-Speicher der Ebene 2 (Second Level Cache oder L2 Cache)
verwendet.
Cache Speicher stellt eine Möglichkeit dar, das Problem des Von-Neumann
Flaschenhalses zu mildern.
Der schnelle Cache steht dem Prozessor als Puffer zur Verfügung. In diesem
Speicher wird immer ein gewisser Teil des Arbeitsspeichers vorgehalten, so daß
die Datenübertragung vom und zum Prozessor beschleunigt wird, da die Lese- und
Schreibzugriffe beim Cache schneller sind, als beim Hauptspeicher.
Für Arbeitsspeicher kommt heutzutage eine verbesserte Version des DRAM zum
Einsatz:

SDRAM (Synchronous Dynamic RAM), dessen Taktrate optimal auf die CPU
abgestimmt ist.

DDR-SDRAM (Double Data Rate) stellt eine Weiterentwicklung von SDRAM
dar.
Bei DDR-SDRAM wird im Vergleich zu SDRAM innerhalb eines Taktes die
doppelte Menge an Daten übertragen.
31
Technische Realisierung
Technisch wird der Arbeitsspeicher heutiger Computer aus speziellen Bauelementen,
den Speicherchips aufgebaut.
Diese werden nicht mehr einzeln, sondern als Speichermodule angeboten.
Dabei sind jeweils mehrere Einzelchips zu sogenannten SIMM- oder DIMMModulen (single/dual inline memory module) zusammengefasst.
Das sind kleine Mini-Platinen mit 72 Pins auf einer Seite der Steckverbindung
(SIMM) oder mit 168 Pins, d.h. je 84 Pins auf beiden Seiten (DIMM), auf denen
jeweils 8 oder 9 gleichartige Speicherchips sitzen.
Gängige Taktfrequenzen für DDR-SDRAM sind:







DDR-266 mit 133 MHz, d.h. 266 MHz effektivem Takt (2,1 GByte/s
DDR-333 mit 166 MHz, d.h. 333 MHz effektivem Takt (2,7 GByte/s)
DDR-400 mit 200 MHz, d.h. 400 MHz effektivem Takt (3,2 GByte/s)
DDR2-1066 mit 533 MHz , d.h. 1066 MHz effektivem Takt
3.3.5 Northbridge
Die Northbridge ist in modernen Computersystemen für den
Hochgeschwindigkeits-Datenaustausch verantwortlich.
So enthält in den meisten PC-Systemen die Northbridge unter anderem den
Speichercontroller, der den Datentransfer zwischen CPU und Hauptspeicher
verwaltet.
Der Speichercontroller ist über den sog. Frontsidebus (FSB) an die CPU
gekoppelt.
Außerdem synchronisiert der Chip der Northbridge noch den Datentransfer und die
Datensteuerung zwischen CPU und AGP- und/oder PCI-Express Grafikkarte.
In neueren Systemen ist der Speichercontroller nicht mehr Bestandteil der
Northbridge, sondern direkt in den Prozessor integriert (z.B. Athlon64).
Auf diese Weise wird der Datentransfer zwischen CPU und Speicher zusätzlich
beschleunigt, da die "externe" Verbindung zwischen CPU und Northbridge (FSB)
wegfällt, wodurch die entstehenden Latenzen beim Speicherzugriff deutlich reduziert
werden können.
Die Northbridge ist über den PCI-Bus, über PCI-X (schnellere 64Bit Variante von
PCI) , oder über eine andere proprietäre Schnittstelle wie z.B. Hypertransport an
die Southbridge angebunden.
32
Northbridge und Southbridge bilden zusammen den Chipsatz.
3.3.6 Southbridge
Neben der Northbridge ist die Southbridge der wichtigste Bestandteil des
Chipsatzes.
Während die Northbridge die Verbindung zwischen CPU und Arbeitsspeicher
darstellt ist die Southbridge für den Datentransfer und die Datensteuerung zwischen
peripheren Geräten (PCI-Steckkarten, IDE-Laufwerke wie Festplatten und CD/DVD-ROM) zuständig.
Die Southbridge kontrolliert neben den angeschlossenen Laufwerken auch die
Maus und die Tastatur, sowie verschiedene über USB angeschlossene Geräte,
wie z.B. Drucker, Scanner usw.
Außerdem sind heutzutage oft Peripheriegeräte bereits in die Southbridge
integriert, wie z.B. OnBoard-Soundlösungen und LAN-Schnittstellen.
In moderneren Systemen kann die Southbridge sogar direkt in die Northbridge
integriert sein, so z.B. bei nVidia's nForce4 Chipsatz.
Die Bezeichnungen Northbridge und Southbridge resultieren aus der räumlichen
Anordnung der jeweiligen Chips auf dem Mainboard.
Dreht man die Platine so, daß der Prozessorsockel oben ist, liegt die Northbridge
33
näher an der CPU, also "im Norden der Platine", während die Southbridge weiter
"südlich" im unteren Teil der Platine zu finden ist.
3.3.7 PCI-Bus
PCI steht für Peripheral Component Interconnect und dient folgerichtig zur
Verbindung von Peripheriegeräten mit dem Chipsatz (Northbridge+Southbridge)
und dem Prozessor.
Der PCI-Bus ist Industriestandard und fester Bestandteil von IBM-kompatiblen PC's,
Apple Macintoshs.
Der PCI-Bus System wurde von Intel im Jahr 1990 entwickelt, um den zu langsam
gewordenen ISA- und dessen Nachfolger, den EISA-Bus abzulösen.
Der ursprüngliche PCI-Bus übertrug 32 Bit pro Zyklus und lief mit 33 Mhz (d.h. einer
Zykluszeit von 30 ns).
Die Bandbreite betrug insgesamt 133 Mbyte/s.
1993 wurde der PCI 2.0 eingeführt, und 1995 kam PCI 2.1 heraus. PCI 2.2 hat
Eigenschaften, die ihn für mobile Computer geegnet machen (vorwiegend solche zur
Einsparung von Batteriestrom). Mittlerweile liegt PCI 2.3 vor.
Der PCI-Bus läuft mit bis zu 66 Mhz und handhabt 64 Bit-Transfers. Somit hat er
eine Bandbreite von 528 Mbyte/s.
Ursprünglich waren auch Grafikkarten über den PCI-Bus ins System eingebunden.
Allerdings reichte die maximale Bandbreite des Busses mit den steigenden
Anforderungen der sich rasch weiterentwickelnden 3D-Grafik nicht mehr aus, nicht
zuletzt deswegen, weil sich aufgrund der Bus-Struktur, mehrere Geräte die
Bandbreite teilen müssen.
34
Als Lösung des Bandbreitenproblems für Grafikkarten wurde deswegen der
Accelerated Graphics Port - kurz AGP - entwickelt.
3.3.8 Accelerated Graphics Port (AGP)
3D-Grafik ist aus keinem Anwendungsbereich mehr weg zu denken. Ob Forschung,
Unterhaltung oder Multimedia, alles setzt auf 3D-Visualisierung.
Echtzeit-3D Grafik benötigt jedoch große Datenmengen. Diese Datenmengen waren
irgendwann zu groß, um vom bereits leicht betagten PCI-Bus bewältigt zu werden,
der sich im Laufe der Zeit als Engpass für die Grafikausgabe herausstellte.
Vor allem deshalb, weil der PCI ein Bus ist, den sich mehrere Erweiterungskarten
und evtl. die Southbridge (die ebenfalls über den PCI-Bus mit der Northbrdige
verbunden war) teilen müssen.
Mitte des Jahres 1996 begann ein Konsortium von Mainboard- und
Grafikkartenherstellern unter der Führerschaft von Intel mit der Entwicklung des
AGP.
Der Accelerated Graphics Port ist weder ein Ersatz für den PCI-Bus noch eine
Erweiterung. Zudem ist er strenggenommen auch kein Bus.
Er ist eine einzelne Punkt-zu-Punkt-Verbindung zwischen Grafikkarte und
Chipsatz (i.e. der Northbridge)
Der AGP ist ein spezielles Interface für die Kopplung von Prozessor,
Hauptspeicher und Grafikkarte.
Um auch Speicherzugriffe an der CPU vorbei zu ermöglichen (und so für Entlastung
des Prozessors zu sorgen), bekommt der AGP vom System-BIOS exklusiv einen
bestimmten Speicherbereich im Hauptspeicher zugewiesen. Dieser Speicher wird
in der AGP-Spezifikation AGP-Aperture genannt.
Allerdings sind Lese- und Schreibzugriffe auf den Hauptspeicher bedeutend
langsamer als auf den lokalen Speicher der Grafikkarte
Der AGP hat eine Breite von 32 Bit und einen Takt von 66 Mhz und somit
ursprünglich eine Bandbreite von 266 MBytes.
In weiteren Ausbaustufen der AGP-Spezifikation wurden die Übertragungsraten
weiter angehoben:



AGP 2x --> 532 MBytes/s
AGP 4x --> 1,06 GByte/s
AGP 8x --> 2,1 GByte/s
Mit der neuesten Version AGP 8x ist das Ende der Fahnenstange erreicht, da sich,
auf Grund von Timing-Problemen bei hohen Taktungen, die durch die parallele
Datenübertragung entstehen, und dem dadurch zunehmend komplizierter
werdenden Platinendesign, die Geschwindigkeit nicht weiter steigern lässt.
35
Dazu kommt noch, dass heutige Grafikkarten über so viel eigenen Speicher
verfügen, dass sie nur noch verhältnismäßig selten auf den Hauptspeicher zugreifen
müssen.
Im Vergleich zum Grafikspeicher ist der Arbeitsspeicher des PCs relativ langsam.
AGP war als schnelle Punkt-zu-Punkt-Verbindung zum Arbeitsspeicher sinnvoll,
solange schnelles VRAM (Video RAM oder Grafikspeicher) auf der Grafikkarte
übermäßig teuer war, was heute nicht mehr der Fall ist.
Seit Mitte 2006 wurden kaum noch neue Hauptplatinen mit Unterstützung für den
AGP vorgestellt. Stattdessen verwenden die meisten Hersteller den schnelleren PCIExpress-Port auf ihren Hauptplatinen.
3.3.9 PCI-Express
PCI-Express (Peripheral Component Interconnect Express) (Abk. PCIe od. PCI-E)
ist der Nachfolger von PCI und AGP und bietet eine höhere Datenübertragungsrate.
PCIe ist im Gegensatz zum PCI-Bus kein paralleler Bus, sondern eine serielle
Punkt-zu-Punkt-Verbindung.
Die Datenübertragung erfolgt über sogenannte Lanes (dt. Spuren, Wege), wobei
jede Lane aus einem Leitungspaar für das Senden und einem zweiten Paar für das
Empfangen besteht.
Trotz dieses völlig anderen physischen Aufbaus ist PCIe softwareseitig voll
kompatibel zu PCI, sodass weder Betriebssysteme, noch Treiber, noch
Anwendungsprogramme angepasst werden müssen.
PCIe ist vollduplexfähig und arbeitet mit einer Taktrate von 1,25 GHz DDR
(effektiven 2,5 GHz).
Daraus berechnet sich die Datenrate einer Lane zu max. 250 MByte/s pro
Richtung, da für ein 8 Bit-Paket ein 10-Bit Paket übertragen wird, in dem 2 Bit für
36
Verwaltungszwecke eingesetzt werden.
Zählt man beide Richtungen zusammen erhält man sogar ca. 500 MByte/s (zum
Vergleich: der Standard-PCI Bus mit 32-Bit Busbreite bei 33 MHz erreicht nur
maximal 133 MByte/s).
Verwendet man nur eine Lane, spricht man von PCIe x1.
Durch Koppelung mehrerer Lanes kann man die Datenrate erhöhen, etwa x2 mit
2 Lanes bis zu x32 mit 32 Lanes. Die PCI-SIG (Special Interest Group) plant
darüberhinaus zukünftig auch Versionen mit 500 und 1000 MByte/s pro Lane.
Im Endnutzerbereich wird PCIe x1 als Ersatz für den PCI-Bus und PCIe x16 zur
Anbindung einer Grafikkarte verwendet (PCI-Express For Graphics, PEG), was
somit auch den AGP überflüssig macht.
PEG oder PCIe x16 bietet eine Bandbreite von 4 GByte/s in jede Richtung, also
insg. 8 GByte/s.
Die Slots sind außerdem abwärts kompatibel, d.h. eine x4 Karte kann z.B. auch in
einen x8 Slot gesteckt werden, die überzähligen vier Lanes werden dann nicht
genutzt.
Umgekehrt ist dies momentan nur bei SLI (Scalable Link Interface) üblich.
Denn obwohl die Slots für die Grafikkarten die Größe von x16 Slots haben, werden
beim Einsatz von zwei Grafikkarten die 16 Lanes auf beide Slots verteilt, so daß jede
Grafikkarte nur 8 Lanes zur Verfügung hat, falls das Mainboard bzw. der darauf
verbaute Chipsatz keine vollwertigen x16 Slots für beide Grafikkarten bereitstellt.
3.3.10
IDE (ATA/ATAPI)
IDE bedeutet Integrated Drive Electronics.
Im Unterschied zu den Festplatten der ersten Generation, ist der FestplattenController nicht mehr auf einer zusätzlichen Steckkarte untergebracht, sondern in
die Festplatte integriert.
Die Weiterentwicklung von IDE ist EIDE (Extended IDE).
Die (E)IDE-Schnittstelle bezeichnet man oft auch als ATA-Schnittstelle (Advanced
Technology Attachment), allerdings ist EIDE und ATA nicht dasselbe:

(E)IDE definiert den Anschluß der Laufwerke, wie Pinbelegung, Stecker,
Kabel und elektrische Signale.

ATA definiert das Protokoll, anhand dessen die Daten über die Leitungen
übertragen werden.
Der Datentransfer zwischen Peripherie und Hauptspeicher kann entweder über die
CPU oder über das sogenannte Bus Mastering ausgeführt werden.
37
Bus Mastering ist das effektivere Verfahren, weil durch direkten Speicherzugriff
(Direct Memory Access: DMA) die CPU entlastet wird.
Für den Datentransfer gibt es zwei Protokolltypen:
1.
2.
Den älteren PIO-Modus (Programmed Input/Output)
den neueren UDMA-Modus (Ultra Direct Memory Access)
Beim PIO-Modus ist der Prozessor für jeden Lese- und Schreibvorgang
verantwortlich.
Der UDMA-Modus kann über den DMA-Controller direkt auf den Arbeitsspeicher
zugreifen. So kann der Prozessor sich um andere Aufgaben kümmern. Das gesamte
System läuft schneller.
Modus
IDE (ATA-1) PIO 0
IDE (ATA-1) PIO 1
IDE (ATA-1) PIO 2
IDE Multiword-DMA 0
IDE Multiword-DMA 1
IDE Multiword-DMA 2
E-IDE (Fast ATA-2) PIO 3
E-IDE (Fast ATA-2) PIO 4
Bandbreite
3,33 MByte/s
5,22 MByte/s
8,33 MByte/s
4,16 MByte/s
13,33 MByte/s
16,66 MByte/s
11,11 MByte/s
16,66 MByte/s
Einführung
1989
1994
Der Ultra-DMA-Modus (Ultra-ATA) unterstützt höhere Datenübertragungsraten und
besitzt eingebaute Sicherheitsmechanismen.
Zusätzlich wird die Belastung des Prozessors bei der Datenübertragung durch das
sog. Bus-Mastering reduziert.
Das Bus-Mastering ist ein Datentransfer-Verfahren für die Übertragung von Daten
und Befehlen, bei dem der Host-Controller direkt auf dem Arbeitsspeicher zugreift,
ohne den Prozessor zu belasten.
Für alle Ultra-ATA-Festplatten (133/100/66) wird ein UDMA-Kabel benötigt. Dieses
Flachbandkabel hat 80 Leitungen. 40 für den Datenverkehr und 40 für die Erdung.
Der Ultra-DMA-Standard 133 ist abwärtskompatibel. An diesen Controllern lassen
sich auch andere Ultra-ATA-Festplatten (66 und 100) betreiben.
Modus
Bandbreite
Einführung
16,66 MByte/s
1996
Ultra-DMA 0 (ATA-16 / ATA-3)
25,0 MByte/s
Ultra-DMA 1 (ATA-25)
33,33 MByte / sek.
1997
Ultra-DMA 2 (ATA-33 / ATA-4)
44,4 MByte / sek.
Ultra-DMA 3 (ATA-44)
1999
Ultra-DMA 4 (Ultra-ATA-66 / ATA-5) 66,66 MByte / sek.
Ultra-DMA 5 (Ultra-ATA-100 / ATA-6)99,99 (100) MByte / sek.2000
2001
Ultra-DMA 6 (Ultra-ATA-133 / ATA-7)133 MByte / sek.
38
Konfiguration von EIDE-Geräten
An einen (E)IDE-Strang können maximal 2 Geräte angeschlossen werden.
Das Master-Gerät wird am Kabelende angesteckt. Der Jumper sollte hinten am Gerät
auf Master (M) gesteckt sein. Ein zweites Gerät kommt an den zweiten Stecker, in
der Mitte des Kabels. Dies wird als Slave betrieben. Entsprechend sollte der Jumper
(S) gesteckt sein.
Bei der Verwendung von Cable Select (CS) muss bei beiden Geräten entsprechend
der Jumper gesteckt sein. Die Position bzw. die Betriebsart wird in diesem Fall über
eine (nicht) durchverbundene Ader im Flachbandkabel eingestellt. In manchen
Gerätekonstellationen funktioniert diese Automatik nicht. Deshalb ist angeraten in
jedem Fall die manuelle Einstellung vorzunehmen. Bei nur zwei Geräten ist das auch
kein Problem.
Die Software-Konfiguration von Festplatten und Laufwerken wird im BIOS
vorgenommen. Dort stellt man den EIDE-Anchluß auf Auto-Detect (Autoerkennung)
ein. Erst wenn diese Einstellung fehlschlägt, dann kommt man um das manuelle
Eintragen der Festplatten oder Laufwerks-Parameter nicht herum. Im Regelfall ist
das nicht notwenig.
ATAPI - Advanced Technology Attachment with Packet Interface
ATAPI bedeutet Advanced Technology (AT) Attachment with Packet Interface
und ist ein Erweiterung des Befehlssatzes ATA zum Anschluss eines CD-RomLaufwerks oder eines anderen Wechsel-Laufwerks (z. B. ZIP-Drive) an die EIDESchnittstelle.
Im aufkommenden Multimedia-Zeitalter wurden Computer mit CD-Rom-Laufwerken
als Wechsel-Massenspeicher ausgestattet. Mit dem Laufwerk wurde auch eine CDRom-Controller-Steckkarte in den Computer eingebaut. Jeder CD-Rom Hersteller
lieferte seine eigene, speziell für seine eigenen CD-Rom-Laufwerke, eine ControllerKarte mit.
Als Zwischenstufe wurden Soundkarten mit den 3 bis 4 wichtigsten Schnittstellen
onboard ausgestattet. Auf die Dauer, und mit dem einsetzenden CD-Rom-Boom,
wurde das aber zu teuer. Außerdem waren die proprietären Schnittstellen nicht
schnell genug, um die Aufgaben in einem Multimedia-Computer erledigen zu können.
Die Lösung war, CD-Rom-Laufwerke entweder an den SCSI-Bus oder die EIDESchnittstelle anzuschließen. Für die billigen Consumer-Computer wurde die EIDESchnittstelle gewählt.
Dabei gab es aber ein Problem: Für jedes Gerät, das an der EIDE-Schnittstelle
angeschlossen ist, wird ein fester Laufwerksbuchstabe vergeben. Und somit ist es
kein Wechsel-Laufwerk mehr. Der CD-Rom-Wechsel würde einen Neustart des
Computers nach sich ziehen.
Aus diesem Grund wurde der ATAPI-Befehlssatz entwickelt um CD-ROMs über die
EIDE-Schnittstelle steuern zu können. Das BIOS ist sogar in der Lage das
Betriebssystem von einer CD-ROM oder DVD zu booten.
39
CD- oder DVD-Laufwerke werden nun ohne einen speziellen Treiber erkannt und
ins System eingebunden. Für ältere Modelle mußte man immer einen zusätzlichen
Treiber für die Controller-Karte installieren.
3.3.11
S-ATA (Serial ATA)
Schnittstellen für Massenspeicher waren bisher immer in paralleler
Ausführung.
Mit zunehmender Übertragungsgeschwindigkeit ergeben sich
technische Schwierigkeiten, die für die Übertragungsrate eine
obere Grenze setzen:

Bei parallelen Bussen wird es bei höheren Übertragungsraten immer
schwieriger, den Datenfluss auf allen Leitungen synchron zu halten
Je länger ein Kabel ist, desto eher treten Laufzeitdifferenzen zwischen den
parallelen Signale auf. Dies kann nur durch eine kürzere Übertragungsstrecke
oder eine geringere Taktrate verhindert werden.

Zusätzliche Masseleitungen führen zu dicken und unflexiblen Flachband- oder
Rundkabeln, die die Luftströmung innerhalb eines Computers verhindert.
Serial-ATA hat sich aus dem älteren ATA, auch IDE genannten, Standard
entwickelt. Zu Gunsten der Leistungsfähigkeit entschied man sich, von einem
parallelen Busdesign zu einem bit-seriellen Bus überzugehen, d. h., dass die
Daten seriell übertragen werden (Bit für Bit) und nicht, wie bei den alten ATAStandards, in 16-Bit-Worten.
Mit 150 MByte/s hat S-ATA direkt an die parallele ATA-Schnittstelle (P-ATA) mit 133
MByte/s angeknüpft. Die Serial-ATA-Schnittstelle unterstützt 1,5 GBit/s bei einer
Nettodatenrate von ca. 150 MByte/s.
Um die Kompatibilität zu gewährleisten werden die parrallel vorliegenden Daten
mit Wandlern in serielle Datenströme konvertiert. Die hohe Integrationsdichte und die
extrem schnelle interne Verarbeitungsgeschwindigkeit in integrierten Schaltungen
erlauben die Wandlung in nahezu Echtzeit.
Gegenüber seinem Vorgänger besitzt S-ATA drei Hauptvorteile:
1. höhere Datentransferrate,
2. vereinfachte Kabelführung und
3. Hot-Plug-Fähigkeit zum Austausch von Datenträgern im laufenden Betrieb
(im Serverbereich wichtig).
Seit der Einführung von Serial-ATA wird der bisherige ATA-Standard häufig als
Parallel ATA (P-ATA) bezeichnet, um Verwechslungen zu vermeiden.
Auf neueren Hauptplatinen findet man zu den üblichen zwei
P-ATA-Steckplätzen zusätzlich zwei bis acht S-ATA-Anschlüsse für Festplatten.
40
S-ATA nutzt auf der Link-Layer-Schicht (Kabel) eine Punkt-zu-Punkt Verbindung.
Jedes Gerät hat also seinen eigenen Anschluss.
S-ATA ist nicht auf Festplatten beschränkt, mittlerweile gibt es z. B. auch SATABandlaufwerke, DVD-Laufwerke und -Brenner.
S-ATA II (Serial ATA II)
S-ATA II bietet eine maximale Geschwindigkeit von 300 MByte/s und ist damit
theoretisch doppelt so schnell wie S-ATA.
Das "theoretisch" bezieht sich darauf, daß die Geschwindigkeit zur Zeit nicht von
der Datenübertragung begrenzt ist, sondern durch die Festplattenmechanik.
Festplatten mit 10.000 Umdrehungen in der Minute (U/min) liefern rund 75 MByte/s
an Daten. Die Schnittstellengeschwindigkeit reicht also auch für die Zukunft locker
aus.
S-ATA II bietet einige zusätzliche Features im Vergleich zu S-ATA:

Native Command Queuing (NCQ)
ist eine Technologie für Festplatten, die die Geschwindigkeit verbessert.
Sie ermöglicht, dass mehrere Anfragen gleichzeitig an die Festplatte
abgesetzt werden und diese dann selbst entscheidet, welche Anfrage sie zu
erst abarbeitet. Durch die Vermeidung unnötiger Kopfbewegungen kann so
der Durchsatz und vor allem die Latenz verbessert werden. Das Laufwerk
selbst, der Controller und der Treiber müssen Command Queuing
unterstützen, um es zu nutzen.
(Weitere Informationen zu NCQ auf TomsHardware.de)

eSATA (external SATA)
für externe Laufwerke, maximale Kabellänge 2m.
eSATA verwendet andere, stärker abgeschirmte Kabel, um externe Geräte
vor elektromagentischen Störungen zu schützen. So soll die sichere
Übertragung über 2m ermöglicht werden.

Port Multiplier
Über diesen Mechanismus kann der Punkt-zu-Punkt-Datenstrom auf
mehrere Geräte aufgeteilt werden.
Bis zu 15 Geräte lassen sich an einem SATA-II-Port betreiben.
Bei 4 Geräten mit ca. 70 MByte/s würden 3 G/Bit/s ausreichen, um alle
Geräte ohne Geschwindigkeitsverlust bedienen zu können.
3.3.12
USB (Universal Serial Bus)
Der Universal Serial Bus (USB) ist ein Bussystem zur Verbindung eines
Computers mit Zusatzgeräten. Ein USB-Anschluss belegt wenig Platz und kann
einfache Geräte wie Mäuse, Telefone oder Tastaturen mit Strom versorgen.
41
Mit USB ausgestattete Geräte können im laufenden Betrieb miteinander verbunden
werden (Hot-Plugging), angeschlossene Geräte und deren Eigenschaften können
automatisch erkannt werden.
Moderne Computer haben meist zwei bis sechs USB-Schnittstellen. Stehen zu wenig
USB-Anschlüsse zur Verfügung, kann man über preiswerte Hubs bis zu 127 USBGeräte an einer Schnittstelle betreiben.
USB eignet sich für viele Geräte wie Drucker, Scanner, Webcams, Maus, Tastatur,
aber auch Dongles, sowie USB-Kaffeewärmer und USB-Weihnachtsbäume;o).
Einige Geräte sind überhaupt erst mit USB entstanden, wie z.B. USBSpeichersticks.
Seit der Einführung der USB-2.0-Spezifikation sind relativ hohe
Datenübertragungsraten möglich, wodurch sich der USB zum Anschluss weiterer
Gerätearten wie Festplatten, TV-Schnittstellen und Foto-Kameras eignet.
USB wurde entwickelt um eine einheitliche Schnittstelle für PC-Peripheriegeräte
anzubieten.
Er ersetzt zunehmend ältere serielle und parallele Anschlüsse, aber auch PCIBussysteme (z.B. bei externen Soundkarten).
Technischer Überblick
USB ist ein bitserieller Bus, die einzelnen Bits des Datenpaketes werden also
nacheinander übertragen. Die Datenübertragung erfolgt differentiell über zwei
verdrillte Leitungen, eine überträgt das Datensignal unverändert und die andere das
invertierte Signal. Der Signalempfänger bildet die Differenzspannung beider
Signale; der Spannungshub zwischen 1- und 0-Pegeln ist dadurch doppelt so groß.
Dies erhöht die Übertragungssicherheit, unterdrückt Gleichtaktstörungen und
verbessert nebenbei die elektromagnetische Verträglichkeit.
Zwei weitere Leitungen dienen, falls nötig, zur Stromversorgung der
angeschlossenen Geräte. Durch Verwendung von vier Adern in einem Kabel können
diese dünner und preiswerter als bei parallelen Schnittstellen ausgeführt werden.
Eine hohe Datenübertragungsrate ist mit relativ geringem Aufwand zu erreichen, da
nicht mehrere Signale mit identischem elektrischen und zeitlichen Verhalten
übertragen werden müssen.
Ähnlich wie bei S-ATA dürfen die Kabel eine bestimmte Länge nicht überschreiten.
USB-Kabel dürfen eine maximale Länge von 5m haben, viele Geräte funktionieren
mit längeren Kabeln nicht richtig.
Die Bus-Spezifikation sieht einen zentralen Host-Controller (dem sog. MASTER)
vor, der die Koordination der angeschlossenen Peripherie-Geräte (den sog. SlaveClients) übernimmt.
Daran können bis zu 127 verschiedene Geräte angeschlossen werden.
42
An einen USB-Port kann immer nur ein einzelnes USB-Gerät angeschlossen werden.
Wenn an einen Host mehrere Geräte angeschlossen werden sollen, muss deshalb
ein Verteiler (Hub) für die Kopplung dieser Geräte sorgen. Durch den Einsatz von
Hubs entstehen Baumstrukturen, die alle im Hostcontroller enden.
Der USB ersetzt die älteren PC-Schnittstellen RS232 (seriell), Gameport, die
Centronics-Schnittstelle (paralleler Drucker-Anschluss) sowie die PS/2Schnittstelle für Tastatur und Maus. Im Vergleich zu diesen bietet USB deutlich
höhere Datenübertragungsraten.
Trotz seines Namens – Universal Serial Bus – ist der USB kein physischer
Datenbus. Bei einem solchen werden mehrere Geräte parallel an eine Leitung
angeschlossen. Die Bezeichnung „Bus“ bezieht sich auf die logische Vernetzung,
die tatsächliche elektrische Ausführung erfolgt nur mit Punkt-zu-Punkt-Verbindungen.
Übertragungsgeschwindigkeiten
USB 1.x bietet folgende Übertragungsgeschwindigkeiten:
o
o
1,5 Mbit/s (Low Speed),
12 Mbit/s (Full Speed)
Ab USB 2.0 erlaubt USB mit bis zu
o
480 Mbit/s (High Speed)
Daten zu übertragen. Diese Übertragungsraten basieren auf dem Systemtakt der
jeweiligen USB-Geschwindigkeit und stellen die physikalisch mögliche
Datenübertragungsrate dar. Die tatsächlich nutzbare Datenrate liegt – z. B. durch
Protokolloverhead – darunter; bei aktuellen Systemen in der Größenordnung 320
Mbit/s.
Wird die Schnittstelle eines Geräts mit „USB 2.0“ angegeben, heißt das nicht
unbedingt, dass dieses Gerät auch die High-Speed-Datenrate von 480 MBit/s
anbietet.
Standpunkt der Anbieter ist dabei, dass ein USB-2.0-kompatibles Gerät grundsätzlich
jede der drei Geschwindigkeiten benutzen kann und die 2.0-Kompatibilität in erster
Linie bedeutet, dass die neueste Fassung der Spezifikation eingehalten wird.
480 MBit/s dürfen also nur erwartet werden, wenn ein Gerät mit dem Logo „Certified
USB Hi-Speed“ ausgezeichnet ist.
Seit 2008 erfolgt Entwicklung von USB 3.0 mit 4,8 Gbit/s.
Seit 2010 zunehmend Geräte verfügbar, die diesen Standard unterstützen.
43
3.4 Speicher-Hierarchie
3.5 Funktionell-struktureller Aufbau der CPU
Aufbau des Zentralprozessors als vereinfachte funktionale Darstellung
44
3.5.1.1 Leitwerk (Steuerwerk)
Das Leitwerk nimmt Koordinationsfunktionen für den Prozessor wahr und stellt den
Kern der CPU dar. Es steuert den Ablauf des Befehls- und Datenflusses und
bestimmt mit seinem Taktgeber die Verarbeitungsgeschwindigkeit. Es besteht aus
logischen Schaltungen und Registern. Das Leitwerk ist für die Übertragung von
Anweisungen aus dem Arbeitsspeicher zuständig, decodiert diese und führt sie aus.
Von den zahlreichen Registern eines Prozessors gehören folgende zum Leitwerk:
Befehlszähler, Befehlsregister und Statusregister.
Der Befehlszähler (Synonym: Programmzähler (programm counter)) enthält jeweils
die Adresse des nächsten zur Ausführung anstehenden Befehles. Zu Beginn der
Programmabarbeitung wird der Befehlszähler mit der Anfangsadresse (= Adresse
des ersten Befehls) geladen. Das Leitwerk holt von der Adresse, die im Befehlszähler
enthalten ist, diesen Befehl zur Verarbeitung ab. Nach vollzogener
Befehlsinterpretation wird der Befehlszähler um die Länge des gerade
übernommenen Befehls erhöht, d.h. um die entsprechende Zahl von
Arbeitsspeicheradressen weitergezählt. Dadurch ergibt sich normalerweise die
Adresse des Folgebefehls, der damit aus dem Arbeitsspeicher (bzw. aus dem
Cache) geholt und verarbeitet werden kann. Ist der Normalfall, bei dem die Befehle
des Programms in aufeinander folgenden Stellen des Arbeitsspeichers stehen, nicht
gegeben, so muss eine Modifikation des Befehlszählers erfolgen. Dies ist z.B. bei
Schleifen der Fall, bei denen zu bereits vorher verarbeiteten Befehlen zurück
gesprungen wird. Bei derartigen Sprungoperationen wird der Befehlszähler nicht auf
den im Arbeitsspeicher nächstfolgenden Befehl eingestellt, sondern er wird mit der
Zieladresse des Sprungbefehls geladen.
Der Befehl, der aus der durch den Befehlszähler adressierten Speicherstelle gelesen
wurde, wird im Befehlsregister (instruction register) gespeichert. Das Befehlsregister
enthält also genau den Befehl, der im Moment ausgeführt wird.
Der Befehlsdekodierer entschlüsselt die im Operationsteil angegebene
Bitkombination und setzt diese in Steuersignale um. Die erzeugten Signale und die
errechneten Operandenadressen werden je nach Befehlstyp an die für die
Ausführung des Befehls zuständigen Teile des Rechenwerks, des Leitwerks, den
Arbeitsspeicher, den I/O-Prozessor usw. weitergeleitet.
Der Status, in dem sich ein Programm befindet, wird in einem Statusregister
angegeben. Beim Mehrprogrammbetrieb bedient der Prozessor abwechselnd in
Zeitabschnitten verzahnt mehrere Programme, sodass bei der Unterbrechung eines
laufenden Programms vermerkt werden muss, wo nach der Wiederaufnahme des
Programms fortgesetzt werden soll. Zu diesem Zweck wird der erreichte
Befehlszählerstand in einem Statusregister sichergestellt.
Es gibt noch weitere Unterbrechungsgründe, wie z.B. Hardware- oder Softwarefehler.
In Abhängigkeit vom jeweiligen Unterbrechungsereignis ergreift das Betriebssystem
die erforderlichen Maßnahmen.
Das Leitwerk enthält noch eine Reihe weiterer Spezialregister, auf die hier aber nicht
eingegangen werden soll.
Das Leitwerk liest und interpretiert Befehl für Befehl. Die abgegebenen Steuersignale
dienen zur Steuerung der verschiedenen Register und Addierwerke (im
45
Rechenwerk), der peripheren Geräte usw. Abgesehen von einigen wenigen
Befehlen, die z.B. die Dateneingabe von Eingabeeinheiten in den Arbeitsspeicher
oder die Ausgabe der verarbeiteten Daten aus dem Arbeitsspeicher zu
Ausgabeeinheiten veranlassen, findet die Befehlsausführung im Wesentlichen im
Zentralprozessor (Rechenwerk) statt. Während der Ausführungsphase werden zum
Beispiel Operanden aus dem Arbeitsspeicher geholt, Daten miteinander verknüpft,
Ergebnisse in den Arbeitsspeicher geschrieben usw.
3.5.1.2 Rechenwerk (ALU – Arithmetic Logic Unit)
Während das Leitwerk sich um die koordinierte Abarbeitung der Befehle kümmert,
führt das Rechenwerk sie letztendlich aus.
Das Rechenwerk erhält vom Leitwerk die auszuführenden Rechenoperationen und
die bereitgestellten Daten, führt die Rechenoperation aus und liefert die Ergebnisse
wieder an das Leitwerk zurück. Die Rechenoperationen werden in arithmetische
(bspw. Addition, Subtraktion, Multiplikation, Bitverschiebeoperationen,
Vergleichsoperationen) und logische Operationen (bspw. UND, ODER, NICHT)
unterteilt.
Die Operanden der Rechenoperationen sind meist Worte (2 Byte, 16 Bit), die als
duale Werte interpretiert werden. Zu den Grundelementen der Rechenoperationen
gehören die Additionen, die durch Addierschaltungen realisiert werden.
Multiplikationen können bspw. darauf aufbauend durch fortgesetzte Addition realisiert
werden. Bei vielen Prozessoren können weitere Operationen durch
Mikroprogrammierung hinzugefügt werden.
Während die Rechenwerke für ganzzahlige Werte (integer) traditionell Bestandteil
des Prozessorchips waren, wurden lange Zeit die Rechenwerke für
Gleitkommaoperationen als eigene Gleitkommaeinheiten (floating point unit) in
separaten Chips integriert. Die heutigen Mikroprozessoren haben integrierte
Gleitkommaeinheiten, wobei diese auch in unterschiedlicher Form für Skalar- oder
Vektoroperationen bereitgestellt werden.
Die Verarbeitungsbreite des Rechenwerks gibt die Größe der Operanden in Bit an.
Eine höhere Verarbeitungsbreite bedeutet dabei, dass in einem Rechenschritt
größere Datenmengen verarbeitet werden können.
Heutige Prozessoren verfügen über eine Verarbeitungsbreite von 32/64 Bit für
Operationen mit ganzen Zahlen (integer arithmetic), während die Gleitkommaeinheit
mit 128 Bit oder mehr (abhängig vom Prozessortyp) arbeitet.
Nachdem die prinzipiellen Bestandteile der Zentraleinheit bekannt sind und deren
Funktionsweise erläutert wurde, sollen im Folgenden die einzelnen Stufen der
Befehlsabarbeitung am Beispiel des Von-Neumann-Zyklus zusammengefasst
werden.
3.5.1.3 Von-Neumann-Zyklus
46
Noch einmal zur Erinnerung: Die zwei Aufgaben der CPU sind die Befehlsausführung
und die Ablaufsteuerung. Gemäß diesen Aufgaben besteht sie aus einem
Rechenwerk/Datenprozessor und einem Leitwerk/Befehlsprozessor.
Die Aufgabe des Datenprozessors besteht in der "klassischen" Verarbeitung von
Daten, d.h. dem Ausführen von Berechnungen. Dazu enthält er die ArithmetischLogische-Einheit (ALU) sowie (mindestens) drei Register zur Aufnahme von
Operanden. Bei den Registern handelt es sich um




den Akkumulator,
ein Multiplikator-Register (MR) (z.B. zur Aufnahme von
Multiplikationsergebnissen) und
ein Link-Register (LR) (zur Aufnahme z.B. eines Additionsübertrages), welche
beide als Akkumulator-Erweiterung angesehen werden können, sowie
das Memory Buffer Register (MBR), über welches die Kommunikation mit dem
Speicher abgewickelt wird.
Während der Akkumulator ein allgemeines Register ist, welches im Prinzip für jede
im Rahmen eines Programms anfallende Aufgabe verwendet werden kann, sind alle
anderen Register spezielle Register, welche alle eine spezielle Funktion besitzen und
ausschließlich für diese verwendet werden können.
Die Aufgabe des Befehlsprozessors besteht darin, Befehle zu entschlüsseln und
deren Ausführung zu steuern. Dazu kann er sich folgender Register bedienen:
1) Der aktuell bearbeitete Befehl befindet sich im Befehlsregister (Instruction
Register (IR)).
2) Die Adresse des Speicherplatzes, welcher als nächstes anzusprechen ist, ist
im Memory Adress Register (MAR) abgelegt.
3) Die Adresse des nächsten auszuführenden Befehls wird im Befehlszähler
(Program Counter (PC)) gespeichert.
Die Entschlüsselung eines Befehls erfolgt durch einen separaten Befehls-Decodierer,
die Steuerung der Ausführung schließlich durch die Operationensteuerung. Oftmals
wird diese auch separat als Steuerwerk bezeichnet.
Die nachfolgende Abbildung 12 zeigt die genannten Funktionseinheiten und
verdeutlicht, welche Einheiten miteinander kommunizieren.
Die Bearbeitung eines speziellen Problems erfolgt gemäß einem Programm, also
anhand einer Folge von Instruktionen.
Vor Beginn der Bearbeitung steht dieses zusammen mit den Daten, die es benötigt,
im Speicher. Daraus leiten sich die wichtigsten Merkmale des Von-NeumannRechners ab:
47
Zentralprozessor mit Daten- und Befehlsprozessor als vereinfachte funktionale Darstellung
1) Zu jedem Zeitpunkt führt die CPU genau einen Befehl aus.
Dieser Befehl kann (höchstens) einen Datenwert bearbeiten (SISD = Single
Instruction, Singe Data)
2) Alle Speicherworte (d.h. Inhalte der Speicherzellen) sind als Daten, Befehle
oder Adressen verwendbar. Die jeweilige Verwendung eines Speicherinhaltes
richtet sich nach dem momentanen Kontext.
3) Da also Daten und Programme nicht in getrennten Speichern untergebracht
werden, besteht grundsätzlich keine Möglichkeit, die Daten vor
ungerechtfertigtem Zugriff zu schützen.
Von Neumann Zyklus - Ausführung von Instruktionen
Der Prozeß der Befehlsverarbeitung bei Von-Neumann-Rechnern wird VonNeumann-Zyklus genannt und besteht aus den folgenden fünf nacheinander
ablaufenden Teilschritten:
1) FETCH
2) DECODE
3) FETCH OPERANDS
4) EXECUTE
5) UPDATE PROGRAM COUNTER (PC)
zu 1)
Beim FETCH-Schritt wird aus dem Speicher der nächste zu bearbeitende Befehl
geholt. Dazu wird der Inhalt von PC nach MAR gebracht und der Inhalt dieser
48
Adresse über das MBR aus dem Speicher geholt und im IR gespeichert.
(Da in der Fetch-Phase nur Befehle verarbeitet werden, geht der Rechner
automatisch davon aus, dass es sich bei der geholten Bitfolge um einen Befehl
handelt.)
zu 2)
Beim DECODE-Schritt wird der geholte Befehl durch den Decodierer in
Schaltinstruktionen für die Hardware aufgelöst.
zu 3)
Bei FETCH-OPERANDS werden nun die Operanden, also die Werte, die durch den
Befehl verändert werden sollen bzw. die als Parameter verwendet werden, also etwa
die beiden Operanden einer Addition, aus dem Speicher geholt
zu 4)
Bei EXECUTE wird die Operation von der ALU ausgeführt
zu 5)
Bei UPDATE PROGRAM COUNTER wird der Befehlszähler erhöht, damit der
Rechner weiß, an welcher Stelle des Programms er sich gerade befindet. Dies
geschieht parallel zum DECODE und FETCH OPERANDS.
Bei EXECUTE kann der PC wieder verändert werden (z.B. durch einen
Sprungbefehl).
Anschließend kann der Zyklus von vorn beginnen und der nächste Schritt des
Programms kann ausgeführt werden.
Bedingte und unbedingte Sprünge
Eine Folge von Befehlen stellt ein Programm für einen Rechner dar. Ausgeführt
werden die Befehle eines Programms im Allgemeinen in der Reihenfolge, in der sie
(hintereinander) im Speicher abgelegt sind (und welche durch den Programmierer
bestimmt wird).
Dazu wird im UPDATE-PROGRAM-COUNTER-Schritt der Inhalt des PC, der die
Adresse des nächsten auszuführenden Befehls angibt, lediglich um eins (bzw. um
die Anzahl der Bytes, die der aktuelle Befehl benötigt) erhöht.
Eine Ausnahme bilden (bedingte oder unbedingte) Sprungbefehle (z.B. bei
Schleifenenden oder Unterprogramm-Sprüngen).
In diesen Fällen ist der Inhalt des PC neu zu laden.
49
Die Beschreibung der Fetch-Phase lässt sich wie folgt zusammenfassen:
PC --> MAR
MAR --> MBR
MBR --> IR
decodiere IR
falls (kein Sprungbefehl) dann
{stelle Operanden bereit;
PC = PC + 1;
}
sonst {
PC = Sprungziel-Adresse;
}
Inhalt des PC ins MAR
Inhalt des MAR ins MBR
Inhalt des MBR ins IR
Befehls-Decodierung
Updaten des PC
Setzen des PC auf Sprungziel
3.6 Einteilung von Rechnerarchitekturen nach Flynn
Flynn teilt Rechnerarchitekturen nach der Art der Befehlsausführung in die vier
folgenden Kategorien ein:
SISD
Ein nach dem SISD-Prinzip (Single Instruction Single Data) aufgebauter Rechner
kann Befehle ausschließlich sequentiell, also nacheinander, abarbeiten. In einem
Bearbeitungsschritt wird auf das entsprechende Datenelement bzw. den Operanden
genau ein Befehl angewendet.
Die Von-Neumann-Architektur folgt diesem Prinzip und stellt damit einen SISDRechner dar.
SIMD
Ein nach dem SIMD-Prinzip (Single Instruction Multiple Data) aufgebauter Rechner
kann einen Befehl mehr oder weniger simultan auf verschiedene Datenelemente
bzw. Operanden anwenden. Entsprechende Prozessoren werden auch als
Vektorprozessoren (vector processor, array processor) oder Vektorrechner
bezeichnet.
Mit einem SIMD-Rechner können Berechnungen mit Vektoren und Matrizen sehr
effizient realisiert werden, was besonders für die grafische Datenverarbeitung, die
Bildverarbeitung und die Verarbeitung von Multimedia-Datenströmen wichtig ist.
Der erste Vektorrechner war der Gray-Supercomputer in den 70er Jahren.
Heute findet man SIMD-Instruktionen bei vielen gängigen Rechnerarchitekturen, wie
bspw. die MMX, SSE, SSE2, SSE3 bei Intel-Prozessoren oder 3DNow! bei AMDProzessoren.
MMX ... Multimedia Extension für ganzzahlige Werte
SSE ... Streaming SIMD Extension für Gleitkommawerte
50
MIMD
Ein nach dem MIMD-Prinzip (Multiple Instruction Multiple Data) aufgebauter Rechner
kann gleichzeitig mehrere Befehle auf mehreren Datenelementen bzw. Operanden
ausführen. Entsprechende Architekturen finden bei Mehrprozessorsystemen
Anwendung, bei denen die einzelnen Prozessoren unabhängig voneinander
Berechnungen ausführen können.
Im weiteren Sinne sind praktisch alle PCs, die Hilfs- bzw. Spezialprozessoren
verwenden, Rechnersysteme, die nach dem MIMD-Prinzip aufgebaut sind.
Eine aktuelle Entwicklung stellen Prozessoren mit mehrfachen Prozessorkernen (z.B.
dual core) dar. Hier werden innerhalb des eigentlichen Prozessorchips mehrere
Prozessoren emuliert, sodass das damit für das Betriebssystem ein
Mehrprozessorsystem vorliegt.
Die Klasse der MIMD-Architekturen lässt sich noch weiter hinsichtlich der Kopplung
der Zentralprozessoren unterteilen.
Bei einem eng gekoppelten Mehrprozessorsystem greifen meist wenige
Zentralprozessoren auf einen gemeinsamen Arbeitsspeicher zu (shared memory).
Die Prozessoren befinden sich physisch innerhalb desselben Rechners und
benutzen einen gemeinsamen Kommunikationskanal (z.B. Bus), um auf den
Arbeitsspeicher zuzugreifen.
Ein lose gekoppeltes Mehrprozessorsystem besteht aus Prozessoren, die jeweils
über einen eigenen lokalen Speicher verfügen. Man spricht auf von verteilten
Speicherstrukturen (distributed memory). Die Kommunikation erfolgt hier
typischerweise durch Nachrichten (messages), die meist über ein Rechnernetz
versendet werden. Daher rührt auch der Begriff des nachrichtengekoppelten
Mehrprozessorsystems.
Harvard-Architektur
Die Harvard-Architektur bezeichnet in der Informatik ein Schaltungskonzept zur
Realisierung besonders schneller CPUs und Signalprozessoren. Der Befehlsspeicher
ist physisch vom Datenspeicher getrennt und beide werden über getrennte Busse
angesteuert. Der Vorteil dieser Architektur besteht darin, dass Befehle und Daten
gleichzeitig geladen, bzw. geschrieben werden können. Bei einer klassischen VonNeumann-Architektur sind hierzu mindestens zwei aufeinander folgende Buszyklen
notwendig.
Zudem sorgt die physikalische Trennung von Daten und Programm dafür, dass bei
Softwarefehlern kein Programmcode überschrieben werden kann. Nachteilig ist
allerdings, dass nicht benötigter Datenspeicher nicht als Programmspeicher genutzt
werden kann.
Die Harvard-Architektur wurde zunächst überwiegend in RISC-Prozessoren
konsequent umgesetzt. Moderne Prozessoren in Harvard-Architektur sind in der
Lage, parallel mehrere Rechenwerke gleichzeitig mit Daten und Befehlen zu füllen.
Bei Signalprozessoren der C6x-Familie von Texas Instruments ist dies
beispielsweise für bis zu acht Rechenwerke möglich.
51
Ein weiterer Vorteil der Trennung ist, dass die Datenwortbreite (die kleinste
adressierbare Einheit) und Befehlswortbreite unabhängig festgelegt werden kann.
Damit kann auch, wenn erforderlich, die Effizienz des Programmspeicherbedarfs
verbessert werden, da sie nicht direkt von den Datenbusbreiten abhängig ist,
sondern ausschließlich vom Befehlssatz. Dies kann z.B. in eingebetteten Systemen
oder kleinen Microcontroller-Systemen von Interesse sein.
Als besonders bekannte Vertreter sollten hier auch die Produkte der Firma Microchip
Technology Inc. erwähnt werden, die ebenso auf dieser Architektur aufbauen
(PICmicro). Ebenso basieren die Mikrocontroller der AVR-Reihe von Atmel auf der
Harvard Architektur.
Eine bedeutende Erweiterung der Harvard-Architektur wurde von der amerikanischen
Firma Analog Devices Anfang der 1990er Jahre durch die Einführung der SHARC(Super-Harvard-Architecture)-Technologie vorgenommen, bei der die genannten
Speichersegmente als Dual-Port-RAMs ausgeführt sind, die kreuzweise zwischen
den Programm- und Daten-Bussen liegen.
Viele moderne Prozessoren verwenden eine Mischform aus Harvard- und vonNeumann-Architektur, bei der innerhalb des Prozessorchips Daten und Programm
voneinander getrennt verwaltet werden, eigene Caches und MMUs haben und über
getrennte interne Busse laufen, extern jedoch in einem gemeinsamen Speicher
liegen.
Einer der ersten bedeutenden Prozessoren, die die Harvard-Architektur intern
einsetzten, war der Motorola 68030.
52
4 Betriebssysteme
4.1 Die ISA-Ebene
Bevor wir uns die Betriebssystemebene anschauen, werfen wir einen kurzen Blick
darauf, wie durch Software auf die Maschine oder Hardware zugegriffen werden
kann.
Die Sprache der ISA-Ebene wird gebildet aus der Instruktionsmenge, die von der
Maschine, oder genauer von der CPU und den dazugehörigen HardwareSchaltungen ausgeführt werden kann.
Diese Instruktionsmenge nennt man auch Mikroprogramm.
Das Mikroprogramm besteht aus Mikrobefehlen. Das sind Bitfolgen, die wie
andere Daten auch in einem Speicher abgelegt werden können.
Ein solcher Mikrobefehlsspeicher ist Teil der CPU.
Er ist als ROM (Read Only Memory) ausgeführt, d.h. er kann nur gelesen, aber nicht
verändert werden. Ansonsten ist das ROM wie jeder andere Speicher aufgebaut,
insbesondere besitzt es ein Adressregister, in dem die Adresse eines
Speicherwertes abgelegt wird, und ein Datenregister, in dem der dort befindliche
Datenwert zurückgegeben wird.
Mikrobefehle sind jedoch für den Menschen nicht besonders gut lesbar. Die
Vorstellung, größere Programme in Mikrocode programmieren zu müssen, ist sehr
abschreckend:
Als Programmierer sollte man sich nicht damit plagen müssen, Schalter in
Datenwegen zu betätigen, Daten mühsam via Adress- und Datenregister aus dem
Speicher zu lesen, Code-Adressen in Code-Adress-Register zu schreiben öder
ähnliche lästige Dinge festzulegen.
Die Details der Benutzung der Busse sollen dem Programmierer ebenfalls verborgen
(erspart) bleiben.
Daher benötigen Programmierer eine Maschinensprache, die besser lesbar ist (so
genannte mnemonische Befehle enthält) und eine abstraktere Sicht der CPU
bietet. Eine solche Sprache ist die Assemblersprache.
4.2 Assemblerprogrammierung
Eine Assemblersprache ist eine Sammlung von Befehlen, die dem Programmierer
für den direkten Zugriff auf die CPU zur Verfügung steht.
Eine reine Assemblersprache produziert von jeder Anweisung genau eine
Maschineninstruktion. Anders ausgedrückt: Es gibt eine Eins-zu-EinsEntsprechung zwischen den Maschineninstruktionen und den Anweisungen im
Assemblerprogramm.
53
Assemblersprache ist also eine symbolischere Form der Maschinensprache, d.h.
sie verwendet symbolische Namen und Adressen anstelle von binären.
Somit lässt sich in Assembler wesentlich leichter programmieren als in
Maschinensprache.
Ein weiterer Vorteil in der Verwendung von Assembler liegt darin, dass man in
Assembler eine sehr genaue Kontrolle über die Ausführungszeiten der
verschiedenen Befehle hat. Man kann zeitkritische Programmteile sehr effizient
in Aktionen der CPU umsetzen.
Trotzdem ist Assemblerprogrammierung sehr schwierig und nichts für Leute mit
schwachen Nerven. Außerdem dauert das Schreiben eines Programms in
Assemblersprache viel länger als in einer Hochsprache, nicht zuletzt, weil auch
das Debugging viel länger dauert.
Außerdem ist die Wartung und Pflege eines Assemblerprogramms sehr viel
aufwendiger.
Zusammengefasst gibt es also zwei gute Gründe für die Verwendung von
Assembler:
1. Direkter Hardwarezugriff auf alle Funktionen der CPU und der
angeschlossenen Geräte.
2. Assemblerprogramme sind meist kompakter und schneller, als Programme
einer Hochsprache.
Als Faustregel kann man sagen, dass 10% des Programm 90% der
Ausführungszeit in Anspruch nehmen!
Eine empfehlenswerte Vorgehensweise ist demnach, zunächst ein Programm
in einer höheren Programmiersprache zu entwickeln, anschließend die
zeitkritischen Stellen, oder die Stellen, die direkten Hardwarezugriff
erfordern, zu identifizieren und diese dann gezielt in Assemblersprache
umzuschreiben.
Jede Computerarchitektur hat ihre eigenen Instruktionen, so dass sich die
Assemblersprachen unterscheiden.
Das heißt, ein Assemblerprogramm ist nur auf dem System, für das es
geschrieben wurde, lauffähig.
Assemblersprache – Maschinensprache und Assembler
Jeder Maschinenbefehl besteht zunächst aus einer Bitfolge.
In reiner Maschinensprache hat jeder Befehl eine Nummer, OpCode genannt.
Die Bedeutung der einzelnen OpCodes müsste man im Grunde jedes Mal in einer
Tabelle nachschlagen.
Deshalb gibt es eine deutlich lesbarere Form von Maschinenbefehlen und ihren
Opcodes: Die Assemblersprache oder kurz Assembler (Assembler bezeichnet
außerdem ein Programm, das Assemblerbefehle in Maschinensprache umwandelt).
54
Es gibt für jeden Maschinenbefehl eine Assembleranweisung. Diese Anweisung hat
den Vorteil, dass man sich die Abkürzungen um einiges leichter merken kann, als
den OpCode dieses Befehls.
Die folgende Abbildung zeigt einige Assemblerbefehle sowie deren entsprechende
Maschinensprachebefehle:
Glücklicherweise besitzt Assembler noch mehr Fähigkeiten, als "nur" für einen
Assemblerbefehl den entsprechenden Maschinenbefehl aus einer Tabelle zu
holen.
Der Assembler erlaubt, symbolische Namen für Speicherplätze (Variablen),
symbolische Sprungadressen (Labels) und Daten (Konstanten) zu verwenden.
Außerdem steht ein einfaches Prozedurkonzept zur Verfügung, das es erlaubt,
häufig verwendete Programmteile über einen zugewiesenen Namen zu
referenzieren.
Darüber hinaus sorgen so genannte Makros für übersichtlicheren Code, da sie es
erlauben, komplexe Programmteile durch einen kurzen Namen zu ersetzen und
diese an beliebigen Stellen im Programm aufzurufen.
 Der Unterschied zu Prozeduren besteht darin, dass der Code für eine
Prozedur nur einmal im Programm existiert, ein Aufruf verzweigt an diese Stelle.
Bei Makros wird der kurze Name des Makros jedes Mal durch den wirklichen
Programmtext ersetzt (-->Expansion), d.h. der Code existiert hier mehrmals.
55
Die obige Abbildung zeigt ein kleines Assembler-Programm, das die Zahlen von 1...N
aufaddiert.
Zunächst wird hier der Inhalt der Speicheradresse 0 in Register A geladen und B
mit dem Wert 0 initialisiert.
Ab dem mit der Marke nochmal gekennzeichneten Befehl wird A zu B addiert und A
um eins heruntergezählt.
Ist A ≠ 0 wird zur Marke nochmal gesprungen und der Code erneut ausgeführt.
4.3 Von der Hardware zum Betriebssystem
Bisher haben wir die Hardware und die Möglichkeiten der Datenrepräsentation
behandelt.
Ohne Programme ist beides aber nutzlos. Die Programme, die einen sinnvollen
Betrieb eines Rechners erst möglich machen, nennt man Software.
Man kann verschiedene Schichten von Software identifizieren. Sie unterscheiden
sich durch ihren Abstand zum menschlichen Benutzer bzw. zur Hardware des
Computers.
Zur Verdeutlichung stellen wir uns zunächst einen "blanken" Computer, d.h. eine
CPU auf einem Motherboard mit Speicher und Verbindung zu Peripheriegeräten
wie Drucker und Laufwerken, aber ohne jegliche Software vor.
Die CPU kann in dieser Situation nicht viel mehr als:




Speicherinhalte in Register laden
Registerinhalte im Speicher ablegen
Registerinhalte logisch oder arithmetisch verknüpfen
mit IN- und OUT-Befehlen Register in Peripheriegeräten lesen und schreiben.
In Zusammenarbeit mit den Peripheriegeräten (Tastatur, Bildschirm, Laufwerke,
Soundkarte) kann man auf diese Weise bereits:




ein Zeichen von der Tastatur einlesen
ein Zeichen an einer beliebigen Position des Textbildschirms ausgeben
einen Sektor einer bestimmten Spur der Diskette oder Festplatte lesen oder
schreiben
einen Ton einer bestimmten Frequenz und Dauer erzeugen.
Alle diese Tätigkeiten bewegen sich auf einer sehr niedrigen Ebene, z.B. auf der
ISA-Ebene oder der darunter liegenden Mikroarchitekturebene.
Wollte man einen Rechner auf dieser Basis bedienen, müsste man sich genauestens
mit den technischen Details jedes einzelnen der Peripheriegeräte auskennen.
Niemand würde einen Rechner benutzen, wenn er sich bei jedem Tastendruck
überlegen müsste, wie ein auf der Tastatur eingegebenes Zeichen in die CPU
gelangt und anschließend auf dem Bildschirm als Text dargestellt wird.
Gar nicht auszumalen, wenn dann auch noch alle Zeichen des bereits auf dem
56
Bildschirm dargestellten Textes verschoben werden müssten, um dem eingefügten
Zeichen Platz zu machen!
Zwischen dem von einem Anwender intuitiv zu bedienenden Rechner und den
Fähigkeiten der Hardware klafft also eine gewaltige Lücke.
Diese Lücke füllt das Betriebssystem aus.
4.4 Aufgaben des Betriebssystems
Ein Betriebsystem ist ein Programm, das dem Benutzer und den
Anwendungsprogrammen elementare Dienste bereitstellt.
Der Nutzer eines Betriebssystems ist nicht notwendigerweise ein Programmierer,
sondern möglicherweise jemand, der vom funktionieren des Rechners keine Ahnung
hat.
Für einen solchen Benutzer präsentiert sich der Rechner über das Betriebssystem.
Die Dienste, die es bereitstellt, sind das, was der Rechner in den Augen eines
solchen Nutzers kann.
Seitdem Rechner in viele Bereiche unseres Lebens Einzug gehalten haben, gibt es
immer mehr Menschen, die mit einem Computer arbeiten müssen.
Daher muss das Betriebssystem immer einfacher zu benutzen sein.
Erst den graphischen Betriebssystemsoberflächen (GUI = Graphical User
Interface) ist es zu verdanken, dass heute jeder einen Rechner irgendwie
bedienen kann und dass es verhältnismäßig leicht ist, mit einem bisher
unbekannten Programm zu arbeiten, ohne vorher umfangreiche Handbücher wälzen
zu müssen.
Der Rechner mit seinen Peripheriegeräten stellt eine Fülle von Ressourcen zur
Verfügung, auf die Benutzerprogramme zugreifen.
Zu diesen Ressourcen gehören:





CPU (Rechenzeit)
Hauptspeicher
Plattenspeicherplatz
interne Geräte (Erweiterungskarten, Onboard-Sound u.ä.)
externe Geräte (Drucker, Scanner, etc.)
Die Verwaltung dieser Ressourcen ist eine schwierige Aufgabe, da viele Benutzer
und deren Programme auf diese Ressourcen gleichzeitig zugreifen wollen. Die
zentralen Bestandteile eines Betriebssystems sind entsprechend seiner zentralen
Aufgaben also die



Dateiverwaltung
Prozessverwaltung
Speicherverwaltung
57
4.4.1 Dateiverwaltung
Eine wichtige Aufgabe des Betriebssystems ist die Dateiverwaltung. Damit ein
Benutzer sich nicht darum kümmern muss, in welchem Bereich der Festplatte noch
Platz ist, um den gerade geschriebenen Text zu speichern, oder wo die Version von
gestern gespeichert war, stellt das Betriebssystem das Konzept der Datei als
Behälter für Daten aller Art zur Verfügung.
Die Übersetzung von Dateien und ihren Namen in bestimmte Bereiche der Festplatte
nimmt das Dateisystem als Bestandteil des Betriebssystems vor.
Moderne Dateisysteme sind hierarchisch aufgebaut. Mehrere Dateien können in
einem Ordner (folder) zusammengefasst werden.
Andere Bezeichnungen für Ordner sind Katalog, Verzeichnis, Unterverzeichnis
(directory, subdirectory).
Da Ordner sowohl Dateien als auch andere Ordner enthalten können, entsteht eine
hierarchische (baumähnlich verzweigte) Struktur.
In Wirklichkeit ist ein Ordner eine Datei, die Namen und einige Zusatzinformationen
von anderen Dateien enthält.
Dateien können sich auf allen Stufen befinden. Von oben gesehen beginnt die
Hierarchie mit einem Wurzelordner (root directory), dieser enthält wieder Dateien
und Ordner, und so fort.
Der linke Teil der folgenden Abbildung zeigt einen Dateibaum, wie er unter Windows
XP dargestellt wird.
Das Wurzelverzeichnis ist F. Dieser enthält unter anderem die Ordner COMEDY,
HÖRSPIELE, MUSIK.
Der Ordner MUSIK enthält den Unterordner MP3 und dieser wiederum diverse
weitere Unterordner.
In der rechten Hälfte des Bildes sieht man die Dateien, die sich im Unterordner
"Frank Sinatra - My Way" befinden.
58
4.4.1.1 Dateinamen und Pfade
Jede Datei erhält einen Namen, unter der sie gespeichert und wiedergefunden
werden kann. Der Dateiname ist im Prinzip beliebig, er kann sich aus Buchstaben,
Ziffern und einigen erlaubten Sonderzeichen zusammensetzen.
Allerdings hat sich als Konvention etabliert, Dateinamen aus zwei Teilen, nämlich
dem eigentlichen Namen und der Erweiterung zu bilden. Beide Bestandteile
werden durch einen Punkt "." voneinander getrennt.
Die letzte Abbildung zeigte die Datei "01-My Way.mp3". Anhand des Namens macht
man den Inhalt der Datei kenntlich, anhand der Erweiterung die Art bzw. das Format
des Inhalts. Von letzterem ist nämlich abhängig, mit welchem Programm die Datei
geöffnet werden kann.
In diesem Falle zeigt die Erweiterung".mp3", dass es sich um eine Datei handelt, die
z.B. mit dem Programm "Winamp" geöffnet werden kann.
Obwohl theoretisch auch Dateinamen ohne Dateierweiterung möglich sind, ist es
sinnvoll, sich an die Konvention zu halten, da auch die Anwenderprogramme von
diesem Normalfall (default) ausgehen.
Ein Anwendungsprogramm wie z.B. Word wird beim ersten Abspeichern einer neuen
Datei als default die Endung ".doc" vorgeben.
Es kann vorkommen, dass zwei Dateien, die sich verschiedenen Ordnern befinden,
den gleichen Namen besitzen.
Dies ist kein Problem, da das Betriebssystem eine Datei auch über ihre Lage im
Dateisystem identifiziert. Diese Lage ist in einer baumartigen Struktur wie dem
Dateisystem immer eindeutig durch den Pfad bestimmt, den man ausgehend von der
Wurzel traversieren muss, um zu der gesuchten Datei zu gelangen. Den Pfad
kennzeichnet man durch die Folge der dabei traversierten Unterverzeichnisse.
Der Pfad zum Verzeichnis der letzten Abbildung ist demnach:
59
o
F:\MUSIK\MP3\Frank Sinatra - My Way - The Best of\
Man erkennt, dass die einzelnen Unterordner durch das Trennzeichen "\" (backslash)
getrennt werden.
In den Betriebssystemen der UNIX-Familie (LINUX, SunOS, Fedora) wird
stattdessen der "/" (slash) verwendet.

Der Pfad, zusammen mit dem Dateinamen (incl. der Erweiterung), muss eine
Datei eindeutig kennzeichnen.
Die MP3-Datei aus unserem Beispiel hat also den vollständig qualifizierten
Dateinamen
F:\MUSIK\MP3\Frank Sinatra - My Way - The Best of\01-My Way.mp3
4.4.1.2 Das Dateisystem
Viele Speichergeräte arbeiten blockweise, indem sie Daten als Blöcke fester Größe
(je nach Dateisystem zwischen 512 Bytes und 64 KBytes) in Bereiche eines
Datenträgers speichern.
Die Hardware bietet somit dem System eine Menge von Blöcken an, die eindeutig
adressierbar sind.
Das Betriebssystem verwaltet alle diese Blöcke in einem Dateisystem und enthält
Routinen zum Lesen und Schreiben von Blöcken für die jeweils verwendeten
Gerätetypen, die so genannten Gerätetreiber.
Das vom Betriebssystem verwaltete Dateisystem erspart dem Benutzer so das
Hantieren mit lästigen Details, wie z.B. das Ausrichten des Schreib-/Lesekopfes
einer Festplatte beim Zugriff auf eine Datei.
Für einen Benutzer ist es viel einfacher und intuitiver, seine Daten in Dateien
(files) zu organisieren.
Eine Datei entspricht intuitiv einer Akte und diese können in Ordnern
zusammengefasst werden.
Jede Datei hat einen Namen und einen Inhalt. Dieser kann aus einer beliebigen
Folge von Bytes bestehen.
Das Betriebssystem muss eine Übersetzung zwischen den von der Hardware
angebotenen Blöcken und den vom Benutzer gewünschten Dateien
gewährleisten.
Das Dateisystem, als zuständiger Teil des Betriebssystems, verwaltet eine Datei als
Folge von Blöcken.
Selbst wenn sie nur ein Byte enthält, verbraucht eine Datei mindestens den
Speicherplatz eines Blockes.
60
Die Dateien eines Dateisystems werden in speziellen Dateien organisiert, den sog.
Ordnern oder Verzeichnissen.
In einem Ordner oder auch Verzeichnis findet sich für jede in ihm enthaltene Datei
ein Eintrag mit Informationen folgender Art:






Dateiname (dazu gehört ggf. auch die Erweiterung)
Dateityp (Normaldatei, ausführbare Datei, Verzeichnisdatei)
Länge in Bytes
zugehörige Blöcke (meist reicht ein Verweis auf den ersten Block der Datei)
Zugriffsrechte (Besitzer, ggf. Passwort, Schreib- und Leserechte)
Datum (Erstellung, Änderung)
Unter Windows ist jedes Laufwerk die Wurzel eines eigenen Dateibaums, die mit
einem "Laufwerksbuchstaben" benannt wird.
Unter UNIX sind die Dateisysteme aller Laufwerke Unterbäume eines globalen
Dateibaums, dessen Wurzel root heißt.
Ein systemweites Dateisystem hat Vorteile, wenn viele Festplatten vorhanden sind
und der Benutzer gar nicht wissen will, auf welchen Geräten sich die Daten befinden.
Es hat aber Nachteile, wenn Geräte mit auswechselbaren Datenträgern (z.B.
Disketten oder CD-ROMS) betrieben werden, da bei jedem Medienwechsel
entsprechende Teile des Gesamt-Katalogs geändert werden müssen.
Außerdem muss das Betriebssystem eine Pseudodatei verwalten, die aus allen
freien (also noch verfügbaren) Blöcken des Datenträgers besteht.
Eine weitere Pseudodatei besteht aus allen Blöcken, die als unzuverlässig gelten,
weil ihre Bearbeitung zu Hardwareproblemen geführt hat. Diese werden dann nicht
mehr für Dateien genutzt. Es gibt also:



belegte Blöcke
freie Blöcke
unzuverlässige Blöcke
Während der Bearbeitung der Dateien ändern sich die Listen dynamisch.
4.4.1.3 Datei-Operationen
Das Dateisystem bietet dem Anwenderprogramm mindestens folgende Operationen
zur Verwaltung von Dateien:

"Neu":
Anlegen einer noch leeren Datei in einem bestimmten Verzeichnis; die
Parameter dieser Operation sind Dateiname und Dateityp.

"Löschen":
Die Datei wird entfernt und damit unzugänglich. Meist wird hier aber nur der
Verweis auf den ersten Block der Datei gelöscht, so dass man die Datei häufig
wiederherstellen kann.
61

"Kopieren":
Dabei kann implizit eine neue Datei erzeugt oder eine bestehende
überschrieben oder verlängert werden.

"Umbenennen":
Änderung des Dateinamens oder anderer Verzeichniseinträge einer Datei.

"Verschieben":
Die Datei wird aus einem Verzeichnis entfernt und einem anderen
hinzugefügt.
Um eine Datei bearbeiten zu können, muss man sie vorher öffnen.
Dabei wird eine Verbindung zwischen der Datei und ihrem Verzeichnis, zwischen der
Zugriffsmethode (lesen/schreiben) und einem Anwenderprogramm, welches den
Zugriff veranlasst hat, hergestellt.
Nach dem Bearbeiten muß die Datei wieder geschlossen werden.
Wenn beispielsweise ein Anwender eine Textdatei editiert, dann muss das
Betriebssystem aus dem Dateinamen die Liste der Blöcke bestimmen, in denen
der Datei-Inhalt gespeichert ist.
Meist ist dazu nur die Kenntnis des ersten Blockes notwendig. Dieser enthält dann
einen Verweis auf den nächsten Block uns so fort.
Wird die editierte Datei gespeichert, so müssen möglicherweise neue Blöcke an
die Liste angehängt werden oder einige Blöcke können entfernt und der Liste der
freien Blöcke (s. Dateisystem) übergeben werden.
Das Betriebssystem muss also Operationen des Anwendungsprogramms wie z.B.
"Datei lesen" und "Datei speichern" umsetzen in elementare Operationen, sog.
Systemaufrufe (system calls), die die Hardware ausführen kann. Dazu gehören:


Lesen eines oder mehrerer Blöcke
Schreiben eines oder mehrerer Blöcke
Die Systemaufrufe Neu oder Löschen führen lediglich dazu, dass Blöcke der Liste
der freien Blöcke entnommen, oder zurückgegeben werden. Der Inhalt der Blöcke
muß nicht gelöscht werden. Aus diesem Grund können "gelöschte" Dateien mit
spezieller Software häufig wieder hergestellt werden, da sie sich noch physisch auf
der Festplatte befinden. Was gelöscht wurde, war lediglich der Verweis auf den
ersten Block der Datei. Sobald man den Anfang wiederfindet, kann man die Datei
lesen.
Dies funktioniert aber nur, solange keine Blöcke der Datei von anderen Dateien
verwendet und damit überschrieben wurden.
Das Dateisystem verwaltet eine Datei als Folge von Blöcken. Eine Datei, die nur
ein Byte enthält, verbraucht mindestens den Speicherplatz eines Blockes.
Für ein Anwenderprogramm wie einen Texteditor besteht eine Datei aus einer
Folge von Bytes (!) . Der Texteditor muss in der Lage sein, ein bestimmtes Byte zu
schreiben, zu löschen oder an einer bestimmten Stelle ein Byte einzufügen.
Die Repräsentation einer Datei als eine solche Folge von Bytes ist Aufgabe des
62
Betriebssystems. Es stellt daher Systemaufrufe zur Verfügung, um Dateien
byteweise zu lesen und zu schreiben.
Selbst der Programmierer des Texteditors muss nichts von der blockweisen
Organisation der Dateien wissen.
Für ihn ist die Abbildung von der Bytefolge in die Blockfolge unsichtbar.
4.4.2 Prozessverwaltung
Ein auf einem Rechner ablauffähiges oder im Ablauf befindliches Programm,
zusammen mit all seinen benötigten Ressourcen wird zusammenfassend als
Prozess oder Task bezeichnet.
Auf einem Rechner mit nur einer CPU ist es nicht wirklich möglich, dass mehrere
Prozesse gleichzeitig laufen.
Wenn man allerdings mehrere Prozesse abwechselnd immer für eine kurze Zeit
(einige Millisekunden) arbeiten lässt, so entsteht der Eindruck, als würden diese
Prozesse gleichzeitig laufen.
Die zur Verfügung stehende Zeit wird in kurze Intervalle unterteilt. In jedem Intervall
steht die CPU einem anderen Prozess zur Verfügung. Dazwischen findet ein
Taskwechsel statt, wobei der bisherige Prozess suspendiert und ein anderer
Prozess
(re-) aktiviert wird.
Ein Prozess kann sich, nachdem er gestartet wurde, in verschiedenen Zuständen
befinden:





Nach dem Start ist er zunächst rechenbereit und wartet auf die Zuteilung von
Prozessorzeit.
Wird er dann vom Betriebssystem zur Ausführung ausgewählt, ist er rechnend.
Ist er nach einer bestimmten Zeit nicht beendet, wird er vom Betriebssystem
suspendiert und ein anderer Prozess erhält den Prozessor zugeteilt.
Unser Prozess ist dann erneut rechenbereit.
Es kann aber auch der Fall eintreten, dass der Prozess auf eine Ressource
wartet (z.B. auf einen Drucker oder auf eine Dateioperation). Dann wird er
blockiert und erst wieder als rechenbereit eingestuft, falls das Signal kommt,
dass die benötigten Ressourcen bereitstehen. In der Zwischenzeit können die
andern Prozesse den Prozessor nutzen.
63
Abb.: Zustandsdiagramm eines Prozesses
Hat ein Prozessor genau eine CPU, so ist höchstens ein Prozess zu jedem
Zeitpunkt aktiv. Dieser wird dann als laufender Prozess bezeichnet.
4.4.2.1 Bestandteile eines Prozesses
Ein Prozess ist eine Instanz eines in Ausführung befindlichen Programms. Jeder
Prozess bekommt einen eigenen Adressbereich zugewiesen.
Wenn er deaktiviert wird, müssen alle notwendigen Informationen gespeichert
werden, um ihn später im exakt gleichen Zustand reaktivieren zu können. Zu
diesen Informationen gehören:




der Programmcode des Prozesses (bzw. dessen Programms)
seine im Arbeitsspeicher befindlichen Daten
der Inhalt der CPU-Register einschließlich des Befehlszählers
eine Tabelle aller geöffneten Dateien mit ihrem aktuellen
Bearbeitungszustand
Abb.: Speicherabbild eines Prozesses
Wenn ein Prozess unterbrochen werden soll, muss der Inhalt der CPU-Register in
den vorgesehenen Adressbereich gerettet werden.
Wenn ein wartender Prozess aktiviert werden soll, muss der Inhalt der Register, so
wie er bei seiner letzten Unterbrechung gesichert wurde, wieder geladen werden.
64
4.4.2.2 Threads
Jeder Prozess besitzt seinen eigenen Speicherbereich. Demgegeüber sind Threads
(Thread = Faden) Prozesse, die keinen eigenen Speicherbereich besitzen. Man
nennt sie daher auch leichtgewichtige Prozesse (lightweight process).
Der Laufzeit-Overhead zur Erzeugung und Verwaltung von Threads ist deutlich
geringer als bei Prozessen. Gewöhnlich laufen innerhalb eines Prozesses mehrere
Threads ab, die den gemeinsamen Speicherbereich nutzen.
Threads sind in den letzten Jahren immer beliebter geworden. Moderne Sprachen,
wie Java, haben Threads in die Sprache integriert.
Computerspiele nutzen Threads zunehmend, um die einzelnen Kerne von MehrkernProzessoren besser auszulasten. So kann z.B. in einem Thread von einem CPUKern die Spielphysik berechnet werden, während ein anderer die KI (Künstliche
Intelligenz) der Computergegner berechnet.
4.4.2.3 Multitasking
a) Prioritätsabhängige Verwaltung / kooperatives Multitasking
Für die Verwaltung der Prozesse hat das Betriebssystem verschiedene
Möglichkeiten. Die einfachste Methode besteht darin, Prozessen eine Priorität
zuzuordnen und dem jeweils bereiten Prozess höchster Priorität die CPU zu
überlassen, so lange bis dieser Prozess die CPU nicht mehr benötigt oder ein
anderer Prozess höherer Priorität bereit ist.
Eine allein durch Prioritäten gesteuerte Verwaltung begünstigt also den Prozess
mit höchster Priorität.
Zeitpunkt und Dauer der Ausführung von Prozessen geringerer Priorität sind nicht
vorhersehbar.
Diese Form des Multitasking ist das von älteren Windows-Versionen und Mac OS
bis Version 9 bekannte kooperative Multitasking.
Dabei ist es jedem Prozess selbst überlassen, wann er die Kontrolle an das
Betriebssystem zurückgibt.
Dies hat den Nachteil, dass Programme, die nicht kooperieren, bzw. die Fehler
enthalten, das gesamte System zum Stillstand bringen können.
b) Zeitscheibenverfahren / präemptives Multitasking
Eine bessere Methode besteht darin, die Zuteilung des Betriebsmittels CPU nicht
allein von der Priorität eines Prozesses abhängig zu machen, sondern jedem
bereiten Prozess eine sogenannte Zeitscheibe zuzuteilen. Diese Methode nennt
man Zeitscheibenverfahren oder präemptives Multitasking.
Das Betriebssystem führt eine neue CPU-Zuteilung durch, wenn der laufende
Prozess entweder
o
eine bestimmte Zeit gerechnet hat, oder
65
o
wenn er auf ein Ereignis warten muss.
Ein solches Ereignis könnte die Ankunft einer Nachricht von einem anderen Prozess
sein oder die Freigabe eines Betriebsmittels wie Laufwerk, Drucker, Modem, das der
Prozess zur Weiterarbeit benötigt. Je nach System werden als Dauer einer
Zeitscheibe 1 Millisekunde, 10 Millisekunden oder mehr gewählt.
Die meisten Betriebssysteme führen allerdings nur eine sehr einfache
Zeitscheibenzuteilung für die bereiten Prozesse durch.
Dieses Verfahren wird round robin (Ringelreihen) genannt und besteht darin, alle
Prozesse, die bereit sind, in einer zyklischen Liste anzuordnen.
Die bereiten Prozesse kommen in einem bestimmten Turnus an die Reihe und
erhalten jeweils eine Zeitscheibe.
Prozesse, die nicht mehr bereit sind, werden aus der Liste entfernt, solche, die
soeben bereit geworden sind, werden an einer bestimmten Stelle in der Liste
eingehängt.
Die Verwaltung der Prozesse erfolgt durch einen Teil des Betriebssystems, den
sogenannten Scheduler, der selbst als einer der Prozesse in der Liste betrieben
wird. Er wird einmal pro Rundendurchlauf aktiv und kann den Ablauf der anderen
Prozesse planen.
4.4.3 Speicherverwaltung
Eine der Aufgaben des Betriebssystems besteht in der Versorgung der Prozesse mit
dem Betriebsmittel Arbeitsspeicher.
Dabei sollen Prozesse vor gegenseitiger Beeinträchtigung durch fehlerhafte
Adressierung gemeinsam benutzter Speicherbereiche geschützt werden.
Die wesentlichen Verfahren zur Speicherverwaltung sind:
o
o
Paging
Swapping
4.4.3.1 Virtueller Speicher
Vor langer Zeit stießen Programmierer zum ersten Mal auf das Problem, dass die
Programme zu groß für den verfügbaren Speicher wurden.
Normalerweise lösten Sie dieses Problem, indem sie die Programme in mehrere
Teile aufspalteten, so genannte Overlays. Zunächst wurde Overlay 0 ausgefügrt,
das dann, sobald es fertig war, das nächste Overlay 1 aufrief, welches dann Overlay
2 lud, usw.
Die einzelnen Overlays wurden auf der Festplatte gespeichert und nach Bedarf vom
Betriebssystem dynamisch ein- und ausgelagert .
Obwohl die Overlays vom Betriebssystem verwaltet wurden, mußte der
Programmierer das Programm selbst aufteilen.
Große Programme in kleine, modulare Teile aufzuspalten, war eine zeitaufwändige
66
und langweilige Arbeit.
Es dauerte also nicht lange, bis sich jemand etwas ausdachte, um auch diese
Aufgabe durch den Computer erledigen zu lassen.
Die Methode wurde als virtueller Speicher bekannt. Die Grundidee dahinter war, zu
erlauben, dass der Programmcode, die Daten und der Stack zusammen größer
sind als der verfügbare Hauptspeicher.
Das Betriebssystem hält die Teile des Programms, die gerade gebraucht werden, im
Hauptspeicher und den Rest auf der Festplatte. Beispielsweise kann man ein 16 MB
großes Programm auf einer 4 MB-Maschine ausführen, indem man die 4 MB, die
im Speicher liegen, zu jedem Zeitpunkt sorgfältig auswählt und Teile des Programms
nach Bedarf ein- und auslagert.
4.4.3.2 Swapping
Eine weitere Methode zur Speicherverwaltung ist das so genannte Swapping.
Hierbei befinden sich zu einem gegebenen Zeitpunkt einige Segmentmengen im
Speicher.
Bei einem Aufruf eines Segmentes, das sich momentan nicht im Speicher befindet,
wird dieses geholt.
Ist kein Platz dafür vorhanden, müssen eines oder mehrere Segmente zuerst auf die
Platte ausgelagert werden.
In dieser Hinsicht ist das Swapping von Segmenten mit dem Paging vergleichbar:
Segmente werden nach Bedarf geholt oder ausgelagert.
Die Segmentierung wird beim Swapping aber anders als beim Paging
implementiert. Swapping hat hier einen entscheidenden Nachteil:
Seiten (--> Paging) haben eine feste Größe, Swapping-Segmente nicht.
67
Die Folgen dieses Nachteils werden deutlich, wenn wir die obige Abbildung
betrachten. Sie zeigt das Abbild eines physikalischen Speichers, der anfangs fünf
Segmente enthält.
Man betrachte, was passiert, wenn in




(a) Segment 1 ausgelagert und das kleinere Segment 7 an seine Stelle
platziert wird. Dies zeigt (b).
(b) Zwischen Segment 7 und Segment 2 liegt ein ungenutzter Bereich, also
ein Loch.
(c) Segment 4 wird durch Segment 5 ersetzt
(d) Segment 3 wird durch Segment 6 ersetzt.
Nachdem das System eine Weile lief, wird der Speicher in Stücke aufgeteilt, von
denen einige Segmente und andere Löcher enthalten.
Dieses Phänomen nennt man externe Fragmentierung, weil außerhalb der
Segmente, in den dazwischen liegenden Löchern, Platz verschwendet wird.
Diese externe Fragmentierung wird beim Paging vermieden, weil sowohl die
Seiten des virtuellen Speichers, als auch die Seitenrahmen des physikalischen
Speichers die gleiche Größe haben.
4.4.3.3 Paging
Das als virtueller Speicher beschriebene Konzept basiert auf der Trennung
zwischen dem (technisch möglichen) Adressraum eines Computers (bei einem 32Bit-System sind das 4 GByte) und den tatsächlich verfügbaren Speicherstellen
(z.B. 1 GByte RAM).
Man unterscheidet also den virtuellen Adressraum und den physikalischen
Adressraum.
Beim Paging wird der virtuelle Adressraum in eine Reihe von Seiten gleicher
Größe gegliedert.
Ebenso wird der physikalische Adressraum in Segmente aufgeteilt, die genauso
groß sind wie die Seiten.
Jedes Stück Arbeitsspeicher kann also genau eine Seite aufnehmen.
Diese Segmente des Arbeitsspeichers (physikalischer Speicher) nennt man
deshalb auch Seitenrahmen.
Das Speicherabbild eines Prozesses wird also in einzelne Seiten aufgeteilt.
Nur die wirklich benötigten Speicherseiten müssen im Arbeitsspeicher geladen
sein, während ein Prozess läuft.
Ein Prozess, der gestartet werden soll, muss dem Betriebssystem mitteilen, wie viel
virtueller Hauptspeicher benötigt wird. Für diesen virtuellen Speicherbereich muß
ein Schattenspeicher in einem speziellen Bereich des Dateisystems reserviert
werden.
Diese Paging Area liegt auf der Festplatte (das Pagefile unter Windows).
68
Ihre Größe begrenzt letztlich den virtuellen Hauptspeicher, der jedem Prozess
zugeordnet werden kann.
Prozesse können den theoretisch adressierbaren Bereich von 4 GB also nur
verwenden, wenn die Festplatte groß genug ist.
4.4.3.4 MMU (Memory Management Unit)
Wenn eine nicht geladene Seite adressiert wird, muss dieser Vorgang von der
Hardware entdeckt werden, dies übernimmt die so genannte MMU oder Memory
Management Unit, bei heutigen Rechnern Bestandteil der CPU, die dann die
virtuelle Adresse (die der nicht geladenen Seite) auf die physische Adresse
abbildet.
Die angeforderte Seite wird dann durch das Betriebssystem von der Festplatte in
den Arbeitsspeicher geladen.
4.4.3.5 Ablauf
Wenn nun eine Adresse angefragt wird, die sich nicht im Hauptspeicher befindet,
geschieht folgendes:
1) Der Inhalt des Arbeitsspeichers wird auf die Platte gespeichert
2) Die Speicherseite mit der gewünschten Adresse wird auf der Platte ausfindig
gemacht.
3) Die gefundene Speicherseite wird in den Arbeitsspeicher geladen.
4) Die Abbildung der virtuellen Adressen auf die physikalisch vorhandenen wird
angepasst, da die Speicherseite nicht mehr auf der Platte liegt, sondern im
Hauptspeicher.
5) Die Ausführung wird fortgesetzt, als wäre nichts Ungewöhnliches passiert.
Die Zuordnung von virtuellen Adressen zu physikalischen Adressen wird über die
sogenannte Seitentabelle verwaltet.
69
5 Algorithmen und Programmierung
5.1 Einführung
Die Programmierung ist ein Teilgebiet der Informatik, das sich im weiteren Sinne
mit Methoden und Denkweisen bei der Lösung von Problemen mit Hilfe von
Computern und im engeren Sinne mit dem Vorgang der Programmerstellung
befasst.
Unter einem Programm versteht man dabei eine in einer speziellen Sprache
verfasste Anleitung zum Lösen eines Problems durch einen Computer.
Programme werden auch unter dem Begriff Software subsumiert. Konkreter
ausgedrückt ist das Ziel der Programmierung bzw. Softwareentwicklung, zu
gegebenen Problemen Programme zu entwickeln, die auf Computern ausführbar
sind und die Probleme korrekt und vollständig lösen, und das möglichst effizient.
Die hier angesprochenen Probleme können von ganz einfacher Art sein, wie das
Addieren oder Subtrahieren von Zahlen oder das Sortieren einer gegebenen
Datenmenge.
Komplexere Probleme reichen von der Erstellung von Computerspielen oder der
Datenverwaltung von Firmen bis hin zur Steuerung von Raketen.
Von besonderer Wichtigkeit für ein systematisches Programmieren ist die
Herausarbeitung der fundamentalen Konzepte einer Programmiersprache.
Eine Programmiersprache ist eine zum Formulieren von Programmen geschaffene
künstliche Sprache. Die Anweisungen, die wir dem Computer geben, werden als Text
formuliert, man nennt jeden solchen Text ein Programm.
Der Programmtext wird nach genau festgelegten Regeln formuliert. Diese Regeln
sind durch die Grammatik einer Programmiersprache festgelegt.
Im Gegensatz zur Umgangssprache verlangen Programmiersprachen das exakte
Einhalten der Grammatikregeln. Jeder Punkt, jedes Komma hat seine Bedeutung,
selbst ein kleiner Fehler führt dazu, dass das Programm als Ganzes nicht verstanden
wird.
In frühen Programmiersprachen standen die verfügbaren Operationen eines
Rechners im Vordergrund. Diese mussten durch besonders geschickte
Kombinationen verbunden werden, um ein bestimmtes Problem zu lösen.
Moderne höhere Programmiersprachen orientieren sich stärker an dem zu lösenden
Problem und gestatten eine abstrakte Formulierung des Lösungswegs, der die
Eigenarten der Hardware, auf der das Programm ausgeführt werden soll, nicht mehr
in Betracht zieht.
Dies hat den Vorteil, dass das gleiche Programm grundsätzlich auf unterschiedlichen
Systemen ausführbar ist.
70
5.2 Übersicht der Programmiersprachen
71
5.3 Definition von Programmiersprachen
Programmiersprachen sind sehr exakte künstliche Sprachen zur Formulierung von
Programmen.
Sie dürfen keine Mehrdeutigkeiten bei der Programmerstellung zulassen, damit der
Computer das Programm auch korrekt ausführen kann.
Bei der Definition einer Programmiersprache müssen deren Lexik, Syntax, Semantik
und Pragmatik definiert werden:
Lexik:
Die Lexik einer Programmiersprache definiert die gültigen Zeichen zw. Wörter, aus
denen Programme der Programmiersprache zusammengesetzt sein dürfen.
Syntax:
Die Syntax einer Programmiersprache definiert den korrekten Aufbau der Sätze aus
gültigen Zeichen bzw. Wörtern, d.h. sie legt fest, in welcher Reihenfolge lexikalisch
korrekte Zeichen bzw. Wörter im Programm auftreten dürfen.
Semantik:
Die Semantik einer Programmiersprache definiert die Bedeutung syntaktisch
korrekter Sätze, d.h. sie beschreibt, was passiert, wenn bspw. bestimmte
Anweisungen ausgeführt werden.
Pragmatik:
Die Pragmatik einer Programmiersprache definiert ihren Einsatzbereich, d.h. sie gibt
an, für welche Arten von Problemen die Programmiersprache besonders gut
geeignet ist.
5.4 Klassifikation von Programmiersprachen
Eine durchaus berechtigte Frage wäre: Wieso gibt es eigentlich nicht nur eine einzige
Programmiersprache, mit der alle Programmierer arbeiten?
Da Programmiersprachen anders als natürliche Sprachen, die sich über
Jahrhunderte hinweg entwickelt haben, ja künstlich definiert werden müssen, hätte
man sich doch von Anfang an auf eine einheitliche Programmiersprache festlegen
können.
Eine mögliche Klassifizierung unterscheidet so genannte niedere
Maschinensprachen (maschinennahe Programmiersprachen) und höhere
problemorientierte Programmiersprachen.
Maschinensprachen ermöglichen die Erstellung sehr effizienter Programme. Sie
sind jedoch abhängig vom speziellen Computertyp.
Dahingegen orientieren sich die höheren Programmiersprachen nicht so sehr an
72
den vom Computer direkt ausführbaren Befehlen, sondern eher an den zu
lösenden Problemen.
Sie sind für Menschen verständlicher und einfacher zu handhaben.
Ein weiterer Grund für die Existenz der vielen verschiedenen Programmiersprachen
liegt in der Tatsache, dass die zu lösenden Probleme nicht alle gleichartig sind.
So werden häufig neue Programmiersprachen definiert, die speziell für bestimmte
Klassen von Problemen konzipiert sind.
Den höheren Programmiersprachen liegen bestimmte Konzepte zugrunde, mit
denen die Lösung von Problemen formuliert wird.
Im Wesentlichen lassen sich hier fünf Kategorien - oder auch
Programmierparadigmen genannt - unterscheiden:





Imperative Programmiersprachen:
Programme bestehen aus Folgen von Befehlen (BASIC, PASCAL, MODULA2).
Funktionale Programmiersprachen:
Programme werden als mathematische Funktionen betrachtet (LISP,
MIRANDA).
Prädikative/deklarative Programmiersprachen:
Programme bestehen aus Fakten (gültige Tatsachen) und Regeln, die
beschreiben, wie aus gegebenen Fakten neue Fakten hergeleitet werden
können (PROLOG).
Regelbasierte Programmiersprachen:
Programme bestehen aus "Wenn-Dann-Regeln"; wenn eine angegebene
Bedingung gültig ist, dann wird eine angegebene Aktion ausgeführt (OPS5).
Objektorientierte Programmiersprachen:
Programme bestehen aus Objekten, die bestimmte (Teil-)Probleme lösen und
zum Lösen eines Gesamtproblems mit anderen Objekten über Nachrichten
kommunizieren können (SMALLTALK).
Nicht alle Programmiersprachen können eindeutig einer dieser Klassen zugeordnet
werden.
So ist bspw. LOGO eine funktionale Programmiersprache, die aber auch
imperative Sprachkonzepte besitzt.
Java und C++ können als imperative objektorientierte Programmiersprachen
klassifiziert werden, denn Java und C++-Programme bestehen aus
kommunizierenden Objekten, die intern mittels imperativer Sprachkonzepte
realisiert werden.
Programmiersprachen einer Kategorie unterscheiden sich häufig nur in
syntaktischen Feinheiten. Die grundlegenden Konzepte sind ähnlich.
Von daher ist es im Allgemeinen nicht besonders schwierig, eine weitere
Programmiersprache zu erlernen, wenn man bereits eine Programmiersprache
derselben Kategorie beherrscht.
Anders verhält es sich jedoch beim Erlernen von Programmiersprachen anderer
Kategorien, weil hier die zugrunde liegenden Konzepte stark voneinander
abweichen.
73
5.5 Vom Programm zur Maschine
Programme, die in einer höheren Programmiersprache geschrieben sind, können
nicht unmittelbar auf einem Rechner ausgeführt werden.
Sie sind anfangs in einer Textdatei gespeichert und müssen erst in Folgen von
Maschinenbefehlen übersetzt werden.
Maschinenbefehle sind elementare Operationen, die der Prozessor des Rechners
unmittelbar ausführen kann.
Sie beinhalten zumindest Befehle, um




Daten aus dem Speicher zu lesen
elementare arithmetische Operationen auszuführen
Daten in den Speicher zu schreiben
die Berechnung an einer bestimmten Stelle fortzusetzen (Sprünge)
Die Übersetzung von einem Programmtext in eine Folge solcher einfacher Befehle
(auch Maschinenbefehle oder Maschinencode genannt), wird von einem Compiler
durchgeführt.
Das Ergebnis ist ein Maschinenprogramm, das in einer als "ausführbar" (executable)
gekennzeichneten Datei gespeichert ist.
Eine solche ausführbare Datei muss noch von einem Ladeprogramm in den
Speicher geladen werden und kann erst dann ausgeführt werden.
Ladeprogramme sind im Betriebssystem enthalten, der Benutzer weiß oft gar nichts
von deren Existenz. So sind in den Betriebssystemen der Windows-Familie
ausführbare Dateien durch die Endung ".exe" oder ".com" gekennzeichnet.
Tippt man auf der Kommandozeile den Namen einer solchen Datei ein und betätigt
die Eingabetaste, so wird die ausführbare Datei in den Hauptspeicher geladen und
ausgeführt.
5.5.1 Virtuelle Maschinen
Die Welt wäre einfacher, wenn sich alle Programmierer auf einen Rechnertyp und
eine Programmiersprache einigen könnten. Man würde dazu nur einen einzigen
Compiler benötigen.
Die Wirklichkeit sieht anders aus. Es gibt (aus gutem Grund) zahlreiche
Rechnertypen und noch viel mehr verschiedene Sprachen.
Fast jeder Programmierer hat eine starke Vorliebe für eine ganz bestimmte Sprache
und möchte, dass seine Programme auf möglichst jedem Rechnertyp ausgeführt
werden können.
Bei n Sprachen und m Maschinentypen würde dies, wie in Abbildung 21 dargestellt,
n x m viele Compiler erforderlich machen.
74
Abbildung 21: Kombination/Zuordnung von Programmiersprachen und Rechnern
Schon früh wurde daher die Idee geboren, eine virtuelle Maschine V zu entwerfen,
die als gemeinsames Bindeglied zwischen allen Programmiersprachen und allen
konkreten Maschinensprachen fungieren könnte.
Diese Maschine würde nicht wirklich gebaut, sondern man würde sie auf jedem
konkreten Rechner emulieren, d.h. nachbilden.
Für jede Programmiersprache müsste dann nur ein Compiler vorhanden sein, der
Code für V erzeugt.
Statt n x m vieler Compiler benötigte man jetzt nur noch n Compiler und m
Implementierungen von V auf den einzelnen Rechnertypen, insgesamt also nur n +
m viele Übersetzungsprogramme - ein gewaltiger Unterschied:
Abbildung 22: Idee einer universellen virtuellen Maschine
Leider ist eine solche virtuelle Maschine nie zu Stande gekommen. Neben dem
Verdacht, dass ihr Design eine bestimmte Sprache oder einen bestimmten
Maschinentyp bevorzugen könnte, stand die begründete Furcht im Vordergrund,
dass diese Zwischenschicht die Geschwindigkeit der Programmausführung
beeinträchtigen könnte.
75
Außerdem verhindert eine solche Zwischeninstanz, dass spezielle Fähigkeiten eines
Maschinentyps oder spezielle Ausdrucksmittel einer Sprache vorteilhaft eingesetzt
werden können.
In Zusammenhang mit einer festen Sprache ist das Konzept einer virtuellen
Maschine jedoch mehrfach aufgegriffen worden, z.B. bei der objektorientierten
Sprache Java.
Ein Java-Compiler übersetzt ein in Java geschriebenes Programm in einen Code für
eine virtuelle Java-Maschine.
Auf jeder Rechnerplattform, für die ein Emulator dieser virtuellen Java-Maschine
verfügbar ist, ist das Programm dann lauffähig.
Weil man also bewusst auf die Ausnutzung besonderer Fähigkeiten der jeweiligen
Hardware verzichtet, wird die Sprache plattformunabhängig.
Abbildung 23: Reale virtuelle Maschine
5.5.2 Interpreter
Ein Compiler übersetzt immer einen kompletten Programmtext in eine Folge von
Maschinenbefehlen, bevor die erste Programmanweisung ausgeführt wird.
Ein Interpreter dagegen übersetzt immer nur eine einzige Programmanweisung in ein
kleines Unterprogramm aus Maschinenbefehlen und führt dieses sofort aus.
Anschließend wird mit der nächsten Anweisung genauso verfahren.
Interpreter sind einfachen zu konstruieren als Compiler, haben aber den Nachteil,
dass ein Befehl, der mehrfach ausgeführt wird, jedes Mal erneut übersetzt werden
muss.
Grundsätzlich können fast alle Programmiersprachen compilierend oder
interpretierend implementiert werden. Trotzdem gibt es einige, die fast ausschließlich
mit Compilern arbeiten.
Dazu gehören Pascal, Modula, COBOL, Fortran, C und C++.
Andere, darunter BASIC, APL, LISP und Prolog, werden überwiegend interpretativ
bearbeitet.
Sprachen wie Java und Smalltalk beschreiten einen Mittelweg zwischen
compilierenden und interpretierenden Systemen - das Quellprogramm wird in Code
für die virtuelle Java bzw. Smalltalk-Maschine, den so genannten Bytecode,
76
compiliert.
Dieser wird dann von der virtuellen Maschine interpretativ ausgeführt.
Damit ist die virtuelle Maschine nichts anderes als ein Interpreter für Bytecode.
5.6 Programmentwicklung
Ziel der Programmierung ist die Entwicklung von Programmen, die gegebene
Probleme korrekt und vollständig lösen.
Ausgangspunkt der Programmentwicklung ist also ein gegebenes Problem,
Endpunkt ist ein ausführbares Programm, das korrekte Ergebnisse liefert.
Den Weg vom Problem zum Programm bezeichnet man auch als Problemlöse- oder
Programmentwicklungsprozess oder kurz Programmierung.
Der Problemelöseprozess kann in mehrere Phasen zerlegt werden, die in der
Regel nicht streng sequentiell durchlaufen werden.
Durch neue Erkenntnisse, aufgetretene Probleme und Fehler wird es immer wieder
zu Rücksprüngen in frühere Phasen kommen.
5.6.1 Analyse
Eine Analyse ist eine vollständige, detaillierte und eindeutige Spezifikation eines
Problems. In der Analysephase wird das zu lösende Problem bzw. das Umfeld des
Problems genauer untersucht.
Insbesondere folgende Fragestellungen sollten bei der Analyse ins Auge gefasst und
auch mit anderen Personen diskutiert werden:





Ist die Problemstellung exakt und vollständig beschrieben?
Was sind mögliche Initialzustände bzw. Eingabewerte (Parameter) für das
Problem?
Welches Ergebnis wird genau erwartet, wie sehen der gewünschte
Endzustand bzw. die gesuchten Ausgabewerte aus?
Gibt es Randbedingungen, Spezialfälle bzw. bestimmte Zwänge
(Constraints), die zu berücksichtigen sind?
Lassen sich Beziehungen zwischen Initial- und Endzuständen bzw. Eingabeund Ausgabewerten herleiten?
Erst wenn alle diese Fragestellungen beantwortet sind und eine exakte
Problemspezifikation vorliegt, sollte in die nächste Phase verzweigt werden.
Es hat sich gezeigt, dass Fehler, die aus einer nicht ordentlich durchgeführten
Analyse herrühren, zu einem immensen zusätzlichen Arbeitsaufwand in späteren
Phasen führen können. Deshalb sollte in dieser Phase mit größter Sorgfalt
gearbeitet werden.
77
5.6.2 Entwurf
Nachdem in der Analysephase das Problem in einer Spezifikation genau
beschrieben worden ist, geht es darum, einen Lösungsweg zu entwerfen. Dies
geschieht in der Entwurfsphase.
Da die Lösung von einem Rechner durchgeführt wird, muss jeder Schritt exakt
vorgeschrieben sein.
Wir kommen zu folgender Begriffsbestimmung:
Ein Algorithmus ist eine detaillierte und explizite Vorschrift zur schrittweisen
Lösung eines Problems.
Im Einzelnen beinhaltet diese Definition:



Die Ausführung des Algorithmus erfolgt in einzelnen Schritten.
Jeder Schritt besteht aus einer einfachen und offensichtlichen Grundaktion.
Zu jedem Zeitpunkt muss klar sein, welcher Schritt als nächster auszuführen
ist.
Der Entwurfsprozess kann im Allgemeinen nicht mechanisch durchgeführt werden,
vor allen Dingen ist er nicht automatisierbar.
Vielmehr kann man ihn als kreativen Prozess bezeichnen, bei dem
Auffassungsgabe, Intelligenz und vor allem Erfahrung des Programmierers eine
wichtige Rolle spielen.
Diese Erfahrung kann insbesondere durch fleißiges Üben erworben werden.
Für den Entwurf eines Algorithmus können die folgenden Ratschläge nützlich sein:

Informieren Sie sich über möglicherweise bereits existierende und verfügbare
Lösungen für vergleichbare Probleme und nutzen Sie diese.

Schauen Sie sich nach allgemeineren Problemen um und überprüfen Sie, ob
Ihr Problem als Spezialfall des allgemeinen Problems betrachtet werden
kann.

Versuchen Sie, das (komplexe) Problem in einfachere Teilprobleme
aufzuteilen. Wenn eine Aufteilung möglich ist, wenden Sie den hier skizzierten
Programmentwicklungsprozess zunächst für die einzelnen Teilprobleme
an und setzen dann die Teillösungen zu einer Lösung für das
Gesamtproblem zusammen.
Eine Möglichkeit zur grafischen Darstellung von Algorithmen sind Flussdiagramme.
Sie haben den Vorteil, unmittelbar verständlich zu sein.
Der Nachteil ist, dass sie für komplexere Algorithmen schnell unübersichtlich
werden.
Flussdiagramme setzen sich zusammen aus folgenden Symbolen:
78
Abbildung 24: Elemente von Flussdiagrammen
Beispiel für einen Algorithmus
Bei dem folgenden Beispiel geht es um die Lösung des Problems, die Summe aller
natürlichen Zahlen bis zu einer vorgegebenen Natürlichen Zahl n zu berechnen.
Mathematisch definiert ist also die folgende Funktion f zu berechnen:
Das folgende Flussdiagramm stellt einen Algorithmus zur Lösung des obigen
Problems dar.
Die Aktionen, die in den Rechtecken dargestellt sind, werden als elementare
Handlungen des Rechners verstanden, die nicht näher erläutert werden müssen.
In unserem Falle handelt es sich dabei um so genannte Zuweisungen, bei denen ein
Wert berechnet und das Ergebnis gespeichert wird.
So wird z.B. durch die Zuweisung "erg = erg + i" der Inhalt der durch erg und i
bezeichneten Speicherplätze addiert und das Ergebnis in dem Speicherplatz erg
abgelegt.
79
Abbildung 25: Flussdiagramm zum Beispielalgorithmus
Algorithmen als Lösung einer Spezifikation
Eine Spezifikation beschreibt also ein Problem, ein Algorithmus gibt eine Lösung
des Problems an.
Ist das Problem durch ein Paar {P} {Q} aus einer Vorbedingung P und einer
Nachbedingung Q gegeben, so schreiben wir:
o
{P} A {Q},
falls der Algorithmus A die Vorbedingung P in die Nachbedingung Q überführt.
Genauer formuliert bedeutet dies:
Wenn der Algorithmus A in einer Situation gestartet wird, in der P gilt, dann wird,
wenn A beendet ist, Q gelten.
In diesem Sinne ist ein Algorithmus eine Lösung einer Spezifikation. Man kann eine
Spezifikation als eine Gleichung mit einer Unbekannten ansehen:
Zu der Spezifikation {P} {Q} ist ein Algorithmus X gesucht mit {P} X {Q}.
80
Nicht jede Spezifikation hat eine Lösung. So verlangt {M<0} {x = log M}, den
Logarithmus einer negativen Zahl zu finden. Diese Spezifikation kann also nicht
gelöst werden.
Wenn eine Spezifikation aber eine Lösung hat, dann gibt es immer unendlich viele
Lösungen. So ist jeder Algorithmus, der das gewünschte Ergebnis liefert - ganz egal,
wie umständlich er dies macht - eine Lösung für unsere Spezifikation.
Terminierung
In einer oft benutzten strengeren Definition des Begriffes Algorithmus wird verlangt,
dass ein solcher nach endlich vielen Schritten terminiert, also beendet ist.
Diese Forderung steht aber vor folgenden Schwierigkeiten:


Manchmal ist es erwünscht, dass ein Programm bzw. ein Algorithmus nicht
von selber abbricht. Ein Texteditor, ein Computerspiel oder ein Betriebssystem
soll im Prinzip unendlich lange laufen können.
Es ist oft nur schwer oder überhaupt nicht feststellbar, ob ein Algorithmus in
endlicher Zeit zum Ende kommen wird. Verantwortlich dafür ist die
Möglichkeit, Schleifen zu bilden, so dass dieselben Grundaktionen mehrfach
wiederholt werden.
5.6.3 Implementierung
Der Entwurf eines Algorithmus sollte unabhängig von einer konkreten
Programmiersprache erfolgen.
Die anschließende Überführung des Algorithmus in ein in einer bestimmten
Programmiersprache verfasstes Programm wird als Implementierung bezeichnet.
Anders als der Entwurf eines Algorithmus ist die Implementierung in der Regel ein
eher mechanischer Prozess.
Die Implementierungsphase besteht selbst wieder aus zwei Teilphasen:


Editieren:
Zunächst wird der Programmcode mit Hilfe eines Editors eingegeben und in
einer Datei dauerhaft abgespeichert.
Compilieren:
Anschließend wird de rProgrammcode mit Hilfe eines Compilers auf
syntaktische Korrektheit überprüft und - falls keine Fehler vorhanden sind - in
eine ausführbare Form (ausführbares Programm) überführt.
Liefert der Compiler eine Fehlermeldung, muss in die Editierphase
zurückgesprungen werden.
Ist die Compilation erfolgreich, kann das erzeugte Programm ausgeführt werden. Je
nach Sprache und Compiler ist die Ausführung entweder mit Hilfe des
Betriebssystems durch den Rechner selbst oder aber durch die Benutzung eines
Interpreters möglich.
81
5.6.4 Test
In der Testphase muss überprüft werden, ob das entwickelte Programm die
Problemstellung korrekt und vollständig löst.
Dazu wird das Programm mit verschiedenen Initialzuständen oder Eingabewerten
ausgeführt und überprüft, ob es die erwarteten Ergebnisse liefert.
Man kann eigentlich immer davon ausgehen, dass Programme nicht auf Anhieb
korrekt funktionieren, was zum einen an der hohen Komplexität des
Programmentwicklungsprozesses und zum anderen an der hohen Präzision liegt,
die die Formulierung von Programmen erfordert.
Insbesondere die Einbeziehung von Randbedingungen wird von
Programmieranfängern häufig vernachlässigt, so dass das Programm im Normalfall
zwar korrekte Ergebnisse liefert, in Ausnahmefällen jedoch versagt.
Genauso wie der Algorithmusentwurf ist auch das Testen eine kreative Tätigkeit,
die viel Erfahrung voraussetzt und darüber hinaus ausgesprochen zeitaufwendig ist.
Im Durchschnitt werden ca. 40% der Programmentwicklungszeit zum Testen und
Korrigieren verwendet.
Auch durch noch so systematisches Testen ist es in der Regel nicht möglich, die
Abwesenheit von Fehlern zu beweisen.
Es kann nur die Existenz von Fehlern nachgewiesen werden. Aus der Korrektheit des
Programms für bestimmte überprüfte Initialzustände bzw. Eingabewerte kann nicht
auf die Korrektheit für alle möglichen Initialzustände bzw. Eingabewerte
geschlossen werden!
5.6.5
Dokumentation
Parallel zu den eigentlichen Programmentwicklungsphasen sollten alle
Ergebnisse dokumentiert, d.h. schriftlich festgehalten werden.
Die Dokumentation besteht also aus:






einer exakten Problemstellung,
einer verständlichen Beschreibung der generellen Lösungsidee und des
entwickelten Algorithmus,
dem Programmcode sowie einer
Erläuterung der gewählten Testszenarien und
Protokollen der durchgeführten Testläufe
Außerdem sollten weitergehende Erkenntnisse, wie aufgetretene Probleme
oder alternative Lösungsansätze, in die Dokumentation mit aufgenommen
werden.
Die Dokumentation dient dazu, dass andere Personen bzw. der Programmierer
selbst, auch zu späteren Zeitpunkten, das Programm noch verstehen bzw. den
Programmentwicklungsprozess nachvollziehen können, um z.B. mögliche
Erweiterungen oder Anpassungen vornehmen oder die Lösung bei der Bearbeitung
vergleichbarer Probleme wieder verwenden zu können.
82
5.7 Elementare Aktionen in Programmiersprachen
Wir haben bisher noch nicht erklärt, welche "elementaren Aktionen" wir
voraussetzen, wenn wir Algorithmen formulieren.
In der Tat sind hier eine Reihe von Festlegungen denkbar.
Wir könnten zum Beispiel in einem Algorithmus formulieren, wie man ein bestimmtes
Gericht zubereitet.
Die Grundaktionen wären dann einfache Aufgaben, wie etwa "Prise Salz
hinzufügen", "umrühren" und "zum Kochen bringen".
Der Algorithmus beschreibt dann, ob, wann und in welcher Reihenfolge diese
einfachen Handlungen auszuführen sind.
In einer Programmiersprache kann man Speicherzellen für Datenwerte mit Namen
kennzeichnen.
Diese nennt man auch Variablen. Man darf den Inhalt einer Variablen lesen oder ihr
einen neuen Wert zuweisen. Der vorher dort gespeicherte Wert geht dabei verloren,
man sagt, er wird überschrieben.
Eine Grundaktion besteht jetzt aus drei elementaren Schritten:



einige Variablen lesen
die gelesenen Werte durch einfache Rechenoperationen verknüpfen
das Ergebnis einer Variablen zuweisen.
Eine solche Grundaktion heißt Zuweisung. In Java und C++ wird sie als x = y
geschrieben.
Dabei ist x eine Variable, das Zeichen "=" ist der Zuweisungsoperator und die
rechte Seite y kann ein beliebiger (arithmetischer) Ausdruck sein, in dem auch
Variablen vorkommen können.
Es handelt sich nicht um eine Gleichung, denn die Variablen, die auf der rechten
Seite des Zuweisungszeichens vorkommen, stehen für den alten Wert und die
Variable auf der linken Seite für den neuen Wert nach der Zuweisung. Am besten
man ignoriert die Ähnlichkeit des Zuweisungsoperators mit dem
Gleichheitszeichen und spricht es als "erhält" aus:
o
"x erhält (den Wert) y" für x = y.
Hat man erst einmal einige nützliche Algorithmen programmiert, kann man diese in
anderen Programmen benutzen - oder " aufrufen" - und wie eine elementare
Aktion behandeln.
Dazu muss man sie nur mit einem Namen versehen und kann danach diesen Namen
anstelle des Algorithmus hinschreiben.
Einige solcher zusätzlicher Aktionen, in Java und C++ Methoden bzw. Funktionen
genannt, sind bei allen Sprachen bereits "im Lieferumfang" enthalten.
So ist die Funktion printf standardmäßig in C++ enthalten. Ihre Wirkung ist die
Ausgabe von Werten in einem Terminalfenster.
Ein Aufruf, wie etwa printf ("Hallo Welt!"), ist also auch eine elementare Aktion.
83
5.8
Programmierumgebungen
Für fast alle Sprachen gibt es heute "integrierte Entwicklungsumgebungen"
(integrated development environment - IDE), die alle zur Programmerstellung
notwendigen Werkzeuge beinhalten:
o
o
o
einen Editor zum Erstellen und Ändern eines Programmtextes,
einen Compiler bzw. Interpreter zum Ausführen von Programmen,
einen Debugger für die Fehlersuche in der Testphase eines
Programms.
Kern dieser Systeme ist immer ein Texteditor zum Erstellen des Programmtextes.
Dieser hebt typischerweise nicht nur die Schlüsselworte der Programmiersprache
farblich hervor, er markiert auch zugehörige Klammerpaare und kann auf Wunsch
den Programmtext auch übersichtlich formatieren.
Klickt man auf den Namen einer Variablen oder einer Funktion, so wird automatisch
deren Definition im Programmtext gefunden und angezeigt.
Soll das zu erstellende Programm zudem eine moderne graphische
Benutzeroberfläche erhalten, so kann man diese mit einem GUI-Editor erstellen,
indem man Fenster, Menüs, Buttons und Rollbalken mit der Maus "zusammenklickt",
beliebig positioniert und anpasst.
5.9
Datentypen und Variablen
5.9.1 Datentypen
Daten sind die Objekte, mit denen ein Programm umgehen soll. Man muss
verschiedene Sorten von Daten unterscheiden, je nachdem, ob es sich um
Wahrheitswerte, Zahlen, Geburtstage, Texte, Bilder, Musikstücke oder Videos
handelt.
Alle diese Daten sind von verschiedenem Typ, insbesondere verbrauchen Sie
unterschiedlich viel Speicherplatz und unterschiedliche Operationen sind mit ihnen
durchführbar.
So lassen sich zwei Geburtstage oder zwei Bilder nicht addieren, wohl aber zwei
Zahlen. Andererseits kann ein Bild komprimiert werden, bei einer Zahl macht dies
aber keinen Sinn.
Zu einem bestimmten Typ von Daten gehört also immer auch ein
charakteristischer Satz von Operationen, um mit diesen Daten umzugehen. Jede
Programmiersprache stellt eine Sammlung von Datentypen samt der zugehörigen
Operationen bereit und bietet zugleich Möglichkeiten, neue Datentypen zu definieren.
5.9.2 Variablen
Eine Variable in einer Programmiersprache ist eine benannte Speicherstelle im
Arbeitsspeicher des Rechners. Über den Variablennamen kann der Programmierer
auf die entsprechende Speicherzelle zugreifen.
84
Eine Variable hat vier Kennzeichen:




Variablennamen
Datentyp
Wert
Adresse (der Speicherzelle)
Der Datentyp ist der Bauplan für eine Variable. Der Datentyp legt fest, welche
Operationen auf einer Variablen möglich sind und wie die Darstellung
(Repräsentation) der Variablen im Speicher des Rechners erfolgt.
Mit der Darstellung wird festgelegt, wie viele Bytes die Variable im Speicher einnimmt
und welche Bedeutung ein jedes Bit dieser Darstellung hat.
Variablen braucht man, um in ihnen Werte abzulegen. Eine Variable ist eine
veränderliche Größe - ihr Wert kann also in ihrem Speicherbereich nach Bedarf
verändert werden.
Der Wert einer Variablen muss der Variablen in der Regel explizit zugewiesen
werden.
Es gibt aber auch Fälle, bei denen von der Programmiersprache aus eine Variable in
impliziter Weise mit einem Wert vorbelegt wird.
Ein solcher Vorbelegungs-Wert wird als Default-Wert oder Standardwert
bezeichnet. Wird einer Variablen weder explizit, noch defaultmäßig - d.h. durch
Vorbelegung - ein Wert zugewiesen, so ist ihr Wert undefiniert.
Da im Arbeitsspeicher die Bits immer irgendwie ausgerichtet sind, hat jede Variable
automatisch einen Wert, auch wenn ihr noch kein definierter Wert zugewiesen wurde.
Ein solcher Wert ist jedoch rein zufällig und führt zu einer Fehlfunktion des
Programms.
Daher darf es der Programmierer nicht versäumen, den Variablen die gewünschten
Startwerte (Initialwerte) zuzuweisen, d.h. die Variable zu initialisieren.
Variablen liegen während der Programmausführung in Speicherzellen des
Arbeitsspeichers. Die Speicherzellen des Arbeitsspeichers sind durchnummeriert.
Die Nummern der Speicherzellen werden Adressen genannt. Eine Variable kann
natürlich mehrere Speicherzellen einnehmen.
Abbildung 26: Variable im Arbeitsspeicher
85
Die Variable aus der obigen Abbildung belegt die Speicherzellen mit den Adressen 5
und 6 und hat den Wert 3. Über den Namen der Variablen kann man ihren Wert aus
den Speicherzellen auslesen und verändern.
5.9.3 Primitive Datentypen
Im Folgenden wollen wir die wichtigsten Datentypen, die auch in den meisten
Programmiersprachen vorhanden sind, vorstellen. Manche dieser Typen wie z.B. die
der ganzen oder der reellen Zahlen, umfassen theoretisch unendlich viele Werte.
Die meisten Programmiersprachen schränken daher die verfügbaren Werte auf
verschieden große endliche Bereiche ein.
Dies hat zur Folge, dass bei der Überschreitung dieser Bereiche Fehler auftreten
können, die sich je nach Anwendungsfall mehr oder weniger katastrophal äußern
können.
5.9.3.1 Datentypen für Boolesche Werte
Der einfachste Datentyp besteht aus den booleschen Werten true und false. Man
bezeichnet diesen Datentyp mit dem englischen Ausdruck boolean (Java) oder mit
bool (C++).
Der benötigte Speicherplatz für diesen Datentyp beträgt in Java 2 Bytes und in
C++ 1 Byte.
Für die Durchführung von logischen Operationen stehen in Java und C++ folgende
Operationen zur Verfügung:
Operator (Java/c++)
& / &&
| / ||
^/
!/!
Verwendung
op1 & op2
op1 | op2
op1 ^ op2
!op1
Operation
AND
OR
XOR
NOT
5.9.3.2 Datentypen für ganze Zahlen
Zur Darstellung ganzer Zahlen ohne Nachkommastelle gibt es
o
o
in Java die Datentypen byte, short, int und long und
in C++ short int, int und long int
Als elementare Operationen auf diesen Datentypen gelten die arithmetischen
Operationen +, -, *, /.
In Java sind alle ganzzahligen Datentypen vorzeichenbehaftet, wohingegen in C++
dies durch die Schlüsselwörter signed und unsigned angegeben wird.
In Java ergeben sich folgende ganzzahlige Datentypen:
86
Datentyp
byte
short
int
long
Größe in Bytes
1
2
4
8
Wertebereich
-27...27-1
-215...215-1
-231...231-1
-263...263-1
5.9.3.3 Datentypen für reelle Zahlen
Datentypen für reelle Zahlen dienen der Speicherung von Zahlen mit
Nachkommastellen. In Java stehen hierzu die Typen float und double und in C++
float, double und long double zur Verfügung. Als elementare Operationen auf
diesen Datentypen gelten ebenfalls die arithmetischen Operationen +, -, *, /.
In Java gibt es die folgenden Gleitkommatypen:
Datentyp
float
double
long double (C++)
Größe in Bytes
4
8
10
Wertebereich
-3,4x10 bis +3,4x1038
-1,7x10308 bis +1,7x10308
-1,1x104932 bis +1,1x104932
38
5.9.3.4 Datentypen für Zeichen
Sowohl in Java als auch in C++ wird ein Zeichen durch den Datentyp char (engl.
character) dargestellt. Während C++ dazu die 256 ASCII-Zeichen verwendet, stellt
Java bereits den gesamten UNICODE-Zeichensatz zur Verfügung.
Zur Darstellung eines Zeichens werden daher in Java 2 Byte und in C++ 1 Byte
benötigt.
5.9.4 Zusammengesetzte Datentypen
Die bisher besprochenen Datentypen sind in einem gewissen Sinne atomar, d.h. ihre
Werte sind nicht weiter in Bestandteile zerlegbar.
Zusammengesetzte Datentypen bestehen aus einer Menge gleichartiger
Datentypen.
5.9.4.1 Strings
Als erstes Beispiel eines zusammengesetzten Typs betrachten wir Strings.
Ein String ist eine Zeichenkette und besteht daher aus einer Folge von Zeichen.
Strings kann man direkt als Stringliterale angeben. Dazu schließt man eine
beliebige Folge von Zeichen in besondere Begrenzungszeichen ein.
Java benutzt dafür doppelte Anführungszeichen:
o
"Ich bin ein Java-Stringliteral"
87
Durch Aneinanderhängen (Konkatenieren) zweier Strings s1 und s2 entsteht ein
neuer String s1+s2.
Auf die einzelnen Zeichen eines Strings kann man zugreifen, Java bietet dafür die
Methode s1.charAt(i) an. i ist dabei eine Variable, die die Position des gewünschten
Zeichens im String angibt.
Mit s.length() kann man die Länge des Strings anzeigen lassen.
s.indexOf("test") liefert die Position des Strings "test" innerhalb des Strings s oder 1, wenn der gesuchte String nicht enthalten ist.
5.9.4.2 Arrays
Ein Array ist ein Objekt, das aus Komponenten (Elementen) zusammengefasst ist,
wobei jedes Element eines Arrays vom selben Datentyp sein muss:
int
int
int
int
Ein Array aus 5 Elementen des Datentyps integer
int
In Java können Arrays aus Elementen eines primitiven Datentyps oder aus
Elementen eines Referenztyps bestehen.
Ein Element eines Arrays kann auch selbst wieder ein Array sein. Dann entsteht ein
mehrdimensionales Array.
Im Folgenden werden zunächst eindimensionale Arrays betrachtet. Die Länge oder
Größe eines Arrays legt die Anzahl der Elemente des Arrays fest.
Die Länge muss als Wert immer eine ganze positive Zahl haben.
Ist laenge die Länge des Arrays, so werden die Elemente von 0 bis laenge-1
durchgezählt. Die Nummer beim Durchzählen wird als Index des Arrays bezeichnet.
Über den Index kann man auf ein Element zugreifen. Der Zugriff auf das i-te
Element des Arrays mit dem Namen arrayName erfolgt durch arrayName[i-1] (weil
die Indizierung mit 0 beginnt!).
Der Vorteil eines Arrays gegenüber mehreren einfachen Variablen ist, dass Arrays
sich leicht mit Schleifen bearbeiten lassen, da der Index einer Array-Komponente
eine Variable sein und als Laufvariable in einer Schleife benutzt werden kann.
char-Arrays in C++

Arrays aus Elementen des Typs char nehmen in C++ eine Sonderstellung ein.
Einerseits kann man in ihnen - in Analogie zu Arrays aus Elementen eines der
anderen Datentypen - einzelne Datenobjekte vom Typ der Arrayelemente
ablegen, also etwa Zeichen oder auch kleine ganze Zahlen, andererseits
dienen sie aber auch zur Speicherung eines speziellen Typs von
Datenobjekten: Strings.
In C++ ist ein String eine Folge von Zeichen, die in einem Array aus
Elementen vom Typ char gespeichert ist und mit dem Nullzeichen '\0'
88
terminiert ist.
Fehlt das Nullzeichen, hat man lediglich einzelne Zeichen in einem charArray vor sich, jedoch noch keinen String. Wegen des zusätzlichen
abschließenden Nullzeichens belegt ein String stets ein Byte mehr Platz im
Speicher, als die eigentliche Zeichenfolge Zeichen hat.
5.9.5 Variablen und Speicher
Um mit einem Computer Algorithmen zu definieren, ist es notwendig,
Zwischenwerte zu speichern und gespeicherte Zwischenwerte für die weitere
Berechnung zu verwenden.
Für die Speicherung von Werten steht der Hauptspeicher zur Verfügung.
Es wäre aber mühsam, wenn sich der Programmierer darum kümmern müsste, an
welcher Stelle im Speicher ein Zwischenwert steht, wie viele Bytes (etwa im Falle
einer Gleitpunktzahl) dazu gehören, welche Speicherplätze noch frei sind etc.
Daher bieten alle Programmiersprachen das Konzept der Variablen an. Aus der
Sicht des Programmierers sind Variablen Behälter für Werte eines bestimmten
Datentyps.
Der Compiler sorgt dafür, dass zur Laufzeit eines Programms für alle Variablen
Speicherplatz reserviert ist und zwar soviel, wie für die Aufnahme von Werten des
jeweiligen Datentyps benötigt wird.
Er setzt automatisch jeden Bezug (Referenz) auf eine Variable in die entsprechende
Hauptspeicheradresse um.
Programmierer können mit Variablen so umgehen wie mit Werten des Datentyps.
Man kann mit ihnen rechnen wie mit Unbestimmten in der Mathematik.
Wenn ein Ausdruck, wie z.B. x*(y+1/y) ausgerechnet wird, so werden für die
Variablen x und y immer die Werte eingesetzt, die sich zur Zeit an den ihnen
zugewiesenen Speicherplätzen befinden.
5.9.6 Deklaration von Variablen
In den meisten höheren Programmiersprachen müssen Variablen vor ihrer ersten
Benutzung deklariert werden.
Dies bedeutet, dass man dem System mitteilen muss, welche Variablen man
benötigt und von welchem Datentyp die Werte sein sollen, die in der Variablen
gespeichert werden sollen.
Generell gilt (und das nicht nur bei Variablennamen), dass C++ und Java zwischen
Groß- und Kleinschreibung unterscheiden:
o
o
o
o
o
int a, b, c;
int A, B, C;
boolean test;
float x, y;
char c;
89
Da nicht alle Sprachen solche Variablendeklarationen verlangen, stellt sich die
Frage, welchen Vorteil eine solche Deklaration mit sich bringt.
Zunächst hilft eine Deklaration dem Compiler, weil er zur Compilierungszeit bereits
weiß, wie viel Speicherplatz er reservieren muss. Für die obige Java-Deklaration
ergibt sich ein Speicherplatzbedarf von:
3x4 Byte + 3x4 Byte + 1 Byte + 2x4 Byte + 2 Byte =
35 Byte.
Zweitens kann der Compiler bereits festlegen, wo die Variablen (relativ zueinander)
im Hauptspeicher angeordnet werden sollen.
In dem Programm kann er dann jede Erwähnung einer Variablen bereits durch eine
Referenz auf den entsprechenden Speicherplatz ersetzen. Dies führt zu einer
Zeitersparnis für jeden Variablenzugriff.
Vor allem aber hilft die Deklaration dem Programmierer - viele Fehler, die aus einer
falschen Benutzung von Variablen entstehen, werden bereits zur
Compilierungszeit erkannt.
5.9.7 Initialisierung von Variablen
Bevor zum ersten Mal ein Wert in einer Variablen gespeichert wurde, ist der darin
befindliche Wert (wie bereits ausgeführt) vom Zufall bestimmt. Daher ist es sinnvoll,
jede Variable möglichst frühzeitig mit einem Ausgangswert zu versehen, d.h. sie zu
initialisieren.
Manche Programmiersprachen initialisieren Variablen automatisch mit einem
Standardwert (engl. Default), die meisten überlassen dies aber dem Programmierer.
C++ und Java bieten die Möglichkeit, Variablen gleichzeitig zu deklarieren und
optional auch mit einem Anfangswert zu initialisieren:
o
o
o
o
int a,b,c = 6;
boolean test = false;
double pi = 3.14;
char c = 'p';
5.9.8 Typkorrekte Ausdrücke
Variablen bezeichnen Werte von bestimmten Datentypen.
Die allgemeinste Form, einen Wert zu bezeichnen, erlaubt neben Variablen und
Elementen der Datenstruktur auch Operationszeichen.
Eine solche Konstruktion nennt man (typkorrekten) Ausdruck, in der Mathematik
auch wohlgeformten Term.
Konstanten und Variablen sind demzufolge Ausdrücke.
Wenn mehrere Ausdrücke durch ein Operationszeichen verknüpft werden, ist die
resultierende Konstruktion wieder ein Ausdruck.
Bedingung dabei ist, dass nur Ausdrücke passender Sorten verknüpft werden
Nehmen wir z.B. an, dass wir in Java folgende Variablen deklariert haben:
90
o
o
o
o
o
int a,b,c = 6;
double pi = 3.14;
double t = 3.00;
char ch = 'c';
char d = 'd';
Dann sind folgende Ausdrücke Beispiele für typkorrekte Ausdrücke:
o
o
a + 2 * (b + c) || nur ganze Zahlen
t = pi + t || nur double-Werte
Nicht typkorrekt ist dagegen:
o
a + ch || Addition von Zahl und Zeichen = sinnlos)
a = b + pi || Zuweisung von double zu Integer führt zu Genauigkeitsverlusten
5.9.9 Typfehler
Die meisten gängigen Programmiersprachen sind statisch getypt, was bedeutet,
dass der Typ jedes Ausdrucks bereits zur Compilierzeit, also vor der Ausführung
des Programms, feststehen muss.
Viele Fehler können daher frühzeitig erkannt werden, weil sie bewirken, dass
Ausdrücke inkompatibler Typen verknüpft werden.
Hätte man sich z.B. vertippt und "length(s+1)" geschrieben, statt "length(s)+1", so
würde der Compiler bereits den Fehler erkennen, denn wenn s als String deklariert
ist, macht s+1 keinen Sinn, und wenn s als Integer deklariert ist, ist die Länge von
s+1 ebenfalls sinnlos.
Ist eine Sprache nicht statisch getypt oder gar völlig untypisiert, so würde der
obige Fehler erst zur Laufzeit oder gar nicht auffallen.
Egal ob s einen Integer oder einen String darstellt, er ist durch eine Bitfolge
repräsentiert - und length arbeitet im Endeffekt auf Bitfolgen.
Es käme also irgendetwas heraus bei length(s+1), nur wäre es vermutlich nicht das
was man eigentlich wollte.
Wird mit diesem sinnlosen Ergebnis weitergerechnet, wird es möglicherweise nicht
einmal als falsch erkannt!
5.10 Der Kern imperativer Sprachen
Programmiersprachen erweitern die Möglichkeiten, die Datenstrukturen durch ihre
Operationen bieten, um die Fähigkeit, Zwischenwerte zu speichern und später
wieder zu verwenden.
Zwischenwerte werden dabei in Variablen abgespeichert.
91
Die jeweilige Belegung einer Variablen mit konkreten Werten nennen wir
Speicherzustand oder kurz Speicher.
Eine Berechnung besteht dann aus einer Folge von elementaren Aktionen, die immer
wieder


einen Ausdruck auswerten
das Ergebnis speichern
Solche elementaren Aktionen werden Zuweisung genannt. Da der Wert eines
Ausdruckes von den darin enthaltenen Variablen, oder genauer von den in diesen
Variablen gespeicherten Zwischenwerten, abhängig ist, können sich durch
geschickte Kombination von Zuweisungen komplexe Berechnungen ergeben.
Ein Programm enthält somit einfache oder zusammengesetzte Anweisungen an
die Maschine, die insgesamt eine gezielte Veränderung des Speicherzustandes
herbeiführen.
Solche Anweisungen nennt man auch Befehle und spricht von befehlsorientierten
oder imperativen Sprachen.
5.10.1
Zuweisungen
Zuweisungen sind die einfachsten Anweisungen zur Veränderung des Speichers.
Sie bewirken die gezielte Veränderung des Wertes einer einzigen Variablen.
Eine Zuweisung besteht aus einer Variablen v und einem Ausdruck t, die durch ein
Zuweisungszeichen verbunden sind:

v=t
(wir gehen hier davon aus, dass "=" der Zuweisungsoperator ist (wie in Java
oder C++). Das ist aber nicht zwangsläufig der Fall: In PASCAL ist ":=" der
Zuweisungsoperator.)
Eine solche Zuweisung wird ausgeführt, indem der Ausdruck t ausgewertet wird und
der resultierende Wert anschließend in v gespeichert wird.
Dabei wird der alte Wert von v überschrieben (siehe nachfolgendes Beispiel):
92
5.10.2
Kontrollstrukturen
Das Wesen einer imperativen Programmiersprache besteht darin, Folgen von
Anweisungen zu neuen Anweisungen zu gruppieren.
Wie bei Zuweisungen ist der Netto-Effekt solcher zusammengesetzter
Anweisungen eine Veränderung des Speicherinhalts.
Selbst eine Bildschirmausgabe ist letztlich Ausdruck einer Speicherveränderung,
denn sie beruht auf einer Veränderung des Bildschirmspeichers, der im Textmodus
für jede Bildschirmposition ein Zeichen, sowie einen Farb- und Helligkeitswert
enthält.
Die Möglichkeiten, die eine Programmiersprache anbietet, um gezielt und kontrolliert
Anweisungen zu neuen komplexeren und abstrakteren Anweisungen
zusammenzusetzen, nennt man Kontrollstrukturen.
Der Begriff Kontrollstrukturen fasst spezielle Anweisungen zusammen, die der
Steuerung (Kontrolle) des Programmflusses dienen.
Sie ermöglichen es dem Programmierer, nicht nur streng sequentiell ablaufende
Programme zu schreiben, sondern auch solche, in denen Anweisungen unter
bestimmten Bedingungen übersprungen oder wiederholt ausgeführt werden
können, wenn die Logik des betreffenden Programms dies erfordert.
Die Kontrollstrukturen in Java und C++ lassen sich in drei Gruppen einteilen:

Auswahl- oder Selektionsanweisungen
Machen die Ausführung von Anweisungen vom Erfülltsein einer Bedingung
abhängig und reduzieren so die generelle Ausführbarkeit einer Anweisung
auf ihre Ausführbarkeit in lediglich einer bestimmten Auswahl von Fällen.

Wiederholungs- oder Iterationsanweisungen
Gestatten es, eine oder mehrere Anweisungen beliebig oft auszuführen.

Sprung- oder Kontrollanweisungen
Bewirken Sprünge - vorwärts oder rückwärts - im Programm (wonach das
Programm mit der Ausführung der Anweisungen fortfährt, die sich an der
Stelle befinden, zu der gesprungen wurde).
Mit diesen drei Typen von Kontrollstrukturen werden wir uns in den nächsten
Abschnitten beschäftigen.
5.10.2.1
Auswahlanweisungen
Es gibt drei Auswahlanweisungen in Java und C++, die mit den Schlüsselwörtern if,
if else und switch bezeichnet sind.
93
Die if-Anweisung
Die if-Anweisung stellt den einfachsten Fall einer bedingten Anweisung dar: Ist die
Bedingung erfüllt, werden die betreffenden Anweisungen ausgeführt.
Wenn nicht, findet keinerlei Operation statt.
Die if-Anweisung gestattet die Ausführung einer oder mehrer Anweisungen nur
dann, wenn die Überprüfung einer zuvor formulierten Bedingung ergeben hat, dass
diese erfüllt ist.
Gibt es nur eine Anweisung, die von dieser Bedingung abhängig ist, kann man die ifAnweisung mit folgender Syntax ausdrücken:
if (ausdruck)
anweisung
Sollen mehrere Anweisungen von der Bedingung abhängig sein, müssen diese als
Block in geschweiften Klammern notiert werden:
if (ausdruck) {
anweisung1
anweisung2
anweisung3
...
}
Die Bedingung der if-Anweisung ist durch den Syntaxteil ausdruck gegeben:

Ergibt ausdruck den Wahrheitswert TRUE, werden die abhängigen
Anweisungen ausgeführt.

Ist ausdruck FALSE, gelangt keine der abhängigen Anweisungen zur
Ausführung.
94
Abbildung 27: Ist ausdruck TRUE, werden die abhängigen Anweisungen ausgeführt.
Ist ausdruck FALSE, wird das Programm mit der auf die ifAnweisung folgenden Anweisung fortgesetzt.
Wird das untenstehende Programmstück ausgeführt, wird die Meldung "a ist größer
als b" auf den Bildschirm ausgegeben, da der Wert von a in der Tat größer als der
von b ist, und somit die Bedingung a > b = TRUE ausgewertet wird.

int a = 2;
int b = 1;
if ( a > b) cout << "a ist größer als b";
95
Ändert man jedoch den Wert von b mit b = a von 1 in 2, sind die Werte der beiden
Variablen gleichgroß und somit die Bedingung
a > b nicht erfüllt, also FALSE. In diesem Falle unterbleibt die Ausgabe auf den
Bildschirm.
Die if else-Anweisung
Sollen auch, wenn die Bedingung nicht erfüllt ist, bestimmte Operationen ausgeführt
werden, verewndet man die if else-Anweisung, die sich von der einfachen ifAnweisung durch das Vorhandensein konkreter Alternativanweisungen für diesen
Fall unterscheidet.
Es kommt nicht selten vor, dass eine Situation eintritt, in der nicht nur für den Fall,
dass die Bedingung einer if-Anweisung TRUE ist, irgendwelche Operationen
stattfinden sollen, sondern auch dann, wenn die Bedingung FALSE ist.
Die passende Kontrollstruktur für derartige Fälle ist die if else-Anweisung. Gibt es
nur eine abhängige Anweisung, wird folgende Syntax verwendet:
o

Sollen mehrere Anweisungen von der Bedingung abhängig sein, müssen
diese wieder als Block in geschweiften Klammern notiert werden:
o

if (ausdruck)
anweisung
else
anweisung
if (ausdruck) {
anweisung
}
else {
anweisung
}
Ist ausdruck TRUE, werden die Anweisung(en) im so genannten if-Zweig der
if else-Anweisung ausgeführt, andernfalls diejenigen des else-Zweiges
(siehe nächste Abbildung)
Die switch-Anweisung
Die switch-Anweisung realisiert eine so genannte Mehrfachauswahl, bei der
Aktionen nicht nur in einem oder zwei, sondern auch in einer größeren Anzahl von
Fällen stattfinden können.
Die switch-Anweisung realisiert eine Auswahl zwischen beliebig vielen Alternativen
und ist insofern mit einer geschachtelten if else-Anweisung vergleichbar. Diese so
genannte Mehrfach-Auswahl wird gemäß der folgenden Syntax durchgeführt:
96
Der Ausdruck nach dem Schlüsselwort switch muss von ganzzahligem Typ sein
und wird der Reihe nach mit jeder der ebenfalls ganzzahligen und voneinander
verschiedenen Konstanten verglichen, die Bestandteil der so genannten caseLabels im Block der switch-Anweisung sind (ein Label ist eine Art Kennung oder
Markierung für einen bestimmten Programmpunkt).
Stimmt der Wert von ausdruck mit dem Wert einer der case-Konstanten überein,
wird das Programm mit den Anweisungen fortgesetzt, die unmittelbar auf das
entsprechende case-Label folgen, wobei es auch möglich ist, dass ein Label keine
Anweisungen besitzt.
Gibt es keine Übereinstimmung von ausdruck mit einer der case-Konstanten,
werden die Anweisungen nach dem default-Label ausgeführt, sofern ein solches
überhaupt in der betreffenden switch-Anweisung enthalten ist (Die eckigen
Optionalitäts-Klammern in der Syntax bedeuten, dass etwas nicht zwingend
vorgeschrieben ist. Jede switch-Anweisung darf maximal einen default-Zweig
besitzen, der nicht unbedingt als letzter Zweig notiert werden muß, sondern an einer
beliebigen Position im switch-Block stehen kann).
Stimmt ausdruck mit keiner der case-Konstanten überein und gibt es keinen
default-Zweig, wird keine der im switch-Block enthaltenen Anweisungen
ausgeführt und das Programm mit der auf die switch-Anweisung folgenden
Anweisung fortgesetzt.
97
Abbildung 28: Switch-Anweisung
Das nachfolgende Programmfragment soll eine Anwender-Eingabe in der Variablen
direction aufnehmen, die aus einem der Buchstaben l, r, o, u für die Richtungen
links, rechts, oben und unten bestehen darf. Anschließend soll ausgegeben werden,
welche Richtung der Anwender gewählt hat:
98
5.10.2.2
Die Wiederholungsanweisungen
Die Notwendigkeit, bestimmte Verarbeitungsschritte mehrfach hintereinander
auszuführen, ergibt sich bei der Entwicklung von Programmen ausgesprochen
häufig. Ein Programm soll z.B. bis 100 zählen und dabei jeden gezählten Wert
ausgeben.
Natürlich denkt man bei derartigen Aufgaben gewöhnlich nicht daran, einhundert
einzelne Anweisungen zu codieren, sondern vertraut vielmehr stillschweigend darauf,
dass die gewählte Programmiersprache für solche Fälle etwas Passendes bereithält.
In der Tat verfügen Java und C++ über drei Wiederholgungs- oder
Iterationsanweisungen:



die while-Schleife
die for-Schleife
die do while-Schleife
Mit diesen Schleifen lassen sich eine oder mehrere Anweisungen beliebig oft
ausführen, ohne das man diese Anweisungen jedes Mal neu hinschreiben muss.
Wiederholungsanweisungen werden aufgrund ihres zirkulären Ablaufs auch als
Schleifen (loops) bezeichnet.
Allen drei Schleifentypen in Java und C++ ist gemeinsam, dass die (wiederholte)
Ausführung von Anweisungen ähnlich wie bei den Auswahlanweisungen vom
Erfülltsein einer Bedingung abhängt.
Die while-Schleife
Die while-Anweisung hat die folgende Syntax:
while (ausdruck)
anweisung
Sollen mehrere Anweisungen innerhalb einer while-Schleife wiederholt werden, wird
wie üblich mit geschweiften Klammern ein Block gebildet:
while (ausdruck) {
anweisung1
anweisung2
anweisung3
anweisung4
}
Die while-Anweisung führt die in ihr enthaltenen Anweisungen aus, wenn die
Auswertung von ausdruck, also die Bedingung für die Ausführung der Schleife
TRUE ist.
Anschließend wird ausdruck erneut überprüft und die Ausführung der Anweisungen
wiederholt, wenn ausdruck immer noch TRUE ist.
99
Dieses Prozedere von


Überprüfen der Laufbedingung für die Schleife und
anschließendem Ausführen der in der Schleife enthaltenen Anweisungen
wird so lange fortgesetzt, bis die Auswertung von ausdruck nicht mehr TRUE
zurückliefert.
Liefert die Auswertung der Schleifenbedingung FALSE zurück, werden die
Anweisungen in der Schleife nicht (mehr) ausgeführt.
Ergibt bereits die erste Auswertung der Schleifenbedingung den Wert FALSE,
werden die Anweisungen in der Schleife überhaupt nicht ausgeführt!
Die while-Schleife ist also eine so genannte abweisende Schleife, da evtl. die in Ihr
enthaltenen Anweisungen kein einziges Mal ausgeführt werden, wenn die
Bedingung nicht erfüllt ist (siehe Abbildung 29).
Das nachfolgende Programmstück erzeugt die Ausgabe: 1 2 3 4 5 6 7 8 9 10:
Dies kommt auf die folgende Weise zustande:
Da i zunächst den Wert 1 hat, ist die Schleifenbedingung erfüllt (TRUE) und die
Anweisungen im Rumpf der Schleife werde ausgeführt.
Das Resultat ist die Ausgabe des aktuellen Werts von i und die Inkrementierung der
Variablen auf den Wert 2.
Da 2 kleiner ist als 11, ergibt auch die erneute Auswertung der Schleifenbedingung
TRUE.
Nach diesem Prinzip wird die Schleife insgesamt zehnmal durchlaufen.
Danach hat i aufgrund der Inkrementierung im 10. Durchgang den Wert 11 und die
nächste Auswertung der Schleifenbedingung ergibt daher FALSE, da i nun nicht
mehr kleiner ist als 11.
Als Folge davon werden die Anweisungen des Schleifenrumpfs nicht mehr
ausgeführt, die Schleife wird beendet und das Programm fährt mit der auf die whileSchleife folgenden Anweisung fort.
100
Abbildung 29: While-Schleife
Die for-Schleife
Die for-Anweisung hat die folgende Syntax:
for (initialisierung;abbruchbedingung;inkrementierung)
anweisung
Soll mehr als eine Anweisung im Schleifenrumpf iteriert werden, wird wie üblich mit
geschweiften Klammern ein Block gebildet:
101
for (initialisierung;abbruchbedingung;inkrementierung) {
anweisung1
anweisung2
anweisung3
anweisung4
...
}
Die for-Schleife in Java und C++ kann auch als so genannte Zählschleife
bezeichnet werden.
Sie wird meist verwendet, wenn die Anzahl der Schleifendurchläufe im Vorhinein
feststeht, wobei man eine so genannte Laufvariable verwendet, um die Anzahl
der Schleifendurchläufe zu zählen.
Die for-Schleife besteht aus folgenden vier Teilen:
1. aus einem Initialisierungsteil, der vor Betreten der Schleife
ausgeführt wird und in dem die Laufvariable(n) einen Wert
bekommt(en);
2. aus einer Abbruchbedingung, die jedes Mal vor Betreten der Schleife
geprüft wird;
3. aus einem Inkrementierungsteil, der am Ende jedes
Schleifendurchlaufs ausgeführt wird und zum Beispiel die Laufvariable
erhöhen kann;
4. aus dem Schleifenrumpf.
Abbildung 30: For-Schleife
Das folgende Programmstück erzeugt die Ausgabe 12 17 76 99:
102
Das kommt folgendermaßen zustande:
i wird zunächst mit dem Wert 0 initialisiert. Anschließend wird in der Bedingung
überprüft, ob i kleiner ist, als die Länge des Arrays a (Länge = 4).
Ergibt die Auswertung der Bedingung TRUE, wird die Anweisung im Schleifenrumpf
ausgeführt, d.h. der Wert an der Indexposition i des Arrays wird ausgegeben und i
wird inkrementiert (im Schleifenkopf).
Nach diesem Prinzip wird die Schleife insgesamt 4x durchlaufen.
Danach hat i aufgrund der Inkrementierung den Wert 4, ist damit also nicht mehr
kleiner als die Länge des Arrays und somit ergibt die darauf folgende Auswertung der
Schleifenbedingung FALSE.
Als Folge davon werden die Anweisungen des Schleifenrumpfs nicht mehr
ausgeführt, die Schleife endet und das Programm fährt mit der auf die forAnweisung folgenden Anweisung fort.
Die do while-Schleife
Im Unterschied zu den beiden anderen Wiederholungsanweisungen führt die die
do while-Schleife zunächst die in ihr enthaltenen Anweisungen aus und wertet
dann erst die Schleifen-Bedingung aus! Die do while-Schleife ist also eine so
genannte annehmende Schleife.
Sie hat die folgende Syntax:
do
anweisung
while (ausdruck)
Sollen mehrere Anweisungen in der Schleife ausgeführt werden, müssen diese mit
geschweiften Klammern in einem Block geklammert werden:
do {
anweisung1
103
anweisung2
anweisung3
anweisung4
...
}
while (ausdruck)
Der Umstand, dass ausdruck (die Bedingung der Schleife) erst am Ende der do
while-Schleife ausgewertet wird, hat zur Folge, dass die Anweisung(en) in der
Schleife - im Unterschied zur for- oder while-Schleife - in jedem Fall mind. einmal
ausgeführt werden, unabhängig davon, ob ausdruck, d.h. die Bedingung TRUE ist
oder nicht.
Ergibt die Überprüfung von ausdruck den Wert TRUE, werden die Anweisungen in
der Schleife erneut ausgeführt.
Ist der Wert von ausdruck FALSE, endet die Schleife und das Programm fährt mit
der auf die do while-Schleife folgenden
Anweisung fort.
Abbildung 31: Do-While-Schleife
Das folgende Programmstück nimmt zunächst im do-Block in der Variablen x eine
Benutzereingabe auf, addiert dann die Variablen x und y und speichert das
Ergebnis in der Variablen x:
104
Anschließend wird überprüft, ob der Wert von x ungleich 0 ist.
Ist dies der Fall, wird der do-Block erneut ausgeführt.
Nach diesem Prinzip wird der do-Block solange ausgeführt, bis der Benutzer den
Wert 0 eingibt. Dann ist x = 0 und die darauf folgende Auswertung der
Schleifenbedingung ergibt FALSE.
Die Schleife endet und das Programm fährt mit der auf die do while-Schleife
folgenden Anweisung fort.
5.10.2.3
Sprunganweisungen
Sprunganweisungen verlagern die Ausführungskontrolle in einem Programm von
einer Stelle an eine andere (ohne dass dazu eine besondere Bedingung erfüllt sein
müßte).
Die Sprache C++ verfügt über vier Sprunganweisungen, zu denen die break-, die
continue-, die goto- und die return-Anweisung gehören.
In Java existiert die goto-Anweisung nicht.
Mit Ausnahme der return-Anweisung führen Sprunganweisungen nur einen
Transfer der Ausführungskontrolle innerhalb ein- und derselben Funktion durch.
Die return-Anweisung überträgt die Kontrolle von einer aufgerufenen Funktion
zurück an die aufrufende Funktion (oder das Betriebssystem).
Wir werden im folgenden exemplarisch lediglich die break- und die continueAnweisung betrachten.
Die break-Anweisung
Die break-Anweisung kann ausschließlich innerhalb einer switch-Anweisung oder
innerhalb einer der drei Schleifenarten auftreten und hat die folgende Syntax:
break;
Innerhalb eines case-Zweigs einer switch-Anweisung bewirkt die break-Anweisung
das sofortige Ende der switch-Anweisung und den Transfer der
Ausführungskontrolle zu der auf die switch-Anweisung unmittelbar folgende
Anweisung.
Eine break-Anweisung in einer Schleife beendet die Schleife und übergibt die
Programmkontrolle an die Anweisung, die unmittelbar auf die Schleife folgt.
105
In dem obigen Beispiel zur switch-Anweisung bewirkt die break-Anweisung, dass
nach der Ausführung einer case-Anweisung die switch-Anweisung verlassen wird,
ohne die darauf folgenden case-Klauseln auszuführen.
Die continue-Anweisung
Die continue-Anweisung hat die folgende Syntax:
continue;
Sie kann ausschließlich in Wiederholungsanweisungen (Schleifen) verwendet
werden.
Sie bewirkt dort den Abbruch eines einzelnen Schleifendurchgangs (der unmittelbar
umgebenden Schleife).
In einer while- oder do while-Anweisung fährt die Programmausführung danach mit
dem nächsten Schleifendurchgang fort, d.h. genauer: mit der erneuten
Überprüfung der Schleifenbedingung.
In einer for-Schleife wird zuvor noch der Inkrementierungsteil ausgeführt.
while (ausdruck_1) {
if (ausdruck_2) continue;
}
5.11
Datenstrukturen
In der Anfangszeit des Programmierens kam den verwendeten Daten nur eine
zweitrangige Bedeutung zu.
Der Schwerpunkt lag auf der Formulierung sauberer Algorithmen. Daten waren
schlicht Bitfolgen, die von dem Algorithmus manipuliert wurden.
Ein Programmierer hat aber stets eine Interpretation der Bitfolgen, d.h. eine
bestimmte Abstraktion im Auge: Gewisse Bitfolgen entsprechen Zahlen, andere
stellen Wahrheitswerte, Zeichen oder Zeichenketten dar.
Die ersten erfolgreichen höheren Programmiersprachen gestatteten daher eine
Typdeklaration.
Variablen konnten als Integer, Boolean, usw. deklariert werden. Der Compiler
reservierte automatisch den benötigten Speicherplatz und überprüfte auch noch,
dass die auf die Daten angewandten Operationen dem Datentyp angemessen
waren.
Eine Addition etwa einer Integer-Größe zu einem Wahrheitswert wurde als
fehlerhaft zurückgewiesen.
Dies erleichterte es dem Programmierer, Denkfehler früh zu erkennen und zu
beheben.
106
Als Programme größer und unübersichtlicher wurden und daher möglichst in einzelne
Teile zerlegt werden sollten, erschien es sinnvoll, Daten und Operationen, die diese
Daten manipulieren, als Einheit zu sehen und auch als abgeschlossenen
Programmteil formulieren und compilieren zu können.
Ein solcher Programmteil, im Allgemeinen Modul genannt, konnte von einem
Programmierer erstellt werden und dann anderen in compilierter Form zur
Verfügung gestellt werden.
Es stellt sich die Frage, was man den Benutzern des Moduls mitteilen soll.
Damit diese einen Gewinn aus der geleisteten Arbeit ziehen können, sollten sie nicht
mit den Interna des programmierten Moduls belästigt werden, sie sollten nur
wissen, was man damit anfangen kann.
Ein konkretes Beispiel wären Kalenderdaten: Ein Benutzer sollte lediglich wissen,
wie man damit umgehen kann, nicht aber, wie die Kalenderdaten intern repräsentiert
sind.
Aus der Kenntnis der Repräsentation könnte sogar eine Gefahr erwachsen: Eine
Änderung des Moduls könnte Programmen, die von einer speziellen internen
Repräsentation ausgehen, den Garaus machen.
Die Forderung, dem Anwender das (und nur das) mitzuteilen, was das Modul als
Funktionalität anbietet, nicht aber Interna der Implementierung, wird in dem
Schlagwort "information hiding" (oder Kapselung von Information)
zusammengefasst.
In einer neueren Programmiersprache wie Java werden Datenstrukturen und darauf
operierende Methoden in Modulen zusammengefasst, die als Klassen bezeichnet
werden.
Als Datenstruktur definieren wir demnach nicht nur die Daten selbst, sondern auch
die Operationen, die darauf ausgeführt werden können.
Im Folgenden werden wir uns mit einigen der wichtigsten Datenstrukturen
befassen.
5.11.1
Stacks
Ein Stack ist ein abstrakter Datentyp, bei dem Elemente eingefügt und wieder
entfernt werden können.
Derartige Datentypen, die als Behälter für Elemente dienen, fasst man oft unter
Oberbegriffen wie Container oder Collection zusammen.
Unter den Behälter-Datentypen zeichnen sich Stacks dadurch aus, dass immer nur
auf dasjenige Element zugegriffen werden kann, das als letztes eingefügt wurde.
Dafür gibt es das Schlagwort: LIFO = Last-In First-Out.
107
Das englische Wort Stack kann man mit Stapel übersetzen. Dabei liegt es nahe, an
einen Stapel von Tellern oder Tabletts zu denken (--> z.B. die "Tellerspender" in der
Mensa) bei dem man auch immer nur auf das oberste Element zugreifen kann dasjenige also, welches als letztes auf den Stapel gelegt wurde.
In einem Stack s sind Elemente x eines beliebigen, aber festen Datentyps
gespeichert. "Speichern" heißt hier Ablegen auf dem Stack und "Entfernen" heißt
Entnehmen des obersten Elements.
Stackoperationen
Die fundamentalen Stackoperationen, die auf einem Stack s angewendet werden
können, sind (die Bezeichnungen für diese Operationen können je nach
Programmiersprache variieren!):



push(x,s)
legt ein Element x auf den Stack s
top(s)
liefert das zuletzt auf den Stack s gelegte Element
pop(s)
entfernt das zuletzt auf den Stack s gelegte Element
isEmpty(s)
gibt an, ob der Stack s leer ist
Abbildung 32: Stack und Stack-Operationen
Anwendungsbeispiel für einen Stack
Ein wichtiges Anwendungsbeispiel für die Datenstruktur Stack ist die Auswertung
von arithmetischen Ausdrücken.
108
Jeder arithmetische Ausdruck in normaler Schreibweise kann in eine PostfixNotation umgewandelt werden, bei der sich die Operatoren stets rechts von den
Operanden befinden.
Dabei kommt man gänzlich ohne Klammern aus.
Bei einer Auswertung von links nach rechts bezieht sich ein Operator immer auf die
unmittelbar links von ihm stehenden Werte.
Beispielsweise wird aus dem arithmetischen Ausdruck (2+4)2/(16-7) der PostfixAusdruck:
2 4 + 2 16 7 - /
Dieser kann mittels eines Stacks von links nach rechts abgearbeitet werden:
Ist das gelesene Datum ein Operand, so wird es mit push auf den Stack
gelegt.
o Ist das gelesene Datum ein n-stelliger Operator, dann wird er auf die
obersten n Elemente des Stacks angewandt.
Das Ergebnis ersetzt diese n Elemente.
o
Abbildung 33 zeigt die 9 Schritte zur Berechnung des obigen Ausdrucks:
Abbildung 33: Beispiel für Stack-Operationen
Jeder Compiler wandelt Ausdrücke bei der Compilierung in Postfix-Ausdrücke um.
109
5.11.2
Queues
Ähnlich einem Stack ist eine Queue ein Behälter, in den Elemente eingefügt und nur
in einer bestimmten Reihenfolge wieder entnommen werden können.
Einfügung eines Elements - "enQueue" - erfolgt an einem, Entfernung - "deQueue"
- an dem anderen Ende der Queue.
Dies bewirkt, dass man immer nur auf das Element zugreifen kann, das am längsten
im Behälter ist (FIFO = First in-First Out).
Abbildung 34: Queue
Anwendung von Queues
Queues werden oft eingesetzt, um ansonsten unabhängige Prozesse miteinander
kommunizieren zu lassen, bzw. um kooperierende Prozesse zu entkoppeln.
Üblicherweise produziert ein Prozess eine Folge von Daten, während der zweite
Prozess diese Daten entgegennimmt und weiter verarbeitet.
Der erste Prozess heißt dann Erzeuger oder Produzent (engl. producer) und der
zweite heißt Verbraucher oder Konsument (engl. consumer).
Man verwendet eine Queue, in die der Produzent die erzeugten Daten ablegt
(enQueue) und aus dem der Konsument die benötigten Daten entnimmt (deQueue).
Die Queue dient der zeitlichen Entkopplung - der Erzeuger kann weiter
produzieren, auch wenn der Verbraucher die Daten noch nicht alle
entgegengenommen hat, und der Verbraucher hat Daten, mit denen er
weiterarbeiten kann, auch wenn der Erzeuger für die Produktion gewisser Daten
gelegentlich eine längere Zeit benötigt.
Beispiele für eine solche Erzeuger-Verbraucher Situation sind:

Die Druckerwarteschlange unter Windows ist eine Queue für Druckaufträge.
Erzeuger sind die Druckprozesse unterschiedlicher Programme (im Netzwerk)
- Verbraucher ist der Druckertreiber.
Die Queue dient hier zur zeitlichen Entkopplung.
110

Ähnlich legt der Festplatten-Controller als Produzent die gelesenen Daten
in einer Queue ab - dort kann das Betriebssystem sie abholen.
Beim Schreiben auf die Platte vertauschen Produzent und Konsument die
Rollen.

Queues können auch zur logischen Entkopplung von Prozessen dienen. So
gibt es z.B. in UNIX eine Sammlung nützlicher kleiner Programme, welche
einen Input in einen Output transferieren. Diese Tools nennt man Pipes.
Mithilfe einer Pipe, die nichts anderes ist, als eine Queue, kann man die
Ausgabe eines Programms mit der Eingabe des nächsten verknüpfen.
5.11.3
Listen
Eine Liste ist eine Folge von Elementen, in der an beliebiger Stelle neue Elemente
eingefügt oder vorhandene Elemente entfernt werden können.
Im Gegensatz dazu darf man bei Stacks und Queues nur am Anfang oder am Ende
Elemente einfügen oder entnehmen.
Listen sind also allgemeinere Datenstrukturen als Stacks und Queues, in der Tat
kann man sowohl Stacks als auch Queues mithilfe von Listen realisieren.
Die Spezifikation eines abstrakten Datentyps Liste bietet viele
Variationsmöglichkeiten. Es stellen sich unter anderem folgende Fragen:


Welche Operationen sollen dazugehören?
Wie sollen wir uns auf bestimmte Elemente in der Liste beziehen?
Wir entscheiden uns hier dafür, Listenelemente anhand ihrer Position in der Liste
anzusprechen - wenn die Liste n Elemente hat, dann nummerieren wir die Elemente
von 1 bis n durch.
Dazu benötigen wir:



eine Operation laenge, um festzustellen, wie viele Elemente die Liste L hat,
eine Methode Inhalt, die zu einer gültigen Position p mit 1 <= p <= laenge(L)
den Inhalt des Elements an der Position p findet und
eine Operation suche, die die Position p eines Elements mit Inhalt e
bestimmt, sofern es denn ein solches gibt.
Einfach verkettete Listen
Wir implementieren eine Liste als eine Folge von Elementen. Jedes Element wird
zum Teil einer Kette.
Zu diesem Zweck trägt jedes Element nicht nur einen Inhalt e, sondern auch einen
Zeiger next, der auf das folgende Element der Liste verweist.
Manch einem mag die Analogie eines Elements zu einem Eisenbahnwaggon hilfreich
111
sein. Jeder Waggon hat einen Inhalt und eine Kupplung, an die der nächste Waggon
gehängt werden kann.
Auf diese Weise lassen sich beliebig lange Züge - Listen - zusammenstellen:
Abbildung 35: Liste
Um die ganze Liste inspizieren zu können, müssen wir nur ihr erstes Element finden.
Dessen next-Zeiger führt uns auf das folgende Element, dessen next-Zeiger
wiederum auf das dritte und so weiter.
Das letzte Glied der Kette erkennen wir daran, dass sein next-Zeiger auf null
verweist.
Von einem Element aus kann man die Liste immer nur den next-Zeigern folgend in
einer Richtung durchlaufen.
Um danach wieder in die Liste einsteigen zu können, benötigen wir einen festen
Zeiger anfang auf das erste Element der Liste.
Hilfszeiger auf innere Elemente nennt man Cursor. Viele Operationen auf Listen
beginnen an dem durch einen Cursor bezeichneten Element.
Abbildung 36: Einfach verkettete Liste
Doppelt verkettete Listen
In einer einfach verketteten Liste sind die Elemente direkt nach dem Cursor einfach
erreichbar.
Will man allerdings auf das Element vor dem Cursor zugreifen, so muss die Liste
vom Anfang her komplett durchlaufen werden!
Durch die Richtung der Verkettung ergibt sich eine Asymmetrie im Aufwand des
Listendurchlaufes.
Spendiert man jedem Element noch einen Zeiger auf seinen Vorgänger, so stellt
sich für den Aufwand des Listendurchlaufs die Symmetrie wieder her.
Eine solche Implementierung (mit oder ohne Cursor) nennt man doppelt verkettete
Liste.
112
Abbildung 37: Doppelt verkettete Liste
5.12 Bäume
Bäume gehören zu den fundamentalen Datenstrukturen der Informatik. In gewisser
Weise kann man sie als mehrdimensionale Verallgemeinerung von Listen
auffassen.
In Bäumen kann man nicht nur Daten, sondern auch relevante Beziehungen der
Daten untereinander, wie Ordnungs- oder hierarchische Beziehungen, speichern.
Daher eignen sich Bäume besonders, gesuchte Daten rasch wieder aufzufinden.
Ein Baum besteht aus einer Menge von Knoten (Punkten), die untereinander durch
Kanten (Pfeile) verbunden sind.
Führt von Knoten A zu Knoten B eine Kante, so schreiben wir dies als A --> B und
sagen A ist Vater/Mutter von B oder B ist Sohn/Tochter von A.
Einen Knoten ohne Söhne nennt man ein Blatt oder terminalen Knoten. Alle
anderen Knoten heißen innere Knoten.
Ein Pfad von A nach B ist eine Folge von Knoten und Kanten, die von A nach B
führen:
A --> X1 --> X2 --> ... --> B
Die Länge des Pfades definieren wir als die Anzahl der Knoten (0 oder mehr). Gibt
es einen Pfad von A nach B, so ist B ein Nachkomme von A und A ein Vorfahre
von B.
Ein Baum muss folgende Bedingung erfüllen:


Es gibt genau einen Knoten ohne Vater. Dieser Knoten heißt Wurzel (root).
Jeder andere Knoten ist ein Nachkomme der Wurzel und hat genau einen
Vater.
113

Weitere wichtige Eigenschaften eines Baumes ergeben sich hieraus
automatisch. Dazu gehören:
o
o
o

Es gibt keinen zyklischen Pfad.
Von der Wurzel gibt es zu jedem anderen Knoten genau einen Pfad.
Die Nachkommen eines beliebigen Knotens K zusammen mit allen
ererbten Kanten bilden wiederum einen Baum, den Unterbaum mit
Wurzel K.
Letzteres ist eine wichtige kennzeichnende Eigenschaft, die zeigt, dass
Bäume rekursiv definiert werden können:
Rekursive Baumdefinition:
Ein Baum ist leer oder er besteht aus einer Wurzel W und einer leeren oder
nichtleeren Liste B1, B2, ..., Bn von Bäumen. Von W zur Wurzel Wi von Bi führt jeweils
eine Kante.
Hinweis:
Ein Objekt (oder eine Definition) heißt rekursiv, wenn es sich selbst als Teil enthält
oder mithilfe von sich selbst definiert ist.
Da Bäume hierarchische Strukturen sind, lässt sich jedem Knoten eine Tiefe
zuordnen.
Diese definieren wir als die Länge des Pfades von der Wurzel zu diesem Knoten.
Die Tiefe eines Baumes definieren wir als 0, falls es sich um einen leeren Baum
handelt, andernfalls als das Maximum der Tiefen seiner Knoten.
Abbildung 38: Baum als Datenstruktur
Beispiele für Bäume
Viele natürliche hierarchische Strukturen sind Bäume. Dazu gehören unter anderem:
114

Stammbäume
Knoten sind Frauen, Kanten führen von einer Mutter zu jeder ihrer Töchter.
Die Wurzel (im abendländischen Kulturkreis) ist Eva.

Dateibäume
Knoten sind Dateien oder Verzeichnisse, Kanten führen von einem
Verzeichnis zu dessen direkten Unterverzeichnissen oder Unterdateien. Die
Wurzel heißt häufig C: oder "/".

Listen
Listen sind Bäume, bei denen jeder Knoten höchstens einen Nachfolger hat.
Bäume stellt man gewöhnlich grafisch dar, indem jeder Knoten durch einen Kreis und
jede Kante durch eine Linie dargestellt wird.
Dabei wird ein Vater-Knoten immer über seinen Söhnen platziert, so dass die
Wurzel der höchste Punkt ist.
5.12.1
Binärbäume
Im Allgemeinen können Baumknoten mehrere Söhne haben. Ein Binärbaum ist
dadurch charakterisiert, dass jeder Knoten genau zwei Söhne hat. Eine rekursive
Definition ist:
Ein Binärbaum ist


leer oder
besteht aus einem Knoten - Wurzel genannt - und zwei Binärbäumen, dem
linken und dem rechten Teilbaum.
Abbildung 39: Binärbaum (I)
Binärbäume sind also 2-dimensionale Verallgemeinerungen von Listen.
Ähnlich wie die Elemente einer Liste kann man in den Knoten eines Baumes
beliebige Informationen speichern.
Im Unterschied zu den Listenelementen enthält ein Knoten eines Binärbaumes zwei
115
Zeiger, einen zum linken und einen zum rechten Teilbaum.
Abbildung 40: Binärbaum (II)
Eine wichtige Anwendung von Bäumen, insbesondere auch von Binärbäumen, ist
die Repräsentation von arithmetischen Ausdrücken.
Innere Knoten enthalten Operatoren, Blätter enthalten Werte oder
Variablennamen.
Einstellige Operatoren werden als Knoten mit nur einem nichtleeren Teilbaum
repräsentiert.
In der Baumdarstellung sind Klammern und Präzedenzregeln überflüssig. Erst
wenn wir einen "zweidimensionalen" Baum eindimensional (als String) darstellen
wollen, sind Klammern notwendig.
Beachten Sie, dass das Vorzeichen "-" und das Quadrieren einstellige Operatoren
sind. Im Baum stellen wir sie durch "+/-" bzw. ()2 dar:
Abbildung 41: Binärbaum zur Repräsentation eines arithmetischen Ausdruckes
116
5.12.2
Traversierungen
Listen konnten wir auf natürliche Weise von vorne nach hinten durchlaufen - bei
Bäumen können wir ähnlich einfach von der Wurzel zu jedem beliebigen Knoten
gelangen, sofern wir seine "Baumadresse" kennen.
Um den Vorgänger eines Elementes e in einer Liste zu finden, mußten wir am
Anfang einsteigen und nach rechts laufen, bis wir zu einem Element z gelangten für
das galt:
z.next = e.
Um in einem Baum den Vater eines bestimmten Knotens zu finden, haben wir es
schwerer - wir müssen an der Wurzel einsteigen und in jedem Schritt raten, ob wir
nach rechts oder nach links gehen sollen. Schlimmstenfalls müssen wir den ganzen
Baum durchsuchen.
Spätestens hier erhebt sich die Frage, wie wir systematisch alle Knoten eines
Baumes durchlaufen - oder traversieren - können.
Traversieren bedeutet, jeden Knoten eines Baumes systematisch zu besuchen.
Diese Operationen sind für lineare Listen aufgrund ihrer Definition trivial, doch für
Bäume gibt es eine Reihe verschiedener Möglichkeiten.
Diese unterscheiden sich vor allem hinsichtlich der Reihenfolge, in der die Knoten
besucht werden. Die wichtigsten Vorgehensweisen sind Pre-Order, Post-Order, InOrder und Level-Order, auf die hier aber nicht detailliert eingegangen werden soll.
5.13 Suchalgorithmen
Gegeben sei eine Sammlung von Daten. Wir suchen nach einem oder mehreren
Datensätzen mit einer bestimmten Eigenschaft. Dieses Problem stellt sich zum
Beispiel, wenn wir im Telefonbuch die Nummer eines Teilnehmers suchen.
Zur raschen Suche nutzen wir aus, dass die Einträge geordnet sind, z.B. nach Name,
Vorname, Adresse.
Wenn wir Namen und Vornamen wissen, finden wir den Eintrag von Herrn Sommer
sehr schnell durch binäres Suchen:

Dazu schlagen wir das Telefonbuch in der Mitte auf und vergleichen den
gesuchten Namen mit einem Namen auf der aufgeschlagenen Seite.
Ist dieser zufällig gleich dem gesuchten Namen, so sind wir fertig.
Ist er in der alphabetischen Ordnung größer als der gesuchte Name, brauchen
wir für den Rest der Suche nur noch die erste Hälfte des Telefonbuchs zu
berücksichtigen. Wir verfahren danach weiter wie vorher, schlagen also bei
der Mitte der ersten Hälfte auf und vergleichen wieden den gesuchten mit
einem gefundenen Namen und so fort.
117
Dieser Algorithmus heißt binäre Suche. Bei einem Telefonbuch mit ca. 1000
Seiten Umfang kommen wir damit nach höchstens 10 Schritten zum Ziel, bei
einem Telefonbuch mit 2000 Seiten nach 11 Schritten.
Noch schneller geht es, wenn wir die Anfangsbuchstabenmarkierung auf dem
Rand des Telefonbuchs ausnutzen.
Wenn wir umgekehrt eine Telefonnummer haben und mithilfe des Telefonbuchs
herausbekommen wollen, welcher Teilnehmer diese Nummer hat, bleibt uns nichts
anderes übrig, als der Reihe nach alle Einträge zu durchsuchen. Diese Methode
heißt lineare Suche.
Bei einem Telefonbuch mit 1000 Nummern müssen wir im Schnitt 500 Vergleiche
durchführen, im schlimmsten Fall sogar alle 1000.
Müssen wir diese Art von Suche öfters durchführen, so empfiehlt sich eine
zusätzliche Sortierung (einer Kopie) des Telefonbuchs nach der Rufnummer, so dass
wir wieder binär suchen können.
5.13.1
Lineare Suche
Allgemein lässt sich das Suchproblem wie folgt formulieren:
Suchproblem:
In einem Behälter A befindet sich eine Reihe von Elementen. Prüfe, ob Element e in
A existiert, das eine bestimmte Eigenschaft p(e) besitzt.
"Behälter" steht hier allgemein für Strukturen wie: Arrays, Dateien, Mengen, Listen,
Stacks, Queues, etc.
Wenn nichts näheres über die Struktur des Behälters oder die Platzierung/Sortierung
der Elemente bekannt ist, dann müssen wir den folgenden Algorithmus anwenden:
Entferne der Reihe nach Elemente aus dem Behälter, bis dieser leer ist oder ein
Element mit der gesuchten Eigenschaft gefunden wurde.
Schlimmstenfalls müssen wir den ganzen Behälter durchsuchen, bis wir das
gewünschte Element finden. Hat dieser N Elemente, so werden wir bei einer
zufälligen Verteilung der Daten erwarten, nach ca. N/2 Versuchen das gesuchte
Element gefunden zu haben.
In jedem Fall ist die Anzahl der Zugriffe proportional zur Elementzahl.

Arrays als Behälter werden in den folgenden Such- und Sortier-Algorithmen
eine besondere Rolle spielen. In Java ist die Indexmenge eines Arrays a mit n
= a.length Elementen stets das Intervall [0...n-1].
Besonders für die rekursiven Algorithmen wird es sich als günstig
herausstellen, wenn wir sie so verallgemeinern, dass sie nicht nur ein ganzes
Array, sondern auch einen Abschnitt (engl. slice) eines Arrays sortieren
können.
118
5.13.2
Binäre Suche
Wenn auf dem Element-Datentyp eine Ordnung definiert ist und die Elemente
entsprechend ihrer Ordnung in einem Array gespeichert sind, dann nennt man das
Array geordnet oder sortiert.
Für die Suche in solchen sortierten Arrays können wir die binäre Suche einsetzen,
wie wir sie aus dem Telefonbuch-Beispiel kennen.
Dazu sei x das gesuchte Element. Wir suchen also nach einem Index i mit a[i] = x.
Wie bei der Namenssuche im Telefonbuch wollen wir den Bereich, in dem sich das
gesuchte Element noch befinden kann, in jedem Schritt auf die Hälfte verkleinern.
Dazu verallgemeinern wir das Problem dahingehend, dass wir i in einem beliebigen
Indexbereich [min...max] des Arrays a suchen, angefangen mit min = 0 und max =
n-1.
Mit anderen Worten: Wenn das gesuchte Element x überhaupt in dem Array
vorhanden ist, dann muss es (auch) im Abschnitt [min...max] zu finden sein.
Davon ausgehend, dass das gesuchte Element überhaupt in a enthalten ist, wählen
wir im ersten Schritt einen Index m, mit min ≤ m ≤ max. Anschließend gehen wir
folgendermaßen vor:



Falls x = a[m] gilt, sind wir fertig; Wir geben m als Ergebnis zurück.
falls x < a[m], suche weiter im Bereich min...m-1, setze also max = m-1
falls x > a[m], suche weiter im Bereich m+1...max, setze also min = m + 1
Für den Index m zwischen min und max nimmt man am besten einen Wert nahe der
Mitte, also z.B. m = (min + max) / 2.
Auf diese Weise halbiert sich in jedem Schritt der noch zu betrachtende Bereich, und
damit der Aufwand für die Lösung des Problems. Wenn der Bereich min...max aus n
Elementen besteht, können wir den Bereich höchstens log2(n)-mal halbieren. Um
1000 Elemente zu durchsuchen, genügen also log2(1000) = 10 Vergleiche. Wir
illustrieren die Methode mit 19 Elementen:
Abbildung 42: Binäre Suche
119
5.14 Komplexität von Algorithmen
Unter der Komplexität eines Algorithmus versteht man grob seinen Bedarf an
Ressourcen in Abhängigkeit vom Umfang der Inputdaten.
Die wichtigsten Ressourcen sind dabei die Laufzeit und der Speicherplatz.
Lineare Suche
Wenn wir davon ausgehen, dass die lineare Suche mit einer while-Schleife
bewerkstelligt wird, hängt der Zeitbedarf im Wesentlichen davon ab, wie oft die whileSchleife durchlaufen wird. Dabei nehmen wir an, dass das gesuchte Element
vorhanden ist, und unterscheiden drei mögliche Fälle:

Best case:
Im günstigsten Fall wird ein Element e mit p(e) beim ersten Versuch
gefunden. Die Schleife wird nur einmal durchlaufen.

Average case:
Im Schnitt kann man davon ausgehen, dass das Element etwa nach der
halben Maximalzahl von Schleifendurchläufen gefunden wird.

Worst case:
Im schlimmsten Fall wird das Element erst beim letzten Versuch oder
überhaupt nicht gefunden. Die Maximalzahl der Schleifendurchläufe ist durch
die Anzahl der Elemente begrenzt.
Binäre Suche
Bei der binären Suche benötigen wir im worst case log2(n) Schleifendurchläufe,
bzw. rekursive Aufrufe - im average case kann man sich überlegen, dass man im
vorletzten Schritt, also nach
log2(n)-1 Schritten erwarten kann, das Element zu finden. Wir können die Anzahl der
Schleifendurchläufe also folgendermaßen tabellieren:
lineare Suche
binäre Suche
Best Case
1
1
Average Case
n
log2(n)-1
Worst Case
n
log2(n)
Der genaue Zeitaufwand im worst case für die lineare Suche in einem Behälter mit
n Elementen setzt sich zusammen aus einer Initialisierungszeit CI und aus dem
Zeitbedarf für die while-Schleife, den wir mit Cw x n ansetzen können, wobei Cw die
Zeitdauer eines Schleifendurchlaufes bedeutet. Wir erhalten für den Zeitbedarf tL(n)
der linearen Suche also die Formel:
tL(n) = CI + CW * n

Zum Vergleich berechnen wir den Zeitbedarf tB(n) für die binäre Suche. Auch
hier haben wir eine konstante Initialisierungszeit KI und eine konstante Zeit KW
für jeden Schleifendurchlauf, bzw. für jeden rekursiven Aufruf. Dies ergibt:
tB(n) = KI + KW * log2(n)
120
Offensichtlich ist die binäre Suche schneller - egal welchen Wert die einzelnen
Konstanten haben. Wenn n nur groß genug ist, wird tL(n) größer als tB(n) sein.
Allgemein interessiert uns beim Laufzeitvergleich verschiedener Algorithmen nur das
Verhalten "für große" n. Auf diese Weise können wir die Güte von Algorithmen
beurteilen, ohne die genauen Werte der beteiligten Konstanten zu kennen.
5.15 Sortieralgorithmen
Viele Sortieralgorithmen übernehmen Strategien, die Menschen bereits im täglichen
Leben anwenden - zum Beispiel beim Sortieren von Spielkarten. Wenn wir
Kartenspieler beim aufnehmen einer "Hand" beobachten, können wir unter anderem
folgende "Algorithmen" beobachten:

Der Spieler nimmt eine Karte nach der anderen auf und sortiert sie in die
bereits aufgenommenen Karten ein. Dieser Algorithmus wird InsertionSort
genannt.

Der Spieler nimmt alle Karten auf, macht eine Hand daraus und fängt jetzt an,
die Hand zu sortieren, indem er benachbarte Karten solange vertauscht, bis
alle in der richtigen Reihenfolge liegen. Dieser Algorithmus wird BubbleSort
genannt.

Bei Kartenspielen, bei denen die Karten zunächst aufgedeckt auf dem Tisch
liegen können: Der Spieler nimmt die jeweils niedrigste der auf dem Tisch
verbliebenen Karten auf und kann sie in der Hand links (oder rechts) an die
bereits aufgenommenen Karten anfügen. Dieser Algorithmus wird
SelectionSort genannt.
Die oben genannten Algorithmen gehören zu den einfachen Sortierstrategien. Es gibt
aber noch schnellere Algorithmen, die zudem auf eleganten mathematischen Ideen
beruhen. Dazu gehören HeapSort, QuickSort und MergeSort mit einer Laufzeit von
n*log(n).
Aufgrund der Komplexität dieser Algorithmen werden wir uns im Folgenden auf
BubbleSort als Beispiel eines Sortieralgorithmus beschränken.
Bei Interesse an anderen Sortieralgorithmen sei hier auf die Literaturangaben zum
Thema verwiesen, besonders auf: Sedgewick, Robert: Algorithmen. AddisonWesley, 2002.
5.15.1
BubbleSort
Dieser Algorithmus sortiert z.B. ein Array von Datensätzen durch wiederholtes
Vertauschen von Nachbarfeldern, die in falscher Reihenfolge stehen.
Dies wiederholt man so lange, bis das Array vollständig sortiert ist. Dabei wird das
Array in mehreren Durchgängen von links nach rechts durchwandert. Bei jedem
Durchgang werden alle Nachbarfelder verglichen und ggf. vertauscht. Nach dem 1.
Durchgang hat man folgende Situation:

Das größte Element ist ganz rechts.
121

Alle anderen Elemente sind zwar zum Teil an besseren Positionen (also näher
an der endgültigen Position), im Allgemeinen aber noch unsortiert.
Die folgende Abbildung illustriert den ersten Durchlauf:
Abbildung 43: Erster Durchlauf beim Sortieralgorithmus BubbleSort
Das Wandern des größten Elements ganz nach rechts kann man mit dem
aufsteigen von Luftblasen in einem Aquarium vergleichen: Die größte Luftblase
(engl. bubble) steigt nach oben (bzw. nach rechts ;o).
Nach dem ersten Durchgang ist das größte Element also an seiner endgültigen
Position.
Für die restlichen Elemente müssen wir nun den gleichen Vorgang anwenden.
Nach dem zweiten Durchgang ist auch das zweitgrößte Element an seiner
endgültigen Position.
Dies wiederholt sich für alle restlichen Elemente mit Ausnahme des letzten.
In unserem konkreten Beispiel sind spätestens nach 14 Durchgängen alle Elemente
an ihrer endgültigen Position, folglich ist das Array geordnet.
Wenn wir uns das Ergebnis der einzelnen Durchgänge anschauen, stellen wir fest,
daß der Sortiervorgang nicht erst nach dem 14ten Durchgang beendet ist, sondern
bereits nach dem 10ten. Dies liegt daran, dass sich, wie oben schon erwähnt, bei
jedem Durchlauf auch die Position der noch nicht endgültig sortierten Elemente
verbessert.
Wir können zwar den ungünstigsten Fall konstruieren, bei dem tatsächlich n-1
122
Durchgänge benötigt werden, im Allgemeinen können wir aber BubbleSort bereits
nach einer geringeren Anzahl von Durchgängen abbrechen - im günstigsten Fall,
wenn die Daten bereits nach dem 1. Durchgang sortiert sind.
Abbildung 43: Vollständiger Durchlauf beim Sortieralgorithmus BubbleSort
123
Herunterladen