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 09.09.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 20.1 Überblick............................................................................................................... 216 20.2 Die Klasse Ort ....................................................................................................... 220 20.3 Zugriffsschutz........................................................................................................ 228 20.4 Typumwandlung Basisklasse – abgeleitete Klasse ............................................... 231 20.5 Überschreiben von Funktionen ............................................................................. 232 20.6 Stack ...................................................................................................................... 233 20.7 Queues (Schlangen)............................................................................................... 239 21 Klassenspezifische Daten und Funktionen................................................................ 248 21.1 Klassenspezifische Konstanten ............................................................................. 252 22 Polymorphismus........................................................................................................ 254 5 22.1 Virtuelle Funktionen ............................................................................................. 254 22.1.1 Verhalten nicht virtueller Funktionen ........................................................... 254 22.1.2 Verhalten virtueller Funktionen .................................................................... 255 23 Abstrakte Klassen...................................................................................................... 258 23.1 Virtuelle Destruktoren........................................................................................... 263 23.2 Virtuelle Basisklassen ........................................................................................... 270 24 Anhang: Kleine Einführung, wie Applikationen mit Win-MFC verbunden werden (Visual Studio 2005) ............................................................................................................. 271 25 Anhang: Kleine Einführung, wie Applikationen mit Win-MFC verbunden werden (Visual Express) .................................................................................................................... 286 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................................................................................................................... 233 Stack als Array .................................................................................................. 234 8 Bild 99. Bild 100. Bild 101. Bild 102. Bild 103. Bild 104. Bild 105. Bild 106. Queue ............................................................................................................... 240 Zirkulare Queue................................................................................................. 240 Queue als verkettete Liste ................................................................................. 242 Einfügen in die leere Queue .............................................................................. 242 Einfügen in nicht leere Queue ........................................................................... 243 Queue nach Operation leer ................................................................................ 244 Queue nach Operation nicht leer:...................................................................... 244 Virtuelle Destruktoren....................................................................................... 263 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 .............................................................. 218 Die Klasse Ort ............................................................................................... 221 Main zur Verwendung der Klasse Ort........................................................... 222 Die Klasse GraphObj .................................................................................... 223 Main zur Verwendung der Klasse GraphObj................................................ 224 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...................................................................................... 225 Die Klasse Strecke ........................................................................................ 225 Graphik-main ................................................................................................ 226 Subobjekt der Oberklasse.............................................................................. 228 Kodebeispiel public und private:................................................................... 229 Readonly Zugriff ........................................................................................... 230 Zugriff aif private Elemente .......................................................................... 231 Überschreiben von Funktionen ..................................................................... 232 Verwendung des Bereichsoperators .............................................................. 232 Definition des ADT Stack. ............................................................................ 234 Implementierung des ADT............................................................................ 235 Stack-Applikation.......................................................................................... 237 Beispiel für this ............................................................................................. 238 Weiteres Beispiel für this .............................................................................. 238 Vermeidung der Kopie auf sich selbst .......................................................... 238 Klasse CQueue .............................................................................................. 241 Queue-Konstruktor........................................................................................ 242 Queue isEmpty Methode............................................................................... 242 Einfügen in leere Queue................................................................................ 243 Einfügen in die nicht leere Queue ................................................................. 243 Queue nach Operation leer ............................................................................ 244 Destruktor der Queue .................................................................................... 245 Testanwendung für die Queue....................................................................... 245 Andere Queueklasse mit Array, Header........................................................ 246 Andere Queueklasse mit Array, Implementierung........................................ 247 Klassenspezifische Daten, Header ................................................................ 248 Klassenspezifische Daten, Implementierung ................................................ 249 Spezieller Kopierkonstruktor ........................................................................ 249 Destruktor...................................................................................................... 250 Testmain ........................................................................................................ 251 Fehlertest ....................................................................................................... 252 Klassenspezifische Konstanten ..................................................................... 253 Polymorphismus, main.................................................................................. 255 Klasse mit virtueller Funktion....................................................................... 256 abgeleitete Klasse mit virtueller Funktion .................................................... 256 main für Klasse mit virtueller Funktion ........................................................ 256 Beispielprogramm ......................................................................................... 257 Graphobjekt als abstrakte Klasse, main ........................................................ 259 Graphobjekt als abstrakte Klasse, Deklaration ............................................. 260 Graphobjekt als abstrakte Klasse, Implementierung..................................... 261 abgeleitete Klasse Quadrat ............................................................................ 263 Test Destruktoren ......................................................................................... 264 Objektgröße bei virtuellem Destruktor ......................................................... 270 Simple_Graph.h............................................................................................. 271 Simple_Graph.cpp......................................................................................... 271 Simple_Graphics_Dlg.h ................................................................................ 284 Eigene Methoden........................................................................................... 284 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 Vererbung Diese Teil befasst sich mit einem wichtigen Bestandteil Objektorientierter Sprachen: der Vererbung von Eigenschaften und damit verbundenen Konzepten. 20.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() 216 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() 217 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: 218 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. 219 Der Vererbungsmechanismus wird am Beispiel einer Klasse GraphObj („graphisches Objekt“) und davon abgeleiteten Klassen für Rechtecke, Linien und Dreiecke demonstriert. 20.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 220 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: 221 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. 222 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: 223 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() 224 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: 225 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 ; } 226 $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 227 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. 20.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 228 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! } 229 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 } 230 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 } 20.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: 231 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 20.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 232 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. 20.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 233 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 }; 234 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 235 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: 236 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; } 237 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 238 könnten wir auch this->size statt nur size schreiben, doch die letzte Schreibweise ist doch einfacher und natürlicher. 20.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. 239 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: 240 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 241 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 242 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. 243 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 244 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 245 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: 246 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 } 247 21 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. 248 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. 249 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. 250 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 251 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. 21.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. 252 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. } 253 22 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. 22.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. 22.1.1 Verhalten nicht virtueller Funktionen Betrachten wir folgendes Programm, bei dem ein Zeiger auf ein GraphObj verwendet wird: 254 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. 22.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. 255 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! 256 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(); } 257 23 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. 258 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) 259 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 260 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 261 $ 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 262 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 23.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 () 263 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"; } }; 264 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)); 265 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)); 266 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)); 267 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; 268 … 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; } $ 269 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. 23.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. 270 24 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;}; 271 Nun wollen wir eine Oberfläche erzeugen. Wir öffnen ein neues Projekt als MFC-Applikation und geben einen Namen 272 Wählen Sie Dialog-basiert 273 Es soll minimiert werden können und maximiert starten. 274 weiter 275 Fertig 276 Wir kopieren unsere beiden Dateien in das Arbeitsverzeichnis der graphischen Applikation: 277 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. 278 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. 279 Nun wählen wir uns aus der Toolbox die Buttons und das Edit-Fenster und ziehen es in die Oberfläche. 280 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. . 281 Dem Edit-Feld fügen wir nach Anklicken mit der rechten Maustaste eine Variable hinzu: 282 Wir wählen als Kategorie Wert, als Typ int und geben ihr den Namen mVal. 283 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. 284 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. 285 25 Anhang: Kleine Einführung, wie Applikationen mit Win-MFC verbunden werden (Visual Express) To do 286