FH D Gliederung - fbi.h

Werbung
h_da
Grundlagen der Informatik für ET
Prof. Dr. J.Wietzke
Email
Tel
Fax
Post
[email protected]
+49 (6151) 16-8487
+49 (6151) 16-8935
Haardtring 100, 64295 Darmstadt
Sprechstunde
siehe Homepage
Gekürzte Fassung
15.10.2007
1
Inhaltsverzeichnis
Inhaltsverzeichnis...................................................................................................................... 2
Bildverzeichnis.......................................................................................................................... 7
1
Überblick......................................................................................................................... 15
1.1
Ziele......................................................................................................................... 15
1.2
Organisation der Veranstaltung............................................................................... 17
1.2.1
Vorlesung ........................................................................................................ 18
1.2.2
Praktikum ........................................................................................................ 18
1.2.3
Folgende Regeln gelten für die Praktika:........................................................ 18
1.2.4
Klausur ............................................................................................................ 19
1.2.5
Persönliche Selbstorganisation........................................................................ 19
1.2.6
Skript der Veranstaltung.................................................................................. 19
2
Einführung....................................................................................................................... 22
2.1
Computer, Dateien, Programme.............................................................................. 22
2.1.1
Grundbausteine eines Computers.................................................................... 22
2.1.2
Prozessor ......................................................................................................... 23
2.1.3
Hauptspeicher.................................................................................................. 23
2.1.4
Plattenspeicher ................................................................................................ 23
2.1.5
Externe Geräte................................................................................................. 24
2.1.6
Dateien und Programme.................................................................................. 24
2.1.7
Das Betriebssystem ......................................................................................... 24
2.1.8
Eingabe von Kommandos ............................................................................... 24
2.2
Algorithmen und Programme.................................................................................. 25
2.2.1
Maschinensprache und Maschinenprogramme ............................................... 25
2.2.2
Höhere Programmiersprache........................................................................... 26
2.2.3
Zusammenfassung: Algorithmus, Programm, Compiler................................. 26
2.3
Zahlendarstellungen ................................................................................................ 26
2.3.1
Hornerschema.................................................................................................. 27
2.4
Übersicht zu C und C++ ......................................................................................... 28
2.4.1
C ...................................................................................................................... 28
2.4.2
C++.................................................................................................................. 28
3
Erstellung eines Programmes .......................................................................................... 30
3.1
Programme: Analyse, Entwurf, Codierung ............................................................. 30
3.1.1
Analyse............................................................................................................ 30
3.1.2
Entwurf............................................................................................................ 30
3.1.3
Codierung ........................................................................................................ 30
3.1.4
Der Editor........................................................................................................ 32
3.1.5
Compiler.......................................................................................................... 32
3.1.6
Linker .............................................................................................................. 32
3.1.7
Debugger ......................................................................................................... 32
3.2
Form eines C-Programms........................................................................................ 33
3.2.1
Schlüsselwörter in ANSI-C............................................................................. 33
3.2.2
Datentypen ...................................................................................................... 33
3.2.3
Allgemeine Form eines C Programms ............................................................ 34
3.2.4
Erstes Beispiel Hello_world............................................................................ 34
3.3
Wie starte ich ein erstes C/C++ Programm unter GCC .......................................... 35
3.3.1
Fehlermeldungen des Compilers..................................................................... 35
3.3.2
Compiler-Optionen.......................................................................................... 35
3.3.3
Syntaktische Fehler – Verstöße gegen die Regeln der Programmiersprache.. 35
2
4
5
6
7
3.3.4
Semantische Fehler – Inhaltliche Fehler in formal korrekten Programmen ... 36
3.3.5
Informationen finden....................................................................................... 38
3.4
Wie starte ich ein erstes Programm unter VC++ Express? ..................................... 39
3.5
Beispiele .................................................................................................................. 46
3.5.1
Beispiel Maßeinheiten Umwandlung .............................................................. 46
3.5.2
Variablen, Rechnen und Ein-/Ausgabe ........................................................... 47
Variablen ......................................................................................................................... 48
4.1
Namen ..................................................................................................................... 48
4.2
Variablen und Konstanten....................................................................................... 48
4.3
Variablendeklaration ............................................................................................... 49
4.3.1
Lokale Variablen ............................................................................................. 49
4.3.2
Formale Parameter .......................................................................................... 50
4.3.3
Globale Variablen ........................................................................................... 51
4.3.4
Konstanten, Const-Deklaration (Zugriffs Modifier) ....................................... 51
4.3.5
Volatile modifier ............................................................................................. 53
4.4
Speicherbedarf und Wertebereich ........................................................................... 53
4.5
Deklaration, Definition und Initialisierung ............................................................. 55
4.6
Storage type Bezeichner.......................................................................................... 56
4.6.1
Extern .............................................................................................................. 56
4.6.2
Lokale Static Variablen................................................................................... 57
4.6.3
Globale static Variablen .................................................................................. 57
4.6.4
Register Variablen........................................................................................... 57
Ein- und Ausgabe in C/C++ ........................................................................................... 58
5.1
Formatierte Ausgabe ............................................................................................... 59
Kontrollstrukturen ........................................................................................................... 62
6.1
Anweisungen und Blöcke........................................................................................ 62
6.1.1
Sequenz ........................................................................................................... 62
6.2
Selektionsanweisungen, Verzweigungen ................................................................ 63
6.2.1
if - else............................................................................................................. 63
6.2.2
else – if ............................................................................................................ 66
6.2.2.1 Fehlerquelle in Verbindung mit if............................................................... 67
6.2.3
Bedingte Bewertung........................................................................................ 67
6.2.4
switch - case .................................................................................................... 68
6.3
Schleifen, Iterationsanweisungen............................................................................ 71
6.3.1
while Schleifen................................................................................................ 71
6.3.2
do while Schleifen........................................................................................... 73
6.3.3
for Schleifen .................................................................................................... 77
6.3.3.1 Variablendeklaration innerhalb von Selektions/Iterationsanweisungen ..... 78
6.3.4
Äquivalenz von for und while......................................................................... 79
6.3.5
Kommaoperator............................................................................................... 79
6.4
Sprünge.................................................................................................................... 80
6.4.1
break ................................................................................................................ 80
6.4.2
continue ........................................................................................................... 81
6.4.3
goto.................................................................................................................. 82
6.4.4
exit() ................................................................................................................ 83
6.5
Ausnahmebehandlung (exception).......................................................................... 83
6.5.1
Grundidee der Ausnahmebehandlung ............................................................. 83
6.5.2
Ablauf im Fehlerfall ........................................................................................ 84
6.5.3
Ausnahmebehandlung unterschiedlichen Typs ............................................... 85
Operatoren....................................................................................................................... 87
7.1
Zuweisungen, Speicherzellen und Werte ................................................................ 87
3
7.2
Mehrfachzuweisungen ............................................................................................ 89
7.3
Implizite Typkonvertierung in Zuweisungen .......................................................... 89
7.4
Explizite Typkonvertierung..................................................................................... 90
7.5
Vorrangregeln.......................................................................................................... 91
7.6
Reihenfolge ............................................................................................................. 92
7.7
Wahrheitswerte........................................................................................................ 92
7.8
Operatoren, die R-Werte liefern.............................................................................. 94
7.8.1
Arithmetische Operatoren ............................................................................... 94
7.8.2
Vergleichsoperatoren, relationale ................................................................... 95
7.8.3
Logische Operatoren ....................................................................................... 96
7.8.4
Bitoperatoren................................................................................................... 97
7.8.5
Referenzen....................................................................................................... 99
7.9
der ? Operator.......................................................................................................... 99
7.10 Zeigeroperatoren & und * ..................................................................................... 100
7.11 Compile Time operator sizeof............................................................................... 100
7.12 Kommaoperator..................................................................................................... 101
7.13 Punkt- und Pfeiloperatoren.................................................................................... 101
7.14 Operatoren [] und ()............................................................................................... 101
7.15 Bibliotheksfunktionen ........................................................................................... 101
8
Benutzerdef. und zusammengesetzte Datentypen ......................................................... 103
8.1
Aufzählungstypen, Enumerationstypen ................................................................ 103
8.2
Arrays .................................................................................................................... 104
8.2.1
Mehrdimensionale Felder.............................................................................. 107
8.2.2............................................................................................................................... 109
8.2.3
Sortieren in Feldern (Bubble Sort) ................................................................ 109
8.2.4
Einen Zeiger auf ein Array erzeugen ............................................................ 111
8.2.5
Zeichenketten und Strings............................................................................. 111
8.3
Strukturen .............................................................................................................. 114
9
Funktionen..................................................................................................................... 117
9.1
Eine Funktion ........................................................................................................ 117
9.2
Grundlagen ............................................................................................................ 118
9.3
Gültigkeitsbereich und Sichtbarkeit ...................................................................... 121
9.4
Parameterübergabemechanismen .......................................................................... 123
9.4.1
Wertübergabe (engl.: call by value) allgemein ............................................. 123
9.4.2
Adressübergabe (Variablenüberg., engl.: call by reference) allg. ................. 124
9.4.3
Namensübergabe (engl.: call by name) allgemein ........................................ 124
9.4.4
Wertübergabe konkret ................................................................................... 124
9.4.5
Referenzübergabe konkret............................................................................. 126
9.4.6
Referenz als Funktionsergebnis .................................................................... 128
9.4.7
Arrays als Parameter ..................................................................................... 128
9.4.7.1 Eindimensionale Arrays ............................................................................ 129
9.4.7.2 Mehrdimensionale Arrays ......................................................................... 129
9.4.8
Variable Anzahl Parameter, Default Parameter ............................................ 130
9.4.9
static Variablen in Funktionen ...................................................................... 131
9.4.10
Seiteneffekte.................................................................................................. 133
9.5
Rekursive Funktionen ........................................................................................... 133
9.5.1
Überladen von Funktionen ............................................................................ 136
9.6
Funktion main...................................................................................................... 137
9.6.1
Argumente aus der Kommandozeile ............................................................. 138
9.7
Ausnahmebehandlung über Funktionsgrenzen ..................................................... 140
9.7.1
Die Funktion als Vertrag ............................................................................... 141
4
9.8
Dokumentation und Testen ................................................................................... 142
9.8.1.1 Testen ........................................................................................................ 143
9.9
Standardfunktionen/Bibliotheken.......................................................................... 143
10
Modulare Gestaltung von Programmen .................................................................... 144
10.1.1
Dateiübergreifende Gültigkeit und Sichtbarkeit ........................................... 147
10.1.2
Compilerdirektive und Makros ..................................................................... 149
10.1.2.1
#include ................................................................................................. 149
10.1.2.2
#ifdef, #ifndef, #define.......................................................................... 149
10.1.2.3
Makros................................................................................................... 151
11
Zeiger ........................................................................................................................ 154
11.1 Zeiger und Adressen.............................................................................................. 154
11.2 Zeiger und die Argumente von Funktionen .......................................................... 158
11.3 Zeiger und Arrays.................................................................................................. 162
11.3.1
Unterschied Arrayname und Zeiger .............................................................. 164
11.3.2
Arrays als Parameter ..................................................................................... 165
11.3.3
Hörsaalübung: ............................................................................................... 166
11.4 Zeigerarithmetik .................................................................................................... 167
11.5 Arrays von Zeigern................................................................................................ 169
12
Ein- und Ausgabe ...................................................................................................... 170
12.1 Standardein- und Ausgabe .................................................................................... 170
12.2 Eingabe.................................................................................................................. 170
12.2.1
Umleitung der Ein/Ausgabe .......................................................................... 172
12.3 Ein- und Ausgabe mit Dateien .............................................................................. 172
12.4 Binärdateien .......................................................................................................... 177
13
Dynamische Datenobjekte......................................................................................... 179
13.1 Erzeugung von dynamische Datenobjekten .......................................................... 179
13.2 Freigabe von dynamisch erzeugten Datenobjekten............................................... 183
13.3 Rekursive dynamische Strukturen......................................................................... 184
13.3.1
Verkettete Listen ........................................................................................... 185
14
Typedef...................................................................................................................... 191
15
Die Idee der Objektorientierung................................................................................ 192
16
Abstrakte Datentypen................................................................................................ 194
16.1 Einleitung .............................................................................................................. 194
17
Klasse und Objekt ..................................................................................................... 199
18
Konstruktoren............................................................................................................ 204
18.1 Standardkonstruktor .............................................................................................. 204
18.2 Allgemeine Konstruktoren .................................................................................... 205
18.3 Initialisierung mit Listen ....................................................................................... 206
18.4 Zuweisungsoperator .............................................................................................. 209
19
Destruktoren .............................................................................................................. 210
20
Konstante Objekte und Methoden............................................................................. 213
21
Vererbung.................................................................................................................. 217
21.1 Überblick............................................................................................................... 217
21.2 Die Klasse Ort ....................................................................................................... 221
21.3 Zugriffsschutz........................................................................................................ 229
21.4 Typumwandlung Basisklasse – abgeleitete Klasse ............................................... 232
21.5 Überschreiben von Funktionen ............................................................................. 233
21.6 Stack ...................................................................................................................... 234
21.7 Queues (Schlangen)............................................................................................... 240
22
Klassenspezifische Daten und Funktionen................................................................ 249
22.1 Klassenspezifische Konstanten ............................................................................. 253
5
23
Polymorphismus........................................................................................................ 255
23.1 Virtuelle Funktionen ............................................................................................. 255
23.1.1
Verhalten nicht virtueller Funktionen ........................................................... 255
23.1.2
Verhalten virtueller Funktionen .................................................................... 256
24
Abstrakte Klassen...................................................................................................... 259
24.1 Virtuelle Destruktoren........................................................................................... 264
24.2 Virtuelle Basisklassen ........................................................................................... 271
25
Anhang: Kleine Einführung, wie Applikationen mit Win-MFC verbunden werden
(Visual Studio 2005) ............................................................................................................. 272
26
Anhang: Kleine Einführung, wie Applikationen mit Win-MFC verbunden werden
(Visual Express) .................................................................................................................... 287
6
Bildverzeichnis
Bild 1.
Bild 2.
Bild 3.
Bild 4.
Bild 5.
Bild 6.
Bild 7.
Bild 8.
Bild 9.
Bild 10.
Bild 11.
Bild 12.
Bild 13.
Bild 14.
Bild 15.
Bild 16.
Bild 17.
Bild 18.
Bild 19.
Bild 20.
Bild 21.
Bild 22.
Bild 23.
Bild 24.
Bild 25.
Bild 26.
Bild 27.
Bild 28.
Bild 29.
Bild 30.
Bild 31.
Bild 32.
Bild 33.
Bild 34.
Bild 35.
Bild 36.
Bild 37.
Bild 38.
Bild 39.
Bild 40.
Bild 41.
Bild 42.
Bild 43.
Bild 44.
Bild 45.
Bild 46.
Bild 47.
Bild 48.
Blockbild eines Rechners ........................................................................................ 23
Entwicklung von C++ ............................................................................................. 29
Der Entwicklungsprozeß ......................................................................................... 31
Erstellung eines Programms.................................................................................... 32
Schlüsselwörter in ANSI-C..................................................................................... 33
Es gibt 5 grundlegende Datentypen: ....................................................................... 33
Datentypen .............................................................................................................. 33
Allgemeine Form eines C Programmes .................................................................. 34
Schlechte Variablennamen ...................................................................................... 48
Zahlenbasis von Int Konstanten .......................................................................... 52
Reelle Konstanten ............................................................................................... 52
Sonderzeichen ..................................................................................................... 53
Speicherbedarf und Wertebereich ....................................................................... 54
manipulatoren...................................................................................................... 59
Sequenz ............................................................................................................... 63
Allgemeine Form: if else Anweisungen.............................................................. 63
Struktogramme if-else ......................................................................................... 64
Flußdiagramm für if-else..................................................................................... 64
Verschachtelte if else Anweisungen ................................................................... 65
Allgemeine Form: if else Ketten ......................................................................... 66
Fehlerquelle =...................................................................................................... 67
Fehlerquelle ;....................................................................................................... 67
Allgemeine Form bedingte Bewertung: .............................................................. 67
Allgemeine Form: switch case ............................................................................ 69
Switch struktogramm .......................................................................................... 69
Allgemeine while schleifen ................................................................................. 71
Allgemeines Struktogramm der while Schleife: ................................................. 71
Flußdiagramm while schleife .............................................................................. 72
Allgemeine Form: do while schleifen ................................................................. 73
do while schleifen Struktogramm ....................................................................... 73
Flußdiagramm do while schleifen ....................................................................... 74
Flußgramm: Fakultät ........................................................................................... 76
For Schleife ......................................................................................................... 77
Flußdiagramm For Schleife................................................................................. 78
Struktogramm For Schleife ................................................................................. 78
Äquivalenz for und while.................................................................................... 79
Allgemeine Form: Kommaoperator .................................................................... 79
Allgemeine Form: break...................................................................................... 81
Verdeutlichung: break ......................................................................................... 81
Allgemeine Form Continue: ........................................................................... 81
Verdeutlichung: continue .................................................................................... 81
Allgemeine Form der Markendefinition: ............................................................ 82
Zuweisungsoperatoren ........................................................................................ 89
Casting................................................................................................................. 90
Casting, was kommt hier heraus?........................................................................ 90
Vorrangregeln...................................................................................................... 91
Beispiele bool:..................................................................................................... 93
Arithmetische operatoren .................................................................................... 95
7
Bild 49.
Bild 50.
Bild 51.
Bild 52.
Bild 53.
Bild 54.
Bild 55.
Bild 56.
Bild 57.
Bild 58.
Bild 59.
Bild 60.
Bild 61.
Bild 62.
Bild 63.
Bild 63.
Bild 64.
Bild 65.
Bild 66.
Bild 67.
Bild 68.
Bild 69.
Bild 70.
Bild 71.
Bild 72.
Bild 73.
Bild 74.
Bild 75.
Bild 76.
Bild 77.
Bild 78.
Bild 79.
Bild 80.
Bild 81.
Bild 82.
Bild 83.
Bild 84.
Bild 85.
Bild 86.
Bild 87.
Bild 88.
Bild 89.
Bild 90.
Bild 91.
Bild 92.
Bild 93.
Bild 94.
Bild 95.
Bild 96.
Bild 97.
Bild 98.
Vergleichsoperatoren, relationale ....................................................................... 95
Logische operatoren ............................................................................................ 96
Bit operatoren...................................................................................................... 97
Bitweise NOT...................................................................................................... 98
2k......................................................................................................................... 98
Bitweise XOR ..................................................................................................... 98
Left shift .............................................................................................................. 98
Right shift............................................................................................................ 98
Variablendeklaration (als Referenz): .................................................................. 99
Ternäre Operator ............................................................................................... 100
Allgemeine Form (Deklaration Aufzählungstyp): ............................................ 103
Beispiel: Enumerationstypen............................................................................. 103
eigene Enumerationskodierung ......................................................................... 103
Beispiel-Array ................................................................................................... 105
Speicherlayout eines ArraysmatrixAus.cpp Ausgabe einer Matrix ................ 108
matrixAus.cpp Ausgabe einer Matrix ............................................................. 109
Allgemeine Form (Strukturdeklaration):........................................................... 114
Die Syntax eines Funktionsprototyps hat die Form: ......................................... 119
Die Syntax einer Funktionsdefinition hat die Form: ......................................... 119
Die Syntax eines Funktionsaufrufs hat die Form: ............................................. 120
Argumente aus der Kommandozeile ................................................................. 138
Dokumentationsregeln ...................................................................................... 142
Modulare Struktur des Zufallsprogramms ........................................................ 145
Abhängigkeiten ................................................................................................. 146
Allgemeine Form eines Zeigers ........................................................................ 154
Zugriff auf Objekte, Beispiele:.......................................................................... 154
Beispiele für pointer:......................................................................................... 156
Zeiger auf Konstanten ....................................................................................... 157
Main vor Aufruf inc ........................................................................................ 159
Main nach Aufruf inc...................................................................................... 159
inc nach (*p)++ .............................................................................................. 159
Zeiger auf Vektor .............................................................................................. 163
Zeigerausdrücke Konsequenzen:....................................................................... 164
Arrayname und Zeiger ...................................................................................... 165
Arrayname und Zeiger ...................................................................................... 165
Eingabe.............................................................................................................. 170
Weitere io Schalter sind: ................................................................................... 176
Speicherabbild................................................................................................... 179
Beispiele für new operator ................................................................................ 180
Vektor von Zeigern ........................................................................................... 181
Zeiger auf array von double .............................................................................. 182
Zeiger auf Zeiger auf double ............................................................................. 183
Delete operator .................................................................................................. 184
Dynamische rekursive Strukturen ..................................................................... 185
Verkettete Liste ................................................................................................. 186
Verkettete Liste ................................................................................................. 186
Einfügen in verkettete Liste: ............................................................................. 187
Ausgeben einer Liste:........................................................................................ 188
Löschen von Listenelementen:.......................................................................... 189
stack................................................................................................................... 234
Stack als Array .................................................................................................. 235
8
Bild 99.
Bild 100.
Bild 101.
Bild 102.
Bild 103.
Bild 104.
Bild 105.
Bild 106.
Queue ............................................................................................................... 241
Zirkulare Queue................................................................................................. 241
Queue als verkettete Liste ................................................................................. 243
Einfügen in die leere Queue .............................................................................. 243
Einfügen in nicht leere Queue ........................................................................... 244
Queue nach Operation leer ................................................................................ 245
Queue nach Operation nicht leer:...................................................................... 245
Virtuelle Destruktoren....................................................................................... 264
9
Listing 1.
Listing 2.
Listing 3.
Listing 4.
Listing 5.
Listing 6.
Listing 7.
Listing 8.
Listing 9.
Listing 10.
Listing 11.
Listing 12.
Listing 13.
Listing 14.
Listing 15.
Listing 16.
Listing 17.
Listing 18.
Listing 19.
Listing 20.
Listing 21.
Listing 22.
Listing 23.
Listing 24.
Listing 25.
Listing 26.
Listing 27.
Listing 28.
Listing 29.
Listing 30.
Listing 31.
Listing 32.
Listing 33.
Listing 34.
Listing 35.
Listing 36.
Listing 37.
Listing 38.
Listing 39.
Listing 40.
Listing 41.
Listing 42.
Listing 43.
Listing 44.
Listing 45.
Listing 46.
Listing 47.
Listing 48.
Listing 49.
Listing 50.
Zufallszahl.cpp .................................................................................................... 17
Klassiker von K&R ............................................................................................. 34
syntaktischer Fehler............................................................................................. 35
Mit semantischem Fehler .................................................................................... 36
Beispiel Maßeinheiten-Umwandlung.................................................................. 46
Quadratzahlen...................................................................................................... 47
Variablendeklaration ........................................................................................... 49
lokale Variablendeklaration ................................................................................ 49
C vs. C++ ............................................................................................................ 50
formale Parameter ........................................................................................... 51
Globale Variablen ........................................................................................... 51
Const mit expliziter Initialisierung.................................................................. 51
octHexOutput.cpp , Zahlenkonstanten .......................................................... 52
Const qualifier ................................................................................................. 53
wertebereich.cpp , Wertebereichsüberschreitung............................................ 54
ueberlauf.cpp , Wertebereichsüberschreitung zur Laufzeit............................ 55
Beispiele für Deklaration, Definition und Initialisierung................................ 56
nichtInitialisiert.cpp , Beispiel für nichtinitialisierte Variablen:................... 56
Externe Deklaration......................................................................................... 57
Reihengenerator mit lokaler static Variablen.................................................. 57
Register Variablen........................................................................................... 57
eaAllgemein.cpp.............................................................................................. 58
eaSeiteneffekt.cpp .......................................................................................... 58
Beispiel: manipulatoren.cpp............................................................................ 60
Beispiel: manipulatoren2.cpp.......................................................................... 61
Typische Beispiele für Anweisungen:............................................................. 62
Typische Beispiele für Blöcke: ....................................................................... 62
Beispiele sequentielle Abarbeitung:................................................................ 63
Beispiel: verzweigung.cpp .............................................................................. 64
verzweigungUnleserlich.cpp ........................................................................... 65
Magisches Zahlenprogramm .......................................................................... 65
Beispiel (Notenberechnung): mehrfachverzweigung.cpp ............................... 66
bedingte Bewertung......................................................................................... 68
schaltjahr.cpp................................................................................................... 68
Überlappende case Fälle bei einer switch -Anweisung................................... 70
switch.cpp (römische Ziffern):....................................................................... 70
additionVonZahlenfolge.cpp........................................................................... 72
unendlichschleife.cpp...................................................................................... 73
doWhile.cpp .................................................................................................... 75
fakultaet.cpp .................................................................................................... 77
for Schleife, ascii.cpp (ASCII Tabelle ausgeben): .......................................... 78
ascii.cpp........................................................................................................... 79
Kommaoperator (Summe von 1 bis 10): ......................................................... 80
Übungsbeispiel ................................................................................................ 80
menue.cpp ....................................................................................................... 82
Exception......................................................................................................... 84
Exception-Beispiel (Wurzel einer negativen Zahl):........................................ 85
Exceptions (unterschiedliche catch-Blöcke): .................................................. 86
Beispiel einer korrekten Zuweisung:............................................................... 88
Mehrfachzuweisung ........................................................................................ 89
10
Listing 51.
Listing 52.
Listing 53.
Listing 54.
Listing 55.
Listing 56.
Listing 57.
Listing 58.
Listing 59.
Listing 60.
Listing 61.
Listing 62.
Listing 63.
Listing 64.
Listing 65.
Listing 66.
Listing 67.
Listing 68.
Listing 69.
Listing 70.
Listing 71.
Listing 72.
Listing 73.
Listing 74.
Listing 75.
Listing 76.
Listing 77.
Listing 78.
Listing 79.
Listing 80.
Listing 81.
Listing 82.
Listing 83.
Listing 84.
Listing 85.
Listing 86.
Listing 87.
Listing 88.
Listing 89.
Listing 90.
Listing 91.
Listing 92.
Listing 93.
Listing 94.
Listing 95.
Listing 96.
Listing 97.
Listing 98.
Listing 99.
Listing 100.
Listing 101.
Typkonvertierung ............................................................................................ 89
Cast operator ................................................................................................... 90
Typumwandlung.............................................................................................. 91
Reihenfolge der Auswertung........................................................................... 92
Define der Bools.............................................................................................. 93
wahrheitswerte.cpp.......................................................................................... 94
Beispiele: arithmOperatoren.cpp..................................................................... 95
cat logOperatoren.cpp ..................................................................................... 96
XOR Funktion ................................................................................................. 97
mul.cpp............................................................................................................ 98
Beispiele: ref.cpp............................................................................................. 99
? Operator...................................................................................................... 100
Zeigeroperatoren ........................................................................................... 100
Sizeof operator .............................................................................................. 100
Kommaoperator............................................................................................. 101
Klammern und Blanks................................................................................... 101
Beispiel wurzel.cpp ...................................................................................... 102
Enumerationstypen........................................................................................ 103
Enumerationsbeispiele .................................................................................. 104
Enumerationsbeispiele .................................................................................. 104
BeispielArray ................................................................................................ 104
Grenzüberschreitung bei der Array-Indizierung ........................................... 105
Initialisierungen von Arrays.......................................................................... 105
Bei der Deklaration können auch Initialwerte mit angegeben werden: ........ 106
Array-Beispiele: ............................................................................................ 106
Array-Beispiel, Größe: .................................................................................. 106
bcd.cpp .......................................................................................................... 107
Mehrdimensionale Felder.............................................................................. 108
Array-Initialisierung...................................................................................... 108
Bubble sort .................................................................................................... 110
bubbleSort.cpp............................................................................................... 110
Zeiger auf array ............................................................................................. 111
Zeichenkette .................................................................................................. 111
c-string Funktionen ....................................................................................... 111
Zeichenkette per string .................................................................................. 112
HalloWelt.cpp ............................................................................................... 112
operationenAufStrings.cpp............................................................................ 113
Struktur.cpp ................................................................................................... 114
Definition einer struktur ................................................................................ 114
Zugriff auf struktur........................................................................................ 114
Bruch.cpp: ..................................................................................................... 115
Beispielobjekt................................................................................................ 115
Zugriff auf das Objekt ................................................................................... 116
wurzel.cpp ..................................................................................................... 117
fakultaet.cpp .................................................................................................. 118
Funktionsprototypen...................................................................................... 119
Funktionsdefinitionen.................................................................................... 119
Funktionsaufrufe ........................................................................................... 120
Funktionsdefinition zuerst, power.cpp .......................................................... 121
gueltigSichtbar01.cpp.................................................................................... 122
gueltigSichtbar02.cpp mit scope ................................................................... 123
11
Listing 102.
Listing 103.
Listing 104.
Listing 105.
Listing 106.
Listing 107.
Listing 108.
Listing 109.
Listing 110.
Listing 111.
Listing 112.
Listing 113.
Listing 114.
Listing 115.
Listing 116.
Listing 117.
Listing 118.
Listing 119.
Listing 120.
Listing 121.
Listing 122.
Listing 123.
Listing 124.
Listing 125.
Listing 126.
Listing 127.
Listing 128.
Listing 129.
Listing 130.
Listing 131.
Listing 132.
Listing 133.
Listing 134.
Listing 135.
Listing 136.
Listing 137.
Listing 138.
Listing 139.
Listing 140.
Listing 141.
Listing 142.
Listing 143.
Listing 144.
Listing 145.
Listing 146.
Listing 147.
Listing 148.
Listing 149.
Listing 150.
Listing 151.
Listing 152.
add01.ccp....................................................................................................... 125
inc1.c ............................................................................................................. 125
Quersumme.cpp............................................................................................. 126
inc2.c ............................................................................................................. 126
vertausche_C ................................................................................................. 127
int & plus (int &a) // Referenz als Rueckgabe .............................................. 128
Call by reference ........................................................................................... 129
Übergabe von Array-Adressen...................................................................... 129
Vereinbarung eines zweidimensionalen Arrays als Funktionsparameter...... 129
kumuliere.cpp (Kumuliere Arraywerte)........................................................ 130
aktienKurs01.cpp........................................................................................... 130
$ cat aktienKurs02.cpp, default parameter.................................................... 131
random01.cpp................................................................................................ 132
Zugriff auf const............................................................................................ 133
gerade.cpp ..................................................................................................... 133
rekursion1.cpp ............................................................................................... 134
Überladen, Maximales Feld in Array, maxElement.cpp............................... 137
main.cpp ........................................................................................................ 138
echo.cpp......................................................................................................... 139
UnixEcho.cpp................................................................................................ 139
env.cpp .......................................................................................................... 140
ausnahme.cpp ................................................................................................ 141
Schnittstellenspezifikation = Vertrag zwischen Aufrufer und Funktion....... 141
assertions (Fragment) .................................................................................... 142
Programm-Dateien: random.h ....................................................................... 147
random.cpp.................................................................................................... 147
print.cpp......................................................................................................... 147
zufall.cpp ....................................................................................................... 147
datei1.cpp ...................................................................................................... 148
datei2.cpp ...................................................................................................... 148
datei3.cpp ...................................................................................................... 148
Datei4.cpp extern const ................................................................................. 149
ifdefs a.h ........................................................................................................ 150
c.cpp
b.cpp............................................... 150
compileroutput .............................................................................................. 150
ifdefs a1.h ...................................................................................................... 151
c1.cpp
b1.cpp..................................................... 151
pi.cpp ............................................................................................................. 151
Makro mit Parametern................................................................................... 152
quad.cpp ........................................................................................................ 152
fak.cpp ........................................................................................................... 153
main.cpp ........................................................................................................ 153
wasWirdGedruckt.cpp................................................................................... 158
inc.cpp ........................................................................................................... 159
inc.cpp ........................................................................................................... 160
max.cpp ......................................................................................................... 161
zeigerFehler01.cpp ........................................................................................ 162
wasWirdGedruckt02.cpp............................................................................... 164
vektorToUpper.cpp ....................................................................................... 165
strlen.cpp ....................................................................................................... 166
Hörsaalübung Lösung ................................................................................... 167
12
Listing 153.
Listing 154.
Listing 155.
Listing 156.
Listing 157.
Listing 158.
Listing 159.
Listing 160.
Listing 161.
Listing 162.
Listing 163.
Listing 164.
Listing 165.
Listing 166.
Listing 167.
Listing 168.
Listing 169.
Listing 170.
Listing 171.
Listing 172.
Listing 173.
Listing 174.
Listing 175.
Listing 176.
Listing 177.
Listing 178.
Listing 179.
Listing 180.
Listing 181.
Listing 182.
Listing 183.
Listing 184.
Listing 185.
Listing 186.
Listing 187.
Listing 188.
Listing 189.
Listing 190.
Listing 191.
Listing 192.
Listing 193.
Listing 194.
Listing 195.
Listing 196.
Listing 197.
Listing 198.
Listing 199.
Listing 200.
Listing 201.
Listing 202.
Listing 203.
Zeigerdifferenzen .......................................................................................... 168
strcpy.cpp ...................................................................................................... 169
simpelCat.cpp Beispiel cat von Unix, type von DOS: ................................. 171
cat Umleitung ................................................................................................ 172
cat Umleitung ................................................................................................ 172
Schreiben eines Datensatzes in eine Datei .................................................... 173
Lesen eines Datensatzes aus einer Datei ....................................................... 173
simpleCp.cpp................................................................................................. 175
Schalter für binärdateien ............................................................................... 176
lineCount ....................................................................................................... 176
Lesen von datei.............................................................................................. 176
Binärdateien .................................................................................................. 177
doubelBinary.cpp .......................................................................................... 178
vektorVonZeigern.cpp................................................................................... 181
zeigerAufVektor.cpp ..................................................................................... 182
verketteteListe.cpp ........................................................................................ 187
einfuegen.cpp ................................................................................................ 187
ausgeben.cpp ................................................................................................. 188
suchen.cpp ..................................................................................................... 188
loeschen.cpp .................................................................................................. 189
main.cpp ........................................................................................................ 190
Typedef Beispiel: .......................................................................................... 191
Bankkonto, 1. Version................................................................................... 195
Bankkonto, 2. Version................................................................................... 196
Bankkonto, 3.Version, Klasse Konto ............................................................ 199
Bankkonto, 3. Version, main......................................................................... 200
Bankkonto, 4. Version, Prototypen ............................................................... 201
Bankkonto, 4.Version, Implementierung ...................................................... 202
Bankkonto, 4.Version, main.......................................................................... 203
Kontoklasse mit Standardkonstruktor ........................................................... 204
Kontoimplementierung Standardkonstruktor................................................ 204
Konstruktor mit Parameter, Header .............................................................. 205
Konstruktor mit Parameter, Implementierung .............................................. 205
Konstruktor mit Parameter, main .................................................................. 205
Signaturunterschiede ..................................................................................... 206
Initialisierungsliste ........................................................................................ 206
Kopierkonstruktor, Header............................................................................ 207
Kopierkonstruktor, Implementierung............................................................ 208
Kopierkonstruktor, main ............................................................................... 208
Destruktor, Beispiel1..................................................................................... 210
Destruktor, Beispiel2..................................................................................... 211
Destruktor, Beispiel3..................................................................................... 212
Beispiel-Klasse für rationale Zahlen ............................................................. 214
Beispiel Zeiger auf const-Objekt................................................................... 214
Weiteres Beispiel, Header ............................................................................. 215
Weiteres Beispiel, Implementierung ............................................................. 215
Vererbung und Mehrfachvererbung .............................................................. 219
Die Klasse Ort ............................................................................................... 222
Main zur Verwendung der Klasse Ort........................................................... 223
Die Klasse GraphObj .................................................................................... 224
Main zur Verwendung der Klasse GraphObj................................................ 225
13
Listing 204.
Listing 205.
Listing 206.
Listing 207.
Listing 208.
Listing 209.
Listing 210.
Listing 211.
Listing 212.
Listing 213.
Listing 214.
Listing 215.
Listing 216.
Listing 217.
Listing 218.
Listing 219.
Listing 220.
Listing 221.
Listing 222.
Listing 223.
Listing 224.
Listing 225.
Listing 226.
Listing 227.
Listing 228.
Listing 229.
Listing 230.
Listing 231.
Listing 232.
Listing 233.
Listing 234.
Listing 235.
Listing 236.
Listing 237.
Listing 238.
Listing 239.
Listing 240.
Listing 241.
Listing 242.
Listing 243.
Listing 244.
Listing 245.
Listing 246.
Listing 247.
Listing 248.
Listing 249.
Listing 250.
Die Klasse Rechteck...................................................................................... 226
Die Klasse Strecke ........................................................................................ 226
Graphik-main ................................................................................................ 227
Subobjekt der Oberklasse.............................................................................. 229
Kodebeispiel public und private:................................................................... 230
Readonly Zugriff ........................................................................................... 231
Zugriff aif private Elemente .......................................................................... 232
Überschreiben von Funktionen ..................................................................... 233
Verwendung des Bereichsoperators .............................................................. 233
Definition des ADT Stack. ............................................................................ 235
Implementierung des ADT............................................................................ 236
Stack-Applikation.......................................................................................... 238
Beispiel für this ............................................................................................. 239
Weiteres Beispiel für this .............................................................................. 239
Vermeidung der Kopie auf sich selbst .......................................................... 239
Klasse CQueue .............................................................................................. 242
Queue-Konstruktor........................................................................................ 243
Queue isEmpty Methode............................................................................... 243
Einfügen in leere Queue................................................................................ 244
Einfügen in die nicht leere Queue ................................................................. 244
Queue nach Operation leer ............................................................................ 245
Destruktor der Queue .................................................................................... 246
Testanwendung für die Queue....................................................................... 246
Andere Queueklasse mit Array, Header........................................................ 247
Andere Queueklasse mit Array, Implementierung........................................ 248
Klassenspezifische Daten, Header ................................................................ 249
Klassenspezifische Daten, Implementierung ................................................ 250
Spezieller Kopierkonstruktor ........................................................................ 250
Destruktor...................................................................................................... 251
Testmain ........................................................................................................ 252
Fehlertest ....................................................................................................... 253
Klassenspezifische Konstanten ..................................................................... 254
Polymorphismus, main.................................................................................. 256
Klasse mit virtueller Funktion....................................................................... 257
abgeleitete Klasse mit virtueller Funktion .................................................... 257
main für Klasse mit virtueller Funktion ........................................................ 257
Beispielprogramm ......................................................................................... 258
Graphobjekt als abstrakte Klasse, main ........................................................ 260
Graphobjekt als abstrakte Klasse, Deklaration ............................................. 261
Graphobjekt als abstrakte Klasse, Implementierung..................................... 262
abgeleitete Klasse Quadrat ............................................................................ 264
Test Destruktoren ......................................................................................... 265
Objektgröße bei virtuellem Destruktor ......................................................... 271
Simple_Graph.h............................................................................................. 272
Simple_Graph.cpp......................................................................................... 272
Simple_Graphics_Dlg.h ................................................................................ 285
Eigene Methoden........................................................................................... 285
14
1 Überblick
Zunächst werden die Ziele der Veranstaltung und die Organisation erläutert. Dann wird
verdeutlicht, weshalb als Sprache C++ verwendet wird.
1.1 Ziele
In der Veranstaltung wird "Programmieren" vermittelt. Leider wurde die Vorlesung von
ursprünglich 2* 3 SWS auf 1*2SWS gekürzt. Um dem Rechnung zu tragen, wurden die
Skripte “C++V1“ und “C++V2“ in ein Skript “C++Gesamt“ zusammengefasst und gekürzt.
Die angesetzten 2SWS Vorlesung reichen nicht aus, gute Grundlagen der Informatik und
ausreichende Fertigkeiten im Programmieren in C++ zu vermitteln. Interessierte seien
deshalb auf dieses Skript und die angegebene Literatur zum Selbststudium hingewiesen.
Die Themen der Vorlesung sind wie folgt strukturiert und finden sich auch im Skript
ausführlich wieder:
1. Organisatorisches, Aufbau eines Computers, Erstellung eines Programmes,
Algorithmenund Programme, Grundlagen der Zahlendarstellung
2. Variablen, Datentypen, Definition, Deklaration, Initialisierung
3. Eingabe/Ausgabe, Kontrollstrukturen
4. Operatoren und Ausdrücke
5. Arrays und Strukturen, Bubble-Sort
6. File-I/O, Funktionen, Rekursion
7. Dynamische Speicherallokation, Zeiger und Referenzen
8. Objektorientierung, Abstrakte Datentypen, Klasse und Objekt
9. Konstruktoren, Destruktoren
10. Vererbung
11. Polymorphie
12. Reserve und Wiederholung, Klausurvorbereitung
Programmieren
• bedeutet Strukturierung komplexer Probleme,
o divide and conquer (teile und herrsche)
o Übung für viele andere Aufgaben
• ist eine äußerst kreative Tätigkeit.
o Entwurf und Realisierung
o nicht nur kodieren vorgegebener Abläufe
• setzt handwerkliche Fertigkeiten voraus.
o Beherrschung der Ausdrucksmittel einer Programmiersprache
Das Bild des Programmierers
war früher:
• unverstandener Freak, Hacker und Guru zugleich:
• tricky programming
• Hauptsache, der Compiler versteht mich
ist heute:
Ingenieur
15
•
•
•
sieht Programmieren als eine Tätigkeit im systematischen Entwurfsprozeß
ist Teamwork, nicht Einzelkämpfer
hat das Ziel, wieder verwendbaren Code zu entwickeln, d.h. leichte Verständlichkeit
für andere Programmierer ist ihm wichtig
Der Erfinder der Programmiersprache C++, B. Stroustrup hat Programmieren wie folgt
umrissen:
• Programm-Design und Programmieren sind menschliche Aktivitäten.
• Es gibt kein "allgemeines Rezept", das Intelligenz, Erfahrung und "guten Geschmack"
ersetzen kann.
• Das Experimentieren ist für alle nicht-trivialen Software-Projekte essentiell.
• Entwurf und Programmieren sind iterative Aktivitäten.
• Die Systeme, die wir entwickeln, tendieren stets zu einer Komplexitäts-Obergrenze,
die durch uns selbst und unsere Werkzeuge gesetzt ist.
16
Zum Programmieren sind Werkzeuge erforderlich:
Programmiersprachen,
• Spezifizieren die Aktionen, die der Rechner ausführen soll.
• Beispiele:
o C++, Java
o C, Pascal
o Prolog
o Occam
o FP
Entwicklungsumgebungen
o Unterstützen bei der Programmentwicklung
o Kodieren in Programmiersprache
o Übersetzen und Binden
o Ausführen des Programms und Test
o Dokumentation
o Versionsführung
• Beispiele:
o Forte for Java
o Borland C++
o Microsoft Developer Studio
Sie werden lernen, „Programm-Texte“ wie den folgenden zu Lesen und zu Schreiben.
Listing 1.
Zufallszahl.cpp
#include <iostream>
#include <set>
#include <ctime> // für time()
const int ZAHLEN = 45;
const int NUMTIP = 6;
unsigned int zufallsZahl()
{
return 1 + random() % ZAHLEN;
}
int main()
{
typedef set<unsigned int> IntSet;
IntSet tip;
// erzeuge Zufallszahl zwischen 1 und 45
srandom(time(NULL));
// initialisiere den Zufallszahlengenerator
while (tip.size() < NUMTIP)
{
//Zufallszahl in die Menge einfügen
tip.insert(zufallsZahl());
}
for (IntSet::iterator i = tip.begin(); i != tip.end(); ++i)
{
//Menge ausgeben
cout << *i << endl;
}
return 0;
}
1.2 Organisation der Veranstaltung
Die Veranstaltung ist unterteilt in
17
Vorlesungen (2 SWS)
Praktika (2 SWS)
1.2.1 Vorlesung
Die Vorlesung dient der zusammenhängenden Darstellung und Vermittlung von
Grundwissen, sowie methodischer Kenntnisse rund um die Thematik „Programmieren in
C++“.
Die Vorlesung ist kein Monolog!
Sie sollten stets Nachfragen, wenn etwas unklar geblieben ist und Sie einen Sachverhalt
anders sehen.
1.2.2 Praktikum
Im Praktikum werden Übungsaufgaben von Ihnen selbstständig bearbeitet.
Ablauf:
1. Die Übungsaufgaben können über meine Homepage herunter geladen werden.
2. Die Lösung wird von Ihnen zu Hause (oder in nicht belegten Labors) erarbeitet:
2.1.Verfeinerung des Entwurfs
2.2.Ausarbeitung des Programms inklusiv Eintippen
2.3.setzt Editor voraus, aber nicht unbedingt Compiler
3. Im Praktikum wird der Entwurf in den Rechner eingegeben bzw. von Ihrem
Datenträger auf die Festplatte kopiert und:
3.1.der Entwurf wird noch mal verfeinert;
3.2.das Programm wird übersetzt, getestet und
3.3.zur Abnahme vorbereitet.
3.4.Die Lösung wird von mir (oder einem Tutor) testiert, wenn
3.4.1. die Lösung erklärt werden kann,
3.4.2. das Programm ablauffähig ist und
3.4.3. die Lösung nachvollziehbar ist.
1.2.3 Folgende Regeln gelten für die Praktika:
•
Die Teilnahme an den Praktikumsterminen ist Pflicht
Ein Testat gibt es i.d.R. nur zum jeweiligen Termin!
Bewertung: erfolgreich / nicht erfolgreich, also keine Noten
• Erfolgreiche Teilnahme am Praktikum ist Zulassungsvoraussetzung für Klausur:
alle Praktikumsübungen müssen testiert sein
• Die Gruppeneinteilung mit Terminen wird in der ersten Vorlesung vorgenommen
• Gruppenarbeit: jeweils 2 Studierende an einem PC erarbeiten gemeinsam ein
Programm
• aber jede(r) muß eine Kopie besitzen
• eine schriftliche Vorbereitung muss vorgelegt werden
• keine Arbeitsteilung: jede(r) muß alles beherrschen
• gemeinsame Vorbereitung ist empfehlenswert
• Modell "Fahrschule":
Autofahren lernt man nicht als Beifahrer
"Fahrer" bedient Tastatur und Maus
"Beifahrer" beobachtet, denkt mit, berät
• in jeder Übung werden die Rollen getauscht!
• Abschreiben und Kopieren ist verboten
Programmieren lernt man nur durch eigenes Üben; wer mit dem Programmieren echte
Schwierigkeiten hat, sollte sich ernsthaft fragen, ob Elektrotechnik/Informatik das richtige
Fach für ihn ist!!!
18
1.2.4 Klausur
Am Ende des Semesters findet eine Klausur statt.
Zulassungsvoraussetzung ist die erfolgreiche Teilnahme am Praktikum, nachgewiesen durch
die Testate.
1.2.5 Persönliche Selbstorganisation
Selbstverantwortliches Lernen
• Fragen stellen
• Üben, üben, üben ...
• Wissenslücken identifizieren und systematisch füllen
Gruppenarbeit
• muß allen Beteiligten nützen
• nicht für Trittbrettfahrer
Besuch der Vorlesungen
• wird nicht kontrolliert
• ist aber dringend zu empfehlen, da hier Tipps gegeben werden und der umfangreiche
Stoff aus dem Skript eingeordnet wird
Sprachkenntnisse
• Englisch unbedingt pflegen!
• auch Originalliteratur lesen und Fachbegriffe lernen
1.2.6 Skript der Veranstaltung
Ein Skript der Veranstaltung ist über meine Homepage abrufbar.
Dort sind auch die Praktikumsaufgaben veröffentlicht und aktuelle Informationen publiziert.
Ich habe in weiten Teilen das Skript des Kollegen Schütte übernommen, dessen Ausarbeitung
wiederum in Teilen auf den Arbeiten des Kollegen Kreling basiert.
19
Literatur
Breymann, C++, Einführung und professionelle Programmierung
Hanser 2001
(einfach geschrieben, sehr gut geeignet zur Nachbereitung der Vorlesung)
Prinz, C++ lernen und professionell anwenden
Vmi Buch
(einfach geschrieben, sehr gut geeignet zur Nachbereitung der Vorlesung)
B. Eckel, Thinking in C++
Prentice Hall 2000
(auch als Online Version im Internet erhältlich)
V. Henkel, Online-Kurs C++
www.volkard.de/vcppkold/inhalt.html
(entstanden aus einer Diplomarbeit an der FHD)
Bjarne Stroustrup, Die C++ Programmiersprache
Addison-Wesley
(Original vom Erfinder der Programmiersprache; sehr tiefgehend; teilweise schwer zu lesen)
Schader, Kuhlins, Programmieren in C++
Springer
(einfach geschrieben)
Stanley B. Lippmann, C++ eine Einführung
Addison-Wesley
(einfach geschrieben, umfangreich)
Herbert Schildt: C++ Entpackt
Mitp, 2001
(umfangreich)
Scott Meyers: Effektive C++ programmieren
Addison-Wesley, München, 2005
(fortgeschritten)
Lippman: Essential C++
C++ in Depth series, Addison-Wesley Longman, 1999
(fortgeschritten)
Kernighan Ritchie: Programmieren in C
Hanser Fachbuch, 1990
(Klassiker)
Plauger: the Standard C Library
Prentice Hall International
(zum Nachschlagen)
Richard C. Lee: UML and C++
Prentice Hall International, 2000
20
(gute Kombination)
Stroustrupe: the C++ programming language
Addison-Wesley Longman, 2000
(Klassiker)
21
2 Einführung
In den Forschungslaboratorien von Bell Telephone in USA war Anfang der siebziger Jahre,
ein Betriebssystem entstanden, das im Wesentlichen für die Bedürfnisse der dortigen
Entwickler gemacht war. Dessen erste Fassung war noch in Assembler geschrieben; für die
Neuimplementierung entschied man sich, eine höhere Sprache zu verwenden - eben C.
Diese Entscheidung hat wohl wesentlich zu der weiten Verbreitung beigetragen, die dieses
Betriebssystem heute erlebt. Sein Name ist UNIX, und es sieht derzeit so aus, als würde
UNIX das wichtigste Standardbetriebssystem neben der Microsoft Welt bleiben.
Die Verwendung von C als Implementierungssprache hat zur Folge, daß das System relativ
leicht zu portieren ist.
Die Väter von C sind Kernighan und Ritchie, die auch das Standardwerk über C verfaßt
haben:
"The C Programming Language" bei Prentice Hall.
Später hat sich ANSI (American National Standards Institute) um eine rigorose Definition
der im Original doch manchmal zu unpräzis beschriebenen Sprache bemüht und einen DraftStandard für ANSI-C veröffentlicht.
Weiterhin hat Bjarne Stroustrup (ebenfalls Bell Labs) die Sprache um objektorientierte
Mechanismen und Konstruktionen erweitert. Diese Weiterentwicklung (Bell Labs, 1983) ist
unter dem Name C++ kommerziell verfügbar. C++ basiert auf dem ANSI-C Standard. Es
stellt also eine Erweiterung von C/C++ dar und ist seit Juli 1998 auch standardisiert.
Zusammen mit dem C/C++ Compiler werden jeweils auch immer der Präprozessor sowie die
Library-Funktionen geliefert und dokumentiert. Diese sind nicht Bestandteil der Sprache
C/C++.
ANSI hat aber gut daran getan, auch diese compilerexternen Komponenten zu
standardisieren.
2.1 Computer, Dateien, Programme
2.1.1 Grundbausteine eines Computers
Jeder Computer (Rechner) enthält folgende Grundbestandteile:
• Den Prozessor, auch: CPU (Central Processing Unit),
• den Hauptspeicher, auch: Arbeitsspeicher, oder RAM (Random Access Memory),
• den Plattenspeicher sowie
• Ein–/Ausgabegeräte wie Monitor, Tastatur, Maus, etc.
Der Prozessor verarbeitet die Daten, die sich im Hauptspeicher befinden. Er befolgt dabei die
Anweisungen eines Programms.
Jedes Programm besteht aus einer Folge von Anweisungen. Anweisungen nennt man auch
Befehle.
Das Programm befindet sich ebenfalls im Hauptspeicher. Es sagt dem Prozessor, und damit
dem Computer insgesamt, was er tun soll. Wenn der Computer läuft, holt sich also der
Prozessor die Befehle des Programms aus dem Hauptspeicher und führt sie aus – Befehl für
Befehl.
Im Plattenspeicher stehen Programme und Daten des Systems und seiner Benutzer. Sollen sie
verarbeitet werden, dann müssen zuerst sie in den Hauptspeicher transportiert (geladen)
werden.
Die Ein–/Ausgabegeräte dienen dazu mit dem Computer zu kommunizieren. Der
Plattenspeicher ist sehr groß. Er enthält alle Programme und Daten.
22
Der Hauptspeicher ist kleiner und enthält nur die Daten und Programme, die gerade
verarbeitet werden. Der Prozessor ist noch kleiner. Er enthält nur den einzelnen Befehl, der
gerade verarbeitet wird und dazu noch einige wenige Daten.
Bild 1.
Blockbild eines Rechners
2.1.2 Prozessor
Der Prozessor (CPU) bildet zusammen mit dem Hauptspeicher den Kern des Rechners. Er
enthält einige wenige Speicherzellen, Register genannt.
In diese Register kann er aus dem Hauptspeicher Daten laden, sie durch Rechenoperationen
verändern und dann wieder in den Hauptspeicher schreiben.
Die Aktivität des Prozessors wird durch ein Programm gesteuert. Bei einem Programm
handelt es sich ebenfalls um Daten, die im Hauptspeicher stehen:
Sie werden Befehl für Befehl vom Prozessor in ein spezielles Register geladen und dann
ausgeführt.
2.1.3 Hauptspeicher
Der Hauptspeicher enthält viele Speicherzellen: Sie sind direkt adressierbar und ihr Inhalt
kann von der CPU sehr schnell gelesen und geschrieben werden. Eine Speicherzelle enthält
typischerweise ein Byte. Ein Byte ist 8 Bit (8 Binärzeichen, 0 oder 1).
“Direkt adressierbar” bedeutet, daß jede Zelle, d.h. jedes Byte, eine Adresse hat und der
Prozessor jederzeit auf jede beliebige Speicherzelle zugreifen kann. Dieser wahlfreie Zugriff
(engl. Random Access) ist sehr wichtig. Ohne ihn könnten die Programme nicht vernünftig
ausgeführt werden.
Daten müssen an beliebigen, nicht vorhersehbaren Stellen geholt und gespeichert werden und
von einer Anweisung muß je nach Bedarf zu einer beliebigen anderen verzweigt werden
können.
2.1.4 Plattenspeicher
Die Schnelligkeit und die direkte Adressierbarkeit machen den Hauptspeicher sehr teuer. Zur
Speicherung von Massendaten wird darum ein billigerer Hintergrundspeicher eingesetzt.
Typischerweise ist das ein Plattenspeicher. Der Plattenspeicher – die “Festplatte” – kann sehr
viele Daten aufnehmen. Er ist aber vergleichsweise langsam und nicht direkt adressierbar.
Daten können nur in großen Einheiten und nicht byteweise gelesen und geschrieben werden.
Daten im Plattenspeicher müssen darum immer zuerst in den Hauptspeicher geladen werden,
bevor der Prozessor sie verarbeiten kann.
23
2.1.5 Externe Geräte
Den Plattenspeicher und die Ein/Ausgabegeräte wie Tastatur, Monitor und Maus, bezeichnet
man auch als externe Geräte. Sie liegen außerhalb von Prozessor und Hauptspeicher, die den
innersten Kern des Computers bilden. Die externen Geräte werden durch den Prozessor
gesteuert. Informationen, die über die externen Geräte eintreffen, werden vom Prozessor
angenommen und im Hauptspeicher und dann eventuell auf der Platte gespeichert.
2.1.6 Dateien und Programme
Die Masse der Daten liegt in Dateien auf der Festplatte. Dateien sind in Verzeichnissen (auch
“Ordner”, oder engl. Directory) organisiert. Eine Datei kann Daten beliebiger Art enthalten:
Texte, Musik, Graphiken, etc. Eine Datei kann auch ein Programm enthalten. Programme
können in den Hauptspeicher geladen und dann ausgeführt werden.
Das nennt man “Aktivieren des Programms”. Oft werden Programme durch kleine Symbole
(Bildchen) auf dem Bildschirm dargestellt. Klickt man sie an, dann werden sie aktiviert, also
von der Platte in den Hauptspeicher geladen und dann ausgeführt. Beim Ausführen liest der
Prozessor Befehl für Befehl des Programms und befolgt ihn. Ein Programm besteht aus einer
– meist sehr langen – Sequenz von Bits, die vom Prozessor Häppchenweise als Befehle
verstanden werden.
Ein Programm ist also eine Datei, deren Inhalt vom Prozessor verstanden wird.
Dateien mit einem anderen Inhalt können nicht aktiviert werden. Eine Textdatei
beispielsweise kann aber gedruckt werden, man kann sie mit Hilfe eines Editor betrachten
und verändern.
2.1.7 Das Betriebssystem
Das Betriebssystem startet Programme. Ein Programm wird also gestartet, indem der Inhalt
der Datei, die es enthält, in den Hauptspeicher kopiert wird und der Prozessor dann von dort
den ersten Befehl des Programms lädt und ausführt. Für das Kopieren in den Hauptspeicher
und die Umorientierung der CPU (Prozessor) auf die neue Aufgabe ist das Betriebssystem
zuständig. Das Betriebssystem ist selbst ein Programm, das ständig aktiv ist und dem
Benutzer und seinen Programmen dabei hilft die Hardware des Systems zu nutzen.
Meist startet das Betriebssystem ein Programm nicht aus eigenem Entschluß, sondern erst
nachdem der Benutzer es dazu aufgefordert hat. Heutzutage klickt man dazu meist ein kleines
Bildchen (“Icon”) an, das das Programm symbolisiert. Das System kennt die Koordinaten des
Bildchens und welches Programm (d.h. welche Datei) damit gemeint ist. Klickt man in
diesem Bereich, dann startet das System das Programm. Das Programm und das zughörige
Bild muß dem Betriebssystem dazu natürlich vorher bekannt gemacht werden, man sagt, das
Programm wird registriert.
2.1.8 Eingabe von Kommandos
Unsere einfachen Übungsprogramme funktionieren oft nicht richtig, und wenn doch, dann
aktivieren wir sie ein einziges Mal, nur um zu sehen, daß sie korrekt sind. Für diese
Fingerübungen wäre eine Registrierung beim Betriebssystem viel zu aufwendig. Wir
benutzen darum eine andere, einfachere Methode, um Programme zu starten:
die Kommandoeingabe.
Ähnlich wie der Prozessor seine Maschinenbefehle interpretiert (= versteht und ausführt), hat
das Betriebssystem eine Menge von Befehlen, die es direkt ausführen kann. Die Befehle
nennt man “Kommandos”. Es sind Texte, die man an der Tastatur eintippt und die dann vom
System befolgt werden. Die Kommandos werden von einem Teilprogramm des
Betriebssystems ausgeführt, das dazu selbst erst gestartet werden muß. Man nennt es
allgemein “Kommandointerpretierer”.
In einem Windowssystem wird der Kommandointerpretierer “DOS–Fenster” oder ähnlich
24
genannt. Bei einem Linux/Unix–System nennt man ihn meist “Terminal”.
Ein Kommando besteht oft einfach aus dem Namen eines Programms. Genau genommen ist
es der Name der Datei die ein Maschenprogramm enthält. Tippt man ihn ein und schickt ein
Return hinterher, dann weiß das System, daß es das Programm ausführen soll.
2.2 Algorithmen und Programme
Algorithmus: Definition einer Daten–Verarbeitung
Ein Algorithmus beschreibt, wie eine komplexe Aufgabe als Folge von einfacheren Aktionen
gelöst werden kann. Es ist eine Handlungsanweisung, eine Aussage darüber “wie etwas
gemacht wird”. Ein Algorithmus beschreibt eine Problemlösung in Form von Einzelschritten.
Ein Backrezept ist ein gutes Beispiel für einen Algorithmus. In ihm wird Schritt für Schritt
beschrieben, wie aus den Backzutaten ein Kuchen gemacht wird.
Die Algorithmen, die uns hier interessieren, befassen sich allerdings nicht mit der Erzeugung
von Kuchen. Es geht darum aus Daten – den Eingabedaten – andere Daten – die
Ausgabedaten – zu erzeugen.
Programm: Syntax und Semantik
Programme sind Algorithmen, die in einer bestimmten festen Form aufgeschrieben sind. Die
Regeln, nach denen ein Programm aufgeschrieben wird, nennt man Programmiersprache. Die
feste Form und die strengen Regeln sind notwendig, da die Anweisungen des Programms von
einem Rechner (= Computer) ausgeführt werden sollen.
Programmiersprachen bestimmen stets zwei Aspekte eines Programms:
1. Syntax (Form): Die exakte Notation in der das Programm genau aufgeschrieben werden
muß.
2. Semantik (Inhalt): Welche Anweisungen sind möglich und was bewirken (bedeuten) sie
genau.
2.2.1 Maschinensprache und Maschinenprogramme
Die interessantesten Programme sind die, die von der Hardware eines Rechners, genauer
gesagt von dessen Prozessor, verstanden werden. Wer sonst will schon Programme
ausführen. Um zum Ausdruck zu bringen, daß eine Maschine die Programme versteht,
nennen wir sie genauer Maschinenprogramme. Die Anweisungen eines
Maschinenprogramms werden also vom Prozessor verstanden. Sie sind darum in einer ihm
angenehmen Form stets als Folgen aus 0–en und 1–en zusammengesetzt: Befehle in Form
von Bitmustern, von denen jedes eine Bedeutung hat, die auf die Fähigkeiten des
Prozessortyps zugeschnitten ist. Die Fähigkeiten eines Prozessors darf man dabei nicht
überschätzen. Viel mehr als einfache arithmetische Operationen, das Verschieben von Daten
(auch wieder Bitmuster) von einer Speicherstelle zur anderen und das Laden neuer
Anweisungen von bestimmten Speicherstellen im Hauptspeicher ist nicht möglich.
Menschen sind kaum in der Lage Maschinenprogramme zu lesen, geschweige denn korrekte
Maschinenprogramme zu schreiben. Die Programme der frühen Pioniere der Informatik
wurden zwar in Maschinensprache verfasst. Sehr schnell hat man dann aber eingesehen, daß
Bitmuster, die das Verändern und Verschieben von Bitmustern beschreiben, nicht unbedingt
eine besonders angenehme Art sind einen Algorithmus zu beschreiben. Für Menschen ist es
einfach eine Zumutung sich auf die Ebene eines Prozessors zu begeben. Prozessoren, die in
der Lage sind Anweisungen auf dem Niveau von Menschen zu bearbeiten, sind dagegen
technisch und ökonomisch nicht realisierbar. Die Lösung des Problems besteht darin,
Menschen Programme in “menschlicher” Form schreiben zu lassen und sie dann in
Maschinensprache zu übersetzen.
25
2.2.2 Höhere Programmiersprache
Programme in einer höheren Programmiersprache enthalten Anweisungen, die sich in Form
und Inhalt an den Fähigkeiten von Menschen orientieren. Beispiele für solche Sprachen sind
Pascal, C, Java und eben die Sprache C++, mit der wir uns hier näher beschäftigen wollen.
Programme in höheren Programmiersprachen bestehen aus Anweisungen, die Menschen
verstehen und schreiben können. Der Nachteil ist, daß es keinen Prozessor – also keine
Maschine – gibt, die diese Anweisungen versteht. Die Programme der höheren
Programmiersprachen enthalten Anweisungen an eine virtuelle Maschine, also eine nur
gedachte Maschine.
Compiler: Programm in Maschinenprogramm übersetzen
Die Lücke zwischen einem Programm, das Menschen konzipieren und schreiben können und
dem Prozessor, der nur mit 0–en und 1–en hantieren kann, wird vom Compiler gefüllt.
2.2.3 Zusammenfassung: Algorithmus, Programm, Compiler
Fassen wir noch einmal die Begriffe in Zusammenhang mit Programmen zusammen:
• Algorithmus: Ein Algorithmus ist ein Verfahren nach dem eine Aufgabe erfüllt werden
kann.
• Programm: Ein Programm ist die Beschreibung eines Algorithmus’ in einer bestimmten fest
definierten Notation. Es ist entweder ein Maschinenprogramm oder ein Programm in einer
höheren Programmiersprache.
• Programmiersprache: Festlegung einer Notation für Programme. Es gibt
Maschinensprachen und höhere (Programmier–) Sprachen.
• Maschinenprogramm: Ein Programm, das vom Prozessor eines Rechners verstanden und
ausgeführt werden kann.
• Programm in einer höheren Programmiersprache: Ein Programm in einer höheren
Programmiersprache kann von Menschen geschrieben und verstanden werden.
• Compiler: Ein Programm das ein Quellprogramm in ein äquivalentes Maschinenprogramm
übersetzt.
• Quellprogramm: Ein Programm in einer höheren Programmiersprache, das vom Compiler
in ein Maschinenprogramm übersetzt wird, nennt man Quellprogramm. Es ist die “Quelle” ¨
des Übersetzungsvorgangs.
2.3 Zahlendarstellungen
So wie Befehle in Maschinensprache in einer Gruppe von einzelnen Bits der Werte ‘0’ oder
‘1’ dargestellt werden, werden auch Zahlen in mehreren 8 Bit Gruppen (Bytes) dargestellt.
Wir sind gewohnt, Zahlen im Dezimalsystem darzustellen und verwenden dafür die zehn
Ziffern (digits) 0, 1, 2, 3, 4, 5, 6, 7, 8, 9. Um diese als Bitfolgen zu codieren, benötigen wir
mindestens 4 Bits, also ein halbes Byte. Ein halbes Byte nennt man ein Nibble (oder
Halbbyte).
Mit einem Nibble lassen sich jedoch 24 = 16 Ziffern codieren. Deshalb führt man weitere 6
Ziffern ein. Da diese auf heutigen Tastaturen nicht vorhanden sind, verwendet man dafür die
Zeichen A, B, C, D, E und F.
26
BCD-Code mit den Pseudotetraden A bis F.
Obenstehende gibt die übliche Codierung dieser 16 Ziffern als sogenannte Tetraden wieder.
Man nennt diesen Code, eingeschränkt auf die Dezimalziffern, BCD-Code (binary coded
decimals).
Für unsere Zwecke ist aber eine andere Darstellung relevanter: die Dualzahl-Darstellung,
denn mit dieser Darstellung „rechnen“ Rechner.
Zahlen(werte) werden in Positionsschreibweise dargestellt, im sogenannten
Stellenwertsystem. Das folgende Bildungsgesetz beschreibt die Darstellung:
Falls i ≥0 ist, erhalten wir die Darstellung einer ganzen positiven ganzen Zahl. Die Zahl Y ist
die Basis des Zahlensystems und i gibt die Stellen der Ziffern an; xi ist der Wert der i-ten
Ziffer. Es gibt genau Y Ziffern.
Im Dezimalsystem ist uns diese Darstellung wohlbekannt.
So lesen wir die dezimale Zahl 1097 als eine Zahl mit 1*1000 + 0*100 + 9*10 + 7.
Mit der Basis Y = 10 (zehn) sind also die Koeffizienten xi wie folgt:
x3 = 1, x2 = 0, x1 = 9, x0 = 7
Das duale Zahlensystem hat die Basis 2 und kommt deshalb mit zwei Ziffern 0 und 1, den
Bits, aus.
Ein Beispiel im Dualsystem: Y=2, x3 = 1, x2 = 0, x1 = 1, x0 = 1
Z = 10112
Dargestellt im Dezimalsystem ist Z = 1110, denn mach den Stellengewichten gilt
1•8 + 0•4 + 1•2 + 1•1 = 1110.
Die Position (Stelle) einer Ziffer legt fest, wieviel sie zum Wert der dargestellten Zahl
beiträgt. In diesen Stellenwertsystemen wird also eine Zahl durch eine Folge von Ziffern xi
beschrieben. Der Ziffernvorrat ist so groß wie die Basis. Im Hexadezimalsystem mit der
Basis 1610 benötigen wir also 16 Ziffern. Den Hexadezimalziffern
A, B, C, D, E und F sind die Werte
1010, 1110, 1210, 1310, 1410 und 1510 zugeordnet.
Mittels Konvertierungsverfahren (z.B. dem Horner Schema) lassen sich Zahlen von einem in
ein anderes Zahlensystem umwandeln.
2.3.1 Hornerschema
Das Horner-Schema ist eine effiziente Methode, Zahlen aus einem Stellenwertsystem zur
Basis Y in das 10er System umzuwandeln. Dazu wird die sukzesive Multiplikation wie folgt
verwendet:
(xN xN-1...x1 x0 ) = (((xNY+xN-1 )Y+xN-2 )Y+...+x1 )Y+x0
Beispielsweise wandlen wir damit folgende Zahlen in das 10er System um: 1213 und 13C15:
120 3
= (( 1*3 + 2)*3 + 0) = 1510
13C15
= ((1*15 + 3)*15 + 12) = 28210
In der Umkehrung lassen sich die Zahlen wieder durch ausdividieren zurückrechnen:
1510 : 3 = 5 Rest 0
5 : 3 = 1 Rest 2
1 : 3 = 0 Rest 1 => Die Zahl im 3er System lautet 120 3
27
28210 : 15 = 18 Rest 12;
12 entspricht C
18 : 15 = 1 Rest 3
1 : 15 = 0 Rest 1 => Die Zahl im 15er System lautet 13C15
Ebenso lassen sich alle Rechenoperationen, wie wir sie vom Dezimalsystem her kennen
(Addition, Subtraktion, Multiplikation und Division) in all diesen Zahlensystemen durch
stellenweises Rechnen mit Weitergabe von Überträgen formulieren.
Negative und positive Zahlen werden durch Hinzufügen (meist durch Vorausstellung) eines
Vorzeichens unterschieden, üblicherweise + und -, man kann im Dualsystem aber auch die
Bits 0,1 als Vorzeichen nehmen (0 positiv, 1 negativ). Man nennt dies die „Vorzeichen mit
Betrag“-Darstellung.
Beispiel:
010112 ->+ 1110
110112 ->- 1110
Auch gebrochene Zahlen (Kommazahlen) lassen sich mit dem Stellenwertsysteme darstellen.
2.4 Übersicht zu C und C++ .
2.4.1 C
als Systemprogrammiersprache ist
• mit Unix „groß geworden“,
• ist nicht herstellerspezifisch,
• wurde anfangs (Mitte 70er) kostenlos im Hochschulbereich verteilt:
o kein Support, aber hacking the kernel für jeden
o ganze Generation von Absolventen mit Unix und C aufgewachsen
o diese sorgte später in der Industrie für Verbreitung
Es existieren Implementierungen
• zunächst im technisch-wissenschaftlichen Bereich (DEC PDP-11)
• dann im gesamten Workstation Sektor
• mittlerweile auf praktisch allen Plattformen von Mikro bis Mainframe
2.4.2 C++
C++ ist eine Obermenge von C:
• alle C++ Compiler können genauso gut C
Ein sanfter Übergang von C ist möglich:
• C C++, prozedural objektorientiert
• sichert Ausbildungsinvestments von Firmen
Oft wird aber C++ nur als „besserer C“ verwendet:
• man kann offiziell C++ programmieren
• Objektorientierung ist "in"
• und in Wirklichkeit ist es ganz konventionelles C
• Umdenken ist nämlich gerade für alte Hasen manchmal schwierig
28
Bild 2.
Entwicklung von C++
29
3 Erstellung eines Programmes
3.1 Programme: Analyse, Entwurf, Codierung
Ein Programm soll in der Regel ein Problem lösen oder eine Aufgabe erfüllen. Als erstes
muß darum das Problem oder die Aufgabe analysiert werden. Als nächstes überlegt man sich,
wie die Aufgabe gelöst werden soll: man entwirft ein allgemeines Vorgehen zur Lösung und
ein Programmkonzept. Schließlich muß noch der Quellcode geschrieben werden: das
Programm wird codiert, oder wie man auch sagt implementiert.
Betrachten wir als Beispiel die Umwandlung von Temperaturwerten von Grad Fahrenheit in
Grad Celsius.
3.1.1 Analyse
Am Anfang der Analyse wird die Problemstellung präzise definiert:
Das Programm soll eine ganze oder gebrochene negative oder positive Zahl einlesen, sie als
Gradangabe in Fahrenheit interpretieren und den eingegebenen Wert und den entsprechenden
Wert in Celsius ausgeben.
Als nächstes macht man sich mit dem Problemfeld vertraut, indem man sich die zur Lösung
notwendigen Informationen besorgt. In unserem Fall sagt uns ein elementares Physikbuch
wie Fahrenheit in Celsius umgewandelt wird:
tC = (tF−32)·5/9
3.1.2 Entwurf
Nachdem in der Analyse festgestellt wurde, was zu tun ist und die zur Lösung notwendigen
Informationen besorgt hat, kann man sich jetzt dem wie zuwenden: dem Entwurf. Im Entwurf
wird festgelegt, wie das Programm seine Aufgabe konkret lösen soll.
Bei einer Berechnungsaufgabe wie in unserem Beispiel wird hier der Algorithmus festgelegt:
Algorithmus zur Umwandlung von Fahrenheit in Grad:
1. Grad–Fahrenheit in Variable f einlesen
2. c mit dem Wert (Wert(f)−32)·5/9 belegen
3. Wert von f und c ausgeben
3.1.3 Codierung
Bei der Codierung (Implementierung) wird der Algorithmus aus dem Entwurf mit einem
Editor in der korrekten C++–Syntax niedergeschrieben, mit einem Compiler übersetzt und
mit einem Linker zu einem ausführbaren Programm gebunden.
30
Bild 3.
Der Entwicklungsprozeß
Es reicht eine frei verfügbare Umgebung, bestehend aus Übersetzer und Debugger aus, um
die Aufgaben zu Hause vorbereiten und testen zu können.
Quellen: Unix-Shell mit GNU C++ Übersetzer und Debugger: www.cygwin.com
31
Den Prozeß der Erstellung eines ausführbaren Programms, ausgehend von einem C++
Quellprogramm kann man wie folgt verdeutlichen:
Bild 4.
Erstellung eines Programms
Bibliotheken
LIB
LIB
LIB
H
H
H
Editor
CP
Compile
OB
CP
Compile
OB
Editor
Linker
EX
3.1.4 Der Editor
Am Anfang steht das Programm als Idee im Kopf der Programmiererin. Mit Hilfe eines
weiteren (Hilfs–) Programms, das Editor genannt wird, tippt man es ein und speichert es dann
in einer Datei (engl. File). Ein Editor ist ein Programm, eine Art “kleines einfaches Word”,
das keine Formatierungen vornimmt und die eingegebenen Zeichen im ASCII–Code in einer
Datei speichern kann. Der ASCII-Code ist eine einfache Form zur Speicherung von Texten.
Im ASCII–Code gibt es große und kleine Buchstaben (“A” und “a”), Zahlen und einige
wenige Sonderzeichen, es gibt aber keine unterschiedlichen Schriftgrößen (10, 12, 14 Punkte,
etc.) oder unterschiedliche Schriftarten (fett, unterstrichen, schräg, etc.).
Am Ende des Editierens steht dann das Programm als Quellcode in der Datei. Man nennt das
Programm in dieser Form Quellprogramm.
3.1.5 Compiler
Das Quellprogramm wird z.B. mit GNU C++ GCC in Object files übersetzt.
3.1.6 Linker
Mit einem Linker werden die verschiedenen Object files und verschiedener
Bibliotheksdateien zusammengebunden, logische Adressen aufgelöst und ein ausführbares
Programm erzeugt.
3.1.7 Debugger
Mit einem Debugger kann das fertig übersetzte Programm schrittweise durchgearbeitet
werden und auf die Funktionalität überprüft werden.
Jeder Schritt hat typische Aktivitäten, die durch Werkzeuge unterstützt werden.
Für das Kodieren, Übersetzen und Testen wird im Praktikum VC++ eingesetzt, eine
integrierte Entwicklungsumgebung, die Editor, Compiler, Linker und Debugger enthält..
32
Diese Umgebung kann für eingeschriebene Studierende des Fachbereichs Informatik von der
FHD bezogen werden.
3.2 Form eines C-Programms
Untenstehende Tabelle listet 32 Schlüsselwörter auf, die mit der formalen C-Syntax
zusammen die Programmiersprache C bilden.
3.2.1 Schlüsselwörter in ANSI-C
Bild 5.
Schlüsselwörter in ANSI-C
Basisdatentypen
Ergänzungen zu Basisdatentypen
Datentypen
Speicherklassen
Qualifizierer
Kontrollstrukturen
Sprunganweisungen
Operatoren
char,int,float,double,void
short,long,signed,unsigned,
enum,struct,union,typedef
auto,extern,register,static
const,volatile
if,while,else,case,switch,default,do,for
break,continue,return,goto
sizeof,new,delete,operator,this
3.2.2 Datentypen
Bild 6.
Es gibt 5 grundlegende Datentypen:
Zeichen
Ganze Zahlen
Gleitkommazahlen
Gleitkommazahlen doppelter Genauigkeit
Ohne Wert
characters
integer
float
double
void
Größe und Bereich dieser Datentypen können abhängig vom Prozessor und Compiler
variieren.
Allerdings ist ein char immer 1 Byte groß
Durch modifier können die Grundtypen angepaßt werden
short, long, signed, unsigned.
Üblich sind folgende Größen (bei 32 Bit Prozessoren)
Bild 7.
Datentypen
char x
signed char x
unsigned char x
short x
signed short x
unsigned short x
int x
signed int x
unsigned int x
long x
signed long x
unsigned long x
1 Byte
1 Byte
1 Byte
2 Byte
2 Byte
2 Byte
(2) oder 4 Byte
(2) oder 4 Byte
(2) oder 4 Byte
4 Byte
4 Byte
4 Byte
float x
double x
long double x
4 Byte
8 Byte
10 Byte
33
enum Aufzählungstyp, z.B.
enum Wochentag {Montag, Dienstag, Mittwoch, Donnerstag, Freitag};
3.2.3 Allgemeine Form eines C Programms
Alle C-Programme bestehen aus einer oder mehreren Funktionen. Die einzige Funktion, die
vorhanden sein muß, heißt main(), sie ist die erste Funktion, die beim Start aufgerufen wird.
Bild 8.
Allgemeine Form eines C Programmes
Glo globale Deklarationen
Re Returntyp main (Parameterliste)
{
Anweisungsfolge
}
Returntyp f1(Parameterliste)
{
Anweisungsfolge
}
Returntyp f2(Parameterliste)
{
Anweisungsfolge
}
….
Der Funktionskopf besteht aus
• dem Funktionstyp, d.h. dem Datentyp des gelieferten Resultats,
• dem Funktionsnamen, welcher den Regeln der Namensgebung in C/C++ für
Variablen, Funktionsnamen etc. (allg. Identifier) unterliegt, sowie
• der Parameterliste eingefasst von Klammern (..). Die Parameterliste kann leer sein,
dann steht zwischen den Klammern (diese sind obligatorisch!) die Bezeichnung für
den leeren Datentyp (void), oder
• sie kann einen oder mehrere Parameter umfassen. Sind Parameter vorhanden, so
müssen diese mit ihrem Datentyp und Namen aufgeführt sein.
• Der Funktionskörper ist von geschweiften Klammern {..} umgeben (syntaktisch ist
alles, was mit {..} eingefasst ist, ein Block).
• Zu Beginn des Blockes stehen die Definitionen und Deklarationen von Variablen,
• danach folgen die Anweisungen.
3.2.4 Erstes Beispiel Hello_world
Listing 2.
Klassiker von K&R
#include <iostream>
using namespace std;
int main()
{
printf("eigenes hello world test\n");
cout << "noch eine Ausgabe" <<endl;
return 0;
}
Es wird von Library Funktionen Gebrauch gemacht, die beim Linken der kompilierten
Sourcen dazugebunden werden.
34
3.3 Wie starte ich ein erstes C/C++ Programm unter GCC
Jetzt ist der Text des Programms erstellt und in einer Datei gespeichert. Der Compiler kann
aktiviert werden, um es in ein ausführbares (Maschinen–) Programm zu übersetzen. Wir
tippen dazu das Kommando
g++ hallo.cc
ein. g++ ist der Name des C++–Compilers und hallo.cc ist der Name der Datei, in der das zu
übersetzende Programm steht. Enthält das Programm keinen Fehler, dann erzeugt der
Compiler ein Maschinenprogramm in der Datei a.out. Es kann mit dem Kommando
./a.out
aktiviert werden und sollte dann die Ausgabe Hallo ! erzeugen. a.out ist hier der Name der
Datei, in die der Compiler das Maschinenprogramm schreibt und ./ bedeutet “das aktuelle
Verzeichnis, der aktuelle Ordner”. Wir aktivieren also das Programm a.out in dem
Verzeichnis (Ordner) in dem wir uns gerade befinden.
3.3.1 Fehlermeldungen des Compilers
Wenn bei der Eingabe des Programms ein Fehler gemacht wurde, wird der Compiler sich mit
einer oder mehreren Meldungen beschweren. Die Meldungen geben stets einen Hinweis auf
die Ursache des Fehlers. Man sollte die Fehlermeldungen lesen und versuchen ihren Sinn zu
verstehen.
3.3.2 Compiler-Optionen
Das vom Compiler aus der Quelldatei erzeugte Maschinenprogramm steht hier in einer Datei
namens a.out. Man sagt dazu auch das Programm heißt a.out. Wenn dieser Name nicht
gefällt, dann kann man mit der Option –o (o wie “Output”) der erzeugten Datei ein beliebiger
anderer Name gegeben werden. Beispielsweise erzeugt das Kommando
g++ -o hallo hallo.cc
ein ausführbares Programm in der Datei hallo, das dann natürlich mit
./hallo
aktiviert wird.
3.3.3 Syntaktische Fehler – Verstöße gegen die Regeln der
Programmiersprache
In der Regel wird man es nicht schaffen, ein Programm gleich beim ersten Versuch korrekt
einzutippen. Bei Programmen, in denen nicht penibel alle Regeln der Sprache C++
eingehalten werden, verweigert der Compiler die Übersetzung. Vergisst man auch nur ein
Komma, oder schreibt ein Komma an eine Stelle, an der ein Semikolon stehen muß, wird das
Programm nicht vom Compiler übersetzt. Stattdessen reagiert er mit einer Fehlermeldung.
Nehmen wir an, daß die vierte Zeile in unserem kleinen Beispiel nicht – wie es richtig wäre –
mit einem Semikolon, sondern mit einem Komma beendet wird:
Listing 3.
syntaktischer Fehler
#include <iostream>
using namespace std;
int main ()
{
cout << "Hallo" << endl;
return 0;
}
Dann kommt prompt die Quittung in Form einer Fehlermeldung:
g++ hallo.cc
35
hallo.cc: In function ‘int main()’:
hallo.cc:5: parse error before ‘}’
Die wesentliche Information dieser Fehlermeldung ist, daß der Compiler vor der
geschweiften Klammer in Zeile 5 des Programms Probleme hat, die er parse error nennt.
Damit meint er, daß die Analyse des Textes (engl. To parse = zergliedern, grammatikalisch
analysieren) gezeigt hat, daß der Text nicht den syntaktischen Regeln der Sprache C++
entspricht. Er enthält einen Syntaxfehler.
3.3.4 Semantische Fehler – Inhaltliche Fehler in formal korrekten
Programmen
Manchmal wird der von uns eingegebene Quellcode vom Compiler ohne Fehlermeldung
übersetzt, aber das erzeugte Programm verhält sich nicht so wie erwartet. Beispielsweise
wollen wir, daß “Hallo” ausgegeben wird, tatsächlich erscheint aber “HallX” auf dem
Bildschirm. Man sagt, das Programm enthält einen semantischen (inhaltlichen) Fehler. Es tut
etwas, aber nicht das was wir von ihm erwarten. In diesem Fall ist die Ursache schnell
festgestellt. Wir haben uns sicher einfach nur vertippt und das Programm sieht
folgendermaßen aus:
Listing 4.
Mit semantischem Fehler
#include <iostream>
using namespace std;
int main ()
{
cout << "HallX" << endl;
return 0;
}
Im Gegensatz zu vorhin ist hier das Programm auch mit dem Tippfehler syntaktisch korrekt
und kann übersetzt werden. Der Compiler kann ja nicht wissen, was wir eigentlich ausgeben
wollten. Er hält sich an das was im Quellprogramm steht. Leider sind die semantischen
Fehler meist nicht so trivial, wie in diesem Beispiel. Ein semantischer Fehler wird in der
Regel nicht durch Tippfehler, sondern durch “Denkfehler” verursacht. Man glaubt das
Programm sei korrekt und mache das, was es machen soll, in Wirklichkeit macht es zwar
etwas, aber nicht das, was wir wollen, sondern das was wir aufgeschrieben haben und von
dem wir nur dachten, es sei das, was wir wollen. Semantische Fehler sind die ernsten, die
richtigen Fehler. Man läßt darum meist die Kennzeichnung “semantisch” weg und spricht
einfach von “Fehler”. Nur wenn ausdrücklich betont werden soll, daß es sich um etwas so
triviales wie einen syntaktischen Fehler handelt, spricht man von einem “Syntax–Fehler”.
36
Wie starte ich ein erstes C/C++ Programm unter MS VC++?
Arbeitsbereich enthält Projekte
Projekt enthält versch. Dateien
–
–
.CPP
–
–
.OBJ
Objektcode
.LIB
Bibliotheken
Quellcode-Dateien
.RC
RessourcenDateien
Vorgehensweise:
– Projekt erstellen mit Datei | Neu
| Projekte (in PG I
Win32 Console Application)
– Quellcode-Dateien anlegen mit
Datei | Neu | Dateien | C++
(dem Projekt hinzufügen)
37
1.
2.
3.
4.
5.
6.
7.
Aufruf MS VCC
New console application
Empty project
New c++ appl. Mit Namen
Code implementieren
compilieren/linken
laufen lassen/debuggen
3.3.5 Informationen finden
Infos über Sprache und Werkzeug
–
–
Stichwortsuche oder Inhaltsverzeichnis
–
Cursor auf Fehlermeldung, F1 drücken
Schlüsselwort oder Namen markieren,
F1 drücken: zeigt passende Hilfe-Seite
Infos über das eigene Programm
–
–
Klassenübersicht und Browser
–
Sprung zu Orten der Verwendung
Sprung zum Ort der Definition
(auch in Bibliotheks-Header)
38
.
3.4 Wie starte ich ein erstes Programm unter VC++ Express?
Wir rufen Visual Express durch anklicken auf.
In dem Reiter Datei wählen wir <neu> und dann <Projekt>.
39
Wir wollen ein C++ Projekt als Konsolenanwendung erstellen. Unten geben wir den Namen
des Projekts ein. Hier zum Beispiel My_Prog.
40
Wir brauchen nun auch noch eine Datei, in der wir unser Programm ablegen können (ein
Projekt kann mehrere Dateien enthalten). Wir fügen die Datei dem Projekt z.B. unter dem
selben Namen wie der des Projekts hinzu.
41
In der Ansicht wählen wir den Projektmappen-Explorer und sehen rechts im Explorer unsere
Datei.
42
In das leere Arbeitsblatt schreiben wir unser Programm und speichern es ab.
43
In dem Reiter <Erstellen> übersetzen wir unser Programm
44
In dem Reiter Debug wählen wir <Starten ohne Debuggen> und lassen unser Programm
laufen.
Für viele Befehle gibt es Abkürzungssymbole, die wir auch direkt wählen können.
45
3.5 Beispiele
3.5.1 Beispiel Maßeinheiten Umwandlung
Listing 5.
Beispiel Maßeinheiten-Umwandlung
#include <iostream>
using namespace std;
int main ()
{
float f;
// Variable, enthaelt Grad in Fahrenheit
float c;
// Variable, enthaelt Grad in Celcius
cout << "Bitte Grad in F : ";
//Grad in Fahrenheit einlesen:
cin >> f;
//Grad in Celsius berechnen:
c = ((f - 32) * 5) / 9;
//Grad in F. und C. ausgeben:
cout << f << " Grad Fahrenheit ist ";
cout << c << " Grad Celsius " << endl;
return 0 ;
}
Das Programm beginnt mit der Zeile
#include <iostream>
Dies ist eine Anweisung an den Präprozessor, an dieser Stelle den Inhalt des Textfiles
iostream einzufügen. Es enthält Deklarationen aller I/O Funktionen.
Mit der Zeile
using namespace std;
wird der Zugriff auf die I/O Funktionen erleichtert. Man könnte auch weiter unten statt der
kompakten cout-Zeilen folgendes schreiben.
std::cout << std::endl << “Zahl...“;
Dem folgt die sogenannte main–Funktion, die in jedem Programm als Startpunkt vorhanden
sein muß:
int main ()
{ ...
}
In ihrem Codeblock stehen die Anweisungen, die ausgeführt werden sollen. Dabei ist alles
hinter // ein Kommentar. Kommentare werden vom Compiler ignoriert. Sie dienen lediglich
dem Verständnis menschlicher Leser.
Die main–Funktion beginnt mit Variablendeklarationen:
float f;
float c;
Hier werden zwei Variablen für Fließkommazahlen angelegt (definiert). Variablen sind
Speicherplätze für Daten. In C/C++ werden die Namen von Variablen, Funktionen, Labeln
und anderen benutzerdefinierten Objekten Bezeichner genannt. Das erste Zeichen des
Bezeichners muß ein Buchstabe sein.
Die Ausgabeanweisung
cout << "Bitte Grad in Fahrenheit";
gibt den Text (die Zeichen innerhalb der Anführungszeichen) aus und die Eingabeanweisung
cin >> f;
liest einen Wert in die folgende Variable (hier f) ein. Mit der Zuweisung
c = ((f - 32) * 5) / 9;
wird aus dem Wert in f der gesuchte Wert berechnet und in c abgelegt.
46
3.5.2 Variablen, Rechnen und Ein-/Ausgabe
Listing 6.
Quadratzahlen
#include <iostream>
using namespace std;
int main ()
{
int n, quadrat;
cout << "Bitte eine natürliche Zahl eingeben: ";
cin >> n;
quadrat = n * n;
cout << "Gelesen wurde n = " << n << endl
<< "Dann ist n * n = " << quadrat << endl;
return 0;
}
$ quadrat
Bitte eine natürliche Zahl eingeben: 6
Gelesen wurde n = 6
Dann ist n * n = 36
$
// Deklaration von Variablen
// Eingabedialog
// Berechnung
// Ergebnis ausgeben
47
4 Variablen
4.1 Namen
Einzelne Bestandteile eines Programms werden über Namen identifiziert.
Vorschriften zur Namensbildung:
• Namen bestehen aus Buchstaben, Ziffern und Unterstrichen "_" ;
• sie müssen mit Buchstabe oder Unterstrich "_" beginnen;
• Groß- und Kleinbuchstaben werden unterschieden;
• die Anzahl signifikanter Zeichen ist implementierungsabhängig (i.a. >30);
• teilweise existieren zusätzliche Einschränkungen für externe Namen (Linker,
Assembler, andere Sprachen, ...)
• Reservierte C++ Worte sind zur Namensgebung verboten. (z.B. if, main, while, const,
class, …)
Über diese Vorschriften hinaus, sind folgende Punkte Empfehlungen, die sich im praktischen
Einsatz als nützlich (lebenswichtig) erwiesen haben:
• Name sollen nicht mit Unterstrich "_" beginnen.
• Namen sollen natürlichsprachig, problemnah, aussagekräftig sein.
• Inhaltsleere Namen (i, n) sind allenfalls für lokale Schleifenzähler etc. sinnvoll.
• Vorsicht mit Abkürzungen: die Verständlichkeit darf nicht leiden.
• Oft werden projektbezogene Konventionen vorgegeben, die den firmenüblichen
Namenskonventionen Rechnung tragen.
Generell muss man sich merken:
Namen sind nicht Schall und Rauch, sondern entscheidend über die Lesbarkeit von
Programmen und sind somit ein Qualitätsmerkmal der erzeugten Software!
Beispiele:
Bild 9.
Schlechte Variablennamen
falsch
1terVersuch
Zeile-1
Zähler
richtig
schlecht
s
cnt
_open
atoi
zins
zb
gut
saldo
counter
openFile
alfanum_to_int
ascii2int
zinsSatz
zinsBetrag
4.2 Variablen und Konstanten
Eine Variable ist ein benannter Ort im Speicher, in dem ein Wert gehalten werden kann. Alle
Variablen müssen vor Gebrauch deklariert werden. Variablen haben einen der genannten 5
Grundtypen, eventuell per Modifier modifiziert.
• zu verschiedenen Zeitpunkten können Variablen verschiedene Werte enthalten;
48
•
alle Werte, die über die Programmlaufzeit hinweg von einer Variablen eingenommen
werden, gehören zum gleichen Typ.
Konstanten sind Speicherzellen, die während der gesamten Programmlaufzeit denselben
Wert enthalten. Im Programmcode fest eingetragene Werte werden ebenfalls als Konstanten
bezeichnet.
Konstanten sind beschrieben durch
• Name (engl.: identifier),
• Typ (engl.: type) und
• Wert (engl.: value).
4.3 Variablendeklaration
Variablen werden an 3 Orten deklariert:
• innerhalb von Funktionen (lokale Variablen)
• in der Definition von Funktionsparametern (formale Parameter)
• außerhalb aller Funktionen (globale Variablen).
4.3.1 Lokale Variablen
// so werden Variablen deklariert, z.Teil mit modifier, lokal oder global, je nach Kontext:
Listing 7.
Variablendeklaration
int
short
unsigned
double
i,j,l;
int
si;
int
ui;
balance, profit, loss;
Listing 8.
lokale Variablendeklaration
void func1(void)
{
int x;
x = 10;
}
void func2(void)
{
int x;
x = -199;
}
Die Variable x ist jeweils lokal im Block bekannt. Beim Verlassen des Blockes wird sie
gelöscht. Lokale Variable werden auf dem sogenannten Stack gespeichert.
In C müssen alle Deklarationen vor den ersten Zuweisungen oder Aufrufen stehen, in C++
können Sie beliebig plaziert werden.
49
Listing 9.
C vs. C++
/* Diese Funktion liefert einen Fehler zurück, wenn sie als C-Programm kompiliert wird, wird
als C++- Programm aber ohne Probleme kompiliert.
*/
void f(void)
{
int i;
i = 10;
int j;
/* diese Zeile verursacht einen Fehler */
j = 20;
}
4.3.2 Formale Parameter
Wenn einer Funktion Daten übergeben werden sollen, dann werden sie im Funktionskopf als
formale Parameter deklariert. Sie verhalten sich wie lokale Variable innerhalb der Funktion.
50
Listing 10.
formale Parameter
/* Liefere 1 zurück, wenn c Teil eines Strings s ist; sonst 0 */
int is_in(char *s, char c)
{
while(*s)
if(*s==c) return 1;
else s++;
return 0;
}
4.3.3 Globale Variablen
Listing 11.
Globale Variablen
#include <stdio >
using namespace std;
int count;
void func1(void);
void func2(void);
/* count ist global */
//Funktionsdeklarationen
int main(void)
{
count = 100;
func1();
return 0;
}
void func1(void)
{
int temp;
temp = count;
func2();
printf("count ist %d", count); /* druckt 100 */
}
void func2(void)
{
int count;
for(count=1; count<10; count++)
putchar('.');
}
Globale Variablen sind im ganzen Programm bekannt.
4.3.4 Konstanten, Const-Deklaration (Zugriffs Modifier)
Variablen vom Typ const können vom Programm nicht geändert werden. Es kann allerdings
ein Anfangswert zugewiesen werden. Dem Compiler steht es frei, Variable dieses Typs im
ROM (read only memory) anzulegen.
Listing 12.
Const mit expliziter Initialisierung
const int a=10;
const int max=10;
char line[(max+1)*10];
Konstantenausdrücke wie oben werden zur Übersetzungszeit berechnet und vom Compiler
als Konstante behandelt.
51
Das folgende Programm zeigt einige Beispiele für Zahlenkonstanten:
Listing 13.
octHexOutput.cpp , Zahlenkonstanten
#include <iostream>
using namespace std;
int main()
{
int i;
float f;
i = 017;
cout << "017: " << i << endl;
i = 0xa7;
cout << "0xa7: " << i << endl;
f = 1.2e5;
cout << "1.2e5: " << f << endl;
f = -1.2E-2;
cout << "-1.2E-2: " << f << endl;
return 0;
}
$ octHexOutput
017: 15
0xa7: 167
1.2e5: 120000
-1.2E-2: -0.012
Bei Integerkonstanten bestimmt der Anfang die Zahlenbasis, die Endung den Datentyp:
Bild 10.
Zahlenbasis von Int Konstanten
Beispiel
18
-18
12345L
23103ul
017
0x18
0x1af
0x1AF
0x5a3ful
Datentyp
int
int
long int
unsigned long int
int
int
Int
Int
unsigned long int
Zahlenbasis
dezimal
dezimal
dezimal
dezimal
oktal (Basis=8)
hexadezimal (Basis=16)
hexadezimal (Basis=16)
hexadezimal (Basis=16)
hexadezimal (Basis=16)
Reelle Konstanten werden wie folgt notiert:
Bild 11.
Beispiel
12.34
12.34f
-12.34
12.34L
1.2e5
1.2e-5
Reelle Konstanten
Datentyp
double
float
double
long double
double
double
52
Die Zeichenkonstanten werden implementierungsabhängig kodiert (DOS, Unix: ASCII,
Windos: ANSI, MVS: EBCDIC, …).
Eine Zeichenkonstante wird in einfachen Hochkommata eingeschlossen; es existieren
Notationen für Sonderzeichen:
Bild 12.
Sonderzeichen
Beispiel
’a’
’A’
’\23’
’\x3e’
’\a’
’\b’
’\f’
’\n’
’\r’
’\t’
’\’’
...
Datentyp
Char
Char
Char
Char
Char
Char
Char
Char
Char
Char
Char
Zeichen
A
A
Oktal 23
hexadezimal 3e
Alert (Piep)
backspace
formfeed
newline
carriage return
horizontaler tab
’
Zeichenketten, strings sind eine Menge von Zeichen in “ “ eingeschlossen.
Mit const Qualifiern werden Variablen vor Veränderungen geschützt. Dies wird im Kapitel
10 näher besprochen.
Listing 14.
Const qualifier
#include <iostream>
using namespace std;
const double pi=3.14;
int main ()
{
cout << pi;
pi=2.48;
cout << pi << endl;
return 0;
}
compile time error:
error C2166: l-value specifies const object
4.3.5 Volatile modifier
Der Modifier volatile schützt die Variable vor Optimierungen durch den Compiler. So
könnten zum Beispiel mehrere aufeinanderfolgende Initialisierungen eines Port-Pins
herausoptimiert werden, da der Compiler keine Zuweisungen der Werte an andere Variablen
erkennt.
4.4 Speicherbedarf und Wertebereich
Es wurden weiter oben 5 grundlegende Datentypen vorgestellt:
Zeichen
characters
Ganze Zahlen
integer
53
Gleitkommazahlen
Gleitkommazahlen doppelter Genauigkeit
Ohne Wert
float
double
void
Sie „verbrauchen“ den angegebenen Platz bzw. haben den angegebenen Wertebereich:
Bild 13.
Speicherbedarf und Wertebereich
unsigned
signed
Z
Typ
char
short int
int
long int
float
R
double
long
double
Bit
8
16
32
32
32
64
80
Min
|Max|
Min
Max
0
255
65.535
0
4.294.967.295
0
4.294.967.295
0
Genauigkeit
±3.4 * 10-38
±1.7 * 10-308
±3.4 * 1038
±1.7 * 10308
1.2 * 10-7
2.2 * 10-16
±1.2 * 10-4932
±1.2 * 104932
1.1 * 10-19
-128
-32.768
-147.483.648
-147.483.648
|Min|
Max
127
32.767
147.483.647
147.483.647
Die o.a. Angaben sind implementierungsabhängig; vgl. limits.h und float.h.
Versucht man, ein Programm zu übersetzen, bei dem eine Konstante den o.a. Wertebereich
überschreitet, kann der Compiler das melden.
Listing 15.
wertebereich.cpp , Wertebereichsüberschreitung
#include <iostream>
using namespace std;
int main()
{
const int i1 = 123;
const int i2 = 12345678901234567890;
cout << "i1: " << i1 << endl;
cout << "i2: " << i2 << endl;
return 0;
}
zu groß
wertebereich.cpp: In function `int main()':
wertebereich.cpp:5: integer constant out of range
wertebereich.cpp:5: warning: decimal integer constant is so large that it is unsigned
Achtung: Wird eine Variable im Laufe einer Berechnung zu groß, passiert ein Überlauf! Das
kann zu Unendlichschleifen führen!
54
Listing 16.
ueberlauf.cpp , Wertebereichsüberschreitung zur Laufzeit
#include <iostream>
using namespace std;
int main()
{
int i = 2;
while ( i < 147483647 )
{
cout << "i: " << i << " i*i: " << i*i << endl;
i = i*i;
}
return 0;
}
$
$ ueberlauf
i: 2 i*i: 4
i: 4 i*i: 16
i: 16 i*i: 256
i: 256 i*i: 65536
Überlauf!
i: 65536 i*i: 0
i: 0 i*i: 0
i: 0 i*i: 0
Unendlichschleife!
i: 0 i*i: 0
i: 0 i*i: 0
i: 0 i*i: 0
…
4.5 Deklaration, Definition und Initialisierung
Variablen sind die „Träger“ von den Daten, die durch ein Programm berechnet werden.
Variablen müssen vor ihrer Verwendung im Programm deklariert werden.
Die allgemeine Form lautet:
Type variable_name = value;
Globale und lokale static Variablen (siehe unten) werden nur zu Beginn initialisiert, nicht
initialisierte werden automatisch auf Null gesetzt.
Lokale Variablen müssen jedes Mal bei Blockeintritt initialisiert werden. Nicht initialisierte
lokale Variablen sind undefiniert. !!
Eine Deklaration
macht dem Compiler die Variable bekannt –
eine Definition
reserviert außerdem Speicherplatz zur Ablage eines Wertes.
55
Listing 17.
Beispiele für Deklaration, Definition und Initialisierung
char Eingabe;
unsigned long int summe;
short index, zustand;
double akkumulator = 0;
const double pi = 3.14159,
e (2.71828);
const char newline ('\n');
// deklariert und definiert eine Variable
// ebenfalls eine Variablendefinition
// zwei Variablen vom selben Typ definieren
// definiert und initialisiert
// definiert 2 Konstanten
// alternative Form der Initialisierung
// definiert eine Zeichenkonstante
Achtung: Variable, die in einem Programm deklariert aber noch nicht initialisiert sind,
haben undefinierte Werte: dies ist bei Missachtung eine Ursache von unvorhersehbaren
Programmabläufen!
Listing 18.
nichtInitialisiert.cpp , Beispiel für nichtinitialisierte Variablen:
#include <iostream>
using namespace std;
main()
{
int i = 017; // definiert und initialisiert i
float f;
// deklariert + definiert f
cout << "i: " << i << endl;
cout << "f: " << f << endl;
return 0;
}
$
$ nichtInitialisiert.exe
i: 15
f: 2.52234e-42
Die Initialisierung von f fehlt:
somit hat f keinen definierten Wert!
4.6 Storage type Bezeichner
4.6.1 Extern
Separate Module können getrennt kompiliert und dann mit dem Linker zusammengefügt
werden. In C++ dürfen globale Variablen aber nur einmal definiert werden.
Lösung ist die Trennung von Deklaration und Definition. Mit
extern var-list;
kann eine Variable nur deklariert werden, mit dem Verweis, daß sie woanders definiert wird.
56
Listing 19.
Externe Deklaration
Datei I
int x, y;
char ch;
int main (void)
{
/* ... */
}
void func1(void)
{
x = 123
}
Datei II
extern int x, y;
extern char ch;
void func22(void)
{
x = y / 10;
}
void func23(void)
{
y = 10;
}
4.6.2 Lokale Static Variablen
Lokale Variablen verlieren nach Verlassen des Blocks ihren Wert. Static-Variablen behalten
ihren Wert zwischen Aufrufen bei.
Beispiel Reihengenerator:
Listing 20.
Reihengenerator mit lokaler static Variablen
int series(void)
{
static int series_num = 100;
series_num = series_num+23;
return series_num;
}
4.6.3 Globale static Variablen
Globale static Variablen sind nur in der Datei bekannt, in der sie erzeugt werden. In C++
sollte man lieber Namensräume (namespace) verwenden.
4.6.4 Register Variablen
Register Variablen werden, wenn möglich, in Prozessorregistern gespeichert. Normalerweise
kann das der Compiler für einige int- und/oder char-Variablen unterstützen. Ist das nicht
möglich, so werden diese Variablen wenn möglich vom Compiler bevorzugt behandelt.
Listing 21.
Register Variablen
register int temp, m;
temp = 1;
m= 3;
for ( ; e; e--) temp=temp + m;
57
5 Ein- und Ausgabe in C/C++
Zur Ein- und Ausgabe sind in C++
iostreams
eingeführt worden. Die von C bekannten E/A Funktionen aus
stdio.h
sind auch in C++ verwendbar, aber nicht so komfortabel. Wir werden sie später aber
behandeln, da man sie noch in vielen Programmen findet, die noch gewartet werden müssen.
Listing 22.
eaAllgemein.cpp
#include <iostream >
using namespace std;
main()
Zeilenvorschub
{
int iPar = 27;
double dPar = 3.5;
cout << dPar << iPar << 3 << endl;
cin >> iPar >> dPar;
cout << dPar << "
" << iPar << endl;
return 0 ;
}
// Standardbibliothek
// Definition mit Initialisierung
// ausgeben
// einlesen
Trennzeichen
Ausgabe:
Eingabe:
Ausgabe:
cout:
cin:
3.5273
87
11.345
11.345 87
die Umwandlung in ein geeignetes Format erfolgt automatisch
- abhängig vom Datentyp; der Operator << ist überladen
eingegebene Zeichenkette muß zum Typ der Argumente passen
- sonst wird die Zeichenkette nicht weiter gelesen; (cin >> x) ist dann 0
- Leerzeichen, Tab, Zeilenvorschub, Seitenvorschub und Return werden
überlesen
- Schreibweise der Zahlen: Zahlenbasis wie bei Konstanten, kein Suffix
Achtung:
Beim Ausgeben von so genannten „Shift-Ketten“ kann es zu unerwünschten Effekten
kommen: Teilausdrücke werden zwar von links nach rechts ausgegeben, die
Berechnungsreihenfolge ist aber nicht definiert.
Listing 23.
eaSeiteneffekt.cpp
wir werden die genaue
Bedeutung später
kennen lernen.
#include <iostream >
using namespace std;
int main()
{
int x = 2;
cout << "x = " << x << ", x**2 = " << (x *= x) << endl;
return 0 ;
}
Die Ausgabe im o.a. Beispiel ist nicht vorhersehbar, sie hängt davon ab, ob die Shift-Kette
58
von links nach rechts
(Resultat: x = 2, x**2 = 4)
oder von rechts nach links
(Resultat: x = 4, x**2 = 4)
ausgewertet wird (abhängig vom verwendeten Compiler).
5.1 Formatierte Ausgabe
Manipulatoren werden in den Ausgabestrom eingefügt. Sie bewirken keine Ausgabe,
sondern bewirken eine Zustandsänderung des Ausgabestroms. (wie Schalter).
Folgende Ausgabemanipulatoren existieren:
Bild 14.
manipulatoren
Manipulator
dec
hex
oct
flush
endl
setw(int n)
setprecision(int n)
setfill(char c)
Bedeutung
dezimale Ausgabe (default)
hexadezimale Ausgabe
oktale Ausgabe
Leeren des Puffers auf das Ausgabemedium
Ausgabe von ’\n’ und Aufruf von flush
n ist minimale Ausgabebreite (nur für
unmittelbar folgende Ausgabe, wird
danach auf 0 gesetzt)
n ist Ausgabegenauigkeit
c ist Füllzeichen
beeinflußt
Z
R
59
Listing 24.
Beispiel: manipulatoren.cpp
#include <iomanip.h>
int main()
{
cout << hex << 127 << endl;
cout << dec << 127 << endl;
cout << oct << 127 << endl << endl;
cout << setfill ('0')
<< setw(5) <<127<<endl;
cout << setw(2) <<127<<endl << endl;
cout << setprecision (4)
<< 123.264567 << endl
<< 3.2 << endl;
cout << setw (10)
<< setprecision (4)
<< 4.5 << endl;
return 0;
}
$ manipulatoren
7f
127
177
00177
177
123.3
3.2
00000004.5
60
Listing 25.
Beispiel: manipulatoren2.cpp
#include <iostream >
#include <iomanip.h>
using namespace std;
int main()
{
int x = 0xff;
cout << setw(15) << "dezimal:"
<< setw(10) << dec << x << '\n'
<< setw(15) << "hexadezimal:"
<< setw(10) << hex << x << '\n'
<< setw(15) << "oktal:"
<< setw(10) << oct << x << endl;
return 0;
}
$ manipulatoren2
dezimal:
255
hexadezimal:
ff
oktal:
377
$
61
6 Kontrollstrukturen
Dieser Teil führt in die Kontrollstrukturen von C++ ein. Eine Kontrollstruktur definiert, wie
die einzelnen Anweisungen ausgeführt werden, z.B.
• sequentiell,
• bedingt oder
• wiederholt.
Kontrollstrukturen steuern somit den Programmfluß. Den Abschluß bildet die Behandlung
von Ausnahmen.
6.1 Anweisungen und Blöcke
Einfache Anweisungen (statement) werden mit Semikolon abgeschlossen. Die "leere"
Anweisung besteht aus nur einem Semikolon.
Listing 26.
Typische Beispiele für Anweisungen:
int a, b;
a = b;
Summe = b + c;
Zaehler++;
push(27);
;
// Deklaration von a und b
// Zuweisung
// Inkrement-Anweisung
// Funktionsaufruf
// leere Anweisung
Ein Block (compound statement) ist eine Zusammenfassung von Anweisungen, die in
geschweifte Klammern eingeschlossen sind.
Die Deklarationen im Block gelten nur innerhalb des Blocks.
Ein Block braucht nicht durch ein Semikolon abgeschlossen zu werden.
Listing 27.
Typische Beispiele für Blöcke:
int a=3, b;
{
int x;
x = 2;
cout >> a*x;
}
if ( a>0 )
{
a++;
cout << a;
}
// blocklokale Deklaration
// Block, der nur ausgeführt wird, wenn a>0
6.1.1 Sequenz
Die Anweisungen eines Programms werden in Aufschreibungsreihenfolge durchlaufen.
Allgemeine Form:
statement1;
statement2;
Zuerst wird statement1 ausgeführt, danach statement2.
62
Listing 28.
Beispiele sequentielle Abarbeitung:
int i;
i = 18;
i = i -6;
cout << i << endl;
Durch Flußdiagramme kann der Ablauf eines Programms verdeutlicht werden. Dazu wird
jeder Anweisungsform eine Darstellung zugeordnet. Ein Flußdiagramm für eine Sequenz aus
zwei Anweisungen ist:
Bild 15.
Sequenz
statement 1
statement 2
6.2 Selektionsanweisungen, Verzweigungen
Soll eine Aktion nur ausgeführt werden, wenn gewisse Bedingungen zutreffen, also
Entscheidungen programmiert werden sollen, können Verzweigungen verwendet werden.
Selektionsanweisungen = Verzweigungen = Entscheidungen = bedingte Anweisungen,
6.2.1 if - else
Bild 16.
Allgemeine Form: if else Anweisungen
if (expression)
statement
expression wird ausgewertet. Ist der Wert true, wird statement ausgeführt.
if (expression)
statement_true
else
statement_false
expression wird ausgewertet.
Ist der Wert true, wird statement_true ausgeführt,
ansonsten statement_false.
63
Dieses Verhalten kann durch ein Struktogramm veranschaulicht werden:
Bild 17.
Struktogramme if-else
if (expression)
statement_true
else
statement_false
expression
!= 0
0
statement_true
statement_false
Ein Flussdiagramm für diese Entscheidung ist:
Bild 18.
Flußdiagramm für if-else
true
expression
statement_true
false
statement_false
Listing 29.
Beispiel: verzweigung.cpp
#include <iostream >
using namespace std;
int main()
{
int x;
cout << "Eingabe Interger: ";
cin >> x;
if ( x%2 == 0 )
cout << x << " ist gerade Zahl";
else
cout << x << " ist ungerade Zahl";
cout << endl;
return 0;
}
64
Das gleiche Programm in einer eher schwer lesbaren Form (besser nicht so programmieren):
Listing 30.
verzweigungUnleserlich.cpp
#include <iostream >
using namespace std;
int main()
{
int x;
cout << "Eingabe Interger: ";
cin >> x;
if ( !(x%2) )
cout << x << " ist gerade Zahl";
else
cout << x << " ist ungerade Zahl";
cout << endl;
return 0 ;
}
D.h.
if (expression)
ist gleichbedeutend mit
if (expression != 0)
Ein weiteres Beispiel
Listing 31.
Magisches Zahlenprogramm
#include <iostream>
#include <stdlib.h>
using namespace std;
int main(void)
{
int magic; /* magische Zahl */
int guess; /* Rate-Vorschlag des Benutzers */
magic = rand(); /* erzeuge die magische Zahl */
printf("Rate die magische Zahl: ");
scanf("%d", &guess);
if(guess == magic) printf("** Richtig **");
else printf("Falsch");
return 0;
}
Anweisungen werden in einem Programm nacheinander hingeschrieben. Werden zwei ifAnweisungen nacheinander benötigt, so muß geregelt werden, zu welchem if ein else gehört.
Regel: Ein else gehört immer zum unmittelbar vorangehenden if; dabei ist die
Blockstruktur zu beachten. Beispiel:
Bild 19.
richtig
if (n>0)
if (a>b)
z = a;
else
z = b;
Verschachtelte if else Anweisungen
falsch
if (n>0)
if (a>b)
z = a;
else
z = b;
richtig
if (n>0)
{
if (a>b)
z = a;
}
else
z = b;
Wie sehen für die
richtigen
Darstellungen die
Struktogramme aus?
65
6.2.2 else – if
Mehrfachverzeigungen können mit if-else-Ketten programmiert werden. Bei tiefen Ketten
wird auf die sonst vereinbarte Einrückung verzichtet.
Bild 20.
Allgemeine Form: if else Ketten
if ( expression_1 )
statement_1
else if ( expression_2 )
statement_2
else if ( expression_3 )
statement_3
else
statement_4
Listing 32.
Sollte immer verwendet werden
(ev. Fehlermeldung)!
Beispiel (Notenberechnung): mehrfachverzweigung.cpp
#include <iostream >
using namespace std;
int main()
{
int punkte;
float note;
cout << "Eingabe Punkte: ";
cin >> punkte;
if ( punkte <= 30 )
note = 5.0;
else if ( punkte <= 50 )
note = 4.0;
else if ( punkte <= 65 )
note = 3.0;
else if ( punkte <= 80 )
note = 2.0;
else
note = 1.0;
cout << "Note: " << note << endl;
return 0 ;
}
$ mehrfachverzweigung
Eingabe Punkte: 52
Note: 3
66
6.2.2.1 Fehlerquelle in Verbindung mit if
Durch die Verwechslung des Vergleichsoperators mit dem Zuweisungsoperators entstehen
Fehler:
Zuweisung: a ist nun immer gleich b
Bild 21.
Fehlerquelle =
nur wenn b ungleich 0 ist, wird Text
if (a = b)
cout << „a ist gleich b“;
ausgegeben
Gemeint war:
if (a == b)
cout << „a ist gleich b“;
Vergleich
Ein Semikolon zuviel kann bewirken, dass eine Anweisung immer ausgeführt wird:
Bild 22.
Fehlerquelle ;
if (a == b);
cout << „a ist gleich b“;
Das Semikolon schließt die if-Anweisung ab.
Der Text wird unabhängig vom erfüllt sein der
Bedingung ausgegeben.
6.2.3 Bedingte Bewertung
Siehe auch ?-Operator.
Ein Operator, mit dem Entscheidungen in Ausdrücken realisiert werden können ist die
"bedingte Bewertung".
Bild 23.
Allgemeine Form bedingte Bewertung:
cond_expr ? expression_true : expression_false
zuerst wird cond_expr ausgewertet; dann wird in Abhängigkeit des Wertes der
expression_true oder expression_false das Resultat des Ausdrucks.
67
Anweisung
Listing 33.
Ausdruck
bedingte Bewertung
if (schaltjahr)
tageImFebruar = 29;
else
tageImFebruar = 28;
tageImFebruar = schaltjahr ? 29 : 28;
Beispiel (Ermittlung Schaltjahr):
Ein Jahr ist ein Schaltjahr, wenn es ohne Rest durch 4 teilbar ist. Alle 100 Jahre fällt das
Schaltjahr aus, es sei denn, die Jahreszahl ist durch 400 teilbar.
Listing 34.
schaltjahr.cpp
#include <iostream >
using namespace std;
int main()
{
int jahr;
$ schaltjahr
bool schaltjahr;
Eingabe Jahreszahl: 1996
cout << "Eingabe Jahreszahl: ";
1996 ist ein Schaltjahr.
cin >> jahr;
$ schaltjahr
if (jahr%4 == 0)
Eingabe Jahreszahl: 2000
{
2000 ist ein Schaltjahr.
if (jahr%100 == 0)
$ schaltjahr
{
Eingabe Jahreszahl: 1900
if (jahr%400 == 0)
{
schaltjahr = true;
} else
schaltjahr = false;
} else
schaltjahr = true;
} else
schaltjahr = false;
cout << jahr << " ist "
<< (schaltjahr ? "ein" : "kein") << " Schaltjahr." << endl;
return 0;
}
6.2.4 switch - case
Fallunterscheidungen können mittels der switch-Anweisung realisiert werden.
68
Bild 24.
Allgemeine Form: switch case
switch ( expression )
{
case const1: statements1
case const2: statements2
default: statements3
}
expression wird ausgewertet und zu der Stelle consti gesprungen, die mit dem
expression-Wert übereinstimmt, bzw. zu default, wenn es keine Übereinstimmung gibt.
Der expression-Wert muß vom Typ integer oder char sein.
Bei reinen Fallunterscheidungen ist die letzte Anweisung von statement eine breakAnweisung, die bewirkt, daß das switch-Statement terminiert.
Dieses Verhalten kann durch ein Struktogramm veranschaulicht werden:
Bild 25.
Switch struktogramm
switch
(expression)
c1
{
case const1:
statement1;
statement
break;
1
case const2:
statement2;
break;
case const3:
statement3;
break;
default:
statement4;
}
expression
c2
c3
statement
2
statement
3
statement
4
Durch Verzicht auf break wird die Ausführung in die nächsten Case-Anweisungen hinein
fortgesetzt, bis entweder ein break oder das Ende der switch-Anweisung erreicht ist.
69
Listing 35.
Überlappende case Fälle bei einer switch -Anweisung
/* Verarbeite einen Wert */
void inp_handler(int i)
{
int flag;
flag = -1;
switch(i)
{
case 1: /* Diese cases haben gemeinsame */
case 2: /* Anweisungssequenzen. */
case 3:
flag = 0;
break;
case 4:
flag = 1;
case 5:
error(flag);
break;
default:
process(i);
}
}
Listing 36.
switch.cpp (römische Ziffern):
#include <iostream>
using namespace std;
int main()
{
int a;
char c;
cout << "Zeichen: ";
cin >> c;
switch(c)
{
case 'I'
: a=1; break;
case 'V' : a=5; break;
case 'X' : a=10; break;
case 'L' : a=50; break;
case 'C' : a=100; break;
case 'D' : a=500; break;
case 'M' : a=1000; break;
default
: a=0;
}
if (a > 0)
cout << a;
else
cout <<"keine römische Ziffer!";
cout << endl;
return 0;
}
$ switch
Zeichen: M
1000
70
Weiteres case Beispiel
Drei Randbedingungen gibt es zu switch Anweisungen
• Switch kann nur auf Gleichheit testen
• Es können keine 2 case Konstanten in demselben switch denselben Wert haben
• Wenn in einer switch Anweisung Zeichenkonstanten verwendet werden, werden sie
automatisch in Integers verwandelt
6.3 Schleifen, Iterationsanweisungen
Sollen Aufgaben wiederholt ausgeführt werden, sind Schleifen-Anweisungen in C++ zu
verwenden. Unterschieden werden mehrere Arten von Schleifen:
Schleifen mit abweisendem Charakter (Prüfung bevor Aktionen ausgeführt werden),
Schleifen mit nicht abweisendem Charakter und
Schleifen, bei denen die Schrittweite vorher feststeht.
6.3.1 while Schleifen
Eine Schleife mit abweisendem Charakter ist die while Schleife.
Bild 26.
Allgemeine while schleifen
Allgemeine Form:
while ( expression )
statement
Zuerst wird die Bedingung expression geprüft; ist sie zu true auswertbar,
wird statement ausgeführt.
Dann wird expression wieder geprüft.
Also wird statement solange ausgeführt, bis expression false ist.
Das Struktogramm der Schleife:
Bild 27.
Allgemeines Struktogramm der while Schleife:
expression
statement
Das Verhalten einer Schleife lässt sich durch ein Flußdiagramm veranschaulichen:
71
Bild 28.
Flußdiagramm while schleife
true
expression
statement
false
Beispielaufgabe: Addieren einer einzulesende Folge von Zahlen mit Endemarke 0.
Listing 37.
additionVonZahlenfolge.cpp
#include <iostream >
using namespace std;
int main()
{
int eingabe=-1;
int summe=0;
while (eingabe != 0)
{
cout << "Integer: ";
cin >> eingabe;
summe = summe + eingabe;
}
$ additionVonZahlenfolge.exe
Integer: 1
Integer: 2
Integer: 3
Integer: 4
Integer: 5
Integer: 0
Summe:15
$
cout <<"Summe:" << summe << endl;
return 0;
}
Achtung:
Wenn man Schleifen verwendet, können leicht Programme entstehen, die nicht terminieren!
Der Programmierfehler ist dann, dass man im Schleifenrumpf nicht dafür sorgt, dass die
Schleifenbedingung jemals zu false auswertbar ist.
Beispiel (Berechnung Quadratzahlen):
72
Listing 38.
unendlichschleife.cpp
#include <iostream >
using namespace std;
int main()
{
int i;
cout << "Integer eingeben: ";
cin >> i;
while (i*i > 0)
{
cout << "i*i=" << i*i << endl;
i--;
}
return 0;
}
Für welche Eingaben terminiert das
Programm nicht?
(für negative Zahlen.
allerdings klappen sie evtl. bei
fortgesetzter Dekrementierung und
Überlauf ins Positive)
6.3.2 do while Schleifen
Bei dieser Schleifenart wird zuerst eine Anweisung ausgeführt und erst danach geprüft, ob sie
nochmals ausgeführt werden soll.
Bild 29.
Allgemeine Form: do while schleifen
do
statement
while (expression)
Zuerst wird statement ausgeführt. Dann wird die Bedingung expression geprüft; ist
sie zu true auswertbar dann wird statement wieder ausgeführt.
Das Struktogramm der Schleife:
Bild 30.
do while schleifen Struktogramm
statement
expression
Das Verhalten dieser Schleifenart läßt sich durch folgendes Flußdiagramm veranschaulichen:
73
Bild 31.
Flußdiagramm do while schleifen
statement
expression
true
false
Eine do while Schleife kann stets in eine while Schleife umgeformt werden.
do
statement;
{
while (expression)
statement
{
} while (expression);
statement;
}
74
Beispiel (Dialog, um nur Werte innerhalb eines gewünschten Bereichs zu erlauben):
Listing 39.
doWhile.cpp
#include <iostream >
using namespace std;
int main()
{
const double Minimum = 3;
const double Maximum = 27;
double Wert;
do
{
// Dialog
cout << "Bitte geben Sie einen Wert im Bereich "
<< Minimum << ".." << Maximum << " ein: ";
cin >> Wert;
} while (Wert < Minimum || Wert > Maximum);
// Berechnung mit Wert im gültigen Bereich
cout << "Wurzel von " << Wert << " ist " << sqrt(Wert) << endl;
return 0;
}
$ doWhile
Bitte geben Sie einen Wert im
Bereich 3..27 ein: 1
Bitte geben Sie einen Wert im
Bereich 3..27 ein: 3
Wurzel von 3 ist 1.73205
$
Die bisherigen Kontrollstrukturen lassen sich auch kombinieren, so ist z.B. auch eine Schleife
in einer Schleife möglich.
Beispiel (von Aufgabe über Flußdiagramm zum Programm)
Aufgabe:
Von dem Zahlenbereich 1 bis 9 soll für jede Zahl die Fakultät berechnet und ausgegeben
werden.
Verfahren:
Ausgabe von n! für n zwischen 1 und 9 wobei n! := 1*2*3* … *n.
75
Bild 32.
Flußgramm: Fakultät
Start
unten=1
oben=9
i=1
i<=oben
true
fak=1
j=1
berechen Fakultät von i
false
j<=i
true fak=fak*j
j=j+1
false
print fak
i=i+1
Ende
76
Programm:
Listing 40.
fakultaet.cpp
#include <iostream>
int main()
{
const int untereGrenze=1;
const int obereGrenze=9;
int i=untereGrenze;
while (i <= obereGrenze)
{
unsigned long fak = 1;
int j = 1;
while (j <= i)
{
fak = fak*j;
j++;
}
cout << "fak("
<< i << ") = "
<< fak << endl;
i++;
}
return 0;
// für alle Zahlen im Bereich
// berechne Fakultät von i
// Ausgabe Fakultät von i
// nächste Zahl im Bereich
}
6.3.3 for Schleifen
Steht die Anzahl der Wiederholungen vorher fest oder hat man feste Schrittweiten, so wird
häufig die for Schleife eingesetzt.
Diese Schleifenversion ist die übersichtlichste, da Sie an einer Stelle Startbedingung,
Endebedingung und Schrittweite erkennen können.
Bild 33.
For Schleife
Allgemeine Form:
for (init_stat; expression; step_stat)
statement
Zuerst wird init_stat ausgeführt; dabei werden i.A. Initialisierungen vorgenommen.
Dann wird die Bedingung expression geprüft; ist sie zu true auswertbar dann wird
statement wieder ausgeführt danach step_stat.
Das Verhalten dieser Schleifenart läßt sich durch folgendes Flußdiagramm veranschaulichen:
77
Bild 34.
Flußdiagramm For Schleife
init_stat
step_stat
true
expression
statement
false
Das Struktogramm dazu:
Bild 35.
Struktogramm For Schleife
init_stat
expression
statement
step_stat
6.3.3.1 Variablendeklaration innerhalb von Selektions/Iterationsanweisungen
Listing 41. for Schleife, ascii.cpp (ASCII Tabelle ausgeben):
#include <iostream.h>
main()
{
const int untereGrenze=65;
const int obereGrenze=70;
for (int i = untereGrenze; i <= obereGrenze; i++)
cout << i << " " << char(i) << endl;
}
$ ascii
65 A
66 B
67 C
68 D
69 E
70 F
$
In C++ können innerhalb der Selektions- und der Iterationsanweisung lokale Variablen
deklariert werden. In C geht das nicht.
Achtung:
Die Variable kann dann nicht außerhalb des Schleifenrumpfs verwendet werden, da sie hier
innerhalb des Blockes lokal definiert ist! Ältere Compiler erlauben dies zwar, Sie sollten dies
dann trotzdem nicht verwenden, da diese Programme nicht portierbar sind, wenn in der
Zielumgebung neue Compiler verwendet werden!
78
Listing 42.
ascii.cpp
#include <iostream>
using namespace std;
int main()
{
const int untereGrenze=65;
const int obereGrenze=70;
for (int i = untereGrenze; i <= obereGrenze; i++)
cout << i << " " << char(i) << endl;
cout << i << " " << endl;
return 0;
}
$ make ascii
g++ ascii.cpp -o ascii
ascii.cpp: In function `int main()':
ascii.cpp:9: name lookup of `i' changed for new ANSI `for' scoping
ascii.cpp:6: using obsolete binding at `i'
make: *** [ascii] Error 1
$
6.3.4 Äquivalenz von for und while
Eine for Schleife entspricht einer while Schleife; es handelt sich eigentlich nur um eine
Umformulierung, solange nicht continue (-> nächster Abschnitt) vorkommt.
Bild 36.
Äquivalenz for und while
for (init_stat; expression; step_stat)
statement
init_stat;
while (expression)
{
statement;
step_stat;
}
6.3.5 Kommaoperator
Der Kommaoperator wird meist in for Schleifen verwendet, um die Bewertungsreihenfolge
festzulegen und den Resultatstyp zu bestimmen.
Bild 37.
Allgemeine Form: Kommaoperator
expr1, expr2
Zuerst wird expr1, dann expr2 bewertet. Das Resultat hat den Typ von expr2.
79
Listing 43.
Kommaoperator (Summe von 1 bis 10):
#include <iostream>
using namespace std;
int main()
{
int i, sum;
kein Kommaoperator
Kommaoperator
for (i=1, sum=0; i<=10; sum += i, i++);
Kommaoperator
cout << "Summe von 1 bis 10 ist " << sum << endl;
return 0;
}
Listing 44.
Übungsbeispiel
// Kommentar Geben Sie die Ausgaben an !
bool a,b,c,d;
int x,y,z;
x=0;
y=0;
z=0;
cout << "x=y
" << (x==y) << endl;
cout << "x=y=z " << (x==y==z) << endl;
a=true;
b=true;
c=true;
cout << "a=b=c
" << (a==b==c) << endl;
a=false;
b=false;
c=false;
cout << "a=b=c
" << (a==b==c) << endl;
for (x=0;x<9;x++);
cout << "schleifendurchlauf: " << x << endl;
cout << "schleifenende" << endl;
6.4 Sprünge
Sprünge zu bestimmten Stellen im Programm sind ein Relikt aus der systemnahen
Programmierung und haben in Hochsprachen in unterschiedlicher Form Einzug gehalten:
kontrolliertes Verlassen von Schleifen (oft sinnvoll einsetzbar!)
Sprünge zu definierbaren Marken (meist schlechter Programmierstil!)
6.4.1 break
break haben wir bereits zum Verlassen von switch-Alternativen gesehen.
80
break in einer Schleife bewirkt, daß die Schleife verlassen wird.
Bild 38.
Allgemeine Form: break
break;
Die umgebene Schleife wird verlassen.
Bild 39.
Verdeutlichung: break
while ( expression2 )
{
statement1
if ( expression1 )
break;
statement2
}
statement3
6.4.2 continue
Durch continue wird in Schleifen zur erneuten Überprüfung der Bedingung gesprungen.
Continue erzwingt die Durchführung der nächsten Schleifeniteration.
In der for-Schleife wird der nächste bedingte Test und der Inkrementteil der Schleife
angesprungen.
In der while- und der do-while-Schleife wird beim Bedingungstest weitergemacht.
Bild 40.
Allgemeine Form Continue:
Continue
Die Bedingung der Schleife wird erneut geprüft.
Bild 41.
Verdeutlichung: continue
while ( expression2 )
{
statement1
if ( expression1 )
continue;
statement2
}
statement3
Beispiel (Menü, das nur gezielt verlassen werden kann):
81
Listing 45.
menue.cpp
#include <iostream >
#include <stdlib.h>
using namespace std;
int main()
{
char auswahl, dummy;
while (true)
{
system("clear");
// clear screen
cout << "u
Uhrzeit" << endl;
cout << "x
Beenden" << endl;
cout << "Wählen Sie: ";
cin >> auswahl;
// Lesen Benutzereingabe
if (auswahl == 'u')
{
// Menuepunkt Uhrzeit
system("date");
cout << "Weiter mit bel. Zeichen und RETURN ";
cin >> dummy;
continue;
}
if (auswahl == 'x')
// Menuepunkt Beenden
break;
cout << "Falsche Ausgabe, bitte wiederholen" << endl;
cout << "Weiter mit bel. Zeichen und RETURN ";
cin >> dummy;
}
cout << "Auf Wiedersehen!" << endl;
return 0;
}
6.4.3 goto
Mit der Anweisung goto kann zu einer beliebigen Marke (label) gesprungen werden.
Eine Marke muß dabei vorher definiert worden sein.
Bild 42.
Allgemeine Form der Markendefinition:
name:
name ist der Name einer Marke.
Allgemeine Form der Sprungs:
goto name;
Das Programm verzweigt zum Sprungziel, das durch name definiert ist.
Bemerkung:
Beim Programmieren kann immer auf „goto“ verzichtet werden. Es gibt nur einige wenige
Ausnahmen (im Bereich systemnahe Programmierung und Programmierung von
Echtzeitanwendungen), bei denen goto sinnvoll einsetzbar ist.
82
um aus sehr tiefen Verschachtelungen sehr
schnelle (zur Ausführungszeit) heraus zu
kommen, kann es sinnvoll sein, ein goto zu
verwenden (in C, in C++ gibt es bessere
Alternativen)
for (…)
for (…)
for (…)
if (Katastrophe)
goto fehlerbehebung;
…
fehlerbehebung: …
Normalerweise sollte stets auf gotos verzichtet werden, da die Programme fehleranfällig
und schwer wartbar bzw. erweiterbar sind!
Im Praktikum sind gotos verboten!
6.4.4 exit()
Exit ist eigentlich keine Programmkontrollanweisung, paßt hier aber in den Kontext. Mit exit
kann man aus einem beliebigen Programm zurück ins Betriebssystem springen. Mit dem
return-Code kann man verschiedene Fehlertypen anzuzeigen.
6.5 Ausnahmebehandlung (exception)
In Anwendungen tritt manchmal ein Fehler auf, d.h. das Programm macht nicht das, was der
Benutzer eigentlich erwartet. Dabei sind folgende Fehlerarten unterscheidbar:
1. Bedienungsfehler, der Benutzer verwendet das Programm nicht im Sinne der
Bedienungsanleitung
2. Programmfehler, im Programm wird nicht gemäß der Spezifikation reagiert
3. Umgebungsfehler, der korrekte Ablauf des Programms kann nicht sichergestellt
werden, weil die Umgebung nicht die Anforderungen erfüllt (Speicher, Plattenplatz
etc)
Je nach Fehlerart sind die Reaktionen und Fehlermeldungen unterschiedlich.
Bedienfehler dürfen nie Programmabbrüche hervorrufen. Fehlbedienungen müssen dem
Benutzer verständlich per Programm erläutert werden (vgl. letztes Menu).
Programmierfehler müssen vom Programmierer behoben werden.
Umgebungsfehler sollten nicht zu Programmabbrüchen führen – dies kann aber nicht
immer vermieden werden. Fehlbedienungen müssen einem Anwendungsbetreuer verständlich
per Programm erläutert.
Um das o.a. Verhalten zu erreichen ist neben sauberem Programmentwurf und sorgfältigem
Testen das Konzept der Ausnahmebehandlung einzusetzen.
6.5.1 Grundidee der Ausnahmebehandlung
Ein Programm soll so strukturiert werden, daß es
• getrennte Bereiche im Kode gibt für
• den normalen . den Fehlerfall und
• kritische Bereiche besonders gekennzeichnet werden.
Der kritische Programmblock wird „versuchsweise“ ausgeführt:
• im Fehlerfall wird der normale Ablauf unterbrochen und
• zum Fehlerbehandlungsblock gesprungen („Fehler goto“);
• tritt kein Fehler ein, wird der normale Ablauf bis zum Ende ausgeführt und
83
• der Fehlerblock ignoriert.
Im Fehlerfall wird dabei ein Ausnahmeobjekt erzeugt, das im Fehlerbehandlungsblock
auswertbar ist und Daten enthält, die zum Wiederaufsetzen oder zu Fehlermeldungen
verwendbar sind.
6.5.2 Ablauf im Fehlerfall
Der Ablauf kann wie folgt charakterisiert werden:
1. Eine Funktion (bis jetzt main) versucht (engl. try) den kritischen Block
auszuführen.
2. Wenn ein Fehler festgestellt wird, wirft (engl. throw) sie eine Ausnahme (engl.
exception) aus.
3. Die Ausnahme wird vom Fehlerbehandlungsblock aufgefangen (engl. catch).
Allgemeine Form
Das folgende Schema zeigt, wie dies in C++ notiert wird:
Listing 46.
Exception
// Programmcode
// Programmcode
// Programmcode
try
{
// Programmcode
if (FehlerAufgetreten)
throw "Info";
// Programmcode
if (andererFehler)
throw "andere Info";
// Programmcode
}
catch (const char* Text)
{
// ggfs. Reparatur// und Aufräumarbeiten
cout << Text << endl;
}
unkritischer Bereich
kritischer Bereich
Bereich d Fehlerbehandlung
if-Abfrage auf Fehlerbedingung mit throw-Anweisung
• an beliebiger Stelle im "normalen" Programmablauf
• throw kann Objekt eines beliebigen Datentyps auswerfen
Sprung zu nächster passender catch-Anweisung
• Auswahl anhand des Datentyps des Ausnahmeobjekts
• der Rest des "normalen" Ablaufs wird übersprungen
Programmabbruch falls kein passender catch-Block existiert
84
Listing 47.
Exception-Beispiel (Wurzel einer negativen Zahl):
#include <iostream >
using namespace std;
int main()
{
float zahl;
cout << "positive Zahl eingeben: ";
try
{
cin >> zahl;
if (zahl < 0)
throw "keine positive Zahl!";
cout << "Wurzel von " << zahl << " ist "
<< sqrt(zahl) << endl;
}
catch (const char * text)
{
cout << "Fehler: " << text << endl;
}
return 0;
}
$ try01
positive Zahl eingeben: 5
Wurzel von 5 ist 2.23607
$
$ try01
positive Zahl eingeben: -2
Fehler: keine positive Zahl!
$
Bei Eingabe von -2
wird die Ausnahme
„keine positive Zahl“
geworfen und in den
catch-Block
Mit dieser Technik ist eine bessere Alternative zu goto verwendbar, wenn aus tief
verschachtelten Schleifen heraus eine Ausnahmebehandlung schnell aktiviert werden muss
(vgl. goto sinnvoll ).
6.5.3 Ausnahmebehandlung unterschiedlichen Typs
In einem try-Block können unterschiedliche Ausnahmen geworfen werden, für jeden
Datentyp aber höchstens eine.
Dabei wird der erste passende catch-Block ausgeführt:
• die Auswahl erfolgt anhand des Datentyps des Ausnahmeobjekts
• das Sprungziel ist durch den Datentyp definiert;
• wenn kein passender catch-Block gefunden wird, wird in der „Schachtelungsebene
höher gesucht, bis einer gefunden wird oder falls keiner vorhanden ist, wird das
Programm beendet.
Dies ermöglicht eine differenzierte Fehlerbehandlung.
85
Listing 48.
Exceptions (unterschiedliche catch-Blöcke):
#include <iostream >
using namespace std;
int main()
{
const int maxValue = 100;
int zahl1, zahl2, ergebnis;
cout << "2 Zahl eingeben: ";
try
{
cin >> zahl1 >> zahl2;
if (zahl1 > maxValue)
throw zahl1 - maxValue;
if (zahl2 > maxValue)
throw zahl2 - maxValue;
if (zahl2 == 0)
throw "Nulldivision nicht möglich";
ergebnis = zahl1 / zahl2;
cout << zahl1 << " / " << zahl2 << " = " << ergebnis << endl;
}
catch (const int wert)
{
cout << "Fehler: Wert um " << wert << " zu gross!" << endl;
}
catch (const char * text)
{
cout << "Fehler: " << text << endl;
}
return 0;
}
$ try02
2 Zahl eingeben: 18 6
18 / 6 = 3
$ try02
2 Zahl eingeben: 18 0
Fehler: Nulldivision nicht möglich
$ try02
2 Zahl eingeben: 18 1001
Fehler: Wert um 901 zu gross!
Auf die Problematik der Ausnahmebehandlung wird nochmals eingegangen, wenn die
Sprachkonstrukte „Funktion“ und „Klasse“ bekannt sind.
86
7 Operatoren
7.1 Zuweisungen, Speicherzellen und Werte
Der Zuweisungsoperator wird verwendet, um einer Variablen einen Wert zuzuweisen.
Variable = Ausdruck;
Variable_name= expression;
Dabei wird zuerst der Wert des Ausdrucks ermittelt, dann wird der Variablen das Ergebnis
zugewiesen.
int x;
x = 5;
// Zuweisung eines Wertes
// (Konstante 5)
x = x + 2; // Zuweisung eines Wertes
// (alter Wert von x + 2)
x
int
?
x
int
5
x
int
7
Ein Ausdruck liefert einen Wert; man unterscheidet:
L-Werte bezeichnen Speicherzellen
o sie dürfen auf der linken Seite einer Zuweisung stehen
o sind Variablennamen oder
o Ausdrücke, die eine Speicheradresse liefern
R-Werte bezeichnen Konstanten und Speicherinhalte
o sind die rechte Seite einer Zuweisung oder
o ein Ausdruck, der eine Zahl o.ä. liefert
o Variablennamen werden dabei automatisch "dereferenziert"
Eine Zuweisung hat stets die Form:
L-Wert = R-Wert;
Deshalb ist folgendes nicht möglich:
int x=1, y=2;
3 = x + y;
kein L-Wert!
87
Listing 49.
Beispiel einer korrekten Zuweisung:
int zelle1=10, zelle2=2, zelle3;
zelle1
10
zelle2
2
zelle3
?
Variable wird dereferenziert: liefert
Wert 2
Zelle3 = zelle1 + zelle2
zelle1
10
zelle2
2
zelle3
12
Ausdruck liefert Wert 12
88
Die restlichen Zuweisungsoperatoren sind lediglich Schreibvereinfachungen mit folgender
Bedeutung:
Bild 43.
Zuweisungsoperatoren
Ausdruck
x += y
x -= y
x *= y
x /= y
x %= y
x <<= y
x >>= y
x &= y
x ^= y
x |= y
Bedeutung
x = x + y
x = x – y
x = x * y
x = x / y
x = x % y
x = x << y
x = x >> y
x = x & y
x = x ^ y
x = x | y
Die Bedeutung der einzelnen Operatoren (+, - , *, ...) wird in den nächsten Abschnitten
erklärt.
An diesen kombinierten Zuweisungsoperatoren kann man erkennen, weshalb C/C++ als
„ausdrucksstarke Sprachen“ bezeichnet werden: jede Zuweisung ist zugleich ein Ausdruck.
7.2 Mehrfachzuweisungen
Sie können vielen Variablen denselben Wert zuweisen
Listing 50.
Mehrfachzuweisung
x=y=z=0;
7.3 Implizite Typkonvertierung in Zuweisungen
Wenn der Inhalt einer Variablen eines Typs einer Variablen eines anderen Typs zugewiesen
wird, so findet eine implizite Typkonversion (Typepromotion) statt. Der Wert der rechten
Seite wird in den Typ der linken Seite (Zielvariablen) umgewandelt.
Listing 51.
Typkonvertierung
int x;
char ch;
float f;
void func(void)
{
ch = x;
/* Zeile 1 */
x = f;
/* Zeile 2 */
f = ch;
/* Zeile 3 */
f = x;
/* Zeile 4 */
}
89
Bei der Konvertierung in Zeile 1 werden die high order Bits abgeschnitten. Bei Werten <256
und >=0 ergäben sich identische Werte.
Zeile 2 ergibt den ganzzahligen Teil der Zahl.
Zeile 3 wird aus ch -> float, es gibt compiler, die Werte >127 in entsprechende float Zahlen
verwandeln, andere interpretieren diese als negative Zahlen!
Char sollte nur für Zeichen verwendet werden, ints, short ints und signed chars für
Zahlendarstellungen.
Bei der Umwandlung von Typen mit größerem Wertebereich in Typen mit kleinerem
Wertebereich verliert man Genauigkeit. Das Verhalten beim Runden bzw. Abschneiden ist
implementierungsabhängig!
Die Umwandlung von signed in unsigned und umgekehrt kann falsche Ergebnisse
liefern:
(-1L < 1U) liefert true
(-1L < 1UL) liefert false
Bild 44.
Casting
Zieltyp
signed char
char
char
char
char
short in
short int
int (16)
int (32)
int
float
double
Bild 45.
Ausdruckstyp
char
short int
int (16)
int (32)
long int
int (16)
int (32)
long int
long int
float
double
long double
möglicher Verlust
>127 evtl. Negativ
high order 8 bits
high order 8 bits
high order 24 bits
high order 24 bits
keiner
high order 16 bits
high order 16 bits
keiner
nachkommastellen und ggf. Mehr
Genauigkeit, Rundung und ggf. Mehr
Genauigkeit, Rundung und ggf. Mehr
Casting, was kommt hier heraus?
int u,v;
u=4;
v=8;
f=u/v;
cout << f;
7.4 Explizite Typkonvertierung
Zum expliziten Umwandeln existiert der cast-Operator. Er ist unär.
(type) expression
Listing 52.
Cast operator
char c=’a’;
int i;
double x;
i = (int) c;
// 1. Schreibweise für cast
i = int (c);
// 2. Schreibweise für cast (funktionale Notation)
x = sqrt( (double) 2 );
90
Listing 53.
Typumwandlung
$ cat typumwandlung.cpp
#include <iostream >
using namespace std;
int main()
{
char c = 'a';
int d,i;
i = c;
// impl. char -> int
cout << "d=" << d << endl;
// impl. char -> int
cout << "d=" << (char) d << " i=" << i << endl;
i = 5 / 2;
cout << "i=" << i << endl;
double d;
d = 5 / 2;
cout << "d=" << d << endl;
d = 5 / 2.0;
// impl. int -> double
cout << "d=" << d << endl;
i = d / 2;
//Genauigkeit geht
//verloren !!
cout << "i=" << i << endl;
return 0;
}
$ Typumwandlung
d=97
d=a i=97
i=2
d=2
d=2.5
i=1
Die Datentypüberprüfung in C++ ist deutlich strenger als in C. In C++ werden Zuweisungen und
Parameterübergaben streng auf Typübereinstimmung geprüft. Wir sollten daher immer den CastOperator verwenden. Ansonsten ist das Mindeste eine (berechtigte) Warnung des Compilers, häufig
liegt sogar eine Fehlermeldung vor. Der Cast-Operator kann in C++ benutzerfreundlicher wie ein
Funktionsaufruf geschrieben werden:
int (ch);
float (3*i);
// statt: (int) ch;
// statt: (float) (3*i);
Wir zeigen noch ein kleines Beispiel, wie wir mit Hilfe des Cast-Operators den ASCII-Wert eines
Zeichens ausgeben können:
cin >> ch;
cout << "Der ASCII-Wert von " << ch << " ist: " << int(ch) << endl;
7.5 Vorrangregeln
Zum Auswerten von Ausdrücken werden folgende Rangfolgen für Operatoren verwendet
(einige der Operatoren werden erst später behandelt):
Bild 46.
Rang
0
1
2
3
4
Vorrangregeln
Operator
::
. -> [] () sizeof
! + -(unär) ++ -- ~
*(Defererenzierung) new
.* ->*
* / %
&(Adreßoperator)
new[] delete delete[]
91
+ 5
<< >>
6
< > <= >=
7
== !=
8
&(bitweise UND)
9
^
10
|
11
&&
12
||
13
?:
14
Zuweisungen, wie z.B. = +=
15
,
16
Ein kleiner Rang bedeutet dabei hohe Priorität. Auf gleicher Prioritätsstufe wird ein
Ausdruck von links nach rechts bewertet (linksassoziativ), unter Berücksichtigung von
Klammerungen. Ausnahme sind unäre Operatoren und Zuweisungsoperatoren, sie sind
rechtsassoziativ
7.6 Reihenfolge
C/C++ legt nicht fest, in welcher Reihenfolge Unterausdrücke eines Ausdrucks ausgewertet
werden.
Listing 54.
Reihenfolge der Auswertung
X = f1() + f2();
Stellt nicht sicher, daß f1 vor f2 ausgeführt wird
7.7 Wahrheitswerte
Wahrheitswerte (true, false) werden in C als Integer dargestellt. Konvention dabei ist: der
Wert 0 repräsentiert false, jeder andere Wert entspricht true. Dies ist in C++ übernommen.
Der Typ bool als eigener Typ für Wahrheitswerte ist im Rahmen der Standardisierung
(ANSI/ISO-Standard) von C++ erst recht spät in die Sprache aufgenommen worden.
bool
ist aus historischen Gründen ein Integertyp mit automatischer Typkonversion in beide
Richtungen
0 == false; 1 == true
Vergleichsoperatoren für Wahrheitswerte sind
==, !=, <, <=, >, >=
und liefern als Ergebnis bool
Logische Verknüpfungen sind && (und), || (oder), ! (nicht)
Bedingungen if, while, etc. erwarten streng genommen int mit C-Konvention (alles != 0
bedeutet true)
92
Bild 47.
Beispiele bool:
int x = 5;
int y = 6;
x
int
y
5
int
6
bool equal;
bool less;
equal = (x == y); // guter Stil
if (x < y)
// schlechter Stil
less = true;
else
less = false;
equal
bool
?
equal
less
bool
?
bool
false (0)
less
bool
true (1)
In älteren Programmen (und C-Programmen) findet man häufig folgende Definition mittels
symbolischer Konstanten (wir werden dies später noch ausführlicher behandeln):
Listing 55.
Define der Bools
typedef int BOOL;
#define FALSE 0
#define TRUE 1
93
Das letzte Beispiel als vollständiges Programm zeigt, wie true und false in int
konvertiert werden und die Verwendung der Vergleichsopertoren.
Listing 56.
wahrheitswerte.cpp
#include <iostream >
using namespace std;
int main()
{
int x = 5, y = 6;
bool equal, less;
equal = (x == y);
// guter Stil
if (x < y)
// schlechter Stil
less = true;
else
less = false;
cout << "equal: " << equal << endl;
cout << "less: " << less << endl;
cout << "y > 0: " << (y > 0) << endl;
cout << "y <= 6: " << (y <= 6) << endl;
cout << "y != 6: " << (y != 6) << endl;
return 0 ;
}
$ wahrheitswerte
equal: 0
less: 1
y > 0: 1
y <= 6: 1
y != 6: 0
7.8 Operatoren, die R-Werte liefern
Ein Ausdruck wird als Folge von Operatoren und Operanden gebildet. Er ist eine Menge von
Literalen, Variablen, Operatoren und Ausdrücken, die zu einem Wert auswertbar sind; ein
Wert kann dabei eine Zahl, ein String oder ein Wahrheitswert sein.
Folgende Operatoren liefern R-Werte, und sind demzufolge nur auf der rechten Seite von
Zuweisungen möglich (später werden wir sehen, daß sie auch zum Indizieren von Arrays
verwendbar sind).
7.8.1 Arithmetische Operatoren
Arithmetische Operatoren zusammen mit numerischen Werten erzeugen in Ausdrücken einen
einzelnen numerischen Wert.
Folgende Operatoren stehen für die Arithmetik zur Verfügung:
94
Bild 48.
Arithmetische operatoren
Operator
+
*
/
Bedeutung
Addition
Subtraktion
Multiplikation
Division
Erklärung
binärer Operator für Addition
binärer Operator für Subtraktion
binärer Operator für Multiplikation
binärer Operator für Division
%
Modulo
++
Inkrement
--
Dekrement
binärer Operator zur Ermittlung des
ganzzahligen Rests nach Division
unärer Operator zur Erhöhung um 1.
++x liefert Wert nach Erhöhung um 1;
Präfix
x++ liefert Wert vor Erhöhung um 1;
Postfix
unärer Operator zur Verminderung um 1.
--x liefert Wert nach Verminderung um 1;
x-- liefert Wert vor Verminderung um 1;
-
Negation
Listing 57.
unärer Operator zum Negieren
des Operanden
Beispiel
5 + 2 ergibt 7
5 - 2 ergibt 3
5 * 2 ergibt 10
5 / 2 ergibt 2
5.0 / 2.0 ergibt 2.5
5 % 2 ergibt 1
x = 3;
++x ergibt 4 und setzt
x auf 4
x++ ergibt 3 und setzt x
auf 4
x = 3;
--x ergibt 2 und setzt x
auf 2
x-- ergibt 3 und setzt x
auf 2
x = 3;
-x ergibt -3
Beispiele: arithmOperatoren.cpp
#include <iostream>
using namespace std;
int main()
{
int x = 5, y = 2;
cout << "x=" << x << " y=" << y << endl;
cout << "x+y: " << x+y << endl;
cout << "x-y: " << x-y << endl;
cout << "x*y: " << x*y << endl;
cout << "x/y: " << x/y << endl;
cout << "1.0*x/y: " << 1.0*x/y << endl;
cout << "x%y: " << x%y << endl;
x= 1;
y = x++;
cout << "x=" << x << " y=" << y << endl;
x= 1;
y = ++x;
cout << "x=" << x << " y=" << y << endl;
return 0 ;
}
$
$ arithmOperatoren
x=5 y=2
x+y: 7
x-y: 3
x*y: 10
x/y: 2
1.0*x/y: 2.5
x%y: 1
x=2 y=1
x=2 y=2
$
7.8.2 Vergleichsoperatoren, relationale ..
Ein Vergleichsoperator vergleicht seine Operanden und liefert als Ergebnis einen WahrheitsWert. Die folgende Tabelle zeigt die Vergleichsoperatoren.
Bild 49.
Vergleichsoperatoren, relationale ..
Operator Bedeutung
Erklärung
Beispiel, das "true" ergibt
95
(Annahme:
x = 3; y = 4;)
3 == x
liefert "true", wenn beide Operanden
gleich sind
liefert "true", wenn beide Operanden
ungleich
nicht gleich sind
liefert "true", wenn der linke Operand
größer
größer als der rechte ist
liefert "true", wenn der linke Operand
größer oder
größer oder gleich dem rechten
gleich
Operand ist
liefert "true", wenn der linke Operand
kleiner
kleiner als der rechte ist
liefert "true", wenn der linke Operand
kleiner oder
kleiner oder gleich dem rechten
gleich
Operand ist
gleich
==
!=
>
>=
<
<=
x != 4
y > x
y >= x
x < y
x <= y
7.8.3 Logische Operatoren
Logische Operatoren werden im Zusammenhang mit Wahrheitswerten verwendet.
Bild 50.
Logische operatoren
Operato
r
&&
||
!
Listing 58.
Bedeutung
logical AND
logical OR
logical NOT
Verwendung
Expr1 && expr2
Expr1 || expr2
!expr
cat logOperatoren.cpp
#include <iostream >
using namespace std;
int main()
{
int x = 5, y = 2;
cout << "x=" << x << " y=" << y << endl;
cout << "x>y && y>0: " << (x>y && y>0) << endl;
cout << "x>y && y<0: " << (x>y && y<0) << endl;
cout << "x<y && y>0: " << (x<y && y>0) << endl;
cout << "x<y && y<0: " << (x<y && y<0) << endl;
cout << "x>y || y>0: " << (x>y || y>0) << endl;
cout << "x>y || y<0: " << (x>y || y<0) << endl;
cout << "x<y || y>0: " << (x<y || y>0) << endl;
cout << "x<y || y<0: " << (x<y || y<0) << endl;
cout << "!x>y: " << (!x>y) << endl;
return 0 ;
}
$ logOperatoren.exe
x=5 y=2
x>y && y>0: 1
x>y && y<0: 0
x<y && y>0: 0
x<y && y<0: 0
x>y || y>0: 1
x>y || y<0: 1
x<y || y>0: 1
x<y || y<0: 0
!x>y: 0
$
Das in C/C++ fehlende XOR kann wie folgt realisiert werden:
96
Listing 59.
XOR Funktion
int xor(int a, int b)
{
return (a || b) && !(a && b);
}
// oder bool xor(int a, int b)
7.8.4 Bitoperatoren
In C++ existieren Operatoren zur Manipulation einzelner Bits in numerischen Werten. Diese
Operatoren nehmen die Bitrepräsentation (32 Bits) der Operanden und verknüpfen auf
Bitebene. Das Ergebnis ist wieder ein numerischer Wert.
Die Bitrepräsentation von 5 ist
5: 0 0 000000000000000000000000000101
Die Bitrepräsentation von 3 ist
3: 0 0 000000000000000000000000000011
Das Ergebnis von "bitweise UND" von 5 und 3 ist 1
5 : 0 0 000000000000000000000000000101
3 : 0 0 000000000000000000000000000011
5&3: 0 0 000000000000000000000000000001
Das Ergebnis von "bitweise ODER" von 5 und 3 ist 7
5 : 0 0 000000000000000000000000000101
3 : 0 0 000000000000000000000000000011
5|3: 0 0 000000000000000000000000000111
Die nachfolgende Tabelle beschreibt die Operatoren.
Bild 51.
Bit operatoren
Operato
Bedeutung
r
&
|
^
~
<<
Erklärung
ergibt in einer Position 1, wenn
bitweise AND beide Operanden an der Position
1 haben
ergibt in einer Position 1, wenn
bitweise OR
einer von beiden Operanden an
der Position 1 haben
exklusive ODER, d.h. ergibt in
einer Position 1, wenn genau einer
bitweise XOR
von beiden Operanden an der
Position 1 haben
bitweise NOT negiert bitweise
a << b verschiebt a bitweise um b
left Shift
Positionen nach links, wobei
rechts 0en nachrücken wird
Beispiel
5 & 3 ergibt 1
5 | 3 ergibt 7
5 ^ 3 ergibt 6
~5 ergibt -6
5 << 3 ergibt 40
97
Das Ergebnis von "bitweise NOT" von 5 ist -6
Bild 52.
Bitweise NOT
5
~5
: 0 0 000000000000000000000000000101
: 1 1 111111111111111111111111111010
Zur Erinnerung: Zahlen im Zweierkomplement
Bild 53.
2k
6
-7 in 2K
+1
: 00 000000000000000000000000000110
: 11 111111111111111111111111111001
: 00 000000000000000000000000000001
-6 in 2K
: 11 111111111111111111111111111010
Das Ergebnis von "bitweise exklusiv ODER" von 5 und 3 ist 6
Bild 54.
Bitweise XOR
5
3
5^3
:0 0 000000000000000000000000000101
:0 0 000000000000000000000000000011
:0 0 000000000000000000000000000110
Das Ergebnis von "Links Shift" von 5 um 3 Stellen ist 6
Bild 55.
Left shift
5
5<<3
: 0 0 000000000000000000000000000101
: 0 0 000000000000000000000000101000
Das Ergebnis von "Rechts Shift" von 5 um 1 Stelle ist 2
Bild 56.
Right shift
5
: 0 0 000000000000000000000000000101
5>>1
: 0 0 000000000000000000000000000010
Das folgende Programm verwendet diese Shift-Operatoren, um eine Zahl mit 2, 4 und 8 zu
multiplizieren.
Listing 60.
mul.cpp
#include <iostream >
using namespace std;
int main()
{
int i = 5;
cout << "i: " << i << endl;
cout << "2*i: " << (i<<1) << endl;
cout << "4*i: " << (i<<2) << endl;
cout << "8*i: " << (i<<3) << endl;
return 0;
}
$ mul
i: 5
2*i: 10
4*i: 20
8*i: 40
98
7.8.5 Referenzen
Eine Referenz ist in C++ ein Datentyp, der einen Verweis auf ein Objekt liefert (->
Parameterübergabe). Eine Referenz kann man sich als Alias-Namen für ein Objekt vorstellen,
über den das Objekt neben seinem eigentlichen Namen auch angesprochen werden kann.
Es wird also kein neues Objekt angelegt sondern nur ein Name für ein bestehendes Objekt.
Bild 57.
Variablendeklaration (als Referenz):
Typ & Name
Name des Alias
Typ eines
Objektes
Listing 61.
Referenz
Operator
Beispiele: ref.cpp
#include <iostream>
using namespace std;
int main()
{
int i,j=3;
int &r = i;
i = 10;
cout << "i: " << i << endl;
r = 100;
cout << "r: " << r << endl;
cout << "i: " << i << endl;
r = j;
cout << "j: " << j << endl;
cout << "r: " << r << endl;
cout << "i: " << i << endl;
return 0;
$ ref
// Referenz auf i,
// d.h. r ist Alias für i
// ändert i
i: 10
// ändert i
r: 100
i: 100
// Wirkung: i = j
j: 3
r: 3
i: 3
}
Referenzen müssen bei der Deklaration initialisiert werden; eine Referenz auf eine Referenz
und Neuinitialisierungen sind nicht erlaubt.
7.9 der ? Operator
Der ternäre Operator ? besitzt die allgemeine Form
99
Bild 58.
Ternäre Operator
Exp1 ? Exp2 : Exp3 ;
Der Ausdruck Exp1 wird ausgewertet, ist er wahr, so wird Exp2 zum Wert des Ausdrucks. Ist
Exp1 falsch, so wird Exp3 zum Wert des Ausdrucks.
Listing 62.
? Operator
x = 10;
y = x>9 ? 100 : 200;
7.10 Zeigeroperatoren & und *
Ein Zeiger ist die Speicheradresse irgendeines Objektes. Eine Zeigervariable ist eine
Variable, die gemäß Deklaration einen Zeiger auf ein Objekt des vereinbarten Typs enthält.
Der Zeigeroperator & ist unär und liefert die Speicheradresse seines Operanden zurück.
m = &count
Die Speicheradresse von count wird an m zugewiesen.
&count bedeutet: die Adresse von count
Der Zeigeroperator * ist das Komplement zu & und ist ebenfalls unär.
q=*m
liefert den Wert unter der Adresse m an q.
Zeigervariablen müssen mit vorangestelltem * deklariert werden.
char * ch
Deklariert einen Zeiger (character-Zeiger) auf ein Zeichen.
Im folgenden Programm erhält die Variable target den Wert 10.
Listing 63.
Zeigeroperatoren
int main(void)
{
int target, source;
int *m;
source = 10;
m = &source;
target = *m;
printf("%d", target);
return 0;
}
7.11 Compile Time operator sizeof
Mit dem unären Operator sizeof kann man die Größe von Variablen oder des Typbezeichners
einer Variablen feststellen. Abhängig vom Compiler könnte sizeof z.B. folgendes liefern:
Listing 64.
Sizeof operator
double f;
printf("%d ", sizeof f);
printf("%d", sizeof(int));
// 8
// 4
Mit diesem Operator kann man compiler unabhängigen Code implementieren.
100
7.12 Kommaoperator
Der Kommaoperator verknüpft mehrere Ausdrücke im Sinne von: erst führe die linke Seite
vom Komma durch und dann die Anweisung rechts. Am Ende ist x=4.
Siehe auch Kontrollstrukturen.
Listing 65.
Kommaoperator
X = (y=3, y+1);
7.13 Punkt- und Pfeiloperatoren
Siehe später
7.14 Operatoren [] und ()
() erhöhen die Rangordnung der Operationen in ihrem Inneren. Außerdem verbessern sie mit
frei einsetzbaren Leerzeichen die Lesbarkeit des Codes. Die Performance des Programmes
verändern sie nicht.
Die [] führen die Array-Indizierung durch, der Wert in der Klammer stellt den Index in dem
Array dar. Dies wird später noch behandelt.
Listing 66.
Klammern und Blanks
x=y/3-100*count+233;
x = (y / 3) - (100*count) + 233;
[] Siehe später
7.15 Bibliotheksfunktionen
C/C++ ist u.a. durch die viele Standardbibliotheken erfolgreich geworden. Eine
Standardbibliothek ist eine Ansammlung von Funktionen, die in Programmen verwendet
werden können.
Diese Bibliotheksfunktionen gehören nicht zum Sprachumfang von C/C++, sind aber in jeder
C/C++ Implementierung enthalten.
101
Beispiele:
Mathematische Funktionen
Charakter Funktionen
#include <cmath>
double x,y,z;
z=sin(x)
Sinus
z=cos(x)
Cosinus
z=tan(x)
Tangens
z=sinh(x)
Sinus Hyperbolicus
z=exp(x)
Exponentialfunktion
z=log(x)
Logarithmus
z=pow(x,y)
xy
#include <ctype >
int c;
unsigned char
int i;
==0 oder !=0
i=isalpha(c)
Buchstabe
i=isdigit(c)
Ziffer
i=islower(c)
Kleinbuchstabe
i=isupper(c)
Großbuchstabe
i=isspace(c)
Leerst.,Tab., ...
i=isxdigit(c)
Hexadez. Ziffer
c=tolower(c)
Groß- -> Kleinbuchstabe
c=toupper(c)
Klein- -> Großbuchstabe.
z=sqrt(x)
z=fabs(x)
…
Quadratwurzel
Absolutwert
Listing 67.
Beispiel wurzel.cpp
…
#include <iostream >
#include <cmath>
using namespace std;
int main()
{
int zahl;
double wurzel;
cout << "positive Zahl eingeben ";
cin >> zahl;
wurzel = sqrt(zahl);
cout << "Die Wurzel von " << zahl << " ist " << wurzel << endl;
return 0;
}
$ wurzel
positive Zahl eingeben 2
Die Wurzel von 2 ist 1.41421
$
Wie man selbst Funktionen schreiben kann und in Bibliotheken zusammenfassen kann, wird
später erklärt.
102
8 Benutzerdef. und zusammengesetzte Datentypen
Neben den einfachen Datentypen existiert die Möglichkeit, selbst Datentypen aus
Grunddatentypen zusammenzusetzen.
Wir werden
• Aufzählungen,
• Arrays und
• Strukturen
kennen lernen.
8.1 Aufzählungstypen, Enumerationstypen
Soll eine Variable eine fest vergebene Menge an nicht-numerischen Werten annehmen
können, kann man Aufzählungstypen (auch Enumerationstypen genannt) verwenden.
Bild 59.
Allgemeine Form (Deklaration Aufzählungstyp):
enum [typename] { enumeration } [list of variables];
optional
Listing 68.
Enumerationstypen
enum Farbe {rot, gelb gruen};
enum Wochentag {Sonntag, Montag, Denstag, Mittwoch, Donnerstag, Freitag, Samstag};
Nach der Deklaration eines Aufzählungstypen können Variablen definiert werden, die zu
diesem Typ gehören:
Bild 60.
Beispiel: Enumerationstypen
Farbe eimer, auto;
Wochentag heute = Dienstag;
Typ
Name
// Definition
// Definition und Initialisierung
Initialwert
Intern werden Aufzählungstype mit Integern kodiert, in Aufschreibungsreihenfolge wird mit
0 begonnen und hoch gezählt. Soll eine andere Kodierung verwendet werden, kann man die
bei der Deklaration angeben.
Bild 61.
eigene Enumerationskodierung
enum Farbe {rot=1, gelb=10, gruen=18};
In Ausdrücken werden enum-Typen in int umgewandelt, nicht umgekehrt.
Als Operation auf enum-Typen ist nur die Zuweisung erlaubt.
103
Listing 69.
Enumerationsbeispiele
enum Farbe {rot, gelb gruen};
enum Wochentag {Sonntag, Montag, Dienstag, Mittwoch, Donnerstag, Freitag, Samstag};
enum Automarke {Audi, VW, Renault} meinAuto;
Wochentag heute;
heute = Montag;
// richtig
heute = 2;
// falsch, Datentyp nicht kompatibel
int i = Samstag;
// richtig, da Samstag in int gewandelt wird
i = heute + Montag;
// richtig
meinAuto = VW;
// richtig
meinAuto = VW + Audi;
// falsch, int-Ausdruck nicht in enum konvertierbar
meinAuto ++;
// richtig oder falsch ??
Listing 70.
Enumerationsbeispiele
#include <iostream >
using namespace std;
int main()
{
enum color {RED, GREEN, BLUE};
color myColor = RED;
if (myColor == RED) {
cout << "hot" << endl;
}
if (myColor == BLUE) {
cout << "cold" << endl;
}
if (myColor == GREEN) {
cout << "Is not easy being" << endl;
}
return 0;
}
8.2 Arrays
Felder (engl. array) sind Zusammenfassungen von Elementen, die alle den gleichen Typ
haben. Der Zugriff erfolgt indiziert über Integer.
Arrays sind statische Objekte, d.h. die Anzahl der Elemente steht zur Compilezeit fest
/später werden wir auch dynamische Objekte behandeln, z.B. Vektoren)
Allgemeine Form (Deklaration Array):
type var_name [number_elements];
Listing 71.
BeispielArray
int tageProMonat[12];
double balance[5];
Dadurch wird ein int-Array mit 12 Integern angelegt und ein double-array mit 5 Elementen.
104
Bild 62.
Beispiel-Array
Der Zugriff auf ein Element erfolgt durch Name plus Index:
tageProMonat
0
1
..
11
Die Indizierung beginnt bei 0.
Ein klassicher Fehler ist, sich bei der Indizierung um ‚eins’ zu vertun (odd plus one error).
Generell werden die Arraygrenzen weder zur Laufzeit noch zur Compile-Zeit überwacht!
Listing 72.
Grenzüberschreitung bei der Array-Indizierung
int count[10], i;
/* dies bewirkt die Ueberschreitung der Grenzen von count */
for(i=0; i<100; i++) count[i] = i;
Listing 73.
Initialisierungen von Arrays
tageProMonat[0] = 31;
tageProMonat[1] = tageProMonat[0] – 3;
tageProMonat
tageProMonat
0
?
0
31
1
?
1
28
.
?
..
?
1
?
11
?
105
Listing 74.
Bei der Deklaration können auch Initialwerte mit angegeben werden:
const int anzahlMonate=12;
double monatsUmsatz[anzahlMoate] =
{120.5, 118, 0, 0, 0, 0, 0, 0, 0, 0, 0, 57};
Listing 75.
monatsUmsatz
0
120.5
1
118
.
0
1
57
Array-Beispiele:
int TageProMonat [] = {31,28,31,30,31,30,31,31,30,31,30,31};
int AlleAufNull [Groesse] = {0};
// nicht initialisierte Elemente werden auf 0 gesetzt
// aber Achtung, das array wird in einen Speicher genommen, der vor start genullt
// wird, es ist keine Einzelinitialisierung
//siehe folgendes Beispiel
int AlleAufNull_A [Groesse] = {5}; //setzt erstes Element auf 5, den Rest auf 0.
//Beispiel
int AlleAufNull_B [Groesse];
//initialisiert überhaupt nicht
Die Größe (in Bytes) eines Array kann durch sizeof() ermittelt werden.
Listing 76.
Array-Beispiel, Größe:
int TageProMonat [] = {31,28,31,30,31,30,31,31,30,31,30,31};
cout << sizeof(TageProMonat << endl;
48
106
Zuweisungen von Arrays als Ganzes sind nicht möglich.
Im Speicher des Rechners wird das o.a. Array wie folgt abgelegt:
abc
987
11
31
…
..
TageProMonat
1
28
0
31
xyz
123
Array-Beispiele (BCD Darstellung von Integern: int->Ziffernfolge): 123 -> 1 2 3
Listing 77.
bcd.cpp
#include <iostream >
using namespace std;
int main()
{
int i;
int zahl, BCD[16]={0};
i = 0;
cout << "Integer: ";
cin >> zahl;
do
{
BCD[i] = zahl % 10;
zahl = zahl / 10;
i++;
} while (zahl > 0);
for (int j = i-1; j >=0 ; j--)
cout << BCD[j] << " ";
cout << endl;
return 0 ;
$ bcd
Integer: 1234
1 2 3 4
$
}
Der benötigte Speicherplatz für ein array berechnet sich aus der Größe des Grundtyps
multipliziert mit der Anzahl der Elemente:
Total Bytezahl = sizeof(Grundtyp) * Größe des Arrays
8.2.1 Mehrdimensionale Felder
Bisher waren die Arrays eindimensional; in C++ sind auch mehrdimensionale Arrays
möglich.
107
Listing 78.
Mehrdimensionale Felder
const int Quartale = 4;
const int Regionen = 3;
int Umsatz [Quartale][Regionen] =
{
{00, 01, 02},
{10, 11, 12},
{20, 21, 22},
{30, 31, 32}
};
cout << Umsatz[3][1];
31
Die Initialisierung von oben formal wie folgt:
Listing 79.
Array-Initialisierung
Type_specifier array_name[size1] [size2] ….[sizeN]= {value_list};
Im Speicher wird das o.a. Array wie folgt abgelegt:
Bild 63.
Speicherlayout eines Arrays
xyz
Quartale
Umsatz
987
2
32
3 1
31
0
30
2
22
2 1
21
0
20
2
12
1 1
11
0
10
2
02
0 1
01
0
00
abc
Regionen
123
108
matrixAus.cpp Ausgabe einer Matrix
#include <iostream>
using namespace std;
int main()
{
const int m=2, n=3;
int A[m][n] =
{
{1, 0, 2},
{2, 1, 1}
};
for (int i = 0; i < m ; i++)
{
for (int j = 0; j < n ; j++)
cout << A[i][j] << " ";
cout << endl;
}
return 0;
$ matrixAus.exe
1 0 2
2 1 1
$
}
8.2.2
8.2.3 Sortieren in Feldern (Bubble Sort)
Im folgenden wird ein bekannter, aber verrufener Algorithmus vorgestellt: Bubble Sort. Seine
Popularität rührt von der Einfachheit – es ist aber einer der bzgl. Effizienz schlechtesten
Verfahren. Die Idee ist, wiederholt benachbarte Elemente zu vertauschen (falls erforderlich)
bis alle Elemente sortiert sind; verglichen wird immer „linker Nachbar“ > „aktuelles
Element“.
109
Listing 80.
Aktuelles
Element
Bubble sort
Beispiel (aufsteigend Sortieren):
12
3
2
6
1. Lauf: 1: Schritt
12
3
2
6
2. Schritt
12
2
3
6
3. Schritt
2
12
3
6
2
12
3
6
1. Schritt
2
12
3
6
2. Schritt
2
3
12
6
3. Lauf: 1. Schritt
2
3
6
12
Ausgangssituation
2. Lauf:
Ausgangssituation
Listing 81.
bubbleSort.cpp
#include <iostream >
using namespace std;
int main()
{
const int max=5;
int zaehler=0, zahl,
int daten[max]={0};
do {
cout << "Integer (0 Abbruch): ";
cin >> zahl;
daten[zaehler] = zahl;
zaehler++;
} while (zahl !=0 && zaehler<max);
// Einlesen der Daten
for (int lauf=1; lauf < zaehler; lauf++)
for (int element=zaehler-1; element>=lauf; element--)
{
if (daten[element-1] > daten[element])
{
int tmp = daten[element-1];
daten[element-1] = daten[element];
daten[element] = tmp;
}
}
for (int i= 0; i<zaehler ; i++)
// Ausgeben
cout << daten[i] << " ";
cout << endl;
return 0;
}
// Sortieren
// Läufe
// ein Lauf
// vertauschen
//dreieckstausch
110
Laufzeitanalyse:
Man erkennt an den zwei geschachtelten Schleifen, dass unabhängig davon, wie die Daten
aussehen (also auch wenn sie bereits sortiert sind), immer die feste Anzahl von Durchläufen
ausgeführt wird.
Die äußere Schleife wird (n-1) mal ausgeführt, die innere Schleife wird n/2 mal ausgeführt,
also werden stets ½*(n2-n) Vergleiche durchgeführt!
Man sagt deshalb, Bubble Sort hat quadratische Laufzeit (wegen n2 in der o.a. Formel).
Dies ist ein schlechter Wert!
Wir werden noch Verfahren mit Laufzeit n*log(n) kennen lernen.
8.2.4 Einen Zeiger auf ein Array erzeugen
Der Name des Arrays bildet einen Zeiger auf das erste Element
Listing 82.
Zeiger auf array
int *p;
int sample[10];
p = sample;
sample und &sample erzeugen dieselben Ergebnisse.
8.2.5 Zeichenketten und Strings
Wenn man als Komponententyp char für Arrays nimmt, hat man ein Feld von einzelnen
Zeichen, eine Zeichenkette.
Listing 83.
Zeichenkette
char zeile[80];
Dies ist in C eine Möglichkeit, mit Strings umzugehen.
Eine Zeichenkette (C-string) in C ist Nullterminiert, d.h. als letztes Element der gültigen
Kette steht eine Null.
Die Zeichenkette “Hello world“ erzeugt den Nullterminator automatisch.
In der Standard Headerdatei <string > sind einige bekannte Funktionen definiert:
Listing 84.
c-string Funktionen
strcpy (str1, str2)
strcat (str1, str2)
strlen (str1)
strcmp (str1, str2)
strchr (str1, ch)
strstr (str1, str2)
// kopiert str2 nach str1
// hängt str2 an str1 an
// liefert die Länge von str1
// liefert 0, wenn beide strings identisch sind,
// <0, wenn str1 kürzer als str2 ist
// >0 wenn str1 länger als str2 ist
// liefert einen Zeiger auf das erste ch in str1
// liefert einen Zeiger auf das erste auftauchen von str2 in str1
Eine eindimensionale Zeichenkette kann wie folgt abkürzt initialisiert werden:
char array_name[size] = “string“;
In C++ existiert eine elegantere Möglichkeit, die Standardklasse string.
Ein String wird wie folgt definiert:
111
Listing 85.
Zeichenkette per string
string beispiel1 = “Hallo“;
string beispiel2(“Welt“);
Es existieren viele Funktionen, z.B.:
Verkettung +
Vergleiche ==, !=, <, <=, >, >=
Das Hallo-Welt-Programm kann damit auch so formuliert werden:
Listing 86.
HalloWelt.cpp
#include <iostream >
#include <string>
using namespace std;
int main()
{
string teil1 = "Hallo";
string teil2("Welt");
string zusammen;
zusammen = teil1 + ", " + teil2;
cout << zusammen << endl;
return 0;
}
Damit wird String-Bibliothek
bekannt gemacht.
$ string_HalloWelt.exe
Hallo, Welt
$ edit string_HalloWelt.cpp
$
Strings sind hier dynamische Objekte, d.h. der erforderliche Speicher wird automatisch
allokiert.
Es kommt nicht zu einer Speicherverletzung, wenn der String zu groß wird (wie bei
char b[80]).
Strings werden in den Beispielen immer wieder verwendet, um weiter Sprachkonstrukte zu
erklären.
Das folgende Beispiel faßt einige Operationen auf Strings zusammen.
112
Listing 87.
operationenAufStrings.cpp
$ operationenAufStrings
#include <iostream >
#include <string>
using namespace std;
int main()
{
string einString= "hallo";
// Stringausdruck ausgeben
cout << einString + "\n";
// String zeichenweise ausgeben
for (long i = 0; i < einString.size(); i++)
cout << einString[i] << " ";
cout << endl;
// String zeichenweise ausgeben
for (long i = 0; i < einString.length(); i++)
cout << einString[i] << " ";
cout << endl;
// Zeichen eines String ohne Indexprüfung ausgeben
cout << einString[100] << endl;
// String zeichenweise mit Indexprüfung ausgeben
for (long i = 0; i < einString.length(); i++)
cout << einString.at(i) << " ";
cout << endl;
// Zeichen eines String mit Indexprüfung ausgeben
cout << einString[100] << endl;
// Kopie eines String erzeugen
string einStringKopie(einString);
cout << einStringKopie << endl;
// Kopie durch Zuweisung
string neuerString("neu!");
einStringKopie = neuerString;
cout << einStringKopie << endl;
// Zuweisung Zeichenkettenkonstante
einStringKopie = "Zeichenkette";
cout << einStringKopie << endl;
// String verketten
einStringKopie += " mit Blanks";
cout << einStringKopie << endl;
// Strings vergleichen
if (einString > einStringKopie)
cout << "ENDE Beispiel!" << endl;
return 0;
hallo
hallo
hallo
hallo
hallo
neu!
Zeichenkette
Zeichenkette mit Blanks
ENDE Beispiel!
}
113
8.3 Strukturen
Ein Array war eine Zusammenfassung von Elementen des gleichen Typs. Will man eine
Komposition von Elementen auch unterschiedlichen Typs, dann sind Strukturen die Lösung.
Hier soll nur auf grundlegende Eigenschaften von Strukturen eingegangen werden, da sie
nochmals genauer im Zusammenhang mit Objekten behandelt werden.
Bild 64.
Allgemeine Form (Strukturdeklaration):
struct [Typname] { Components } [Variable_list];
Man beachte, daß hinter der Deklaration ein Semikolon gesetzt wird. Normalerweise werden
hinter der schließenden Klammer die Variablen des Typs definiert (Variable_list), deshalb
folgt das Semikolon.
Alternativ werden die Variablen separat definiert, trotzdem folgt auf die schließende
Klammer das Semikolon.
Listing 88.
Struktur.cpp
enum Wochentag {Sonntag, Montag, Dienstag, Mittwoch, Donnerstag, Freitag, Samstag};
struct Datum
{
int Tag;
int Monat;
int Jahr;
Wochentag TagBezeichnung;
};
Soll eine Variable dieses Typs definiert werden, so erfolgt dies durch:
Listing 89.
Definition einer struktur
Datum heute;
Auf eine Komponenten der Variablen kann durch den Selektionsoperator „.“ zugegriffen
werden.
Listing 90.
Zugriff auf struktur
heute.Monat = 7;
heute.TagBezeichnung = Montag;
114
Listing 91.
Bruch.cpp:
#include <iostream>
using namespace std;
int main()
{
struct bruch
{
// Deklaration
int zaehler;
int nenner;
};
bruch a, b, mul, sum;
// Definition von Variablen
a.zaehler = 2;
a.nenner = 3;
b.zaehler = 2;
b.nenner = 3;
sum.zaehler = a.zaehler * b.nenner + a.nenner * b.zaehler;
sum.nenner = a.nenner * b.nenner;
mul.zaehler = a.zaehler * b.zaehler;
mul.nenner = a.nenner * b.nenner;
cout << "a+b = " << sum.zaehler << "/" << sum.nenner << endl;
cout << "a*b = " << mul.zaehler << "/" << mul.nenner << endl;
return 0;
}
Ebenso wie Aufzähltypen sind auch Strukturen (Schlüsselwort struct) vom Benutzer
definierte Datentypen. Strukturen sind dann zweckmäßig, wenn die Werte (Objekte) eines
Datentyps keine unteilbaren "Atome" sind (also nicht weiter aufteilbar wie boolesche
Wahrheitswerte, char-Zeichen und ähnliches), sondern wenn die Objekte sich aus mehreren
Bestandteilen aufbauen und somit innere Struktur haben.
Ein Punkt der Ebene beispielsweise ist aufgebaut aus seinen Koordinaten, er hat zwei
Koordinaten. Ein Farbtupfer auf der Leinwand hat seine Position (also auch seine beiden
Koordinaten) und hat außerdem noch seine Farbe. Eine Person hat ihren Namen, ihr Alter,
eine Nummer im Personalausweis und viele andere Attribute. In C++-Strukturen werden all
diese Attribute in einer Liste zusammengefaßt und zwar individuell für jedes Objekt der
Struktur (also für jeden Punkt, für jeden Farbtupfer und für jede Person).
Listing 92.
Beispielobjekt
enum Farbtyp {rot, gruen, blau, gelb};
struct punkt
{
int x,y;
bool sichtbar;
Farbtyp farbe;
} p;
// p ist ein Punkt
punkt q;
// q auch
Auf die Attribute einer Struktur kann man bequem mittels Punkt-Notation zugreifen:
115
Listing 93.
Zugriff auf das Objekt
p.x = 270;
p.y = 20;
p.sichtbar = false;
p.farbe = gelb;
q = p;
q.sichtbar = true;
Um Klarheit in die Begrifflichkeiten Objekt, Datentyp, Programm, Laufzeit zu
bekommen, wollen wir betonen:
Datentypen sind im Programmtext implementiert. Der Datentyp punkt findet sich wieder im
obigen Programmfragment. Objekte (Instanzen) bestehen nur zur Laufzeit eines Programms
im Arbeitsspeicher des Rechners. Das obige Fragment legt zur Laufzeit zwei Objekte p und q
des Typs punkt an:
[ mit dem Debugger anschauen ]
116
9 Funktionen
Große Anwendungen werden müssen in kleinere Teile zerlegt werden. Hier werden
Mechanismen behandelt, wie dies erfolgen kann.
Diskutiert werden dazu die nicht objektorientierten Sprachmittel von C++:
• Funktionen,
• Compilerdirektive,
• Standardfunktionen
Funktionen sind ein C++ Sprachmittel, mit dem man große Programme in übersichtliche
Teile zerlegen kann. Funktionen sind Mittel zum Zweck der Modularisierung von
Anwendungen:
• eine Anwendung wird durch Aufteilung in überschaubare Einheiten (Module) zerlegt,
• ein einzelnes Modul ist austauschbar,
• man erhält einfache, klare Schnittstellen und
• kann eine Aufteilung von Aufgaben an mehrere Programmierer vornehmen.
Funktionen dienen der Abstraktion:
• eine Lösung wird schrittweise verfeinert, wobei
• Details der Implementierung verborgen werden.
Da eine Funktion eine klare Schnittstelle und Arbeitsweise hat, kann dieselbe Funktion
mehrfach verwendet werden
• im selben Programm
• oder in anderen Programmen, indem die Funktion in eine Bibliothek gestellt wird.
9.1 Eine Funktion
Die Verwendung von Funktionen verdeutlicht das folgende Beispiel.
Listing 94.
wurzel.cpp
#include <iostream >
using namespace std;
float Wurzel(int x)
// Funktionskopf
{
// Funktionsrumpf
return sqrt(x);
Vereinbarung
}
int main ()
{
Verwendung
int n;
// Deklaration von Variablen
float wurzel;
cout << "Bitte eine natürliche Zahl eingeben: ";
// Eingabedialog
cin >> n;
wurzel = Wurzel(n);
// Berechnung
cout << "Gelesen wurde n = " << n << endl
// Ergebnis ausgeben
<< "Dann ist Wurzel(n) = " << wurzel << endl;
return 0;
}
117
$ wurzel
Bitte eine natürliche Zahl eingeben: 2
Gelesen wurde n = 2
Dann ist Wurzel(n) = 1.41421
$
9.2 Grundlagen
Ein weiteres einfaches Programm, an dem die Verwendung von Funktionen demonstriert
werden kann, berechnet die Fakultät von eingegebenen Zahlen:
Listing 95.
fakultaet.cpp
#include <iostream >
using namespace std;
long fakultaet(int);
// Funktionsprototyp (Deklaration)
int main()
aktueller
{
Parameter
int x;
long res;
cout << "Eingabe Integer x: ";
cin >> x;
res = fakultaet(x);
// Aufruf der Funktion
formaler
cout << "fak(" << x << ") = " << res << endl;
Parameter
return 0 ;
}
long fakultaet(int n)
{
// Funktionsimplementierung (Definition)
long fak = 1;
for (int i=2; i<= n; i++)
fak = fak*i;
return fak;
}
Die Funktionsdeklaration teilt dem Compiler mit, um welche Art von Funktion es sich
handelt.
Die Funktionsdefinition spezifiziert, was genau die Funktion macht.
Der Funktionsaufruf bewirkt, dass die Aktionen der Funktion mit den Werten ausgeführt
werden, die der aktuelle Parameter definiert. Dabei wird nach der Ausführung der
Funktionsaktionen, der Name assoziiert mit dem berechneten Wert (return-wert).
118
Bild 65.
Die Syntax eines Funktionsprototyps hat die Form:
Rückgabetyp Funktionsname ( Parameterliste );
long fakultaet(int);
Listing 96.
Funktionsprototypen
int func();
int func (int, char);
int func (int x, char y);
void proc (int x, char y);
Bild 66.
// leere Parameterliste
// Liste mit Parametertypen
// Liste mit Parametertypen und Parameternamen
// „ohne“ Rückgabetyp = Prozedur
Die Syntax einer Funktionsdefinition hat die Form:
Rückgabetyp Funktionsname ( formale Parameterliste ) { Block }
long fakultaet(int) { … return res; };
Listing 97.
Funktionsdefinitionen
long power (long a, int b)
{
long result = 1;
while (b > 0)
{
result *= a;
b--;
}
return result;
}
double pi ()
{
return 3.1415927;
}
119
Bild 67.
Die Syntax eines Funktionsaufrufs hat die Form:
Funktionsname ( aktuelle Parameterliste )
res = fakultaet(x);
Zu beachten ist, dass die formalen und aktuellen Parameter in Anzahl und
korrespondierenden Typen übereinstimmen müssen.
Durch den Aufruf wird der Kode der Funktion (ihr Block) ausgeführt, dabei werden formale
Parameter durch aktuelle Parameter ersetzt und das Ergebnis nach Beendigung der Funktion
an den Aufrufer übergeben.
Listing 98.
Funktionsaufrufe
…..
double pi ()
{
return 3.1415927;
}
int main()
{
float x;
x = 2.3 * pi();
cout << x;
return 0 ;
}
double pi ()
{
return
int main()
{
float x;
x = 2.3 * ?
cout << x;
return 0 ;
}
Anstelle der Struktur
1. Funktionsprototyp
2. main mit Funktionsaufruf
3. Funktionsdefinition
kann auch folgendes stehen:
1. Funktionsdefinition
2. main mit Funktionsaufruf
120
Listing 99.
Funktionsdefinition zuerst, power.cpp
#include <iostream>
using namespace std;
long power (long a, long b)
{
long result = 1;
while (b > 0)
{
result *= a;
b--;
}
return result;
}
int main()
{
int x, y;
cout << "Eingabe Integer x y: ";
cin >> x >> y;
cout << x << " hoch " << y << " = " << power(x,y) << endl;
return 0;
}
9.3 Gültigkeitsbereich und Sichtbarkeit
In C++ gibt es Gültigkeits- und Sichtbarkeitsregeln für Namen:
Namen sind nur nach der Deklaration und nur innerhalb des Blocks gültig, in dem
sie deklariert wurden. Sie sind also block lokal.
Namen sind auch gültig für Blöcke, die innerhalb des Blocks angelegt sind (innerer
Block).
Die Sichtbarkeit (engl. visibility) (z.B. von Variablen) wird eingeschränkt durch die
Deklaration von Variablen gleichen Namens: im inneren Block ist die Variable des
äußeren Blocks nicht sichtbar, sie ist verdeckt.
Die Variablen, die innerhalb eines Funktionsblocks deklariert sind funktionslokale
Objekte und daher beim Aufrufer nicht bekannt. Z.B. kann in der o.a. main nicht auf
die lokale Variable result der Funktion power zugegriffen werden.
Formale Parameter von Funktionen verhalten sich bzgl. der Gültig- und Sichtbarkeit
wie lokale Variable.
121
Listing 100. gueltigSichtbar01.cpp
#include <iostream >
using namespace std;
int a=0;
int b=2;
int main()
$ gueltigSichtbar01
// global gültig
{
// Blocktiefe 1
cout << "a: " << a << endl;
// neues a, verdeckt globales a
int a = 1;
cout << "a: " << a << endl;
{
a: 0
a: 1
// Blocktiefe 2
a: 1
cout << "a: " << a << endl;
// a aus Block 1
int a = 2
// a aus Block 1 gültig,
//aber nicht mehr sichtbar
cout << "a: " << a << endl;
a: 2
}
cout << "a: " << a << endl;
return 0;
a: 1
}
122
In C++ existiert ein Scope-Operator (::), mit dem der Zugriff auf die Objekte, die in der
ganzen Datei gültig sind (also nicht jeweils eine Stufe höher), möglich ist.
Listing 101. gueltigSichtbar02.cpp mit scope
#include <iostream>
using namespace std;
int a=0;
int b=2;
int main()
{
cout << "a: " << a << endl;
$ gueltigSichtbar02
// global gültig
// Blocktiefe 1
int a = 1;
// neues a, verdeckt globales a
cout << "a: " << a << endl;
cout << "::a: " << ::a << endl;
{
a: 0
a: 1
::a: 0
// Blocktiefe 2
// a aus Block 1
a: 1
cout << "a: " << a << endl;
int a = 2;
// a aus Block 1 gültig,
//aber nicht mehr sichtbar
a: 2
cout << "a: " << a << endl;
::a: 0
cout << "::a: " << ::a << endl;
}
cout << "a: " << a << endl;
return 0;
}
$
a: 1
Im Rechner wird ein Datenbereich für Block lokale Objekte bei Betreten des
Gültigkeitsbereichs auf einem speziellen Speicherbereich (Stack) angelegt und bei Verlassen
des Gültigkeitsbereichs automatisch entfernt.
Eine Ausnahme bilden als static markierte Variable: static Variable erhalten einen
festen Speicherplatz und werden nur einmal beim ersten Aufruf der Funktion initialisiert.
9.4 Parameterübergabemechanismen
Die Parameter einer Funktion bilden die Schnittstelle zum Datentransfer zwischen Aufrufer
und aufgerufener Funktion (neben dem Rückgabewert der Funktion).
In Programmiersprachen werden u.a. folgende Arten der Parameterübergabe unterschieden:
9.4.1 Wertübergabe (engl.: call by value) allgemein
Mechanismus:
• Der Wert des aktuellen Parameters wird beim Aufruf des Unterprogramms ermittelt
und in den formalen Parameter kopiert.
Folge:
• Als aktuelle Parameter sind auch Ausdrücke möglich.
• Änderungen am formalen Parameter im Unterprogramm beeinflussen den Wert des
aktuellen Parameter nicht.
123
•
Soll mit call by value eine Änderung durch das aufgerufene Unterprogramm erzielt
werden, so müssen Zeiger übergeben werden und das Unterprogramm muss
dereferenzieren. Dabei bleibt aber der Parameterwert (Zeiger) unverändert.
Das werden wir später sehen!
Programmiersprachen:
• Ada, ALGOL 60, C, C++, Pascal
9.4.2 Adressübergabe (Variablenüberg., engl.: call by reference) allg.
Mechanismus:
• Die Speicheradresse des aktuellen Parameters wird ermittelt und in den formalen
Parameter kopiert.
Folge:
• Ausdrücke haben keine Adressen, damit können sie nicht als Parameter übergeben
werden.
• Änderungen des formalen Parameters sind beim Aufrufer sichtbar.
Programmiersprachen:
Ada, C++, Pascal
9.4.3 Namensübergabe (engl.: call by name) allgemein
Mechanismus:
• Der Wert des aktuellen Parameters wird bei jeder Verwendung im Unterprogramm
neu berechnet (im Gegensatz zur einmaligen Berechnung beim Eintritt in das
Unterprogramms bei Adressübergabe).
Folge:
• Ausdrücke sind als aktuelle Parameter möglich.
• Falls der aktuelle Parameter ein Ausdruck ist oder einen enthält (z.B. als Index), so
entsteht ein hoher Rechenaufwand zur Laufzeit.
• Änderungen des formalen Parameters sind beim Aufrufer sichtbar.
• Die Wirkung ist i.A. schwer nachvollziehbar.
Programmiersprachen:
ALGOL 60
9.4.4 Wertübergabe konkret
Der Wert des aktuellen Parameters wird beim Aufruf der Funktion ermittelt und in den
formalen Parameter kopiert.
Somit sind als aktuelle Parameter auch Ausdrücke möglich:
124
Listing 102. add01.ccp
#include <iostream >
using namespace std;
float addiere(float x, float y)
{
return x + y;
}
Ausdruck
int main()
{
float x = 2;
float y;
y = addiere(x, x*3);
cout << y << endl;
return 0 ;
}
$ add01
8
$
Im folgenden Beispiel wird die locale Variable inkrementiert.
Listing 103. inc1.c
#include <iostream>
using namespace std;
void inc1(int p)
{
p++;
cout <<" in inc1: p = " << p << endl;
}
int main()
{
int x;
x=1;
cout << " vor inc1: x = " << x << endl;
inc1(x);
cout << "nach inc1: x = " << x << endl;
return 0;
}
$
$ inc1
vor inc1: x = 1
in inc1: p = 2
nach inc1: x = 1
$
Der Aufruf der Funktion inc1 hat keinen Effekt beim Aufrufer hinterlassen.
Dies ist aber kein Nachteil, denn damit kann man die formalen Parameter ändern und als
lokale Variable verwenden.
125
Listing 104. Quersumme.cpp
#include <iostream >
using namespace std;
long qsum(long z)
{
z wird verändert
long sum = 0;
while (z>0)
{
sum += z % 10;
z = z / 10;
}
$ quersumme
return sum;
Integer: 1001
}
Quersumme von
int main()
ist 2
{
long x;
$
cout << "Integer: ";
cin >> x;
cout << "Quersumme von " << x << " ist " << qsum(x) << endl;
return 0;
}
1001
9.4.5 Referenzübergabe konkret
Die Speicheradresse des aktuellen Parameters wird ermittelt und in den formalen Parameter
kopiert.
Verwendet werden muss hier ein formaler Parameter vom Typ „Referenz“, ausgedrückt
durch das Zeichen „&“. Damit arbeiten Aufrufer und aufgerufene Funktion mit derselben
Speicherstelle, also mit demselben Objekt.
Der Aufruf der Funktion inc2 hat einen Effekt beim Aufrufer hinterlassen.
Listing 105. inc2.c
#include <iostream >
using namespace std;
void inc2(int &p)
{
p++;
cout <<" in inc1: p = " << p << endl;
}
int main()
{
int x;
x=1;
cout << " vor inc1: x = " << x << endl;
inc2(x);
cout << "nach inc1: x = " << x << endl;
return 0;
}
$ inc2
vor inc2: x = 1
in inc2: p = 2
nach inc2: x = 2
$
erhöht
Hier kann als aktueller Parameter kein Ausdruck und keine Konstante angegeben werden!
126
Call-byreference-Aufrufe sind in C nur sehr umständlich über Zeiger zu realisieren. Dank des
Referenzoperators ‘&’ in C++ können wir auch die Verweisübergabe ganz einfach
programmieren. Betrachten wir zum Vergleich die Funktion vertausche in C und C++:
Listing 106. vertausche_C
void vertausche_C (int * a, int * b) /* call by reference */
{
int hilf = *a;
*a = *b;
*b = hilf;
}
// C++ Version:
void vertausche (int& a, int& b) // call by reference
{
int hilf = a;
a = b;
b = hilf;
}
Nicht nur die Definition der C++ Funktion ist einfacher, auch der Aufruf ist benutzerfreundlicher.
Sind i und j zwei int-Variablen, so werden in C++ nur die Variablen und nicht die Zeiger übergeben:
vertausche_C (&i, &j);
vertausche (i, j);
// Zeiger
Wenn man Parameter nicht verändern will oder wenn man Referenzparameter nur aus
Effizienzgründen verwendet (also nicht zur Rückgabe von Werten), dann sollte man solche
Parameter als const deklarieren:
• dies ist die Zusicherung an den Compiler, dass der Funktionskörper diese Parameter
nicht ändert;
• der Compiler kontrolliert das und entdeckt versehentliche Änderungen.
Soll etwa eine Variable anton vom Typ struct person übergeben werden, die im Unterprogramm nicht
verändert wird, so ist es aus Laufzeitgründen sicher vorteilhaft, nur einen Zeiger, sprich eine
Referenz, auf diese Struktur zu verwenden.
Um Änderungen an dieser Struktur zu unterbinden, die dann ja auch im aufrufenden Programm
wirksam würden, genügt folgende Deklaration einer Funktion xyz:
int xyz ( const struct person & anton );
Der Referenzoperator ist nicht nur auf Parameter anwendbar. Es gibt auch Referenzvariablen.
Betrachten wir ein Beispiel:
int i = 1;
int &r = i; // r zeigt auf den gleichen Speicherinhalt wie i
r = 2;
cout << i; // es wird der Wert 2 ausgegeben
Die Referenzvariable r ist genaugenommen ein konstanter Zeiger. Bei der Deklaration muss
diese Referenzvariable daher mit einer existierenden Variable vorbelegt werden. Diese Vorbelegung
ist nicht mehr veränderbar. Bei jeder Verwendung einer Referenzvariablen erfolgt automatisch eine
Dereferenzierung, so dass sich diese Referenzvariable wie eine gewöhnliche Variable verhält. Im
obigen Beispiel wurde r auf die Adresse von i gesetzt. Sowohl i als auch r beziehen sich damit auf
den gleichen Speicherplatz. Eine Änderung dieses Speicherinhalts durch eine der beiden Variablen
wirkt sich demnach immer auf beide Variablen aus.
127
Bei Referenzen als Parameter einer Funktion entfällt die Initialisierung, da ja beim Aufruf
automatisch eine Wertezuweisung erfolgt. Mit obiger Erklärung einer Referenzvariablen wird auch
das Verhalten beim Aufruf einer Funktion mit Referenzparametern klar: die Referenz identifiziert sich
mit der aufrufenden Variable, so dass Zugriffe mit der Referenzvariablen immer auch Zugriffe auf die
aufrufende Variable sind.
9.4.6 Referenz als Funktionsergebnis
Betrachten wir noch eine besondere Anwendung von Referenzen, um ihre Mächtigkeit zu erahnen:
Referenzen dürfen auch als Funktionsergebnis zurückgeliefert werden. In diesem Fall ist das
Ergebnis einer solchen Funktion nicht nur ein Ausdruck, sondern im Sinne der Syntax von C++ eine
Variable. Schließlich identifiziert sich eine Referenz ja immer mit einer Variablen. Wir können auf
diese Funktion daher sogar den Inkrementoperator ‘++’ anwenden:
Listing 107. int & plus (int &a) // Referenz als Rueckgabe
int & plus (int &a)
{
return ++a;
}
// Referenz als Rueckgabe
void main( void )
{
int i;
cout << "Geben Sie bitte eine Ganzzahl ein: ";
cin >> i;
cout << "Ergebnis: " << ++ plus(i) << '\n';
}
Beachten Sie bei der Rückgabe des Funktionsergebnisses als Referenz, dass dieses Funktionsergebnis
eine Variable mit eindeutig definiertem Inhalt sein muss! Der Bezeichner ‚++a‘ entspricht
dieser Vorgabe: es handelt sich um die Variable a, die vorher um den Wert 1 erhöht wurde. Die
Angabe ‚a++‘ hingegen wäre fehlerhaft. Hier handelt es sich um keinen sogenannten L-Value, ein
Wert, der links vom Gleichheitszeichen stehen darf.
Weitere Anwendungen, auch mit Referenzen als Funktionsrückgabe, werden beim Überladen von
Funktionen behandelt. Zunächst soll es genügen, dass wir ab sofort Referenzübergaben von
Parametern ganz einfach realisieren können.
9.4.7 Arrays als Parameter
Aus dem Erbe von C gibt es in C++ einen Spezialfall: Arrays als Parameter.
Man kann in C/C++ nicht ganze Arrays an Funktionen übergeben.
Die normale Übergabekonvention ist ”call by value”, die diese Array-Übergabe verhindert.
Allerdings kann eine Zeiger auf das Array übergeben werden (call by reference).
Deshalb:
• es wird nicht das Array, sondern seine Anfangsadresse übergeben ohne dass man
explizit call by reference (durch &) angibt;
• die Funktion arbeitet nicht mit einer lokalen Kopie, sondern mit dem Original;
• die Funktion kennt die Größe des Arrays nicht
• Array als Ergebnis einer Funktion ist illegal.
128
Das folgende Fragment übergibt die Adresse sample des Arrays
Listing 108. Call by reference
int main(void)
{
int sample[10];
func1(sample);
.
.
.
}
9.4.7.1 Eindimensionale Arrays
Da hier nur Zeiger übergeben werden, kann man eindimensionale Arrays gleichwertig als
Zeiger, als Array fester Größe oder als Array ohne Größenangabe übergeben.
Listing 109. Übergabe von Array-Adressen
void func(int *sample)
void func(int sample[10])
void func(int sample[])
/* Zeiger */
/* Array mit fester Groesse*/
/* Array unbestimmter Groesse*/
9.4.7.2 Mehrdimensionale Arrays
Beispiel zweidimensionales Array. In Wirklichkeit wird auch hier nur ein Zeiger auf das erste
Element übergeben. Allerdings muß der Parameter, der das Array übernimmt, wenigstens die
Größe der rechtsstehenden Dimension (Spalte) definieren, da der Compiler die Länge einer
Zeile zur korrekten Adressierung kennen muß.
Listing 110. Vereinbarung eines zweidimensionalen Arrays als Funktionsparameter
void func1(int sample[][10])
{
.
.
.
}
129
Listing 111. kumuliere.cpp (Kumuliere Arraywerte)
#include <iostream>
using namespace std;
void kumuliere (int v[], int laenge)
{
int i;
cout << "kum sizeof " << sizeof(v) << endl;
for (i=1; i<laenge; i++)
v[i] += v[i-1];
}
int main ()
{
int x[] = {1,2,3,4,5,6};
cout << "main sizeof " << sizeof(x) << endl;
kumuliere (x, 3);
for (int i=0; i<5; i++) cout << x[i];
cout << endl;
return 0;
}
// sizeof(v) liefert 4 !!!
// return v wäre illegal
$ kumuliere
main sizeof 24
kum sizeof 4
13645
$
$
9.4.8 Variable Anzahl Parameter, Default Parameter
Funktionen können mit variabler Anzahl von Parametern aufgerufen werden. In der
Prototypendeklaration müssen für die nicht angegebenen Parameter Vorgabewerte (engl.
defaults) angegeben werden.
Dadurch können Programme in ihrer Funktionalität erweitert werden, ohne dass die „alten„
Funktionsaufrufe geändert werden müssen.
Beispiel (Ausgabe Aktienkurs)
Listing 112. aktienKurs01.cpp
#include <iostream>
#include <iomanip.h>
using namespace std;
void printRate(float) ;
int main()
{
float r=19.126;
printRate(r);
return 0;
}
void printRate(float rate)
{
cout << setw(10) << setprecision(6)
<< rate << endl;
}
$ aktienKurs01
19.126
$
Das Programm soll so geändert werden, dass die Währung mit ausgegeben wird, der Default
soll „USD“ sein.
130
Listing 113. $ cat aktienKurs02.cpp, default parameter
include <iostream>
#include <iomanip.h>
#include <string>
using namespace std;
void printRate(float rate, string currency="USD") ;
int main()
{
float r=19.126;
printRate(r);
printRate(r, "EUR");
return 0;
}
void printRate(float rate, string currency)
{
cout << setw(10) << setprecision(6)
<< rate << " " << currency << endl;
}
$ aktienKurs01
19.126 USD
19.126 EUR
$
9.4.9 static Variablen in Funktionen
In C++ gibt es eine Speicherklasse static, mit der der Speicherplatz funktionslokaler
Variablen nach Terminierung erhalten bleibt. Damit hat man die Möglichkeit, dass
Funktionen ein „Gedächtnis“ erhalten. Beim ersten Aufruf der Funktion wird der
Speicherplatz initialisiert, bei jedem weiteren Aufruf wird mit dem vorherigen Wert weiter
gerechnet, siehe auch Kapite.
Beispiel (Zufallszahlengeneratoren):
Algorithmus:
Zufallszahlen können nach der linearen Kongruenzmethode wie folgt ermittelt
werden:
Rn+1 = (a * Rn + c)%m mit R, a, c ≥ 0, m ≥ R, a, c
Die Kunst besteht darin, a,c und m so zu wählen, dass der Generator eine
Gleichverteilung erzeugt. Als gute ausreichend gut haben sich große Zahlen für a und
Primzahlen für c und m erwiesen.
Eine Funktion rand verwendet eine static Variable, die jeweils das zuletzt berechnete Rn
speichert.
131
Listing 114. random01.cpp
#include <iostream>
using namespace std;
float random(long seed)
{
static long r = seed * 1000;
const int a = 125;
const int c = 3;
const int m = 1717;
r = (r*a+c)%m;
return (float) r/m;
}
int main()
{
for (int i = 1; i<=20; i++)
cout << random(101) << endl;
return 0;
}
// erster Wert
// Zahl [0,1]
$ random01
0.942924
0.86721
0.403029
0.380314
0.54106
0.634246
0.282469
0.310425
0.804892
0.613279
0.661619
0.704135
0.0186372
0.331392
0.425743
0.219569
0.447874
0.986022
0.254514
0.815958
Schutz gegen Veränderung im Unterprogramm-Modul
#include <stdio >
using namespace std;
void sp_to_dash(const char *str);
int main(void)
{
sp_to_dash("Dies ist ein Test");
return 0;
}
void sp_to_dash(const char *str)
{
while(*str)
{
if(*str== ' ') printf("%c", '-');
else printf("%c", *str);
str++;
}
}
Der String, der in der Funktion bearbeitet wird, ist gegen Veränderung geschützt.
Deshalb führt folgender Versuch zum Compile-Time Fehler
132
Listing 115. Zugriff auf const
/* Dies ist falsch. */
void sp_to_dash(const char *str)
{
while(*str)
{
if(*str==' ' ) *str = '-';
printf("%c", *str);
str++;
}
}
/* dies geht nicht; str ist const */
9.4.10 Seiteneffekte
Seiteneffekte entstehen, wenn Funktionen globale Variable verändern. Dies sollte stets
vermieden werden. Eine Funktion sollte ausschließlich Werte ändern, die sie über ihre
Parameter erhält, d.h. die Wirkung einer Funktion ist an der Aufrufstelle ersichtlich.
Negativ Beispiel:
Listing 116. gerade.cpp
#include <iostream >
using namespace std;
int x = 0;
void nexteGeradeZahl()
{
x += 2;
}
int main()
{
cout << x << endl;
nexteGeradeZahl();
cout << x << endl;
x++;
nexteGeradeZahl();
cout << x << endl;
return 0;
}
$
// globale Variable
Funktion verändert
globale Variable
// ab jetzt bewirkt nexteGeradeZahl eine ungerade Zahl
9.5 Rekursive Funktionen
In einer Funktion kann eine andere Funktion aufgerufen werden. C++ erlaubt es auch (wie
die meisten anderen Programmiersprachen), dass die gleiche Funktion im eigenen
Funktionskörper aufgerufen wird. Dies bezeichnet man als direkte Rekursion.
Das folgende Programm erzeugt eine rekursive Struktur als Ausgabe:
133
Listing 117. rekursion1.cpp
#include <iostream>
using namespace std;
void druck(int x, int l)
{
for (int i = 0; i <= l; i++)
cout<< " ";
cout << x << endl;
if (x != 0)
druck(x-1, l+1);
for (int i = 0; i <= l; i++)
cout<< " ";
cout << x << endl;
}
int main()
{
int x;
cout << "Eingabe Integer: ";
cin >> x;
druck ( x, 0 );
return 0;
}
$ rekursion1.exe
Eingabe Integer: 4
4
3
2
1
0
0
1
2
3
4
$
Dies sieht verdächtig nach einem Endlosprogramm aus. Parallelen zu while-Schleifen werden
sichtbar. Dort waren Abbruchkriterien erforderlich. Meist wurde ein Schleifenzähler
mitgeführt, dessen maximal erlaubter Grenzwert abgefragt wurde. Die Endlosaufrufe unserer
obigen Funktion xyz lässt sich ebenfalls vermeiden, wenn weitere Prozeduraufrufe an eine
Bedingung geknüpft werden, etwa mit einer If-Anweisung.
Rein prinzipiell wären also sich selbst aufrufende Funktionen möglich. Die meisten
Funktionen besitzen aber auch Parameter und lokale Variablen. Hier stellt sich die nächste
Frage: Kommen sich diese Variablen innerhalb der einzelnen Aufrufe gegenseitig in die
Quere? Die Antwort ist nein.
Für einen Compiler sind alle Funktionsaufrufe gleichwertig, egal ob es sich beim Aufruf um
ein anderes oder das gleiche Unterprogramm handelt. Entsprechend sind alle Parameter und
Variablen der aufgerufenen Funktion lokal. Sie beeinflussen daher in keinster Weise
Parameter und Variablen gleichen Namens des aufrufenden Programmteils. Ein Compiler
legt dazu je Funktionsaufruf dessen sämtliche Parameter und Variablen neu an. Sehen wir
uns grob das Vorgehen von Compilern an, die solche Selbstaufrufe zulassen (dazu gehören C
und C++ Compiler!) und betrachten den erforderlichen Speicherbedarf eines Programms.
Nach dem Übersetzen eines Programms wird der grau hinterlegte Bereich statisch angelegt.
Dies sind das Laufzeitsystem, der gesamte Code und die globalen Daten des Programms.
Wichtig ist, daß die lokalen Daten der einzelnen Funktionen erst zur Laufzeit des Programms
bei Bedarf, also bei Aufruf der entsprechenden Funktionen, angelegt werden. Entsprechend
werden nach dem Verlassen dieser Funktionen deren Daten sofort wieder freigegeben. Dies
ist auch der Grund, warum Daten von Funktionen nach deren Beendigung undefiniert sind.
134
Das Programm kann diesen Datenbereich nämlich sofort anderweitig nutzen, der Inhalt geht
verloren.
Langsam wird klar, dass sich ein Programm ohne gegenseitige Beeinflussung durch interne
Variablen mehrfach selbst aufrufen kann. Bei jedem neuen Aufruf wird im Speicher einfach
ein zusätzlicher Datenbereich angelegt. Beim Verlassen der zuletzt aufgerufenen Funktion
wird der Speicher wieder freigegeben usw. Rief sich eine aufgerufene Funktion etwa noch
drei weitere Male auf, so sieht der Speicher des Programms wie folgt aus:
Das Aufrufen der eigenen Funktion aus der Funktion heraus heißt rekursiver Aufruf. Die
Verwendung solcher Aufrufe heißt Rekursion. Nicht rekursive Programme heißen zur
Unterscheidung iterativ. Der Sinn der Rekursion liegt darin, daß viele Probleme in rekursiver
Form definiert sind.
Betrachten wir dazu ein erstes Beispiel, die Definition der Fakultätsfunktion:
Im letzten Fall konnte die Funktion fakul analog zur mathematischen Definition
niedergeschrieben werden. Wir wollen das Arbeiten mit Rekursionen anhand des Beispiels
nachvollziehen. Betrachten wir dazu das Vorgehen des Programms beim Aufruf von fakul(3):
135
Wir erkennen, dass in diesem Fall die Funktion fakul viermal aufgerufen wird. Erst beim
Aufruf
von fakul(0) erfolgen keine weiteren rekursiven Aufrufe mehr, der Wert kann direkt
berechnet wer
den. Dies hat dann ein schrittweises Beenden aller rekursiv aufgerufenen Funktionen zur
Folge.
Dieses Beispiel zeigt eindrucksvoll, daß die Rekursion tatsächlich abbricht. Im obigen
Beispiel ist die Abbruchbedingung durch n==0 gegeben.
Die Rekursion ist eine Fertigkeit, die nicht oft, aber gerade bei mathematischen Problemen
immer wieder benötigt wird. Interessante Beispiele sind die Türme von Hanoi oder das
sogenannte Acht-Damen-Problem. Dieses Problem lautet, 8 Damen auf einem Schachbrett so
zu platzieren, dass diese sich nicht gegenseitig schlagen können. Es gelten dabei die
normalen Schachregeln. Wichtige Sortieralgorithmen wie Mergesort und der extrem schnelle
Quicksort bauen auf rekursiven Algorithmen auf. Auch viele interessante Grafiken erhält
man über Rekursion (Hilbertkurven, Pythagoras- Bäume).
9.5.1 Überladen von Funktionen
In C++ können Funktionen überladen (engl. overloading) werden. Dies bedeutet, dass
derselbe Funktionsname verwendet werden kann, um gleichartige Operationen mit Daten
unterschiedlichen Typs ausführen zu können.
Dadurch werden mehrere Funktionen, die alle denselben Namen haben erzeugt. Die
Entscheidung, welche der Funktionen ausgeführt wird, wird durch den Aufrufkontext
bestimmt: die Funktionen müssen alle unterschiedliche Signaturen (=Reihenfolge und Typ
der Parameter) haben. Die Signatur wird vom Compiler verwendet, um die Funktion zu
bestimmen.
So werden im folgenden Beispiel drei Funktionen mit demselben Namen realisiert, die das
maximale Element eines Arrays ermitteln, aber sich in der Signatur unterscheiden.
136
Listing 118. Überladen, Maximales Feld in Array, maxElement.cpp
#include <iostream>
using namespace std;
double maxElement(double daten[], int anzahl)
{
double max=daten[0];
for (int i=1; i<anzahl; i++)
if (daten[i] > max)
max=daten[i];
return max;
}
int maxElement(int daten[], int anzahl)
{
int max=daten[0];
for (int i=1; i<anzahl; i++)
if (daten[i] > max)
max=daten[i];
return max;
}
int maxElement(int daten[], int anzahl, int start)
{
int max=daten[start];
for (int i=start+1; i<anzahl; i++)
if (daten[i] > max)
max=daten[i];
return max;
}
int main()
{
double doubleFeld[]={1.2, 3.4, 4.5, -6.7};
cout << maxElement(doubleFeld, 4) << endl;
int intFeld[]={1, 3, 4, -6};
cout << maxElement(intFeld, 4) << endl;
cout << maxElement(intFeld, 3, 1) << endl;
return 0;
// Fkt. 1
// Fkt. 2
// Fkt. 3
$ maxElement
4.5
4
4
$
// Fkt. 1
// Fkt. 2
// Fkt. 3
}
9.6 Funktion main
Ein C++ Programm (nicht objektorientierte Sicht) besteht aus einer Menge von Funktionen,
von denen genau eine den Namen main hat.
Somit darf main nicht überladen werden. Weiterhin darf main nicht von einer anderen
Funktion aus aufgerufen werden.
Der Ergebnistyp von main ist nicht fest vorgegeben; normalerweise ist es int. Damit kann
die Laufzeitumgebung eines Programms den return-Wert von main auswerten. In Unix
gibt es die Konvention, dass ein Programm im Fehlerfall einen von 0 verschiedenen Wert
zurück liefert, ansonsten 0.
Die einfache Form der Funktion main ist:
137
Listing 119. main.cpp
#include <iostream>
using namespace std;
$ main
Positive Zahl: 2
1.41421
$ echo $?
0
$ main
Positive Zahl: -2
$ echo $?
1
$
int main()
{
int x;
cout << "Positive Zahl: ";
cin >> x;
if (x>0)
cout << sqrt(x) << endl;
else
return 1;
return 0;
}
Die Version von main mit Parametern, bei der auf die komplette Aufrufumgebung
zugreifbar ist, werden wir später kennen lernen, nachdem Zeiger bekannt sind.
9.6.1 Argumente aus der Kommandozeile
Bisher haben wir die Funktion main immer ohne Parameter betrachtet. Als formale
Parameter der Funktion main sind zumindest zwei in jeder C++ Implementierung
vorhanden. Die meisten Compilerhersteller erlauben aber drei Parameter.
Die Funktion main wird beim Aufruf eines Programms von der Konsole mit den aktuellen
Parametern versorgt – man gibt also beim Aufruf die Parameter von der Kommandozeile aus
mit.
Die Funktion main ist definiert als:
Bild 68.
Argumente aus der Kommandozeile
int main( int argc, char* argv[]) {…}
Anzahl Parameter
inklusive Programmname
Zeichenkette, enthält Programmname
argv
0
1
Zeichenkette, enthält
1. Parameter
…
Wird ein C++ Programm z.B. mit 2 Parametern aufgerufen, so ist argc==3, und argv[1],
… argv[2] sind die Parameter.
138
Beispiel (Echo der Parameter):
Listing 120. echo.cpp
#include <iostream>
using namespace std;
int main(int argc, char* argv[])
{
cout << "Anzahl Argumente: "
<< argc << endl;
for (int i=0; i<argc; i++)
cout << i << ": " << argv[i]
<< endl;
return 0;
}
Daten in main
argv
e
c
h
o
1
2
3
\0
a
s
\0
\0
0
1
$ echo 123 as
Anzahl Argumente: 3
0: echo
1: 123
2: as
$
3
argc
…
Was druckt das folgende Programm?
Listing 121. UnixEcho.cpp
#include <iostream>
using namespace std;
int main(int argc, char* argv[])
{
argv++;
while ( *argv)
cout << *argv++ << " ";
cout << endl;
return 0;
}
139
Der dritte Parameter von main zeigt auf die Aufrufumgebung (=Umgebungsvariable mit
Wert) eines Programms:
Listing 122. env.cpp
#include <iostream.h>
int main(int argc, char* argv[], char *env[])
{
while ( *env)
cout << *env++ << endl;
return 0;
}
$ env
PROCESSOR_ARCHITECTURE=x86
TEMP=/temp
LOGNAME=Alois Schütte
MAKE_MODE=unix
HOME=/home/Alois Schütte
PATH=.:/usr/local/bin:/usr/bin…:/bin:.:/u
sr/local/bin:/usr/bin:/bin:/cygdrive/c/WI
NDOWS/system32:/cygdrive/c/WINDOWS:/
$
9.7 Ausnahmebehandlung über Funktionsgrenzen
Eine throw-Anweisung erzeugt bekanntlich ein Ausnahmeobjekt, das eine Fehlermeldung
und/oder Daten zum Wiederaufsetzen enthält.
Das Ausnahmeobjekt wird in der Aufrufhierarchie zur Fehlerbehandlung weitergereicht:
• jede Funktion, die kein passendes catch enthält, wird ordentlich beendet,
• dann wird in der Aufrufumgebung weiter nach einem catch gesucht.
Damit ist die Fehlerbehandlung über mehrere Aufrufebenen hinweg möglich:
• eine Bibliotheksfunktion erkennt Fehler, kann sie aber nicht behandeln
• die Aufrufumgebung kann Fehler behandeln, aber nicht selbst erkennen.
Beispiel (Funktion wurzel catched nicht alle Fehler)
140
Listing 123. ausnahme.cpp
#include <iostream>
using namespace std;
$ ausnahme
Zahl eingeben: 2
1.41421
$ ausnahme
Zahl eingeben: -2
Fehler: negative
Diskriminante
-1
$ ausnahme
Zahl eingeben: 105
Fehler: Wert um 5 zu groß
$
float wurzel(int x, int maxWert)
{
try
{
if (x<0)
throw "negative Diskriminante";
if (x > maxWert)
throw x-maxWert;
} catch (const char * text)
{
cout << "Fehler: " << text << endl;
return -1;
} // kein catch für int !!
return sqrt(x);
}
int main()
{
int zahl; float ergebnis;
cout << "Zahl eingeben: ";
try { cin >> zahl;
ergebnis = wurzel(zahl, 100);
cout << ergebnis << endl;
} catch (int wert)
{
cout << "Fehler: Wert um " << wert << " zu groß" << endl;
}
}
9.7.1 Die Funktion als Vertrag
Eine Funktion führt eine Teilaufgabe aus und ändert dabei den Zustand eines Programms.
In guten Programmen sind die Bedingungen, die vor (Precondition) und nach (Postcondition)
dem Funktionsaufruf erfüllt sind, im Programmkopf dokumentiert.
Sind die Bedingungen nicht erfüllt, sollte ein Programm geordnet abbrechen.
Diese Sichtweise kann als Vertrag aufgefasst werden.
Listing 124. Schnittstellenspezifikation = Vertrag zwischen Aufrufer und Funktion
Beispiel Autoreparatur:
Kunde
Aufrufumgebung
• erteilt Reparaturauftrag
• Funktionsaufruf
• liefert sein defektes Auto ab und gibt eine
• aktuelle Parameter
Fehlerbeschreibung
Werkstatt
Funktion
• repariert
• führt Funktionskörper { } aus
• liefert repariertes Auto ab
• return Ausdruk
Vertrag zwischen beiden
Schnittstellenspezifikation
• wenn die Voraussetzungen erfüllt sind
• Precondition
(richtiger Autotyp, Reparaturfähigkeit)
• Postcondition
• dann ist danach der Fehler behoben
Pre- und Postcondition beschreiben "von außen sichtbare" Bedingungen:
141
• PRE:
betrifft aktuelle Parameter (und globale Daten)
• POST:
betrifft aktuelle Parameter, (globale Daten) und Ergebnis
PRE und POST sind Teil der Funktionsbeschreibung:
• können umgangssprachlich formuliert werden
• besser ist aber: formale Spezifikation, logischer Ausdruck
Die Korrektheit einer Funktion ist dann:
• wenn PRE eingehalten ist, dann gilt nach Ausführung auch POST
Noch besser als Korrektheit ist Robustheit:
• Verletzung der Precondition führt nicht zum Absturz, sondern
• zu definiertem Verhalten für jeden möglichen Parametersatz
Listing 125. assertions (Fragment)
#include <assert.h>
double DritteWurzel (double x)
// liefert die Kubikwurzel von x
der Name "Wurzel" wäre eine
ungenaue Beschreibung
// PRE: x >= 0
// POST: Ergebnis = dasjenige y mit
//
abs(y3-x) <= 10-4 und y >= 0
{
assert (x>=0.0);
mathematisch oder
umgangssprachlich
PRE: programmiersprachlich
double y;
// hier folgt der Algorithmus zur ...
// ... Berechnung der Kubikwurzel
assert ((fabs(y*y*y-x)<=1e-4) && (y>=0.0));
return y;
PRE: programmiersprachlich
}
Mit Zusicherungen (engl. assertion) werden im Entwicklungsstadium Programmierfehler
abgefangen.
Die Verletzung einer assertion bewirkt das kontrollierte Beenden des Programm. Die
Einhaltung von PRE und POST wird mit assert überprüft.
Es ist eine reine Testhilfe, damit sollen Programmierfehler abgefangen werden, es hilft nicht
gegen fehlerhafte Benutzereingaben.
9.8 Dokumentation und Testen
Bild 69.
Dokumentationsregeln
Funktionskopf
Funktionsnamen spiegeln die eigentliche
Aufgabe wider
o wo der Name nicht ausreicht, wird eine
umgangssprachliche Kurzbeschreibung
als Kommentar im Funktionskopf
142
Funktionskörper
eingefügt
PRE und POST spezifizieren Details und
Einschränkungen
assert / throw an "strategisch wichtigen"
Stellen plazieren
sonstige Kommentare erläutern
Vorgehensweise und Algorithmen, soweit
nicht offensichtlich
o nicht Informationen des
Programmtextes duplizieren
9.8.1.1 Testen
Testen ist praktisch immer unvollständig, deshalb muss man sich die Testfälle sorgfältig
überlegen.
Testen bezüglich des zulässigen Wertebereichs:
• „Black Box Test“
• erstes und letztes Element und eins in der Mitte
Korrektheit
• unzulässiges Element oberhalb und unterhalb
Robustheit
Testen bezüglich der inneren Struktur
• „White Box Test“
• jeden Programmpfad mindestens einmal durchlaufen
• Schleifen: 0, 1, n-maliger Durchlauf
• switch: undefinierte case Marke
9.9 Standardfunktionen/Bibliotheken
C++ zeichnet sich durch eine Vielzahl von verfügbaren Bibliotheksfunktionen aus (->
Klassen). Dies Funktionen gehören nicht zur Sprache, werden aber in allen C++
Umgebungen bereitgestellt.
Wir werden im Verlauf der Veranstaltung noch einige davon kennen lernen.
143
10 Modulare Gestaltung von Programmen
Eine Anwendung besteht i.A. aus mehreren Dateien, die jeweils einzeln übersetzt und
getestet werden. Am Ende der Programmentwicklung werden dann die einzelnen Teile erst
zu einer Anwendung zusammen geführt.
Jedes Programmierteam ist dann für die Entwicklung und den Test eines Moduls
verantwortlich. Ein Projektleiter bzw. ein Teamkoordinator sorgt für das Zusammenfügen der
einzelnen Moduln und organisiert einen Integrationstest.
Damit jede Datei einzeln übersetzt werden kann, müssen Konstanten, Funktionsprototypen
(Klasseninterfaces) etc. bekannt sein. Dies wird möglich, indem eine Datei andere Dateien
inkludiert (#include).
Folgender Aufbau hat sich bewährt:
• In Dateien mit Endung .h (Header Dateien) sollten Konstanten,
Schnittstellenbeschreibungen, Deklarationen, Funktionsprototypen und (wenn
unbedingt notwendig) globale Daten abgelegt werden. (Später werden wir sehen, auch
Klassendeklarationen)
• Implementierungsdateien (da steht der eigentliche Kode drin) haben die Endung
.cpp.
• Eine Datei mit der Funktion main erhält den Anwendungsnamen mit Endung .cpp.
Getrenntes Übersetzen von Programmen
Den Aufbau und die getrennt Übersetzung verdeutlichen wir am Beispiel einer
„Anwendung“, die Zufallszahlen erzeugt.
Ein Programm „zufall“ soll eine Folge von 10 Zufallszahlen auf den Bildschirm schreiben.
Verwendet werden soll der Algorithmus von Knuth.
Das Programm soll folgende modulare Struktur aufweisen:
144
Bild 70.
Modulare Struktur des Zufallsprogramms
// definition of
// random parameter
#define …
random.h
print.cpp
random.cpp
// definition of
// print function
printParameter()
…
printRandom()
…
// definition of
// random function
generateRandom()
main()
{
printParameter();
printRandom();
}
zufall.cpp
Neben den „logischen“ Abhängigkeiten, gibt es bei C-Programmen die Abhängigkeiten:
145
Bild 71.
Abhängigkeiten
QuellDateien
Zufall.cpp
random.h
random.cpp
print.cpp
Übersetzen
ObjektDateien
main.o
random.o
print.o
Binden
ProgrammDatei
zufall
146
Listing 126. Programm-Dateien: random.h
const long R=1000001;
const long a=100001;
const long m=1717;
const long c=3;
Listing 127. random.cpp
#include "random.h"
float generateRandom(void)
{
static long int r=R;
r = (a*r)%m;
return r/(float)m;
}
Listing 128. print.cpp
#include "random.h"
#include <iostream>
using namespace std;
void printParameter(void)
{
cout << "a=" << a << endl;
cout << "c=" << c << endl;
cout << "m=" << m << endl;
}
void printRandom(float i)
{
cout << i << endl;
}
Listing 129. zufall.cpp
extern void printParameter(void);
extern void printRandom(float);
extern float generateRandom(void);
int main()
{
printParameter();
for (int i=1; i<=10; i++)
printRandom(generateRandom());
return 0;
}
10.1.1 Dateiübergreifende Gültigkeit und Sichtbarkeit
Variablen in C++ gehören zu einer Speicherklasse. Eine Speicherklasse bestimmt die
Gültigkeit und die Sichtbarkeit und ev. den Speicherort. Die Speicherklasse wird bei der
Variablendeklaration angegeben.
147
Globale Variablen werden außerhalb aller Funktionen angegeben. Sie sind in allen Teilen des
Programms gültig, auch in anderen Dateien. In einer anderen Datei muss die Variable aber
als extern deklariert werden. Dadurch wird kein neues Objekt angelegt, es wird nur bekannt
gemacht (für den Compiler)
Beispiel
Listing 130. datei1.cpp
#include <iostream >
using namespace std;
int global;
void fkt();
int main()
{
global = 18;
cout << global << endl;
fkt();
return 0:
}
// Deklaration und Definition
Listing 131. datei2.cpp
#include <iostream>
using namespace std;
extern int global;
void fkt()
{
global = 42;
cout << global << endl;
}
// Deklaration, aber keine Definition
Beide Dateien können getrennt übersetzt werden und danach zusammen gebunden werden:
Soll die Gültigkeit auf eine Datei beschränkt werden, wird bei der Definition das
Schlüsselwort static vorangestellt (-> modulglobal).
Listing 132. datei3.cpp
#include <iostream>
using namespace std;
static int global;
// nicht mehr global!
void fkt();
int main()
{
global = 18;
cout << global << endl;
fkt();
return 0;
}
$
Jetzt können datei2.cpp und datei3.cpp zwar noch einzeln übersetzt werden. Beim
Binden wird aber ein Fehler angezeigt, da die Variable global nicht in datei2.cpp
bekannt ist.
148
Auf Dateiebene außerhalb aller Funktionen definierte Variable sind global und in anderen
Dateien benutzbar, wenn sie dort als extern deklariert sind.
Konstanten sind nur in der Definition sichtbar. Sollen sie anderen Dateien zugänglich
gemacht werden, müssen sie explizit als extern definiert sein.
Listing 133. Datei4.cpp extern const
//
extern const float pi = 3.14159;
…
Datei5.cpp Autovariablen
extern const float pi;
…
// Definition
// Deklaration (Initialisierung in Datei4.cpp
Alle Variablen die innerhalb eines Blockes definiert werden, heißen auto Variablen, da sie
beim Betreten des Blocks angelegt (nicht initialisiert) und beim Verlassen des Blocks
automatisch zerstört werden. Sie können, müssen aber nicht mit dem Schlüsselwort auto
gekennzeichnet werden.
10.1.2 Compilerdirektive und Makros
Am normalen Übersetzungsprozess sind folgende Programme beteiligt, die in der
angegebenen Reihenfolge ausgeführt werden:
1. Präprozessor
2. Compiler
3. Binder
Mit Compilerdirektiven kann der Präprozessor gesteuert werden. Sie werden durch das
Zeichen # am Zeilenanfang eingeleitet.
10.1.2.1 #include
Mit include können Dateien in eine Datei hineinkopiert werden. Dabei kann der Pfadname
absolut und relativ angegeben werden:
#include “Datei18.h“
// Datei im aktuellen Verzeichnis
#include “../Datei18.h“
// Datei im Vaterverzeichnis
#include “/home/as/Vorlesungen/ProgI/Programmstuktur/bsp18.h“ // absoluter Pfad
Sollen Dateien aus den include-Verzeichnissen der Programmierumgebung genommen
werden, sind sie in spitze Klammern einzuschließen:
#include <iostream >
10.1.2.2 #ifdef, #ifndef, #define
in allen Dateien, die zu einem Programm gehören darf es beliebig viele Deklarationen, aber
nur genau eine Definition geben.
Es kann zu Übersetzungsproblemen kommen, wenn eine Header-Dateien mehrfach inkludiert
wird und die Header-Datei eine Definition enthält.
Beispiel:
149
Listing 134. ifdefs a.h
#include <iostream>
using namespace std;
int global=1;
Listing 135. c.cpp
#include "a.h"
void fkt()
{
global = 42;
cout << global << endl;
}
b.cpp
#include "a.h"
#include "c.cpp"
int main()
{
global = 18;
cout << global << endl;
fkt();
return 0;
}
Der Versuch, das Programm zu übersetzen scheitert:
Listing 136. compileroutput
$ g++ -c c.cpp
$ g++ -c b.cpp
In file included from c.cpp:1,
from b.cpp:2:
a.h:2: redefinition of `int global'
a.h:2: `int global' previously defined here
$
150
Abhilfe schafft die Möglichkeit des bedingten Definierens mit #if defined (#ifdef)
und #if !defined (#ifndef).
Die Header-Datei wird erweitert um eine bedingte Definition
falls der Name globaleDefinition
nicht definiert ist,
dann definiere ihn und akzeptiere alles bis
#endif
Listing 137. ifdefs a1.h
#include <iostream>
#ifndef globaleDefinitionen
#define globaleDefinitionen
using namespace std;
int global=1;
#endif // globaleDefinitionen
// Deklaration und Definition
Listing 138. c1.cpp
#include „a1.h“
void fkt()
{
global = 42;
cout << global << endl;
}
b1.cpp
#include "a1.h"
#include "c1.cpp"
int main()
{
global = 18;
cout << global << endl;
fkt();
return 0;
}
Damit kann das Programm übersetzt und gebunden werden.
10.1.2.3 Makros
Durch #define können neben symbolische Konstanten allgemeine Textersetzungen
spezifiziert werden, die vom Präprozessor ausgeführt werden.
Beispiel
Listing 139. pi.cpp
Präprozessor
#include <iostream>
#define PI 3.14
using namespace std;
int main()
{
cout << PI << endl;
return 0;
}
#include <iostream>
using namespace std;
int main()
{
cout << 3.14 << endl;
return 0;
}
Makros können parametrisiert werden:
151
Listing 140. Makro mit Parametern
#define max(A,B) ((A)>(B)?(A):(B))
x = max(y, 2+x);
X = ((y)>(2+x)?(y):(2+x));
Dadurch lässt sich eine „Funktion“ realisieren, die für unterschiedliche Datentypen
verwendbar ist.
Achtung:
Die ist eine Fehlerquelle!
Listing 141. quad.cpp
#include <iostream>
#define QUAD(X) ((X)*(X))
using namespace std;
int main()
{
int z = 2;
cout << QUAD(++z) << endl;
return 0;
}
$ quad
16
$
es wird nicht z erhöht und dann
quadriert, sondern
++z wird zweimal ausgeführt!
Ein eher sinnvolles Beispiel der Verwendung von Makros ist es, Testsequenzen ein- und
auszublenden.
Zunächst der Schritt der Programmentwicklung, also mit Testausgaben (Beispiel Fakultät):
152
Listing 142. fak.cpp
#include <iostream.h>
#define TEST_EIN
#ifdef TEST_EIN
#define PRINT(WERT) cout << (WERT) << endl;
#else
#define PRINT(WERT) /* nichts */
#endif
int fak(int n)
{
if (n == 0 || n == 1)
{
PRINT(1);
return(1);
}
else
{
int erg = n*fak(n-1);
PRINT(erg);
return(erg);
}
}
Listing 143. main.cpp
int main()
{
int x;
cout << "Eingabe Integer x: ";
cin >> x;
cout << "fak(" << x << ") = " << fak(x) << endl;
return 0;
}
$ fak
Eingabe Integer x: 3
1
2
6
fak(3) = 6
Am Ende der Programmentwicklung und des erfolgreichen Testens wird einfach die
#define-Zeile auskommentiert; dadurch wird der leere String beim Ersetzen verwendet:
// #define TEST_EIN
$ fak
Eingabe Integer x: 3
fak(3) = 6
$
Somit sind Testausgaben (nach Neuübersetzung) unterdrückt.
153
11 Zeiger
Zeiger (engl. pointer) sind Variablen, die als Inhalt eine Adresse enthalten. Wie andere
Variablen auch, haben sie einen Namen und es gibt erlaubte Operationen.
Viele Klassen in C++ und sehr viele Programme in C sind ohne Zeiger nicht realisierbar.
Zeiger haben aber einige Tücken:
Sie sind die Ursache von vielen Programmfehlern,
sie können zu schlecht lesbaren und wartbaren Programmen führen.
Aber richtig verwendet ist damit ein effektiver Kode erzeugbar.
11.1 Zeiger und Adressen
Ein Zeiger muss wie jede Variable vor ihrer ersten Verwendung deklariert werden.
Bild 72.
Allgemeine Form eines Zeigers
Typbezeichnung * Name
kennzeichnet Name als Zeiger auf Speicher,
der Objekte vom Typ Typebezeichnung
enthält
Mit dem Adressoperator & kann man die Adresse einer Speicherzelle ermitteln. Der
Adressoperator kann auf Variablen und Arrayelemente angewendet werden, nicht auf
Ausdrücke (sie haben keinen Speicherplatz).
Da ein Zeiger die Adresse eines Objektes enthält, kann auf das Objekt auf zwei Arten
zugegriffen werden:
• direkt über den Namen
• indirekt über den Zeiger.
Um indirekt zuzugreifen, wird der Operator * zum Dereferenzieren verwendet:
Bild 73.
Zugriff auf Objekte, Beispiele:
C++ Fragment
int *p;
int *p;
int x;
p = &x;
Erklärung
Veranschaulichung
p ist eine Zeigervariable auf ein
p
?
int-Objekt.
Da keine Initialisierung
vorgenommen ist, ist der Inhalt
unbestimmt.
p wird die Adresse der Variablen
x zugewiesen, man sagt p zeigt
p 123
auf x.
x
?
?
123
154
int
int
x =
p =
y =
x,y;
*p;
2;
&x;
*p;
p zeigt auf x.
Durch y=*p wird derselbe Effekt
erzielt, wie y = x.
p 123
x
2
123
y
2
124
x
2
123
p dereferenzieren: Inhalt der Speicherstelle, auf die p
zeigt.
int x;
int *p1,
*p2;
x = 2;
p1 = &x;
p2 = p1;
Der Variablen p2 wird der Wert
der Variablen p1 zugewiesen;
dadurch zeigt p2 auf dasselbe
Objekt wie p1.
p1
p2
123
123
int* p, x;
Alle diese Deklarationen sind
int * ip, x; äquivalent, da der * sich nur auf
int *ip, x; den ihm nachfolgenden Namen
bezieht.
Wird eine Zeigervariable deklariert, so ist sie nicht automatisch initialisiert; sie zeigt
irgendwo hin. Will man sie auf einen definierten Wert setzen, aber noch nicht auf ein
bestimmtes Objekt, so kann der spezielle Zeigerwert NULL verwendet werden.
NULL wird intern als OL dargestellt; man sollte der besseren Lesbarkeit wegen aber den
Namen NULL verwenden. Die Definition von Null ist im Header <cstddef> enthalten.
In Ausdrücken, wie y = *p + 1; haben die Operatoren * und & höheren Rang als
arithmetische Operatoren. D.h. zuerst wird oben das Objekt ermittelt (dereferenziert) dann
wird der Wert um 1 erhöht. Achtung: y = *(p+1) möglich, hat aber andere Bedeutung (>später).
Verweise sind auch links in Zuweisungen möglich.
155
Bild 74.
Beispiele für pointer:
C++ Fragment
Erklärung
Veranschaulichung
int *p=NULL; p ist eine Zeigervariable auf ein
int-Objekt.
p
OL
Da keine Initialisierung
vorgenommen ist, ist der Inhalt
unbestimmt.
int *p=NULL; Zuerst wird dereferenziert, d.h. der
int x=2;
Wert von der Speicherstelle, auf die
p = &x;
p zeigt ermittelt (x==2), dann der
p 123
x = *p + 1; Wert erhöht (3) und der Variablen
x zugewiesen.
x
int *p=NULL;
int x=2;
p = &x;
*p = -5;
int *p=NULL;
int x=2;
p = &x;
*p += 1;
int *p=NULL;
int x=2;
p = &x;
(*p)++;
int *p=NULL;
int x=2,
y=5;
p = &x;
*p++=7;
Zuerst die Speicherstelle, auf die p
zeigt ermittelt (x), dann der Wert
dieser Speicherstelle auf -5 gesetzt.
Inkrementiert x.
p 123
x
Adressiert das Objekt, das im
Speicher auf x folgt und setzt es auf
3. Achtung: dies führt zu
unleserlichen Programmen und ist
-5
123
p 123
x
Inkrementiert x.
Die Klammern sind wegen Rang
erforderlich.
123
3
2 3
123
p 123
x
2 3
123
2
7
123
124
p 123
x
y
extrem fehleranfällig!
Z.B. kann die Speicherfolge
absteigend nicht aufsteigend sein,
wenn die Variablen lokal auf dem
Stack angelegt werden.
156
Zeiger sind auch als Konstante deklarierbar – sie zeigen dann stets auf dasselbe Objekt. Auch
können Zeiger auf Konstanten deklariert werden.
Bild 75.
Zeiger auf Konstanten
C++ Fragment
const int x=42;
const int c=18;
const int *p=&c; //blau
…
p=&x;
//grün
int x=42;
const int c=18;
int *const p=&x;
*p = -1;
int x=42;
const int c=18;
const int *const p=&c;
Erklärung
Veranschaulichung
Solche Deklarationen
sind von rechts nach
p 123
links zu lesen:
p ist Zeiger auf eine
18 123
c
Int-Konstante.
42 124
x
Der Zeiger kann danach
auf anderes Int-Objekt
zeigen.
Solche Deklarationen
sind von rechts nach
p 123
links zu lesen:
p ist konstanter Zeiger
18 123
c
auf ein Int-Objekt.
-1 124
Der Zeiger kann danach
x
nicht verändert werden,
aber das Objekt auf das
er zeigt.
Solche Deklarationen
sind von rechts nach
p 123
links zu lesen:
18 123
p ist konstanter Zeiger
c
auf eine Int-Konstante.
42 124
x
Der Zeiger kann danach
nicht verändert werden,
das Objekt auf das er
zeigt ebenfalls nicht.
Welche Ausgabe erzeugt das nachfolgende Programm?
157
Listing 144. wasWirdGedruckt.cpp
#include <iostream>
using namespace std;
int main()
{
double zahl = 1;
double zahl2 = 2;
const double konstante = 11;
const double konstante2 = 12;
double *pd = &zahl;
cout << zahl - *pd << endl;
cout << *pd << endl;
cout << pd << endl;
const double *pdc = &konstante;
cout << *pdc << endl;
cout << pdc << endl;
double *const cpd = &zahl2;
cout << *cpd << endl;
cout << cpd << endl;
const double *const cpdc = &konstante2;
cout << *cpdc << endl;
cout << cpdc << endl;
pdc = &konstante2;
cout << *pd + *pdc + *cpd + *cpdc << endl;
return 0;
}
0
1
0x0012FF78
11
0x0012FF68
2
0x0012FF70
12
0x0012FF60
1+12+2+12=27
11.2 Zeiger und die Argumente von Funktionen
Die Parameterübergabemechanismen haben wir bereits behandelt. Soll die Änderung eines
Parameters einer Funktion beim Aufrufer verfügbar sein, muss der Übergabemechanismus
„Call by Referenz“ verwendet werden.
Mittels Wertübergabe (Call by Value) kann dieser Effekt erzielt werden, wenn ein Zeiger
übergeben wird. Dies bleibt Wertübergabe, da der Zeiger selbst nicht verändert wird,
lediglich das Objekt auf das er zeigt wird geändert.
Soll mit call by value eine Änderung durch das aufgerufene Unterprogramm erzielt werden,
so müssen Zeiger übergeben werden und das Unterprogramm muss dereferenzieren.
Beispiel (increment):
158
Klammer wg.
Rang $ inc3
erforderlich!
Listing 145. inc.cpp
#include <iostream >
using namespace std;
void inc(int *p)
{
(*p)++;
cout <<" in inc:*p = " << *p << endl;
}
int main()
{
int x=1;
int *pi = &x;
cout << " vor inc: x = " << x << endl;
inc(pi);
cout << "nach inc: x = " << x << endl;
return 0 ;
}
Bild 76.
vor inc: x = 1
in inc:*p = 2
nach inc: x = 2
$
Main vor Aufruf inc
pi 124
Bild 77.
124
1
x
Main nach Aufruf inc
pi 124
Bild 78.
124
2
x
inc nach (*p)++
pi 124
x
2
124
Dass sich aber der Zeiger selbst nicht geändert hat, also immer noch Wertübergabe und nicht
Referenzübergabe durchgeführt wurde, verdeutlicht die folgende Erweiterung am Programm:
159
Listing 146. inc.cpp
$ inc
#include <iostream>
using namespace std;
void inc4(int *p)
{
(*p)++;
cout <<" in inc: *p = " << *p << endl;
cout <<" in inc: p = " << p << endl;
}
int main()
{
int x=1;
int *pi = &x;
cout << " vor inc4: x = " << x << endl;
cout << " vor inc4: pi = " << pi << endl;
inc4(pi);
cout << "nach inc4: x = " << x << endl;
cout << "nach inc4: pi = " << pi << endl;
return 0;
}
vor inc4: x = 1
vor inc4: pi = 0x22feb0
in inc4: *p = 2
in inc4: p = 0x22feb0
nach inc4: x = 2
nach inc4: pi = 0x22feb0
In C ist ausschließlich Wertübergabe möglich. Deshalb sind Zeiger in C der einzige Weg, den
fehlenden Refernzübergabemechanismus zu überwinden (Arrays werden eigentlich wie
Zeiger behandelt. Den Referenzübergabemechanismus von C++ haben wir bereits
kennengelernt).
Das Ergebnis einer Funktion kann selbst ein Zeiger sein.
Beispiel (max)
160
Listing 147. max.cpp
#include <iostream>
using namespace std;
int * max(int *a, int *b)
{
if (*a > *b)
return a;
else
return b;
}
int main()
{
int x,y;
int *pi;
cout << "Zwei Integer eingeben: ";
cin >> x >> y;
pi = max(&x, &y);
cout << *pi << endl;
return 0 ;
}
161
Oft wird beim Programmieren mit Zeigern der Fehler gemacht, dass eine Funktion einen
Wert über den Zeiger zurück geben will, der nach dem Funktionsaufruf nicht mehr lebt, da
der Zeiger auf ein auto-Objekt gezeigt hat.
Beispiel (fehlerhafte Verwendung von Zeigern)
Listing 148. zeigerFehler01.cpp
#include <iostream>
using namepsace std;
int *fkt()
{
int x=1;
return &x;
// x existiert nach return nicht mehr!
}
int main()
{
int *pi ;
pi = fkt();
cout << "fkt(): " << *pi << endl;
return 0;
}
warning C4172: returning address of local variable or temporary
11.3 Zeiger und Arrays
Von C her hat C++ eine Erbschaft übernommen; Zeiger und Arrays sind stark verwandt:
• Jede Operation mit Array-Indizes kann auch mit Zeigern formuliert werden!
• Die mit Zeiger formulierte Version ist i.a. effizienter, jedoch manchmal schwerer zu
„lesen“.
Durch
int a[10]
wird ein Array mit Namen a definiert, der 10 Integer-Komponenten a[0], a[1], bis a[9]
hat. Dann bezeichnet a[i] die Komponente, die i Positionen von der ersten Komponente
entfernt ist. Wenn pa ein Zeiger auf ein int-Objekt ist, also
int *pa
dann kann durch
pa = &a[0]
pa die Adresse der Komponente 0 zugewiesen werden.
162
Bild 79.
Zeiger auf Vektor
int a[10];
int *pa = &a[0];
Speicher
pa
a[0]
a[1]
…
a[i]
…
a[9]
Zeiger auf Array
int a[10];
int *pa = &a[i];
Speicher
pa
a[0]
a[1]
…
a[i]
…
a[9]
Der C/C++-Compiler verwandelt einen Verweis auf ein Array stets in einen Zeiger-Wert auf
den Anfang des Vektors.
D.h. der Name eines Vektors ist die Startadresse im Speicher. Daher gilt immer:
*(pa+i) == a[i]
163
Bild 80.
Zeigerausdrücke Konsequenzen:
Ausdruck
pa = &a[0]
ist äquivalent zu
pa = a
&a[i]
a[i]
pa[i]
a+i
*(a+i)
*(pa+i)
Erklärung
Der Name des Arrays ist Synonym für
die Adresse der Komponente 0.
Adresse der Komponente i
Inhalt der Komponente i
pa ist Synonym für Vektoranfang
Allgemein kann man sagen:
Statt Arrayname und Indexausdruck kann man immer einen Zeiger und den Abstand
zum Anfang angeben und umgekehrt.
Diese Konvention ist unabhängig vom Typ des Arrays, da die Addition von 1 zu einem
Zeiger definiert ist als das Inkrementieren in Abhängigkeit des Speicherplatzbedarfs des
Objektes, auf den der Zeiger zeigt.
Beispiel (was wird gedruckt?)
Listing 149. wasWirdGedruckt02.cpp
#include <iostream>
using namespace std;
main()
{
int a[] = {0,1,2,3,4};
int *p;
cout << "A: ";
for (int i = 0; i <= 4; i++)
cout << a[i] << " ";
cout << endl;
cout << "B: ";
for (p = &a[0]; p <= &a[4]; p++)
cout << *p << " ";
cout << endl;
cout << "C: ";
int j;
for (j = 0, p = &a[0]; j <= 4; j++)
cout << p[j] << " ";
cout << endl;
}
A: 0 1 2 3 4
B: 0 1 2 3 4
C: 0 1 2 3 4
11.3.1 Unterschied Arrayname und Zeiger
Ein Zeiger ist eine Variable, also sind die Anweisungen
164
Bild 81.
Arrayname und Zeiger
int a[10];
int *pa;
pa = a;
pa++;
sinnvoll und möglich. Ein Vektorname ist vom Wesen her eine Konstante und kann daher
kein L-Wert sein. Somit sind
Bild 82.
Arrayname und Zeiger
int a[10];
int *pa;
a = pa;
a++;
pa = &a;
nicht möglich.
11.3.2 Arrays als Parameter
Wir haben gesehen, dass es unterschiedliche Parameterübergabemechanismen gibt. Wird ein
Array als Parameter an eine Funktion übergeben, wird in Wirklichkeit die Adresse der
Komponente 0 übergeben, da ja der Compiler den Arraynamen durch die Startadresse ersetzt.
Konsequenz:
1. Obwohl keine Referenzübergabe (durch &) beim formalen Parameter angegeben ist,
ist der Effekt einer Änderung in der Funktion beim Aufrufer ersichtlich.
2. Die Größe eines Arrays kann innerhalb der Funktion nicht mit sizeof ermittelt
werden, da innerhalb der Funktion nur ein Zeiger verwendet wird.
Beispiel (Vektor als Parameter):
Listing 150. vektorToUpper.cpp
#include <iostream>
#include <ctype.h>
using namespace std;
void toUpper(char str[], int start, int end)
{
for (int i=start; i<=end; i++)
str[i] =toupper(str[i]);
}
int main()
{
char s[]="Jennifer Lopez";
toUpper(s,0,8);
cout << s << endl;
return 0;
}
$ vektorToUpper
JENNIFER Lopez
$
Das Ändern des Strings innerhalb der Funktion ist beim Aufrufer nach dem Aufruf
ersichtlich, da ja eine Adresse übergeben wird. Aber auch hier ist der Übergabemechanismus
immer noch „Call by Value“; der Zeiger selbst wird nicht verändert.
Dies kann man sich zunutze machen, indem man den Zeiger in der Funktion ändert, um eine
Variable in der Funktion zu sparen:
Beispiel (strlen)
165
Listing 151. strlen.cpp
#include <iostream>
als formale Parameter
using namespace std
sind char s[] und
int strlen(char *s)
char *s
{
gleichbedeutend!
int n;
for (n=0; *s != '\0'; s++)
n++;
return n;
’\0’ schließt C}
int main()
Zeichenketten ab!
{
char str[80];
cout << "Wort: ";
cin >> str;
cout << str << " hat Länge " << strlen(str) << endl;
return 0;
}
11.3.3 Hörsaalübung:
Zu realisieren ist eine Funktion atoi, die die als formalen Parameter übergebene
Zeichenkette (String mit Ziffern) in den korrespondierenden Integer konvertiert.
Gesucht sind zwei Lösungen
eine Lösung mit Arrays,
eine Lösung mit Zeigern.
Beispiel:
cout << atoi(“12345“) ; gibt den Wert 12345 aus.
166
Listing 152. Hörsaalübung Lösung
#include <iostream.h>
int atoi1(char s[])
{
int n=0;
for (int i=0; s[i] >= '0' && s[i] <='9'; i++)
n = 10*n + s[i] -'0';
return n;
}
int atoi2(char *s)
{
int n=0;
for (; *s >= '0' && *s <='9'; s++)
n = 10*n + *s -'0';
return n;
}
int atoi3(char *s)
{
int n=0;
for (int i=0; *(s+i) >= '0' && *(s+i) <='9'; i++)
n = 10*n + *(s+i) -'0';
return n;
}
int main()
{
char str[80];
cout << "Ziffernfolge: ";
cin >> str;
cout << "atoi(" << str << "): " << atoi1(str) << endl;
cout << "atoi(" << str << "): " << atoi2(str) << endl;
cout << "atoi(" << str << "): " << atoi3(str) << endl;
….
11.4 Zeigerarithmetik
Im letzten Beispiel haben wird gesehen, dass man einen Zeiger inkrementieren kann. In C++
ist das „Rechnen“ mit Zeigern möglich.
Vorsicht
:
Programme, die Zeigerarithmetik verwenden, können sehr effizient sein, aber die
Gefahr von Fehlern und nicht mehr wartbarem Kode ist extrem groß!
Erlaubte Operationen mit Zeigern sind:
1. Addition oder Subtraktion von Zeiger-Wert und ganzer Zahl
Subtraktion von 2 Zeigern
2. Vergleich von Zeigern
D.h. zwei Zeiger können nicht:
addiert,
multipliziert,
dividiert oder
geshiftet
167
werden.
Die Addition bzw. Subtraktion von Zeiger mit Konstante erfolgt immer in Abhängigkeit des
Typs, auf den der Zeiger zeigt.
Beispiel (Zeigerdifferenzen)
Listing 153. Zeigerdifferenzen
#include <iostream>
using namespace std;
Zeigerarithmetik
int main()
{
double d[10];
double *dp1 = d;
double *dp2 = dp1+1;
cout << "Elemente zwischen dp1 und dp2: " << dp2-dp1 << endl;
//1
cout << "Bytes zwischen dp1 und dp2: " << long(dp2) - long(dp1) << endl; //8
return 0;
}
Umwandlung Adresse in long
Kein Zeigerarithmetik, sondern long
Arithmetik:
daher Differenz ist Byteanzahl zwischen dp1
und dp2
168
Beispiel (strcpy)
Listing 154. strcpy.cpp
#include <iostream >
using namespace std;
void strcpy(char *s, char *d)
{
while ( (*d = *s) != '\0')
{
s = s+1;
d++;
}
}
void strcpy(char *s, char *d)
{
while ( (*d++ = *s++) != '\0')
;
}
// copy s to d
Zeigerarithmetik
void strcpy(char *s, char *d)
{
while ((*d++ = *s++));
}
korrekt, aber nicht gut lesbar!
11.5 Arrays von Zeigern
Zeiger sind normale Datentypen. Deshalb sind auch Arrays von Zeigern möglich.
C++ Fragment
Erklärung
Veranschaulichung
int *a[10];
a ist ein Vektor von
a
10 Zeigern auf jeweils
ein Int-Objekt.
0
?
?
1
?
…
?
9
?
?
?
?
Die Verwendung von solchen Zeigerarrays werden wir im nächsten Abschnitt sehen, wenn
wir mit dynamisch erzeugten Speicherbereichen arbeiten.
169
12 Ein- und Ausgabe
Die elementare Ausgabe von Daten auf externe Medien, wie Dateien und das Einlesen davon
wird demonstriert.
Komplexe E/A-Operationen werden erst diskutiert, nachdem das Klassenkonzept erarbeitet
ist.
12.1 Standardein- und Ausgabe
Die Ein- und Ausgabe unter Verwendung von Ein/Ausgabekanälen haben wird bereits
gesehen.
• Folgende Ein- und Ausgabeströme sind vordefiniert:
• cin – Standardeingabe (i.A. die Tastatur)
• cout – Standardausgabe (i.A. der Bildschirm)
• cerr – Standardfehlerausgabe (i.A. der Bildschirm)
12.2 Eingabe
Durch die Funktion get() kann man einzelne Zeichen des Eingabestroms lesen, mit
put() ein Zeichen auf den Ausgabestrom schreiben:
Bild 83.
Eingabe
char c;
while (cin.get(c))
cout.put(c);
Zeichenweise Kopieren
char buf[10];
cin.get(buf,6);
char buf[10];
cin.get(buf,6,’\n’);
Lesen von maximal 6 Zeichen und Ablage im Array
buf.
Übernahme von maximal 6 Zeichen, wobei maximal
bis zu einem Terminatorzeichen ‚\n’ gelesen wird.
Das Terminatorzeichen verbleibt im Eingabestrom
Get holt nächstes Zeichen und erzeugt int-Wert
gemäß ASCII Tabelle.
int i;
while ( (i=cin.get()) != EOF
cout.put(char(i));
Eine ganze Zeile kann mit der Funktion getline() gelesen werden.
Eine Vorschau (also ohne ein Zeichen zu konsumieren) auf den Eingabestrom ist durch
peek() möglich.
Ein bereits konsumiertes Zeichen c kann durch putback(c) zurückgelegt werden;
also ist
„c=cin.get(); cin.putback(c));“
gleichbedeutend mit
„c=peek();“.
Durch Ein/Ausgabeumlenkung auf Ebene des Betriebssystems kann man so genannte
Filterprogramme schreiben:
Ein Filter liest von Standardeingabe und schreibt das Ergebnis auf die Standardausgabe ohne
Benutzerinteraktion.
170
Listing 155. simpelCat.cpp Beispiel cat von Unix, type von DOS:
#include <iostream >
using namespace std;
int main(int argc, char* argv[])
{
char c;
while (cin.get(c))
cout.put(c);
cout << endl;
return 0 ;
}
171
12.2.1 Umleitung der Ein/Ausgabe
Auf Betriebssystemebene kann man in Unix (auch unter DOS) die Eingabe umlenken, indem
man das Zeichen „<“ verwendet. Damit lässt sich dann das o.a. Programm verwenden, um
eine beliebige Datei zu lesen:
Listing 156. cat Umleitung
$ simpleCat < simpleCat.cpp
#include <iostream>
using namespace std;
int main(int argc, char* argv[]) {
char c;
while (cin.get(c))
cout.put(c);
cout << endl;
return 0 ;
}
Die Umlenkung der Ausgabe ist auch möglich, damit ist das Programm auch zum Kopieren
von Dateien zu gebrauchen:
Listing 157. cat Umleitung
$ simpleCat < simpleCat.cpp > kopieVonSimpleCat.cpp
$ simpleCat < kopieVonSinpmleCat.cpp
#include <iostream>
using namespace std;
int main(int argc, char* argv[])
{
char c;
while (cin.get(c))
cout.put(c);
cout << endl;
}
$
12.3 Ein- und Ausgabe mit Dateien
Das Lesen und Schreiben von Dateien über die Ein-/Ausgabeumlenkung ist eigentlich nur für
die Systemprogrammierung gedacht (neue BS-Kommandos, …).
Das Schreiben und Lesen von Dateien in C funktioniert wie folgt:
Schreiben:
Es wird ein Filehandle (Pointer auf eine Streamvariable) unter einem gegebenen Dateinamen
angelegt. Es wird zum Schreiben mit unveränderten Binärdaten (w+b) geöffnet. In die Datei
können nun Daten nacheinander geschrieben werden mit fwrite ( meine Variable, Größe der
Variable in Byte, Anzahl der Daten, die Datei mit Ihrem Filehandle)
Anschließend wird die Datei geschlossen.
172
Listing 158. Schreiben eines Datensatzes in eine Datei
….
cout << "unter welchem namen speichern ? " << endl;
cin >> speichername;
// oder fester Dateiname
FILE *stream;
/* Open for write */
if( (stream = fopen( speichername, "w+b" )) == NULL )
{
cout << "The file was not opened" << endl;
//Fehlerbehandlung
return(-1);
}
else
{
cout << "The file was opened" << endl;
// alles ok
fwrite ( datensatz, sizeof(datensatz), 1, stream); // jetzt einen Datensatz
/* Close stream */
if( fclose( stream ) )
cout << "The file was not closed" << endl;
else
cout << "file was closed " << endl;
}
}
Das Lesen erfolgt entsprechend, nur wird die Datei zum Lesen geöffnet (rb):
Listing 159. Lesen eines Datensatzes aus einer Datei
……
if( (stream = fopen( speichername, "rb" )) == NULL )
{
cout << "The file was not opened" << endl;
return(-1);
}
else
{
cout << "The file was opened" << endl;
fread ( datensatz, sizeof(datensatz), 1, stream);
/* Close stream */
if( fclose( stream ) )
cout << "The file was not closed" << endl;
else
cout << "file was closed " << endl;
// falls Fehler
// alles ok
}
Das Schreiben und Lesen von Dateien in C++ funktioniert wie folgt:
Dazu können die Ein- und Ausgabeoperatoren >> und << zusammen mit dem Header
<fstream> verwendet werden.
file stream
173
Die zu verwendenden Funktionen put(), get(), open(), close()benutzen dabei
Systemaufrufe, die das zugrunde liegende Betriebssystem bereitstellt.
Der Zugriff auf eine Datei geschieht etwa wie folgt:
1. Definition eines Datei-Objektes, über das die Datei angesprochen wird:
Datentyp des Dateiobjektes ist ifstream zum Lesen, ofstream zu Schreiben.
2. Die Verbindung zu einer Datei wird durch die Funktion open() hergestellt; dabei
übergibt man den Namen der Datei als Parameter.
3. Nachdem kein Zugriff mehr auf die Datei erforderlich ist, kann die Verbindung durch
close() gelöst werden.
Das Schreiben auf eine Datei erfolgt i.A. gepuffert, d.h. ein spezieller Datenbereich (Puffer)
wird reserviert. Erst wenn der Puffer voll ist, wird der Pufferinhalt physisch auf die Platte
geschrieben. Ein close() sorgt dafür, dass stets geschrieben wird, auch wenn der Puffer
noch nicht voll ist. Das Lesen erfolgt i.A. auch gepuffert.
Beispiel (simpleCp):
174
Listing 160. simpleCp.cpp
#include <iostream>
// wg. cerr
#include <cstdlib>
// wg. exit
#include <fstream.h>
// wg. Dateioperationen
using namespace std;
int main(int argc, char *argv[])
{
// copy argv[1] argv[2]
if (argc != 3)
{
// falsche Verwendung
cerr << "Verwendung: " << argv[0] << " Quelle Ziel" << endl;
exit (-1);
}
ifstream quelle;
// Quelldatei (Lesen)
quelle.open(argv[1], ios::in);
// zum Lesen öffnen
if (!quelle)
{
// Datei kann nicht geoeffnet werden
cerr << argv[1] << " kann nicht geöffnet werden!\n";
exit(-2);
}
ofstream ziel;
// Zieldatei (Schreiben)
ziel.open(argv[2], ios::out);
// zum Schreiben öffnen
if (!ziel)
{
// Datei kann nicht geoeffnet werden
cerr << argv[2] << " kann nicht geöffnet werden!\n";
exit(-3);
}
char c;
while (quelle.get(c))
// zeichenweise kopieren
ziel.put(c);
quelle.close();
// Schliessen der Quelldatei
ziel.close();
// Schliessen der Zieldatei
return 0;
}
$
$ simpleCp
Verwendung: simpleCp.exe Quelle Ziel
$ simpleCp /etc/passwda xx
/etc/passwda kann nicht geöffnet werden!
$ simpleCp /etc/passwd xx
175
Sollen nicht nur Textdateien, sondern auch Binärdateien bearbeitet werden, so ist beim
Öffnen der Schalter ios::binary anzugeben.
Dabei wird verhindert, dass die (in DOS) sonst automatisch Umwandlung von
Zeilenendekennung ’\n’ in CR/LF erfolgt:
Listing 161. Schalter für binärdateien
ziel.open(argv[2], ios::binary|ios::out);
ODER Verknüpfung von Schaltern
Bild 84.
Weitere io Schalter sind:
ios::app
ios::ate
trunc
append: beim Schreiben am Ende der Datei anhängen
nach dem Öffnen an das Dateiende springen
Voheriger Inhalt der Datei Löschen
Weiterhin sind gewisse Statusinformationen mittels Funktionen abfragbar, etwa ob das Ende
der Datei schon erreicht ist durch eof().
Beispiel (lineCount)
Listing 162. lineCount
char c;
int zaehler = 0;
while (!datei.eof())
{
datei.get(c);
if (c=='\n') zaehler++;
}
// Zeichenweise lesen
Neben dem Arbeiten mit Zeichen, können auch Werte (im ASCII Format) von einer Datei
gelesen bzw. auf sie geschrieben werden (im ASCII Format) werden.
Listing 163. Lesen von datei
double x;
ifstream dateiIn;
ofstream dateiOut;
…
dateiIn >> x; // Lesen von Datei
…
dateiOut << x+1;
176
12.4 Binärdateien
Bisher sind die Lese- und Schreioperationen immer auf ASCII Dateien ausgeübt worden.
Dabei wird ein double immer in eine Folge von Ziffern (und Dezimalpunkt) umgewandelt.
Wird z.B. der Wert -1.234567890 in eine ASCI Datei geschrieben, so werden insgesamt 12
Byte in die Datei geschrieben.
Wird derselbe Wert in eine Binärdatei geschrieben, so werden so viele Bytes belegt, wie ein
double ausmacht: in vielen Rechnern 8 Byte.
Während die ASCII Datei mit einem normalen Editor lesbar ist, kann eine Binärdatei nur
mit Hex-Editoren gelesen werden oder mit speziellen Programmen, die „wissen“, wie das
Binärbild zu lesen ist.
Binäre Ein-/ und Ausgabe ist also unformatiert: beim Lesen und Schreiben wird keine
Konvertierung, etwa von ‚\n’ vorgenommen.
Zum Schreiben und Lesen existieren die Funktionen
Listing 164. Binärdateien
write(adresse,bytes) und
read(adresse, bytes).
Die Wirkung ist:
Speicher
Datei
write(p, n)
Speicher
Datei
read(p, n)
Soll also ein double-Wert in eine Datei geschrieben und anschließend gelesen werden, ist
folgendes Programm erforderlich:
177
Listing 165. doubelBinary.cpp
…
double d;
ofstream ausgabe;
ausgabe.open(dateiname.c_str(), ios::out|ios::binary);
if (!ausgabe)
{
// Datei kann nicht geoeffnet werden
cerr << dateiname << " kann nicht geöffnet werden!\n";
exit(-2);
}
// Anzahl Bytes von double ab Adresse von d in Datei schreiben
ausgabe.write((char *)&d, sizeof(d));
ausgabe.close();
ifstream eingabe;
eingabe.open(dateiname.c_str(), ios::in|ios::binary);
if (!eingabe)
{
// Datei kann nicht geoeffnet werden
cerr << dateiname << " kann nicht geöffnet werden!\n";
exit(-2);
}
// Anzahl Bytes von double aus Datei lesen und ab Adresse d speichern
eingabe.read((char *)&d, sizeof(d));
eingabe.close();
…
178
13 Dynamische Datenobjekte
Die bisher behandelten Datentypen waren statisch – der Compiler konnte den
Speicherplatzbedarf ermitteln.
In vielen Anwendungen ist der erforderliche Platzbedarf aber erst zur Laufzeit des
Programms bekannt; z.B. wenn der Platz von der Anzahl der Benutzereingaben abhängt.
Eine Idee ist es, ein sehr großes Array von Datenobjekten zu deklarieren und dort die Werte
zu speichern. Besser wäre es, man könnte Speicher zur Laufzeit, bei Bedarf anfordern
(allokieren).
Ein Programm wird vom Betriebssystem (je nach Betriebssystem unterschiedlich) im
Speicher etwa wie folgt abgelegt:
Bild 85.
Speicherabbild
Speicher
Stack (Stapel)
Auto Variable
aktuelle Parameter
FFFF…F
Überlauf möglich
über Zeiger kann auf
den Bereich zugegriffen
werden
(Fehler)
freier Bereich
Heap (Halde)
dynamisch erzeugte
Objekte
zur Compilezeit
berechnet
permanente Daten
globale Variable
static Variable
Programmkode
0000…
Der Zugriff auf Daten im Stack und den Bereich permanente Daten erfolgt über Namen (der
Variablen), der Zugriff auf Elemente des Heap erfolgt über Zeiger.
13.1 Erzeugung von dynamische Datenobjekten
Um ein neues Element im Heap ablegen zu könne, verwendet man den Operator new.
Der erforderliche Speicherplatz wird durch new in Abhängigkeit des Typs automatisch
ermittelt.
179
Bild 86.
Beispiele für new operator
C++ Fragment
int *p=NULL;
p = new int;
int *p=NULL;
p = new int;
*p = 18;
Erklärung
Ein neues Element wird auf
dem Heap angelegt, es hat
keinen Namen. Der Zugriff
erfolgt über den Zeiger p.
Der Wert ist undefiniert.
Die Größe ist
sizeof(int).
Veranschaulichung
p
?
Zuweisung eines Wertes.
int *pa=NULL; Anlegen von 3 Int-Objekten.
pa = new
Zugriff wie auf Arrays über
int[3];
Zeiger und Index.
pa[1] = 42;
p
18
pa
0
?
1
42
2
?
180
Ein Programm, mit dem ein Vektor von Zeigern definiert ist, bei dem jedes Vektorelement
auf ein double-Objekt zeigt ist nachfolgend angegeben:
Listing 166. vektorVonZeigern.cpp
#include <iostream>
using namespace std;
int main()
{
double *pd[10];
for (int i=0; i<10; i++)
{
cout << "Double: ";
pd[i] = new double;
cin >> *pd[i];
}
// Vektor von Zeigern auf double;
// Komponente i zeigt auf neues Element im Heap
// Wertzuweisung an neues Element über Zeiger
for (int i=9; i>=0; i--)
cout << *pd[i] << " ";
cout << endl;
return 0;
}
Bild 87.
Vektor von Zeigern
pd
0
?
1
?
…
?
9
?
?
?
Heap
?
?
181
Ein Programm mit einem Zeiger auf ein Array von double-Werten ist:
Listing 167. zeigerAufVektor.cpp
#include <iostream>
using namespace std;
int main()
{
double *pv = new double[10];
for (int i=0; i<10; i++)
{
cout << "Double: ";
cin >> pv[i] ;
}
// Zeiger auf Vektor von double-Werten;
// Wertzuweisung an Vektor-Element
for (int i=9; i>=0; i--)
cout << pv[i] << " ";
cout << endl;
delete [] pv;
return 0;
}
Bild 88.
Zeiger auf array von double
pv
0
?
…
?
2
?
182
Beide „Spielarten“ können auch gemeinsam verwendet werden, um einen Zeiger auf einen
Vektor von Zeigern auf double-Werte zu haben:
Bild 89.
Zeiger auf Zeiger auf double
C++ Fragment
Erklärung
double *p = new double;
*p= -1.2;
Anlegen von double
und Zeiger darauf.
double *pa;
pa = new double[3];
pa[1] = 42.1;
Anlegen von 3
double-Objekten.
double **pp= new double
*[2];
=(double *) *pp,
also Zeiger auf
Zeiger;
pp[1] zeigt auf das
Objekt, auf das p
zeigt.
pp[0] zeigt auf
Komponente 1 des
Arrays im Heap.
pp[1] = p;
pp[0] = &pa[1]
Veranschaulichung
-1.2
p
pa
0
?
1
42
2
?
pp
0
1
13.2 Freigabe von dynamisch erzeugten Datenobjekten
Der Operator delete kann verwendet werden, um ein vorher mit new erzeugtes Objekt
wieder freizugeben, damit er erneut allokiert werden kann.
183
Bild 90.
Delete operator
C++ Fragment
int *p;
p = new int;
*p = 18;
Erklärung
int *p;
p = new int;
*p = 18;
delete p;
Freigabe, auf das
Element kann man
nicht mehr zugreifen.
Veranschaulichung
p
18
p
?
Ein Array von Datenobjekten wird mit delete [] pa; freigegeben.
Einige Besonderheiten sind bzgl. delete zu beachten:
• Dynamisch allokierter Speicher, auf den kein Zeiger mehr zeigt, ist verloren!
• C++ detektiert nicht, ob ein Speicher noch benutzt wird (wg. Laufzeiteffizienz) [dies
ist in Java anders: die JVM überwacht den Heap und führt automatische ein Garbage
Collection durch].
• delete darf ausschließlich auf Objekte angewendet werden, die mit new erzeugt
wurden.
• delete auf einen NULL Zeiger ist wirkungslos.
• Mit new erzeugte Datenobjekte unterliegen nicht den Gültigkeitsregeln für Variablen
–
• sie existieren solange bis sie mit delete gelöscht werden.
13.3 Rekursive dynamische Strukturen
Zeiger sind nicht nur auf einfache Objekte möglich. Ein Zeiger kann auch auf eine Struktur
zeigen. Da ein Element der Struktur selbst wieder ein Zeiger sein kann, entsteht eine
rekursive Datenstruktur.
Wenn ein Zeiger auf eine Struktur zeigt, kann eine Komponente selektiert werden durch
Verwendung des Selektion- und des Dereferenzierungsoperators:
184
Bild 91.
Dynamische rekursive Strukturen
C++ Fragment
struct tupel
{
int i;
double d;
} wert;
wert.i = 18;
wert.d = 1.2
struct tupel
*ps;
ps = &wert;
Erklärung
Definition einer Variable wert,
die eine Struktur von einem int
und einem double ist.
Veranschaulichung
wert
Zeiger auf Struktur tupel;
ps zeigt auf Variable wert.
i
18
d
1.2
wert
i
18
d
1.2
ps
(*ps).i = 42;
cout << ps->i;
Komponente i der Struktur
selektieren, auf die ps zeigt und
den Wert 42 zuweisen.
Kurzschreibweise für (*ps).i
ist:
ps->i
Ausgabe: 42
wert
i
42
d
1.2
ps
Achtung:
Auch hier ist der Rang der Operatoren wichtig:
++ps->i;
(++ps)->i
Entspricht ++(p->i) also wird die Komponente i inkrementiert.
Inkrementiert ps, zeigt also auf anderes Objekt, dann wird die
Komponente i selektiert!
13.3.1 Verkettete Listen
Als erstes Anwendungsbeispiel werden verkettete Listen demonstriert.
Eine verkette Liste ist eine rekursive dynamische Struktur, bei dem ein Strukturelement auf
ein Datenobjekt des gleichen Typs zeigt.
D.h. ein Knoten hat die Form:
185
Bild 92.
Verkettete Liste
C++ Fragment
struct knoten
{
int info;
struct knoten *next;
};
Veranschaulichung
info
next
Damit lassen sich dann Listenelemente verketten:
Bild 93.
Verkettete Liste
Fehler! anfang
ende
Wir werden nun Operationen auf der verketten Liste implementieren:
• void einfuegen(int x)
Ein neues Element mit Infowert x soll immer am Ende der Liste eingefügt werden.
• void ausgeben()
Die Liste wird vom Anfang an bis zum Ende durchlaufen und der Info-Teil jeden
Knotens wird ausgegeben.
• void loeschen(struct knoten *p)
Das Element, auf das p zeigt soll gelöscht werden.
• struct knoten * suchen(int x)
Das erste Element mit Infowert x soll gesucht werden Wenn kein solches Element in
der Liste ist, soll Ergebnis NULL sein.
Die Struktur und die beiden Zeiger werden global definiert.
186
Listing 168. verketteteListe.cpp
..
struct knoten
{
int info;
struct knoten *next;
};
struct knoten *anfang, *ende;
Bild 94.
// Zeiger auf Anfang und Ende der Liste
Einfügen in verkettete Liste:
anfang
ende
x
p
Listing 169. einfuegen.cpp
void einfuegen(int x)
{
struct knoten *p = new knoten;
p->info = x;
p->next = NULL;
if (anfang == NULL) {
anfang = p;
ende = p;
} else
{
ende->next = p;
ende = p;
}
}
// einfuegen am Ende der Liste
// erzeuge neuen Knoten
// Infowert wird x
// next-Wert wird initialisiert mit NULL
// Leere Liste
// Liste nicht leer
// einfuegen am Ende
// neues Ende
187
Bild 95.
Ausgeben einer Liste:
anfang
Fehler!
ende
p
Listing 170. ausgeben.cpp
void ausgeben()
{
struct knoten *p = anfang;
cout << "Liste: ";
while (p != NULL)
{
cout << p->info << " ";
p = p->next;
}
cout << endl;
}
// ausgeben von Anfang bis Ende
// p zeigt auf aktuellen Knoten
// Infoteil des aktuellen Knotten ausgeben
// p auf Folgeelement setzen
Listing 171. suchen.cpp
struct knoten * suchen(int x)
{
struct knoten *p=anfang;
while (p != NULL)
{
if (p->info == x)
return p;
else
p = p->next;
}
return NULL;
}
// sequentielles Suchen
// p auf Folgeelement setzen
// nicht gefunden
188
Bild 96.
Löschen von Listenelementen:
p
anfang
ende
p1
Listing 172. loeschen.cpp
void loeschen(struct knoten *p)
{
struct knoten *p1;
p1=anfang;
if (p==anfang)
{
anfang = anfang->next;
return;
}
while (p1->next != p)
p1 = p1->next;
p1->next = p->next;
delete p;
}
// neuer Anfang
// suche Vorgaenger von p
// verkette neu
// Speicher freigeben
189
Listing 173. main.cpp
int main()
{
anfang = ende = NULL;
// Leere Liste
int eingabe;
while (true)
{
cout << "Integer (Abbruch mit negativem Wert): ";
cin >> eingabe;
if (eingabe<0) break;
// Beende Eingabeaufforderung
einfuegen(eingabe);
}
ausgeben();
cout << "Integer, der gelöscht werden soll: ";
cin >> eingabe;
struct knoten *p;
p = suchen(eingabe);
if (p != NULL)
loeschen(p);
else
cout << eingabe << "nicht in Liste vorhanden" << endl;
ausgeben();
return 0;
}
190
14 Typedef
Komplexe Deklarationen sind (wenn möglich) zu vermeiden; manchmal sind sie aber nicht
zu umgehen.
Durch diese Deklaration
char *(*(*seltsam)(double,int)[3];
wird ein Zeiger (seltsam) auf eine Funktion mit double und int Parametern, die einen
Zeiger auf ein Array von Zeigern auf char zurückgibt, definiert.
Diese Deklaration führt zu schwer lesbarem Kode.
Um solch komplexe Deklarationen zu vereinfachen, existiert die Möglichkeit, Namen zu
definieren.
Mit
typedef bestehenderTyp neuerName ;
kann dem bestehenden Typ ein neuer Name gegeben werden.
Es wird kein neuer Typ definiert, sondern nur ein neuer Name eingeführt.
Listing 174. Typedef Beispiel:
typedef float real;
int main()
{
real x = 1.2;
cout << x << endl;
return 0;
}
Komplexe Deklarationen werden durch typedef besser lesbar, weil neue Typnamen zur
Strukturierung verwendet werden können:
typedef char* ArrayVon3CharZeigern [3];
Damit kann man dann z.B. eine Variable p definieren:
ArrayVon3CharZeigern p;
Durch
entspricht wg. typedef dann
char *p[3]
typedef ArrayVon3CharZeigern *ZeigerAufArrayVon3CharZeigern;
haben wird nun den Namen ZeigerAufArrayVon3CharZeigern definiert.
Damit lässt sich nun die unlesbare Deklaration insgesamt definieren als
typedef char* ArrayVon3CharZeigern [3];
typedef ArrayVon3CharZeigern *ZeigerAufArrayVon3CharZeigern;
ZeigerAufArrayVon3CharZeigern (*seltsam)(double,int);
191
15 Die Idee der Objektorientierung
Das Konzept der Objektorientierung wird aus folgenden Blickwinkeln betrachtet:
Modellierung von Aufgabenstellungen aus der realen Welt,
Probleme beim Entwurf von Softwaresystemen mit prozeduraler Programmierung.
Objektorientierung als Klassifizieren von Problemen
Die Grundidee der Objektorientierten Programmierung (kurz OOP) ist es, einen Ausschnitt
der realen Welt in einem Programm abzubilden.
In allen wissenschaftlichen Disziplinen hat sich das hierarchische Ordnungsprinzip bewährt:
Einteilung der Objekte der realen Welt in Klassen.
Beispiel Biologie: Einteilung der Lebewesen die Klassen Tiere, Pflanzen, Wirbeltiere, …
Dabei sind Gruppen von Objekten mit gleichen Eigenschaften zu einer Klasse
zusammengefasst: gehört ein Objekt zu einer Klasse, dann hat es auch deren Eigenschaften.
Die so gebildeten Klassen sind geordnet, so sind alle Säugetiere auch Wirbeltiere.
Lebewesen
Pflanzen
Tiere
…
Wirbeltier
…
Oberklasse von Säugetiere
Säugetier
Unterklasse
(abgeleitete Klasse)
von Wirbeltiere
OOP ist ein Klassifizieren von Problemen, weniger eine Beschreibung der Aktionen (dies
ist Hauptgegenstand der Prozeduralen Programmierung).
Probleme der Prozeduralen Programmierung
Eines der Probleme der Prozeduralen Programmierung ist die Wiederverwendung und
Anpassung von Kode.
Software wird häufig um neue Funktionen erweitert, indem ein „ähnliches“ Modul kopiert
und angepasst wird. Dann wird es in das System integriert.
Diese Vorgehensweise hat zwei große Nachteile:
Das kopierte und modifizierte Modul muss vollständig getestet werden, also auch bereits
getestete Teilfunktionalität (des Orginals).
Durch die Redundanzen müssen Fehler, die im redundanten Teil auftreten, in allen Kopien
bereinigt werden. Dies ist eine weitere Fehlerquelle und sehr aufwendig.
192
Die Redundanzen im Quellkode erschweren die Änderungs- und Wartbarkeit des so
entstandenen Systems – schlimmstenfalls ist das System nicht mehr wartbar.
Durch OOP soll dies verhindert werden, da Gemeinsamkeiten in Oberklassen
zusammengefasst werden und die Erweiterungen in abgeleiteten Klassen behandelt werden.
193
16 Abstrakte Datentypen
In diesem Teil der Veranstaltung werden die Konzepte Klasse und Objekt beleuchtet, um
abstrakte Datentypen verwirklichen zu können
16.1 Einleitung
Zunächst werden einige Begriffe geklärt. Dazu wird ein Beispiel betrachtet, bei dem ein
Bankkonto modelliert werden soll.
Ein Bankkonto hat einen Kontostand (Saldo) und eine Überziehungslinie. Man kann Geld
einzahlen, abheben und den Kontostand abfragen.
Ein Bankkonto kann in unstrukturierter Weise realisiert werden, indem zwei globale
Variable verwendet werden, auf die direkt an beliebiger Stelle im Programm zugegriffen
wird.
194
Listing 175. Bankkonto, 1. Version
$ cat konto01.cpp
#include <iostream>
// Konto durch unstrukturierten Zugriff
double kontostand=0;
// Kontosaldo bei Eroeffnung
double linie;
// Linie für Überziehungen
int main()
{
char auswahl;
while (true) {
double betrag;
cout << "---- Buchen ----" << endl;
// Menü
cout << "e Einzahlung" << endl;
cout << "a Auszahlung" << endl;
cout << "k Kontostand" << endl;
cout << "b Beenden" << endl;
cout << "Auswahl: ";
cin >> auswahl;
switch (auswahl) {
// Auswahl
case 'e': cout << " Einzahlungsbetrag: "; // Einzahlung
cin >> betrag;
kontostand += betrag;
// Kontostand erhöhen
break;
case 'a': cout << " Auszahlungsbetrag: ";// Auszahlung
cin >> betrag;
if (kontostand - betrag >= linie) // Linienprüfung
kontostand -= betrag;
// Kontostand verringern
else cout << " Linie überschritten!" << endl;
break;
case 'k': cout << " Kontostand: " << kontostand << endl;
break;
case 'b': exit(0);
// Beenden
default: cout << "ungültige Auswahl" << endl;
}
}
return 0;
}
Diese Lösung ist schwer erweiterbar und schlecht wartbar. Nach den ersten Erweiterungen ist
der Kode nicht mehr lesbar. Dies führt zu fehlerbehafteten Programmen.
Die grundlegenden Konzepte des Software-Entwurfs sind:
Strukturierung,
Modularität und
Objektorientierung.
Strukturiertes Programmieren ist eine methodische Vorgehensweise, um Programm
systematisch zu konstruieren: Die Verwendung einiger weniger Kontrollanweisungen für die
Steuerung des Programmflusses und anwendungsbezogenen Datenstrukturen macht
Programme übersichtlich und die Programmierung und Wartung produktiver.
(„Programm = Algorithmus + Datenstruktur“)
Mit dieser Vorgehensweise lässt sich unser Beispiel des Bankkontos nun formulieren:
195
Listing 176. Bankkonto, 2. Version
$ cat konto02.cpp
#include <iostream>
struct Konto
{
double kontostand;
double linie;
} konto1;
// Konto durch strukturierten Zugriff
// Kontosaldo
// Linie für Überziehungen
void kontostandAnzeigen(Konto k)
{
cout << " Kontostand: " << k.kontostand << endl;
}
void kontoInitialisieren(Konto &k)
anwendungsspez.
{
Zugriffsfunktionen
k.kontostand = 0;
k.linie = -100.0;
}
void einzahlen(Konto &k)
{
double betrag;
cout << " Einzahlunsbetrag: ";
// Einzahlung
cin >> betrag;
k.kontostand+= betrag;
// Kontostand erhöhen
}
void auszahlen(Konto &k)
{
double betrag;
cout << " Ausahlunsbetrag: ";
// Einzahlung
cin >> betrag;
if (k.kontostand - betrag >= k.linie)
// Linienprüfung
k.kontostand -= betrag;
// Kontostand verringern
else cout << " Linie überschritten!" << endl;
}
196
int main()
{
char auswahl;
kontoInitialisieren(konto1);
// Initialisierung
while (true)
{
cout << "---- Buchen ----" << endl;
// Menü
cout << "e Einzahlung" << endl;
cout << "a Auszahlung" << endl;
cout << "k Kontostand" << endl;
cout << "b Beenden" << endl;
cout << "Auswahl: ";
cin >> auswahl;
switch (auswahl)
{
// Auswahl
case 'e':
einzahlen(konto1);
break;
case 'a':
auszahlen(konto1);
break;
case 'k':
kontostandAnzeigen(konto1);
break;
case 'b':
exit(0); // Beenden
default:
cout << "ungültige Auswahl" << endl;
}
}
return 0;
}
Trotz strukturierter Programmierung könnte in einer Erweiterung des Programms direkt auf
die globale Variable konto1 zugegriffen werden, ohne eine Funktion zu schreiben.
Die strukturierte Programmierung löst die Probleme, die beim „Programmieren im Großen“
auftreten nicht.
Eine Aufgabe ist in Teile (Module) zu zerlegen, die nur über definierte Schnittstellen
zugreifbar sind. Der Zugriff auf eine Datenstruktur eines Moduls darf nur ausschließlich
durch speziell für die Datenstruktur entwickelte Funktionen erfolgen.
Von außen her betrachtet ist ein Modul eine Abstraktion, dessen innere Komponenten
verborgen und damit geschützt sind.
konto
Daten
Schnittstellenfunktionen
kontostand
linie
einzahlen()
auszahlen()
kontostandAnzeigen()
kontoInitialisieren()
Zugriff nur über
Schnittstellenfunktionen
197
Durch den ausschließlichen Zugriff über die Schnittstellenfunktionen sind Änderungen im
Inneren für den Benutzer des Moduls unwichtig. Dieses Prinzip, die Implementierung zu
verbergen wird Geheimhaltungsprinzip genannt.
Das Prinzip der Zusammenfassung von Daten und sie verändernden Funktionen nennt man
auch Datenkapselung (engl. encapsulation). Ein so gekapselter Datentyp (wie konto) wird
abstrakter Datentyp (kurz ADT) genannt.
Somit werden Sprachkonstrukte gebraucht, mit denen man solche abstrakte Datentypen
realisieren kann.
Wir werden im folgenden C++ Sprachmittel dafür besprechen.
198
17 Klasse und Objekt
Eine Klasse ist ein abstrakter Datentyp, der in einer Programmiersprache formuliert ist. Es
ist somit die Beschreibung des Verhaltens und der Eigenschaften von ähnlichen Objekten.
In C++ dient eine Klasse dazu, dem Compiler die Beschreibung von Objekten mitzuteilen,
die später durch eine Objektdefinition erzeugt werden.
Ein Objekt ist die konkrete Ausprägung einer Klasse; es belegt also Speicherplatz; die
Klasse verbraucht keinen Speicherplatz.
Die Eigenschaften (Attribute) eines Objektes werden durch C++ Datentypen (elementare
und zusammengesetzte) dargestellt. Der Zustand eines Objektes ist der Wert seine Attribute.
Das Verhalten wird durch klassenspezifische Funktionen (Methoden) beschreiben. Eine
Methode, angewendet auf ein Objekt, verändert den Objektzustand. Die öffentlichen
Methoden der Klasse bilden die Schnittstellen des ADT, private Methoden sind
klassenspezifische, von außen nicht zugreifbare „Hilfsroutinen“.
Objekte können durch die Position im Speicher eindeutig identifiziert werden; d.h. zwei
Objekte, auch wenn sie zur gleichen Klasse gehören, haben immer unterschiedliche
Adressen.
Listing 177. Bankkonto, 3.Version, Klasse Konto
$ cat konto03.cpp
#include <iostream>
// Konto als Klasse
class CKonto
{
public:
Methoden
void kontoInitialisieren()
öffent {
lich
kontostand = 0;
linie = -100.0;
}
void kontostandAnzeigen()
{
cout << " Kontostand: " << kontostand << endl;
}
void einzahlen()
{
…
}
Attribute
void auszahlen()
privat {
…
}
private:
double kontostand;
// Kontosaldo bei Eroeffnung
double linie;
// Linie für Überziehungen
};
199
Daten und Methoden sind durch class {} zusammengefasst. Der Gültigkeitsbereich von
Klassenelementen ist lokal zu der Klasse. Das Schlüsselwort private könnte entfallen, wenn
die Attribute vor public-Bereiche stehen würden (Default ist private).
Alle Elemente nach dem Schlüsselwort public sind öffentlich zugänglich.
Die Methoden nennt man auch „Elementfunktionen“.
Um ein Objekt einer Klasse verwenden zu können, muss es zuerst definiert werden. Dann
kann man die öffentliche Methode „aufrufen“, um den Zustand des Objekts zu verändern.
Listing 178. Bankkonto, 3. Version, main
int main()
Erzeugung eines Objektes
{
CKonto konto1;
char auswahl;
konto1.kontoInitialisieren();
// Initialisierung
while (true)
{
cout << "---- Buchen ----" << endl; // Menü
cout << "e Einzahlung" << endl;
cout << "a Auszahlung" << endl;
cout << "k Kontostand" << endl;
cout << "b Beenden" << endl;
Methodenaufrufe
cout << "Auswahl: ";
cin >> auswahl;
switch (auswahl)
{
// Auswahl
case 'e':
konto1.einzahlen();
break;
case 'a':
konto1.auszahlen();
break;
…
default:
cout << "ungültige Auswahl" << endl;
}
}
return 0;
}
So wie bei einer Variablendefinition wird durch „CKonto konto1“ hier Speicher
bereitgestellt; eben Speicher, der Daten und Programmkode für das Objekt konto1 vom Typ
CKonto enthält.
Der Zugriff auf Elemente eines Objekts erfolgt in der Form „Objektname.Elementname“.
Bis jetzt ist also die Idee der ADT mit dem Konzept Klasse in C++ abbildbar. Die
Geheimhaltung ist aber noch offen, da die Realisierung der Methoden direkt in den Kode der
Klasse integriert ist. Das Verbergen der Implementierung kann erreicht werden, in dem
man in der Klasse nur die Prototypen verwendet und die Implementierung in eine eigene
Datei verlagert.
200
zugänglich
verborgen
konto.h
Deklaration der Klasse mit Prototypen
kontoAnwendung.cpp
Objekte der Klasse erzeugen und
verwenden
konto.cpp
Implementierung der Methoden der
Klasse
Listing 179. Bankkonto, 4. Version, Prototypen
$ cat konto.h
Prototypen
#include <iostream>
class CKonto
{
public:
void kontoInitialisieren();
// Initialisierung{
void kontostandAnzeigen();
// Ausgabe des Kontostandes
void einzahlen();
// Einzahlen
void auszahlen();
// Auszahlen
private:
double kontostand;
// Kontosaldo bei Eroeffnung
double linie;
// Linie für Überziehungen
};
201
Nun wird in der Implementierung der Bereichsoperator „::“ verwendet, um zu definieren,
zu welcher Klasse die Implementierung der Methode gehört.
Listing 180. Bankkonto, 4.Version, Implementierung
$ cat konto.cpp
#include "konto.h"
void CKonto::kontoInitialisieren()
{
kontostand = 0;
linie = -100.0;
}
void CKonto::kontostandAnzeigen()
{
cout << " Kontostand: " << kontostand << endl;
}
void CKonto::einzahlen()
{
double betrag;
cout << " Einzahlunsbetrag: ";
// Einzahlung
cin >> betrag;
kontostand+= betrag;
// Kontostand erhöhen
}
void CKonto::auszahlen()
{
double betrag;
cout << " Ausahlunsbetrag: ";
// Einzahlung
cin >> betrag;
if (kontostand - betrag >= linie)
// Linienprüfung
kontostand -= betrag;
// Kontostand verringern
else cout << " Linie überschritten!" << endl;
}
202
Innerhalb von main ist jetzt nur die Headerdatei erforderlich:
Listing 181. Bankkonto, 4.Version, main
$ cat kontoMain.cpp
#include "konto.h"
int main()
{
CKonto konto1;
char auswahl;
konto1.kontoInitialisieren();
// Initialisierung
while (true)
{
cout << "---- Buchen ----" << endl; // Menü
cout << "e Einzahlung" << endl;
cout << "a Auszahlung" << endl;
cout << "k Kontostand" << endl;
cout << "b Beenden" << endl;
cout << "Auswahl: ";
cin >> auswahl;
switch (auswahl)
{
// Auswahl
case 'e':
konto1.einzahlen();
break;
case 'a':
konto1.auszahlen();
break;
case 'k':
konto1.kontostandAnzeigen();
break;
case 'b':
return 0;
// Beenden
default:
cout << "ungültige Auswahl" << endl;
}
}
return 0;
}
203
18 Konstruktoren
Objekte sollten definierte Initialwerte für ihre Attribute haben, wenn sie erzeugt worden
sind. Im letzten Beispiel CKonto war dazu eine Methode explizit implementiert worden
(kontoInitialisieren()).
Wenn ein Objekt erzeugt wird, wird Speicherplatz für das Objekt angelegt und automatisch
ein Konstruktor aufgerufen, der die Versorgung mit Anfangswerten übernimmt.
Ein Konstruktor ist so ähnlich aufgebaut wie eine Funktion, der Name ist immer identisch mit
dem Namen der zugehörenden Klasse. Konstruktoren haben keine Return-Typen, auch nicht
void.
18.1 Standardkonstruktor
Wenn im Programm kein Konstruktor angegeben ist, wird ein Default-Konstruktor
verwendet. Ein Konstruktor ohne formale Parameter, heißt Standardkonstruktor.
Also ändern wir unser Beispiel und verwenden einen Standardkonstruktor anstelle der
expliziten Methode kontoInitialisieren().
Listing 182. Kontoklasse mit Standardkonstruktor
$ cat konto.h
#include <iostream>
class CKonto
{
public:
CKonto();
// Initialisierung per Konstruktor
void kontostandAnzeigen();
// Ausgabe des Kontostandes
…
private:
double kontostand;
// Kontosaldo bei Eroeffnung
…
};
Listing 183. Kontoimplementierung Standardkonstruktor
$ cat konto.cpp
#include "konto.h"
CKonto::CKonto()
{
kontostand = 0;
linie = -100.0;
}
void CKonto::kontostandAnzeigen()
{
…
204
18.2 Allgemeine Konstruktoren
Im Gegensatz zu Standardkonstruktoren, haben Allgemeine Konstruktoren Argumente.
Weiterhin können Allgemeine Konstruktoren überladen werden, wobei es aber stets eine
eindeutige Signatur geben muss.
Somit können wir unser Beispiel ändern, und die Initialwerte für Konten beim Erzeugen
eines Objektes bestimmen:
Listing 184. Konstruktor mit Parameter, Header
$ cat CKonto02/konto.h
#include <iostream>
class CKonto
{
public:
CKonto();
// Initialisierung per Konstruktor
CKonto(double kontostand, double linie);
void kontostandAnzeigen();
// Ausgabe des Kontostandes
…
private:
double kontostand;
// Kontosaldo bei Eroeffnung
…
};
Listing 185. Konstruktor mit Parameter, Implementierung
$ cat konto.cpp
#include "konto.h"
CKonto::CKonto()
{
kontostand = 0;
linie = -100.0;
}
CKonto::CKonto(double pKontostand, double pLinie)
{
kontostand = pKontostand;
if (pLinie > 0) linie = -pLinie;
// linie muss negativ oder null sein
else linie = pLinie;
}
void CKonto::kontostandAnzeigen()
{
…
Listing 186. Konstruktor mit Parameter, main
int main()
{
CKonto konto1(100, 1000);
CKonto konto2();
…
205
Wenn es mehrere Konstruktoren für eine Klasse gibt, wird der Compiler anhand der Anzahl
und der Typen der Parameter (Signatur) des Aufrufs entscheiden, welcher Konstruktor
verwendet wird.
Achtung:
Wie bei normalen Funktionen auch, kann man bei Konstruktoren die Anzahl der Parameter
variabel halten. Dabei muss aber stets die Eindeutigkeit der Signatur über alle Konstruktoren
einer Klasse hinweg gegeben sein.
Listing 187. Signaturunterschiede
CKonto(double kontostand, double linie=-1000.0);
…
CKonto konto3(0.0);
18.3 Initialisierung mit Listen
Beim Aufruf eines Konstruktors wird
zunächst Speicherplatz für die Attribute der Klasse reserviert,
dann erfolgt die Parameterübergabe (aktuelle Parameter -> formale Parameter).
Zuletzt wird dann der Block (Konstruktorrumpf) ausgeführt.
Da normalerweise durch die Parameter des Konstruktor klassenlokale Attribute initialisiert
werden, kann Schritt 1 und 2 zusammen ausgeführt werden, wodurch bei vielen und/oder
großen Objekten Laufzeitvorteile bei der Erzeugung der Objekte resultieren.
Dazu dienen Initialisierungslisten:
Vor dem Konstruktorrumpf wird eine Liste der Form
: AttributName_i(Parameter_j), … , AttributName_l(Parameter_m)
angebeben.
Listing 188. Initialisierungsliste
$ cat CKonto03/konto.cpp
#include "konto.h"
CKonto::CKonto()
{
kontostand = 0;
linie = -100.0;
}
Initialisierungsliste
CKonto::CKonto(double pKontostand, double pLinie)
:kontostand(pKontostand), linie(pLinie)
{
if (linie > 0) linie = -linie; // linie muss negativ oder null sein
}
void CKonto::kontostandAnzeigen() {
…
Achtung:
Die Initialisierung innerhalb der Initialisierungsliste erfolgt in der Reihenfolge der
Attributdeklarationen in der Klasse, nicht in der Aufschreibungsreihenfolge der
Initialisierungsliste
Ein weiteres Anwendungsfeld von Initialisierungslisten ist das Vorbesetzen von Konstanten
einer Klasse.
206
Kopierkonstruktor (engl. copy constructor)
Der Kopierkonstruktor wird verwendet, um ein Objekt erzeugen zu können und es mit den
Attributwerten eines anderen Objektes derselben Klasse initialisieren zu können.
Die allgemeine Form der Deklaration eines Kopierkonstruktors ist:
KlassenName( KlassenName& name) {}
Im Normalfall sollte das Objekt, das man als Kopiervorlage nimmt, nicht verändert werden.
Deshalb ist die gebräuchliste Form:
KlassenName( const KlassenName& name) {}
Wird in einer Klasse kein eigener Kopierkonstruktor deklariert, wird ein
Standardkopierkonstrukor verwendet. Grunddatentypen (es sind einfache Objekte) werden
kopiert, indem das zugehörige Speicherabbild (Bitmuster) kopiert wird.
Ein Objekt wird mittels eines Kopierkonstruktor erzeugt durch:
Klassenname objekt1;
Initialisierung durch
Klassenname objekt2 = objekt1;
Kopierkonstruktor, keine
Zuweisung!
Listing 189. Kopierkonstruktor, Header
$ cat CKonto04/konto.h
#include <iostream>
class CKonto
{
public:
CKonto();
CKonto(double kontostand, double linie);
CKonto::CKonto(const CKonto& pKonto);
void kontostandAnzeigen();
Kontostandes
…
private:
double kontostand;
Eroeffnung
…
};
// Initialisierung
// Kopierkonstruktor
// Ausgabe des
// Kontosaldo bei
207
Listing 190. Kopierkonstruktor, Implementierung
$ cat konto.cpp
#include "konto.h"
CKonto::CKonto()
{
kontostand = 0;
linie = -100.0;
}
CKonto::CKonto(double pKontostand, double pLinie)
{
kontostand = pKontostand;
if (pLinie > 0) linie = -pLinie;
// linie muss negativ oder null sein
else linie = pLinie;
}
CKonto::CKonto(const CKonto& pKonto)
// Kopierkonstruktor
:kontostand(pKonto.kontostand), linie(pKonto.linie)
{
cout << "Kopierkonstruktor wurde aufgerufen!" << endl;
}
void CKonto::kontostandAnzeigen() {
…
Der Kopierkonstruktor wird nur verwendet,
wenn ein neues Objekt erzeugt wird oder
wenn ein Objekt durch Wertübergabe an eine Funktion übergeben wird oder
wenn eine Funktion ein Objekt als Ergebnis zurück liefert.
Bei der Zuweisung eines Objektes oder der Initialisierung mit einem Allgemeinen
Konstruktor wird der Kopierkonstruktor nicht verwendet!
Listing 191. Kopierkonstruktor, main
$ cat CKonto04/kontoMain.cpp
main()
Kopierkonstruktor wird verwendet
{
CKonto konto1(0, 100);
CKonto konto2 = konto1;
Zuweisung: Kopierkonstruktor wird nicht
CKonto konto3(100, 100);
verwendet
CKonto konto4;
konto2 = konto3;
konto4 = Ckonto(10, 20);
Allgemeiner Konstruktor: Kopierkonstruktor
wird nicht verwendet
}
Welche Ausgabe erzeugt das o.a. Programm?
Häufig muß der Kopierkonstruktor überladen oder blockiert werden um Probleme zu
vermeiden.
Folgende Situation kann vorkommen:
In einer Klasse myClass wird Speicher alloziert. A sei ein Objekt aus dieser Klasse. Wird A
verwendet, um B zu initialisieren
MyClass B=A;
So würde mit dem Standard-Kopierkonstruktor eine bitweise exakte Kopie entstehen. B
verwendet also denselben Speicher wie A. Gibt es nun in myClass einen Destruktor, dann
208
kann der Speicher bei Vernichtung eines Objektes freigegeben werden, obwohl das andere
Objekt ihn noch braucht.
In einem Beispiel
array a(10);
....
array b(10);
b=a;
wird nicht der Kopierkonstruktor aufgerufen sondern der Zuweisungsoperator ausgeführt.
Auch hier kann es zu unerwünschten Effekten kommen durch die bitweise Kopie. In einigen
Fällen muß daher auch der Operator = mit einem eigenen Operator überladen werden, um die
gewünschten Kopien/Zuweisungen zu erzeugen.
18.4 Zuweisungsoperator
Eine Operatorfunktion als Elementfunktion einer Klasse hat folgende Syntax:
ret-type class-name::operator # (arg-list)
Das Zeichen # dient als Platzhalter für den Operator. Eine Zuweisung wird demnach wie
folgt überladen:
CBeispiel::CBeispiel& operator=( const CBeispiel& rhs ) // Zuweisungsop.
{
//hier folgt eigene Implementierung des Zuweisungsoperators
}
209
19 Destruktoren
Konstruktoren sind für die Aktionen bei der „Geburt“ eines Objektes verantwortlich.
Destruktoren dienen zum Aufräumen, bevor ein Objekt „stirbt“. Wenn ein Destruktor nicht
explizit programmiert ist, wird der Default Destruktor verwendet.
Die wichtigste Aufgabe eines Destruktors ist es, den nicht mehr benötigten Speicherplatz
wieder frei zu geben.
Destruktoren haben weder Argumente noch einen Rückgabewert. Sie werden durch den
Klassennamen mit Vorangestellter Tilde (‚~’) gekennzeichnet.
Die Gültigkeit eines Objektes beginnt mit der öffnenden Klammer und endet mit der
korrespondierenden schließenden Klammer; d.h. der Konstruktor wird beim Eintreten in
einen Block aufgerufen, der Destruktor bei Verlassen des Blocks.
Die Reihenfolge der Aufrufe von Konstruktor und Destruktor ist umgekehrt.
Beispiel:
Listing 192. Destruktor, Beispiel1
$ cat destruktor.cpp
#include<iostream>
using namespace std;
class CBeispiel
{
int zahl;
public:
CBeispiel(int i=0);
// Konstruktor
~CBeispiel();
// Destruktor
};
CBeispiel::CBeispiel(int i)
{
// Konstruktor
zahl=i;
cout << "Objekt " << zahl << " wird erzeugt.\n";
}
CBeispiel::~CBeispiel()
{
// Destruktor
cout << "Objekt " << zahl << " wird zerstört.\n";
}
$ destrukt
int main()
main wird begonnen
{
cout << "main wird begonnen\n" ; Objekt 1 wird erzeugt.
main wird verlassen
CBeispiel einBeispiel(1);
cout << "main wird verlassen\n" ; Objekt 1 wird zerstört.
$
}
$
Dabei gilt dies nicht nur auf Ebene von Funktionen, sondern auch für beliebige Blöcke:
210
Listing 193. Destruktor, Beispiel2
$ cat destrukt02.cpp
class Beispiel
{
int zahl;
public:
Beispiel(int i=0);
// Konstruktor
~Beispiel();
// Destruktor
};
Beispiel::Beispiel(int i)
{
// Konstruktor
zahl=i;
cout << "Objekt " << zahl << " wird erzeugt.\n";
}
Beispiel::~Beispiel()
{
// Destruktor
cout << "Objekt " << zahl << " wird zerstört.\n";
}
int main()
$ destrukt02
{
main wird begonnen
cout << "main wird begonnen\n" ;
Objekt 1 wird erzeugt.
Beispiel einBeispiel(1);
neuer Block
Objekt 2 wird erzeugt.
{ cout << " neuer Block\n ";
Block wird verlassen
Beispiel einBeispiel(2);
Objekt 2 wird zerstört.
cout << " Block wird verlassen\n ";
main wird verlassen
}
Objekt 1 wird zerstört.
cout << "main wird verlassen\n" ;
$
}
$
Zu beachten ist, dass dadurch Aktionen vor Ausführung der Funktion main und nach ihrer
Ausführung stattfinden:
• Wenn in einem Programm globale Objekte existieren, wird ihr Konstruktor vor der
ersten Anweisung von main aufgerufen.
• Innerhalb des äußeren Blocks von main definierte Objekte werden erst nach Verlassen
von main freigegeben.
• Wegen der umgegehrten Reihenfolge werden globale Objekte zuletzt freigegeben.
CBeispiel g;
main()
{
Konstruktor für g
CBeispiel l;
…
}
Konstruktor für
Destruktor für l
Konstruktor für g
211
Listing 194. Destruktor, Beispiel3
class Beispiel
{
int zahl;
public:
Beispiel(int i=0);
// Konstruktor
~Beispiel();
// Destruktor
};
Beispiel::Beispiel(int i)
{
// Konstruktor
zahl=i;
cout << "Objekt " << zahl << " wird erzeugt.\n";
}
Beispiel::~Beispiel()
{
// Destruktor
cout << "Objekt " << zahl << " wird zerstört.\n";
}
Beispiel ein_globales_Beispiel; // globale Variable, durch Vorgabewert mit 0 initialisiert
int main()
{
$ destrukt03
cout << "main wird begonnen\n" ;
Objekt 0 wird erzeugt.
Beispiel einBeispiel(1);
main wird begonnen
{
Objekt 1 wird erzeugt.
cout << " neuer Block\n ";
neuer Block
Beispiel einBeispiel(2);
Objekt 2 wird erzeugt.
cout << " Block wird verlassen\n ";
Block wird verlassen
}
Objekt 2 wird zerstört.
cout << "main wird verlassen\n" ;
main wird verlassen
}
Objekt 1 wird zerstört.
Objekt 0 wird zerstört.
$
212
20 Konstante Objekte und Methoden
Variable können durch den Zusatz const als konstant deklariert werden. Dies ist bei Objekten
genauso möglich.
Dabei sind Änderungen des Zustandes bzw. der Daten/Attribute des Objektes über
normale Methodenaufrufe nicht mehr möglich. Nur die Konstruktoren und Destruktoren
und konstante Elementfunktionen sind erlaubt.
Ein const Qualifizierer wird beim Überladen von Methoden ausgewertet, d.h. die Signatur
mit und ohne const ist verschieden.
213
Listing 195. Beispiel-Klasse für rationale Zahlen
void rationale::ausgabe();
// Methode 1
void rationale::ausgabe() const;
// Methode 2
//Je nach Deklaration eines Objektes wird die entsprechende Methode aufgerufen:
rationale r;
const rationale cr(1,2);
r.ausgabe();
cr.ausgabe();
// „normale“ rationale Zahl
// konstante rationale Zahl ½
// Methode 1 wird ausgeführt
// und darf Datenelemente der Klasse verändern
// Methode 2 wird ausgeführt und darf keine
//Datenelemente der Klasse verändern
Listing 196. Beispiel Zeiger auf const-Objekt
Class Rechteck
{
.....
int GetLaenge() const
{return Laenge;}
void SetLaenge(int InLaenge) {Laenge=InLaenge;}
….
}
Rechteck::Rechteck()
{
Laenge=5;
}
int main ()
{
Rechteck * pRect = new Rechteck;
const Rechteck * pConstRect = new Rechteck;
Rechteck * const pConstPtr = new Rechteck;
cout << pRect->GetLaenge<< endl;
cout <<pConstRect->GetLaenge<<endl;
cout <<pConstPtr->GetLaenge<<endl;
pRect->SetLaenge(10);
pConstRect ->SetLaenge(10);
pConstPtr ->SetLaenge(10);
//Zeiger auf Objekt
//Zeiger auf konstantes Objekt
//konstanter Zeiger
// ok
// das geht nicht, konstant
// ok
}
Anmerkung: Tatsächlich sind durch „mutable“ Attribute und „explizite Typumwandlung
(„casting the const away“) trotzdem Änderungen an konstanten Objekten möglich.
214
Listing 197. Weiteres Beispiel, Header
prog.h:
bool lookup_r (const string &, const Node * &) const;
Listing 198. Weiteres Beispiel, Implementierung
prog.cpp
bool Tree::lookup_r (const string & v,const Node * &r) const
//Übergabe Referenz auf String und Referenz
// auf den Pointer auf aktuellen Knoten, der konstant sein soll
// dann sind auch seine Pointer right und left child konstant
// der aufruf unten lässt sich aber nicht umcasten, da der Pointer
nicht
//als konst definiert ist, siehe Fehlermeldung
//abhilfe: const Node * const &r, dann ist auch der aufgerufene //refpointer konstant
{
if (r == NULL)
return (false);
else
//falls Endeknoten noch nicht erreicht
{
if (r->val > v)
// val größer als suchwert ?
{
return(lookup_r (v, r->mLeftChild));
// dann links
weitersuchen
}
else if (r->val < v)
{
// val kleiner als suchwert ?
return(lookup_r (v, r->mRightChild));
// dann rechts weitersuchen
}
else
return (true);
}
}
error C2664: 'lookup_r' : cannot convert parameter 2 from 'class
Tree::Node *const ' to 'const class Tree::Node *& '
Conversion loses qualifiers
Die roten const’s vertragen sich nicht mit den return Zeilen
215
216
21 Vererbung
Diese Teil befasst sich mit einem wichtigen Bestandteil Objektorientierter Sprachen: der
Vererbung von Eigenschaften und damit verbundenen Konzepten.
21.1 Überblick
OOP ist ein Klassifizieren von Problemen. Ein Beispiel dafür ist die Einteilung von
Transportmitteln:
Transportmittel
Klasse
Hoehe
Breite
Daten
bewegen()
Methoden
LandTranportmittel
WasserTransportmittel
RadZahl
Bruttoregistertonnen
fahren()
schieben()
anlegen()
ablegen()
Fahrrad
Auto
AnzahlGänge
Benzinverbrauch
…
fahren()
tanken()
Mehrfachvererbung
Amphibienfahrzeug
Wassersensor
schotten_dicht()
217
Eine Oberklasse ist die Abstraktion oder Generalisierung von ähnlichen Eigenschaften der
Unterklasse.
Die Unterklasse fügt zu den allgemeinen Eigenschaften der Oberklasse nur die
unterklassenspezifischen Eigenheiten hinzu. Die allgemeinen Eigenschaften erbt die
Unterklasse von der Oberklasse.
Damit ist eine Unterklasse eine Spezialisierung der Oberklasse.
LandTranportmittel
RadZahl
fahren()
schieben()
Auto
Benzinverbrauch
Spezialisierung
Generalisierung,
Abstraktion
…
In C++ wird Abstraktion durch „: public“ ausgedrückt. Es wird als „ist ein“ oder „ist eine
Art“ gelesen.
class Klassenname : public Oberklassenname
„Ein Auto ist ein Landtransportmittel“
Damit ist das Beispiel der Transportmittel in C++ wie folgt formulierbar:
Transportmittel
Hoehe
Breite
bewegen()
LandTranportmittel
WasserTransportmittel
RadZahl
Bruttoregistertonnen
fahren()
schieben()
anlegen()
ablegen()
Fahrrad
Auto
AnzahlGänge
Benzinverbrauch
…
…
Amphibienfahrzeug
Wassersensor
schotten_dicht()
218
Listing 199. Vererbung und Mehrfachvererbung
class Transportmittel
{
public:
void bewegen();
private:
double Hoehe, Breite;
};
erben
class LandTransportmittel : public Transportmittel
{
public: void fahren();
void schieben();
private:
int RadZahl;
};
class WasserTransportmittel : public Transportmittel
{
public: void anlegen(); void ablegen();
private: double Bruttoregistertonnen;
};
class Auto : public LandTransportmittel
{
überschreibt Methode
public:
Landtransportmittel::fahren()
void fahren();
void tanken();
private:
int Benzinverbrauch;
};
class Amphibienfahrzeug : public Auto , public WasserTransportmittel
{
public: void schotten_dichts();
Mehrfachvererbung
private:
String Wassensensor;
};
Vererbung (class Abgeleitet : public Oberklasse) bedeutet dabei in C++:
Jedes Objekt ObjektAbgeleitet vom Typ Abgeleitet beinhaltet ein Objekt vom Typ
Oberklasse (Subobjekt genannt), das entsprechend Speicher belegt.
Das Subobjekt wird noch vor der Erzeugung von ObjektAbgeleitet durch den
Oberklassen-Konstruktor erzeugt.
Durch „Auto meinAuto;“ entsteht folgendes Konstrukt:
219
mein Auto
Auto
LandTranportmittel
Transportmittel
Hoehe
Breite
bewegen()
Somit existieren
die Attribute:
meinAuto.
Benzinverbrauch
meinAuto.RadZahl
RadZahl
meinAuto.Hoehe
fahren()
schieben()
meinAuto.Breite
Benzinverbrauch
fahren()
tanken()
Jede (public-) Elementfunktion von Oberklasse kann auf ein Objekt vom Typ Abgeleitet
angewendet werden. Am Aufruf einer Operation ist nicht erkennbar, ob es eine Methode der
Oberklasse oder der Unterklasse ist.
Somit ist im o.a. Beispiel z.B. meinAuto.bewegen() aufrufbar.
Wenn eine Methode von Abgeleitet dieselbe Signatur wie eine Methode von Oberklasse hat,
überschreibt die Abgeleitet-Methode die Oberklasse-Methode.
Somit ist meinAuto.fahren() die speziell für Auto definierte Operation für das Fahren.
In C++ ist eine Klasse ein Datentyp. Eine abgeleitete Klasse ist wie ein Subtyp der
Oberklasse anzusehen (so, wie int ein Subtyp von double ist).
Somit sind Zuweisungen der Form:
ObjOberklasse = ObjAbgeleitet
möglich
LandTransportmittel meinLandtransportmitttel;
Auto meinAuto;
meinLandtransportmitttel = meinAuto; // ☺
ObjAbgeleitet = ObjOberklasse
nicht möglich
meinAuto = meinLandtransportmitttel; //
Die nur zu ObjAbgeleiteten
Attribute werden nicht kopiert,
da in ObjOberklasse kein Platz
dafür vorhanden ist.
Dies ist nicht möglich, da
ansonsten in ObjektAbgeleitet
nicht initialisierte Attribute
vorhanden wären.
220
Der Vererbungsmechanismus wird am Beispiel einer Klasse GraphObj („graphisches
Objekt“) und davon abgeleiteten Klassen für Rechtecke, Linien und Dreiecke demonstriert.
21.2 Die Klasse Ort
Zunächst wird eine Klasse Ort gebraucht, die einen Bezugspunkt P als Pixelkoordinaten (x,y)
zur Verfügung stellt und einige Methoden, z.B. Entfernung implementiert.
y
P2
P1
P1 , P2 = (x2 − x1 )2 + (y 2
x
X1
X2
221
Listing 200. Die Klasse Ort
$ cat ort.h
#ifndef ort_h
#define ort_h
#include<string>
#include<cmath>
// wegen sqrt()
#include<iostream>
using namespace std;
class Ort
{
public:
Ort(int einX = 0, int einY = 0)
: xKoordinate(einX), yKoordinate(einY)
{
}
// Default: Nullpunkt
int X() const { return xKoordinate;}
int Y() const { return yKoordinate;}
void aendern(int x, int y)
{
xKoordinate = x;
yKoordinate = y;
}
private:
int xKoordinate,
yKoordinate;
};
// globale Funktionen
// Berechnung der Entfernung zwischen zwei Orten
inline double Entfernung(const Ort &Ort1, const Ort &Ort2)
{
double dx = static_cast<double>(Ort1.X() - Ort2.X());
double dy = static_cast<double>(Ort1.Y() - Ort2.Y());
return sqrt(dx*dx + dy*dy);
}
// Anzeige auf der Standardausgabe
inline void anzeigen(const Ort &O)
{
cout << '(' << O.X() << ", " << O.Y() << ')';
}
#endif // ort_h
$
Damit kann ein Ort angezeigt und verändert werden:
222
Listing 201. Main zur Verwendung der Klasse Ort
$ cat ortmain.cpp
#include"ort.h"
using namespace std;
// Funktion zum Verschieben des Orts um dx und dy
Ort Ortsverschiebung(Ort derOrt, int dx, int dy)
{
derOrt.aendern(derOrt.X() + dx, derOrt.Y() + dy);
return derOrt;
// Rückgabe des neuen Orts
}
int main()
{
Ort einOrt(10, 300);
Ort verschobenerOrt = Ortsverschiebung(einOrt, 10, -90);
cout << " alter Ort: ";
anzeigen(einOrt);
cout << "\n neuer Ort: ";
anzeigen(verschobenerOrt);
cout << endl;
}
$
Mit dieser einfachen Klasse, die Orte im Koordinatensystem abbildet, wird nun die Klasse
Graphobj definiert.
223
Listing 202. Die Klasse GraphObj
$ cat erben/graphobj.h
#ifndef graphobj_h
#define graphobj_h
#include"ort.h"
class GraphObj
{
public:
GraphObj(const Ort &einOrt)
: Referenzkoordinaten(einOrt) {}
// Version 1
// allg. Konstruktor
// Bezugspunkt ermitteln
const Ort& Bezugspunkt() const { return Referenzkoordinaten; }
// alten Bezugspunkt ermitteln und gleichzeitig neuen wählen
Ort Bezugspunkt(const Ort &nO)
{
Ort temp = Referenzkoordinaten;
Referenzkoordinaten = nO;
// neuen Bezugspunkt setzen
return temp;
// alten Bezugspunkt zurückgeben
}
// Koordinatenabfrage
int X() const { return Referenzkoordinaten.X(); }
int Y() const { return Referenzkoordinaten.Y(); }
// Standardimplementation:
double Flaeche() const {return 0.0;}
private:
Ort Referenzkoordinaten;
};
/* Die Entfernung zwischen 2 GraphObj-Objekten ist hier als
Entfernung ihrer Bezugspunkte (überladene Funktion) definiert.
*/
inline double Entfernung(const GraphObj &g1, const GraphObj &g2)
{
return Entfernung(g1.Bezugspunkt(), g2.Bezugspunkt());
}
#endif // graphobj_h
Damit lassen sich Objekte erzeugen und ausgeben:
224
Listing 203. Main zur Verwendung der Klasse GraphObj
$ cat erben/main.cpp
#include "ort.h"
#include "graphobj.h"
int main()
{
Ort Nullpunkt;
// Default-Konstruktoraufruf
GraphObj G0(Nullpunkt);
Ort einOrt(10, 20);
GraphObj G1(einOrt);
cout << "G0.X() = " << G0.X() << endl; // Ausgabe beider Bezugspunkte auf
verschiedene Art
cout << "G0.Y() = " << G0.Y() << endl;
Ort R1 = G1.Bezugspunkt();
cout << "R1.X() = " << R1.X() << endl;
cout << "R1.Y() = " << R1.Y() << endl;
cout << "Entfernung = " << Entfernung(G0, G1) << endl;
// Ausgabe der Entfernung
}
Nun sollen abgeleitete Klasse erzeugt werden, die Rechtecke und Strecken ermöglichen;
zunächst Rechtecke.
GraphObj
Referenzkoordinaten
bewegen()
Strecke
Rechteck
Endpunkt
hoehe
breite
laenge()
Flaeche()
225
Listing 204. Die Klasse Rechteck
$ cat rechteck.h
#ifndef rechteck_h
#define rechteck_h rechteck_h
#include"graphobj.h"
class Rechteck : public GraphObj // von GraphObj erben
{ public:
Rechteck(const Ort &p1, int h, int b)
: GraphObj(p1), hoehe(h), breite(b) {}
double Flaeche() const
{ // int-Überlauf vermeiden
return double(hoehe) * breite;
}
private:
int hoehe, breite;
};
#endif // rechteck_h
Ein Rechteck ist ein graphisches Objekt, die Klasse Rechteck erbt von GraphObj.
Auch eine Strecke ist ein graphisches Objekt.
Listing 205. Die Klasse Strecke
$ cat strecke.h
#ifndef strecke_h
#define strecke_h strecke_h
#include"graphobj.h"
class Strecke : public GraphObj
{
// erben von GraphObj
public:
// Initialisierung mit Initialisierungsliste
Strecke(const Ort &Ort1, const Ort &Ort2)
: GraphObj(Ort1),
// Initialisierung des Subobjekts
Endpunkt(Ort2) {
// Initialisierung des Attributs
}
// leerer Code-Block
double Laenge() const
{ // Methode Laenge verwendet Methode Entfernung
return Entfernung(Bezugspunkt(), Endpunkt);
}
private:
Ort Endpunkt;
// zusätzlich: 2. Punkt der Strecke
// (der erste ist GraphObj::Referenzkoordinaten)
};
#endif // strecke_h
Eine Strecke besteht aus 2 Punkten (Orten). Ein Ort ist bereits gegeben, da Strecke ein
Subobjekt (GraphObj) enthält, also ist nur noch der Endpunkt zusätzlich erforderlich.
Somit können wir die Klassen in einem Testprogramm verwenden:
226
Listing 206. Graphik-main
$ cat erben/main02,cpp
#include"strecke.h"
#include"rechteck.h"
using namespace std;
int main()
{
// Definition zweier graphischer Objekte
// Default-Konstruktoraufruf
Ort Nullpunkt;
GraphObj G0(Nullpunkt);
Ort einOrt(10, 20);
GraphObj G1(einOrt);
Ort R1 = G1.Bezugspunkt();
Ort O;
Strecke S1(O, R1);
cout << "Strecke von ";
anzeigen(O);
cout << " bis ";
anzeigen(R1);
cout << "\n Fläche der Strecke S1 = "
<< S1.Flaeche()
// geerbte Methode
<< endl;
cout << "Länge der Strecke S1 = "
<< S1.Laenge()
// zusätzliche Methode
<< endl;
einOrt = Ort(20, 30);
Ort O2(100, 50);
// Neuzuweisung
Strecke S2(einOrt, O2);
cout << "= Entfernung der Bezugspunkte: "
<< Entfernung(S1.Bezugspunkt(), S2.Bezugspunkt())
<< endl;
cout << "Entfernung der Strecken S1, S2 = "
<< Entfernung(S1, S2) << endl;
Rechteck R0(Ort(0,0), 20, 50);
cout << "R0.Flaeche = "
<< R0.Flaeche() << endl; // 1000
cout << R0.GraphObj::Flaeche(); // null!
return 0 ;
}
227
$main02
G0.X() = 0
G0.Y() = 0
R1.X() = 10
R1.Y() = 20
Entfernung = 22.3607
neuer Bezugspunkt für G0:
G0.Bezugspunkt() = (10, 20)
Entfernung = 0
Strecke von (0, 0) bis (10, 20)
Fläche der Strecke S1 = 0
Länge der Strecke S1 = 22.3607
= Entfernung der Bezugspunkte: 36.0555
Entfernung der Strecken S1, S2 = 36.0555
R0.Flaeche = 1000
228
Vererbung und Initialisierung
Jedes Objekt einer abgeleiteten Klasse enthält ein anonymes Subobjekt der Oberklasse.
Deshalb kann der Oberklassenkonstruktor in einer Initialisierungsliste direkt aufgerufen
werden:
Listing 207. Subobjekt der Oberklasse
class Strecke : public GraphObj
{
public:
// erben von GraphObj
// Initialisierung mit
Initialisierungsliste
Strecke(const Ort &Ort1, const Ort &Ort2)
: GraphObj(Ort1),
Endpunkt(Ort2)
{
…
// Initialisierung des Subobjekts
// Initialisierung des Attributs
Die Initialisierungsliste kann folgende Elemente enthalten:
Elemente der Klasse selbst, also keine geerbten Elemente und
Konstruktoraufrufe der Oberklassen.
21.3 Zugriffsschutz
Durch die Schlüsselworte public und private haben wir bis jetzt den Zugriff auf Attribute und
Methoden geregelt, wobei gilt:
Mit public gekennzeichnete Elemente unterliegen keiner Restriktion, sie sind
auch von außen zugreifbar;
private Elemente sind nur innerhalb der Klasse und für friend Klassen verfügbar.
Diese Zugriffsspezifizierer gelten auch in der Vererbungskette. Um den Zugriff auf die
Vererbungskette zu beschränken, existiert ein weiteres Schlüsselwort:
Mit protected definierte Elemente sind in der eigenen und in allen public
abgeleiteten Klassen zugreifbar; nicht jedoch in anderen Klassen oder außerhalb
der eigenen Klasse.
Zu der o.a. Definition gelten zusätzlich folgende Regel:
private Elemente sind in einer abgeleiteten Klasse nicht zugreifbar;
in allen anderen Fällen gilt immer das restriktivere Zugriffsrecht, bezogen auf die
Zugriffsrechte für ein Element und die Zugriffskennung der Vererbung einer
Klasse.
Beispiel:
protected Elemente mit private vererbter Klasse ⇒ private in vererbter Klasse
protected Elemente mit public vererbter Klasse ⇒ protected in vererbter Klasse
229
Listing 208. Kodebeispiel public und private:
class Oberklasse
{
private:
// Voreinstellung
int Oberklasse_priv;
void private_Funktion_Oberklasse();
protected:
int Oberklasse_prot;
public:
int Oberklasse_publ;
void Funktion_Oberklasse(){};
};
class abgeleiteteKlasse : public Oberklasse
{
int abgeleiteteKlasse_priv;
public:
int abgeleiteteKlasse_publ;
void Funktion_abgeleiteteKlasse()
{
Oberklasse_priv = 1;
// Fehler!
Oberklasse_prot = 2;
// ok
Oberklasse_publ = 3;
// ok
}
};
void main()
{
int m;
abgeleiteteKlasse Objekt;
m = Objekt.Oberklasse_publ;
m = Objekt.Oberklasse_prot;
m = Objekt.Oberklasse_priv;
m = Objekt.abgeleiteteKlasse_publ;
m = Objekt.abgeleiteteKlasse_priv;
Objekt.Funktion_abgeleiteteKlasse();
Objekt.Funktion_Oberklasse ();
Objekt.private_Funktion_Oberklasse ();
// ok
// Fehler, nicht außerhalb der Klasse!
// Fehler!
// ok
// Fehler!
// ok
// ok
// Fehler!
}
230
Read-Only Daten
Sollen Daten einer Klasse zum Lesen auch außerhalb der Klasse verfügbar gemacht werden,
ohne dass die Daten aber außerhalb veränderbar sind, kann dies über den Zugriff einer
Methode umgesetzt werden.
Listing 209. Readonly Zugriff
$ cat readonly.cpp
#include<iostream>
using namespace std;
class Zahl
{
public:
Zahl(int i)
: privateZahl(i)
{
}
int lesen()
{
return privateZahl;
}
private:
int privateZahl;
};
int main()
{
Zahl X(18);
// X.privateZahl = 18;
// Fehler! Zugriff nicht möglich!
cout << "X.privateZahl= "<< X.lesen()<< endl;
// erlaubter lesender Zugriff über Methodenaufruf
}
231
Der direkte Zugriff auf private Elemente kann realisiert werden durch eine public constReferenz auf das private Element:
Listing 210. Zugriff aif private Elemente
$ cat readonly02.cpp
#include<iostream>
using namespace std;
class Zahl
{
public:
Zahl()
: readonlyZahl(privateZahl),
privateZahl(0)
{
}
void aendern(int wert)
{
privateZahl = wert;
}
const int& readonlyZahl;
Konstruktor
private:
int privateZahl;
};
// Initialisierung der Referenz
// Initialisierung der privaten Daten
// public-Referenz auf Konstante, Initialisierung im
int main()
{
Zahl X;
cout << "X.readonlyZahl="
<< X.readonlyZahl << endl;
// erlaubter direkter lesender Zugriff:
// 0
// X.privateZahl = 18;
// X.readonlyZahl = 18;
// Fehler! Zugriff nicht möglich!
// Fehler! Änderung nicht möglich!
X.aendern(18);
// erlaubte Änderung
cout << "X.readonlyZahl="
<< X.readonlyZahl << endl;
return 0;
// erlaubter direkter lesender Zugriff:
// 18
}
21.4 Typumwandlung Basisklasse – abgeleitete Klasse
Da eine abgeleitete Klasse quasi ein Subtyp ihrer Oberklasse ist, ist ein Objekt der
abgeleiteten Klasse zuweisungskompatibel zu einem Objekt der Oberklasse.
Beispiel:
232
Ort O1, O2, O3;
GraphObj g(O1);
Strecke s(O1, O2);
g = s;
s = g;
GraphObj &rg = g;
Strecke &rs = s
rg = rs;
GraphObj *pg = &g;
Strecke *ps = &s
pg = ps;
Wirkung: g.Referenzkoordinaten =
s.Referenzkoordinaten
Der Endpunkt der Strecke wird nicht kopiert, da er
in g nicht als Attribut vorhanden ist!
Nicht möglich!
Zuweisungen können auch mit Referenzen und
Zeigern vollzogen werden, mit der Wirkung wie
oben:
Wirkung:
g.Referenzkoordinaten = s.Referenzkoordinaten
21.5 Überschreiben von Funktionen
Jedes GrapObj hat eine Fläche, die durch die Methode Flaeche() ermittelt wird. Der
Flächeninhalt einzelner abgeleiteter Klassen, wie Rechteck oder Kreis ist aber
unterschiedlich.
Daher kann jede abgeleitete Klasse ihre eigene Methode Flaeche() definieren und so die
Methode Flaeche() der Oberklasse überschreiben.
Listing 211. Überschreiben von Funktionen
class Rechteck : public GraphObj // von GraphObj erben
{ public:
Rechteck(const Ort &p1, int h, int b)
: GraphObj(p1), hoehe(h), breite(b) {}
double Flaeche() const // int-Überlauf vermeiden
{
return double(hoehe) * breite;
}
private:
int hoehe, breite;
};
Überschriebene Methode
Soll eine Methode der Oberklasse verwendet werden, so ist der Bereichsoperator zu
verwenden, z.B:
Listing 212. Verwendung des Bereichsoperators
Rechteck r(Ort(0,0), 20, 50);
cout << r.Flaeche() << endl;
// 1000
cout << r.GraphObj::Flaeche() << endl; // 0
233
Abstrakte Datentypen
Hier werden die ADT’s Stapel und Schlange beschrieben und jeweils eine Implementierung
gezeigt.
Stapel (engl. stack) werden häufig verwendet, um geschachtelte Strukturen zu bearbeiten.
Die Reihenfolge der Bedienung ist prägnant durch die englische Bezeichnung LIFO (last in
first out) charakterisiert.
Demgegenüber ist eine Schlange (engl. queue) eine Struktur mit Verarbeitungssystematik
FIFO (first in first out). Verwendung finden solche Strukturen z.B. in Anwendungen, bei
denen Warteschlangen verarbeitet werden müssen.
21.6 Stack
Die grundlegenden Operationen für Stacks sind push und pop. Durch push wird ein
Datenelement auf dem Stack abgelegt, pop holt das zuletzt abgelegte Element und entfernt es
vom Stapel.
Die Position des obersten Elements (=kleinste Verweildauer) bezeichnen wir mit topPos, die
Position des Elementes mit der größten Verweildauer bezeichnen wir mit bottomPos.
Bild 97.
stack
push
Elementn
Elementn-1
…
Element1
pop
topPos
bottomPos
Soll das oberste Element nur gelesen werden, könnte dies durch die Folge pop und
anschließend push erfolgen. Einfacher ist es, eine zusätzliche Operation top dafür zu
definieren. Zusätzlich sollen Operationen zur Abfrage des Status des Stack existieren.
Definition der Operationen:
a = an , an-1 , … , a1
Sei der Stack. Dann sind die Operationen wie folgt definiert:
top() == an
a = an , an-1 , … , a1
push(b)
a = b, a n , a n-1 , … , a1
pop() == an
a = an-1 , … , a1
length() == n
a = an , an-1 , … , a1
isEmpty() == false (true wenn a leer)
a = an , an-1 , … , a1
isFull() == true, wenn kein weitere Platz mehr frei ist
234
Implementierung als Array
Ein Stack wird als Array implementiert, wobei wir hier von double als Basistyp ausgehen.
Die folgende Abbildung zeigt die unterschiedlichen Fälle, die bei der Realisierung zu
berücksichtigen sind und wie die Variable topPos verwendet werden soll:
Bild 98.
Stack als Array
topPos
m-1
m
m
m-1
m-1
topPos
1
i
1
1
0
0
0
topPos
Stack leer
Stack voll
Stack
teils gefüllt
Listing 213. Definition des ADT Stack.
$ cat Stack.h
// Klasse "Stack" als Array von double Werten
class CStack
{
// Stapel für Records vom Typ double
public:
CStack(unsigned max); // leerer Stapel für maximal max Records
~CStack();
// Destruktor
void push(double r);
// legt Record r oben auf Stapel
double pop();
// holt obersten Record vom Stapel
double top();
// liefert den Wert des obersten Records
unsigned length();
// Anzahl der Records im Stapel
bool isEmpty();
// true, wenn Stapel leer, sonst false
bool isFull();
// true, wenn Stapel voll, sonst false
private:
unsigned m;
// Arraylänge; m: max Anzahl Records
unsigned topPos;
// zeigt auf oberste Position im Stack
double *pa;
// Zeiger auf Array
};
235
Listing 214. Implementierung des ADT
$ cat Stack.cpp
// Elementfunktionen der Klasse "Stack": Implementation als Array
#include <iostream>
#include "Stack.h"
CStack::CStack(unsigned max) : topPos(0),m(max) // ctor: legt Speicher für max Records an
{
pa = new double[m];
}
CStack::~CStack()
{
delete pa;
}
void CStack::push(double r)
{
if (!isFull())
{
pa[topPos] = r;
topPos++;
}
}
double CStack::pop()
{
double r;
if (!isEmpty())
{
topPos--;
r = pa[topPos];
}
return r;
}
// oben im Stapel ablegen, Bedingung: Stapel nicht voll
// obersten Record holen und entfernen,
// Bedingung: Stapel nicht leer
// initialer Record
236
double CStack::top()
{
// Wert des obersten Records im nicht leeren Stapel holen
double r;
if (!isEmpty())
r = pa[topPos-1];
return r;
}
unsigned CStack::length()
{
return topPos;
}
bool CStack::isEmpty()
{
return topPos==0;
}
bool CStack::isFull()
{
return topPos==m;
}
$
// Anzahl der Records im Stapel
// prüfen, ob Stapel leer ist
// prüfen, ob Stapel voll ist
Zum Testen werden Werte eingelesen und ausgegeben:
237
Listing 215. Stack-Applikation
$ cat Stackapp.cpp
// Main-Funktion für Stack-Beispiele, Implementation als Array
// Eingabe von der Tastatur: maximal 20 Integers, Abschluss mit 9999.
// Diese Integers werden in dem Stack abgelegt, danach wird Inhalt des Stacks ausgegeben
#include <iostream>
#include "Stack.h"
#include "Stackdef.h"
int main()
{
double x;
int i;
CStack s(20);
// Stack-Objekt erzeugen
cout << "Stack als Array implementiert\n"
<< "=============================\n";
cout << endl << "Eingabe von Ganzzahlen, Abschluss mit 9999:" << endl;
while ((cin >> i) && !s.isFull() && i!=9999)
{
x = i;
s.push(x);
// auf Stack ablegen
}
cout << endl << endl << "Stackinhalt" << endl;
while (!s.isEmpty())
{
cout << s.pop() << endl; // vom Stack holen
}
return 0;
}
238
Das Schlüsselwort „this“
Es gibt Anwendungsfälle, in denen eine Memberfunktion auf das eigene Klassenobjekt als
Ganzes zugreifen möchte. Zumindest möchte man die Adresse des eigenen Objekts kennen.
Aus diesem Grund gibt es in C++ das reservierte Wort this. this ist als Zeiger auf das eigene
Objekt realisiert.
Ein Beispiel aus unserer Übung Binärbäume ist die Ausgabe des Wertes (val) des zu
löschenden Objektes im Konstruktor.
Listing 216. Beispiel für this
Tree::Node::~Node ()
{
cout << "Destruktor Node " << this->val<< endl;
}
Wollte man in unserem Übungsbeispiel den Destruktor von Node so umbauen, daß er auch
die Unterbäume eines Knotens löscht, so käme ebenfalls der this-Pointer zum Einsatz.
Listing 217. Weiteres Beispiel für this
Tree::Node::~Node ()
{
cout << "Destruktor Node" << this->val<< endl;
delete this->mLeftChild;
delete this->mRightChild;
}
In einem weiteren Beispiel führen wir in einer Klasse stack eine Funktion copy ein, die den
Inhalt des als Parameter übergebenen Stacks in unseren Stack hineinkopiert. Dazu wird der
dynamische Speicher unseres Stacks zunächst freigegeben und dann neu erzeugt. Damit ist
gewährleistet, dass unser neuer Stack mit dem übergebenen Stack übereinstimmt.
Dieser Algorithmus enthält allerdings eine Falle: Übergebe ich an diese Funktion copy
meinen eigenen Stack, so lösche ich durch die Speicherfreigabe meinen eigenen Inhalt. Wir
müssen daher dies unterbinden und fragen folglich zunächst ab, ob der eigene Stack
übergeben wurde. Wir brauchen dazu den Zeiger this. Betrachten wir das Programm:
Listing 218. Vermeidung der Kopie auf sich selbst
void stack::copy( const stack& st)
{
if (this != &st)
// keine Kopie auf sich!!!
{ delete bottom;
size = st.size;
bottom = top = new int [size];
for (int i=0; i<(st.top-st.bottom); i++)
*top++ = st.bottom[i];
// Inhalt kopieren
}
}
Liegt also nicht das eigene Element vor, so wird der dynamische Speicher des Stacks
gelöscht, neu erzeugt, die Variablen belegt und dann der Inhalt im dynamischen Speicher
zugewiesen. Wir merken uns, dass der Zeiger this immer als privates Element definiert ist.
Nur Member- oder Friendfunktionen dürfen daher diesen Zeiger verwenden. Natürlich
239
könnten wir auch this->size statt nur size schreiben, doch die letzte Schreibweise ist doch
einfacher und natürlicher.
21.7 Queues (Schlangen)
Schlangen verwalten Elemente in FIFO Manier.
enqueue
Elementn
Elementn-1
…
Element1
dequeue
rear
front
Definition der Operationen:
a = an , an-1 , … , a1
Sei die Schlange. Dann sind die Operationen definiert:
dequeue() = a1
nimmt Element, das am längsten wartet und entfernt es
enqueue(b)
stellt neues Element in die Schlange „hinten an“
a = an , an-1 , … , a2
a = b, a n , a n-1 , … , a1
front() = a1
Element, das am längsten in der Schlage ist
a = an-1 , … , a1
length() = n
a = an , an-1 , … , a1
isEmpty() = false (true wenn a leer)
a = an , an-1 , … , a1
isFull() = true, wenn kein weiterer Platz mehr frei ist.
Schlangen könnten analog zu Stacks als Array implementiert werden, gelesen wird von out,
geschrieben an in. Nach dem Lesen müssten dann alle Elemente nach vorne verschoben
werden, um nicht schnell an die Arraygrenzen zu stoßen.
240
Bild 99.
Queue
Konsument
Produzent
Queue
in
0
18
1
69
2
6
out
…
n-1
Der Produzent schreibt an der Position in.
Der Konsument liest von der Position out und reorganisert dann die Schlange.
Dieses Reorganisieren kostet Zeit und Speicherzugriffe. Dies kann vermieden werden, wenn
man den Zugriff zirkular organisiert.
Bild 100.
Zirkulare Queue
Konsument
out
in
m-1 0 1
Produzent
2
3
Queue
Die Implementierung des ADT Schlange als zirkulares Array ist mit der o.a. Bemerkung
analog zum Stack – wir werden eine Realisierung als zirkulär verkettete Liste betrachten.
Dabei hat ein Objekt der Klasse CQueue Komponenten, die zur Klasse CQueueNode
gehören; dies sind double-Werte, die über Zeiger verkettet sind. Hier werden wir sehen, dass
Klassen geschachtelt werden können.
Zunächst die Spezifikation der Klasse:
241
Listing 219. Klasse CQueue
$ cat Qulist.h
#ifndef QUEUE
#define QUEUE
class CQueue
//
Klasse "Queue" - als verkettete Liste
{
public:
CQueue();
// leere Schlange
~CQueue();
// gibt Schlange frei
void enqueue(double &r); // fügt Record r am Ende ein
double dequeue();
// holt am längsten wartenden Record
double front();
// Wert des am längsten wartenden Records
unsigned length();
// Anzahl Records in der Schlange
bool isEmpty();
// true, wenn Schlange leer ist
bool isFull();
// true, wenn Schlange voll ist
void printQueue();
// Warteschlange ausgeben
private:
class CQueueNode
{
// Knoten einer Liste
public:
CQueueNode(double r) // Konstruktor für Listenknoten
: dvalue(r),next(NULL)
{ // leerer Body} }
double dvalue;
// Knotenwert, Typ double
CQueueNode *next; // Zeiger auf nächsten Knoten
};
CQueueNode *q;
// Repräsentation der Schlange
// Zeiger auf letzten Knoten der Schlange
unsigned n;
// Anzahl Records in der Schlange
};
#endif
242
Die Idee der Implementierung des ADT Schlange ist es, eine zirkulär verkettet Liste zu
verwenden, wobei immer am Ende eingefügt wird, und das Ende wieder auf den Anfang
zeigt:
Bild 101.
Queue als verkettete Liste
q
a1
a2
…
an
rear
front
Der Konstruktor zum Erzeugen einer leeren Schlage muss lediglich die Anzahl der
Elemente n auf 0 setzen und den Zeiger q mit NULL initialisieren:
Listing 220. Queue-Konstruktor
CQueue::CQueue()
{
// Konstruktor: kreiert leere Schlange
q = NULL;
n = 0;
}
Die Methode isEmpty() fragt einfach, ob p auf ein Element zeigt oder nicht.
Listing 221. Queue isEmpty Methode
bool CQueue::isEmpty()
{
// prüfen, ob die Schlange leer ist
return q==NULL;
}
Eine Schlange, die als verkettete Liste realisiert ist, hat keine Größenbeschränkung, deshalb
liefert die Methode isFull() stets false zurück.
Die Methode enqueue() fügt ein neues Element am Ende ein. Auf das Ende zeigt das Attribut
q. Dabei ist zu unterscheiden, ob die Liste leer ist oder nicht.
Bild 102.
q
Einfügen in die leere Queue
NULL
n
0
enqueue(a1)
q
a1
p
n
1
243
Listing 222. Einfügen in leere Queue
void CQueue::enqueue(double &r)
{
// am Queueende einfügen
CQueueNode *p = new CQueueNode(r);
if (isEmpty()) p->next = p;
else
{
…
}
q = p;
n++;
}
Bild 103.
Einfügen in nicht leere Queue
enqueue(a2)
q
a1
a2
p
n
2
Listing 223. Einfügen in die nicht leere Queue
void CQueue::enqueue(double &r)
{
// am Queueende einfügen
CQueueNode *p = new CQueueNode(r);
if (isEmpty()) p->next = p;
else
{
p->next = q->next;
q->next = p;
}
q = p;
n++;
}
Die Methode dequeue() zum Entfernen eines Elementes muss unterscheiden, ob die Liste
nach der Entnahme eines Elementes leer ist oder nicht.
244
Bild 104.
Queue nach Operation leer
q
a1
n
1
p
dequeue()
q
n
NULL
0
Listing 224. Queue nach Operation leer
double CQueue::dequeue()
{
// holt am längsten wartenden Record
CQueueNode *p = q->next;
// erster Record
double r = p->dvalue;
if (p==q) q = NULL;
// Schlange nach Entnahme leer
else …
delete p;
n--;
return r;
}
Bild 105.
Queue nach Operation nicht leer:
double CQueue::dequeue()
q{
a1 wartenden Record a2
// holt am längsten
CQueueNode *p = q->next;
// erster Record
double r = p->dvalue;
// Schlange nach Entnahme leer
p if (p==q) q = NULL;
dequeue()
else q->next = p->next;
delete p;
n--;
return r;
q}
n
a
2
p
n
2
1
245
Der Destruktor gibt alle Elemente der Liste frei.
Listing 225. Destruktor der Queue
CQueue::~CQueue()
{
// gibt Schlange frei
if (isEmpty()) return;
if (n==1)
{
delete q;
q = NULL;
return;
}
CQueueNode *p = q;
// Zeiger auf ersten Record
while (n)
{
q = p->next;
delete p;
p = q;
n--;
}
q = NULL;
}
Damit lässt sich eine Testanwendung formulieren:
Listing 226. Testanwendung für die Queue
$ cat Qulapp.cpp
//
#include <iostream>
#include "Qulist.h"
Main-Funktion für Queues - als Liste
int main()
{
CQueue ws;
// ADT Schlange erzeuen
for (double d=1; d<=6; d++)
ws.enqueue(d);
// Element einfügen
ws.printQueue();
// Schlange ausgeben
cout << "Front Element: " << ws.front() << endl; // front Element
cout << "Entferntes Elemtent: " << ws.dequeue() << endl; // Element lesen
ws.printQueue();
// Schlange ausgeben
return 0;
}
$ Qulapp
1-ter Record: 1
2-ter Record: 2
…
5-ter Record: 5
6-ter Record: 6
Front Element: 6
Entferntes Elemtent: 1
1-ter Record: 2
2-ter Record: 3
3-ter Record: 4
4-ter Record: 5
5-ter Record: 6
246
Listing 227. Andere Queueklasse mit Array, Header
// CQueue.h
typedef int CMessage ;
typedef int Int32;
class CQueue
{
public:
CQueue(Int32 queuesize);
~CQueue();
bool add(const CMessage& msg); // add Message into the queue
bool get(CMessage& msg);
// get a Message from the queue
Int32 getNumOfMessages(void); // get the number of Messages
Int32 getNumOfByteNeeded(void); // get number of Bytes occupied
// by the queue-Object
private:
Int32
mSize;
// size of the queue
Int32
mCurrentSize; // current size
Int32
mHeadIndex; // first possible element for reading
Int32
mTailIndex; // position to insert
CMessage * mQueue;
};
Es folgt die Implementierung der Klasse in CQueue.cpp:
247
Listing 228. Andere Queueklasse mit Array, Implementierung
#include "CQueue.h"
CQueue::CQueue(Int32 queuesize)
: mSize(queuesize),
mCurrentSize(0),
mHeadIndex(0),
mTailIndex(0)
{
mQueue = new CMessage[queuesize];
}
CQueue::~CQueue(){ delete mQueue;}
bool CQueue::add(const CMessage& msg)
{
if (mCurrentSize == mSize)
{
return false;
}
mQueue[mTailIndex] = msg;
mTailIndex--;
if (mTailIndex < 0)
{
mTailIndex = mSize - 1;
}
mCurrentSize++;
return true;
}
bool CQueue::get(CMessage& msg)
{
if (mCurrentSize == 0)
{
return false; }
msg = mQueue[mHeadIndex];
mCurrentSize--;
mHeadIndex--;
if (mHeadIndex < 0)
{
mHeadIndex = mSize - 1;
}
return true;
// return true if msg is delivered
//just in case
//at this point we have whole control
//Ringbuffer!!
//get a Message from Q
//just in case
//get message
//Ringbuffer
}
Int32 CQueue::getNumOfMessages(void)
{
Int32 size;
size = mCurrentSize;
return size;
}
Int32 CQueue::getNumOfByteNeeded(void)
{
return (sizeof(CQueue)
+ sizeof(CMessage)*(mSize));
//ret number of events in queue
//get size of queue
}
248
22 Klassenspezifische Daten und Funktionen
Objekte beinhalten Daten und Methoden. In vielen Anwendungsfällen braucht man eine
Möglichkeit, Daten und Funktionen für alle Objekte, die zu einer Klasse gehören, verfügbar
zu haben.
Eine Möglichkeit wäre die Verwendung von globalen Objekten, die von allen Instanzen einer
Klasse (aber auch von Instanzen anderer Klassen) verwendet werden können. Dies ist kein
guter Weg, da er alle Nachteile der Verwendung globaler Variablen mit sich bringt.
Klassenspezifische Daten sind Daten, die nur einmal für alle Objekte der Klasse
existieren. Diese Daten sind dann gekapselt und können nur von den Objekten der Klasse
(von keinen anderen Klassen) gemeinsam verwendet werden. In C++ werden diese Daten mit
static gekennzeichnet.
Klassenspezifische Funktionen führen Aktionen aus, die an einer Klasse, nicht an einem
Objekt der Klasse gebunden sind. Klassenspezifische Funktionen werden ebenfalls mit static
gekennzeichnet. Jeder Konstruktor ist ein Beispiel für eine solche Funktion, hier entfällt
static.
Beispiel (Bankkonto)
Ein typisches Beispiel ist eine Klasse, die nummerierte Objekte realisiert. Z.B. kann ein
Bankkonto modelliert werden, dann braucht man einen Automatismus, der bei jeder
Kontoeröffnung eine neue Kontonummer erzeugt, bei Löschung des Kontos wird sie wieder
frei.
Im folgenden Beispiel ist der Mechanismus verdeutlicht:
Wir betrachten eine Klasse, die jedem Objekt eine eindeutige Seriennummer mitgibt und
objektunabhängig über die insgesamt erzeugten Objekte Buch führt.
Listing 229. Klassenspezifische Daten, Header
$ cat numobj.h
#ifndef numobj_h
#define numobj_h mstring_h
class nummeriertesObjekt {
public:
nummeriertesObjekt();
nummeriertesObjekt(const nummeriertesObjekt&);
~nummeriertesObjekt();
unsigned long Seriennummer() const { return SerienNr;}
static int Anzahl() { return anzahl;}
static bool Testmodus;
private:
static int anzahl;
static unsigned long maxNummer;
const unsigned long SerienNr;
};
#endif
klassenspezifisch
klassenspezifisch
// Ende von numobj.h
Die Methode Seriennummer ist objektspezifisch, sie gibt die dem Objekt zugeordnete
Seriennummer (SerienNr) zurück. Die Methode Anzahl ist klassenspezifisch, die gibt die
insgesamt aktiven Objektinstanzen zurück.
Die Variable Testmodus ist dazu da, die Erzeugung und Löschung von Objekten zu
visualisieren.
249
Listing 230. Klassenspezifische Daten, Implementierung
$ cat numobj.cpp
#include<iostream>
#include"numobj.h"
#include<cassert>
using namespace std;
// Initialisierung der klassenspezifischen Variablen:
int
nummeriertesObjekt::anzahl = 0;
unsigned long nummeriertesObjekt::maxNummer = 0L;
bool
nummeriertesObjekt::Testmodus = false;
// Default-Konstruktor
nummeriertesObjekt::nummeriertesObjekt()
: SerienNr(++maxNummer)
{
++anzahl;
if (Testmodus)
{
if (SerienNr == 1)
cout << "Start der Objekterzeugung!\n";
cout << " Objekt Nr. "
<< SerienNr << " erzeugt" << endl;;
}
}
Die Initialisierung der static
Variablen darf nicht innerhalb
eines Konstruktors erfolgen, da
ansonsten bei jeder Objekterzeugung die Initialisierung
erfolgen würde.
Der Standardkonstruktor initialisiert die
objektspezifische
Konstante
SerienNr und inkrementiert die
klassenspezifische
Variable
maxNummer.
Der Standard-Kopierkonstruktor kann nicht verwendet werden, es darf hier keine Kopie
erstellen, ansonsten hätten zwei Objekte dieselbe Seriennummer, also muss ein eigener
Kopierkonstruktor implementiert werden.
Listing 231. Spezieller Kopierkonstruktor
// Kopierkonstruktor
nummeriertesObjekt::nummeriertesObjekt(const nummeriertesObjekt &X)
: SerienNr(++maxNummer)
{
++anzahl;
if (Testmodus)
cout << " Objekt Nr. " << SerienNr
<< " mit Nr. " << X.Seriennummer()
<< " initialisiert" << endl;;
}
Der Destruktor muss die Anzahl der aktiven Objekte dekrementieren. Er wird aufgerufen,
implizit beim Verlassen eines Blocks, in dem ein Objekt erzeugt wurde und explizit durch
Verwendung von delete.
250
Listing 232. Destruktor
// Destruktor
nummeriertesObjekt::~nummeriertesObjekt()
{
anzahl--;
if (Testmodus)
{
cout << " Objekt Nr. "
<< SerienNr << " gelöscht" << endl;
if (anzahl == 0)
cout << "letztes Objekt gelöscht!" << endl;
if (anzahl < 0) // deshalb int und nicht unsignet int
cout << " FEHLER! zu oft delete aufgerufen!"
<< endl;;
}
else assert(anzahl >= 0);
} // Ende von numobj.cpp
Die Verwendung der Klasse wird in folgendem Hauptprogramm demonstriert.
251
Listing 233. Testmain
$ cat nummain.cpp
int main()
{
// Testmodus für alle Objekte der Klasse einschalten
nummeriertesObjekt::Testmodus=true;
Zugriff auf klassenspez. Variable
nummeriertesObjekt dasNumObjekt_X;
// ... wird erzeugt
cout << "Die Seriennummer von dasNumObjekt_X ist: "
<< dasNumObjekt_X.Seriennummer() << endl;
Objekterzeugung
// Anfang eines neuen Blocks
{ nummeriertesObjekt dasNumObjekt_Y; // ... wird erzeugt
// objektgebundener Aufruf:
cout << dasNumObjekt_Y.Anzahl()
<< " Objekte aktiv" << endl;
schlechter Stil
(objektbezogen)
// *p wird dynamisch erzeugt:
nummeriertesObjekt *p = new nummeriertesObjekt;
// objektgebundener Aufruf über Zeiger:
cout << p->Anzahl()
<< " Objekte aktiv" << endl;
delete p;
// *p wird gelöscht
// klassenbezogener Aufruf:
cout << nummeriertesObjekt::Anzahl()
<< " Objekte aktiv" << endl;
}
schlechter Stil (objektbezogen)
guter Stil (klassenbezogen)
// Blockende: dasNumObjekt_Y wird gelöscht
252
cout << " Kopierkonstruktor: " << endl;
nummeriertesObjekt dasNumObjekt_X1 = dasNumObjekt_X;
Kopierkonstruktor
cout << "Die Seriennummer von dasNumObjekt_X ist: "
<< dasNumObjekt_X.Seriennummer() << endl;
cout << "Die Seriennummer von dasNumObjekt_X1 ist: "
<< dasNumObjekt_X1.Seriennummer() << endl;
}
// dasNumObjekt_X wird gelöscht
return 0;
Will man im Hauptprogramm eine Zuweisung von Objekten durchführen, die zur Klasse
numeriertesObjekt gehören, so wird der Compiler einen Fehler melden:
Listing 234. Fehlertest
$ cat numobj02/nummain.cpp
….
cout << " Kopierkonstruktor: " << endl;
nummeriertesObjekt dasNumObjekt_X1 = dasNumObjekt_X;
cout << "Die Seriennummer von dasNumObjekt_X ist: "
<< dasNumObjekt_X.Seriennummer() << endl;
cout << "Die Seriennummer von dasNumObjekt_X1 ist: "
<< dasNumObjekt_X1.Seriennummer() << endl;
// Zuweisung wird wegen const SerienNr vom Compiler verboten
dasNumObjekt_X1 = dasNumObjekt_X; // Fehler
}
// dasNumObjekt\_X wird gelöscht
g++ -c nummain.cpp
nummain.cpp: In member function `nummeriertesObjekt&
nummeriertesObjekt::operator=(const nummeriertesObjekt&)':
nummain.cpp:47: non-static const member `const long unsigned int
nummeriertesObjekt::SerienNr', can't use default assignment operator
Die Zuweisung würde bewirken, dass dem privaten Attribut des Objektes (const SerienNr),
eine Konstante, ein Wert zugewiesen würde. Einer Konstanten kann aber nichts zugewiesen
werden, also kann der Zuweisungsoperator nicht erzeugt werden.
22.1 Klassenspezifische Konstanten
Im letzten Beispiel ist gezeigt, dass klassenspezifische Variable außerhalb der
Klassendefinition initialisiert werden müssen. Klassenspezifische Konstanten für
Aufzählungen und einfache Typen können innerhalb der Klasse definiert und initialisiert
werden.
253
Listing 235. Klassenspezifische Konstanten
class bsp
{
enum RGB { rot = 1, gelb = 2, blau = 4};
static const int max = 1000;
static int messwerte[max];
…
static kann entfallen, es sind immer
klassenspezifische Konstanten
Verwendung von klassensp. Konstante zur Definition einer
klassensp. Variable.
}
254
23 Polymorphismus
Eine der wichtigsten Eigenschaften, die C++ zu einem wirklich objektorientierten System
machen, ist die Fähigkeit zum Polymorphismus (Vielgestaltigkeit).
Polymorphismus heißt, dass dieselben Methode eines Objektes in verschiedener Klassen
verschiedene Aktionen auslösen können.
Eine Form des Polymorphismus haben wir beim Überladen von Funktionsnamen und
Operatoren kennen gelernt. Diese Methodik wird frühe Bindung (early binding) genannt, da
schon beim Compilieren feststeht, welche der vorhandenen Implementationen von
Funktionen und Operatoren benützt wird.
In C++ wird noch eine andere, wesentlich leistungsfähigere Art von Polymorphismus
unterstützt. Sie wird mit "später Bindung" (late binding) bezeichnet und vom Konzept der
virtuellen Funktionen getragen. Erst zur Laufzeit des Programms (abhängig von der
Interaktion des Benutzers mit dem Programm) wird entschieden, welcher Programmcode
eigentlich ausgeführt wird.
23.1 Virtuelle Funktionen
Soll erst zur Laufzeit entschieden werden, welches Objekt verwendet werden soll und
demzufolge welche Methode aufgerufen werden soll, so sind virtuelle Funktionen der
Basisklasse zu überladen.
Die Deklaration einer Funktion als virtual bewirkt, dass den Objekten die Information über
den Objekttyp indirekt über einen Zeiger mitgegeben wird. Zu einem Objekt gehören zwei
(verstecktes) Attribute
vptr, ein Zeiger auf die Tabelle vtbl und
vtbl, ein Array von Zeigern auf virtuelle Funktionen.
Wird eine virtuelle Funktion zur Laufzeit über einen Zeiger (oder eine Referenz) aufgerufen,
wird über den Zeiger vprt in der Tabelle vtbl die auszuführende Funktion gesucht und
aufgerufen.
Besitzt das Objekt keine Funktion, die zu der Signatur passt, wird eine Funktion mit
passender Signatur in der Oberklasse gesucht.
23.1.1 Verhalten nicht virtueller Funktionen
Betrachten wir folgendes Programm, bei dem ein Zeiger auf ein GraphObj verwendet wird:
255
Listing 236. Polymorphismus, main
$ cat polymorphismus/main.cpp
#include"rechteck.h"
using namespace std;
int main()
{
GraphObj objGraphobj(Ort(20,20));
Rechteck objRechteck(Ort(100,100), 20, 50);
GraphObj *ptrGraphObj;
// Zeiger auf ein GraphObj
ptrGraphObj = &objGraphobj;
// Zeiger auf objGraphobj setzen
cout << "objGraphobj->Flaeche(): " << ptrGraphObj->Flaeche() << endl; // 0
ptrGraphObj = &objRechteck;
// Zeiger auf objRechteck setzen
cout << "objGraphobj->Flaeche(): " << ptrGraphObj->Flaeche() << endl; // 0
cout << "objRechteck.Flaeche(): " << objRechteck.Flaeche() << endl; // 1000
}
Bei der zweiten Ausgabe wird 0 ausgegeben, da der Zeiger als Zeiger auf GraphObj definiert
ist und somit die Information, dass er auf ein Rechteck zeigt, fehlt. Somit wird die Methode
Flaeche der Klasse GraphObj aufgerufen und nicht die der Klasse Rechteck. Dabei wird das
anonyme Subobjekt vom Typ GraphOb , das bei der Erzeugung des Rechtecks angelegt
wurde, angesprochen und dessen Methode Flaeche verwendet.
23.1.2 Verhalten virtueller Funktionen
Bei der Verwendung virtueller Funktionen wird das Laufzeitsystem dem Objekt, auf das der
Zeiger zeigt, die erforderliche Typinformation mitgeben. Damit wird dann die „richtige“
Methode ausgewählt.
256
Listing 237. Klasse mit virtueller Funktion
class GraphObj
{
// Version 1
public:
…
// Standardimplementation:
double Flaeche() const {return 0.0;}
virtual double v_Flaeche() const {return 0.0;}
private:
Ort Referenzkoordinaten;
};
Listing 238. abgeleitete Klasse mit virtueller Funktion
class Rechteck : public GraphObj // von GraphObj erben
{ public:
…
virtual double v_Flaeche() const
{
// int-Überlauf vermeiden
return double(hoehe) * breite;
}
private:
int hoehe, breite;
};
Listing 239. main für Klasse mit virtueller Funktion
#include"rechteck.h"
using namespace std;
int main() {
GraphObj objGraphobj(Ort(20,20));
Rechteck objRechteck(Ort(100,100), 20, 50);
GraphObj *ptrGraphObj;
// Zeiger auf ein GraphObj
ptrGraphObj = &objGraphobj;
// Zeiger auf objGraphobj setzen
cout << "objGraphobj->v_Flaeche(): " << ptrGraphObj->v_Flaeche() << endl; // 0
ptrGraphObj = &objRechteck;
// Zeiger auf objRechteck setzen
cout << "objGraphobj->v_Flaeche(): " << ptrGraphObj->v_Flaeche() << endl; // 1000
cout << "objRechteck.v_Flaeche(): " << objRechteck.v_Flaeche() << endl;
// 1000
}
$ main02
objGraphobj->v_Flaeche(): 0
objGraphobj->v_Flaeche(): 1000
objRechteck.v_Flaeche(): 1000
$
Virtuelle Funktionen sind automatisch auch in nachfolgend abgeleiteten Klassen virtuell.
Man sollte der besseren Lesbarkeit wegen aber stets den Modifizierer virtual voranstellen!
257
Achtung:
Alle eine virtuelle Funktion überladenen Funktionen müssen dieselbe Signatur besitzen.
Ansonsten wird nicht erkannt, dass die Funktion in einer abgeleiteten Klasse virtuell ist und
es wird der Vererbungskette nach oben folgend eine zur Signatur passende Funktion gesucht.
Wenn Überschreiben notwendig ist, sollte die Funktion in der Basisklasse als virtuell
gekennzeichnet werden, da nur dann das Verhalten eines Aufrufs unabhängig davon ist, ob es
über einen Zeiger oder über einen Objektnamen erfolgt.
Welche Ausgabe erzeugt folgendes Programm?
Listing 240. Beispielprogramm
class Basisklasse
{
public:
virtual void vf1() { cout << "BK: vf1" << endl; };
virtual void vf2() { cout << "BK: vf2" << endl; };
virtual void vf3() { cout << "BK: vf3" << endl; };
void f() { cout << "BK: f" << endl; };
};
class AbgeleiteteKlasse : public Basisklasse {
public:
void vf1() { cout << "AK: vf1" << endl; };
void vf2(int i) { cout << "AK: vf2" << endl; };
// char void vf3() { cout << "AK: vf3" << endl; }; // Fehler:
falscher rTyp
void f() { cout << "AK: f" << endl; };
};
int main() {
AbgeleiteteKlasse d;
Basisklasse *bp = &d;
bp->vf1();
bp->vf2();
d.vf2(6);
bp->f();
}
258
24 Abstrakte Klassen
Bei der Realisierung großer Softwarepakete wird oft eine Menge von allgemeinen
Basisklassen definiert, die über die Lebensdauer des Produktes (weitgehend) unverändert
bleiben soll. Diese Klassen enthalten oft keine Implementierung. Sie dienen ausschließlich
als Basis zur Ableitung anderer Klassen, die dann die allgemeinen Attribute und Methoden
erben. Von diesen abgeleiteten Klassen können dann Objekte erzeugt werden.
Solche allgemeinen Klassen heißen „abstrakte“ Klassen. Die von abstrakten abgeleiteten
Klassen, die selbst nicht abstrakt sind, nennt man konkrete Klassen.
In C++ werden abstrakte Klassen dadurch gebildet, dass sie mindestens eine „rein virtuelle“
Methode (engl. pure virtual) enthalten. Eine rein virtuelle Methode hat normalerweise keinen
Implementierungsteil (kann aber einen haben).
Eine rein virtuelle Funktion wird gekennzeichnet durch Ergänzung um „=0“.
Virtual int rein_virtuell_funktion (int) = 0;
Dadurch ist sicher gestellt, dass stets die zum Objekttyp passende Methode der Hierarchie
aufgerufen wird.
Von einer abstrakten Klasse können keine Objekte instantiiert werden.
Wenn eine Klasse von einer abstrakten Klasse abgeleitet wird und in der abgeleiteten Klasse
die Implementierung fehlt, so ist die abgeleitete Klasse selbst abstrakt.
Beispiel (Klasse Graphobj)
Wir wollen das Beispiel so erweitern, dass GraphObj eine abstrakte Klasse wird, indem
die Methode Flache() eine rein virtuelle Methode wird. Außerdem erweitern wir die Klasse
um eine rein virtuelle Methode zeichnen(), die für jede abgeleitete Klasse überschrieben
werden muss, da z.B. Rechtecke anders gezeichnet werden müssen als Kreise.
259
Listing 241. Graphobjekt als abstrakte Klasse, main
$ cat abstrakt/main.cpp
#include"strecke.h"
#include"quadrat.h" // schliesst rechteck.h ein
using namespace std;
int main()
{
// GraphObj G; // Fehler: Instanzen abstrakter Klassen gibt es nicht.
Rechteck R(Ort(0,0), 20, 50);
Strecke S(Ort(1,20), Ort(200,0));
Quadrat Q(Ort(122, 99), 88);
// Feld mit Basisklassenzeigern, initialisiert mit den
// Adressen der Objekte und NULL als Endekennung
GraphObj* GraphObjZeiger[] = {&R, &S, &Q, NULL};
// Ausgabe der Fläche aller Objekte
int i=0;
while (GraphObjZeiger[i])
cout << "Fläche = " << GraphObjZeiger[i++]->Flaeche() << endl;
Durch Polymorphie
wird immer die
richtige Methode
ausgewählt
// Zeichnen aller Objekte
i=0;
while (GraphObjZeiger[i])
GraphObjZeiger[i++]->zeichnen();
std::cout << "Auch Referenzen sind polymorph:\n";
GraphObj &R_Ref = R, // Der statische Typ ist derselbe,
&S_Ref = S,
&Q_Ref = Q;
R_Ref.zeichnen(); // der dynamische nicht,
S_Ref.zeichnen(); // d.h. immer andere Methode
Q_Ref.zeichnen(); // für zeichnen()
return 0;
}
$ main
Fläche = 1000
Fläche = 0
Fläche = 7744
Zeichnen: Rechteck (h x b = 20 x 50) an der Stelle
(0, 0)
Zeichnen: Strecke von (1, 20) bis (200, 0)
Zeichnen: Quadrat (Seitenlaenge = 88) an der Stelle
(122, 99)
Auch Referenzen sind polymorph:
Zeichnen: Rechteck (h x b = 20 x 50) an der Stelle
(0, 0)
Zeichnen: Strecke von (1, 20) bis (200, 0)
Zeichnen: Quadrat (Seitenlaenge = 88) an der Stelle
(122, 99)
260
Listing 242. Graphobjekt als abstrakte Klasse, Deklaration
$ cat abstrakt/graphobj.cpp
#ifndef graphobj_h
#define graphobj_h graphobj_h
#include "ort.h"
#include<iostream>
class GraphObj
{
// Version 2
public:
GraphObj(const Ort &einOrt)
// allg. Konstruktor
: Referenzkoordinaten(einOrt) {}
virtual ~GraphObj() {}
// virtueller Destruktor, Erklärung später
// Bezugspunkt ermitteln
const Ort& Bezugspunkt() const { return Referenzkoordinaten;}
// alten Bezugspunkt ermitteln und gleichzeitig neuen wählen
Ort Bezugspunkt(const Ort &nO)
{
Ort temp = Referenzkoordinaten;
Referenzkoordinaten = nO;
return temp;
}
// Koordinatenabfrage
int X() const { return Referenzkoordinaten.X(); }
int Y() const { return Referenzkoordinaten.Y(); }
// rein virtuelle Methoden
virtual double Flaeche() const = 0;
virtual void zeichnen() const = 0;
private:
Ort Referenzkoordinaten;
};
// Standardimplementierung einer rein virtuellen Methode
inline void GraphObj::zeichnen() const
{
cout << "Zeichnen: ";
}
/* Die Entfernung zwischen 2 GraphObj-Objekten ist hier als
Entfernung ihrer Bezugspunkte (überladene Funktion) definiert.*/
inline double Entfernung(const GraphObj &g1, const GraphObj &g2)
{
return Entfernung(g1.Bezugspunkt(), g2.Bezugspunkt());
}
#endif // graphobj_h
261
Die Klassen Strecke und Rechteck müssen die rein virtuellen Methoden implementieren, da
sie sonst selbst abstrakt wären und somit keine Instanzen erzeugbar wären.
Listing 243. Graphobjekt als abstrakte Klasse, Implementierung
$ cat abstrakt/strecke.h
#ifndef strecke_h
#define strecke_h strecke_h
#include"graphobj.h"
class Strecke : public GraphObj
{// erben von GraphObj
public:
// Initialisierung mit Initialisierungsliste
Strecke(const Ort &Ort1, const Ort &Ort2)
: GraphObj(Ort1),
// Initialisierung des Subobjekts
Endpunkt(Ort2)
// Initialisierung des Attributs
{}
// leerer Code-Block
double Laenge() const
{
return Entfernung(Bezugspunkt(), Endpunkt);
}
virtual double Flaeche() const // Definition der virtuellen Methoden
{
return 0.0;
}
virtual void zeichnen() const
{
Verwendung der Methode zeichnen der
GraphObj::zeichnen();
abstrakten Basisklasse
cout << "Strecke von ";
anzeigen(Bezugspunkt());
cout << " bis ";
anzeigen(Endpunkt);
cout << std::endl;
}
private:
Ort Endpunkt;
// zusätzlich: 2. Punkt der Strecke
// (der erste ist GraphObj::Referenzkoordinaten)
};
#endif // strecke_h
262
$ cat abstrakt/rechteck.h
#ifndef rechteck_h
#define rechteck_h rechteck_h
#include"graphobj.h"
class Rechteck : public GraphObj // von GraphObj erben
{ public:
Rechteck(const Ort &p1, int h, int b)
: GraphObj(p1), hoehe(h), breite(b) {}
// wird von Quadrat benötigt
int Hoehe() const {return hoehe;}
int Breite() const {return breite;}
// Definition der rein virtuellen Methoden
virtual double Flaeche() const
{
// int-Overflow vermeiden
return double(hoehe) * breite;
}
virtual void zeichnen() const {
GraphObj::zeichnen();
cout << "Rechteck (h x b = "
<< hoehe << " x "
<< breite
<< ") an der Stelle ";
anzeigen(Bezugspunkt());
cout << endl;
}
private:
int hoehe, breite;
};
#endif // rechteck_h
263
Listing 244. abgeleitete Klasse Quadrat
$ cat abstrakt/quadrat.h
#ifndef quadrat_h
#define quadrat_h quadrat_h
#include"rechteck.h"
class Quadrat : public Rechteck
{
public:
Quadrat(const Ort &O, int seite)
: Rechteck(O, seite, seite) {}
// Definition der rein virtuellen Methoden
// Bezugspunkt(), Flaeche(), Hoehe() werden geerbt
virtual void zeichnen() const
{
GraphObj::zeichnen();
std::cout << "Quadrat (Seitenlaenge = " << Hoehe()
<< ") an der Stelle ";
anzeigen(Bezugspunkt());
std::cout << std::endl;
}
};
#endif // quadrat.h
24.1 Virtuelle Destruktoren
Destruktoren sind für die Freigabe von belegtem Speicher zuständig.
Wird ein Objekt mit p = new Klasse(); angelegt, so kann es mit delete p gelöscht werden.
Was passiert, wenn ein Zeiger vom Typ „Zeiger auf Basisklasse“, der auf ein Objekt einer
abgeleiteten Klasse zeigt verwendet wird?
Bild 106.
Virtuelle Destruktoren
Basis
int bWert
Basis(int)
~Basis()
Abgeleitet
double aWert
Abgeleitet(int,double)
~ Abgeleitet ()
264
Listing 245. Test Destruktoren
$ cat virtuelleDestruktoren/dest.cpp
#include<iostream>
using namespace std;
#define PRINT(X) cout << (#X) << " = " << (X) << endl
class Basis
{
private:
int bWert;
public:
Basis(int b=0): bWert(b) {}
~Basis()
{
cout << "Objekt " << bWert << " Basis-Destruktor aufgerufen!\n";
}
};
class Abgeleitet : public Basis
{
private:
double aWert;
public:
Abgeleitet(int b = 0, double a = 0.0) : Basis(b), aWert(a) {}
~Abgeleitet()
{
cout <<"Objekt " << aWert << " Abgeleitet-Destruktor aufgerufen!\n";
}
};
265
int main ()
{
Basis *pb = new Basis(1);
Basis
int bWert: 1
pb
sizeof liefert die statisch aus dem
Typ des Zeigers ermittelbare Größe
(int=4).
Basis(int)
~Basis()
sizeof(*pb)
= 4
PRINT(sizeof(*pb));
266
Abgeleitet *pa = new Abgeleitet(2, 2.2);
Abgeleitet
Basis
int bWert: 2
pab
Basis(int)
~Basis()
double aWert: 2.2
sizeof liefert die
statisch aus dem Typ
des Zeigers
ermittelbare Größe .
Abgeleitet(int,
double)
sizeof(*pa)
= 16
PRINT(sizeof(*pa));
267
Basis *pba = new Abgeleitet(3, 3.3);
Abgeleitet
Basis
int bWert: 3
pba
Basis(int)
~Basis()
double aWert: 3.3
Abgeleitet(int,dou
ble)
sizeof(*pba) =
4
PRINT(sizeof(*pba));
268
cout << "pb löschen:\n";
delete pb;
pb
pa
Abgeleitet
Basis
int bWert: 3
pba
Basis(int)
~Basis()
double aWert: 3.3
Abgeleitet(int,double)
~ Abgeleitet ()
pb löschen:
Objekt 1 Basis-Destruktor aufgerufen!
pa löschen:
Objekt 2.2 Abgeleitet-Destruktor
aufgerufen!
cout << "pa löschen:\n";
delete pa;
269
… Basis *pba …
pa
Abgeleitet
pb
pba
Da der Destruktor
nicht virtual ist, bleibt
der Speicherplatz
nach delete stehen,
nur der Speicher
gelöscht wird, der
zum Typ des Zeigers
passt.
double aWert: 3.3
Abgeleitet(int,double)
~ Abgeleitet ()
pba löschen:
Objekt 3 Basis-Destruktor
aufgerufen!
cout << "pba löschen:\n";
delete pba;
}
$
270
Ist der Destruktor virtual, liefert sizeof die dynamisch ermittelte Größe des Objektes.
Weiterhin wird mit delete der komplette Speicher gelöscht:
Listing 246. Objektgröße bei virtuellem Destruktor
$ cat virtuelleDestruktoren/virtdest.cpp
…
class Basis {
private:
int bWert;
public:
Basis(int b=0)
: bWert(b) {}
virtual ~Basis() {
cout << "Objekt " << bWert << " Basis-Destruktor aufgerufen!\n";
}
};
…
$ virtdest
sizeof(*pb) = 8
sizeof(*pa) = 16
sizeof(*pba) = 8
pb löschen:
Objekt 1 Basis-Destruktor aufgerufen!
pa löschen:
Objekt 2.2 Abgeleitet-Destruktor aufgerufen!
Objekt 2 Basis-Destruktor aufgerufen!
pba löschen:
Objekt 3.3 Abgeleitet-Destruktor aufgerufen!
Objekt 3 Basis-Destruktor aufgerufen!
Regel:
Virtuelle Destruktoren sollten immer verwendet werden, wenn Basisklassenzeiger auf
dynamisch erzeugte Objekte benutzt werden. Dies ist meistens der Fall bei virtuellen
Methoden.
24.2 Virtuelle Basisklassen
Soll bei Mehrfachvererbung nur ein Basisklassenobjekt erzeugt werden, sind virtuelle
Basisklassen zu verwenden. Von einer virtuellen Basisklasse wird nur ein Subobjekt
erzeugt, auf das über verschiedene Vererbungspfade zugegriffen werden kann.
Da Mehrfachvererbung nicht empfohlen wird, sparen wir uns die weiteren Ausführungen.
271
25 Anhang: Kleine Einführung, wie Applikationen mit
Win-MFC verbunden werden (Visual Studio 2005)
Nehmen wir an, wir haben ein einfaches Konsolenprogramm, für das wir jetzt eine
graphische Oberfläche brauchen. Das Programm kann einen int-Wert inkrementieren,
dekrementieren und ausgeben. Die -.h und die -.cpp sind hier gezeigt:
Listing 247. Simple_Graph.h
Simple_Graph….h
class TestClass
{
public:
TestClass();
~TestClass();
void Add();
void Sub();
int Ausgabe();
private:
int mWert;
};
Listing 248. Simple_Graph.cpp
Simple_Graph…..cpp:
#include "Simple_Graph_SS_07.h"
TestClass::TestClass():mWert(10)
{};
TestClass::~TestClass(){};
void TestClass::Add()
{
mWert++; };
void TestClass::Sub()
{
mWert--;};
int TestClass::Ausgabe()
{
return mWert;};
272
Nun wollen wir eine Oberfläche erzeugen.
Wir öffnen ein neues Projekt als MFC-Applikation und geben einen Namen
273
Wählen Sie Dialog-basiert
274
Es soll minimiert werden können und maximiert starten.
275
weiter
276
Fertig
277
Wir kopieren unsere beiden Dateien in das Arbeitsverzeichnis der graphischen Applikation:
278
Anschließend fügen wir die beiden existierenden Dateien dem Projekt hinzu (rechte
Maustaste auf Projekt)
Sollte es eine Fehlermeldung missing precompiled Header geben, so kann man in den
Projekteigenschaften unter C++ die Verwendung von precompiled Header ausschalten.
Jetzt müsste das Projekt bereits bauen.
279
Nun generieren wir drei Buttons und ein Edit-Fenster
Dazu gehen wir in den Resource-View und klicken im Dialog-Ordner den
Simple_Graphics….Dlg –Eintrag an.
280
Nun wählen wir uns aus der Toolbox die Buttons und das
Edit-Fenster und ziehen es in die Oberfläche.
281
Durch einmaliges Anklicken der Knöpfe mit der rechten Maustaste öffnet sich ein
Eigenschaftsfenster des Knopfes, in dem der Name (Caption) und die ID geändert werden
können.
Wir benennen die Knöpfe mit add, sub und ausgabe, ebenso die die IDs.
.
282
Dem Edit-Feld fügen wir nach Anklicken mit der rechten Maustaste eine Variable hinzu:
283
Wir wählen als Kategorie Wert, als Typ int und geben ihr den Namen mVal.
284
Nun gehen wir in die Dateien und inkludieren in der
Simple_Graphics_...Dlg.h
unsere h.-Datei.
Listing 249. Simple_Graphics_Dlg.h
// Simple_Graphics_SS_07Dlg.h : header file
//
#pragma once
// CSimple_Graphics_SS_07Dlg dialog
#include "Simple_Graph_SS_07.h"
und fügen am Ende ein Objekt unserer Klasse hinzu (private:).
public:
afx_msg void OnBnClickedButton1();
public:
int mVal;
private:
TestClass myClass;
};
Damit hat die Dialogklasse ein Objekt unserer Klasse und wir können auf unsere Methoden
zugreifen.
Wir gehen wieder in den Ressource-View und klicken jeweils die Knöpfe zweimal an. Es
öffnen sich Funktionen, die aufgerufen werden, wenn einer der Knöpfe gedrückt wird.
Hier schreiben wir unsere Aufrufe hinein.
Listing 250. Eigene Methoden
void CSimple_Graphics_SS_07Dlg::OnBnClickedadd()
{
// TODO: Add your control notification handler code here
myClass.Add();
}
void CSimple_Graphics_SS_07Dlg::OnBnClickedSub()
{
// TODO: Add your control notification handler code here
myClass.Sub();
}
void CSimple_Graphics_SS_07Dlg::OnBnClickedausgabe()
{
// TODO: Add your control notification handler code here
mVal = myClass.Ausgabe();
UpdateData(FALSE);
}
In der Ausgabefunktion fügen wir ein updateData(False) ein, mit dem wir die Ausgabe im
Edit-Fenster erreichen.
285
Ein Test des Programmes zeigt, dass uns die Verbindung zwischen unseren Funktionen und
der Grafik gelungen ist.
Achtung: Cout und printf sind Konsolenausgaben, die hier nicht aufs display gehen.
286
26 Anhang: Kleine Einführung, wie Applikationen mit
Win-MFC verbunden werden (Visual Express)
To do
287
Herunterladen