C/C++
Dirk Louis
C/C++ new reference
Markt+Technik Verlag
Die Deutsche Bibliothek CIP-Einheitsaufnahme Ein Titeldatensatz für diese Publikation ist bei Der Deutschen Bibliothek erhältlich.
Die Informationen in diesem Produkt werden ohne Rücksicht auf einen eventuellen Patentschutz veröffentlicht. Warennamen werden ohne Gewährleistung der freien Verwendbarkeit benutzt. Bei der Zusammenstellung von Texten und Abbildungen wurde mit größter Sorgfalt vorgegangen. Trotzdem können Fehler nicht vollständig ausgeschlossen werden. Verlag, Herausgeber und Autoren können für fehlerhafte Angaben und deren Folgen weder eine juristische Verantwortung noch irgendeine Haftung übernehmen. Für Verbesserungsvorschläge und Hinweise auf Fehler sind Verlag und Herausgeber dankbar. Alle Rechte vorbehalten, auch die der fotomechanischen Wiedergabe und der Speicherung in elektronischen Medien. Die gewerbliche Nutzung der in diesem Produkt gezeigten Modelle und Arbeiten ist nicht zulässig. Fast alle Hardware- und Software-Bezeichnungen, die in diesem Buch erwähnt werden, sind gleichzeitig auch eingetragene Warenzeichen oder sollten als solche betrachtet werden. Umwelthinweis: Dieses Buch wurde auf chlorfrei gebleichtem Papier gedruckt. Die Einschrumpffolie zum Schutz vor Verschmutzung ist aus umweltverträglichem und recyclingfähigem PE-Material.
10 9 8 7 6 5 4 3 2 1 04 03 02 01
ISBN 3-8272-6121-X © 2001 by Markt+Technik Verlag, ein Imprint der Pearson Education Deutschland GmbH. Martin-Kollar-Straße 1012, D81829 München/Germany Alle Rechte vorbehalten Einbandgestaltung: Helfer Grafik Design, München Lektorat: Erik Franz,
[email protected] Herstellung: Anja Zygalakis,
[email protected] Satz: reemers publishing services gmbh, Krefeld (www.reemers.de) Druck und Verarbeitung: Media-Print, Paderborn Printed in Germany
Inhaltsverzeichnis
Inhaltsverzeichnis Übersicht . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
13
Grundlagen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
14
Die Programmiersprachen C und C++. . . . . . . . . . . . . . . . . . . . . . . . . . . . Leistungsmerkmale . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Der ANSI-Standard . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Schreibkonventionen in C/C++ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Aufbau eines C/C++-Programms . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Typische Komponenten von C/C++-Programmen . . . . . . . . . . . . . . . . . . Größere Programme und Modularisierung . . . . . . . . . . . . . . . . . . . . . . . . Objektorientiertes Programmieren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Verwendung der Standardbibliotheken . . . . . . . . . . . . . . . . . . . . . . . . . . Ein- und Ausgabe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Programmfluß . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Programmerstellung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
14 15 15 17 17 20 26 28 29 31 33 34
Sprachkonzepte und Syntax . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
41
Elemente der Sprache . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Zeichensatz . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Trigraphsequenzen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Schlüsselwörter und Symbole . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Eigene Bezeichner: Definition und Deklaration . . . . . . . . . . . . . . . . . . . . Variablen und Datentypen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Variablen und Datentypen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die elementaren Datentypen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Speicherbelegung der Integer-Typen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Speicherbelegung der Gleitkommatypen . . . . . . . . . . . . . . . . . . . . . . . . . Vorzeichen integraler Datenypen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
41 41 43 43 46 48 48 50 51 52 53
5
Inhaltsverzeichnis
Variablendeklaration . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Gültigkeitsbereiche . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Namensbereiche . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Sichtbarkeit und Verdeckung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Lebensdauer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Zusammengesetzte Datentypen. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Zusammengesetzte Datentypen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Aufzählungstypen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Arrays . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Mehrdimensionale Arrays . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Zeichenketten (Strings) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Strukturen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Bitfelder . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Unions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Zeiger und Referenzen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Zeiger . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Spezielle Zeiger . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Referenzen (nur C++) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Typdefinitonen und Typumwandlungen . . . . . . . . . . . . . . . . . . . . . . . . . . Typdefinitionen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Standardkonvertierungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Typumwandlung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Konstanten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Konstanten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Literale . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . #define – symbolische Konstanten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . const – konstante Variablen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Operatoren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Operatoren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Vorzeichen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Arithmetische Operatoren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . In- und Dekrementoperator . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Zuweisungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Vergleichende Operatoren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Logische Operatoren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Datenzugriff . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Bitweise Operatoren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Streamoperatoren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die Operatoren new und delete . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Typumwandlung und -identifizierung . . . . . . . . . . . . . . . . . . . . . . . . . . . Sonstige Operatoren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Synonyme . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Priorität und Assoziativität . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Überladung von Operatoren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Programmsteuerung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Programmsteuerung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
6
54 58 59 63 64 65 65 66 67 70 72 74 77 79 81 81 84 87 89 89 90 92 94 94 94 96 98 99 99 101 102 103 104 106 106 109 113 116 117 119 125 128 128 129 132 132
Inhaltsverzeichnis
Die if-Bedingung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die if-else-Verzweigung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . if-else-Ketten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . switch-Verzweigung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Schleifen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die for-Schleife . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die while-Schleife . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die do-while-Schleife . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Abbruchbefehle für Schleifen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Funktionen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Funktionen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Funktionsdeklaration und -definition . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die Funktion main() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Datenaustausch zwischen Funktionen . . . . . . . . . . . . . . . . . . . . . . . . . . . Der Rückgabewert . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Parameter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Funktionsargumente und Vorgabe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Funktionen und der Stack . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Funktionen mit beliebig vielen Argumenten . . . . . . . . . . . . . . . . . . . . . . . Inline-Funktionen (nur C++) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Spezifizierer für Funktionen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Überladung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Überladung von Funktionen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Überladung von Methoden . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Überladen, Überschreiben, Verdecken . . . . . . . . . . . . . . . . . . . . . . . . . . . Klassen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Klassen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Klassen und OOP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Kapselung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Datenelemente . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Statische Datenelemente . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Konstante Datenelemente . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Klasseninstanzen als Datenelemente . . . . . . . . . . . . . . . . . . . . . . . . . . . . Lokale und verschachtelte Klassen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Methoden . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Statische Methoden . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Konstante Methoden . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Zugriffsspezifizierer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Zugriff innerhalb der Klasse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Zugriff von außerhalb der Klasse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Zugriff auf Basisklassenelemente . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Der Konstruktor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Der Kopierkonstruktor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Der Destruktor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Standardmethoden . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Der this-Zeiger . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
132 133 134 135 137 138 139 141 142 143 143 145 147 149 150 150 153 154 157 158 159 160 162 163 163 164 164 166 167 168 169 170 170 172 173 174 175 176 178 179 181 183 184 186 187 187
7
Inhaltsverzeichnis
Instanzbildung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Vererbung und Polymorphie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Vererbung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Vererbung versus Einbettung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Zugriffsbeschränkung bei der Vererbung . . . . . . . . . . . . . . . . . . . . . . . . . Zugriffsrechte für einzelne Elemente auflockern . . . . . . . . . . . . . . . . . . . . Vererbung und Konstruktor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Nicht vererbbare Methoden . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Polymorphie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Überschreibung und virtuelle Methoden . . . . . . . . . . . . . . . . . . . . . . . . . Vererbung und Destruktor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Abstrakte Methoden und Klassen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Basisklassenzeiger . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . RTTI – Laufzeittypidentifizierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Mehrfachvererbung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Virtuelle Basisklassen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Templates . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Templates . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Funktionentemplates . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Klassentemplates . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Methoden in Klassentemplates . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Instanziierung und Spezialisierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Implizite Instanziierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Explizite Instanziierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Explizite Spezialisierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Der Präprozessor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Quellcode einfügen und ersetzen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Makros definieren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Argumente in Makros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Direktiven zur bedingten Kompilierung . . . . . . . . . . . . . . . . . . . . . . . . . . Zeichenkettenbildung und Grundsymbolverbindung . . . . . . . . . . . . . . . . Sonstige Direktiven und Symbole . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Exception-Behandlung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Exception-Behandlung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Exceptions auslösen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Exceptions abfangen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Handler-Bereich festlegen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Verwandte Funktionen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Zeichenketten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . C-Strings . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die Klasse string . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Wide characters und Multibyte-Zeichen . . . . . . . . . . . . . . . . . . . . . . . . . Ein- und Ausgabe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Formatierte und unformatierte E/A . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Streams in C . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Streams in C++ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Pufferung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
8
188 189 189 191 192 194 194 197 197 199 201 201 202 203 204 205 207 207 209 210 211 212 213 215 216 217 219 220 221 222 224 225 226 226 230 232 233 233 235 235 237 238 239 239 240 242 246
Inhaltsverzeichnis
Die C-Standardbibliothek . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 247 Die Header-Dateien . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . assert.h . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ctype.h . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . errno.h . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . float.h . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ios646.h . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . limits.h . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . locale.h . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . math.h . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . setjmp.h . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . signal.h . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . stdarg.h . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . stddef.h . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . stdio.h . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . stdlib.h . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . string.h . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . time.h . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . wchar.h . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . wctype.h . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die Funktionen der C-Standardbibliothek . . . . . . . . . . . . . . . . . . . . . . . .
247 247 248 248 249 250 251 251 252 253 254 255 255 255 257 259 260 262 263 264
Die C++-Standardbibliothek . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 329 Übersicht über die Standardbibliothek . . . . . . . . . . . . . . . . . . . . . . . . . . . Die Header-Dateien . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . algorithm . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . bitset . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . complex . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . deque . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . exception . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . fstream . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . functional . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . iomanip . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . iosfwd . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . iostream . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . istream . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . iterator . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . limits . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . list . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . locale . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . map . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . memory . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . new . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . numeric . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ostream . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
329 330 330 332 333 334 335 335 336 338 338 340 340 341 342 343 344 344 346 347 347 348 349
9
Inhaltsverzeichnis
queue . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . set . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . sstream . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . stack . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . stdexcept . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . streambuf . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . string . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . typeinfo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . utility . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . valarray . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . vector . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die Container-Klassen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Allgemeine Eigenschaften . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . bitset . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . deque . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . list . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . map und multimap . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . priority_queue . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . queue . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . set und multiset . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . stack . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . vector . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die Iteratoren-Klassen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Allgemeines . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . reverse_iterator . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Insert-Iteratoren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Stream-Iteratoren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Unterstützung eigener Implementierungen . . . . . . . . . . . . . . . . . . . . . . . . Die Algorithmen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die Stream-Klassen. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Übersicht . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ios_base . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . basic_ios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . basic_streambuf . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . basic_istream . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . basic_ostream . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . basic_iostream . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . basic_stringbuf . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . basic_istringstream . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . basic_ostringstream . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . basic_stringstream . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . basic_filebuf . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . basic_ifstream . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . basic_ofstream . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . basic_fstream . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die String-Klassen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
10
349 350 351 351 352 352 353 354 354 354 356 357 357 360 362 365 367 372 373 375 378 379 382 382 384 385 387 390 392 400 400 402 404 405 407 409 411 411 413 414 415 415 417 418 419 420
Inhaltsverzeichnis
Übersicht . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . char_traits . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . basic_string . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die Klassen zur lokalen Einstellung. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Übersicht . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . locale . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die Hilfsklassen für die einzelnen Kategorien . . . . . . . . . . . . . . . . . . . . . . Die numerischen Klassen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . complex . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . valarray . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Teilmengen von valarray-Objekten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . numeric_limits . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Klassen und Funktionen zur Exception-Behandlung . . . . . . . . . . . . . . . . . exception . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Abgeleitete Exception-Klassen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Funktionen zur Exception-Behandlung . . . . . . . . . . . . . . . . . . . . . . . . . . Laufzeittypidentifizierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . type_info . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Utilities . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die Struktur pair . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Funktionsobjekte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Speicherallokation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . allocator . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . auto_ptr . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Funktionen und Iteratoren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
420 420 421 425 425 427 428 443 443 444 447 452 453 453 454 456 458 458 459 459 459 464 464 465 466
Index . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 467
11
C/C++ new reference Übersicht Grundlagen. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
14
Sprachkonzepte und Syntax . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
41
Die C-Standardbibliothek. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
247
Die C++-Standardbibliothek . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
329
13
Grundlagen
Grundlagen Die Programmiersprachen C und C++ Im Gegensatz zu anderen Hochsprachen wie Pascal oder Basic, die ursprünglich als reine Lehrsprachen konzipiert wurden, verdankt C seine Entstehung dem Umstand, daß man bei Bell Laboratories eine höhere Programmiersprache für die Implementierung eines neu entwickelten Betriebssystems suchte. Bis dato programmierte man das gesamte Betriebssystem, das später als UNIX bekannt wurde, nämlich in Assembler. Um sich die Arbeit zu erleichtern, bastelte man an einer eigenen Programmiersprache, die möglichst effizient, gut zu portieren und leicht zu compilieren sein sollte. Brian W. Kernighan und Dennis M. Ritchie waren es, die diese Bemühungen zum Erfolg führten und gleichzeitig eine der leistungsfähigsten Programmiersprachen überhaupt schufen: die Programmiersprache C. In der Folgezeit fand C schnell weite Verbreitung und wurde aufgrund seiner Qualitäten und trotz der Schwierigkeiten, denen sich gerade Anfänger bei der Erlernung dieser Sprache gegenüber sehen, immer beliebter. Mit der Zeit wandelten sich die Anforderungen an eine professionelle Programmiersprache. Neben der Erzeugung schneller und kompakter Codes trat die Wartbarkeit und Wiederverwertung bestehender Codes immer weiter in den Vordergrund, und damit auch das Interesse an objektorientierten Konzepten. Mitte der Achtziger begann Bjarne Stroustrup C um objektorientierte Konzepte zu erweitern. Die Betonung liegt dabei auf dem Wort »Erweiterung«, denn die Kompatibilität zu C war bei der Entwicklung der neuen Programmiersprache ein vordringliches Designmerkmal. Eine der ersten Stationen auf dem Weg zur neuen Programmiersprache wurde daher noch als »C mit Klassen« bezeichnet. 1985 kam dann eine Version heraus, die bereits zurecht als objektorientierte Programmiersprache gelten kann, und die unter dem Namen C++ bekannt wurde. Mittlerweile ist C++ voll ausgereift und ebenso erfolgreich wie C. Ein Grund hierfür ist sicherlich die (nahezu) vollständige Abwärtskompatibilität zu C, die dem C-Programmierer den Einstieg in die objektorientierte Programmierung wesentlich erleichtert und die Verwertbarkeit bereits existierenden C-Codes garantiert.
14
Leistungsmerkmale
Im Gegensatz zu rein objektorientierten Sprachen wie Modula oder Smalltalk besteht damit allerdings auch die Gefahr, C++ lediglich als weniger strikte C-Version einzusetzen. Programmieren in C++ ist daher nicht automatisch mit objektorientierter Programmierung gleichzusetzen.
Leistungsmerkmale C wurde ursprünglich als High-Level-Sprache zur Systemprogrammierung, namentlich der Entwicklung des Betriebssystems UNIX, konzipiert. Insofern war C nur ein untergeordnetes Hilfsmittel, und C-Programme sollten einfach zu kompilieren und schnell sein. Das Ergebnis war eine Sprache, die sich durch ●
ihren geringen Umfang an Sprachelementen,
●
die schnelle Laufzeit ihrer Programme,
●
die Unterstützung modularer Programmierung und
●
ihrer guten Portabilität bei gleichzeitiger Systemnähe
auszeichnete. Heutzutage sieht die Situation so aus, daß Rechenzeit immer billiger und der Programmieraufwand immer kostspieliger wird. Ziel der objektorientierten Programmierung mit C++ ist es daher, Quelltext zu schaffen, der sich durch ●
einfache und sichere Verwendung,
●
hohe Wiederverwertbarkeit,
●
einfache Wartbarkeit,
●
gute Verständlichkeit und Lesbarkeit
auszeichnet.
Der ANSI-Standard Der Erfolg einer Programmiersprache hängt nicht nur von der Sprache selbst, sondern auch von der Verfügbarkeit passender Compiler (oder Interpreter) und unterstützender Bibliotheken ab. Letztere waren für die Verbreitung von C sogar von besonderer Bedeutung, da es in C für viele zentralen Aufgaben (Ein- und Ausgabe, Stringbearbeitung, dynamische Speicherverwaltung) keine in der Sprache verankerte Elemente gibt. Die Abhängigkeit von Standardbibliotheken bedeutet aber auch zusätzliche Abhängigkeit von den Compiler-Entwicklern. Um diesem Einfluß entgegenzuwirken und zu verhindern, daß jeder C-Compiler-Entwickler seinen eigenen C-Dialekt definiert, trat im Jahre 1983 im American National Standard Institute (ANSI) ein Komitee aus
15
Grundlagen
Compiler-Herstellern, Software-Entwicklern und Hardware-Produzenten zusammen, das einen Standard für die Programmiersprache C erarbeiten sollte. Der 1989 ratifizierte Standard (Referenznummer ISO/IEC 9899:1990) definiert sowohl Syntax und Semantik der Sprache wie auch die Zusammensetzung der Standardbibliothek. Programmierer, die sich an die vom Standard vorgegebenen Regeln halten und keine plattformspezifischen Funktionen verwenden (beispielsweise Programmierung mit DOS-Interrupts), können dank des ANSI-Standards sicher sein, daß sie ihr Programm von jedem ANSI-kompatiblen Compiler erstellen lassen können. Dies bedeutet aber auch, daß man ein Programm auf jedes beliebige System (Computer/ Betriebssystem) portieren kann, sofern es nur einen passenden ANSI-kompatiblen Compiler für dieses System gibt. 1995 wurde die C-Standardbibliothek um eine Sammlung von Funktionen zur Programmierung mit sogenannten Wide Characters – Zeichen, die durch 16- und 32Bit-Zeichencodes codiert werden – ergänzt. Bei den meisten dieser Funktionen, die in den neuen Headern wchar.h und wctype.h deklariert sind, handelt es sich um Adaptionen bestehender C-Funktionen an den Zeichentyp wchar_t. Die Erweiterungen sind im Amendment ISO/IEC 9899:1990/Amd.1:1995(E) zusammengefaßt. Auch für C++ gibt es mittlerweile einen ANSI-Standard. Die wichtigsten Neuerungen gegenüber dem bis dato geltenden Quasi-Standard von Bjarne Stroustrup dürften in der Einführung von Namensbereichen als frei definierbare globale Gültigkeitsbereiche und der Standardisierung der C++-Laufzeitbibliothek zu sehen sein. Die Ausführungen und Beschreibungen in diesem Buch richten sich nach dem neuen C++-Standard. Im Praxisteil dieses Buches finden Sie viele Abschnitte, die zeigen, wie man die Klassen der C++-Laufzeitbibliothek bei der täglichen Programmierarbeit nutzen kann. Obwohl die Abwärtskompatibilität zu C bei der Entwicklung von C++ höchste Priorität hatte, wurden einige wenige der von C übernommenen Konzepte und Elemente in ihrer Semantik geändert (siehe Anhang). Sollte sich ein C-Programm nach der Kompilation mit einem C++-Compiler anders als zuvor verhalten, sollte man prüfen, ob das Programm von diesen Änderungen betroffen ist.
Bezugsquellen C- und C++-Standard können Sie online beim ANSI-Institut oder IHS (Information Handling Service) anfordern: www.ansi.org www.ihs.de
Kopien der Standards können auch aus der Schweiz ISO/IEC Copyright Office Case Postale 56 CH-1211 Genève 20 Schweiz
16
Schreibkonventionen in C/C++
oder aus München bezogen werden: IHS Information Handling Services Tel. 089 / 89526999 Den abschließenden C++-Draft kann man sich derzeit noch aus dem Internet herunterladen: ftp://ftp.maths.warwick.ac.uk/pub/c++/std/cd2/
Schreibkonventionen in C/C++ Bei der Festlegung der Namen von Bezeichnern, das sind Variablen-, Funktions-, Klassen-, Typen- und Makronamen, dürfen folgende Zeichen benutzt werden: ●
alle Buchstaben des englischen Alphabets (a bis z, A bis Z)
●
die Ziffern 0 bis 9
●
der Unterstrich »_«
●
●
●
Der Name eines Bezeichners muß mit einem Buchstaben oder dem Unterstrich beginnen. Die Verwendung einer Ziffer als ersten Buchstaben eines Bezeichners ist nicht erlaubt. Die deutschen Umlaute und andere Sonderzeichen dürfen also nur in Kommentaren und in Zeichenketten vorkommen. Die maximale Länge der Bezeichner ist nicht vorgeschrieben, jedoch müssen nach ANSI C mindestens die ersten 31 Zeichen zur Unterscheidung herangezogen werden.
Warnung Die Sprache C unterscheidet zwischen Groß- und Kleinschreibung. Die Bezeichner var1 und Var1 können also für zwei verschiedene Variablen benutzt werden.
Aufbau eines C/C++-Programms #include <stdio.h> int main() { /* Anweisungen */ return 0; }
17
Grundlagen
Beschreibung C/C++-Programme bestehen aus einer Ansammlung von Deklarationen und Definitionen von Datentypen, Variablen, Funktionen und Klassen (sowie einigen speziellen Direktiven an den Präprozessor). Anweisungen, die festlegen, was ein Programm macht, findet man nur innerhalb von Funktionsdefinitionen (in C++ auch in den Methoden (Elementfunktionen) der Klassen). Jedes C/C++-Programm muß eine main()-Funktion definieren, mit deren Ausführung das Programm beginnt.
Anwendung Im einfachsten Fall besteht ein C/C++-Programm aus einer oder mehreren include-Direktiven, die den Zugriff auf bestimmte Funktionen und Klassen der C/C++-Standardbibliotheken ermöglichen, sowie
●
der obligatorischen Funktion main(), in der alle Anweisungen zur Ausführung des Programms stehen,
●
aus Variablen, die deklariert werden, um Werte zu repräsentieren und zwischenzuspeichern,
●
aus Anweisungen, in denen die Werte in den Variablen bearbeitet werden.
●
Beispiel Um ein einfaches Programm aufzusetzen, das zwei Zahlen multipliziert und das Produkt der beiden Zahlen ausgibt, könnte man folgendermaßen vorgehen: Um zwei ganzzahlige Werte im Programm verwalten zu können, deklariert man zwei Variablen des Datentyps int (Datentyp für ganzzahlige Werte). Für das Ergebnis kann man eine dritte Variable definieren. int main() { int zahl1, zahl2; int ergebnis;
Mit Hilfe des Zuweisungsoperators kann man den Variablen Werte zuweisen: int main() { ... zahl1 = 120; zahl2 = 4;
/* Werte zuweisen */
Zur Multiplikation von int-Werten verwendet man den *-Operator. Das Ergebnis der Berechnung weist man direkt der dritten Variablen zu. ergebnis = zahl1 * zahl2;
18
Aufbau eines C/C++-Programms
Um Werte auszugeben, kann man sich einer speziellen Funktion namens printf() bedienen, die Teil der C-Laufzeitbibliothek ist. printf("Das Produkt der Zahlen ist : %d\n", ergebnis);
Bevor man eine Funktion aufruft, muß die Funktion allerdings dem Compiler per Deklaration bekanntgemacht worden sein. Die Deklarationen für die Funktionen der Laufzeitbibliothek sind in einer Reihe von Header-Dateien zusammengefaßt. Nachdem man sich erkundigt hat, in welcher Header-Datei die gewünschte Funktion deklariert ist, bindet man die Header-Datei mittels einer #include-Anweisung am Anfang des Programms ein. #include <stdio.h>
Warnung Leser, die mit C/C++ für Windows programmieren wollen, seien darauf hingewiesen, daß Windows-Programme statt der main()-Funktion die Funktion WinMain() verwenden. Je nach verwendetem Compiler und Klassenbibliothek kann WinMain() selbst wieder durch eine andere Funktion (beispielsweise OwlMain() für Borland C++ mit OWL) oder eine Klasseninstanz (beispielsweise ein globales Objekt der MFCKlasse CWinApp in Visual C++) ersetzt sein. Konsolenprogramme, die unter Windows ausgeführt werden (sprich im Fenster der MS-DOS-Eingabeaufforderung ablaufen und über keine eigene Windows-Oberfläche verfügen), verwenden aber wie reine DOS- oder UNIX-Programme die main()Funktion.
Beispiel Das folgende Programm kann nach der Kompilation von der Konsole (unter Windows die MS-DOS-Eingabeaufforderung) Ihres Betriebssystems aus aufgerufen und ausgeführt werden. #include <stdio.h> int main() { int zahl1, zahl2; int ergebnis;
/* Faktoren */ /* Produkt */
zahl1 = 120; zahl2 = 4; ergebnis = zahl1 * zahl2;
/* Werte zuweisen */ /* Wert berechnen */
printf("Das Produkt der Zahlen ist : %d\n", ergebnis); return 0; }
19
Grundlagen
Verweise Siehe nachfolgende Abschnitte für weitere einführende Informationen zum Aufbau von C/C++-Programmen. Siehe Kapitel zur Sprache für detaillierte Informationen zu Variablen, Datentypen, Operatoren, Funktionen.
Typische Komponenten von C/C++-Programmen /* Das erste C-Programm */ #include <stdio.h>
Kommentar Präprozessor-Direktive
Funktion
Anweisungs -block
int main() { printf("Hello World\n"); return 0; }
Anweisungen
Typische Komponenten eines C/C++-Programms
Beschreibung Die Funktionalität eines Programmes wird bestimmt durch die Daten, die es verarbeitet, und die Operationen, die es auf den Daten ausführt. Insofern könnte man meinen, ein Programm bestünde nur aus Variablendefinitionen und Anweisungen. Dem ist aber nicht so. Der Compiler unterscheidet noch ganz andere Elemente.
20
Typische Komponenten von C/C++-Programmen
Aus Sicht des Compilers besteht ein Programm aus: Definitionen Deklarationen Anweisungen Präprozessor-Direktiven Kommentaren
Definitionen und Deklarationen Bei Ausführung des Programms müssen der Code und die Daten des Programms in den Arbeitsspeicher des Computers geladen werden. Welcher Code und welche Daten zu einem Programm gehören, legen Sie beim Aufsetzen des Quelltextes selbst fest. Der Code des Programms besteht aus den in Maschinencode übersetzten Anweisungen aus dem Quelltext. In C/C++ dürfen Anweisungen nur in Funktionsdefinitionen stehen. Indem Sie eine Funktion definieren, beispielsweise die main()-Funktion des Programms, teilen Sie dem Compiler mit, daß er für die Funktion Speicher reservieren und dafür Sorge tragen soll, daß der Code der Funktion bei Aufruf des Programms an die entsprechende Stelle im Speicher geladen wird. Für die Verwaltung der Daten definieren Sie Variablen. Hinter den Variablen stehen Speicherbereiche im RAM, die der Compiler für Sie reserviert. In den Variablen können Sie die Daten Ihres Programms speichern. Durch eine Definition führt man neue Elemente (Funktionen, Variablen, Datentypen) in ein Programm ein. Gleichzeitig wird das Element im Zuge der Definition mit einem Bezeichner verbunden (Variablenname, Funktionsname, etc.), der im Programmquelltext verwendet wird, um auf das Element zuzugreifen. Da mit der Definition von Funktionen und Variablen die Bereitstellung von Speicherbereichen einhergeht, müssen diese Elemente eindeutig sein, d. h., sie dürfen nur einmal im ganzen Programm definiert sein (sonst gäbe es Bezeichner, die mit mehreren Speicherbereichen assoziert wären). Andererseits fordert der Compiler, daß alle Elemente, die man verwendet, ihm vorab bekanntgemacht werden müssen. Bevor man eine Variable verwendet, muß man dem Compiler mitteilen, welchen Datentyp die Variable hat, bevor man eine Funktion aufrufen kann, muß man dem Compiler mitteilen, welche Parameter die Funktion übernimmt und was für einen Rückgabetyp sie zurückliefert. Die Bekanntmachung der Elemente bezeichnet man als Deklaration. Grundsätzlich ist jede Definition auch eine Deklaration. Es gibt aber auch die Möglichkeit, Elemente zu deklarieren, ohne daß man die Elemente neu deklariert. Für Variablen stellt man dabei der »Definition« das Schlüsselwort extern voran, für Funktionen gibt man nur den Funktionskopf (Rückgabetyp, Name, Parameterliste) ohne Anweisungsteil an.
21
Grundlagen
Verweise Siehe Elemente der Sprache.
Variablen und Konstanten Feste Werte, die sich im Laufe des Programms nicht ändern oder nur einmal benötigt werden, kann man direkt in Form von Literalen in den Quelltext schreiben. Für veränderliche Werte definiert man Variablen. Der Compiler weist der Variablen einen Speicherbereich zu, in den man im Laufe des Programms verschiedene Werte abspeichern kann. Eine Variable kann aber immer nur Werte eines Datentyps aufnehmen (mit Ausnahme der unions). Dies liegt daran, daß unterschiedliche Daten (Buchstabe, ganzzahliger Wert, Gleitkommawert) unterschiedlich viel Speicher benötigen. Auch die Codierung der Werte in Binärdarstellung für die Ablage im RAM ist für die einzelnen Datentypen unterschiedlich. Aus diesem Grund gehört jede Variable einem Datentyp an, der bei der Definition der Variablen vom Programmierer angegeben wird. int var1; double var2; char var3; var1 = 3; var2 = 3.14; var3 = 'c';
// // // // // //
Deklaration einer Variablen für ganzzahlige Werte Deklaration einer Variablen für Gleitkommawerte Deklaration einer Variablen für Zeichen Zuweisung einer ganzzahligen Konstanten Zuweisung einer Gleitkommakonstanten Zuweisung einer Zeichenkonstanten
Verweise Siehe Abschnitt, Kategorie Variablen und Datentypen.
Anweisungen
;
Der eigentliche Code des Programms. Die Anweisungen werden vom Compiler in Maschinenbefehle umgesetzt, die vom Prozessor des Computers bei Ausführung des Programms nacheinander abgearbeitet werden. Anweisungen enden stets mit einem Semikolon. ●
● ●
●
22
Anweisungen dürfen nur innerhalb der Anweisungsblöcke von Funktionen stehen. Jede Programmanweisung muß mit einem Semikolon abgeschlossen werden. Mehrere Anweisungen können – durch Semikolons getrennt – in einer Zeile stehen. Außer Anweisungen werden auch Deklarationen mit einem Semikolon abgeschlossen.
Typische Komponenten von C/C++-Programmen var1 = 3 * var2; printf("Hallo"); return 0;
// // // // //
Zuweisung eines Wertes an eine Variable Aufruf einer Funktion Schlüsselwort zur Beendigung einer Funktion
Anweisungsblöcke
{}
In C/C++ werden Anweisungen in Anweisungsblöcken zusammengefaßt. Ein Anweisungsblock beginnt mit einer öffnenden geschweiften Klammer und endet mit einer schließenden geschweiften Klammer. Anweisungsblöcke können ineinander verschachtelt werden. Der äußerste Block ist dabei stets der Definitionsblock einer Funktion. Innerhalb des Anweisungsblocks der Funktion kann man weitere Blöcke definieren, beispielsweise zur Implementierung einer Schleife oder zur Verzweigung des Programmablaufs in Abhängigkeit von einer if-Bedingung . void func() { long loop = 1;
// Funktion // Anweisungsblock zu Schleife
while (loop <= 10) // Schleife und Schleifenbedingung { // Anweisungsblock zu Schleife // Potenzen berechnen printf("%d ^ %d = %f\n", loop, loop, pow(loop,loop)); loop++; } }
Funktionen Funktionen unterstützen die Modularisierung des Quellcodes. Statt alle Anweisungen in die main()-Funktion zu schreiben, kann man zusammengehörende Anweisungen, die eine gemeinsame Aufgabe erfüllen, als Funktionen auslagern. Eine Funktion verfügt über ●
einen Anweisungsblock,
●
eine Schnittstelle zu den anderen Teilen des Programms und
●
einen Namen, über den die Funktion aufgerufen werden kann.
Die folgende Funktion vergleich() definiert zwei Parameter, denen beim Aufruf der Funktion Werte übergeben werden können. Als Ergebnis liefert die Funktion mittels des Schlüsselworts return einen ganzzahligen Wert zurück, der anzeigt, ob die beiden übergebenen Werte gleich oder unterschiedlich sind. Funktionsdefinition: int vergleich (int parameter1, int parameter2) {
23
Grundlagen if (parameter1 == parameter2) return 1; else return 0; }
Funktionsaufruf: int ergebnis; int var = 25; ergebnis = vergleich(23, var);
// weist ergebnis den Wert 0 zu
Verweise Siehe Kapitel Funktionen.
Präprozessor-Direktiven
#
Präprozessor-Direktiven sind spezielle Anweisungen an den Compiler (genauer gesagt, den Präprozessor), die vor der eigentlichen Übersetzung des Programms in Maschinencode ausgeführt werden. Präprozessor-Direktiven beginnen stets mit einem »#« und stehen immer allein in einer Zeile. Die wichtigste Präprozessor-Direktive lautet: #include. Sie dient dazu, den Inhalt einer Textdatei in den Quelltext der aktuellen Datei einkopieren zu lassen. In C/C++ werden selbst Anfänger gleich mit diesem Konzept konfrontiert, da man nur auf diese Weise auf die Funktionen/Klassen der Standardbibliothek zugreifen kann. Die Standardbibliotheken von C und C++ liegen als kompilierte LIB-Dateien vor und enthalten die Definitionen einer Reihe von nützlichen Funktionen (und Klassen). Um eine dieser Funktion in einem Programm aufrufen zu können, muß man die Funktion zuvor beim Compiler bekanntmachen. Dies geschieht durch eine Deklaration. Um dem Programmierer die Tipparbeit für die Deklaration der Bibliothekselemente zu ersparen, sieht der ANSI-Standard eine Reihe von Header-Dateien vor, in denen die Deklarationen der Bibliothekselemente stehen. Jede Header-Datei ist einem speziellen Themengebiet gewidmet. Die Funktion printf() zur Textausgabe fällt beispielsweise in den Bereich Ein- und Ausgabe, englisch Input/Output (IO), ihre Deklaration ist in der Header-Datei stdio.h enthalten. Statt printf() und weitere Funktionen zur Ein- und Ausgabe extra im Programm vorab zu deklarieren, genügt es daher die Header-Datei stdio.h einzubinden: #include <stdio.h>
24
Typische Komponenten von C/C++-Programmen
Kommentare
/* */ und //
Kommentare sind Textpassagen, die vom Compiler ignoriert werden. Sie erlauben dem Programmierer, Anmerkungen in den Quelltext aufzunehmen – meist zur Erklärung des nebenstehenden oder nachfolgenden C/C++-Codes. C
Kommentare beginnen in C mit der Zeichenkombination Schrägstrich – Stern »/*« und enden mit der Zeichenkombination Stern – Schrägstrich »*/«. Die dazwischenstehenden Zeichen werden vom Compiler ignoriert. Dies gilt auch für das Zeilenendezeichen, so daß sich Kommentare auch über mehrere Zeilen erstrecken dürfen. Sie dürfen jedoch nach dem ANSI-Standard nicht verschachtelt werden. (Einige Compiler erlauben optional die Verschachtelung von Kommentaren.) /* Dies ist ein Kommentar, der sich über mehrere – genauer gesagt zwei Zeilen erstreckt */ /* Das folgende Programm gibt einen Gruß auf den */ /* Bildschirm Ihres Computers aus */ #include <stdio.h> int main() { printf("Hallo!\n"); /* Aufruf einer Funktion */ return 0; } C++
In C++ kann die Auskommentierung auch durch doppelte Schrägstriche »//« erfolgen. Der nachfolgende Kommentar reicht bis zum Zeilenende. In C++ können beide Konzepte zur Kommentierung unabhängig voneinander verwendet werden. /* Das folgende Programm gibt einen Gruß auf den */ /* Bildschirm Ihres Computers aus */ #include <stdio.h> int main() { printf("Hallo!\n"); // Aufruf einer Funktion return 0; }
Tip In C++ bietet es sich an, innerhalb von Anweisungsblöcken nur //-Kommentare zu verwenden und die Klammern /* und */ zum Auskommentieren größerer Blöcke beim Debuggen einzusetzen.
25
Grundlagen
Größere Programme und Modularisierung Beschreibung Beim Aufsetzen größerer Programme empfiehlt es sich, den Code zu modularisieren, statt diesen Anweisung für Anweisung untereinander in die main()-Funktion zu schreiben. Die Modularisierung und Strukturierung des Codes sieht so aus, daß man einerseits Anweisungen und Code in Form von Funktionen und Klassen (nur C++) organisiert, andererseits den Quelltext selbst auf mehrere Dateien verteilt.
Aufteilung in Funktionen und Klassen Größere Programmieraufgaben löst man üblicherweise, indem man Teilaufgaben formuliert und diese in Form eigener Funktionen löst. Ein Programm, das zwei Daten einliest, auf der Grundlage der Daten eine Berechnung ausführt und dann das Ergebnis ausgibt, könnte drei Teilaufgaben definieren: ●
Daten einlesen
●
Berechnung durchführen
●
Daten ausgeben
Für jede dieser Teilaufgaben könnte man eine eigene Funktion definieren (wobei man für die Ein- und Ausgabe in einfachen Fällen auch direkt die entsprechenden Funktionen der Laufzeitbibliothek verwenden kann). Die Aufteilung in Funktionen hat den Vorteil, daß ●
der Quelltext übersichtlicher ist,
●
der Quelltext einfacher zu debuggen und zu warten ist.
Letzeres gilt insbesondere, wenn eine der Teilaufgaben mehrfach auszuführen ist. Statt jedes Mal die zugehörigen Anweisungen einzutippen, ruft man nur die entsprechende Funktion auf. Der Code wird nur einmal – in der Funktionsdefinition – aufgesetzt. Stellt sich heraus, daß die Funktion fehlerhaft ist oder erweitert werden muß, kann dies zentral in der Funktionsdefinition geschehen. In C++ kann Code nicht nur in Funktionen, sondern auch in Klassen (Datentypen, in denen Variablen und Funktionen zusammengefaßt werden) organisiert werden.
Aufteilung in mehrere Quelltextdateien Umfangreichen Code kann man auf mehrere Quelltextdateien verteilen. Dies ist beispielsweise angebracht, wenn man eine Sammlung von nützlichen Funktionen in einer eigenen Datei verwahren will (meist der erste Schritt zu einer eigenen Bibliothek), oder wenn man zu mehreren an einem größeren Programmprojekt arbeitet. Aufgabe des Compilers und Linkers ist es dabei, die einzelnen Quelltextdateien zu einem Programm zusammenzufassen.
26
Größere Programme und Modularisierung
Alle Quelltextdateien, die zusammen in einem Schritt vom Compiler übersetzt und zu einer Objektdatei kompiliert werden, bezeichnet man als Modul oder Übersetzungseinheit. Eine Übersetzungseinheit besteht aus einer Implementierungsdatei (Extension .c oder .cpp) und allen Quelltextdateien, deren Inhalt direkt oder indirekt per #include-Anweisung eingebunden werden.
Beispiel Für ein Programm, das zwei Zahlen von der Tastatur einliest und sowohl das Produkt als auch den Mittelwert der beiden Zahlen berechnet und ausgibt, könnte man beispielsweise zwei spezielle Funktionen zur Berechnung des Produkts und des Mittelwerts aufsetzen und diese dann in der main()-Funktion aufrufen. (Für zwei so simple Berechnungen wie das Produkt und den Mittelwert zweier Zahlen, würde man in der Praxis allerdings keine eigene Funktion aufsetzen. Doch geht es hier nur darum, das Prinzip der Modularisierung zu verdeutlichen.) #include <stdio.h> int produkt_berechnen(int z1, int z2) { int ergebnis; ergebnis = z1 * z2; return ergebnis;
/* Produkt berechnen */ /* Ergebnis zurückliefern */
} double mittelwert_berechnen(int z1, int z2) { double ergebnis; ergebnis = (z1 + z2)/2.0; return ergebnis;
/* Mittelwert berechnen */ /* Ergebnis zurückliefern */
} int main(int argc, char **argv) { /* Variablen deklarieren */ int zahl1, zahl2; int produkt; double mittelwert; /* Zahlenwerte einlesen */ printf("Geben Sie zwei Zahlen zwischen 0 und 100 ein: \n\n"); scanf("%d %d", &zahl1, &zahl2); fflush(stdin); /* Ergebnisse berechnen */ produkt = produkt_berechnen(zahl1, zahl2); mittelwert = mittelwert_berechnen(zahl1, zahl2);
27
Grundlagen /* Ergebnisse ausgeben */ puts("\n"); printf("Produkt = %d\n", produkt); printf("Mittelwert = %f\n", mittelwert); return 0; }
Verweise Siehe Abschnitt zur Sprache für detailliertere Informationen zu Funktionen und Klassen.
Objektorientiertes Programmieren Objektorientiert zu programmieren bedeutet nicht einfach, sich die Annehmlichkeiten von C++ gegenüber C zunutze zu machen oder die Klassenbibliotheken statt der C-Bibliotheken zu benutzen. In diesem strengeren Sinne kann man das folgende Programmbeispiel, in dem statt der C-Funktion printf() die Klasseninstanz cout und der überladene <<-Operator verwendet werden, nicht als objektorientiert programmiert bezeichnen.
Beispiel /* Das erste C++-Programm. */ #include
using namespace std; int main() { cout << "Hello world!" << endl; return 0; }
Objektorientierte Programmierung beginnt eigentlich erst da, wo Klassen definiert und die wichtigen Konzepte der Kapselung und des Polymorphismus sinnvoll eingesetzt werden, und sie rechtfertigt sich vor allem dort, wo Klassenbibliotheken entstehen, die sich leicht und sicher verwenden lassen. Letztendlich laufen alle wesentlichen Konzepte der objektorientierten Programmierung darauf hinaus, den Prozeß der Programmentwicklung zu beschleunigen, sei es durch ● ●
28
die Wiederverwertbarkeit bereits implementierter Codes, schnellere Entwicklung von Datentypen durch Vererbung und Polymorphismus,
Verwendung der Standardbibliotheken
●
geringere Fehleranfälligkeit durch Kapselung oder
●
eine bessere Lesbarkeit der Programme, dank der objektorientierten Sichtweise.
Wo diese Kriterien nicht greifen, spricht nichts gegen eine Implementierung in C, wobei der Verzicht auf die objektorientierten Konzepte eine geringere Codegröße und bessere Laufzeiten mit sich bringt.
Verweise Siehe Kapitel über Klassen.
Verwendung der Standardbibliotheken #include
Beschreibung Zu C und C++ gehört eine umfangreiche Sammlung von Funktionen und Klassen, die der Bewältigung der wichtigsten Programmieraufgaben dienen etwa der Ein- und Ausgabe von Daten, der Programmierung mit Zeichenketten (Text), der Verwendung mathematischer Funktionen wie Sinus, Kosinus, Potenz, etc.
Anwendung C/C++ verfügt über einen recht kleinen Satz von Schlüsselwörtern, etlichen Operatoren und einer äußerst umfangreichen Sammlung von Syntaxregeln, die vorschreiben, wie man Schlüsselwörter, Operatoren und selbst definierte Bezeichner zu korrekten C/C++-Programmen verknüpft. Das Potential dieser Sprachelemente und -regeln ist enorm, doch wäre die Entwicklung selbst einfachster Programme allein mit diesen Sprachelementen eine mühsame Angelegenheit, denn selbst für elementarste Programmieraufgaben bieten diese Sprachelemente keine Unterstützung. So müßte man ●
●
●
zum Kopieren von Zeichenketten (sprich Textpassagen) Schleifen programmieren, in denen die Zeichenfolgen Zeichen für Zeichen kopiert werden, zur Berechnung des Sinus einer Zahl sich zuerst in einem Lehrbuch der Mathematik darüber informieren, wie man den Sinus einer Zahl durch Berechnung einer trigonometrischen Reihe annähern kann, für Ein- und Ausgaben auf Betriebssystemebene programmieren.
29
Grundlagen
Voraussehende Programmierer werden diese häufig wiederkehrenden Programmieraufgaben in Form von eigenen Funktionen implementieren, damit sie – etwa bei der Berechnung des nächsten Sinus – nicht mehr die ganze Berechnung ausführen müssen, sondern nur noch die entsprechende Funktion aufrufen müssen. Klevere Programmierer wissen, daß genau dies für die wichtigsten Programmieraufgaben bereits geschehen ist. Bevor Sie jedoch eine dieser Funktionen (für C++ auch Klassen) aufrufen und verwenden können, müssen Sie ●
●
●
sich informieren, welche Funktion / Klasse Ihr Problem lösen könnte (siehe Referenz der Laufzeitbibliotheken) sich informieren, wie die Funktion / Klasse verwendet wird (siehe Referenz der Laufzeitbibliotheken). Für Funktionen ist es beispielsweise wichtig, welche Argumente man der Funktion beim Aufruf übergibt und welchen Ergebniswert die Funktion zurückliefert. dem Compiler, der Ihren Programmquelltext übersetzt, die Funktion (Klasse) bekanntmachen.
Die Präprozessor-Direktive #include dient dazu, den Inhalt einer Datei in den Quelltext einkopieren zu lassen. In C/C++ werden selbst Anfänger gleich mit diesem Konzept konfrontiert, da man nur auf diese Weise auf die Funktionen/Klassen der Standardbibliothek zugreifen kann. Die Standardbibliotheken von C und C++ liegen als kompilierte LIB-Dateien vor und enthalten die Definitionen einer Reihe von nützlichen Funktionen (und Klassen). Um eine dieser Funktion in einem Programm aufrufen zu können, muß man die Funktion zuvor per Deklaration beim Compiler bekanntmachen. Um dem Programmierer die Tipparbeit zu ersparen, sieht der ANSI-Standard eine Reihe von Header-Dateien vor, in denen die Deklarationen der Bibliothekselemente stehen. Jede Header-Datei ist einem speziellen Themengebiet gewidmet. Die Funktion printf() zur Textausgabe fällt beispielsweise in den Bereich Ein- und Ausgabe, englisch Input/Output (IO), ihre Deklaration ist in der Header-Datei stdio.h enthalten. Statt printf() und weitere Funktionen zur Ein- und Ausgabe extra im Programm vorab zu deklarieren, genügt es daher die Header-Datei stdio.h einzubinden: #include <stdio.h>
Warnung Gemäß ANSI-C lauten alle Header-Dateien für die C-Laufzeitbibliothek auf .h aus. Gemäß ANSI-C++ haben die Header-Dateien dagegen keine Extension und den alten C-Headern wird der Buchstabe c vorangestellt (cstdio statt stdio.h). Zudem sind alle Bibliothekselemente in dem Namensbereich std deklariert. Die korrekte Einbindung der Header-Dateien in C++, beispielsweise der Header-Datei iostream mit den Stream-Klassen zur Ein- und Ausgabe, sähe damit wie folgt aus:
30
Ein- und Ausgabe #include using namespace std;
Einige Compiler unterstützen derzeit aber auch noch ältere Versionen von iostream oder haben die Stream-Klassen nicht im Namensbereich std deklariert. In solchen Fällen muß man sich notgedrungen nach der vom Compiler vorgegebenen Syntax richten, meist: #include
oder #include
Ein- und Ausgabe printf() / scanf() cout / cin
Beschreibung Um mit dem Anwender interagieren zu können, muß ein Programm in der Lage sein, Informationen und Daten vom Anwender entgegenzunehmen und selbst Daten und Informationen über den Bildschirm auszugeben.
Anwendung Im Unterschied zu Sprachen wie Fortran oder Basic besitzt die Sprache C keine eigenen Sprachelemente zur Ein- oder Ausgabe. Dies bedeutet nicht, daß mit Coder C++-Programmen keine E/A-Operationen durchgeführt werden können. Man muß nur statt eines Schlüsselworts der Sprache eine passende Funktion der Laufzeitbibliothek aufrufen. In C sind dies vor allem die Funktionen ●
printf() für die Ausgabe auf die Konsole (Bildschirm)
●
scanf() für das Einlesen von Tastatur.
In C++ bedient man sich der Klassen ●
cout (Ausgabe auf Konsole) und
●
cin (Einlesen von Tastatur)
und der Stream-Operatoren << und >>.
31
Grundlagen
Warnung Die oben beschriebenen Funktionen und Klassen sind gemäß ANSI Teil der Standardbibliotheken von C/C++ und daher stets verfügbar. Der Austausch von Informationen zwischen Anwender und Programm über diese Funktionen und Klassen ist allerdings auf Konsolenprogramme beschränkt. Programme mit grafischer Benutzeroberfläche (GUI), die unter Windows, X-Windows oder OSF Motif laufen, verwenden andere Möglichkeiten der Ein- und Ausgabe, die durch spezielle APIs (Sammlungen von Funktionen und Klassen für bestimmte Programmieraufgaben) unterstützt und implementiert werden.
Beispiele Ein- und Ausgabe unter C: #include <stdio.h> int main() { int zahl1, zahl2; /* Daten einlesen */ printf("Geben Sie zwei Zahlen zwischen 0 und 100 ein: \n\n"); scanf("%d %d", &zahl1, &zahl2); fflush(stdin); /* Daten ausgeben puts("\n"); printf("1. Zahl = printf("2. Zahl = printf("Produkt = return 0;
*/ %d\n", zahl1); %d\n", zahl2); %d\n", zahl1 * zahl2);
}
Ein- und Ausgabe unter C++: #include using namespace std; int main() { int zahl1, zahl2; /* Daten einlesen */ cout << "Geben Sie zwei Zahlen zwischen 0 und 100 ein: \n\n"; cin >> zahl1 >> zahl2; /* Daten ausgeben */ cout << endl; cout << "1. Zahl = " << zahl1 << endl; cout << "2. Zahl = " << zahl2 << endl;
32
Programmfluß cout << "Produkt = " << zahl1 * zahl2 << endl; return 0; }
Programmfluß C/C++-Programme bestehen aus einer Ansammlung von Deklarationen und Definitionen. Anweisungen findet man nur in Funktionen (in C++ auch in den Methoden (Elementfunktionen) von Klassen). Damit stellt sich die Frage, mit welcher Anweisung die Ausführung eines Programmes beginnt. ●
● ●
●
Jedes C/C++-Programm beginnt mit der Hauptfunktion main(), die im Quelltext des Programms definiert sein muß. Die Anweisungen in der main()-Funktion werden nacheinander ausgeführt. Durch Aufruf einer Funktion aus einer anderen Funktion heraus, beispielsweise auch aus main(), kann der Programmfluß gesteuert und der Quellcode durch Aufteilung auf mehrere Funktionen modularisiert werden. Innerhalb einer Funktion kann der Programmfluß durch Verzweigungen (if, switch) und Schleifen (for, while) gesteuert werden.
Warnung Leser, die mit C/C++ für Windows programmieren wollen, seien darauf hingewiesen, daß Windows-Programme statt der main()-Funktion die Funktion WinMain() verwenden. Je nach verwendetem Compiler und Klassenbibliothek kann WinMain() selbst wieder durch eine andere Funktion (beispielsweise OwlMain() für Borland C++ mit OWL) oder eine Klasseninstanz (beispielsweise ein globales Objekt der MFCKlasse CWinApp in Visual C++) ersetzt sein. Konsolenprogramme, die unter Windows ausgeführt werden (sprich im Fenster der MS-DOS-Eingabeaufforderung ablaufen und über keine eigene Windows-Oberfläche verfügen), verwenden aber wie reine DOS- oder UNIX-Programme die main()Funktion.
Verweise Siehe die Anschnitte zu Programmsteuerung, Funktionen, Exception-Behandlung.
33
Grundlagen
Programmerstellung Beschreibung Um aus einem in C/C++ formulierten Programmquelltext ein ausführbares Programm zu machen, muß man es kompilieren. Bei der Kompilation wird der Programmquelltext vom Compiler in Maschinencode umgewandelt. In Zusammenarbeit mit dem Linker wird aus diesem Maschinencode eine ausführbare Datei (unter Windows mit der Extension .exe) erzeugt, die allerdings nur auf bestimmten Plattformen (Kombination aus Prozessor und Betriebssystem, beispielsweise Intel-kompatibler Prozessor und Windows 95-Betriebssystem) ausgeführt werden kann.
Anwendung Die Erstellung eines Programmes ist natürlich nicht mit der Eingabe des Quelltextes in einen Editor abgeschlossen. Bevor ein Programm ausgeführt werden kann, muß es in maschinenlesbaren Code umgewandelt werden. Besteht ein Programm aus mehreren Modulen, müssen diese zusammengebunden werden. Diese Aufgaben übernehmen der Compiler und der Linker. 1. Quelltext in Editor eingeben. Dies kann prinzipiell in jedem ASCII-Texteditor geschehen. PC-Compiler verfügen meist über eine integrierte Entwicklungsumgebung (IDE) mit eigenem Editor, der mehr Unterstützung bei der Programmerstellung bietet. 2. Header-Dateien einkopieren. Zu Beginn der Kompilation wird der Quelltext mittels der #include-Direktive aufgeführter Header-Dateien in den Quelltext der Programmdatei kopiert. 3. Quelltextdateien kompilieren. Der Compiler übersetzt dann den Quelltext in maschinenlesbaren Objektcode. Der Objektcode ist meist an eine bestimmte Umgebung (Computerkonfiguration) angepaßt und nicht zwischen unterschiedlichen Umgebungen portierbar. Syntaktische Fehler im Programm werden vom Compiler angezeigt und müssen korrigiert werden. 4. Objektcode-Dateien (.obj) und Bibliotheken (.lib) zusammenbinden. Besteht ein Programm aus mehreren Quelltextdateien, werden diese vom Compiler einzeln in Objektcode übersetzt. Dem Linker obliegt dann die Aufgabe, diese ObjektcodeDateien sowie den Objektcode der benutzten Bibliotheksfunktionen in eine Programmdatei (.exe unter Windows) zusammenzubinden. 5. Debuggen. Nach dem Kompilieren und Linken liegt ein ausführbares, aber selten ein korrektes Programm vor. Bezüglich lauffähigen Programmen unterscheidet man Laufzeitfehler, die sich meist durch Programmabstürze oder Endlosschleifen äußern, und logische Fehler, wenn das Programm zwar läuft, aber nicht seinen Zweck erfüllt. Zum Aufspüren dieser Fehler verwendet man üblicherweise einen Debugger, mit dessen Hilfe man das Programm schrittweise ausführen und kontrollieren kann.
34
Programmerstellung
Ablauf der Programmerstellung
Die gängigsten Compiler Der GNU-Compiler Der GNU-Compiler ist ein frei verfügbarer Compiler, den man kostenlos aus dem Internet beziehen kann (http://Sourceware.cygnus.com). Den GNU-Compiler gibt es für UNIX wie für Windows. Etwas unbequem für Windows-Anwender ist, daß der GNU-Compiler über keine integrierte Entwicklungsumgebung verfügt, und man daher den Compiler explizit aus der MS-DOS-Eingabeaufforderung heraus aufrufen muß. 1. Setzen Sie Ihren Quelltext mit einem beliebigen ASCII-Editor auf, und speichern Sie die Datei. 2. Sofern Sie das /bin-Verzeichnis des Compilers in Ihren Pfad eingetragen haben, so daß der Compiler gcc oder g++ von überall aufrufbar ist, wechseln Sie in das Verzeichnis, in dem die Quelltextdatei steht. 3. Rufen Sie den C++-Compiler mit dem Dateinamen der Quelltextdatei auf: g++ quelltext.cpp
oder gcc quelltext.c
für den Aufruf des reinen C-Compilers. Das fertige Programm lautet a.exe (a.out unter Unix) und kann direkt ausgeführt werden.
35
Grundlagen
Wenn Sie möchten, können Sie auch einen expliziten Namen für die ausführbare Datei angeben. g++ quelltext.cpp -o progname
Programme, die aus mehreren Modulen bestehen, erstellen Sie durch Auflistung aller beteiligten Quelldateien (#include-Dateien werden nicht aufgeführt). gcc quelltext1.cpp quelltext2.cpp -o progname
Wenn Sie dem Compiler irgendwelche Optionen übergeben wollen (beispielsweise ansi für strikt ANSI-kompatiblen Code oder -c für Kompilieren ohne zu Linken), setzen Sie diese hinter den Dateinamen. gcc quelltext.cpp -ansi -c
Für die Erstellung der Windows-Programme müssen verschiedene Bibliotheken explizit mit eingebunden werden: gcc winprog.cpp -lkernel32 -luser32 -lgdi32
Welche weiteren Bibliotheken unter Umständen aufgenommen werden müssen, entnehmen Sie der Compiler-Dokumentation.
Warnung Die neueste Version des GNU-Compilers heißt jetzt egcs.
Der Borland C++-Compiler In der IDE (integrierten Entwicklungsumgebung) des Borland C++-Compilers werden Programme in Form von Projekten verwaltet. Konsolenanwendungen, die nur aus einer Datei bestehen, können zwar ohne Projektverwaltung erstellt werden, doch warum sollte man die angebotenen Möglichkeiten nicht nutzen. 1. Legen Sie ein neues Projekt an (Befehl Datei/Neu/Projekt für BorlandC++ 5.0, unter 4.5 befand sich der Befehl noch im Menü Projekt). Es erscheint der Target Expert, in dem Sie angeben können, welche Art von Anwendung Sie erstellen wollen. 2. Geben Sie das Projektverzeichnis und einen Namen für die ausführbare Datei an. Wählen Sie als Zieltyp Anwendung, als Umgebung Win32 und als Zielmodell Konsole (oder als Umgebung DOS, wenn Sie noch mit einem Win16-Betriebssystem arbeiten). Die weiteren Einstellungen werden für Sie angepaßt, achten Sie aber darauf, daß die OWL nicht eingebunden wird. 3. Öffnen Sie eine neue Quelltextdatei. Klicken Sie im erscheinenden Projektfenster auf den Knoten der .cpp-Datei. (Wurden weitere Knoten angelegt (.rc, .def) können Sie diese mit den Befehlen des Kontextmenüs löschen). Es erscheint ein Editorfenster für die Quelltextdatei. 4. Geben Sie Ihren Quelltext ein und sichern Sie.
36
Programmerstellung
5. Rufen Sie den Compiler auf (Befehl Projekt/Projekt neu kompilieren für BorlandC++ 5.0). 6. Führen Sie das Programm aus (Befehl Debug/Ausführen für BorlandC++ 5.0). Unter Win32 läuft das Programm in einem MS-DOS-Fenster ab, das sofort wieder verschwindet, nachdem das Programm beendet wurde. Um dies zu verhindern, können Sie die Beispielprogramme um einen Aufruf der Funktion getchar() vor der return-Anweisung von main() erweitern. Das Programm wartet dann auf eine Tastatureingabe, und das DOS-Fenster bleibt sichtbar. Die Einstellungen für den Compiler und Linker werden in entsprechenden Dialogfeldern gesetzt (Befehl Optionen/Projekt für BorlandC++ 5.0). Zur Erstellung von Windows-Programmen wählen Sie im Target Expert als Zieltyp Anwendung, als Umgebung Win32 und als Zielmodell GUI (statt Konsole). Achten Sie darauf, daß keine zusätzliche Klassenbibliothek (OWL, OCF, MFC, etc.) eingebunden wird, da in diesem Buch nur reine API-Programme beschrieben sind.
Der C++-Builder Der C++Builder von Borland arbeitet ebenfalls mit einer Projektverwaltung. Während der C++Builder 1.0 die Projekte jedoch noch einzeln verwaltete, gibt es in der Version 3.0 nun auch die Möglichkeit, mehrere zusammengehörende Projekte zusammen in einer Projektgruppe zu verwalten (analog den Arbeitsbereichen in Visual C++). 1. Legen Sie ein neues Konsolen-Projekt an (Befehl Datei/Neu für C++Builder 3.0). Es erscheint ein mehrseitiges Dialogfenster, in dem Sie zur Seite Neu wechseln. 2. Doppelklicken Sie auf das Symbol Konsolen-Experte (Textbildschirm-Anwendung für 1.0). 3. Es erscheint direkt der Quelltexteditor des C++Builders mit einer extra Seite für Ihren Quelltext. Speichern Sie Projekt und Quelltext (Befehl Datei/Alles speichern). 4. Geben Sie Ihren Quelltext ein – entweder indem Sie ihn in das vorgegebene Gerüst einpassen oder indem Sie das vorgegebene Gerüst zuvor löschen und dann Ihren Text eintippen. 5. Führen Sie das Programm aus (Befehl Start/Start). Unter Win32 läuft das Programm in einem MS-DOS-Fenster ab, das sofort wieder verschwindet, nachdem das Programm beendet wurde. Um dies zu verhindern, können Sie die Beispielprogramme um einen Aufruf der Funktion getchar() vor der return-Anweisung von main() erweitern. Das Programm wartet dann auf eine Tastatureingabe, und das DOS-Fenster bleibt sichtbar.
37
Grundlagen
Die Einstellungen für den Compiler und Linker werden in entsprechenden Dialogfeldern gesetzt (Befehl Optionen/Projekt für C++Builder 1.0 oder Projekt/Optionen für 3.0). Zur Erstellung von Windows-Programmen doppelklicken Sie im Schritt 1 auf das Symbol Anwendung statt auf Konsolen-Experte.
Warnung Wenn Sie mit dem C++Builder 3 arbeiten, müssen Sie in Ihre Konsolenanwendungen unter Umständen auch die Header-Datei aufnehmen.
Tip Der große Vorzug des C++-Builders liegt in der RAD-Umgebung zur Entwicklung von Windows-Programmen. RAD bedeutet Rapid Application Development und steht für die visuelle, komponentenbasierte Erstellung von Programmen.
Der Visual C++-Compiler Auch der Visual C++-Compiler von Microsoft arbeitet mit einer Projektverwaltung. Zusätzlich werden in Visual C++ Projekte in Arbeitsbereichen verwaltet (auf diese Weise können beispielsweise die Projekte für eine Windows-Anwendung und etwaige unterstützende DLLs und LIBs zusammen in einem Arbeitsbereich verwaltet werden). 1. Legen Sie ein neues Konsolen-Projekt an (Befehl Datei/Neu ab Visual C++ 5.0). Es erscheint ein mehrseitiges Dialogfenster, in dem Sie zur Seite Projekte wechseln. 2. Geben Sie einen Projektnamen ein, wählen Sie den Pfad aus, unter dem das Verzeichnis für die Quelldateien des Projekts eingerichtet wird, aktivieren Sie die Option Neuen Arbeitsbereich erstellen und doppelklicken Sie dann auf das Symbol Win32-Konsolenanwendung. (In Visual C++ 6 erscheint daraufhin ein Dialogfenster, in dem Sie neben einem leeren Projekt auch Projekte mit Datei und Grundgerüst erstellen lassen können. Die nachfolgenden Schritte 3 und 4 erübrigen sich dann.) 3. Öffnen Sie eine neue Quelltextdatei (Befehl Datei/Neu, Seite Dateien ab Visual C++ 5) Geben Sie einen Dateinamen ein, und doppelklicken Sie dann auf das Symbol C++-Quellcodedatei. 4. Nachdem die Datei erstellt wurde, doppelklicken Sie im Arbeitsbereich-Fenster (Seite Dateien) auf den gleichnamigen Knoten, um die Datei in ein Editorfenster zu laden. 5. Geben Sie Ihren Quelltext ein und sichern Sie. 6. Kompilieren und linken Sie das Projekt (Befehl Erstellen/ erstellen). 7. Führen Sie das Programm aus (Befehl Erstellen/Ausführen von ).
38
Programmerstellung
Die Einstellungen für den Compiler und Linker werden in entsprechenden Dialogfeldern gesetzt (Befehl Projekt/Einstellungen ab VisualC++ 5.0). Zur Erstellung von Windows-Programmen doppelklicken Sie im Schritt 1 auf das Symbol Win32-Anwendung statt auf Win32-Konsolenanwendung.
39
Zeichensatz
Sprachkonzepte und Syntax Elemente der Sprache Zeichensatz Beschreibung Man unterscheidet zwischen zwei Zeichensätzen: Dem Basiszeichensatz, den jeder ANSI-C/C++-Compiler zur Aufsetzung des Programmcodes bereitstellt. Dem Ziel- oder Umgebungszeichensatz, den das Programm bei der Ausführung verwendet, etwa für Ein- und Ausgaben.
Der Basiszeichensatz Der Basiszeichensatz, den jeder C/C++-Compiler versteht und in dessen Zeichen der Programmquelltext aufgesetzt wird, enthält die folgenden Zeichen: a
b
c
d
e
f
u
v
w
x
y
z
A
B
C
D
E
F
U
V
W
X
Y
Z
0
1
2
3
4
5
g
h
i
j
k
l
m
n
o
p
q
r
s
t
G
H
I
J
K
L
M
N
O
P
Q
R
S
T
6
7
8
9 >
%
:
;
.
?
*
+
-
/
^
_
{
}
[
]
#
(
)
<
&
|
~
!
=
,
\
"
'
sowie: Leerzeichen
Neue Seite (FF)
Horizontaler Tabulator (HT)
Neue Zeile (NL)
Vertikaler Tabulator (VT)
41
Sprachkonzepte und Syntax
Quell- und Zielzeichensatz Der Zielzeichensatz ist compilerspezifisch. Compiler, die unter Windows laufen, verwenden derzeit üblicherweise den ANSI-Zeichensatz von Windows, dessen erste 128 Zeichen den Zeichen des traditionellen ASCII-Zeichensatzes entsprechen (siehe Anhang). In Zukunft wird sich wohl UNICODE durchsetzen (das bereits von Windows NT 4.0 unterstützt wird). Auf die Zeichen dieses Zeichensatzes können Sie in Kommentaren und Zeichenbzw. String-Literalen zurückgreifen. In Zeichen- und String-Literalen kann man darüber hinaus folgende Escape-Sequenzen verwenden: Zeichen
Escape-Sequenz
Neue Zeile
NL (LF)
\n
Horizontaler Tabulator
HT
\t
Vertikaler Tabulator
VT
\v
Rücktaste
BS
\b
Wagenrücklauf
CR
\r
Seitenvorschub
FF
\f
Signalton
BEL
\a
Backslash
\
\\
Fragezeichen
?
\?
Hochkomma
'
\'
Anführungszeichen
"
\"
Oktaler Code
\ooo
Hexadezimaler Code
\xhhh
Beispiele #include <stdio.h> #include <string.h> int main(int argc, char **argv) { char kaempfer[100]; // in Bezeichnern sind Umlaute // nicht erlaubt (nur Basiszeichensatz), strcpy(kaempfer,"Hägar"); // in String-Literalen schon printf("Es kämpft für Sie: %s\n", kaempfer); return 0; }
42
Trigraphsequenzen
Trigraphsequenzen Beschreibung Trigraphsequenzen sind Folgen von jeweils drei Zeichen, die als Ersatz für bestimmte Zeichen des Basiszeichensatzes verwendet werden können.
Anwendung Der Basiszeichensatz ist eng an die englische Sprache und die englische Tastaturbelegung geknüpft. Für Programmierer anderer Nationen kann die Eingabe bestimmter Zeichen auf Grund ihrer landesspezifischen Tastaturbelegung unbequem, wenn nicht sogar nur über ALT+ASCII-Code möglich sein. Der ANSI-Standard definiert aus diesem Grund eine Reihe von Trigraphsequenzen, die vom Compiler zu Beginn der Kompilation in die entsprechenden Zeichen des Basiszeichensatzes umgewandelt werden. Synonym
Bedeutung
Synonym
Bedeutung
??=
#
??!
|
??/
\
??<
{
??'
^
??>
}
??(
[
??-
~
??)
]
Tip Für die deutsche Tastaturbelegung bedeuten die Trigraphsequenzen keine große Erleichterung. Um den Programmcode lesbar zu gestalten, sollte man auf Trigraphsequenzen verzichten.
Beispiele ??=include <stdio.h> int main(int argc, char **argv) ??< printf("Hallo??/n"); return 0; ??>
Schlüsselwörter und Symbole Beschreibung C/C++ kennt eine Reihe von Schlüsselwörtern und Symbolen, die innerhalb der Sprache eine feste Bedeutung haben.
43
Sprachkonzepte und Syntax
Schlüsselwörter asm (nur C++)
false (nur C++)
sizeof
auto
float
static
bool (nur C++)
for
static_cast (nur C++)
break
friend (nur C++)
struct
case
goto
switch
catch (nur C++)
if
template (nur C++)
char
inline (nur C++)
this (nur C++)
class (nur C++)
int
throw (nur C++)
const
long
true (nur C++)
const_cast (nur C++)
mutable (nur C++)
try (nur C++)
continue
namespace (nur C++)
typedef
default
new (nur C++)
typeid (nur C++)
delete (nur C++)
operator (nur C++)
typename (nur C++)
do
private (nur C++)
union
double
protected (nur C++)
unsigned
dynamic_cast (nur C++)
public (nur C++)
using (nur C++)
else
register
virtual (nur C++)
enum
reinterpret_cast (nur C++)
void
explicit (nur C++)
return
volatile
export (nur C++)
short
wchar_t (nur C++)
extern
signed
while
Warnung Schlüsselwörter dürfen nicht als Bezeichner für Variablen, Funktionen, etc. verwendet werden.
Symbole C/C++ kennt eine erstaunliche Zahl an bedeutungstragenden Symbolen, wobei ein Großteil auf die vielen Operatoren der Sprache fällt. Symbol
Kurzbeschreibung
Symbol
Kurzbeschreibung
;
Abschluß einer Anweisung oder Deklaration
--
Dekrement
{ }
Anweisungsblock definieren
->
Elementverweis-Operator
[ ]
Array-Definition
.
Punkt-Operator
[ ]
Array-Indizierung
*
Zeigerdeklaration
( )
Typumwandlung
*
Dereferenzierungs-Operator
44
Schlüsselwörter und Symbole
Symbol
Kurzbeschreibung
Symbol
Kurzbeschreibung
( )
Parameterliste von Funktionen
==
Test auf Gleichheit
...
variable Parameterzahl
<
Kleiner als-Vergleich
=
Zuweisung
>
Größer als-Vergleich
+
Addition
<=
Kleiner-gleich-Vergleich
+=
Addition und Zuweisung
=>
Größer-gleich-Vergleich
-
Subtraktion
<<
Bitweise Linksverschiebung
-=
Subtraktion und Zuweisung
<<=
Bitweise Linksverschiebung und Zuweisung
*
Multiplikation
>>
Bitweise Rechtsverschiebung
*=
Multiplikation und Zuweisung
>>=
Bitweise Rechtsverschiebung und Zuweisung
/
Division
?:
Bedingungsoperator
/=
Division und Zuweisung
^
Bitweises exklusives ODER
%
Modulo-Operator
^=
Bitweises exklusives ODER und Zuweisung
%=
Modulo-Operator und Zuweisung
|
Bitweises ODER
!
Logische Verneinung
|=
Bitweises ODER und Zuweisung
!=
Vergleich auf Ungleichheit
||
Logisches ODER
&
Referenzdeklaration
~
Komplement
&
unär: Adresse des Operanden
:
Klasseninitialisierer
&
binär: bitweise UND-Verknüpfung
::
Bereichsoperator (nur C++)
&=
Bitweise UND-Verknüpfung und Zuweisung
.*
Dereferenzierung von Zeigern auf Klassenelemente (nur C++)
&&
Logische UND-Verknüpfung
->*
Dereferenzierung von Zeigern auf Klassenelemente (nur C++)
,
Sequentielle Auswertung
#
Zeichenkettenbildung
++
Inkrement
##
Grundsymbolverbindung
Alternative Zeichen Für Programmierer mit ungünstigen nationalen Tastaturbelegungen gibt es für einige Symbole alternative Zeichenfolgen (ähnlich den Trigraphsequenzen). Alternative
für
<%
{
Anweisungsblock beginnen
%>
}
Anweisungsblock beenden
<:
[
Array-Definition und -Indizierung
45
Sprachkonzepte und Syntax
Alternative
für
:>
]
Array-Definition und -Indizierung
%:
#
Zeichenkettenbildung
%:%:
##
Grundsymbolverbindung
and
&&
Logische UND-Verknüpfung
and_eq
&=
Bitweise UND-Verknüpfung und Zuweisung
bitand
&
Bitweise UND-Verknüpfung
bitor
|
Bitweises ODER
compl
~
Komplement
not
!
Nicht-Operator, Verneinung
not_eq
!=
vergleicht zwei arithmetische Operanden auf Ungleichheit
or
||
Logisches ODER
or_eq
|=
Bitweises ODER und Zuweisung des Ergebnisses
xor
^
Bitweises exklusives ODER
xor_eq
^=
Bitweises exklusives ODER und
Eigene Bezeichner: Definition und Deklaration Beschreibung Nur mit den Schlüsselwörtern und Operatoren der Sprache C/C++ kann man keine Programme schreiben. C/C++ erlaubt daher die Einführung eigener Elemente in ein Programm. Alle vom Programmierer eingeführten Elemente müssen deklariert und definiert werden.
Anwendung Die wichtigsten Elemente, die ein Programmierer in ein Programm einführen kann, sind: ● ●
●
Variablen, um Daten zwischenzuspeichern, eigene Datentypen, um die Verwaltung der Daten zu organisieren und zu vereinfachen (einschließlich der Definition von Klassen), Funktionen, um zusammengehörige Anweisungen zusammenzufassen und den Quelltext zu modularisieren.
Warnung Die beiden Begriffe Deklaration und Definition werden häufig in etwas nachlässigem Sprachgebrauch synonym gebraucht, auch wenn sie eigentlich Unterschiedliches bedeuten.
46
Eigene Bezeichner: Definition und Deklaration
Deklaration Deklaration bedeutet, dem Compiler einen Namen (Bezeichner) einer Variablen, Funktion, Klasse, eines Templates, Datentyps oder Sprungmarke (Label) bekanntzumachen. Neben dem Namen des neuen Elements enthält die Deklaration bereits alle nötigen Informationen, die den Compiler in die Lage versetzen die ordnungsgemäße Verwendung des Elements im Programmquelltext zu kontrollieren, und Anweisungen, in denen das Element verwendet wird, in Maschinencode zu übersetzen. ●
●
● ●
Mit der Deklaration einer Variablen wird ihr Name, ihr Typ, die Speicherklasse und die Sichtbarkeit des Bezeichners festgelegt. Bei der Deklaration einer Funktion wird ihr Name, ihr Rückgabewert, die Liste der Parameter und die Sichtbarkeit festgelegt. Mit einer Deklaration geht keine Speicherreservierung einher. Eine Variablendeklaration ist automatisch auch eine Definition, es sei denn, die Deklaration ist als extern gekennzeichnet und enthält keine explizite Initialisierung, oder es wird ein statisches Element einer Klasse deklariert.
Definition Erst durch die Definition stellt der Compiler Speicherplatz für ein Element zur Verfügung. ●
Für Variablen wird in diesem Speicher der Wert der Variablen abgelegt.
●
Für Funktionen wird in diesem Speicher der Code der Funktion abgelegt.
●
Die Zuordnung von Namen zu Speicherbereichen muß eindeutig sein, d. h., ein Name kann nicht gleichzeitig auf zwei verschiedene Speicherbereiche weisen.
In C bedeutet dies, daß innerhalb eines Gültigkeitsbereiches jeder Bezeichner nur einmal definiert verwendet werden kann. In C++ können, dank der Möglichkeit der Überladung, Bezeichner von Funktionen und Operatoren innerhalb eines Gültigkeitsbereiches gleichzeitig in mehreren Definitionen benutzt werden. (Der Compiler löst dies, indem er die Namen um eine codierte Form der Parameter oder Operanden erweitert, so daß jedem erweiterten Namen wieder genau ein Speicherbereich zukommt.)
Verweise Siehe Gültigkeitsbereiche, Sichtbarkeit, Lebensdauer
47
Sprachkonzepte und Syntax
Variablen und Datentypen Variablen und Datentypen Beschreibung Ganz egal, welche Art von Programm Sie schreiben: Die Hauptaufgabe eines Programmes besteht immer darin, Daten zu verarbeiten. Während das Programm abläuft, werden diese Daten an verschiedenen Stellen im Arbeitsspeicher abgelegt. Um auf die Daten zugreifen zu können, ohne sich mit Speicheradressen herumärgern zu müssen, werden Variablen benutzt.
Variablen Eine Variable ist ein Name für einen Speicherbereich, in den ein Datenobjekt abgelegt werden kann (beispielsweise eine ganze Zahl, eine Gleitkommazahl, eine Zeichenkette, etc.). Über den Namen der Variablen kann auf den Speicherbereich zugegriffen werden, um die sich dort befindenden Daten zu lesen oder Daten dorthin zu schreiben. Der Compiler legt zu diesem Zweck eine Symboltabelle an, in der zu jedem Variablenname die Anfangsadresse des zugehörigen Speicherbereichs vermerkt ist. Bei der Kompilation kann er dann jedes Vorkommen eines Variablennamens durch die passende Adresse ersetzen. Für das Anlegen dieser Symboltabelle ist es aber notwendig, daß jede Variable vor ihrer Verwendung deklariert wird, beispielsweise: int erste_Zahl;
Die wichtigste Angabe bei der Deklaration ist, neben dem Namen der Variablen, der Datentyp.
Datentypen Je nach Art der Daten, die in einer Variablen abgelegt werden können, unterscheidet man verschiedene Datentypen, beispielsweise: ●
int für ganze Zahlen
●
double für reelle Zahlen (auch Gleitkommazahlen genannt)
●
char für Zeichen (Buchstaben, Ziffern, Sonderzeichen)
●
bool für Boolesche Variablen, die einen Wahrheitswert annehmen können (true, false)
Die Datentypen bestimmen ●
48
die interne Darstellung der Werte der Variablen im Rechner. (Bedenken Sie, daß alle Daten im Speicher als eine Folge von Nullen und Einsen dargestellt
Variablen und Datentypen
werden müssen. Die Codierung eines Zeichens in eine Bitfolge erfolgt dabei nach anderen Regeln als die Codierung einer ganzen Zahl oder einer Gleitkommazahl), ●
●
●
die Größe des Speicherbereichs. (Eine Gleitkommazahl benötigt mehr Speicher als ein einzelnes Zeichen, und eine Strukturvariable benötigt meist mehr Speicher als eine Gleitkommazahl), den Wertebereich. (Dieser ergibt sich letztendlich aus der Größe des Speicherbereichs und der Art der Codierung), die Operationen, die auf den Werten des Datentyps ausgeführt werden können. (Beispielsweise kann man ganze Zahlen addieren, nicht aber Zeichenketten).
C/C++ unterscheiden zwischen elementaren Datentypen, die in der Sprache verankert sind (int, float, double, char, wchar_t und bool für C++), und zusammengesetzten Datentypen, die vom Programmierer definiert werden (Aufzählungen, Arrays, Strukturen und Klassen in C++). Nach ihren gemeinsamen Eigenschaften und Einsatzgebieten bei der Programmierung kann man die Datentypen weiter klassifizieren: ●
●
●
Integrale Typen. Der Datentyp bool, die Datentypen char und wchar_t, die Datentypen int und long, Aufzählungen (enum), Bitfelder. Integrale Datentypen sind als Datentyp für switch-Variablen erlaubt. Arithmetische Typen. Alle integralen Typen plus die Datentypen float, double und long double. Sie sind als Operanden für die arithmetischen Operatoren (+, --, *, /) erlaubt. Skalare Typen. Arithmetische Datentypen und Zeiger. Sie sind als Schleifenvariablen für for erlaubt. char wchar_t short int long
class struct union array Integertypen
Objekttypen
Aufzählungen
Arithmetische Typen Gleitkommatypen
Skalare Typen
Bitfelder
float double long double
Zeigertypen
49
Sprachkonzepte und Syntax
Die elementaren Datentypen short, int, long float, double, long double char, wchar_t bool
Beschreibung Die elementaren Datentypen sind Datentypen, die fest in Form von Schlüsselworten in der Sprache verankert sind.
Anwendung Mit Hilfe der elementaren Datentypen kann der Programmierer Variablen für die verschiedenen Arten von Daten deklarieren (Zahlen, Zeichen, Wahrheitswerte). Darüber hinaus kann man auf der Grundlage der elementaren Datentypen eigene zusammengesetzte Datentypen definieren. Die Programmiersprache C/C++ kennt folgende elementare Datentypen: ●
die Datentypen short, int und long für Ganzzahlwerte
●
die Datentypen float, double und long double für Gleitkommazahlen die Datentypen char und wchar_t für Zeichen (Buchstaben, Ziffern, Satzzeichen, etc.)
●
den Datentyp bool für Wahrheitswerte (nur C++)
●
Der Wertebereich der Datentypen – also der Bereich von Werten, die man Variablen dieses Datentyps zuweisen kann – hängt von der Anzahl Bytes ab, die der Compiler für eine Variable dieses Datentyps im Speicher bereitstellt. Bis auf den Datentyp bool sind alle diese Datentypen vorzeichenbehaftet, d. h., man kann den Variablen diesen Typs negative wie positive Werte zuweisen. Durch Voranstellung des Schlüsselworts unsigned kann man festlegen, daß für den Datentyp nur positive Werte erlaubt sind. Das Schlüsselwort signed gibt explizit an, daß ein Datentyp vorzeichenbehaftet ist, und ist üblicherweise redundant, da die Datentypen ehedem vorzeichenbehaftet sind. Einzige Ausnahme: der Datentyp char.
Warnung Für den Datentyp char ist gemäß ANSI nicht festgelegt, ob der Datentyp vorzeichenbehaftet ist oder nicht. Im Zweifelsfall empfiehlt sich bei der Variablendeklaration die explizite Deklaration als signed char oder unsigned char. Bezeichner
Bytes
Wertebereich*
bool
1
false, true
signed char
1
-128 bis 127
50
Speicherbelegung der Integer-Typen
Bezeichner
Bytes
Wertebereich*
unsigned char
1
0 bis 255
wchar_t
2
0 bis 65.535
int signed int
4
-2.147.483.648 bis 2.147.483.647
unsigned int
4
0 bis 4.294.967.295
short, short int
2
-32.768 bis 32.767
unsigned short int
2
0 bis 65.535
long, long int signed long int
4
-2.147.483.648 bis 2.147.483.647
unsigned long int
4
0 bis 4.294.967.295
float
4
ca. -3.4E +38 bis 3.4E+38
double
8
ca. -1.8E +308 bis 1.8E+308
long double
10
ca. -3.4E +4932 bis 3.4 E+4932
* Die angegebenen Wertebereiche sind für 32-Bit-Systeme typische Wertebereiche. Es ist allerdings zu beachten, daß die Wertebereiche von der Codierung der Werte in Binärdarstellungen abhängt. Diese Binärcodierung wird allerdings nicht bis ins Detail vom ANSI-Standard vorgegeben und ist daher implementierungsspezifisch – hängt also letztendlich vom jeweiligen Compiler ab.
Tip Die minimalen und maximalen Werte für die einzelnen Datentypen sind in den ANSI-C Headerdateien limits.h und float.h bzw. für C++ in den Spezialisierungen der Template-Klasse numeric_limits (aus limits) festgelegt und können von dort abgefragt werden.
Speicherbelegung der Integer-Typen short, int, long
Beschreibung Vorzeichenbehaftete Integer-Werte werden üblicherweise gemäß dem 2er-Komplement binärcodiert. Das erste Bit von links codiert dabei das Vorzeichen: 0 steht für positive Zahlen, 1 für negative Zahlen. Multiplikationen mit -1 entsprechen der Invertierung aller Bits mit anschließender Addition von 1.
51
Sprachkonzepte und Syntax
R|∑ a ⋅ 2 | ∂ ba , K , a g := S ||∑ a ⋅ 2 - 2 T n −1 i=0
n
0
i
n −1 i=0
i
i
i
für an = 0 n
für an = 1
Speicherbelegung int i = 3; 00000000
00000000
00000000
00000011
&i
Speicherbelegung der Gleitkommatypen float, double, long double
Beschreibung Gleitkommazahlen werden durch zwei Werte codiert, von denen der eine Mantisse, der andere Exponent genannt wird. In dieser sogenannten Exponential-Schreibweise wird z. B. die Zahl 54.123 folgendermaßen aussehen: 0,54123*10e4. Die Zahl 0,54123 ist in dem Beispiel die (normalisierte) Mantisse, die Zahl 4 der Exponent zur Basis 10 (Compiler verwenden allerdings üblicherweise die Basis 2). Der ANSI-Standard schreibt nicht explizit vor, wie Gleitkommazahlen zu codieren sind, die Codierung muß aber dem von ANSI vorgegebenen Gleitkommamodell folgen, und die Charakteristika der Codierung müssen in den Konstanten aus der Header-Datei float.h festgehalten werden (numeric_limits und numeric_limits<double> aus der Header-Datei limits für C++).
x = s ⋅ be ⋅
LM∑ f N p
k=1
k
OP Q
⋅ b− k , emin ≤ e ≤ emax
s = Vorzeichen b = Basis des Exponenten e = Exponent p = Anzahl Stellen der Mantisse (bestimmt die Genauigkeit) fk = Ziffer der Mantisse
52
Vorzeichen integraler Datenypen
Speicherbelegung Vorzeichenbit (1) Exponent (8)
Mantisse (23)
= single precision
Mantisse (52)
= double precision
Vorzeichenbit (1) Exponent (11)
Vorzeichen integraler Datenypen Beschreibung Durch Angabe der Schlüsselwörter unsigned beziehungsweise signed kann man bei der Variablendeklaration festlegen, ob in dem Datentyp das Vorzeichen codiert sein soll oder nicht. Entsprechend verschiebt sich der Wertebereich des Datentyps.
Anwendung Wertebereichsverschiebungen durch signed/unsigned: Datentyp
signed
unsigned
char
-128 bis 127
0 bis 255
short
-32768 bis 32767
0 bis 65535
int
-2.147.483.648 bis
0 bis 4.294.967.295
2.147.483.647 long
-2.147.483.648 bis
0 bis 4.294.967.295
2.147.483.647
Warnung ●
●
Variablen von unsigned Datentypen können nicht überlaufen. Wird versucht, Werte außerhalb ihres Gültigkeitsbereichs an sie zuzuweisen, wird automatisch der Modulo-Operator auf den Wert angewandt. Beachten Sie, daß in C/C++ die Datentypen von Operanden intern angeglichen werden (Arithmetische Konvertierungen). Vergleicht man beispielsweise einen signed-Wert mit einem unsigned-Wert, wird der signed-Wert als unsigned-Wert interpretiert. Auf diese Weise kann ein kleiner negativer Wert zu einem großen positiven Wert werden. (Beachten Sie insbesondere, daß der Rückgabewert des sizeof-Operators unsigned ist.)
53
Sprachkonzepte und Syntax ●
Liegt eine char-Variable mit Vorzeichen im Bereich von -128 bis 127, liegt eine entsprechende char-Variable ohne Vorzeichen im Bereich zwischen 0 und 255. Dies kann bei der Portierung unter Umständen zu Problemen führen. Man kann dem durch die Voranstellung eines der Qualifizierer signed oder unsigned vorbeugen.
Variablendeklaration datentyp variablenname;
Beschreibung Variablen sind vom Programmierer eingeführte Bezeichner, denen Werte zugewiesen werden können. Welche Werte einer Variablen zugewiesen werden können, hängt vom Datentyp der Variablen ab. Der Compiler verbindet die Variable mit einem Speicherbereich, in dem der jeweils aktuelle Wert der Variablen abgespeichert wird.
Anwendung Variablen sind für ein Programm wie ein Zwischenspeicher, in dem Daten abgelegt und bei Bedarf wieder hervorgeholt und weiter verarbeitet werden können. Eine Variable wird durch eine Deklaration eingeführt und ist grundsätzlich vom Ort ihrer Deklaration ab verfügbar.
Warnung Die Verfügbarkeit einer Variablen hängt von mehreren Faktoren ab: dem Ort der Deklaration, den bei der Deklaration verwendeten Spezifizierern, dem Gültigkeitsbereich und der Lebensdauer der Variablen.
54
Variablendeklaration
Syntax datentyp variablenname; datentyp var1, var2, var2; ●
●
datentyp: Bei der Deklaration wird zuerst der Datentyp durch Verwendung des entsprechenden Typbezeichners angegeben. Erlaubt sind sowohl elementare wie auch zuvor definierte, zusammengesetzte Datentypen. variablenname: Durch mindestens ein Leerzeichen getrennt, folgt auf den Typbezeichner der Name der Variablen. Es können mehrere Variablen gleichzeitig deklariert werden, wobei die Bezeichner durch Kommata getrennt werden.
Initialisierung Variablen kann man direkt im Zuge der Deklaration einen Wert zuweisen. int monate = 12;
Spezifizierer spezifizierer datentyp variablenname;
Die Variablendeklaration kann durch eine Reihe von speziellen Schlüsselwörtern näher spezifiziert werden: Schlüsselwort const
Legt fest, daß der Variablen nach der Initialisierung (Wertzuweisung im Zuge der Deklaration) kein anderer Wert mehr zugewiesen werden kann.
const int monate = 12; Schlüsselwort volatile
Teilt dem Compiler mit, daß diese Variable nicht etwaigen Optimierungen im Zuge der Kompilation zum Opfer fallen darf.
volatile double var; Speicherklassenspezifizierer
Zur expliziten Vergabe einer Speicherklasse. Zusammen mit dem Ort der Deklaration legt die Speicherklasse die Sichtbarkeit und Lebensdauer der Variablen fest.
auto extern register static
Speicherklasse
auto
Das Schlüsselwort auto weist einer Variablen explizit die automatische Speicherklasse zu. Diese Speicherklassenangabe kann nur innerhalb von Blockbereichen (und für Parameter) verwendet werden und ist im übrigen überflüssig, da lokale Variablen standardmäßig auto sind.
55
Sprachkonzepte und Syntax
Der Speicherplatz für automatische Variablen wird bei Abarbeitung des Blocks, in dem sie deklariert werden, auf dem Stack erzeugt, und beim Verlassen des Blocks wieder freigegeben.
Speicherklasse
register
Grundsätzlich entspricht der Spezifizierer register dem Schlüsselwort auto. Es teilt dem Compiler darüber hinaus aber mit, daß auf die derart deklarierte Variable so schnell wie möglich zugegriffen werden soll. Für PCs bedeutet dies, daß die Variablen in die Register des Prozessors geladen werden sollen. Der Compiler braucht dies jedoch nicht zu tun. In C kann im Gegensatz zu C++ die Adresse eines Objekts, das mit register deklariert worden ist, nicht mehr ermittelt werden, gleichgültig, ob dieses Objekt sich in einem Prozessorregister befindet oder nicht. In C gibt es daher keine Zeiger auf Register-Variablen. Grundsätzlich sollte man heutzutage die Aufteilung der Register den mittlerweile sehr leistungsfähigen Compilern überlassen und auf die Spezifikation des Schlüsselworts register verzichten.
Speicherklasse
static
Grundsätzlich impliziert das Schlüsselwort static lokale Gültigkeit und einmalige Allokation (letzteres bedeutet, daß statische Elemente nicht auf dem Stack angelegt werden). Damit ergeben sich drei wichtige Einsatzbereiche: Bezeichner, die global deklariert sind, haben üblicherweise externe Bindung (das heißt, in Programmen, die aus mehreren Modulen zusammengelinkt werden, bezeichnen sie in allen Modulen das gleiche Objekt). Werden Sie als static deklariert, sind sie nur innerhalb ihres Moduls gültig. Lokale Variablen von Funktionen werden bei Aufruf der Funktion neu allokiert und bei Verlassen der Funktion zerstört. Wird eine solche Variable als static deklariert, wird nur beim ersten Aufruf der Funktion Speicherplatz für die Variable reserviert und gegebenenfalls initialisiert. Zusätzlich endet ihre Lebensdauer erst mit dem Programm und nicht der Funktion. Die Sichtbarkeit bleibt unverändert. Klassen können nicht als static deklariert werden, aber Klassenelemente (siehe Kategorie Klassen). ●
56
Ein statisches Klassen-Datenelement erfüllt ähnliche Aufgaben wie eine globale Variable. Ein solches Datenelement wird nur einmal initialisiert (und zwar außerhalb der Klasse!) und dann von allen Instanzen der Klasse geteilt. (Üblicherweise hat jede Instanz der Klasse natürlich eine eigene Kopie der Klassenelemente.)
Variablendeklaration
●
Eine statische Klassenmethode ist eine Methode, die nur auf statische Klassenelemente zugreifen kann und wie ein statisches Datenelement von allen Instanzen der Klasse oder über den Klassennamen aufgerufen werden kann.
Deklarationen von Funktionsparametern und Funktionsdeklarationen in einem Blockbereich können nicht als static spezifiziert werden.
Speicherklasse,
extern
Mit dem Spezifizierer extern kann einem Objekt oder einer Funktion externe Bindung zugewiesen werden. Externe Bindung bedeutet dabei, daß alle Vorkommen eines Bezeichners mit externer Bindung sich auf ein und dasselbe Objekt beziehen. Bei der internen Bindung beziehen sich nur die Bezeichner aus einer Datei auf ein und dasselbe Objekt. Bezeichner ohne Bindung identifizieren eindeutige Objekte. ●
●
●
●
Objekte und Funktionen, die in einem Namensbereich oder global deklariert sind, haben automatisch externe Bindung, es sei denn, sie wurden schon zuvor mit einem anderen Speicherklassenspezifizierer deklariert. (In C++ haben zudem auch globale Objekte, die als const deklariert sind, interne Bindung.) Klassenelemente und Funktionsparameter können nicht als extern deklariert werden. Objektdefinitionen ohne gleichzeitige Initialisierung werden durch den Spezifizierer als reine Deklaration gekennzeichnet. Auf diese Weise kann ein Objekt mit externer Bindung in allen Modulen bekannt gemacht werden, ohne daß es zu mehrfachen Definitionen kommt. C++ erlaubt die mehrfache Definition von Funktionen gleichen Namens, soweit sich diese durch Anzahl und Typen ihrer Parameter unterscheiden. Intern wird diese Namensmehrdeutigkeit vom Compiler so aufgelöst, daß er den einzelnen Funktionsdefinitionen neue Namen zuordnet, in die die Parameter in codierter Form mit aufgenommen sind. Dies ist nicht immer erwünscht, beispielsweise, wenn Funktionen aus kompiliertem C-Code benutzt werden. Die Namenserweiterung kann daher durch den Bindungs-Spezifizierer extern "C" unterdrückt werden.
Beispiele short int woche; char zeichen; int tag, monat; int jahr = 1998; // Initialisierung const int monate = 12; // konstante Variable
Verweise Siehe Gültigkeitsbereiche, Sichtbarkeit, Lebensdauer
57
Sprachkonzepte und Syntax
Gültigkeitsbereiche Beschreibung In C/C++ wird jeder Variablen in Abhängigkeit davon, an welcher Stelle des Programms die Variable deklariert wird, ein Gültigkeitsbereich zugeordnet.
Anwendung Der Gültigkeitsbereich einer Variablen ist der Bereich eines Programms, in dem eine Variable gültig ist. Variablen können nur innerhalb ihres Gültigkeitsbereichs und nach ihrer Deklaration verwendet werden.
Tip Diese automatische Vergabe von Gültigkeitsbereichen kann vom Programmierer durch die Aufnahme bestimmter Speicherklassenspezifizierer in die Deklaration an seine Bedürfnisse angepaßt werden.
Gültigkeitsbereiche ●
Blockbereich: Variablen, die innerhalb eines Anweisungsblocks (abgegrenzt durch die geschweiften Klammern {..}) deklariert werden, sind von lokaler Gültigkeit. Sie sind nur innerhalb des Blocks bekannt und existieren nur solange, wie der Block abgearbeitet wird. Der Compiler erzeugt diese Variablen dynamisch. Nach der Beendigung des Blocks wird der Speicherplatz der lokalen Variablen freigegeben. Damit ist auch ihr Wert verloren. Lokale Variablen werden zumeist innerhalb von Funktionen deklariert. C erlaubt Deklarationen nur zu Beginn eines Blocks, also vor irgendwelchen Anweisungen. C++ erlaubt Deklarationen dagegen an jeder beliebigen Stelle im Block (sogar in Schleifenheadern).
●
●
●
●
58
Dateibereich: Variablen, die außerhalb jeden Anweisungsblocks, jeder Klasse und jeden Namensbereiches deklariert werden, sind von globaler Gültigkeit und können ab dem Punkt ihrer Deklaration benutzt werden. Sie haben globale Lebensdauer, das heißt, sie existieren vom Start des Programms bis zu dessen Ende. Funktionsbereich: Dieser Bereich gilt nur für die Vergabe von Marken für die goto-Anweisung , denn Marken dürfen nicht aus der Funktion, in der sie deklariert wurden, herausweisen. Klassenbereich (nur C++): Elemente von Klassen sind – mit Ausnahme statischer Elemente – nur innerhalb ihrer Klasse gültig. Sie werden bei der Instanzbildung erzeugt und bei der Auflösung der Instanz zerstört. Namensbereiche (nur C++): In C++ ist es möglich, den globalen Gültigkeitsbereich durch die Deklaration von Namensbereichen aufzuteilen. Die Dekla-
Namensbereiche
ration eines Namensbereiches faßt eine Reihe von Deklarationen in geschweifte Klammern zusammen. Eingeleitet wird die Deklaration durch das Schlüsselwort namespace.
Beispiel #include <stdio.h> namespace Spezielles { int var1;
// Deklarationen in Namensbereich
class demo { // Deklarationen in Klassenbereich int wert; public: demo(): wert(3) {} }; } void func(int param) { int var; }
// Deklarationen in Blockbereich
int var;
// Deklarationen in Dateibereich
int main() { ... return 0; }
Verweise Siehe Namensbereiche Siehe Sichtbarkeit, Lebensdauer
Namensbereiche namespace bereichsname {
}
Beschreibung Mit Hilfe des Schlüsselwortes namespace kann der Programmierer den globalen Gültigkeitsbereich (Dateibereich) in Unterbereiche aufteilen.
59
Sprachkonzepte und Syntax
Zudem können Deklarationen aus Namensbereichen gezielt in bestimmte Gültigkeitsbereiche eingeführt werden (etwa in den Blockbereich einer Funktion oder einen anderen Namensbereich).
Anwendung Die Aufteilung des globalen Gültigkeitsbereichs ist vor allem für Programmierer interessant, die mit anderen Programmierern zusammen an umfangreicheren Softwareprojekten arbeiten. Würden in so einem Fall alle Programmierer ihre globalen Variablen in einem gemeinsamen globalen Gültigkeitsbereich deklarieren, kann es bei der Zusammenführung der Quelltextdateien zu einem Programm schnell zu Fehlermeldungen des Linkers wegen mehreren globalen Variablen gleichen Namens kommen. Durch die Einrichtung individueller Namensbereiche kann man dem abhelfen. Zudem steht es dem Programmierer, der Elemente aus einem Namensbereich verwenden will, frei, diese bei Bedarf einzeln anzusprechen oder mit Hilfe des usingSchlüsselwortes alle Deklarationen aus dem Namensbereich mit einer Anweisung in seinen eigenen globalen Gültigkeitsbereich oder den Blockbereich einer speziellen Funktion einzuführen.
Deklaration von Namensbereichen namespace bezeichner { // Deklarationen } ●
●
●
Namensbereiche können nur im globalen Bereich oder innerhalb eines anderen Namensbereiches deklariert werden. Namensbereichen, die ohne eigenen Bezeichner deklariert wurden, wird der Bezeichner unique zugewiesen, der bei der Kompilation durch einen eindeutigen Bezeichner ersetzt wird. Dieser Bezeichner ist in jeder Übersetzungseinheit (Datei) verschieden. Die Deklaration eines Objektes im unbenannten Namensbereich kommt daher einer Deklaration als static im globalen Bereich gleich. Elemente eines Namensbereiches können direkt innerhalb der Deklaration des Namensbereiches definiert werden oder außerhalb unter Verwendung des Namensbereich-Bezeichners und des Bereichsoperators.
Namensbereiche verwenden using namespace NameDesNamensbereichs;
Mit Hilfe des Schlüsselwortes using kann ein Namensbereich in einen anderen Gültigkeitsbereich eingeführt werden. Danach sind alle in dem neu eingeführten Namensbereich deklarierten Elemente auch in dem Gültigkeitsbereich bekannt, in den der Namensbereich eingeführt wurde.
60
Namensbereiche
Nach Einführung des Namensbereichs kann man im umliegenden Gültigkeitsbereich auf die Elemente aus dem Namensbereich direkt über den Bezeichner des Elements (ohne Voranstellung des Namensbereichs) zugreifen. ●
●
●
Gibt es im umliegenden Gültigkeitsbereich ein Element, das genauso heißt wie eines der Elemente aus dem eingeführten Namensbereich, verdeckt das Element aus dem umliegenden Gültigkeitsbereich das Element aus dem Namensbereich. Durch Voranstellung des Namensbereichs kann auf das Element des Namensbereichs zugegriffen werden. Unbenannte Namensbereiche sind immer im umliegenden Namensbereich bekannt. Namensbereiche können nicht in Klassen, wohl aber in Methoden von Klassen eingeführt werden.
Namensbereiche erweitern namespace bezeichner { // Deklarationen } namespace bezeichner { // weitere Deklarationen }
Namensbereiche können mehrfach deklariert werden, wobei nachfolgende Deklarationen als Erweiterungen behandelt werden. Auf diese Weise können Namensbereiche über Dateigrenzen hinweg deklariert, ausgetauscht und erweitert werden.
Zugriff auf Elemente in Namensbereichen Elemente, die in Namensbereichen deklariert sind, können außerhalb des Namensbereichs nicht einfach über den Namen, unter dem sie im Namensbereich deklariert sind, aufgerufen werden. Entweder ●
●
●
muß der vollständige Bezeichner (einschließlich des Namens des Namensbereichs) verwendet werden, oder der Bezeichner wird durch eine using-Anweisung eingeführt. Dies entspricht einer Neudeklaration. Nach der Neudeklaration kann auf das Element des Namensbereichs ohne Voranstellung des Namensbereichs zugegriffen werden. Gibt es allerdings im umliegenden Gültigkeitsbereich bereits einen gleichlautenden Bezeichner scheitert die Neudeklaration. der Namensbereich und alle in ihm enthaltenen Deklarationen werden durch eine using-Anweisung eingeführt. Danach kann man auf die Elemente des Namensbereichs ohne Voranstellung des Namensbereichs zugreifen. Es liegt dabei jedoch keine Neudeklaration vor. Werden im umliegenden Gültigkeitsbereich und im Namensbereich gleichlautende Bezeichner verwendet, verdecken die
61
Sprachkonzepte und Syntax
Bezeichner des umliegenden Gültigkeitsbereichs die Elemente aus dem Namensbereich. Durch vollständige Qualifizierung (Voranstellung des Namensbereichs vor den Bezeichner) kann jedoch auch auf die verdeckten Elemente zugegriffen werden.)
Synonyme namespace Deklarationen_für_speziellen_Zweck namespace Spez1 = Dekarationen_für_speziellen_Zweck
Für die Bezeichner von Namensbereichen können Synonyme eingeführt werden. Für Namensbereiche mit extralangen Namen können so kürzere Aliase definiert werden.
Beispiele Das folgende Programm demonstriert die Definition und Einführung von Namensbereichen: namespace A { int i = 1; } namespace B { int i = 2; using namespace A; } int main() { //cout << i; cout << A::i; using namespace A; cout << i; using namespace B; //cout << i; return 1; }
// Definition eines Namensbereichs
// Einführung eines Namensbereichs
// Fehler, da i nicht global deklariert // und kein Namensbereich angeschaltet // ok, Zugriff über vollständigen Qualifizierer // ok, liefert A::i // Fehler, kann A::i oder B::i sein
Das zweite Beispiel zeigt Möglichkeiten und Fallstricke bei der Erweiterung von Namensbereichen: namespace demo { int i = 1; void func1(int); void func2(int); class X { int i = 2; }; } namespace demo { //int i = 3;
62
// demo::i
// demo::X::i
// Erweiterung // Fehler: doppelte Definition
Sichtbarkeit und Verdeckung void func1(float); // Überladung void func2(int); // ok: Redeklaration namespace { int i = 4; // demo::unique::i } }
Verweise Siehe Kapitel über Verwendung der Standardbibliotheken
Sichtbarkeit und Verdeckung Beschreibung Prinzipiell ist jedes per Deklaration eingeführte Element ab dem Ort der Deklaration in seinem Gültigkeitsbereich sichtbar kann also über seinen Namen angesprochen und benutzt werden.
Verdeckung Wird ein und derselbe Bezeichner mehrfach benutzt, um Elemente in ineinander verschachtelten Gültigkeitsbereichen zu deklarieren (beispielsweise für eine globale Variable und eine gleichlautende lokale Variable in einer Funktion), verdeckt die Variable des innersten Gültigkeitsbereichs alle Variablen der umschließenden Gültigkeitsbereiche. In C++ kann man auf verdeckte Variablen aus dem globalen Gültigkeitsbereich mit Hilfe des Gültigkeitsbereichoperators weiterhin zugreifen: ●
●
für globale Variablen aus dem Dateibereich stellt man dem Bezeichner einfach den Operator voran ::var_name für Variablen aus Namensbereichen gibt man zusätzlich den Namensbereich an: namensbereich::var_name
Zudem kann man in abgeleiteten Klassen auf verdeckte Elemente der Basisklassen über den Gültigkeitsbereichoperator zugreifen: basisklasse::element.
Warnung Die Mehrfachverwendung eines Bezeichners innerhalb eines Gültigkeitsbereichs ist nur in Ausnahmefällen möglich: ●
Klassen- und enum-Bezeichner können durch Bezeichner für Variablen, Funktionen und enum-Elemente im gleichen Gültigkeitsbereich verdeckt werden.
63
Sprachkonzepte und Syntax ●
Die Überladung von Funktionen in C++ beruht auf der Verwendung des gleichen Bezeichners für die Definition mehrerer Funktionen in einem Gültigkeitsbereich. (Der Compiler löst die Namen intern anhand der Parameterliste auf.)
Beispiele int wert = 1; void func() { int wert = 2; cout << " wert = " cout << "::wert = " };
// global deklariert // lokal deklariert, verdeckt globales wert << wert << endl; // Ausgabe: 2 << ::wert << endl; // Ausgabe: 1
Lebensdauer Beschreibung Wenn Sie eine Variable definieren, erzeugt der Compiler für diese Variable ein Objekt im Speicher (auf das Sie über den Namen der Variablen zugreifen können). In ähnlicher Weise werden Objekte im Speicher erzeugt, wenn Sie dynamischen Speicher reservieren, wenn bei einem Funktionsaufruf die Parameter eingerichtet werden, wenn Instanzen von Klassen gebildet werden (wobei Klassenobjekte erst nach Aufruf des Konstruktors existieren). Wie lange diese Objekte verfügbar und existent sind, hängt von verschiedenen Faktoren ab.
Anwendung ●
●
●
Ort der Deklaration. Variablen (einschließlich der Instanzen von Klassen), die in einem Blockbereich definiert werden, existieren nur bis zum Ende des Blockbereichs und werden bei Verlassen des Blocks aufgelöst (liegt daran, daß der Speicher für die Variablen auf dem Stack abgelegt wird). Variablen, die global deklariert werden, existieren bis zum Programmende. Speicherklassenspezifizierer static. Für Variablen, die innerhalb eines Blockbereichs als static deklariert werden, wird der Speicher nicht auf dem Stack reserviert. Die Variablen existieren bis zum Programmende. Art der Speicherreservierung. Objekte, für die der Speicher dynamisch reserviert wird (mit malloc() oder new), wird der Speicher auf dem Heap reserviert und wird erst freigegeben, wenn der Programmierer dies explizit fordert (free() oder delete) oder das Programm beendet wird.
Verweise Siehe Funktionen, Funktionen und der Stack
64
Zusammengesetzte Datentypen
Zusammengesetzte Datentypen Zusammengesetzte Datentypen Beschreibung Mit den elementaren Datentypen ist bereits der Grundstock für die Datenverarbeitung in Programmen gelegt. Tatsächlich käme man sogar ohne die Möglichkeit zur Definition weiterer Datentypen aus, doch die Programmierung würde sich dann in vielen Fällen äußerst mühsam gestalten. Aus diesem Grunde sieht C/C++ verschiedene Möglichkeiten zur Definition eigener Datentypen vor. Bevor wir uns diese aber im einzelnen anschauen werden, sollte man sich noch einmal klar machen, daß man mit der Definition eines neuen Datentyps nicht nur die eigenen Bedürfnisse, sondern auch die Bedürfnisse des Compilers befriedigen muß. Konkret bedeutet dies, daß der Compiler wissen muß, wie er für Variablen dieses Datentyps Speicher belegen muß, wie die Werte der Variablen codiert und im Speicher abgelegt werden sollen, welche Operationen auf den Variablen des Datentyps erlaubt sind und schließlich wie Typkonvertierungen vorgenommen werden können. Aus all diesen Anforderungen könnte man nun ablesen, daß die Möglichkeiten zur Definition eigener Datentypen entweder sehr eingeschränkt oder mit sehr viel Arbeit verbunden sind. Nun beides ist nicht der Fall. Der Trick dabei ist einfach, daß man die neuen selbst definierten Datentypen aus den elementaren Datentypen ableitet. Insgesamt stehen Ihnen folgende Möglichkeiten zur Definition eigener Datentypen zur Verfügung: Definition eines Datentyps durch Aufzählung der Wertemenge des Datentyps (enum) Definition eines Datentyps durch Zusammenfassung mehrerer Variablen eines Datentyps (Arrays) Definition eines Datentyps durch Zusammenfassung mehrerer Variablen verschiedener Datentypen (Strukturen) Definition eines Datentyps durch Zusammenfassung mehrerer Variablen verschiedener Datentypen, und durch Bereitstellung typspezifischer Funktionen und Operatoren (Klassen). Dies ist die freieste und weitreichendste Form der Typdefinition. Schließlich gibt es noch die Möglichkeit, Zeiger zu definieren, mit denen man direkt auf Speicherbereiche zugreifen und Objekte im Speicher manipulieren kann.
65
Sprachkonzepte und Syntax
Aufzählungstypen enum {element1, element2, ...}; enum typbezeichner {element1, element2, ...};
Beschreibung Aufzählungstypen sind Datentypen, deren mögliche Werte durch explizite Aufzählung von Integer-Konstanten festgelegt werden. Die aufgeführten Werte können im wesentlichen wie im umgebenden Gültigkeitsbereich definierte Konstanten verwendet werden.
Anwendung Bei allen anderen Datentypen sind die Wertebereiche durch die interne Codierung der Werte in Binärdarstellung vorgegeben. Welche Werte beispielsweise eine intVariable annehmen kann, hängt nicht von Ihnen, sondern von Ihrem Compiler ab. Bestenfalls können Sie über Spezifizierer wie short, long oder unsigned auf den Wertebereich einwirken. Bei einem Aufzählungstyp können Sie durch explizite Auflistung selbst angeben, welche Werte zum Wertebereich des neuen Aufzählungstyps gehören. Dies kann einen Programmcode lesbarer und leichter verständlich machen. In C++ kommt hinzu, daß der Compiler prüfen kann, daß Variablen eines Aufzählungstyps auch wirklich nur Werte aus seinem Wertebereich zugewiesen werden. Da intern alle Werte eines Aufzählungstyps durch Integer-Konstanten repräsentiert werden, kann man Werte aus Aufzählungen beispielsweise auch in switch-Verzweigungen verwenden.
Syntax enum {element1, element2, ...}; enum typbezeichner {element1, element2, ...}; ●
●
66
elemente: Die in einem Aufzählungstyp definierten Elemente werden wie Integer-Konstanten behandelt. Damit die Konstanten nicht undefinierte Werte annehmen, werden sie vom Compiler automatisch initialisiert. Nichtinitialisierte Elemente bekommen dabei den Wert ihres Vorgängers +1 zugewiesen. Dem ersten Element wird 0 zugewiesen. Mit Hilfe des Zuweisungsoperators kann der Programmierer den Elementen selbst Werte zuordnen. Es sind allerdings nur Integer-Werte erlaubt oder Werte, die als Integer-Werte interpretiert werden können. Verschiedene Elemente dürfen gleiche Werte haben. typbezeichner: Wird ein typbezeichner spezifiziert, kommt dies einer Typdefinition gleich. Später können dann Variablen dieses Typs definiert werden. In C lautet der Typ enum typbezeichner, in C++ reicht einfach der typbezeichner.
Arrays
Variablendeklaration enum {elem1 = 1, elem2 } var1, var2;
Oder enum typbez {elem1 = 1, elem2 }; enum typbez var1, var2;
Wertzuweisung var1 = elem2; var1 = typbez(3);
Warnung Der Compiler reserviert für enum-Variablen Speicher, der groß genug sein muß, um alle Integer-Werte zwischen dem kleinsten und größten Wert des enum-Typs aufnehmen zu können. Prinzipiell kann man enum-Variablen jeden Wert innerhalb dieses Wertebereichs zuweisen, auch wenn der Wert nicht explizit in der Aufzählung definiert ist (in C++ erfordert dies eine explizite Typumwandlung).
Beispiele #include <stdio.h> enum {ERROR_LESEN = 101,ERROR_SCHREIBEN
= 102};
void fehlermeldung(int fehlernummer) { switch(fehlermeldung) { case ERROR_LESEN: /* Anweisungen */ break; case ERROR_SCHREIBEN: /* Anweisungen */ break; default: break; } } int main() { fehlermeldung(ERROR_NO_MEM); return 0; }
Arrays typ feldname[groesse];
Beschreibung Ein Array, auch Datenfeld oder Feld genannt, ist die Zusammenfassung von mehreren Daten des gleichen Typs zu einer Variablen.
67
Sprachkonzepte und Syntax
Anwendung Arrays ermöglichen Ihnen, eine (nahezu) beliebige Zahl von Variablen gleichen Datentyps en bloc zu definieren: int i[100];
Der Vorteil der Array-Deklaration liegt jedoch nicht nur in der eingesparten Tipparbeit. Die einzelnen Elemente eines Arrays werden im Speicher hintereinander, in einem zusammenhängenden Block abgelegt. Dies bringt eine Reihe interessanter Vorteile, beispielsweise die Möglichkeit, über einen Index auf einzelne Elemente im Block zuzugreifen: i[3] = i[2]+i[1];
Syntax typ feldname[groesse]; ●
●
●
typ: Die Typangabe legt fest, aus welchen Elementen das Array gebildet wird, und liefert zusammen mit der groesse Informationen über den Speicherplatzbedarf (groesse * sizeof(typ)). Folgende Datentypen sind für Elemente eines Arrays nicht zulässig: Referenzen, Funktionen, abstrakte Klassen. feldname: Frei wählbarer Name für die Array-Variable. Array-Typen sind immer an Variablen gebunden. Dies ist eine Besonderheit der Arrays. groesse: Die groesse legt die Anzahl der Elemente fest, aus denen das Array besteht. Als Größenangabe sind nur konstante Ausdrücke erlaubt, die größer Null sind. Die Größenangabe kann auch weggelassen werden. In solch einem Fall muß eine vollständige Deklaration nachfolgen, oder das Array muß bei der Deklaration mit Hilfe einer Initialisierungsliste initialisiert werden. (Aus der Anzahl der Initialisierungswerte wird dann die Dimension errechnet.)
Variablendeklaration int feld[3]; struct person leser[40];
// Array für drei int-Objekte // 40 Elemente vom // Typ struct person
Oder #define ANZAHL 400; double feld[ANZAHL];
Initialisierung Es ist möglich, bei der Definition eines Arrays Initialisierungen vorzunehmen. Dem Namen des Arrays folgt dann das Gleichheitszeichen und in geschweifte Klammern gefaßt die einzelnen Werte, die zugewiesen werden sollen.
68
Arrays
●
●
Es ist nicht erforderlich, alle Elemente zu initialisieren. Elemente, die nicht initialisiert worden sind, erhalten automatisch den Wert 0. Fehlt die Größenangabe, wird die Größe aus der Anzahl der Elemente errechnet.
float werte[3] = { 2.34, 5.76 };
float werte[] = { 2.34, 5.76 };
/* entspricht: werte[0] = 2.34; werte[1] = 5.76; werte[2] = 0; */ /* entspricht float werte[2] */
Zugriff auf Array-Elemente Auf die einzelnen Elemente des Arrays wird über einen Index zugegriffen, der in eckigen Klammern steht. Das erste Element eines Arrays hat immer den Index 0. Das letzte Element hat den Index groesse – 1. feld[0] = 13;
Oder #define ARRAY_GROESSE 20 int array[ARRAY_GROESSE]; for (i=0; i < ARRAY_GROESSE; i++) array[i] = i;
Warnung Die Indizierung der Elemente im Array beginnt immer mit 0, weswegen beispielsweise die Indizes für ein Array, das als feld[20] deklariert ist, von 0 bis 19 laufen. Der Zugriff über höhere Indizes ist möglich (der Compiler kontrolliert nicht die Verwendung korrekter Indizes), führt aber fast ausnahmslos zu Laufzeitfehlern. Wenn man sowohl die Arraygröße angibt als auch eine Initialisierungsliste definiert, darf letztere nicht mehr Elemente enthalten als durch die Arraygröße festgelegt.
Beispiele #include <stdio.h> #include <stdlib.h> #include <mem.h> #define MAX 100 int main() { int feld[MAX]; int i; memset(feld, 0, sizeof(feld));
// Anzahl der Elemente in Array
// Array deklarieren
// Initialisieren des Arrays
69
Sprachkonzepte und Syntax // Array ganz durchlaufen und Zufallswerte zuweisen for (i = 0; i < MAX; i++) { feld[i] = rand() % 100; } // Array durchlaufen und Werte ausgeben for (i = 0; i < MAX; i++) { printf("Element %d \t = %d\n", i, feld[i] ); } return 0; }
Verweise Siehe Mehrdimensionale Arrays Siehe Zeichenketten (Strings)
Mehrdimensionale Arrays typ feldname[dim1][dim2];
Beschreibung Zwei- oder mehrdimensionale Arrays sind Arrays, deren Elemente wiederum Arrays sind. Auch mehrdimensionale Arrays werden nacheinander im Speicher abgelegt. Das Prinzip funktioniert so, daß der letzte Index sich am schnellsten verändert. Die Elemente des Arrays int matrix[3][2];
würden im Speicher also in folgender Reihenfolge abgelegt: matrix[0][0] matrix[0][1] matrix[1][0] matrix[1][1] matrix[2][0] matrix[2][1]
Anwendung Mehrdimensionale Arrays werden am sinnvollsten dort eingesetzt, wo Daten verwaltet werden sollen, die von sich aus schon mehrdimensional organisiert sind, so daß sich eine 1:1-Entsprechung zwischen der natürlichen Anordnung der Daten und ihrer Abspeicherung im Array ergibt.
70
Mehrdimensionale Arrays
Ein Beispiel wären die Elemente einer zweidimensionalen Matrix, die sich wunderbar in einem zweidimensionalen Array verwalten lassen.
Syntax typ feldname[dim1][dim2]; ●
typ: Die Typangabe legt fest, aus welchen Elementen das Array gebildet wird.
●
feldname: Frei wählbarer Name für die Array-Variable.
●
dim: Die Definition enthält so viele Paare eckiger Klammern mit Größenangaben, wie das Array Dimensionen besitzt. Bei mehrdimensionalen Arrays kann die erste eckige Klammer leer bleiben. Dies erklärt sich dadurch, daß ein mehrdimensionales Array als ein einfaches Array angesehen wird, dessen Elemente wiederum Arrays sind. Die Größe des Arrays darf später deklariert werden, aber die Größe der Elemente muß bei der Deklaration feststehen.
Variablendeklaration int feld[3][2];
Initialisierung Mehrdimensionale Arrays können auf zwei Arten initialisiert werden. Entweder wird für jede Komponente eine eigene Initialisierungsliste benutzt, die in geschweifte Klammern eingeschlossen wird, oder jeder Komponenten des untergeordneten Aggregattyps werden die Werte einfach der Reihe nach zugewiesen. int matrix[][2] = {
{ 1, 2 }, { 10, 20 }, { 100, 200 } };
Oder int matrix[3][2] = { 1, 2 , 10, 20, 100, 200 };
Zugriff auf Array-Elemente Um auf ein Element eines mehrdimensionalen Arrays zuzugreifen, muß man für jede Dimension einen Index angeben. var = matrix[2][0];
Obige Anweisung greift auf das dritte Unterarray (Indizierung beginnt mit 0!) und in dem Unterarray auf das erste Element zu (wäre im Beispiel aus dem Initialisierungsabschnitt das Element mit dem Wert 100).
Beispiele #include <stdio.h> #define SPALTEN 10 #define ZEILEN 5
71
Sprachkonzepte und Syntax int main() { int x[ZEILEN][SPALTEN]; int i = 0; int j = 0; for (i = 0; i < ZEILEN; i++) { for (j = 0; j < SPALTEN; j++) { x[i][j] = 'a' + i; } } for (i = 0; i < ZEILEN; i++) { for (j = 0; j < SPALTEN; j++) { printf("%c ",x[i][j]); } printf("\n"); } return 0; }
Ausgabe a b c d e
a b c d e
a b c d e
a b c d e
a b c d e
a b c d e
a b c d e
a b c d e
a b c d e
a b c d e
Verweise Siehe Arrays
Zeichenketten (Strings) charTyp str[groesse];
Beschreibung Arrays bieten sich auch als Variablen für Zeichenketten (Strings) an. In diesem Fall sind die Elemente des Arrays die Zeichen der Zeichenkette.
Anwendung
72
Zeichenketten (Strings)
ANSI C/C++ verwendet zur Repräsentation von Text die sogenannten ASCIIZStrings. Dies sind Zeichenketten, die durch das Zeichen \0 abgeschlossen werden. Variablen, die einen solchen Strig aufnehmen können, kann man auf verschiedene Weise deklarieren: ●
als Array mit Elementen eines char-Typs.
●
als Zeiger auf einen char-Typ.
●
als Instanz einer string-Klasse (nur C++).
Entscheidet man sich für ein Array, genießt man den Vorteil, daß zusammen mit der Array-Deklaration auch gleich der Speicher für den String reserviert wird. Man muß aber aufpassen, daß man der Array-Variablen keinen String zuweist, der aus mehr Zeichen besteht als das Array reserviert hat.
Warnung Beachten Sie zudem, daß ein String in C/C++ immer mit dem Zeichen \0 abgeschlossen werden muß. Dieses Terminierungszeichen ist bei der Berechnung des Speicherbedarfs mit einzurechnen.
Syntax charTyp feldname[groesse]; ●
charTyp: Als Elementtyp wählt man in diesem Fall char, signed char, unsigned char oder wchar_t.
● ●
feldname: Frei wählbarer Name für die Array-Variable. dim: Anzahl der Zeichen, für die Speicher reserviert wird und die in dem Array abgelegt werden können.
Initialisierung char str[ ]
= {'e', 'i', 'n', 's', '\0'};
// mit Initialisierungsliste
Oder char str2[50] = "Dies ist ein String"; // mit String-Literal wchar_t str3[10] = L"zwei"; // Wide Character-Zeichen (UNICODE)
Bei der Initialisierung mit String-Literalen wird das abschließende Nullterminierungszeichen \0 automatisch durch den Compiler angehängt.
Zuweisung Die Zuweisung mit Hilfe des Zuweisungsoperators ist nur bei der Initialisierung möglich. Danach bedient man sich am besten der Funktionen der Laufzeitbibliothek (C-Header-Datei string.h). strcpy(str2, "Neuer Text"); strcat(str1, str2);
// kopieren // anhängen
73
Sprachkonzepte und Syntax
Zugriff auf einzelne Zeichen str1[4] = 'c';
Beispiele #include <stdio.h> #include <string.h> int main() { char string1[] = "C und C++"; char string2[] = "C++ und C"; if(strcmp(string1, string2) > 0) printf("%s ist groesser als %s\n",string1,string2); else printf("%s ist kleiner oder gleich %s\n", string1, string2); return 0; }
Ausgabe C und C++ ist kleiner oder gleich C++ und C
Verweise Siehe Arrays Siehe Zeichenketten, C-Strings
Strukturen struct typbezeichner { typ1 element1; typ2 element2; };
Beschreibung Strukturen fassen mehrere Daten, die unterschiedlichen Datentypen angehören können, zu einer Variablen zusammen. In C++ ist eine Struktur ein Sonderfall einer Klasse, in der alle Elemente standardmäßig public sind.
Anwendung Strukturen sind Datentypen, in denen mehrere Variablen verschiedener Datentypen zusammengefaßt werden können. Die in der Struktur zusammengefaßten »Variablen« bezeichnet man als Felder oder Elemente der Struktur. Der Vorteil dabei ist, daß mehrere, logisch zusammengehörende Daten in einem Datentyp vereinigt werden können.
74
Strukturen
Ein typisches Beispiel wäre die Definition von Variablen zum Abspeichern von Vektoren. Für einen zweidimensionalen Vektor benötigt man zwei int-Werte für die x- und die y-Koordinate. Diese als einzelne int-Variablen zu definieren, ist unbequem und äußerst unübersichtlich: int v1_x, v1_y; int v2_x, v2_y;
// erster Vektor // zweiter Vektor
Besser ist die Einführung eines Strukturtyps für Vektoren: struct int int }; struct
vektor { x; y; vektor v1, v2;
Warnung ●
●
Eine Struktur kann keine Variable ihres eigenen Typs als Element enthalten. Zeiger oder Referenzen auf Variablen ihres eigenen Typs sind aber erlaubt. In C++ sind Strukturen Klassen mit public-Zugriff auf die Strukturelemente. In C++ dürfen Strukturen daher auch Funktionen als Elemente enthalten.
Syntax struct typbezeichner { typ1 element1; typ2 element2; /* weitere Elemente */ }; ●
●
typbezeichner: Wird ein typbezeichner spezifiziert, kommt dies einer Typdefinition gleich. Später können dann Variablen dieses Typs definiert werden. In C lautet der Typ struct typbezeichner, in C++ reicht einfach der typbezeichner. typ element: Innerhalb der Klammern werden die Elemente der Struktur deklariert.
Definition von Strukturvariablen Strukturvariablen können entweder nach der Definition der Struktur mit Hilfe des Typbezeichners definiert werden (vorausgesetzt ein Typbezeichner wurde angegeben) oder direkt im Zuge der Strukturdefinition. struct struct person struct
person { /* Definition */ }; person leser; autor; // nur C++ person leser[40]; // 40 Elemente vom Typ struct peson
Oder struct { int x;
75
Sprachkonzepte und Syntax int y; float laenge; } vektor1, vektor2;
Initialisierung Strukturvariablen können bei ihrer Definition direkt initialisiert werden. Dazu werden die Werte, mit denen die Strukturelemente initialisiert werden sollen, in geschweiften Klammern aufgelistet und mittels des Zuweisungsoperators der Variablen zugewiesen. ●
Es ist nicht erforderlich, alle Elemente der Struktur zu initialisieren.
struct person { char name[20]; int plz; char ort[20]; char strasse[20]; }; struct person leser = {"Klaus Müller", 7652 , "Entenhausen", "Duckstr. " };
Zugriff auf Strukturelemente Es gibt zwei Möglichkeiten, um auf die Elemente einer Struktur zuzugreifen. Das benutzte Verfahren hängt davon ab, ob man über eine Strukturvariable oder einen Zeiger auf eine Struktur, auf die Elemente zugreift. strukturvariable.strukturelement strukturzeiger->strukturelement
Beispiele struct person { char name[20]; int plz; char ort[20]; }; int main() { struct person leser; struct person *pleser; pleser = &leser; leser.plz = 5100; strcpy(leser.name, "Otte"); strcpy(pleser->ort, "Karlsbad"); printf("Leser: %s, PLZ: %d Ort: %s\", leser.name, leser.plz, leser.ort); printf("Leser: %s, PLZ: %d Ort: %s\",
76
Bitfelder pleser->name, pleser->plz, pleser->ort); return 0; }
Bitfelder struct typbezeichner { intTyp1 element1 : Zahl; intTyp2 element2 : Zahl; };
Beschreibung Bitfelder sind besondere Elemente in Strukturtypen, für die man explizit angeben kann, wieviel Bit für das Element benötigt werden.
Anwendung Bitfelder werden benutzt, wenn Daten gespeichert werden sollen, die weniger als 1 Byte Speicherplatz benötigen. Hierzu gehören zum Beispiel 1 Bit große Flags, die ausreichen, um Wahrheitswerte darzustellen. Nach der Typangabe und dem optionalen Namen des Elements folgt ein Doppelpunkt. Daran schließt sich ein ganzzahliger Wert an, der bestimmt, wie viele Bits diesem Element zur Verfügung gestellt werden sollen. Bitfelder werden traditionell häufig zur Speichereinsparung eingesetzt. Dahinter verbirgt sich die Idee, daß die Bits der Bitfelder hintereinander abgespeichert werden, daß eine Struktur mit zwei Bitfeldern von 3 und 5 Bit Länge, also beispielsweise ingesamt nur 8 aufeinanderfolgende Bits (1 Byte), belegen würde; ob dem so ist, hängt allerdings vom Compiler ab. Deswegen und wegen des heute relativ billigen Arbeitsspeicher, ist von dem Einsatz von Bitfeldern zur Speicherersparnis eher abzuraten.
Warnung ●
● ●
Ein einzelnes Bitfeld muß nach ANSI C immer den Datentyp int, signed int oder unsigned int besitzen. C++ erlaubt dagegen jeden Typ, der als Integer interpretierbar ist, also auch char, short, long und Aufzählungstypen. Für Bitfelder können keine Zeiger und Referenzen deklariert werden. Unbenannte Bitfelder der Größe 0 bewirken, daß das nächste Bitfeld in einer neuen Allokationseinheit (meist int oder word) beginnt.
77
Sprachkonzepte und Syntax
Syntax struct typbezeichner { intTyp1 element1 : Zahl; intTyp2 element2 : Zahl; /* weitere Elemente */ }; ●
●
typbezeichner: Wird ein typbezeichner spezifiziert, kommt dies einer Typdefinition gleich. Später können dann Variablen dieses Typs definiert werden. In C lautet der Typ struct typbezeichner, in C++ reicht einfach der typbezeichner. typ element : zahl: Innerhalb der Klammern werden die Bitfelder deklariert. Als Typ dürfen nur int-Typen verwendet werden. Auf den Elementnamen folgt ein Doppelpunkt und die Angabe der Bits, die für das Bitfeld reserviert werden sollen. Gibt man keinen Elementnamen an, erhält man ein sogenanntes unbenanntes Bitfeld, mit dessen Hilfe man die Ausrichtung der Bitfelder im Speicher steuern kann. Ein unbenanntes Bitfeld der Größe 0 weist den Compiler an, das nächste Element an den vom Compiler intern zur Speicheraufteilung verwendeten Bytegrenzen auszurichten (reduziert die Zugriffszeiten).
Beispiele struct s_karten { unsigned int wert unsigned int farbe unsigned int spieler unsigned int gespielt } karten[32];
: : : :
3; 2; 2; 1;
int main() { int farbe, wert, loop; for(farbe=0; farbe<4; farbe++) for(wert=0; wert<8; wert++) { karten[8*farbe+wert].farbe = farbe; karten[8*farbe+wert].wert = wert; karten[8*farbe+wert].spieler = 0; karten[8*farbe+wert].gespielt = 0; } for(loop=0; loop < 32; loop++) printf("Farbe = %d, Wert = %d\n",karten[loop].farbe, karten[loop].wert); return 0; }
Verweise Siehe Strukturen
78
Unions
Unions union typbezeichner { Typ1 element1; Typ2 element2; };
Beschreibung Eine Union ist ein Datentyp, der für verschiedene Elemente deklariert wird, dessen Variablen aber nur eines der Elemente aufnehmen können. Die Größe einer Unionvariablen entspricht der Größe des größten Elements.
Anwendung Unions sparen gegenüber Strukturen Speicher, wenn nur ein Element benötigt wird, dessen Typ aber vom Programmierer nicht vorhergesehen werden kann.
Warnung Eine Union kann keine Variable ihres eigenen Typs als Element enthalten. Zeiger oder Referenzen auf Variablen ihres eigenen Typs sind aber erlaubt. Wird eine Unionvariable bei ihrer Definition initialisiert, kann nur dem ersten Unionelement der Wert zugewiesen werden. In C++ gibt es einige Besonderheiten zu beachten: ●
In C++ sind Unions Klassen und dürfen auch Funktionen als Elemente enthalten.
●
Virtuelle Elementfunktionen sind nicht erlaubt.
●
Unions können nicht Bestandteil von Klassenhierarchien sein.
●
Klassen mit eigenen Definitionen für Konstruktor, Destruktor, Kopierkonstruktor oder Zuweisungsoperator können nicht Element einer Union sein.
Syntax union typbezeichner { typ1 element1; typ2 element2; /* weitere Elemente */ }; ●
●
typbezeichner: Wird ein typbezeichner spezifiziert, kommt dies einer Typdefinition gleich. Später können dann Variablen dieses Typs definiert werden. In C lautet der Typ union typbezeichner, in C++ reicht einfach der typbezeichner. typ element: Innerhalb der Klammern werden die Elemente der Union deklariert.
79
Sprachkonzepte und Syntax
Definition von Unionvariablen union Login { /* Definition */ }; union Login user;
Oder union Login{ char passwort[10]; int kennung; } user1, user2;
Initialisierung Bei der Initialisierung wie der Zuweisung kann man nur einem Element der Union einen Wert zuweisen. union Login user = {"Geheim"};
Zugriff auf Strukturelemente unionvariable.strukturelement unionzeiger->strukturelement
Beispiele #define KENNUNG union Login{ char passwort[10]; int kennung; }; int main() { union Login user1; #ifdef KENNUNG printf("Geben Sie ihre Kennummer ein: \n"); scanf("%d",&user1.kennung); fflush(stdin); #else printf("Geben Sie ihr Passwort ein: \n"); scanf("%s",user1.passwort); fflush(stdin); #endif printf("\n"); printf("Groesse der Union: return 0; }
Verweise Siehe Strukturen
80
%d\n",sizeof(Login));
Zeiger
Zeiger und Referenzen Zeiger typ* zeigervariable;
Beschreibung Ein Zeiger ist eine Variable, in der sich die Adresse eines Datenobjektes oder einer Funktion befindet. Die Verwendung von Zeigern bringt verschiedene Vorteile mit sich.
Anwendung Einsatzgebiete für Zeiger ●
●
●
●
●
●
Speicherersparnis. Zeigervariablen speichern Adressen. Ihr Speicherbedarf ist also auf 16 oder 32 Bit festgelegt, wohingegen die Objekte, auf die sie zeigen, wesentlich größer sein können. Übergabe an Funktionen. Durch die Übergabe von Zeigern (call by reference) an Funktionen wird die Funktion in die Lage versetzt, auf Objekte zuzugreifen, die außerhalb ihres Gültigkeitsbereiches liegen. Zudem ist die Übergabe von Zeigern wesentlich schneller als das Kopieren großer Datenobjekte. (In C++ werden aus den gleichen Gründen oft Referenzen übergeben.) Dynamische Speicherverwaltung. Mittels Zeigern kann der Programmierer selbst entscheiden, wann Speicher reserviert und wann freigegeben wird. Zeiger erlauben die Implementierung dynamischer Datenstrukturen wie Listen und Bäume. Rekursive Datenstrukturen. Zeiger können für Datentypen definiert werden, die deklariert, aber noch nicht definiert sind. Auf diese Weise kann eine Klasse beispielsweise Zeiger auf ihre eigenen Instanzen haben. Arrays von Zeigern auf Funktionen sind erlaubt, i.G. zu Arrays von Funktionen. Generische Implementierungen. Mit Zeigern lassen sich viele Probleme effizienter und/oder allgemeiner lösen. Beispielsweise durch Verwendung von Zeigern auf void, Zeigern auf Funktionen, um Funktionen als Argumente an andere Funktionen zu übergeben, Basisklassenzeiger auf abgeleitete Objekte zur Verwaltung von Objekten verschiedener abgeleiteter Klassen in einem Array, Nutzung virtueller Methoden statt expliziter Laufzeitidentifizierung,
81
Sprachkonzepte und Syntax
Warnung Ein Zeiger kann auf jedes beliebige Datenobjekt zeigen, mit zwei Ausnahmen: ●
Referenzen und
●
Bitfelder.
Syntax typ* zeigervariable; ●
●
●
typ: Datentyp der Objekte, auf die der Zeiger weist. Diese Angabe wird für die korrekte Dereferenzierung des Zeigers benötigt. *: kennzeichnet die Variable als Zeiger und kann sowohl hinter der Typangabe stehen als auch vor dem Zeigernamen. zeigervariable: Name des Zeigers.
Initialisierung Die Initialisierung eines Zeigers erfolgt durch die Zuweisung einer Adresse und eines mit der Adresse verbundenen Speicherbereichs. Die Initialisierung kann auf verschiedene Weisen geschehen. 1. Zuweisung der Adresse eines existierenden Objekts. ●
Die Adresse einer Variablen kann über den Adreß-Operator & ermittelt werden.
●
Zeiger sind selbst Adressen.
●
Die Namen von Arrays und Funktionen stellen ebenfalls Adressen dar. int i_var = 13; int *ptr1 = &i_var; int *ptr2 = ptr1; int feld[10] = {11,11,11}; ptr3 = feld;
2. Zuweisung von NULL. ●
Zeiger, die auf kein Objekt gerichtet sind, sollten mit der NULL-Adresse initialisiert werden. Auf diese Weise kann leicht getestet werden, ob der Zeiger auf ein Objekt weist oder nicht. double *ptr4 = NULL;
82
Zeiger
3. Allokation durch malloc() oder new. ●
Die Funktion malloc() sowie der Operator new dienen dazu, Speicher zu reservieren und die Adresse an den Zeiger zu übergeben. Auf diese Weise werden Objekte eingerichtet, die nur über Zeiger erreichbar sind. double *ptr5 = new double; int *ptr6 = (int*) malloc(3*sizeof(int));
Nichtinitialisierte Zeiger enthalten undefinierte Bitmuster, die bei der Dereferenzierung als Adresse interpretiert werden. Dies führt unweigerlich zu Programmfehlern und oftmals zu Programmabstürzen.
Dereferenzierung Zeiger sind Adressen. Dereferenzierung bedeutet, über diese Adresse auf das referenzierte Objekt zuzugreifen. Dazu dienen die Dereferenzierungs-Operatoren *, >,.* und ->* . ● ●
●
●
*: der eigentliche Dereferenzierungs-Operator. ->: eine Verbindung aus Dereferenzierungs- und Punkt-Operator, der einen Zeiger auf einen Klassentyp dereferenziert und gleichzeitig auf ein Element des referenzierten Objekts zugreift. .*: dereferenziert einen Zeiger auf ein Klassenelement für ein gegebenes Klassenobjekt. ->*: dereferenziert einen Zeiger auf ein Klassenelement für einen gegebenen Zeiger auf ein Klassenobjekt.
Beispiel #include <stdio.h> int main() { int i; int *p_i = &i; struct koord { int x,y; } pos; struct koord *p_pos1 = &pos; // Dereferenzierung *p_i = 2; (*p_pos1).x = 3; p_pos1->y = p_pos1->x+1;
// Deklaration und Initialisierung
// Deklaration und Initialisierung
// Dereferenzierung einer Struktur mit * // Dereferenzierung einer Struktur mit ->
printf("x = %d; y = %d\n",pos.x, pos.y); return 0; }
83
Sprachkonzepte und Syntax
liefert folgende Ausgabe: x = 3; y = 4
Warnung ●
●
●
Das Zeichen * wird also in der Deklaration dazu benutzt, eine Zeigervariable zu spezifizieren. In Anweisungen teilt es dem Compiler mit, daß nicht die Adresse, die in der Zeigervariablen gespeichert ist, gefragt ist, sondern das Objekt, das an dieser Adresse abgelegt ist. Ein Zeigername ist eine Adresse. Diese Adresse verweist auf die Speicherzelle, in der der Wert des Zeigers abgelegt wird. Der Wert eines Zeigers ist ebenfalls eine Adresse. Diese Adresse wird benutzt, um auf andere Datenobjekte zu verweisen. Dereferenzieren Sie niemals Zeiger, die ungültige Adressen beinhalten (nichtinitialisierte Zeiger, Zeiger auf Objekte, die nicht mehr existieren, etc.).
Spezielle Zeiger Zeiger auf Zeichenketten
char*
Ein Zeiger vom Typ char* weist auf ein einzelnes Zeichen. Viele Bibliotheksfunktionen, die Zeichenketten verarbeiten (strcpy(), strlen() etc.), erwarten als Argument solch einen Zeiger auf das erste Zeichen einer zu verarbeitenden Zeichenkette. Die so übergebene Zeichenkette beginnt mit der Adresse, die der Zeiger liefert, und endet mit dem ersten Auftauchen des Nullzeichens '\0'. Bei der Verwendung der entsprechenden Bibliotheksfunktionen ist daher darauf zu achten, daß die übergebenen Zeiger auch stets auf nullterminierte Zeichenketten verweisen, und daß Zeichenketten nur in Speicherbereiche kopiert oder geschrieben werden, die groß genug sind, die gesamte Zeichenkette einschließlich Nullterminierungszeichen aufzunehmen. Ansonsten kommt es zu unerwünschten Speicherzugriffen – meist begleitet von Programmabstürzen. char str[20] = "Hello World!"; // reserviere Speicher so groß wie der String in str plus '\0' p_str = (char*) malloc(strlen(str)+1); strcpy(p_str,str);
Zeiger auf void
void*
Ein void* (Zeiger auf void) ist ein universeller, typenloser Zeiger. Er wird eingesetzt, wenn der Typ des Zeigers nicht feststeht oder allgemein gehalten werden soll. Erst in einer nachfolgenden expliziten Typumwandlung wird festgelegt, wie der zum Zeiger gehörende Speicherplatz interpretiert werden soll, ob zum Beispiel als floatWert oder als String.
84
Spezielle Zeiger // Vergleichsfunktion für Verwendung mit qsort() // vergleicht zwei Adressen nach dem Namen int vgl(const void* p1,const void* p2) { struct adresse* adr1,*adr2; adr1=(struct adresse*)p1; adr2=(struct adresse*)p2; return strcmp(adr1->name,adr2->name); }
Zeiger auf Funktionen
*func
Zeiger auf Funktionen werden dazu benötigt, Funktionen als Parameter an andere Funktionen zu übergeben. Rückgabetyp (*zeigername) (Parametertypen); ●
● ●
Rückgabetyp: gibt den Rückgabetyp der Funktionen an, auf die der Zeiger gerichtet werden kann. zeigername: ist der Name des Zeigers. Parametertypen: Liste der Parametertypen der Funktionen, auf die der Zeiger gerichtet werden kann.
void func(int i, class person leser) {...}; void (*func_ptr) (int, class person) = func;
Konstante Zeiger
*const
Bei der Zeigerdeklaration unterscheidet man zwischen const-Zeigern, die nicht auf andere Objekte umgelenkt werden können, und Zeiger auf const-Objekte. Letztere werden häufig in Funktionsdefinitionen verwendet, um zu verhindern, daß in der Funktion über den Zeigerparameter das Objekt, auf das der Zeiger weist, verändert wird. char c = 'A'; const char *ptr_c; // Zeiger auf const-Objekt char const *ptr_c; // Zeiger auf const-Objekt char *const ptr_c = &c; // const-Zeiger int func (int par1, const char *par2);
Zeiger auf Datenelemente
class::*
Mit Hilfe des Bereichsoperators :: kann ein Zeiger einer Klasse zugeordnet werden. Zur Definition und Initialisierung des Zeigers wird dabei lediglich der Name der Klasse verwendet. Erst beim Zugriff über den Zeiger werden Instanzen oder Zeiger auf Instanzen benötigt. // Definition: int person::* int_ptr;
// person sei eine Klasse
// Initialisierung: int person::* int_ptr = &person::plz;
85
Sprachkonzepte und Syntax // Zugriff: class person leser, *p_leser; leser.*int_ptr = 66111; p_leser->*int_ptr = 66111;
Zeiger auf Methoden
class::*func
Mit Hilfe des Bereichsoperators :: kann ein Zeiger einer Klasse zugeordnet werden. Zur Definition und Initialisierung des Zeigers wird dabei lediglich der Name der Klasse verwendet. Erst beim Zugriff über den Zeiger werden Instanzen oder Zeiger auf Instanzen benötigt. // Definition: void (person::*func_ptr) (int);
// person ist eine Klasse
// Initialisierung: void (person::*func_ptr) (int) = &leser::print_adresse; // Zugriff: class person leser, *p_leser; (leser.*func_ptr) (64); (p_leser->*func_ptr) (64);
Zeiger auf statische Klassenelemente
class::static*
Zeiger auf statische Klassenelemente werden wie Nicht-Element-Zeiger behandelt, d. h., der Zeiger wird ohne eine Klassenangabe dereferenziert: int *int_ptr = &Klassenname::Statisches_Datenelement; int a = *int_ptr;
Basisklassenzeiger
(basis*)
In C++ kann jedes abgeleitete Objekt auch als Objekt einer seiner eindeutigen Basisklassen angesehen werden. Entsprechend kann ein Zeiger auf ein abgeleitetes Objekt ohne Probleme in einen Zeiger auf eines seiner eindeutigen Basisklassen umgewandelt werden. Dies erlaubt beispielsweise, die Verwaltung abgeleiteter Objekte unterschiedlicher Klassen als Basisklassenobjekte in einem gemeinsamen Array. Basisklassenzeiger werden zumeist in Zusammenhang mit virtuellen Methoden eingesetzt. class Basis {...}; class Abg1 : public Basis {...}; class Abg2 : public Basis {...};
// Klassenhierarchie
class Basis
// Array
*feld[10];
feld[0] = new Abg1; feld[1] = new Abg2;
86
// Zeiger in Array // aufnehmen
Referenzen (nur C++)
this-Zeiger
this
Jede nicht-statische Methode einer Klasse erhält vom Compiler intern einen zusätzlichen Parameter zugewiesen. Dies ist der this-Zeiger, der auf die Anfangsadresse der jeweiligen Instanz verweist, für die die Methode aufgerufen wird. #include using namespace std; class demo { public: void func() };
{cout << this;}
int main() { class demo d1,d2; cout << "This-Zeiger der Instanz d1 = " << d1.func() << endl; cout << "This-Zeiger der Instanz d2 = " << d2.func() << endl; return 0; }
Referenzen (nur C++) typ& referenzname = var;
Beschreibung Referenzen verweisen wie Zeiger auf Objekte, können jedoch nur einmal mit einem Objekt initialisiert und danach nicht mehr auf andere Objekte umgelenkt werden. Zudem entfällt für Referenzen die typische Zeigersyntax sie werden wie ganz normale Variable, quasi als Synonyme der Variablen, auf die sie verweisen, verwendet. Technisch gesehen entspricht eine Referenz damit einem const-Zeiger, der bei jedem Zugriff automatisch dereferenziert wird.
Anwendung Referenzen, auch Aliase genannt, sind im Gebrauch den Zeigern verwandt und werden in ähnlicher Weise zur Übergabe von Variablen an Funktionen eingesetzt, wenn die Funktionen die übergebenen Variablen direkt ändern (call by reference) oder Laufzeitverschlechterungen durch Kopieren großer Datenobjekte vermieden werden sollen. Referenzen haben dabei aber den Vorteil, ●
●
daß sie wie normale Variablen angesprochen werden (die zeigertypische Dereferenzierungssyntax entfällt) daß ihr Einsatz sicherer ist als der von Zeigern (da sie immer auf ein genau definiertes Objekt verweisen)
87
Sprachkonzepte und Syntax
Warnung Eine Referenzvariable muß direkt bei ihrer Deklaration initialisiert werden, außer ●
sie ist explizit als extern deklariert,
●
es handelt sich um einen Funktionsparameter,
●
es handelt sich um ein Klassenelement.
Eine Referenzvariable kann nur auf eine Variable gesetzt werden. Sie kann nicht auf eine andere Variable umgesetzt werden! Es lassen sich keine Zeiger auf Referenzen definieren. Es lassen sich keine Arrays von Referenzen bilden. Es gibt keine Referenzen auf Referenzen. Referenzen können nicht vom Typ void& sein.
Syntax typ& referenzname = var; ● ●
typ: Datentyp des Objekts, auf das die Referenz verweist. &: Referenzoperator &, der in der Deklaration eine Variable als Referenz kennzeichnet.
●
referenzname: Name der deklarierten Referenzvariablen.
●
= var: Referenzen müssen direkt bei der Definition initialisiert werden.
Beispiele Das folgende Beispiel demonstriert die Übergabe von Argumenten an Referenzparameter. #include using namespace std; class Leute { int alter; public: Leute(int age) {alter = age;} void altern() {alter++;} int get_alter() {return alter;} }; // Funktion mit Referenzparameter void uebergebe_referenz(Leute& par) { par.altern(); } int main()
88
Typdefinitionen { Leute dennis(30); cout << "Dennis ist " << dennis.get_alter() << " Jahre alt\n"; // Übergabe des Klassenobjekts an Referenzparameter uebergebe_referenz(dennis); cout << "Dennis ist " << dennis.get_alter() << " Jahre alt\n"; return 0; }
Ausgabe Dennis ist 30 Jahre alt Dennis ist 31 Jahre alt
Warnung Bei der Parameterübergabe müssen Datentyp des Referenzparameters und Datentyp des Arguments genau übereinstimmen. Ansonsten gilt: ●
●
Für const-Referenzparameter nimmt der Compiler eine Typumwandlung vor und erzeugt eine zu dem Referenzparamter passende temporäre Variable. Der Referenzparameter weist dann auf diese temporäre Variable. Die Zurücklieferung von Werten über den Parameter an das Aufrufargument ist daher nicht mehr möglich, für const-Parameter aber auch sicherlich nicht vorgesehen. Für nicht-const-Referenzparameter gibt der Compiler eine Fehlermeldung aus. (Einige ältere Compiler geben nur eine Warnung aus und legen eine temporäre Variable an.)
Typdefinitonen und Typumwandlungen Typdefinitionen typedef
Beschreibung C erlaubt die Definition beliebiger Datentypen auf der Grundlage der im Sprachumfang enthaltenen Typen. Bei einer Typdefinition wird ein neuer Typ spezifiziert und mit einem Bezeichner für den Typ verbunden. Eine solche Typdefinition kann durch die Schlüsselworte struct, union, class oder typedef erfolgen.
89
Sprachkonzepte und Syntax
Anwendung Mit typedef werden im Grunde keine neuen Typen definiert, sondern lediglich Synonyme für bereits mögliche Typspezifikationen eingerichtet. Dies kann aus zwei Gründen geboten sein: ●
●
Eine Typdefinition soll per Präprozessorschalter anpaßbar sein (erstes Beispiel). Eine komplexe Typdefinition soll durch ein einfaches Synonym repräsentiert werden (zweites Beispiel)
Warnung typedef-Typen sind nicht erweiterbar: typedef int INT; unsigned INT x;
// nicht erlaubt
Syntax typedef typ_spezifikation typbezeichner; ●
●
typ_spezifikation: Hier wird der neue Typ definiert. Die Form der Typangabe entspricht der Angabe in Variablendeklarationen. typbezeichner: Name für den neuen Typ.
Beispiele Typdefinitionen in Abhängigkeit von Compiler-Schaltern: #if defined (LONG) typedef wchar_t long; #else typedef wchar_t int; #endif
Typdefinitionen als Vereinfachungen: typedef unsigned int UINT; typedef void (*func_typ) (int, int);
// Beispiel aus windows.h // definiert func_typ
Standardkonvertierungen Beschreibung In verschiedenen Situationen nimmt der Compiler automatische Typkonvertierungen vor.
Anwendung Die wichtigsten automatischen Konvertierungen sind:
90
Standardkonvertierungen
Bezeichner ●
●
●
Auf der rechten Seite von Zuweisungen werden Bezeichner als R-Werte (Wert, nicht Speicheradresse) aufgefaßt. Array-Namen werden auf der rechten Seite von Anweisungen (inklusive Funktionsaufrufen) als Zeiger auf ihr erstes Element aufgefaßt. Funktionsnamen repräsentieren auf der rechten Seite von Anweisungen den Rückgabewert der Funktion.
Typangleichung ●
●
Arithmetische Datentypen können ineinander konvertiert werden, wenn der Datentyp, in den konvertiert wird, geeignet ist, den zu konvertierenden Wert aufzunehmen (bei Konvertierung von Gleitkommazahlen in Integer-Werte geht dabei der Nachkommaanteil verloren). Für binäre Operatoren, die arithmetische Operanden übernehmen, gleicht der Compiler automatisch die Typen beider Operanden an – man bezeichnet dies als die standardmäßige arithmetische Konvertierung.
Boolesche Wahrheitswerte ●
●
Null-Zeiger und Null-Repräsentationen arithmetischer Datentypen können in den Booleschen Wert false konvertiert werden, andere Werte dieser Datentypen werden entsprechend als true interpretiert. Umgekehrt kann der Boolesche Wert false in eine arithmetische Null, true in eine Eins konvertiert werden.
Zeiger und Klassen ●
● ●
●
●
●
Arraynamen werden auf der rechten Seite von Anweisungen (inklusive Funktionsaufrufen) als Zeiger auf ihr erstes Element aufgefaßt. Funktionsnamen können als Zeiger auf sich selbst aufgefaßt werden. Zeiger auf spezielle Datentypen können in Zeiger auf void verwandelt werden (der umgekehrte Weg ist nur durch explizite Typumwandlung erreichbar). Ein Zeiger auf eine abgeleitete Klasse kann in einen Zeiger auf eine eindeutige, zugängliche Basisklasse konvertiert werden. Unter den gleichen Voraussetzungen kann die Instanz einer abgeleiteten Klasse als Instanz einer Basisklasse interpretiert werden. Zeiger auf Elemente einer Basisklasse können in Zeiger auf Elemente einer eindeutigen, zugänglichen abgeleiteten Klasse konvertiert werden.
91
Sprachkonzepte und Syntax ●
Wenn Sie einer Klasseninstanz einen Wert zuweisen und in der Klasse ein entsprechender Konstruktor definiert ist, der mit genau einem Argument des Typs aufgerufen wird, dem auch der Wert angehört, wird der Konstruktor wo nötig zur automatischen Konvertierung von Werten des entsprechenden Datentyps in Objekte der Klasse herangezogen (kann durch explicit-Deklaration unterbunden werden).
const und volatile ●
Die Qualifizierer const und volatile können automatisch beigefügt, aber nicht abgezogen werden (so kann ein nicht-const Argument an einen const-Parameter übergeben werden, aber nicht umgekehrt).
Beispiele Operandenkonvertierung float f = 3/4;
float f = 3/4.0;
// // // // // // //
ergibt 0.0; Integer-Division von 3/4 ergibt 0 0 wird für Zuweisung in 0.0 umgewandelt ergibt 0.75; 4.0 ist Gleitkommazahl, der Integer-Operand 3 wird daher in Gleitkommazahl umgewandelt Es wird eine Gleitkomma-Division durchgefuehrt
Konstruktoren mit einem Argument class Feld { public: int anzahl; Feld(int a) {anzahl = a;} }; ... Feld stack(3); stack = 13; // impliziter Aufruf: stack = Feld(13);
Typumwandlung (typ) wert
Beschreibung Gelegentlich ist es wünschenswert, daß eine Variable eines bestimmten Datentyps als einem anderen Datentyp angehörig behandelt wird. Sofern eine solche Typumwandlung durchführbar ist, kann sie durch den C-Cast-Operator () oder die C++-Operatoren const_cast, dynamic_cast, static_cast und reinterpret_cast, die als Schlüsselworte in die Sprache aufgenommen sind, explizit herbeigeführt werden.
92
Typumwandlung
Anwendung Für Typkonvertierungen gibt es fünf typische Gründe: ●
●
●
●
Sie wollen eine Gleitkommazahl in eine Integer-Zahl verwandeln oder umgekehrt. Letzeres ist beispielsweise geboten, wenn Sie eine Integer-Zahl durch eine andere Integer-Zahl teilen wollen, das Ergebnis aber mit Divisionrest in einer Gleitkommavariablen gespeichert werden soll. Sie haben einen void-Zeiger vorliegen, den Sie in einen Zeiger auf einen bestimmten Datentyp umwandeln wollen. Sie haben einen Basisklassenzeiger auf ein abgeleitetes Objekt, den Sie in einen Zeiger auf die abgeleitete Klasse umwandeln wollen. Sie wollen durch die Typumwandlung eines Zeigers eine bestimmte Speicherinterpretation erzwingen. Diese Möglichkeit ist selten erforderlich, dafür aber recht fehleranfällig.
Der Cast-Operator von C Der Cast-Operator kann benutzt werden, um in einer Anweisung den Typ des Ausdrucks der rechten Seite in den Typ des Bezeichners der linken Seite umzuwandeln: bezeichner1 = (Typ_von_bezeichner1) AUSDRUCK; bezeichner1 = (Typ_von_bezeichner1) bezeichner2;
Eine zweite Form der Syntax wandelt den Typ einer Variablen direkt um bezeichner = Datentyp();
Die Cast-Operatoren von C++ Die C++-Operatoren sind vor allem dazu gedacht, die verschiedenen Konvertierungen nach ihrem Gefahrenpotential zu kategorisieren, und die Textsuche nach den Konvertierungen zu erleichtern. Operator
Einsatzgebiet
static_cast(var)
Zur Umwandlung zwischen »nahe verwandten« Typen – also alle Fälle, in denen der Compiler auch eine implizite Konvertierung vornehmen könnte, plus einige einfache Sonderfälle.
reinterpret_cast(var)
Für fast alle anderen erlaubten Umwandlungen.
const_cast(var)
Entfernt eine const- oder volatile-Deklaration aus einem Typ.
dynamic_cast(var)
Zur Umwandlung zwischen Zeigern oder Referenzen auf Klassentypen einer gemeinsamen Klassenhierarchie
93
Sprachkonzepte und Syntax
Beispiele int i = int(3.14); double var = (double) 13/2); int ptr = (int *) malloc(80 * sizeof(int)); Abgeleitet *p_abg = new Abgeleitet; Basis *p_bp = p_abg; p_abg = dynamic_cast (p_bp);
Konstanten Konstanten Beschreibung Neben den Variablen stellen die Konstanten die zweite Möglichkeit zur Repräsentation von Daten in Programmen dar. Der Unterschied zwischen Variablen und Konstanten liegt auf der Hand: Während eine Variable im Laufe eines Programms unterschiedliche Werte annehmen und repräsentieren kann, stellt eine Konstante immer einen festen Wert dar. C/C++ kennt 3 verschiedene Typen von Konstanten: Literale Symbolische Konstanten Konstante Variablen
Anwendung Konstanten stellen eine Möglichkeit dar, feststehende Daten direkt im Programmcode zu verankern: etwa um den Zahlenwert von PI in eine Formel einzubauen oder in Form einer String-Tabelle für Fehlermeldungen des Programms.
Literale Beschreibung Die einfachste Form der Konstante ist ein konstanter Wert. Um sie von den konstanten Variablen zu unterscheiden, bezeichnet man die konstanten Werte auch als Literale.
94
Literale
Anwendung Konstante Werte werden bei der Übersetzung durch den Compiler hartcodiert, d. h., die Werte sind nicht irgendwo im Speicher abgelegt, von wo sie bei Bedarf ausgelesen werden können, sondern die Werte stehen direkt in den betreffenden Maschinenbefehlen. Die einzige Ausnahme: für String-Literale wird Speicher reserviert, doch hat der Programmierer üblicherweise keinen Zugriff auf diesen Speicherbereich. printf("Hello World\n");
Die automatische Speicherallokation erlaubt allerdings die Zuweisung von StringLiteralen an Zeiger auf char. char *str1; str1 = "Hello World\n"; Datentyp
Literal
bool
true, false
char
'c', 'Ü', '\n', '\\', 'ab' (Multibyte-Zeichen)
wchar_t
L'c', L'Ü', L'\n', L'\\', L'ab' (Multibyte-Zeichen)
Zeichenketten (char*)
"Dies ist ein String"
Zeichenketten (wchar_t*)
L"Dies ist ein String"
short
12, -128
int
12, -128, 128u, 1277U 012, -077 (Oktal) 0x12, 0XEE (Hexadezimal)
long
12l, 1400000L, 12lu
float
47.11f, 12.0F, 10e-2F
double
47.11, 12.0, 10e-2
long double
47.11l, 12.0L, 10e-2L
Warnung Wichtig bei der Verwendung von Literalen ist, daß Sie sich an die vorgesehene Syntax zur Angabe der Werte halten, damit der Compiler aus dem Wert auch den zugehörigen Datentyp ablesen kann.
95
Sprachkonzepte und Syntax
Beispiele int i = 513; // Zuweisung einer Konstanten an eine Variable double feld[30]; // Angabe der Array-Groesse if( (x < 100) && (x > 50)) // Zur Formulierung von if(!strcmp(str,"Vergleichstext"); // Bedingungen
Verweise Siehe #define Siehe const-Variablen
#define symbolische Konstanten Beschreibung Symbolische Konstanten sind Bezeichner, die als Aliase für einen bestimmten Wert definiert sind.
Anwendung Wird eine Konstante mehrfach verwendet, ist es sinnvoll, sie vorab zu deklarieren und mit einem symbolischen Namen zu verbinden, der dann im weiteren Verlauf des Programms die Konstante repräsentiert. Dies ermöglicht die PräprozessorDirektive #define: #define
symbolBezeichner
KONSTANTE
Danach kann der symbolische Bezeichner im weiteren Quelltext stellvertretend für die Konstante eingesetzt werden. Bei der Kompilation werden alle Vorkommen des symbolischen Bezeichners durch die zugehörige Konstante ersetzt. Der Vorteil besteht einmal darin, ●
●
daß Sie eine Konstante mit einem aussagekräftigen Namen verbinden können, und zum anderen nur die #define-Anweisung anpassen müssen, wenn Sie die Konstante mit einem anderen Wert verbinden wollen (beispielsweise ist es sinnvoll, Obergrenzen für Arrays, Abbruchbedingungen für Schleifen oder Schalter für die bedingte Kompilation mit Hilfe von #define am Dateianfang festzulegen, damit man sie später bei Bedarf schnell findet und zentral ersetzen kann).
Vordefinierte Konstanten Eine Reihe interessanter Konstanten sind bereits in den Header-Dateien der Standardbibliothek vordefiniert, so zum Beispiel
96
#define – symbolische Konstanten
Header-Datei
Konstante
Wert
stddef.h
NULL
Makro, das zu implementierungsspezifischer Nullzeigerkonstante expandiert wird, beispielsweise 0 oder 0L.
stdio.h
EOF
Üblicherweise -1 bzw (wint_t)(0xFFFF) für wchar_t
FOPEN_MAX
Makro, das zu Integer-Wert expandiert wird, der die Mindestanzahl der Dateien anzeigt, die gleichzeitig geöffnet werden können.
CHAR_BIT
8 (Anzahl Bits in Typ char)
INT_MIN
-2147483647L-1 (für 32-Bit-System)
INT_MAX
2147483647L (für 32-Bit-System)
FLT_RADIX
2 (Basis der Exponentdarstellung)
FLT_DIG
6 (Anzahl der Dezimalstellen)
limits.h
Beispiele #define STUNDE 60 #define dutzend 12 #define Name "C++Compiler"
Der symbolische Bezeichner ist lediglich ein Aliasname für ein Literal. Vor der eigentlichen Übersetzung in Objektcode geht der Compiler (genauer gesagt der Präprozessor) den Quelltext durch und ersetzt alle Vorkommen des symbolischen Bezeichners durch die zugehörige Konstante. Wenn es dann an die eigentliche Übersetzung geht, sieht der Quelltext so aus, daß es keine symbolischen Konstanten, sondern nur noch Literale gibt: Original-Quelltext #define MAX 30 double feld[MAX]; ... for (int i = 0; i < MAX; i++) {...} ... for (int i = 0; i < MAX; i++) {...} ... if (i < MAX) {...}
Nach Bearbeitung durch Präprozessor: double feld[30]; ... for (int i = 0; i < 30; i++) {...} ... for (int i = 0; i < 30; i++) {...} ... if (i < 30) {...}
Verweise Siehe Zusammengesetzte Datentypen, Aufzählungstypen
97
Sprachkonzepte und Syntax
const konstante Variablen Beschreibung Die letzte Form der Konstanten ist die konstante Variable eine Variable, deren Wert nach der Initialisierung nicht mehr verändert werden kann.
Anwendung Konstante Variablen werden mit den Schlüsselwort const deklariert. Sie sind aus Sicht des Compilers keine echten Konstanten, sondern vielmehr Variablen, die er vorm Überschreiben schützt. In C wie C++ kann man mit const ●
●
beliebige Objekte (also auch Variablen selbst definierter Datentypen wie Strukturen und Klassen) als konstant deklarieren, verhindern, daß als Zeiger übergebene Funktionsargumente in einer Funktion verändert werden,
●
Zuweisungsmöglichkeiten erweitern,
●
stärker von der Typprüfung des Compilers profitieren.
Warnung In C++ wird eine konstante Variable wie eine echte Konstante angesehen. In C, das nach ANSI ebenfalls das Schlüsselwort const kennt, können konstante Variablen jedoch nicht an Stellen verwendet werden, an denen der Compiler eine Konstante bzw. einen konstanten Ausdruck erwartet – also ●
nicht bei der Angabe der Größe eines Arrays,
●
nicht als case-Marken.
Beispiele const int monate = 12; int func (const int par1, const char *par2);
Verweise Siehe Literale Siehe #define Siehe Klassen, Konstante Datenelemente und Methoden
98
Operatoren
Operatoren Operatoren Beschreibung ANSI C/C++ kennt mehr als 40 Operatoren. Mit diesem Wert übersteigt bei C/C++, und das ist bei Programmiersprachen nicht üblich, die Anzahl der Operatoren die Anzahl der Schlüsselwörter der Sprache. Der größte Teil der Operatoren stammt bereits aus der Zeit der ersten Sprachdefinition durch Kernighan & Ritchie. Bei den hier aufgelisteten Formen der Operatoren handelt es sich um ihre ursprünglichen Definitionen und nicht um überladene Versionen. Die meisten der Operatoren können von Programmen überladen werden (nicht jedoch für die elementaren Datentypen, für die sie bereits standardmäßig definiert sind) und nur unter Beibehaltung ihrer üblichen Syntax. Operator
Kurzbeschreibung
+
addiert zwei Operanden
+=
addiert zwei Operanden und weist Ergebnis zu
-
subtrahiert zwei Operanden
-=
subtrahiert zwei Operanden und weist Ergebnis zu
*
multipliziert zwei arithmetische Operanden
*=
multipliziert zwei arithmetische Operanden und weist Ergebnis zu
/
dividiert zwei arithmetische Operanden
/=
dividiert zwei arithmetische Operanden und weist Ergebnis zu
%
Modulo-Operator, ermittelt den Rest einer Ganzzahldivision
%=
Modulo-Operator mit gleichzeitiger Zuweisung
!
Nicht-Operator, Verneinung
not
Nicht-Operator, Verneinung
!=
vergleicht zwei arithmetische Operanden auf Ungleichheit
not_eq
vergleicht zwei arithmetische Operanden auf Ungleichheit
&
Bei einem Operanden liefert dieser Operator die Adresse des Operanden
&
Bei zwei Operanden wird eine bitweise UND-Verknüpfung ausgeführt
bitand
Bitweise UND-Verknüpfung
&=
Bitweise UND-Verknüpfung und Zuweisung
and_eq
Bitweise UND-Verknüpfung und Zuweisung
&&
Logische UND-Verknüpfung
99
Sprachkonzepte und Syntax
Operator
Kurzbeschreibung
and
Logische UND-Verknüpfung
()
Cast-Operator, erzwingt Typumwandlung
,
Operator für sequentielle Auswertung, Kommaoperator
++
inkrementiert den Operanden
--
dekrementiert den Operanden
->
Elementverweis-Operator
.
Punkt-Operator
*
Indirektions-Operator
=
Zuweisungsoperator
==
testet auf Gleichheit
<
Kleiner als
>
Größer als
<=
Kleiner als oder gleich
=>
Größer als oder gleich
<<
Bitweise Linksverschiebung
<<=
Bitweise Linksverschiebung und Zuweisung des Ergebnisses
>>
Bitweise Rechtsverschiebung
>>=
Bitweise Rechtsverschiebung und Zuweisung des Ergebnisses
?:
Bedingungsoperator
[]
Array-Indizierung
^
Bitweises exklusives ODER
xor
Bitweises exklusives ODER
^=
Bitweises exklusives ODER und Zuweisung des Ergebnisses
xor_eq
Bitweises exklusives ODER und Zuweisung des Ergebnisses
|
Bitweises ODER
bitor
Bitweises ODER
|=
Bitweises ODER und Zuweisung des Ergebnisses
or_eq
Bitweises ODER und Zuweisung des Ergebnisses
||
Logisches ODER
or
Logisches ODER
~
Komplement
compl
Komplement
:
Klasseninitialisierer
::
Bereichsoperator
.*
Dereferenzierung von Zeigern auf Klassenelemente
->*
Dereferenzierung von Zeigern auf Klassenelemente
100
Vorzeichen
Operator
Kurzbeschreibung
sizeof
ermittelt Größe des Operanden in Bytes
new
Dynamische Speicherreservierung
new[]
Dynamische Speicherresevierung für Arrays
delete
Speicherfreigabe
delete[]
Speicherfreigabe für Arrays
#
Zeichenkettenbildung
##
Grundsymbolverbindung
defined
testet, ob Makro definiert ist
typeid
Dynamische Typinformation
Klassifizierung Alle Operatoren lassen sich, je nachdem, mit wie vielen Operanden sie arbeiten, einer der folgenden Gruppen zuordnen: ●
unäre Operatoren (arbeiten mit einem Operanden),
●
binäre Operatoren (arbeiten mit zwei Operanden) und
●
ternäre Operatoren (arbeiten mit drei Operanden).
Vorzeichen Beschreibung Unärer Operator der festlegt, ob ein Zahlenwert positiv oder negativ ist.
Anwendung Mit den unären Operatoren »+« und »-« können Sie arithmetischen Typen ein Vorzeichen zuweisen. Werte ohne Vorzeichen sind automatisch positiv. Operator
Bedeutung
+
positiver Wert
-
negativer Wert
Beispiele int var1, var2; var1 = -3; var2 = 3 * -var1;
101
Sprachkonzepte und Syntax
Arithmetische Operatoren Beschreibung Binäre Operatoren zur Durchführung der algebraischen Verknüpfungen: Addition, Subtraktion, etc., sowie der Divisionsrestberechnung.
Anwendung Mit den arithmetischen Operatoren können Sie, wie es der Name vermuten läßt, einfache arithmetische Operationen durchführen, wie sie auch in anderen Programmiersprachen üblich sind. Operator
Bedeutung
+
Addition
-
Subtraktion
*
Multiplikation
/
Division %
Modulo, Rest einer Ganzzahldivision
Warnung ●
●
●
Werden in einem Ausdruck mehrere Operatoren verwendet, gilt: Punktrechnung geht vor Strichrechnung. Vorzeichen, Inkrement und Dekrement kommen vor Punktrechnung. Bei gleicher Priorität werden die Operatoren von links nach rechts abgearbeitet. Generell gilt, daß jeder Operator einer Prioritätstufe zugeordnet ist, und Operatoren höherer Priorität vor Operatoren niedrigerer Priorität ausgewertet werden. Durch Setzen von Klammern können Sie die Reihenfolge der Auswertung von Operatoren selbst festlegen.
Beispiele var1 var2 var2 var2
= = = =
5+3; 20 % var1; 3 * 4 + var1; 3 * (4 + var1);
// = 4 // = 20 // = 36
Verweise Siehe Priorität und Assoziativität
102
In- und Dekrementoperator
In- und Dekrementoperator Beschreibung Inkrement und Dekrement bezeichnet die Erhöhung beziehungsweise Erniedrigung eines Wertes um 1 Einheit.
Anwendung Die Programmiersprache C/C++ stellt zwei Operatoren zur Verfügung, mit denen der Wert einer Variablen inkrementiert (um eins erhöht) oder dekrementiert (um eins vermindert) werden kann, wobei die Operatoren vor oder nach der Variablen stehen können. lwert++ ++lwert lwert---lwert
Die Position des Operators legt fest, wann die Erhöhung oder Erniedrigung der Variablen berechnet wird. Wichtig ist dies, wenn der Operator in komplexeren Ausdrücken verwendet wird. int var1, var2 = 12, var3 = 12; var1 = 3 * ++var2; // ergibt var1 = 39 var1 = 3 * var3++; // ergibt var1 = 36
Die Notation, bei der der Operator vor der Variablen steht, wird Präfix-Schreibweise genannt und führt dazu, daß der Wert der Variablen zuerst neu berechnet wird, bevor er benutzt wird. In der Postfix-Schreibweise (der Operator steht nach der Variablen) wird der Wert des Operanden benutzt, bevor er verändert wird.
Operator
Bedeutung
++
Inkrementoperator, erhöht seinen Operanden um 1
--
Dekrementoperator, vermindert seinen Operanden um 1
Warnung ● ●
●
Die In- und Dekrementoperatoren sind nicht für Aufzählungstypen definiert. Die In- und Dekrementoperatoren können auch bei Zeigern benutzt werden. In diesem Fall wird die Adresse um die Größe des Objekts, auf das der Zeiger zeigt, erhöht oder vermindert. Das Inkrement einer Variablen vom Typ bool ergibt 1, das Dekrement ist nicht definiert.
103
Sprachkonzepte und Syntax
Beispiel #include <stdio.h> int main() { int x,y; x = 5; y = 5; printf("Wert von ++x ist %d\n", ++x); printf("Wert von y++ ist %d\n", y++); printf("x nach der Inkrementierung: %d\n", x); printf("y nach der Inkrementierung: %d\n", y); return 0; }
Das Programm erzeugt folgende Ausgabe: Wert von ++x ist 6 Wert von y++ ist 5 x nach der Inkrementierung: 6 y nach der Inkrementierung: 6
Zuweisungen Beschreibung Mit Zuweisungsoperatoren können Werte zugewiesen und verändert werden. Neben dem einfachen Zuweisungsoperator gibt es auch die sogenannten zusammengesetzten Zuweisungsoperatoren, mit denen in einer Anweisung sowohl eine Berechnung als auch eine Zuweisung durchgeführt werden kann.
Anwendung Grundsätzlich gilt, daß mit einer Zuweisung dem linken Operanden der Wert des rechten Operanden zugewiesen wird. Damit dies möglich ist, muß der linke Operand veränderbar sein, sich also auf eine Adresse im Speicher beziehen. Diese Ausdrücke werden als »L-Wert« bezeichnet.
Einfache Zuweisung
=
Bei der einfachen Zuweisung erhält der linke Operand den Wert des rechten Operanden. int main() { int bestand, eingang; bestand = 10; eingang = 20; bestand = bestand + eingang; return 0; }
104
Zuweisungen
Zusammengesetzte Zuweisungen
<x>=
Neben dem einfachen Zuweisungsoperator »=« gibt es die zusammengesetzten Zuweisungsoperatoren, mit denen durch einen Operator gleichzeitig eine Berechnung und eine Zuweisung durchgeführt werden kann. Die zusammengesetzten Zuweisungsoperatoren haben das Format: <x>=
wobei <x> durch die Operatoren +, –, *, /, %, <<, >>, &, ^ oder | ersetzt werden kann. int main() { int bestand, eingang; bestand = 10; eingang = 20; bestand += eingang; return 0; }
Zuweisung an Klasseninstanzen
operator =
Zuweisungen an Klasseninstanzen erfolgen durch Aufruf des überladenen Operators =. Wird der Operator nicht in der Klasse überladen, wird der Standardoperator aufgerufen, der elementweise kopiert. Neben der Zuweisung einer Klasseninstanz an eine Instanz der gleichen Klasse, erlaubt der Standard-Zuweisungsoperator die Zuweisung eines Objekts einer eindeutigen als public abgeleiteten Klasse an ein Objekt ihrer Basisklasse, aber nicht die Zuordnung eines Objekts einer Basisklasse an ein Objekt ihrer abgeleiteten Klasse. Durch Kopierkonstruktoren werden Zuweisungen bei der Initialisierung vorgenommen. class Basis { ... }; class Abgeleitet : public Basis { ... }; int main() { Abgeleitet abg; Basis bs; //abg = bs; bs = abg;
// Fehler
105
Sprachkonzepte und Syntax
Vergleichende Operatoren Beschreibung Mit den vergleichenden Operatoren, die auch relationale Operatoren genannt werden, wird ein Vergleich zwischen zwei Operanden, die auch Ausdrücke sein können, durchgeführt. Das Ergebnis dieser Operation ist entweder wahr oder falsch.
Anwendung Ausdrücke, die vergleichende Operatoren enthalten, werden häufig als Kontrollbedingung in do-, while- oder for-Schleifen und if-Anweisungen eingesetzt. Operator
Bedeutung
==
Gleich
!=
Ungleich
<
Kleiner als
>
Größer als
<=
Kleiner als oder gleich
>=
Größer als oder gleich
Warnung ●
Das Ergebnis eines Vergleichs ist ein Wahrheitswert (true, false).
●
Jeder Wert, der ungleich 0 ist, wird von C/C++ als true interpretiert.
●
Zwei Zeiger gleichen Typs, die auf das gleiche Objekt weisen, sind gleich.
●
Zeiger auf verschiedene Elemente einer Klasse sind ungleich, wobei spätere Deklarationen höhere Adressen haben. Dies gilt allerdings nur, wenn kein Zugriffsspezifizierer zwischen den Deklarationen steht. Sollte dies der Fall sein, ist das Ergebnis undefiniert.
Beispiele if (var1 <= var2) funktion(); do { ergebnis = funktion(); } while (ergebnis != 0);
Logische Operatoren Beschreibung Die logischen Operatoren dienen der Verknüpfung oder Negierung logischer Aussagen.
106
Logische Operatoren
Anwendung Die Programmiersprache C kennt drei logische Operatoren: UND, ODER und NICHT. Beim logischen UND- und ODER-Operator wird der Wahrheitswert der Operanden verknüpft, der logische NICHT-Operator konvertiert den logischen Wert seines Operanden in dessen Gegenteil. Die Operanden müssen als Boolesche Werte (true, false) interpretierbar sein. Operator
Bedeutung
&&
Logisches UND
||
Logisches ODER
!
Logisches NICHT
logisches UND
&&
Mit dem Operator »&&« wird eine logische UND-Verknüpfung seiner Operanden durchgeführt. Das Ergebnis der Verknüpfung hat nur dann den Wert true (ungleich Null), wenn beide Operanden den Wert true (ungleich Null) besitzen. Die Operanden werden von links nach rechts ausgewertet. Wenn der erste Operand den Wert false (also Null) ergibt, wird der zweite Operand nicht ausgewertet. Die folgende Wahrheitstabelle zeigt Ihnen die möglichen Kombinationen: 1. Operand
2. Operand
Ergebnis
wahr
wahr
wahr
wahr
falsch
falsch
falsch
wahr
falsch
falsch
falsch
falsch
logisches ODER
||
Der logische ODER-Operator verknüpft seine Operanden, so daß das Ergebnis der Verknüpfung den Wert true hat, wenn einer oder beide Operanden den Wert true hat. Die Operanden werden von links nach rechts ausgewertet. Wenn der erste Operand den Wert true (also ungleich Null) ergibt, wird der zweite Operand nicht ausgewertet. Die folgende Wahrheitstabelle zeigt Ihnen die möglichen Kombinationen: 1. Operand
2. Operand
Ergebnis
wahr
wahr
wahr
wahr
falsch
wahr
falsch
wahr
wahr
falsch
falsch
Falsch
107
Sprachkonzepte und Syntax
Beispiele Das Beispielprogramm gibt eine formatierte ASCII-Tabelle auf den Bildschirm aus. In Zeile 7 wird der logische UND-Operator eingesetzt, um zu erreichen, daß die Ausgabe nur dann erfolgt, wenn spalte einen Wert kleiner als 13 hat (pro Zeile sollen dreizehn Zeichen ausgegeben werden) und der Wert von zeichen kleiner ist als 256 (um auch nur die ASCII-Zeichen auszugeben). In Zeile 8 wird der logische ODER-Operator eingesetzt, um zu erreichen, daß auch die Zeichen des erweiterten IBM-Zeichensatzes ausgegeben werden, da isprint() nur für die ASCII-Zeichen mit den Codes 32 bis 126 das Resultat true liefert. #include int main() { int zeile, spalte, zeichen; printf("ASCII-Tabelle\n"); for(zeile = 0, zeichen = 0; zeile < 20; zeile++) { for(spalte=0; spalte<13 && zeichen<256; spalte++) { if (isprint(zeichen) || zeichen>127) printf("%3c %3d else printf(" %3d zeichen++; } printf("\n"); } return 0; }
logisches NICHT
!
Der logische NICHT-Operator verkehrt den Wahrheitswert seines logischen Operanden in dessen Gegenteil. Dabei wird der Wert 0 (false) in den Wert 1 (true) umgewandelt und alle Werte ungleich 1 zu 0.
Beispiele Das folgende Programm enthält eine Endlosschleife, in der der Anwender aufgefordert wird, eine Zahl einzugeben. In der Schleife wird mit einer if-Abfrage überprüft, ob die Zahl 0 eingegeben wurde. Wenn ja, wird die Schleife mit break beendet. #include <stdio.h int main() { int zahl: for (;;) { printf("Geben Sie eine Zahl ein: ");
108
Datenzugriff scanf("%d", &zahl); if (!zahl) break; } return 0; }
Verweise Siehe Kategorie Programmsteuerung, if-Bedingung Siehe Kategorie Programmsteuerung, Schleifen
Datenzugriff Beschreibung Mehrere Operatoren von C und C++ dienen dem Zugriff auf Daten.
Anwendung Diese Operatoren benötigt man, um auf einzelne Elemente eines Arrays, einer Struktur, Union oder Klasse zuzugreifen. Auch der Gültigkeitsbereichoperator und die Operatoren zur Dereferenzierung von Zeigern, die den Zugriff auf die Objekte ermöglichen, auf die die Zeiger verweisen, gehören in diese Kategorie. Operator
Bedeutung
*
Indirektions-Operator
[]
Array-Indizierung
.
Punkt-Operator
->
Elementverweis-Operator
.*
Dereferenzierung von Zeigern auf Klassenelemente
->*
Dereferenzierung von Zeigern auf Klassenelemente
::
Gültigkeitsbereichoperator
Dereferenzierungs-Operator
*
Der Dereferenzierungs-Operator »*« dient dazu, einen Zeiger zu dereferenzieren und auf dessen Objekt zuzugreifen. Der Typ des Zeigers bestimmt den Typ des Ergebnisses. Wenn also der Operand ein Zeiger auf int ist, hat das Ergebnis den Datentyp int. int func(int *zeiger) { int ergebnis; *zeiger = ergebnis; }
/* Wert zuweisen */
109
Sprachkonzepte und Syntax
Warnung ●
●
Der Dereferenzierungs-Operator kann nicht auf einen unvollständigen Typ, beispielsweise einen Zeiger auf void, angewendet werden. Das Zeichen »*« wird ebenfalls bei der Deklaration von Variablen benutzt, um anzugeben, daß die deklarierte Variable ein Zeiger ist. Es handelt sich dann dabei nicht um einen Dereferenzierungs-Operator. Die Bedeutung ergibt sich somit immer aus dem Kontext.
Array-Indizierung
[]
Der Operator »[ ]« kann verwendet werden, um gezielt auf einzelne Elemente eines Arrays oder eines dynamisch reservierten Feldes zuzugreifen. #define FELD_GROESSE 20 int array[FELD_GROESSE]; int dyn_feld = (int*) malloc(FELD_GROESSE*sizeof(int)); for (i=0; i < Feld_GROESSE; i++) { array[i] = i; dyn_feld[i] = i; };
Warnung ● ●
Das erste Element eines Arrays (Feldes) besitzt den Index 0. Intern wird der Indizierungsoperator vom Compiler durch eine Zeigerberechnung aufgelöst. Die Syntax array[i] ist daher identisch zu *(array+i).
Punkt-Operator
.
Der Punkt-Operator dient dazu, auf einzelne Elemente einer Klasse, Struktur oder einer Union zuzugreifen. objekt.element ●
objekt: muß der Name einer Klasse, Struktur oder einer Union sein.
●
element: muß der Name eines Elements sein.
struct person { char name[20]; int plz; }; int main() { struct person leser; strcpy(leser.name, "Otto"); leser.plz = 5100; return 0; }
110
Datenzugriff
Elementverweis-Operator
->
Der Operator bietet eine vereinfachte Syntax, um über Zeiger auf Klasseninstanzen (inklusive Strukturen und Unions) auf deren Elemente zugreifen zu können. pObjekt->element ●
pObjekt: Zeiger auf eine Variable vom Typ Klasse, Struktur oder Union.
●
element: ein Element.
struct person { char name[20]; int plz; }; int main() { struct person p_leser; strcpy(p_leser->name, "Otto"); p_leser->plz = 5100; return 0; }
Zeiger auf Klassenelemente
->*, .*
Die beiden Operatoren dienen dazu, Zeiger auf Klassenelemente zu dereferenzieren. klassenobj.*p_classelement p_klassentyp->*p_classelement ● ●
●
p_classelement: Zeiger auf ein Element der Klasse T. klassenobj: Instanz der Klasse T oder einer von T abgeleiteten Klasse, von der aus das Element der Basisklasse T eindeutig erreichbar ist. p_klassentyp: Zeiger auf Instanz der Klasse T oder einer von T abgeleiteten Klasse, von der aus das Element der Basisklasse T eindeutig erreichbar ist.
#include <stdio.h> #include <stdlib.h> // Definition der Klasse class extreme { public: // liefert Maximum der Parameter int maximum(int var1, int var2) {return ( (var1 > var2) ? var1 : var2);} // liefert Minimum der Parameter int minimum(int var1, int var2) {return ( (var1 < var2) ? var1 : var2);} }; int main() { class extreme extr; int extremum; int wert1, wert2; wert1 = rand()%20; wert2 = rand()%40;
// Instanzbildung
111
Sprachkonzepte und Syntax // Definition der Zeiger auf Methoden int (extreme::*z_max) (int, int); int (extreme::*z_min) (int, int); // Initialisierung der Zeiger z_max = extreme::maximum; z_min = extreme::minimum; // Dereferenzierung der Zeiger zum Funktionsaufruf extremum = (extr.*z_max) (wert1, wert2); printf("Das Maximum von %d und %d ist %d\n",wert1, wert2, extremum); extremum = (extr.*z_min) (wert1, wert2); printf("Das Minimum von %d und %d ist %d\n",wert1, wert2, extremum); return 0; }
Bereichsoperator
::
Objekte und Funktionen gleichen Namens und gleicher Signatur (Parametertypen) aus unterschiedlichen Gültigkeitsbereichen verdecken sich gegenseitig. Mittels des Bereichsoperators kann aus dem aktuellen Gültigkeitsbereich auf verdeckte Variablen und Funktionen anderer Gültigkeitsbereiche zugegriffen werden. ::global_name
Der unäre Operator dient dazu, auf verdeckte globale Variablen zugreifen zu können (siehe erstes Beispiel). space::name
In seiner binären Form wird der Operator eingesetzt, um ●
auf bestimmte Namensbereiche zuzugreifen,
●
auf statische Klassenelemente zuzugreifen,
●
auf Basisklassenelemente zuzugreifen.
Der Zugriff funktioniert nur, wenn der Name eindeutig aufgelöst werden kann (siehe zweites Beispiel).
Beispiele Zugriff auf verdeckte, globale Variable: #include <stdio.h> int i = 1; int main() { int i = 2; printf("i = %d\n",i); printf("i = %d\n",::i); return 0; }
112
// lokales i verdeckt globales i // Zugriff auf globales i
Bitweise Operatoren
Zugriff auf Variablen aus einem Namensbereich: namespace space1 { int i = 2; } class basis { public: int i; void func() {cout << space1::i;}; // Zugriff auf Namensbereich }; class abgeleitet : public basis { public: int i; void func() {cout << basis::i;}; // Zugriff auf Basisklasse };
Verweise Siehe Kategorie Klassen, Zugriff von außerhalb der Klasse
Bitweise Operatoren Beschreibung Operatoren zur bitweisen Manipulation von Daten. Als Operanden sind allerdings nur IntegerWerte erlaubt.
Anwendung Mit Hilfe der Bit-Operatoren lassen sich bestimmte Programmieraufgaben sehr elegant und effizient lösen. Voraussetzung für den sicheren Gebrauch der Operatoren sind allerdings Kenntnisse über die Binärcodierung der manipulierten Daten.
bitweises UND
&
Wenn der Operator »&« mit zwei Operanden benutzt wird, handelt es sich um den bitweisen UND-Operator. Die Operanden können beliebige Integer-Werte sein. Die beiden Operanden werden bitweise verglichen. Das Bit im Ergebnis wird nur dann gesetzt, wenn in beiden Operanden die korrespondierenden Bits gesetzt sind. Dieser Operator kann eingesetzt werden, um Bits in seinem ersten Operanden gezielt zu löschen. In der ASCII-Tabelle unterscheiden sich die Klein- von den Großbuchstaben des englischen Alphabets (also nicht die deutschen Umlaute) binär dadurch, daß bei den Kleinbuchstaben immer Bit 5 (von rechts her gezählt) gesetzt ist. Um einen
113
Sprachkonzepte und Syntax
Klein- in einen Großbuchstaben umzuwandeln, soll Bit 5 des ersten Operanden gelöscht werden. Um dies zu erreichen, werden im zweiten Operanden, der Maske, alle Bits, bis auf Bit 5, das gelöscht werden soll, gesetzt. Dadurch bleiben die anderen Bits unverändert. & & & &
0110 0001 1101 1111 -------------------0100 0001
Buchstabe 'a' Maske (dezimal 223) Buchstabe 'A'
bitweises ODER
|
Der Operator für die bitweise ODER-Verknüpfung »|« vergleicht die Bitmuster seiner Operanden und setzt das entsprechende Bit im Ergebnis, wenn eines der Bits in den Operanden gesetzt ist. Dieser Operand kann beispielsweise dazu verwendet werden, um gezielt einzelne Bits zu setzen, ohne die anderen Bits der ersten Operanden zu verändern. Ein Beispiel dafür ist die Umwandlung von Groß- in Kleinbuchstaben. In der ASCII-Tabelle unterscheiden sich die Klein- von den Großbuchstaben des englischen Alphabets (also nicht die deutschen Umlaute) binär dadurch, daß bei den Kleinbuchstaben immer Bit 5 gesetzt ist. Um einen Großbuchstaben in den entsprechenden Kleinbuchstaben umzuwandeln, braucht also nur Bit 5 gesetzt zu werden, was Sie im folgenden Beispiel sehen: 0100 0001 | 0010 0000 -------------------0110 0001
Buchstabe 'A' Maske dezimal 32 Buchstabe 'a'
bitweises exklusives ODER
^
Der Operator für die bitweise exklusive ODER-Verknüpfung vergleicht die Bitmuster seiner Operanden und setzt das entsprechende Bit im Ergebnis, wenn eines der Bits in den Operanden, aber nicht beide, gesetzt ist. 0001 1001 ^ 0000 1100 ---------------------0001 0101
114
Bitweise Operatoren
bitweises Komplement
~
Der bitweise Komplement-Operator »~« kippt alle Bits seines Operanden. Durch Invertierung werden im Ergebnis alle Bits, die im Operanden gesetzt waren, gelöscht, und alle Bits, die gelöscht waren, gesetzt. Die Zahl 4 besitzt folgendes Bitmuster: 0000
0100
Durch die Operation ~4 erhalten wir folgendes Muster: 1111
1011
Rechtsverschiebung
>>
Bei der bitweisen Rechtsverschiebung werden die einzelnen Bits eines Operanden um die festgelegte Anzahl nach rechts verschoben. Der Operator kann beispielsweise für eine schnelle Programmierung der Division durch die Potenzen der Zahl 2 benutzt werden. Der Ausdruck 80 >> 3 entspricht 80 / 2^3 und erzeugt folgendes Ergebnis: dezimal
binär
vor Verschiebung
80
0101 0000
nach Verschiebung
10
0000 1010
Linksverschiebung
<<
Bei der bitweisen Linksverschiebung werden die einzelnen Bits eines Operanden um die festgelegte Anzahl nach links verschoben. Der Operator kann beispielsweise für eine schnelle Programmierung der Multiplikation mit den Potenzen der Zahl 2 benutzt werden. Der Ausdruck 3 << 2 entspricht 3 * 2^2 und erzeugt folgendes Ergebnis: dezimal
binär
vor Verschiebung
3
0000 0011
nach Verschiebung
12
0000 1100
Beispiele #include <stdio.h> int main() { printf(" a & 223 printf(" A | 0x20 printf(" 25 ^ 12 printf(" 3 << 2
= = = =
%c\n", %c\n", %d\n", %d\n",
'a' 'A' 25 3
& 223); | 0x20); ^ 12); << 2);
115
Sprachkonzepte und Syntax printf(" 80 >> 3 printf("~4 return 0; }
= %d\n", 80 >> 3 ); = %d\n", ~4);
Das Programm erzeugt folgende Ausgabe: a A 25 3 80 ~4
& 223 | 0x20 ^ 12 << 2 >> 3
= = = = = =
A a 21 12 10 -5
Verweise Siehe Streamoperatoren
Streamoperatoren Beschreibung In C++ sind die Operatoren >> und << für das formatierte Einlesen und Schreiben von und in Streams überladen.
Anwendung Der Einsatz der Streamoperatoren für die Ein- und Ausgabe ist recht komfortabel und sehr übersichtlich. int wert = 3; cout << "Wert = " << wert << endl;
// cout ist der Standardausgabestream
Die Programmierung mit Streamoperatoren ist zudem in vorbildlicher Weise objektorientiert: ●
●
die Streamoperatoren sind standardmäßig für alle elementaren Datentypen überladen (int, float, char, etc.). durch Überladung kann das Einsatzgebiet der Operatoren auf selbst definierte Klassentypen ausgedehnt werden.
Durch sogenannte Manipulatoren kann die Formatierung der Ein- und Ausgabe gesteuert werden.
Beispiele int wert; cout << "Geben Sie einen Wert ein:" << endl; cin >> wert; cout << left; cout << "Wert = " << wert << endl;
116
Die Operatoren new und delete
Verweise Siehe Referenz der C++-Laufzeitbibliothek, Header-Dateien iostream, iomanip, etc.
Die Operatoren new und delete Beschreibung Die Operatoren new und delete dienen zur dynamischen Speicherverwaltung und können in C++-Programmen die Funktionen malloc() und free() ersetzen. Intern sind die Operatoren meist mit Hilfe der Funktionen malloc() und free() implementiert, sie erlauben aber eine einfachere und erweiterte Handhabung.
new-Operator
new
Der new-Operator dient zur dynamischen Speicherallokation. Für die Implementierung des Operators sind im ANSI C++-Standard allein 6 Operatorfunktionen vorgesehen. Die verschiedenen Operatorfunktionen sind für die Unterscheidung von einfachen Objekten und Arrays als Operanden, und für die einfache Steuerung der Fehlerbehandlung und die Zuweisung eines bestimmten Speicherbereiches gedacht. Zudem gibt es zwei Formen des Aufrufs, je nachdem, ob die Typangabe geklammert wird oder nicht. Dies ist notwendig, um in Typangaben, die Operatoren wie &, * oder [] enthalten, unterscheiden zu können, ob die Deklarations- oder die Zuweisungsoperatoren gemeint sind: new [freie_arg] Typ [initialisierer] new [freie_arg] (Typ) [initialisierer] ●
freie_arg: Argumentenliste, die überladenen Formen übergeben werden kann.
●
Typ: Datentyp, der für den Speicher zu reservieren ist.
●
initialisierer: Wert, der benutzt wird, den reservierten Speicherplatz zu initialisieren.
Beispiel 1: 2: 3: 4: 5: 6: 7: 8: 9: 10:
long double *p, *q, **pp; p = new long double; pp = new long double*; p = new long double(1.2); //q = new long double * 3; q = new long double [3]; //q = new long double [3](1,2,3); class demo { int i; } *p_demo;
117
Sprachkonzepte und Syntax 11: 12: 13: 14: 15: 16:
p_demo = new demo[10]; void (**func_ptr)(int); //func_ptr = new void(*[3])(int); func_ptr = new (void(*[3])(int)); char ziel[sizeof(demo)]; p_demo = new (ziel) demo;
Erläuterung In Zeile 2 wird ein Zeiger auf Objekte des Datentyps long double eingerichtet, in Zeile 3 ein Zeiger auf einen Zeiger auf Objekte des Datentyps long double. In Zeile 4 wird der Zeiger p eingerichtet und der Speicherbereich, auf den der Zeiger p verweist, mit dem Wert 1.2 initialisiert. In Zeile 5 wird versucht, einen Zeiger auf ein Array von 3 long double Werten einzurichten. Die verwendete Syntax ist ähnlich dem Gebrauch der Funktion malloc(), führt hier aber zu Fehlern, da der Compiler den Aufruf als (new long double)*3 versteht. In Zeile 6 erfolgt die korrekte Allokation des Arrays. Zeile 7: syntaktisch korrekt, aber Arrays können nicht initialisiert werden. In Zeile 11 wird ein Array von Klassenobjekten eingerichtet. In Zeile 12 wird ein Zeiger func_ptr auf Funktionszeiger deklariert. Danach soll für den Zeiger func_ptr Speicher für ein Array von Funktionszeigern reserviert werden, dessen erstes Element ihm als Adresse übergeben wird. Die Zuweisung in Zeile 13 führt wie in Zeile 5 zu einem Interpretationsfehler. In Zeile 14 wird daher die zweite Syntaxform gewählt, bei der die Typangabe eingeklammert ist. In Zeile 15 wird Speicher von der Größe eines demo-Objekts reserviert. In Zeile 16 wird das demo-Objekt in dem Speicherbereich, auf den ziel verweist, eingerichtet (Plazierungsform des Operators).
Warnung ●
●
●
●
Objekte, die mit new eingerichtet wurden, haben unbegrenzte Lebensdauer. Der Speicherplatz muß daher mittels des delete-Operators wieder freigegeben werden. Wird Speicher für ein Array reserviert, liefert der new-Operator einen Zeiger auf das erste Array-Element zurück. Bei der Reservierung von Arrays müssen alle Dimensionen durch konstante, positive Ausdrücke angegeben werden; die erste Dimension kann durch einen allgemeinen Ausdruck spezifiziert werden. Arrays können nicht initialisiert werden.
118
Typumwandlung und -identifizierung
●
●
●
●
Klassenobjekte können nur mit new eingerichtet werden, wenn die Klasse einen Standardkonstruktor besitzt, oder die Argumente für den Konstruktor dem Operator übergeben werden. Referenzen können nicht mit new eingerichtet werden, da sie keinen Speicherplatz haben. Der Operator berechnet den Speicherbedarf für den einzurichtenden Datentyp selbst, weswegen der erste Parameter einer new-Operatorfunktion immer vom Typ size_t ist (zu beachten bei Überladung). Wird der new-Operator überladen, sollte auch eine entsprechende Form des delete-Operators implementiert werden.
delete-Operator
delete
Der delete-Operator dient zur dynamischen Freigabe von mittels new allokiertem Speicher. delete p_objekt delete [] p_objekt
Warnung ●
Zeiger auf Konstanten können nicht gelöscht werden.
●
Für Klassen wird beim Löschen der Destruktor aufgerufen.
●
●
Die zweite Version ist zur Freigabe von Arrays. Die Dimension wird dabei nicht angegeben. Aufrufe mit Nullzeigern sind ohne Effekt.
Beispiele class demo *p; p = new demo[10]; delete[] p; int **p; p = new int*; delete p;
Typumwandlung und -identifizierung Beschreibung Mit Hilfe der Operatoren zur Typumwandlung kann man innerhalb bestimmter Grenzen Werte und Objekte eines Datentyps in einen anderen Datentyp umwandeln. Mit Hilfe des typeid-Operators kann man in C++ den Typ eines Klassenobjekts zur Laufzeit feststellen.
119
Sprachkonzepte und Syntax
Anwendung Gelegentlich ist es wünschenswert, daß eine Variable eines bestimmten Datentyps als einem anderen Datentyp angehörig behandelt wird. Sofern eine solche Typumwandlung durchführbar ist, kann sie durch den C-Cast-Operator () oder die C++Operatoren const_cast, dynamic_cast, static_cast und reinterpret_cast, die als Schlüsselworte in die Sprache aufgenommen sind, explizit herbeigeführt werden. Die C++-Operatoren sind vor allem dazu gedacht, die verschiedenen Konvertierungen (die auch mit dem Cast-Operator () vorgenommen werden könnten) nach ihrem Gefahrenpotential und dem Einsatzgebiet zu kategorisieren. Bei Auftreten eines Fehlers kann der Programmierer dann eventuell aus der Art des Fehlers auf die betreffende Typumwandlung schließen und mit Hilfe der Suchfunktion seines Editors schnell zu allen Vorkommen des jeweiligen Operators springen. Operator
Beschreibung
static_cast(var)
Zur Umwandlung zwischen »nahe verwandten« Typen – also alle Fälle, in denen der Compiler auch eine implizite Konvertierung vornehmen könnte, plus einige einfache Sonderfälle.
reinterpret_cast(var)
Für fast alle anderen erlaubten Umwandlungen.
const_cast(var)
Entfernt eine const- oder volatile-Deklaration aus einem Typ.
dynamic_cast(var)
Zur Umwandlung zwischen Zeigern oder Referenzen auf Klassentypen einer gemeinsamen Klassenhierarchie
Der Cast-Operator
()
Der Cast-Operator kann benutzt werden, um in einer Anweisung den Typ des Ausdrucks der rechten Seite in den Typ des Bezeichners der linken Seite umzuwandeln: bezeichner1 = (Typ_von_bezeichner1) AUSDRUCK; bezeichner1 = (Typ_von_bezeichner1) bezeichner2;
Eine zweite Form der Syntax wandelt den Typ einer Variablen direkt um bezeichner = Datentyp();
Beispiele intptr = (int *) malloc(80 * sizeof(int)); printf("%f\n", (double) 13/2); int i = int(3.14); int i = int(); // weist undefinierten Wert zu
120
Typumwandlung und -identifizierung
Typumwandlung
const_cast (nur C++)
Der Operator const_cast dient dazu, die Qualifizierer const und volatile aus einer Typangabe zu entfernen oder gegeneinander auszutauschen. Den Operator const_cast kann man beispielsweise dazu nutzen, um eine Variable, die durch const-Deklaration grundsätzlich vor ungewollter Überschreibung geschützt wurde, für eine nachfolgende Zuweisung veränderbar zu machen. const_cast < Zieltyp > (variable) ●
●
Zieltyp: Angabe des Typs, in den die variable umgewandelt werden soll. Der Zieltyp muß mit dem Typ der variable bis auf die Qualifizerer const oder volatile übereinstimmen. variable: Umzuwandelndes Objekt.
Beispiele const int* p; int* q; p = q; q = p; q = const_cast (p);
// korrekt: Zuweisung eines int*-Objekts // an const int* // Fehler: const int* an int* // ok.
Typumwandlung
dynamic_cast (nur C++)
Der dynamic_cast-Operator kann dazu verwendet werden, Zeiger (oder Referenzen) auf Objekte einer Basisklasse in Zeiger (oder Referenzen) auf Objekte einer abgeleiteten Klasse umzuwandeln oder vice versa. UpCast. In einem Objekt einer abgeleiteten Klasse sind die Elemente der Basisklassen praktisch als Teilobjekte enthalten. Ist T eine solche eindeutige public Basisklasse, kann ein Zeiger auf das Gesamtobjekt der abgeleiteten Klasse in einen Zeiger auf das Teilobjekt der Basisklasse umgewandelt werden (gleiches gilt für Referenzen). Dieser Vorgang gehört zu den Standardkonvertierungen von C++ und erfordert an sich nicht den Einsatz des dynamic_cast-Operators. DownCast. Umgekehrt kann ein Zeiger auf ein Basisklassenobjekt, das Teil eines abgeleiteten Objekts ist, in einen Zeiger auf das abgeleitete Objekt zurückverwandelt werden, wenn es sich um eine polymorphe Basisklasse (Klasse, die virtuelle Methoden enthält) handelt. (Für nicht polymorphe Basisklassen kann man den static_cast-Operator verwenden.) class_zeiger1 = dynamic_cast < Zieltyp > (class_zeiger2) ●
●
●
class_zeiger1: Zeiger oder Referenz auf Klassenobjekt, dem der umgewandelte Zeiger (Referenz) class_zeiger2 zugewiesen wird. Zieltyp: Angabe des Typs, in den class_zeiger2 umgewandelt werden soll (Typ von class_zeiger1 oder void* ). class_zeiger2: Umzuwandelnder Zeiger (Referenz).
121
Sprachkonzepte und Syntax
Beispiele #include using namespace std; class Basis { public: virtual void func() {cout << "basisklasse" << endl;} }; class Abgeleitet : public Basis { public: void func_abg() {cout << "abgeleitete Klasse" << endl;} }; int main() { Abgeleitet *p_abg = new Abgeleitet; Basis *p_bp = new Basis; // Umwandlung Zeiger auf abgeleitete Klasse zu Basisklasse // p_bp = p_abg; ginge auch p_bp = dynamic_cast (p_abg); if(p_bp != NULL) p_bp->func_abg(); // Fehler // Umwandlung Zeiger auf Basisklasse in abgeleitete Klasse // p_abg = p_bp; nicht möglich p_abg = dynamic_cast (p_bp); if(p_abg != NULL) p_abg->func_abg(); // o.k. return 0; }
Zudem kann man unter bestimmten Voraussetzungen mit dynamic_cast auch einen CrossCast von einer Basisklasse in eine andere Basisklasse bewirken. Im Fehlerfall wird für Referenzen eine bad_cast-Exception ausgelöst und für Zeiger ein NULLZeiger zurückgeliefert.
Typumwandlung
static_cast (nur C++)
Mit dem Operator static_cast können alle gängigen Konvertierungen explizit vorgenommen werden. Darüber hinaus erlaubt er eine Konvertierung eines Zeigers auf eine nicht-polymorphe Basisklasse in eine ihrer abgeleiteten Klassen, und ergänzt hierin den Operator dynamic_cast. bezeichner1 = ●
static_cast < Zieltyp > (bezeichner2)
bezeichner1: Objekt, dem das umgewandelte Objekt bezeichner2 zugewiesen wird.
●
Zieltyp: Angabe des Typs, in den bezeichner2 umgewandelt werden soll.
●
bezeichner2: Umzuwandelndes Objekt.
122
Typumwandlung und -identifizierung
Beispiele #include using namespace std; class Basis { public: void func() {cout << "basisklasse" << endl;} }; class Abgeleitet : public Basis { public: void func_abg() {cout << "abgeleitete Klasse" << endl;} }; int main() { Abgeleitet *p_abg = new Abgeleitet; Basis *p_bp = new Basis; // Umwandlung Zeiger auf abgeleitete Klasse zu Basisklasse p_bp = dynamic_cast (p_abg); if(p_bp == NULL) cout << "cast gescheitert" << endl; else p_bp->func_abg(); // Fehler // Umwandlung Zeiger auf Basisklasse in abgeleitete Klasse p_abg = static_cast (p_bp); if(p_abg == NULL) cout << "cast gescheitert" << endl; else p_abg->func_abg(); // o.k. return 0; }
Typumwandlung
reinterpret_cast (nur C++)
Der Operator reinterpret_cast ist für besonders kritische Konvertierungen gedacht, beispielsweise die Konvertierung von Integer-Typen in Zeiger und vice versa, sowie zur Konvertierung von Zeigern auf Funktionen. bezeichner1 = reinterpret_cast < Zieltyp > (bezeichner2) ●
bezeichner1: Objekt, dem das umgewandelte Objekt bezeichner2 zugewiesen wird.
●
Zieltyp: Angabe des Typs, in den bezeichner2 umgewandelt werden soll.
●
bezeichner2: Umzuwandelndes Objekt.
123
Sprachkonzepte und Syntax
Warnung ●
●
●
●
Werden Integer-Typen in Zeiger umgewandelt oder umgekehrt, ist nicht sichergestellt, daß der Wert des konvertierten Objektes korrekt interpretiert werden kann. Es ist allerdings sichergestellt, daß bei einer nachfolgenden Rückkonvertierung der ursprüngliche Wert wieder erscheint (natürlich vorausgesetzt, daß zwischen den Konvertierungen keine Zuweisung an die Variable erfolgte). Entsprechendes gilt, wenn Zeiger auf Funktionen in Zeiger auf Funktionen anderen Typs oder in Zeiger auf Objekte umgewandelt werden. Werden Zeiger oder Referenzen konvertiert, sind undefinierte Klassenbezeichner in der Typangabe erlaubt.
Typidentifizierung
typeid
Der Operator typeid erzeugt für das ihm übergebene Objekt eine entsprechende Instanz der Klasse type_info (definiert in typeinfo). In dieser Instanz, die vom Operator als Referenz zurückgeliefert wird, sind Informationen zum Datentyp des übergebenen Objekts abgelegt. Die type_info-Objekte können direkt mit Hife der Operatoren == und != verglichen werden. Zudem kann die Methode name() aufgerufen werden, die einen String zurückliefert, der den Typ des typeid-Operanden identifiziert.
Warnung Laut ANSI-Standard sind Typen, die sich nur in den CV-Qualifizierern unterscheiden (const, volatile) identisch. Einige Compiler differenzieren hier aber.
Beispiele #include #include using namespace std; class demo {int wert;}; int main() { demo d1(); const demo d2(); if(typeid(d1) == typeid(d2)) cout << "gleich" << endl; if(typeid(d1).name() == typeid(d2).name()) cout << typeid(d1).name() << endl; return 0; }
124
Sonstige Operatoren
Sonstige Operatoren sizeof-Operator
sizeof
Der sizeof-Operator liefert als Ergebnis die Größe seines Operanden in Byte zurück. Der Rückgabewert ist vom vorzeichenlosen Integer-Typ size_t. Als Operand kann sowohl ein Datentyp als auch eine Variable übergeben werden. Typische Einsatzgebiete für den sizeof-Operator sind Speicherbedarfberechnungen für die dynamische Speicherallokation sowie die Berechnung der Anzahl an Elementen in einem Array.
Warnung ●
●
●
●
●
●
Für Referenzen liefert der sizeof-Operator die Größe des referenzierten Objektes. Nicht zulässig sind: Funktionen, Bitfelder, undefinierte Klassen, Arrays mit fehlender Dimensionsangabe sowie der Typ void. Wenn einer der Typen char, unsigned char oder signed char als Argument verwendet wird, ist das Ergebnis per Definition immer 1. Die Größe eines Zeichenliterals (sizeof('Z')) entspricht in C der Größe von int, in C++ der Größe von char. Wird sizeof auf ein Array angewendet, ist das Ergebnis die Gesamtgröße des Arrays in Bytes. Doch Vorsicht! Wenn sizeof innerhalb einer Funktion benutzt wird, um die Größe eines Arrays zu ermitteln, das an einen Parameter der Funktion übergeben wurde, liefert sizeof die Größe des Zeigers auf das Array zurück, da Arraynamen als Zeiger übergeben werden. Handelt es sich beim Argument um eine Klasse, Struktur oder Union, wird die tatsächliche Größe zurückgegeben, in der auch Füllbytes zur Ausrichtung an Speichergrenzen enthalten sein können. Das Ergebnis der sizeof-Operation kann dann von der Summe des Speicherplatzbedarfs der einzelnen Elemente verschieden sein.
Beispiele int main() { int feld[123]; // liefert anz = 123; int anz = sizeof(feld)/sizeof(int); // reserviert Feld für 123 double-Werte double *ptr = (double*) malloc(anz*sizeof(double)); return 0; }
125
Sprachkonzepte und Syntax
Adreß-Operator
&
Wenn der Operator »&« mit einem Operanden benutzt wird, liefert er die Adresse seines Operanden zurück. Als Operand kann jeder Bezeichner eingesetzt werden, der einen L-Wert verkörpert.
Warnung ●
●
Der Operator kann nicht auf das Element eines Bitfeldes oder auf ein Objekt, das mit der Speicherklasse register definiert wurde, angewendet werden. Funktions- und Array-Namen können direkt als Adressen interpretiert werden, weswegen für sie die Anwendung des Adreß-Operators nicht erforderlich ist.
Beispiele // Integer-Wert von Tastatur einlesen int zahl; scanf("%d", &zahl); // Zeiger auf eine Variable richten int zahl_wert; int *zahl_zeiger; zahl_zeiger = &zahl_wert; // Uebergabe der Funktion demo() an func() void demo() {...} void func(int* i, void (*f)() ) {...} func(&zahl_wert, demo);
Bedingungs-Operator (bedingte Bewertung)
?:
ANSI C/C++ kennt einen einzigen ternären Operator, der auch Bedingungs-Operator genannt wird. Der Bedingungs-Operator (?:) ist eigentlich eine Kurzform der if-else-Anweisung. Das allgemeine Format dieses Operators lautet folgendermaßen: Bedingung ? Ausdruck1 : Ausdruck2
Auswertung ●
●
Wenn die Auswertung der Bedingung nicht 0 (also wahr) ergibt, wird Ausdruck1 ausgeführt. Ergibt die Auswertung der Bedingung 0 (also unwahr), wird Ausdruck2 ausgewertet.
126
Sonstige Operatoren
Beispiele int max, eins, zwei; eins = 1; zwei = 2; max = (eins > zwei) ? eins : zwei;
Die gleiche Zuweisung kann auch als if-else-Anweisung formuliert werden: if (eins > zwei) max = eins; else max = zwei;
Sequentielle Auswertung (Komma-Operator)
,
Wenn das Zeichen »,« nicht eine Liste von Elementen trennt (Funktionsparameter, Initialisierungswerte etc.), dient es zur sequentiellen Auswertung. Er wird üblicherweise an Stellen verwendet, an denen nur ein Ausdruck erlaubt ist, um zwei oder mehr Ausdrücke auszuwerten.
Warnung ●
●
●
Das Ergebnis der durch den Komma-Operator getrennten Ausdrücke hat den gleichen Wert und Typ wie der rechte Operand. Die Operanden können einen beliebigen Typ besitzen; Typumwandlungen werden nicht durchgeführt. In Listen müssen die entsprechenden Ausdrücke geklammert werden.
Beispiele func(int par1, float par2) {..};
Das nächste Beispiel verwendet den Komma-Operator im Reinitialisierungsteil einer for-Schleife, um bei jedem Schleifendurchlauf nicht nur eine, sondern beide Schleifenvariablen i und j zu inkrementieren. for (i=j=0; i<MAXWERT; i++, j++) { ...
Im dritten Beispiel wird zuerst der linke Operand (i++) ausgewertet. Anschließend wird das Ergebnis von zahl2/4 der Variablen zahl1 zugewiesen. zahl1 = (i++, zahl2 / 4);
Wird der Operator in Listen eingesetzt, müssen die Anweisungen geklammert werden. func(i, (f=3.14, f+0.0015));
127
Sprachkonzepte und Syntax
Synonyme In C++ wurden für einige Operatoren synonyme Bezeichnungen eingeführt. Synonym
Operator
bitand
&
and
&&
bitor
|
or
||
xor
^
compl
~
and_eq
&=
or_eq
|=
xor_eq
^=
not
!
not_eq
!=
Priorität und Assoziativität Beschreibung Um vorhersagen zu können, wie ein Ausdruck ausgewertet wird, müssen Sie wissen, wie der Compiler Ausdrücke auswertet. Priorität
Jeder Operator gehört einer bestimmten Prioritätsklasse an. In einem Ausdruck werden die Operatoren in der Reihenfolge ihrer Priorität abgearbeitet, wobei Operatoren höherer Priorität zuerst ausgewertet werden.
Assoziativität Gibt es in einem Ausdruck mehrere Operatoren gleicher Priorität, werden diese in der für die Operatoren charakteristischen Richtung abgearbeitet. Operanden
In welcher Reihenfolge die Operanden eines binären Operatoren ausgewertet werden, ist nicht festgelegt (meist wird zuerst der linke, dann der rechte Operand ausgewertet). Für die logischen Operatoren ist vorgeschrieben, daß der linke Operand zuerst ausgewertet wird. Ist der linke Operand eines &&-Operators gleich 0 (false), wird der rechte Operand nicht mehr ausgewertet. Gleiches gilt für den ||-Operator, wenn der linke Operand gleich 1 (true) ist.
Klammern
Durch das Setzen von Klammern kann man die Reihenfolge, in der die Operatoren abgearbeitet werden, beeinflussen.
Priorität und Assoziativität der einzelnen Operatoren können Sie der nachfolgenden Tabelle entnehmen.
128
Überladung von Operatoren
Priorität
Operator
18
:: (expr)
17
[] -> .
Assoziativität
L-R
Postfix: ++ -typeid, cast-Operatoren 16
unär: + – * ! ~ & ()
R-L
Präfix: ++ -new, delete, sizeof 15
.* ->*
L-R
14
*/%
L-R
13
binär: + -
L-R
12
<< >>
L-R
11
< <= > >=
L-R
10
== !=
L-R
9
& (bitand)
L-R
8
^ (xor)
L-R
7
| (bitor)
L-R
6
&&
L-R
5
||
L-R
4
= *= /* %= += – = <<= >>=
R-L
&= |= ^= 3
?:
R-L
2
throw
L-R
1
,
L-R
Überladung von Operatoren Beschreibung Die Überladung von Operatoren wird eingesetzt, um das Verhalten eines Operators an die ihm übergebenen Operanden anzupassen. Der Vorteil für den Programmierer besteht darin, daß er die zur Verfügung stehenden Operatoren für eigene Datentypen, genauer gesagt Klassen, umdefinieren kann. Ein Laufzeitvorteil ergibt sich dadurch nicht, da sich hinter den überladenen Operatoren Funktionen verbergen.
129
Sprachkonzepte und Syntax
Anwendung Operatoren können innerhalb der Deklaration ihrer Klasse oder außerhalb im Dateibereich überladen werden. Bei Überladung im Klassenbereich ist der erste Operator automatisch ein Objekt der Klasse und wird bei der Definition der Operatorfunktion nicht mehr als Parameter aufgeführt. Im Klassenbereich überladene Operatoren haben Zugriff auf die private und protected Elemente der Klasse (siehe nachfolgende Beispiele). Bei Überladung im Dateibereich müssen alle Operanden bei der Definition der Operatorfunktion als Parameter aufgeführt werden. Im Dateibereich überladene Operatoren haben keinen Zugriff auf die private und protected Elemente der Klasse. Man kann dem aber abhelfen, indem man die Operatorfunktion in der Klasse als friend deklariert (siehe Überladung der Stream-Operatoren). Operatoren, die aneinandergereiht werden können (beispielsweise var1 = var2 = var3;), müssen dazu eine Referenz auf ihren linken Operanden als Rückgabewert zurückliefern.
Warnung ● ●
●
●
●
Es können keine neuen Operatoren definiert werden. Folgende Operatoren können nicht überladen werden: ::, ?:, ., .*, #, ##, defined, typeid, die C++-Cast-Operatoren. Folgende Operatoren können nur in einem Klassenbereich überladen werden: =, (), [], -> . Mindestens ein Operand eines überladenen Operators muß eine Klasse oder eine Referenz auf eine Klasse sein. (Die Funktionsweise der Operatoren für die elementaren Datentypen kann also nicht geändert werden.) Die allgemeine Auslegung des Operators (Anzahl Operanden, Priorität, Vorgabeargumente) kann nicht verändert werden.
Überladung unärer Operatoren Unäre Operatoren, die außerhalb des Klassenbereichs überladen werden, erwarten die Übergabe eines Operanden vom Klassentyp. Unäre Operatoren, die innerhalb einer Klasse überladen werden, dürfen nur durch nicht-statische Elementfunktionen ohne Parameter überladen werden. Die aufrufende Klasseninstanz wird automatisch als Operand genommen. Syntax: Deklaration in Klasse Rückgabetyp operator @() {}
// @ gleich !, ++, --, ...
Syntax: Deklaration in Dateibereich Rückgabetyp operator @(Klassentyp) {}
130
// @ gleich !, ++, --, ...
Überladung von Operatoren
Beispiele class person { int alter; public: ... operator ++ () { ++alter; return *this; } };
//Praefix-Inkrement
Die Deklaration eines int-Parameters für den Postfix-Inkrement-Operator spezifiziert keinen wirklichen zweiten Operanden, sondern dient nur zur Unterscheidung vom Präfix-Inkrement-Operator.
Überladung binärer Operatoren Binäre Operatoren, die außerhalb des Klassenbereichs überladen werden, erwarten die Übergabe beider Operanden. Einer der Operanden muß ein Klassentyp sein. Binäre Operatoren, die innerhalb einer Klasse überladen werden, dürfen nur durch nicht-statische Elementfunktionen mit einem Parameter überladen werden. Die aufrufende Klasseninstanz wird automatisch als erster Operand genommen.
Beispiele class person { .. }; class adressbuch { /* enthält als Datenelemente eine Menge von person-Einträgen, die nicht als statisches Array, sondern als dynamische Liste oder Baum verwaltet werden */ ... public: person& operator[] (int); }; person& adressbuch::operator[] (int i) { class person *gefunden; /* durchsucht dynamische Struktur */ ... return *gefunden; }; int main() { class adressbuch persoenlich; class person eintrag; ... eintrag = persoenlich[3]; return 0; }
131
Sprachkonzepte und Syntax
Programmsteuerung Programmsteuerung Beschreibung Je komplexer Programme werden, desto öfter ist es erforderlich, je nach Situation und Zustand des Programms zu verschiedenen Anweisungen zu verzweigen. Zur Steuerung des Programmflußes gibt es veschiedene Konzepte: Verzweigungen Schleifen Exceptions Bedingte Kompilation
Anwendung ●
●
●
●
Verzweigungen. Verzweigungen ermöglichen es dem Programmierer, einen oder mehrere Anweisungsblöcke nur in Abhängigkeit von bestimmten Bedingungen ausführen zu lassen. Schleifen. Schleifen ermöglichen es dem Programmierer, einen Anweisungsblock mehrmals hintereinander ausführen zu lassen. Exceptions. Exeptions und das Konzept der Exceptions-Behandlung ermöglichen es einem Programmierer, die Fehlerbehandlung weitgehend von dem Programmfluß des eigentlichen Programmcodes zu trennen. Dazu gehört auch, daß der Ort der Fehlerentstehung nicht mehr mit dem Ort der Fehlerbehandlung zusammenfallen muß. Bedingte Kompilation. Mit Hilfe von Präprozessor-Direktiven kann der Programmierer steuern, welche Codeblöcke in Abhängigkeit von PräprozessorSchaltern kompiliert und in das ausführbare Programm aufgenommen werden sollen und welche nicht.
Die if-Bedingung Beschreibung Die if-Bedingung kontrolliert, ob eine nachfolgende Anweisung oder ein Anweisungsblock ausgeführt wird oder nicht.
132
Die if-else-Verzweigung
Anwendung Auf das Schlüsselwort if folgt in Klammern eine Bedingung. Bei Ausführung des Programms wird diese Bedingung ausgewertet. Ergibt die Bewertung das Ergebnis true (also einen Wert ungleich Null), wird die zu if gehörende Anweisung ausgeführt. Liefert die Auswertung des Ausdrucks als Ergebnis false, wird die Ausführung des Programms mit der ersten Anweisung hinter der Verzweigung fortgesetzt.
Syntax if (Bedingung) Anweisung;
●
●
if (Bedingung) { Anweisungsblock; }
Bedingung: Die Bedingung, die ausgewertet wird, muß einen booleschen Wert ergeben. Hier kann beispielsweise eine Ganzzahl, eine arithmetische Operation, ein Vergleich oder der Aufruf einer Funktion mit entsprechendem Rückgabetyp stehen. Anweisung/Anweisungsblock: Auf die if-Bedingung kann eine einzelne Anweisung oder ein Anweisungsblock folgen. Eine einzelne Anweisung muß immer mit einem Semikolon (dem Anweisungsbegrenzer) beendet werden. Wenn ein Anweisungsblock ausgeführt werden soll, muß dieser in geschweifte Klammern eingeschlossen werden.
Warnung Nie ein Semikolon hinter die Bedingung setzen. Dies würde der Compiler als eine leere Anweisung interpretieren. Die if-Bedingung bezöge sich dann nur auf diese leere Anweisung.
Beispiele if (cZeichen == 'a') printf("Das Zeichen ist a."); if ( isdigit(cZeichen) ) printf("Das Zeichen ist eine Zahl");
Die if-else-Verzweigung Beschreibung Die if-else-Verzweigung regelt, welcher von zwei Anweisungsblöcken in Abhängigkeit von einer Bedingung ausgeführt werden soll.
133
Sprachkonzepte und Syntax
Anwendung Die einfache if-Bedingung kann nur steuern, ob ein einzelner Anweisungsblock ausgeführt werden soll oder nicht. Es gibt aber auch Fälle, in denen man – je nachdem, ob die Bedingung true oder false ergibt – den einen oder anderen Anweisungsblock ausführen möchte. Diese Sprachkonstruktion wird durch das Schlüsselwort else eingeleitet. if (Bedingung) Anweisung; else Anweisung;
if (Bedingung) { Anweisungsblock } else { Anweisungsblock }
Beispiele if (cZeichen == 'a') printf("Das Zeichen ist a."); else printf("Das Zeichen ist nicht a.");
if-else-Ketten Beschreibung Sprachkonstrukte, in denen der else-Teil einer if-else-Verzweigung selbst wiederum eine ifelse-Verzweigung ist.
Anwendung Mit den Schlüsselworten if und else ist es möglich, sogenannte else-if-Ketten zu erstellen, bei denen mehrere Ausdrücke überprüft werden können. if (Bedingung) Anweisung; else if (Bedingung) Anweisung; else if (Bedingung) Anweisung; else Anweisung;
Auswertung der else-if-Ketten Die Bedingungen werden in der Reihenfolge ausgewertet, in der sie im Programmcode stehen. Wenn eine der Bedingungen als Ergebnis true liefert, wird der zugehörende Anweisungsteil ausgeführt und damit die Abarbeitung der Kette beendet. Die zum letzten else gehörenden Anweisungen werden ausgeführt, wenn keine der vorher überprüften Bedingungen das Ergebnis true liefert.
134
switch-Verzweigung
Beispiele printf("Geben Sie ein: Zahl Operator Zahl \n"); scanf("%f %c %f", &zahl1, &Operator, &zahl2); if (Operator == '+') printf("= %f", zahl1 + zahl2); else if (Operator == '-') printf("= %f", zahl1 – zahl2); else if (Operator == '*') printf("= %f", zahl1 * zahl2); ...
Verweise Siehe switch-Verzweigung
switch-Verzweigung Beschreibung Komplexe Sprunganweisung, die in Abhängigkeit von einem Ausdruck zu verschiedenen Einsprungmarken in einen Anweisungsblock verzweigt.
Anwendung Eine einfachere und meist auch übersichtlichere Programmiertechnik als die im vorigen Abschnitt beschriebenen if-else-Ketten bietet die switch-Anweisung. Auch mit ihr kann ein Programm zwischen mehreren Alternativen auswählen – allerdings i.G. zur if-else-Kette nur in Abhängigkeit vom Wert eines Ausdrucks. switch(Ausdruck) { case Konstante1: break; case Konstante2: break; case Konstante3: break; case Konstante4: break; default: Anweisung; } ●
●
Anweisung; Anweisung; Anweisung; Anweisung;
Ausdruck: Ausdruck ist ein ganzzahliger Wert (auch Aufzählungstyp), der mit allen innerhalb der switch-Anweisung stehenden case-Marken verglichen wird. Hier kann auch der Aufruf einer Funktion stehen, die eine Ganzzahl als Ergebnis zurückgibt. Gleichzeitig kann hier, wie bei der if-Anweisung, eine Zuweisung des Funktionsergebnisses erfolgen. case Konstante: Konstanten, die mit dem Ausdruck verglichen werden.
135
Sprachkonzepte und Syntax ●
●
Anweisung: Anweisung wird ausgeführt, wenn die zugehörende Konstante und der switch-Ausdruck identisch sind. Wenn einmal eine Übereinstimmung gefunden wurde, werden alle Anweisungen (auch die zu anderen Konstanten gehörenden) ausgeführt, bis entweder das Schlüsselwort break die switch-Anweisung beendet oder der switch-Block beendet ist. default: Zu dem zu default gehörenden Anweisungsblock wird verzweigt, wenn die Überprüfung des switch-Ausdrucks mit den case-Marken keine Übereinstimmung ergibt.
Ausführung: Auswertung der switch-Anweisung ●
●
●
●
●
●
●
Die Konstanten bei den case-Marken werden in der Reihenfolge, in der sie im Programmcode stehen, mit dem switch-Ausdruck verglichen. Wenn an einer Stelle Ausdruck und Konstante identisch sind, wird zu der zugehörigen Anweisung verzweigt. Ergeben alle Vergleiche das Ergebnis false, wird, falls die default-Marke vorhanden ist, zu den Anweisungen hinter default verzweigt. Da die Konstanten nur als Einsprungmarken dienen, wird die Ausführung nach gefundener Übereinstimmung solange fortgesetzt, bis die switch-Anweisung zu Ende ist. Darum wird (es sei denn, dieses Durchfallen wird explizit gewünscht) jede zu einem case gehörende Anweisung mit dem Schlüsselwort break beendet. Trifft der Compiler auf das break, wird die switch-Anweisung sofort verlassen. Innerhalb einer switch-Anweisung dürfen keine zwei Konstanten den gleichen Wert haben. Sollte dies doch der Fall sein, erzeugt der Compiler eine Fehlermeldung. Zeichenkonstanten werden in switch-Anweisungen automatisch in ihren Integer-Wert umgewandelt. Innerhalb des switch-Blocks können keine Variablen definiert und initialisiert werden.
Beispiele #include <stdio.h> int main(){ float zahl1, zahl2; char Operator; printf("Ein kleiner Taschenrechner:\n"); printf("Geben Sie ein: Zahl Operator Zahl \n"); scanf("%f %c %f", &zahl1, &Operator, &zahl2); switch (Operator) { case '+':
136
Schleifen printf("= %f", zahl1 + break; case '-': printf("= %f", zahl1 – break; case 'X': case 'x': case '*': printf("= %f", zahl1 * break; case ':': case '/': printf("= %f", zahl1 / break; default: printf("Operator nicht } // end of switch
zahl2);
zahl2);
zahl2);
zahl2);
bekannt");
printf("\n\n"); return 0; }
Verweise Siehe if-Anweisung
Schleifen for, while
Beschreibung Schleifen werden eingesetzt, wenn ein Anweisungsblock mehrere Male hintereinander ausgeführt werden soll. Schleifen bestehen aus ●
●
einer Abbruchbedingung, die festlegt, wann die Ausführung der Schleife zu beenden ist, und einem in geschweifte Klammern eingefaßten Anweisungsblock.
Anwendung In C/C++ gibt es while, for und do-while-Schleifen. Prinzipiell aber läßt sich jede Schleife als while-Schleife formulieren. Unabhängig von der verwendeten Schleife kann diese auf mehrere Weisen verlassen werden: ●
Abbruchbedingung der Schleife wird erfüllt.
●
Abbruch durch break , return oder exit().
●
Sprung aus Schleife heraus durch goto oder longjmp().
137
Sprachkonzepte und Syntax
Warnung Es ist Aufgabe des Programmierers sicherzustellen, daß eine Schleife auch wieder verlassen wird, da das Programm sonst in einer Endlosschleife verharren kann.
Die for-Schleife Beschreibung Mit dem Schlüsselwort for wird eine Schleife eingeleitet, die durch einen dreiteiligen Schleifenkopf kontrolliert wird. Dieser Schleifenkopf steht in runden Klammern und enthält die drei Elemente Initialisierung, Bedingung und Veränderung, die durch Semikolon voneinander getrennt werden.
Anwendung Die for-Schleife wird immer dann eingesetzt, wenn die gewünschten Wiederholungen des Anweisungsblocks abzählbar sind (im Gegensatz zur while-Schleife, die auch eingesetzt werden kann, wenn die Anzahl der Wiederholungen nicht berechnet werden kann). In der Praxis sieht dies meist so aus, daß man eine eigene Variable definiert (die sogenannte Schleifenvariable), diese zu Beginn der Schleife initialisiert und dann bei jedem Durchgang der Schleife um einen bestimmten Betrag erhöht oder erniedrigt, bis sie einen bestimmten Wert übersteigt oder unterschreitet, und dadurch die Schleifenbedingung nicht mehr erfüllt ist. Die Syntax der for-Schleife ist speziell auf diese Formulierung einer Schleife abgestimmt.
Syntax for (Initialisierung; Bedingung; Reinitialisierung) Anweisung; ●
●
●
Initialisierung: Die im Initialisierungsteil stehenden Anweisungen werden ausgeführt, bevor die Schleife beginnt. Dort werden üblicherweise die Schleifen- oder Zählvariablen initialisiert. Es ist auch möglich, in dieser Anweisung mehrere Variablen zu initialisieren. Die Zuweisungen werden dann durch Kommata voneinander getrennt. Bedingung: Der Bedingungsteil enthält einen Ausdruck, der wahr sein muß, damit der zur for-Schleife gehörende Anweisungsteil ausgeführt wird. Reinitialisierung: Die hier stehenden Anweisungen werden nach jedem Durchlauf des zu for gehörenden Anweisungsblocks abgearbeitet. Hier wird meist die Zählervariable erhöht oder vermindert. Es kann dort jedoch auch jede andere Anweisung aufgenommen werden. Stehen hier mehrere Anweisungen, werden sie durch Kommata getrennt.
138
Die while-Schleife
●
Anweisung: Der Anweisungsteil kann aus einer einzelnen Anweisung oder aus einem Anweisungsblock bestehen. Eine einzelne Anweisung muß immer mit einem Semikolon (dem Anweisungsbegrenzer) beendet werden. Wenn ein Anweisungsblock ausgeführt werden soll, muß dieser in geschweifte Klammern eingeschlossen werden.
Warnung Eine Sonderform der for-Schleife stellt die Endlosschleife dar. for(;;)
Sie erhält man, wenn alle drei Elemente der Definition der for-Schleife wegfallen, und wenn nur die Semikolons, die die Elemente trennen, stehenbleiben. Endlosschleifen dieser Art können, da keine Abbruchbedingung vorhanden ist, nur mit break, return oder goto verlassen werden.
Beispiele #include <stdio.h> int main() { int zaehler; for(zaehler = 0; zaehler < 10; zaehler++) printf("Der Wert von zaehler beträgt: %d\n", zaehler); return 0; }
Verweise Siehe Abbruchbefehle Siehe while-Schleife
Die while-Schleife Beschreibung Die while-Schleife ist die allgemeinste Form der Schleife, mit der man einen Anweisungsblock in Abhängigkeit von einer Schleifenbedingung mehrfach ausführen lassen kann.
Anwendung Die while-Schleife gleicht in der Syntax der if-Bedingung, nur daß die Bedingung nicht kontrolliert, ob der Anweisungsblock überhaupt ausgeführt wird, sondern wie oft er ausgeführt wird.
139
Sprachkonzepte und Syntax
Ansonsten verfügen aber auch while-Schleifen – genau wir for-Schleifen – häufig über Schleifenvariablen, die in die Schleifenbedingung eingehen und innerhalb des Anweisungsblocks der Schleife verändert werden – nur, daß Initialisierung und Veränderung der Schleifenvariable nicht wie bei der for-Schleife in den Schleifenkopf integriert werden können. Die while-Schleife wird aber auch häufig eingesetzt, wenn der Schleifenabbruch nicht von einer Schleifenvariablen, sondern von anderen Ereignissen (z. B. Benutzereingaben) abhängt. Die Schleife wird dann üblicherweise mittels einer break-Anweisung verlassen.
Syntax while (Bedingung) Anweisung; ●
●
Bedingung: Die Bedingung, die ausgewertet wird, muß einen Booleschen Wert ergeben. Hier kann beispielsweise eine Ganzzahl, eine arithmetische Operation, ein Vergleich oder der Aufruf einer Funktion mit entsprechendem Rückgabetyp stehen. Anweisung: Das Element Anweisung kann eine einzelne Anweisung sein oder aus einem Anweisungsblock bestehen. Eine einzelne Anweisung muß immer mit einem Semikolon (dem Anweisungsbegrenzer) beendet werden. Wenn ein Anweisungsblock ausgeführt werden soll, muß dieser in geschweifte Klammern eingeschlossen werden.
Beispiele Die while-Schleife ist die allgemeinste Form der Schleife. So kann man beispielsweise die folgende for-Schleife for (i=1; i <= 5; i++) printf("%d ", i);
auch als while-Schleife formulieren: i = 1; while (i <= 5) { printf("%d ", i); i++; }
Ausführung: Auswertung der while-Schleife ●
●
Die Bedingung der while-Schleife wird getestet, bevor die zur Schleife gehörende Anweisung ausgeführt wird. Die Schleife endet, wenn die Bedingung nicht mehr den Wert true ergibt.
140
Die do-while-Schleife
Liefert die Auswertung der Bedingung der while-Schleife bereits beim Eintritt den Wert false, werden die zur Schleife gehörenden Anweisungen nie ausgeführt.
●
Verweise Siehe Abbruchbefehle Siehe for-Schleife
Die do-while-Schleife Beschreibung Neben der while-Schleife gibt es noch das Konstrukt der do-while-Schleife, die mit dem Schlüsselwort do eingeleitet wird und bei der die Schleifenbedingung am Ende des Anweisungsblocks steht.
Anwendung Der wichtigste Unterschied zur while-Schleife besteht darin, daß der Anweisungsteil der do-Schleife mindestens einmal ausgeführt wird, da die Bewertung des Ausdrucks, der die Bedingung zum Abbruch oder der Schleife enthält, immer nach dem Durchlaufen des Anweisungsteils stattfindet.
Syntax do Anweisung; while (Bedingung);
Beispiele #include <stdio.h> int main() { int zahl = 99; do { printf("Geben Sie eine Zahl ein. "); printf("99 um Programm zu beenden.\n"); scanf("%d", &zahl); printf("Die eingelesene Zahl "); printf("war: %d\n", zahl); } while (zahl != 99); return 0; }
141
Sprachkonzepte und Syntax
Verweise Siehe for-Schleife Siehe while-Schleife
Abbruchbefehle für Schleifen Beschreibung Mit einer Reihe von Befehlen kann man Schleifen bei Bedarf vorzeitig abbrechen.
Anwendung Grundsätzlich kontrolliert die Schleifenbedingung, wie oft der Anweisungsblock einer Schleife auszuführen ist. Darüber hinaus gibt es aber auch die Möglichkeit, einzelne Schleifendurchgänge gezielt zu überspringen oder eine Schleife vorzeitig abzubrechen. continue
Mit der continue Anweisung wird zum Ende des Schleifenblocks gesprungen. Die Ausführung der nach continue stehenden Anweisungen unterbleibt und der nächste Schleifendurchgang beginnt.
break
Das Schlüsselwort break wurde bereits zusammen mit der switch-Anweisung vorgestellt. Neben der Beendigung der switch-Verzweigung können Sie auch die Ausführung einer Schleife mittels break beenden. Im Gegensatz zu continue wird dabei nicht nur der aktuelle Schleifendurchgang abgebrochen, sondern die Schleife wird ganz verlassen.
Neben continue und break gibt es noch weitere Schlüsselwörter und Funktionen, die man zum Verlassen einer Schleife – oder allgemein eines Anweisungsblocks – verwenden kann. return
Mit dem Schlüsselwort return kann die Ausführung einer Funktion unterbrochen und sofort an der Stelle fortgesetzt werden, an der die Funktion aufgerufen wurde.
exit()
Mit der Funktion exit() kann ein Programm an beliebiger Stelle abgebrochen werden. Dies wird meist benutzt, wenn ein schwerwiegender Fehler aufgetreten ist, der den weiteren Programmablauf sinnlos oder unmöglich macht.
throw
Löst eine Exception aus und springt zum nächsten passenden ExceptionHandler (nur für die Fehlerbehandlung).
goto
Mit goto kann zu einem beliebigen anderen Punkt innerhalb der aktuellen Funktion verzweigt werden, der durch eine Sprungmarke gekennzeichnet wurde. Die Sprungmarke erhält einen Namen und wird, wie die Marken der switch-Anweisung, mit einem Doppelpunkt abgeschlossen.
longjmp()
Setzt das Programm in einen Zustand zurück, der zuvor durch einen Aufruf von setjmp() abgespeichert wurde.
142
Funktionen
Beispiele Überspringen einzelner Iterationen mit continue: #include <stdio.h> int main() { int zahl; do { printf("Geben Sie eine Zahl ein. "); printf("99 um Programm zu beenden.\n"); scanf("%d", &zahl); if (zahl < 0) continue; printf("Die eingelesene Zahl "); printf("war: %d\n", zahl); } while (zahl != 99); return 0; }
Vorzeitiges Abbrechen einer Schleife mit break: int main() { int zahl; for (zahl = 3; zahl < 15; zahl++) { if (zahl == 8) break; printf("zahl hat folgenden Wert: %d\n", zahl); } return 0; }
Verweise Siehe for-Schleife Siehe while-Schleife
Funktionen Funktionen Beschreibung C-Programme bestehen aus einer beliebigen Anzahl von Funktionen, deren Namen innerhalb der Konventionen von C (siehe Grundlagenteil) frei gewählt werden dürfen.
143
Sprachkonzepte und Syntax
Anwendung Sinn einer Funktionsdefinition ist es, einen Anweisungsblock, der eine bestimmte Aufgabe erledigt (beispielsweise die Ausgabe auf Konsole, die Berechnung des Sinus, etc.), mit einem Namen zu verbinden. Wird dann irgendwo im Programm die Funktion über ihren Namen aufgerufen, wird die Programmausführung mit der ersten Anweisung aus dem Anweisungsteil der Funktion fortgesetzt. Nach Abarbeitung der Funktion springt die Programmausführung in die Zeile des Funktionsaufrufs zurück. Die Aufteilung des Quelltextes in Funktionen ermöglicht es, redundanten Code zu vermeiden, häufig benötigte Funktionen zu sogenannten Bibliotheken zusammenzufassen und die Programme modularer und damit wartungsfreundlicher zu gestalten. Zum Datenaustausch mit der aufrufenden Funktion kann eine Funktion eine beliebige Anzahl Parameter und einen Rückgabwert definieren. Funktionen können an beliebiger Stelle im Quelltext definiert werden (nicht aber innerhalb anderer Funktionen). Bevor man eine Funktion aufrufen kann, muß sie aber dem Compiler bekannt sein. Dies erreicht man, indem man die Funktionsdefinition an den Anfang der Quelltextdatei setzt oder indem man die Funktion an beliebiger Stelle definiert und lediglich eine Vorwärtsdeklaration der Funktion an den Programmanfang setzt. Letzteres System liegt auch der Verwendung von Header-Dateien für Bibliotheken zugrunde.
Beispiele #include <stdio.h> /* Deklarationen für printf, scanf, fflush */ double fakul(int zahl); /* Vorwärtsdeklaration zu fakul() */ int main() { int zahl; double fakultaet; printf("Geben Sie die Zahl ein: "); scanf("%d",&zahl); fflush(stdin); fakultaet = fakul(zahl); printf("\nDie Fakultaet von %d ist %lf\n", zahl, fakultaet); return 0; } double fakul(int zahl) { int loop; double fakultaet = 1; for(loop=zahl; loop>=1; loop--) { fakultaet *= loop; } return fakultaet; }
144
Funktionsdeklaration und -definition
Funktionsdeklaration und -definition Beschreibung Mit der Funktionsdeklaration werden der Rückgabetyp, der Name und die Parameter festgelegt. Eine Funktionsdefinition enthält zusätzlich den Funktionskörper, der in geschweiften Klammern die Anweisungen umfaßt.
Anwendung Bei der Funktionsdefinition wird ein Anweisungsblock mit einem Funktionsnamen verbunden, so daß beim Aufruf des Funktionsnamens im Programmcode der Anweisungsblock der Funktion ausgeführt wird. Zudem wird durch Angabe der Parameter und des Rückgabetyps der Funktion festgelegt, wie die Funktion Daten beim Aufruf übernimmt und bei ihrer Rückkehr zurückliefert.
Syntax Typ func_name (PARAMETERLISTE) { FUNKTIONSKöRPER } ●
●
●
●
Typ: Mit der Typangabe wird festgelegt, welchen Rückgabewert die Funktion hat. Arrays und Funktionstypen sind als Rückgabetyp nicht zulässig, wohl aber Zeiger auf diese Objekte. func_name: Der Name der Funktion. Dieser Name muß in C, nicht aber in C++, eindeutig sein (siehe Überladung). PARAMETERLISTE: In einer Definition stehen hier durch Kommata getrennt die einzelnen Parameter mit ihren Typen. In einer Deklaration reicht die Angabe der Parametertypen, die der Compiler zur Typenüberprüfung und zur Auflösung überladener Namen braucht (C++). In C++ können Vorgabeargumente an die Parameter vergeben werden. FUNKTIONSKÖRPER: Der Funktionskörper ist ein Anweisungsblock, der in geschweifte Klammern eingeschlossen wird und bei Aufruf der Funktion ausgeführt wird.
145
Sprachkonzepte und Syntax
Warnung ●
●
●
● ●
●
●
●
●
●
●
Funktionen haben standardmäßig die Speicherklasse extern. Darüber hinaus kann man Funktionen auch als static deklarieren. Die standardmäßige extern-Deklaration von Funktionen bewirkt, daß die Funktionen externe Bindung haben (es sei denn, es gibt im Quelltext eine vorangehende static-Deklaration der Funktion). Alle extern-Deklarationen einer bestimmten Funktion beziehen sich daher über Dateigrenzen hinweg auf ein und dieselbe Definition der Funktion. Auf diese Weise ist es möglich, über eine Vorwärtsdeklaration in einer Quelltextdatei eine Verbindung zu einer Funktion herzustellen, die in einer anderen Quelltextdatei oder einer Bibliothek definiert ist . Deklariert man eine Funktion als static, hat die Funktion interne Bindung, d. h., der Name der Funktion ist nur innerhalb der eigenen Quelltextdatei bekannt. Auf diese Weise kann man in den verschiedenen Quelltextdateien eines Programmes gleichnamige Funktionen definieren (aber natürlich nur eine pro Datei), ohne daß es zu Linker-Fehlern wegen Mehrfachdefinitionen kommt. Funktionen, die keinen Wert zurückgeben sollen, erhalten die Typangabe void. Funktionen, für die kein Rückgabetyp angegeben wurde, bekommen in C automatisch den Rückgabetyp int zugewiesen (in C++ ist dies ein Fehler). Werden in einer Funktionsdeklaration, die nicht Teil einer Definition ist, keine Parameter spezifiziert, wird dies unter C als Funktion mit beliebig vielen Parametern interpretiert, in C++ als Funktion ohne Parameter. Durch Angabe des Schlüsselworts void statt einer Parameterliste kann in C eindeutig eine Funktion ohne Parameter deklariert werden. Neben dem überflüssigen Schlüsselwort auto ist register die einzige Speicherklassenangabe, die für Parameter vergeben werden kann. Dadurch wird beim Aufruf der Funktion, sofern möglich, das so deklarierte Argument in ein Prozessorregister geladen. Die Angabe ... am Ende der Parameterliste bedeutet, daß noch beliebig viele Argumente übergeben werden können. Funktionsname und die Parameterliste bilden zusammengenommen die Signatur der Funktion. Innerhalb des Funktionskörpers können eigene Variablen definiert werden, die, wenn nicht anders angegeben, die Speicherklasse auto besitzen und lokalen Gültigkeitsbereich haben. Diese lokalen Variablen werden beim Aufruf der Funktion auf dem Stack erzeugt und beim Verlassen der Funktion automatisch wieder entfernt. Soll der Wert einer lokalen Variablen auch nach dem Verlassen der Funktion erhalten bleiben, kann die Speicherklasse static benutzt werden. Meist wird dies benutzt, um die Werte lokaler Variablen von Funktionsaufruf zu Funktionsaufruf weiterzureichen.
146
Die Funktion main()
●
●
In C++ kann man Funktionen als inline deklarieren, um den Compiler anzuweisen, wenn möglich, die Funktionsaufrufe nicht durch einen Sprung in den Anweisungsteil der Funktion, sondern direkt durch Anweisungen, die mit der Ausführung der Funktion identisch sind, zu ersetzen. Dies ist allerdings nur eine Empfehlung an den Compiler. In C++ kann man mit Hilfe des Schlüsselwortes throw bei der Funktionsdeklaration eine Liste der Exceptions angeben, die in der Funktion ausgelöst werden dürfen. Treten in der Funktion Exceptions anderer Typen auf, wird eine unexpected-Exceptions ausgelöst.
Beispiele int main(); int func1(char* s, int n); void func2();
Vorgabeargumente in C++: int
func3(double par, char* s = "Vorgabe", int n = 2);
Überladene Funktionen in C++: void func(int n); int func(int n, int m); int func(float f);
Warnung Die alte auf Kernighan/Ritchie zurückgehende Form der Funktionsdefinition, bei der die Parameter unter dem Funktionsnamen definiert wurden, ist in C obsolet und in C++ nicht mehr möglich. // func_name(a,b) // int a, double b; // { ...
Verweise Siehe Klassen, Methoden
Die Funktion main() Beschreibung Der ANSI-Standard schreibt vor, daß eine der Funktionen in einem C/C++-Programm den Namen main() tragen muß. Diese Funktion ist die Eintrittsfunktion eines Programms, der nach dem Start des Programms automatisch die Kontrolle übergeben wird. Zusätzlich soll sie die Möglichkeit bieten, Kommandozeilenargumente zu übernehmen.
147
Sprachkonzepte und Syntax
Anwendung Der ANSI-Standard sieht folgende Definitionen vor: int main() { } int main(int argc, char *argv[]) {
}
Die meisten Compiler erlauben aber auch void als Rückgabewert: void main() { } void main(int argc, char *argv[]) { }
main() ohne Argumente
main()
Programme, die keine Argumente aus dem Programmaufruf übernehmen, definieren main() ohne Parameter. int main () { } void main ( ) { }
main() und ihre Parameter
main(int, char**)
Der ANSI-Standard sieht auch vor, daß der Funktion main() optional zwei Argumente übergeben werden können. Die Namen für diese Argumente sind frei wählbar, jedoch haben sich die Namen argc und argv eingebürgert. void main(int argc, char *argv[]) { ●
●
}
argc: (Abkürzung für »argument count«) ist der übliche Name für das erste Argument der main()-Funktion. Der Datentyp von argc ist int. Der Wert von argc entspricht der Anzahl der Zeichenketten in dem Array, auf das argv zeigt. Per Definition kann argc nie einen negativen Wert annehmen. argv: (Abkürzung für »argument vector«) ist der übliche Name für das zweite Argument der main()-Funktion. argv zeigt auf ein Array mit Zeigern vom Typ char. Die Zeichenketten werden vom Lademodul des Systems an das Programm übergeben.
Die Anzahl der Parameter wird durch das Argument argc festgelegt. Per Definition zeigt argv[0] (also der erste Zeiger) immer auf den Namen des Programms, während argv[argc] ein NULL-Zeiger ist. Die einzelnen Parameter befinden sich in argv[1] bis argv[argc-1].
Beispiele #include <stdio.h> #include <stdlib.h> int main( int argc, char *argv[]) { int i; printf( "\nBefehlszeilen-Argumente:\n" ); for( i = 0; i < argc; i++ ) printf( " argv[%d] %s\n", i, argv[i] );
148
Datenaustausch zwischen Funktionen return( EXIT_SUCCESS ); }
Nachstehend sehen Sie die Ausgabe des Programms, wenn der Name des Programms MAIN.EXE ist, und es mit folgender Befehlszeile aufgerufen wurde: C:\CC>main eins zwei drei Befehlszeilen-Argumente: argv[0] C:\CC\MAIN.EXE argv[1] eins argv[2] zwei argv[3] drei
Verweise Siehe Grundlagenteil, Aufbau eines C/C++-Programms
Datenaustausch zwischen Funktionen Beschreibung Prinzipiell besteht ein C-Quelltext nur aus Deklarationen und Definitionen. Anweisungen finden sich nur in Funktionsdefinitionen. Damit Funktionen zusammenarbeiten können, müssen sie Informationen, sprich Daten, austauschen. Dazu gibt es in C verschiedene Konzepte.
Anwendung Parameter
Parameter sind spezielle Variablen einer Funktion, denen beim Aufruf der Funktion Werte zugewiesen werden. Werden als Parameter Zeiger definiert, kann die Funktion auf diesem Weg auch Daten zurückliefern.
Rückgabewert
Ein einzelner Wert, den die Funktion an den Aufrufer zurückgibt.
Globale Variablen
Eine weitere Möglichkeit, Daten zwischen Funktionen auszutauschen, läuft über die globalen Variablen. Dieser Weg ist sehr einfach, hat aber den Nachteil, daß die Unabhängigkeit und Abgeschlossenheit der Funktion dadurch beeinträchtigt wird.
Statische Variablen Sollte es erforderlich sein, daß eine lokale Variable nicht nach dem Verlassen der Funktion gelöscht werden darf, damit ihr Wert beim nächsten Aufruf der Funktion noch erhalten ist, wird die Variable als static deklariert.
149
Sprachkonzepte und Syntax
Verweise Siehe nachfolgende Abschnitte
Der Rückgabewert Beschreibung Funktionen können das Ergebnis ihrer Berechnung über den Rückgabewert zurückliefern. Dazu wird der Typ des Rückgabewerts im Funktionskopf spezifiert. Mittels der Anweisung return (ergebnis);
wird der Wert dann an die aufrufende Funktion zurückgeliefert.
Anwendung Es gibt zwei Möglichkeiten, das Konzept des Rückgabewertes sinnvoll zu nutzen: ●
●
Das Ergebnis der Funktion wird einer Variablen der aufrufenden Funktion zugewiesen. Die Funktion meldet zurück, ob sie ihre Aufgabe erfolgreich gelöst hat, oder ob in der Funktion ein Fehler aufgetreten ist.
Warnung Funktionen, die keinen Wert zurückliefern, erhalten den Rückgabetyp void. Eine solche Funktion kann return nur ohne Wert aufrufen: return; .
Verweise Siehe Parameter Siehe Kategorie Programmsteuerung, Abbruchbefehle für Schleifen
Parameter Beschreibung Die Parameter dienen dazu, Werte von der aufrufenden Funktion an die aufgerufene Funktion zu übergeben. Parameter sind stets lokale Variablen, deren Gültigkeitsbereich der äußerste Block der Funktion ist. Zur Übergabe von Daten an Parameter benutzt C standardmäßig ein Verfahren, das man »call by value« nennt.
150
Parameter
Wert kopieren
call by value
Dies bedeutet, daß beim Funktionsaufruf der Wert des übergebenen Arguments in die lokale Variable kopiert wird. Dieses Konzept stellt sicher, daß als Argument übergebene Variablen nicht von den aufgerufenen Funktionen verändert werden können:
Beispiele #include <stdio.h> void change(int param) { printf("\tParam nach Funktionsaufruf: %d\n",param); param = param*param; printf("\tParam nach Berechnung: %d\n",param); } int main() { int var = 10; printf("var vor Funktionsaufruf: %d\n",var); change(var); printf("var nach Funktionsaufruf: %d\n",var); return 0; }
liefert folgende Ausgabe: var vor Funktionsaufruf: 10 Param nach Funktionsaufruf: 10 Param nach Berechnung: 100 var nach Funktionsaufruf: 10
Adresse kopieren
call by reference
Das Pendant heißt »call by reference«. Dabei wird nicht der Wert einer Argumentvariablen übergeben, sondern ihre Adresse! Da die Funktion in diesem Fall statt einer Kopie eine Referenz auf das Aufrufargument erhält, kann sie dieses verändern. Referenzparameter sind daher neben dem Rückgabewert ein weiteres Mittel, um Daten von der aufgerufenen Funktion an die aufrufende Funktion zurückzugeben. Für »call by reference«-Übergaben gibt es folgende Möglichkeiten: Parameter
Argument
Zeiger:
Typ *
ptr
typ *
&var
(Zeiger vom Typ typ*) (Adresse einer Variablen vom Typ typ)
Referenz, nur C++:
151
Sprachkonzepte und Syntax
Parameter
Argument
typ &
var
(Variable vom Typ typ)
Beispiele #include <stdio.h> void change1(int param) { param = param*param; } void change2(int *param) { *param = *param * *param; } void change3(int ¶m) { param = param*param; } int main() { int var = 10; change1(var); change2(&var); change3(var);
printf("var : %d\n",var); printf("var : %d\n",var); printf("var : %d\n",var);
return 0; }
liefert folgende Ausgabe: var : 10 var : 100 var : 10000
Warnung Gelegentlich werden Zeiger übergeben, ohne daß eine Änderung des übergebenen Wertes erwünscht wäre. Dies kann zum Beispiel bei der Übergabe von Arrays der Fall sein, die immer als Zeiger übergeben werden, oder wenn Zeiger auf größere Datenstrukturen übergeben werden, um das Kopieren zu sparen. In solchen Fällen kann durch den Spezifizierer const in der Parameterdeklaration die entsprechende Variable vor dem Überschreiben geschützt werden.
Tip In C++ werden statt Zeigern häufig Referenzen zur effizienten Übergabe von Argumenten an Funktionen benutzt.
Verweise Siehe Rückgabewert
152
Funktionsargumente und Vorgabe
Funktionsargumente und Vorgabe Beschreibung In C++ können in der Funktionsdeklaration Vorgabeargumente vergeben werden. Parametern, für die Vorgabeargumente deklariert wurden, brauchen beim Funktionsaufruf keine Argumente mehr übergeben zu werden.
Anwendung Auf diese Weise kann der Aufruf für häufig übergebene Argumente vereinfacht werden.
Warnung ●
●
●
●
Auf einen Parameter mit Vorgabeargument dürfen keine Parameter ohne Vorgabeargumente mehr folgen. Bei einer Redeklaration dürfen weitere Vorgabeargumente angegeben werden, es dürfen jedoch keine bereits deklarierten Vorgabeargumente redeklariert werden. Als Vorgabeargumente können auch Ausdrücke oder Variablen deklariert werden. Es dürfen jedoch keine lokalen Variablen verwendet werden. Die Reihenfolge, in der die Funktionsargumente ausgewertet werden, ist nicht festgelegt und kann somit von Compiler zu Compiler verschieden sein.
Beispiele int void char* char* int
func1(int n, int m = 3); func2(char *s = "Vorgabe"); func3(int x, int y=4); func3(int x = 2, int y); func4(int a = 0, int b);
// gueltige Redeklaration // Fehler
153
Sprachkonzepte und Syntax
Funktionen und der Stack Beschreibung Der Stack ist ein dynamisch wachsender und schrumpfender Speicherbereich, der vom Compiler verwaltet wird. Hier legt der Compiler alle Informationen ab, die er für die Protokollierung und Verwaltung der Funktionsaufrufe benötigt. Immer wenn Sie eine Funktion aufrufen, wird der Stack um einen neuen Datenblock erweitert. In diesem Datenblock werden die Parameter der Funktion, die lokalen Variablen der Funktion und einige interne Informationen (von denen die Rücksprungadresse zum Code der aufrufenden Funktion die wichtigste ist) abgelegt. Diesen Datenblock nennt man auch den Stack-Frame oder Stackrahmen der Funktion.
Stack-Aktionen ●
●
Solange eine Funktion abgearbeitet wird, bleibt ihr Stack-Frame auf dem Stack liegen. Werden aus einer Funktion heraus weitere Funktionen aufgerufen, werden deren Stack-Frames auf den Stack-Frame der jeweils zuvor aufgerufenen Funktion draufgepackt.
154
Funktionen und der Stack
● ●
Wird eine Funktion beendet, wird ihr Stack-Frame vom Stack entfernt. Der Stack ist also immer das genaue Abbild der zur Zeit noch in Ausführung befindlichen Funktionen. Die Stapelreihenfolge der Funktionen zeigt an, welche Funktion von welcher anderen Funktion aufgerufen wurde.
Stack-Aufbau Für einen von oben nach unten wachsenden Stack gilt: ●
●
● ●
Die unterste Funktion ist die aktuelle Funktion, deren Anweisungen gerade abgearbeitet werden. Die darüberliegende Funktion hat die unterste Funktion aufgerufen und wartet nun, daß die aufgerufene Funktion beendet wird. Die oberste Funktion ist die Eintrittsfunktion main(). Die einzelnen Stack-Frames von oben nach unten gelesen zeigen an, über welche Funktionen die aktuelle Funktion aufgerufen wurde.
Der genaue Aufbau der Stack-Frames hängt von dem Rechner ab, für den das Programm kompiliert wurde (die Abbildung ist also nur als eine Art allgemeines Modell zu verstehen). Sie können der Stack-Verwaltung Ihres Rechners aber ein wenig nachspüren, indem Sie sich der Debug-Tools Ihres Compilers bedienen (Stack-Anzeige) oder sich die Adressen der Parameter und lokalen Variablen ausgeben lassen.
Beispiele #include <stdio.h> #include <stdlib.h> func(int par1, int par2, int par3) { int lokal1, lokal2, lokal3; printf("%d. Aufruf von func\n",par1); printf("\t Adresse von par3 = %p\n",&par3); printf("\t Adresse von par2 = %p\n",&par2); printf("\t Adresse von par1 = %p\n",&par1); printf("\t Adresse von lokal1 = %p\n",&lokal1); printf("\t Adresse von lokal2 = %p\n",&lokal2); printf("\t Adresse von lokal3 = %p\n",&lokal3); if(par1<2) func(++par1,0,0); } int main() { func(1,0,0); return 0; }
155
Sprachkonzepte und Syntax
Ausgabe 1. Aufruf von func Adresse Adresse Adresse Adresse Adresse Adresse
von von von von von von
par3 par2 par1 lokal1 lokal2 lokal3
= = = = = =
0065FE00 0065FDFC 0065FDF8 0065FDEC 0065FDE8 0065FDE4
par3 par2 par1 lokal1 lokal2 lokal3
= = = = = =
0065FDE0 0065FDDC 0065FDD8 0065FDCC 0065FDC8 0065FDC4
2. Aufruf von func Adresse Adresse Adresse Adresse Adresse Adresse
von von von von von von
Erläuterung Die obige Ausgabe ist das Abbild einer sich selbst aufrufenden Funktion. Die Adressen der Parameter werden in umgekehrter Reihenfolge ausgegeben, weil der Compiler den letzten Parameter zuerst auf dem Stack ablegt. Wie man der Ausgabe entnehmen kann, wird der Stack im Speicher von oben nach unten aufgebaut, d. h., der Stack-Frame der Funktion, die als letztes aufgerufen wird, und der damit den obersten Stack-Frame bildet, liegt im Speicher an der niedrigsten Speicheradresse. Der Stack-Frame für den ersten Aufruf von func() beginnt an der Adresse 0065FE00, der Stack-Frame für den zweiten Aufruf an der Adresse 0065FDE0. Im Stackrahmen werden zuerst die drei int-Parameter abgelegt. Jeder int-Parameter belegt 4 Byte (wie für 32-Bit-Systeme üblich). Dann folgt ein Leerraum, in dem Daten abgelegt werden, auf die wir keinen direkten Zugriff haben (vermutlich die Rücksprungadresse und ein Zeiger auf den Beginn des vorangehenden Stack-Frames). Danach folgen die lokalen Variablen, die gemäß ihrem Datentyp wiederum jeweils 4 Byte belegen.
Warnung ●
●
Wenn in C/C++ eine Funktion aufgerufen wird, wird für die Funktion Speicherplatz reserviert. Die Einrichtung und Verwaltung des Speicherbereichs der Funktion kostet auch Laufzeit. Diesen Zeitverlust bezeichnet man als Function Overhead. In dem für die Funktion bereitgestellten Speicherbereich liegen die Speicherzellen der Funktionsparameter, der lokalen Variablen und der Rücksprungadresse, die sicherstellt, daß nach Abarbeitung der Funktion wieder an die richtige Stelle im Programm zurückgesprungen wird. Bei Beenden der Funktion gibt
156
Funktionen mit beliebig vielen Argumenten
C/C++ diesen Speicherbereich wieder frei, es entsteht also kein Speicherverlust. Es sei denn, Sie verwenden rekursive Funktionen. Ist die Rekursionstiefe sehr groß oder die lokalen Variablen der rekursiven Funktion sehr speicheraufwendig, kann es passieren, daß während der Rekursion der Speicher vollständig belegt wird, da für jeden Funktionsaufruf Speicherplatz reserviert werden muß. Das Programm stürzt dann mit einer entsprechenden Fehlermeldung ab. Dies passiert zwangsläufig, wenn ihre Rekursion keine Abbruchbedingung erreicht.
Verweise Siehe Kategorie Variablen und Datentypen, Gültigkeitsbereiche
Funktionen mit beliebig vielen Argumenten func(typ param, ...)
Beschreibung C/C++ erlaubt auch die Definition von Funktionen mit unbekannter Parameterzahl. Das kommt nicht gänzlich überraschend, denn schließlich erlauben auch einige gängige Bibliotheksfunktionen eine variable Anzahl von Argumenten: printf() und scanf().
Anwendung Die Implementierung von Funktionen mit beliebig vielen Argumenten beruht auf zwei Elementen: double mittel(int anzahl, ...) ●
●
Der Ellipse. Die Ellipse ... in der Parameterliste zeigt an, daß bei Aufruf noch eine beliebige Zahl von Argumenten folgen kann. Die Ellipse muß am Ende der Parameterdefinition stehen, und ihr muß ein ordentlicher Parameter vorausgehen, an dem man innerhalb der Funktion ablesen kann, wie viele Parameter folgen. Die va_Makros aus <stdarg.h>. Diese werden benötigt, um die Argumente zu übernehmen und auszuwerten.
Beispiele double mittel(int anzahl, ...) double summe = 0; va_list ap; int loop;
{
157
Sprachkonzepte und Syntax va_start(ap, anzahl); for(loop=1;loop<= anzahl;loop++) summe += va_arg(ap,int); va_end(ap); return summe/anzahl; }
Inline-Funktionen (nur C++) Beschreibung Inline-Funktionen sind Funktionen deren Aufrufe im Quelltext der Compiler möglichst nicht durch wirkliche Funktionsaufufe und das damit verbundene Anlegen eines Stack-Frames auflöst, sondern statt dessen den Anweisungsblock der Funktion so in den Quelltext einbaut, daß dessen Ausführung wirkt, als wäre die Funktion aufgerufen worden.
Anwendung Aufrufe von Funktionen bedingen eine zeitliche Verzögerung, den sogenannten Function Overhead. Man kann dies vermeiden, indem man Makros statt Funktionen einsetzt. Ein Makroaufruf wird vom Compiler beim Übersetzen durch die Makrodefinition ersetzt. Makros bringen jedoch einige Probleme mit sich: ●
Sie nehmen keine Typüberprüfung vor, und
●
Ihre Formulierung ist fehleranfälliger als die Definition von Funktionen.
C++ bietet daher die Alternative, Funktionen als inline zu deklarieren, indem das Schlüsselwort inline einfach an den Anfang des Funktionskopfes gestellt wird. Das Schlüsselwort inline weist den Compiler an, ähnlich wie bei einem Makro, die Funktionsaufrufe durch die Funktionskörper zu ersetzen. Der Nachteil gegenüber dem Gebrauch von Makros liegt darin, daß die inline-Direktive vom Compiler nur als Empfehlung angesehen wird, die er nicht beachten muß.
Warnung ●
●
Allgemein ist zum Gebrauch von Makros und Inline-Funktionen zu sagen, daß sie zwar die Laufzeit eines Programms beschleunigen, dafür aber den Code aufblähen. Sie eignen sich daher vor allem für kleinere Funktionen, die nicht allzu oft aufgerufen werden. Methoden von Klassen, die innerhalb der Klasse definiert werden, werden vom Compiler automatisch als Inline-Funktionen behandelt.
158
Spezifizierer für Funktionen
Die inline-Deklaration ist nur eine Empfehlung. Für umfangreichere oder komplexere Funktionen übergeht der Compiler üblicherweise die inline-Deklaration.
●
inline-Funktionen müssen in jeder Übersetzungseinheit des Programms iden-
●
tisch definiert werden (siehe Praxisteil, Elemente modulübergreifend nutzen).
Verweise Siehe Funktionen und der Stack
Spezifizierer für Funktionen Beschreibung Einsatzbereich und Arbeitsweise einer Funktion können durch eine Reihe von speziellen Schlüsselwörtern, die der Funktionsdeklaration voran- oder nachgestellt werden, manipuliert werden.
Spezifizierer für Funktionen Funktionen können mit einer der Speicherklassenangaben extern oder static versehen werden: static
Die Verwendung von static als Schlüsselwort vor der Typangabe einer Funktion verengt deren Sichtbarkeit auf die Datei, in der sie definiert ist.
extern
Die Verwendung von extern als Schlüsselwort vor der Typangabe einer Funktion macht sie in allen Dateien sichtbar, die zu dem Programm gehören (Funktionen sind per Voreinstellung extern).
-
Fehlt die Angabe, hat eine Funktion standardmäßig die Speicherklasse extern.
In C++, nicht in C, können Funktionen als inline deklariert werden. inline
Die inline-Deklaration ist eine Empfehlung an den Compiler, Funktionsaufrufe durch direkten, äquivalenten Code zu ersetzen.
159
Sprachkonzepte und Syntax
Spezielle Spezifizierer für Methoden static
static-Methoden sind klassenspezifisch, aber nicht instanzspezifisch.
const
const-Methoden können nur mutable-Instanzvariablen der Klasse verändern. Zudem können für const-Instanzen nur const-Methoden der Klasse aufgerufen werden. (Einige Compiler geben bei Zuwiderhandlung allerdings nur eine Warnung aus.) Das Schlüsselwort const wird hinter die Parameterliste gesetzt.
volatile
volatile-Methoden. Für volatile-Instanzen können nur volatile-Methoden der Klasse aufgerufen werden. Das Schlüsselwort volatile wird hinter die Parameterliste gesetzt.
virtual
virtual-Methoden werden bei der Vererbung eingesetzt und schalten die späte Bindung für eine Methode ein.
Methoden können nicht als extern deklariert werden. extern ist im Grunde ein Bezug auf eine globale Deklaration, die üblicherweise in externen Bindung resultiert. Dies ist für Methoden weder erlaubt noch sinnvoll.
Beispiele extern int func1(); static int func2(); inline int func3(); class demo { static int func4(); int func5() const; int func6() volatile; virtual int func7(); };
Verweise Siehe Funktionsdeklaration und -definition Siehe Inline-Funktionen Siehe Kategorie Klassen, Methoden
Überladung Beschreibung C++ erlaubt die Definition mehrerer Funktionen (oder Operatoren) gleichen Namens, sofern die Funktionen sich in Anzahl oder Typ der Parameter unterscheiden. Die Definition mehrerer Funktionen gleichen Namens bezeichnet man als Überladung.
160
Überladung
Realisierung und Auflösung Prinzipiell gilt für C++ ebenso wie für C, daß Objekte über einen eindeutigen Namen adressiert werden können. C++ lockert dieses Dogma für den Programmierer etwas auf, indem Funktionenbezeichner automatisch um codierte Informationen zu den Parametern respektive Operanden erweitert werden. Auf diese Weise kann der Programmierer identische Bezeichner vergeben, während der Compiler mit eindeutigen Namen arbeitet. Dies erklärt auch, warum beispielsweise Variablen oder Funktionen ohne Parameter nicht überladen werden können. Umgekehrt muß der Compiler in der Lage sein, beim Aufruf einer überladenen Funktion anhand der übergebenen Argumente die aufzurufende Funktion festzustellen. Man bezeichnet dies als die Auflösung einer Überladung. Zuerst sucht der Compiler dabei nach einer überladenen Funktion, die exakt in den Datentypen ihrer Parameter mit den Datentypen der Argumente aus dem Funktionsaufruf übereinstimmt.
● ●
Ein Typ T kann nicht von seiner Referenz T& unterschieden werden. Ein Typ T kann nicht von const T oder volatile T unterschieden werden, wohl aber T* von const T*.
Findet der Compiler keine exakte Übereinstimmung, sucht er die bestmögliche Übereinstimmung. ●
●
●
Zuerst wendet der Compiler die arithmetischen Promotionen auf die Argumente an. Dann versucht er es mit Standardkonvertierungen (double zu int, int zu double, Abgeleitet* zu Basis*, etc.), Am Schluß berücksichtigt er noch benutzerdefinierte Konvertierungen für Klassen.
Warnung Überladene Funktionen müssen in einem gemeinsamen Gültigkeitsbereich liegen, sonst findet keine Überladung, sondern die übliche Verdeckung statt. Der Rückgabetyp spielt bei der Überladung keine Rolle – überladene Funktionen können also nicht anhand des Rückgabetyps unterschieden werden. Findet der Compiler für ein Set von Argumenten zwei oder mehr überladene Versionen, die alle gleich gut zu den Argumenten passen, gibt er eine Fehlermeldung aus, daß er die Überladung nicht eindeutig auflösen kann. void func(char) {}; void func(short) {}; void call(char c, int i, short s)
161
Sprachkonzepte und Syntax { func(c); func(s); func(i); }
// ruft func(char) auf // ruft func(short) auf // Fehler: ambiguity
Das Problem liegt hier darin, daß es keine überladene Funktion für den Datentyp int gibt, daß aber sowohl char als auch short durch integrale Promotion in int verwandelt werden können.
Verweise Siehe Kategorie Variablen und Datentypen, Gültigkeitsbereiche Siehe Überladen, Überschreiben, Verdecken
Überladung von Funktionen Beschreibung C++ erlaubt die Definition mehrerer Funktionen (oder Operatoren) gleichen Namens, sofern die Funktionen sich in Anzahl oder Typ der Parameter unterscheiden. Die Definition mehrerer Funktionen gleichen Namens bezeichnet man als Überladung.
Anwendung Die Überladung von Funktionen wird eingesetzt, um das Verhalten einer Routine an die ihr übergebenen Parametertypen anzupassen. Der Vorteil für den Programmierer besteht darin, daß er nicht mehrere Funktionsnamen für eine Routine zu vergeben braucht, und der Compiler die Aufgabe übernimmt, die Parameter zu checken und die richtige Version der Routine aufzurufen.
Warnung Überladene Funktionen müssen in einem gemeinsamen Gültigkeitsbereich liegen, sonst findet keine Überladung, sondern die übliche Verdeckung statt.
Beispiele int max(int a, int b) { return ( a > b ? a : b); } const char* max(const char* s1, const char* s2) { return (strcmp(s1,s2) ? s1 : s2); } int max(int a, char* s) { return ( a > strlen(s) ? a : strlen(s)); }
162
Überladung von Methoden
Überladung von Methoden Beschreibung Methoden werden ebenso überladen, wie oben für Funktionen im Dateibereich ausgeführt wurde. Besonderheiten ergeben sich jedoch für die Überladung ererbter Methoden, da Basisklasse und abgeleitete Klasse unterschiedliche Klassenbereiche darstellen.
Warnung ● ●
●
Ererbte Funktionen können in abgeleiteten Klassen nicht überladen werden. In der abgeleiteten Klasse deklarierte Funktionen verdecken gleichnamige Funktionen der Basisklassen, die weiterhin über den Bereichsoperator aufrufbar sind. Soll eine Überladung simuliert werden, müssen die Versionen der Basisklasse zuerst überschrieben werden.
Beispiele class basis { public: void func(char * s); void func(char c); }; class abgeleitet : public basis { public: void func(int wert); } int main() { class abgeleitet abg; abg.func("Test"); // Fehler abg.func('c'); // Fehler abg.func(3.2); // wird als 3 interpretiert abg.basis::func("Test"); return 0; }
Überladen, Überschreiben, Verdecken Überladene Funktionen ●
befinden sich im gleichen Gültigkeitsbereich.
●
sind anhand ihrer Parameter unterscheidbar.
●
dienen zur Anpassung einer Funktion an verschiedene Parametertypen.
163
Sprachkonzepte und Syntax
Überschriebene Methoden ●
liegen in verschiedenen Gültigkeitsbereichen, wobei die überschriebene Methode in einer Basisklasse und die überschreibende Methode in einer von der Basisklasse abgeleiteten Klasse definiert ist.
●
haben identische Namen und Parametertypen.
●
werden üblicherweise als virtual deklariert.
●
dienen der Implementierung polymorphen Verhaltens in Klassenhierarchien.
●
sind über den Gültigkeitsbereichoperator weiterhin erreichbar.
Verdeckte Methoden ●
liegen in verschiedenen Gültigkeitsbereichen, wobei die überschriebene Methode in einer Basisklasse und die überschreibende Methode in einer von der Basisklasse abgeleiteten Klasse definiert ist.
●
haben nicht-identische Namen und Parametertypen.
●
sind über den Gültigkeitsbereichoperator weiterhin erreichbar.
Klassen Klassen Beschreibung C++ basiert auf der Sprache C. Die vollständige Aufwärtskompatibilität garantiert, daß C++ die Syntax von C vollkommen einschließt. Objektorientiertes Programmieren ist daher nicht gleichzusetzen mit Programmieren in C++, sondern beginnt erst mit der Verwendung der objektorientierten Konzepte, um die C++ den C-Standard erweitert. Die wichtigsten dieser Konzepte (Kapselung, Vererbung, Polymorphie, Templates) gehen zurück auf den neuen Datentyp class.
Anwendung Klassen dienen dazu, Datenelemente und Elementfunktionen (Methoden) in einem Datentyp zu kapseln. Sie gestatten es, verschiedene Zugriffsberechtigungen für die einzelnen Elemente zu vergeben, und können durch Vererbung Klassenhierarchien aufbauen.
164
Klassen
Syntax klassentyp klassenname { ELEMENTLISTE } instanzen; ●
● ●
[: BASISKLASSEN]
klassentyp: Als Klassentyp können neben dem Schlüsselwort class auch struct und union verwendet werden. Die Verwendung der Schlüsselwörter struct und union beruht darauf, daß in C++ Strukturen und Unions spezielle Klassen darstellen. Dies erleichtert die Verwendung von C-Modulen in C++-Programmen, sollte den Programmierer aber nicht dazu verführen, beide Konzepte zu vermischen. Es ist empfehlenswert, Klassen stets als class zu deklarieren und Unions und Strukturen nur gemäß der C-Konvention zu verwenden. klassenname: Name der zu deklarierenden Klasse. :BASISKLASSEN: Durch Kommata getrennte Liste von Klassen, deren Elemente übernommen (geerbt) werden. Der Zugriff der abgeleiteten Klasse auf die ererbten Elemente kann durch die Spezifizierer public, protected und private modifiziert werden. Ein Eintrag in dieser Liste hat folgende Syntax: [virtual] [public, protected, private] basisklassenname
●
●
ELEMENTLISTE: In der Elementliste werden Datenelemente und Methoden der Klasse deklariert und unter Umständen auch schon definiert. Der Zugriff auf die Elemente kann durch die Spezifizierer public, protected und private geregelt werden. Alle Elemente haben ihre Klasse als Gültigkeitsbereich. instanzen: Hinter der abschließenden Klammer können durch Kommata getrennt bereits Instanzen, also Variablen der Klassentyps, eingerichtet werden.
Warnung ●
●
Eine Basisklasse muß deklariert sein, bevor sie zur Vererbung herangezogen werden kann! Ein Zugriffsspezifizierer gilt für alle folgenden Klassenelemente bis zum Auftreten eines neuen Zugriffsspezifizierers.
●
Zugriffsspezifizierer können nicht kombiniert werden.
●
Zugriffsspezifizierer dürfen in der Klassendeklaration mehrfach auftauchen.
●
Klassenelemente am Anfang der Deklaration, für die kein Zugriffsspezifizierer angegeben wurde, gelten automatisch als private.
Beispiele class beispiel { int wert; public: beispiel() {wert = 1;}; protected:
// private // Konstruktor
165
Sprachkonzepte und Syntax int gebe_wert_aus() {return wert;}; private: int loesche_wert(); };
Verweise Siehe Variablen und Datentypen, Strukturen Siehe Variablen und Datentypen, Unions Siehe Vererbung und Polymorphie
Klassen und OOP Beschreibung Das Konzept, welches hinter den Klassen und allgemein der objektorientierten Programmierung steht, beruht auf der alltäglichen Erfahrung, daß wir die Objekte der realen Welt nach zwei Maßstäben beurteilen. Einmal nach Merkmalen wie Form und Farbe, zum anderen nach bestimmten »Verhaltensweisen«, die ein Objekt aufweist, und die beispielsweise festlegen, wie man mit ihm umzugehen hat. In der objektorientierten Terminologie entsprechen die Merkmale den Datenelementen und die Verhaltensweisen den Methoden (Elementfunktionen).
Beispiel Objekte, die gleiche Merkmale und Verhaltensweisen aufweisen, können in einer Klasse zusammengefaßt werden. So könnte eine Klasse videorecorder folgendermaßen aussehen: class videorecorder { /* Merkmale */ char *hersteller; int anzahl_videokoepfe; boolean zeitlupe, longplay, zweikanalton; public: /* Verhaltensweisen */ void anschalten(); void abspielen(); void aufnahme(); void in_zeitlupe_abspielen(); void ausschalten(); };
Bei der Bildung einer Variablen der Klasse – im folgenden als Instanzbildung bezeichnet – werden den Merkmalen Werte zugewiesen. Auf diese Weise entsteht durch Spezialisierung eine Instanz, die nur noch einen besonderen Videorecorder repräsentiert. (Ein weiteres Beispiel wäre die Klasse Mensch und ihre Instanzen, nämlich jeder einzelne von uns – die Individuen. Eine Geburt wäre folglich eine Instanzbildung.)
166
Kapselung
Verweise Siehe Kapselung Siehe Instanzbildung
Kapselung Beschreibung Die Zusammenfassung von Datenelementen und Methoden wird als Kapselung bezeichnet. Zudem verbinden sich mit dem Begriff noch zwei weitere Designkriterien: Information hiding und Abgeschlossenheit
Information hiding Klassen sollten vornehmlich über ihre Methoden angesprochen werden. Diese Funktionen bilden die Schnittstelle zwischen Klasse und Programm. (Die genaue Festlegung der Schnittstelle erfolgt über die Vergabe von Zugriffsspezifizierern.) Nach der Implementierung der Klasse ist es für den Programmierer wichtig, daß er die Klasse nur noch über ihre Schnittstelle einzusetzen braucht, ohne sich weitere Gedanken um deren Implementierung machen zu müssen. Für die fehlerlose Bearbeitung der Funktionsaufrufe und das korrekte Verhalten der Klasse, trägt diese selbst Sorge. Wurde zum Beispiel eine Klasse videorecorder implementiert, um Videorecorder über den Computer anzusteuern, braucht der Programmierer nur noch eine Instanz der Klasse zu bilden und kann danach die Instanz einsetzen wie einen realen Videorecorder: my_recorder.anschalten(); my_recorder.in_zeitlupe_abspielen(); my_recorder.ausschalten();
Die Ansteuerung des Videorecorders über die parallele Schnittstelle oder die Kontrolle, ob der Videorecorder überhaupt über Zeitlupe verfügt, nimmt die Klasse selbst vor, d. h.,sie versteckt solche Details vor dem Programmierer und entlastet ihn dadurch.
Abgeschlossenheit Aus der Forderung des Information hidings ergibt sich das Kriterium der Abgeschlossenheit. Die Schnittstelle einer Klasse sollte möglichst vollständig sein, in dem Sinne, daß die Klasse auch sinnvoll eingesetzt werden kann. Eine Klasse videorecorder, die über keine Funktion in_zeitlupe_abspielen() verfügt, wäre von vornherein zur Bedienung von Videorecordern mit Zeitlupe ungeeignet.
167
Sprachkonzepte und Syntax
Umgekehrt sollten in einer Klasse nur Daten und Funktionen enthalten sein, die die Objekte der Klasse auch wirklich kennzeichnen. Eine Methode berechne_bruch() würde in der Klasse videorecorder keinen Sinn machen.
Verweise Siehe Praxisteil, Kategorie Klassen
Datenelemente Beschreibung Variablen, die innerhalb einer Klasse definiert werden, stellen die Datenelemente der Klasse dar.
Anwendung Wird eine Instanz von der Klasse gebildet, wird der Instanz Speicher für die Datenelemente zur Verfügung gestellt. Jede Instanz hat also ihre eigene Kopie der Datenelemente, die man daher auch Instanzvariablen nennt. Gleichzeitig wird die Instanzbildung dazu genutzt, die Datenelemente zu initialisieren. Diese Aufgabe übernimmt der Konstruktor. Klassen stellen einen eigenen Gültigkeitsbereich dar, d. h., die Instanzvariablen verfallen mit ihrer Instanz (wofür der Destruktor Sorge trägt), und Namen aus anderen Gültigkeitsbereichen werden verdeckt. Neben der Deklaration von Datenelementen bekannter Typen ist es auch möglich, innerhalb einer Klasse eine lokale Typdefinition vorzunehmen.
Warnung Datenelemente ● ●
●
können als public, protected oder private deklariert werden. werden bei der Instanzbildung eingerichtet und zusammen mit der Instanz aufgelöst. können als static, const, volatile oder mutable deklariert werden.
Beispiele class demo { int anzahl; int *feld; public: char *str; ...
168
// private Daten
// public Daten
Statische Datenelemente
Verweise Siehe Konstruktor und Destruktor Siehe Zugriffsspezifizierer
Statische Datenelemente Beschreibung Statische Datenelemente werden bei der Instanzbildung nicht kopiert. Statt dessen greifen alle Instanzen auf die gleiche Variable zu.
Anwendung Statische Elemente müssen außerhalb der Klasse, im umgebenden namespace (oder Dateibereich), definiert werden und können nicht nur über Instanzen, sondern auch über den Klassennamen angesprochen werden:
Warnung Statische Elemente können nicht als auto, register oder extern deklariert werden.
Beispiele class Demo { public: static int instanzenzaehler; Demo() {instanzenzaehler++;} ~Demo() {instanzenzaehler--;} }; int Demo::instanzenzaehler = 0; int main() { Demo lok; cout << Demo::instanzenzaehler << endl; cout << lok.instanzenzaehler << endl; return 0; }
Verweise Siehe Statische Methoden
169
Sprachkonzepte und Syntax
Konstante Datenelemente Beschreibung Datenelemente, die als const deklariert werden, können nach der Instanzbildung nicht mehr verändert werden.
Warnung const-Datenelemente müssen in der Konstruktorliste initialisiert werden.
Beispiele class Demo { public: const int j; Demo() : j(2) { ... };
// konstante Instanzvariable // Initialisierung
Verweise Siehe Konstante Methoden Siehe Kategorie Konstanten
Klasseninstanzen als Datenelemente Beschreibung Instanzvariablen können auch vom Datentyp einer Klasse sein, man spricht dann auch von eingebetteten Objekten.
Anwendung Eingebettete Objekte werden im Zuge der Instanzbildung des ihnen übergeordneten Objekts erzeugt und mit diesem aufgelöst. Zur Einrichtung des eingebetteten Objekts muß ein Konstruktor des eingebetteten Objekts aufgerufen werden. Dies kann ein Standardkonstruktor sein, der Programmierer muß sich dann nicht um die Einrichtung kümmern. Es kann auch ein Konstruktor mit Argumenten sein, der Programmierer muß dann den Konstruktor des übergordneten Objekts um eine Konstruktorliste erweitern, in der der Konstruktor des eingebetteten Objekts aufgerufen wird. (Beachten Sie, daß der Konstruktor über den Objektnamen und nicht wie bei Basisklassen über den Klassennamen aufgerufen wird.) Zur Auflösung des eingebetteten Objekts wird sein Destruktor aufgerufen.
170
Klasseninstanzen als Datenelemente
Warnung ●
●
●
Eine Klasse kann keine Instanzen von Klassen als Element enthalten, wenn diese Klassen nicht zuvor definiert wurden. Da die Definition einer Klasse mit dem Semikolon abschließt, folgt daraus, daß eine Klasse keine Instanz ihres eigenen Typs als Element enthalten kann. Wohl aber ist es möglich, Zeiger oder Referenzen auf nicht definierte Klassen zu definieren, sofern der Klassenname bekannt ist. Die umliegende Klasse hat keine besonderen Zugriffsrechte auf die Elemente des eingebetteten Objekts.
Beispiele Zeiger auf Objekte von noch nicht definierten Klassen: class demo; class beispiel { int a; class beispiel falsch;
// kann von Compiler nicht // aufgelöst werden
class beispiel* richtig; class demo& auch_richtig; };
Instanziierung eingebetteter Objekte durch den Standardkonstruktor: class A { public: int i; A() {i = 1;} // Standardkonstruktor }; class B { public: int n; A obj; B() {n = 2;} };
Instanziierung eingebetteter Objekte über Konstruktorliste: class A { public: int i; A(int i_par) {i = i_par;} }; class B { public: int n; A obj; B(int p1, int p2) : obj(p1) {n = p2;} };
171
Sprachkonzepte und Syntax
Verweise Siehe Lokale und verschachtelte Klassen Siehe Konstruktor Siehe Vererbung und Polymorphie, Vererbung versus Einbettung
Lokale und verschachtelte Klassen Beschreibung Klassen, die im Anweisungsteil einer Funktion definiert werden, werden als lokale Klassen bezeichnet. Klassen, die im Definitionsteil einer Klasse definiert werden, werden als verschachtelte oder eingeschlossene Klassen bezeichnet.
Warnung ●
● ●
Eine eingeschlossene Klasse kann Typen und statische Elemente der einschließenden Klasse direkt benutzen. Eingeschlossene Klassen dürfen keine statischen Elemente enthalten. Die eingeschlossene Klasse hat keine besonderen Zugriffsrechte auf die Elemente der einschließenden Klasse, d. h., sie kann nur über Instanzen der eingeschlossenen Klasse auf deren Elemente zugreifen (Ebenso hat auch die einschließende Klasse keine besonderen Zugriffsrechte auf die eingeschlossene Klasse).
Beispiele int i; class global { static int n; public: int i; static float f; class intern { void func(global i = 3; f = 3; ::i = 3; glob.i = 3; n = 3; } ...
172
&glob) { // Fehler // korrekt // i aus Dateibereich // i aus Klasse global // Fehler
Methoden
Verweise Siehe Klasseninstanzen als Datenelemente
Methoden Beschreibung Funktionen, die als Elemente einer Klasse definiert werden, bezeichnet man als Methoden.
Anwendung Methoden können sowohl innerhalb als auch außerhalb der Klassendeklaration definiert werden. ●
●
Methoden, die innerhalb der Klassendefinition definiert werden, werden automatisch als inline interpretiert. Auch wenn das Schlüsselwort inline nur als Empfehlung für den Compiler gedacht ist, ergibt sich daraus, daß innerhalb einer Klassendefinition nur kurze Funktionen definiert werden sollten. Zum einen, um Speicherplatz zu sparen, zum anderen, um die Klassendefinition übersichtlich zu halten. Bei der Definition außerhalb der Klassendefinition muß der Klassenname mit dem Bereichsoperator vorangestellt werden.
Definition innerhalb der Klassendefinition: class Demo { int wert; public: Demo() {wert = 1;} int gebe_wert_aus() {return wert;} };
Definition außerhalb der Klassendefinition: class Demo { int wert; public: Demo(); int func(); }; inline Demo::Demo() int Demo::func() { ... }
{wert = 1;}
173
Sprachkonzepte und Syntax
Warnung Methoden ●
können als public, protected oder private deklariert werden.
●
haben Zugriff auf alle Elemente der Klasse.
●
stellen neben Friends die einzige Möglichkeit dar, auf private Elemente ihrer Klasse zuzugreifen.
●
können als virtual deklariert werden.
●
können als inline deklariert werden.
●
●
●
können als const deklariert werden. Methoden, die als const deklariert sind, sind die einzigen Methoden, die von const-deklarierten Instanzen aufgerufen werden können. (Das Schlüsselwort const wird der Parameterliste nachgestellt). können als volatile deklariert werden. Methoden, die als volatile deklariert sind, sind die einzigen Methoden, die von volatile-deklarierten Instanzen aufgerufen werden können. (Das Schlüsselwort volatile wird der Parameterliste nachgestellt). können nur von Methoden der gleichen Klasse überladen werden.
Verweise Siehe Überladung Siehe Kategorie Vererbung und Polymorphie, Überschreibung und virtuelle Methoden
Statische Methoden Beschreibung Statische Methoden sind Methoden, die mit dem Schlüsselwort static deklariert werden. Statische Methoden sind ebenso wie statische Datenelemente klassen- und nicht instanzspezifisch. Statische Methoden besitzen keinen this-Zeiger und können daher nur andere statische Elemente (Daten wie Methoden) aufrufen.
Anwendung Statische Methoden können über Instanzen oder den Klassennamen aufgerufen werden.
Warnung Statische Methoden können nicht als virtual deklariert werden.
174
Konstante Methoden
Beispiele class Demo { private: static int standardwert; public: static void std_setzen(int i) { if( i > 0 && i < 100 ) standardwert = i; } }; int Demo::standardwert = 1; int main() { Demo obj1, obj2; Demo::std_setzen(33); return 0; }
Verweise Siehe Statische Datenelemente
Konstante Methoden Beschreibung Konstante Methoden sind Methoden, die mit dem Schlüsselwort const deklariert werden. Methoden, die als const deklariert werden, können Elementdaten nicht ändern (es sei denn, diese sind explizit als mutable deklariert) und können Zeiger oder Referenzen auf Elementdaten nur als const zurückgeben.
Anwendung const-Methoden sind die einzigen Methoden, die von const-deklarierten Instanzen
aufgerufen werden können. (Manche Compiler geben bei Zuwiderhandlung allerdings nur eine Warnung aus). Das Schlüsselwort const wird der Parameterliste nachgestellt.
Beispiele class Demo { public: int i; const int j; mutable int n; Demo() : j(2) {} // Initialisierung void inkr() const { i++; // Fehler
175
Sprachkonzepte und Syntax j++; n++; } void func() {...}
// Fehler // ok
};
Konstante Klasseninstanzen: const demo obj2; obj2.func(); obj2.inkr();
// Fehler // o.k.
Verweise Siehe Konstante Datenelemente
Zugriffsspezifizierer Beschreibung Die Zugriffsspezifizierer public, private und protected regeln, wie von innerhalb und außerhalb der Klasse auf deren Elemente zugegriffen werden kann.
Anwendung Die Zugriffsspezifizierer sind daher ein wichtiges Instrument der Kapselung. Erst durch diese Spezifizierer kann eine Klasse vor Mißbrauch geschützt werden. Die Zugriffsspezifizierer werden sowohl bei der Klassendefinition wie auch bei der Vererbung eingesetzt.
Zugriffsspezifizierer bei der Klassendefinition Innerhalb der Klassendefinition kann man die Zugriffsspezifizierer verwenden, um nachfolgend definierten Klassenelementen einer Zugriffsebene zuzuordnen. class demo { ... public: ... protected: ... private: ... };
176
Zugriffsspezifizierer
Spezifizierer
Bedeutung
public
Das Element kann von jeder Methode der eigenen und abgeleiteten Klassen benutzt werden. Der Zugriff von außen erfolgt über den Namen der Instanz und einem der Zugriffsoperatoren:
instanz_name.elementname referenz_der_instanz.elementname zeiger_auf_instanz->element_name private
Das Element kann nur von Methoden und Friends der Klasse, in der es deklariert wurde, benutzt werden.
protected
Es gilt das gleiche wie für private. Zusätzlich ist jedoch die Benutzung durch Methoden von Klassen, die von der deklarierten Klasse abgeleitet sind, möglich. Gibt es keine abgeleiteten Klassen, sind private und protected identisch.
Zugriffsspezifizierer bei der Vererbung Im Zuge der Vererbung kann man die Zugriffsspezifizierer verwenden, um den Zugriff über Instanzen der abgeleiteten Klasse auf die geerbten Elemente zu verschärfen. class abgeleitet : public basisklasse { ... Spezifizierer
Bedeutung
public
Für die geerbten Elemente gelten die gleichen Zugriffsrechte, die in der Basisklasse festgelegt wurden.
private
Alle geerbten Elemente sind von außen als private anzusehen.
protected
public-Elemente werden als protected-Elemente angesehen.
Warnung ●
Ein Zugriffsspezifizierer gilt für alle folgenden Klassenelemente bis zum Auftreten eines neuen Zugriffsspezifizierers.
●
Zugriffsspezifizierer können nicht kombiniert werden.
●
Zugriffsspezifizierer dürfen in der Klassendeklaration mehrfach auftauchen.
●
●
Klassenelemente am Anfang der Deklaration, für die kein Zugriffsspezifizierer angegeben wurde, gelten automatisch als private. Zu den Zugriffsspezifizierern im weiteren Sinne zählt auch das Schlüsselwort friend (siehe Zugriff von außerhalb der Klasse).
177
Sprachkonzepte und Syntax
Zugriff auf Klassenelemente Prinzipiell werden Klassen so ausgelegt, daß sie ihre Datenelemente durch die Deklaration als private verstecken und Manipulationen nur über eine Auswahl bestimmter als public deklarierter Methoden ermöglichen. In der Praxis gibt es durch die Verwendung der Zugriffsspezifizierer bei der Klassendeklaration sowie bei der Vererbung und des Zugriffsspezifizierers friend eine Vielzahl von Situationen mit unterschiedlichen Zugriffsberechtigungen. Um den Überblick zu behalten, werden diese Situationen in den folgenden Abschnitten zusammengefaßt.
Verweise Siehe nachfolgende Abschnitte Siehe Vererbung und Polymorphie, Zugriffsbeschränkung bei der Vererbung
Zugriff innerhalb der Klasse el_name
Beschreibung Zugriff innerhalb einer Klasse bedeutet, daß innerhalb des Funktionskörpers von Methoden auf andere Elemente (Daten oder Funktionen) der eigenen Klasse zugegriffen wird. Dies ist der innerste Bereich, der praktisch keinen Beschränkungen unterliegt. Der Zugang zu den Elementen erfolgt direkt über deren Namen.
Warnung ●
●
●
Methoden, die als const deklariert sind, können auf alle Datenelemente zugreifen, aber nur mutable-Datenelemente verändern. Statische Methoden können nur statische Elementdaten direkt ansprechen. (Dies liegt daran, daß statische Methoden keinen this-Zeiger besitzen.) Nichtstatische Elemente müssen über eine existierende Instanz (die beispielsweise als Argument übergeben werden kann) angesprochen werden. Für geerbte Elemente aus Basisklassen gelten die Zugriffsrechte, die in der Basisklasse festgelegt wurden.
Beispiele class demo { private: int wert1, wert2; int func1() {return wert1*wert2;}
178
Zugriff von außerhalb der Klasse public: ... void func2(int i) { wert1=i+1; wert2 = func1(); } };
Verweise Siehe nachfolgende Abschnitte Siehe this-Zeiger
Zugriff von außerhalb der Klasse objekt.el_name
Beschreibung Von außerhalb kann auf Elemente der Klasse T nur über eine Instanz der Klasse T in Verbindung mit einem der Operatoren . -> .* ->* oder über Friends der Klasse zugegriffen werden.
Zugriff über Instanz
.,->
Der Zugriff unterliegt den Einstellungen durch die Modifizierer public, protected und private, d. h. Elemente, die in der Klasse als protected oder private deklariert sind (dies gilt auch für ererbte Elemente), können auf diese Weise nicht angesprochen werden. class demo { int wert1; // standardmäßig private int func1(); public; int wert2; int func2(); }; int main() { class demo d, *ptr_d; int dummy; dummy = d.wert1; // Fehler dummy = ptr_d->func1(); // Fehler dummy = d.wert2; dummy = ptr_d->func2(); }
179
Sprachkonzepte und Syntax
Zugriff über Zeiger auf Klassenelemente
.*,->*
Auch hier gilt, daß nur auf public-Elemente zugegriffen werden kann. Wie die Syntax zur Definition, Initialisierung und Dereferenzierung von Zeigern auf Klassenelemente aussieht, entnehmen Sie bitte dem Abschnitt Operatoren, Datenzugriff.
Zugriff über Friends
friend
Manchmal ist es sinnvoll, die strengen Regeln der Zugriffsberechtigungen durch public, protected und private zu umgehen und einer beliebigen Funktion (also nicht bloß Klassenfunktionen) Zugriff auf protected oder private Elemente zu erteilen. Das Schlüsselwort friend ermöglicht dies. Um eine Friend-Funktion einzurichten, bedarf es zweierlei: ●
Erstens muß die Funktion in der Klasse, auf die sie Zugriff haben soll, als friend deklariert werden, und
●
Zweitens muß eine Referenz oder Instanz der Klasse an die Funktion übergeben werden.
#include using namespaces std; class demo { int wert; friend int func(demo&); // Friend-Deklaration }; // Friend-Funktion int func(demo& objekt) { return objekt.wert; } int main() { class demo d; cout << func(d); return 0; }
Warnung ●
●
Beachten Sie die Feinheit, daß die Deklaration als friend in der Klasse vorgenommen werden muß. Die Klasse bestimmt also selbst, welche Funktionen friend sind und welche nicht, und kann ihre Daten immer noch vor unkontrolliertem Gebrauch schützen. Private Elemente, die über keine public-Methoden verfügen, können über friend-Funktionen erreicht werden.
180
Zugriff auf Basisklassenelemente
●
Neben der Deklaration einzelner Funktionen als friend können Sie auch sämtliche Funktionen einer Klasse auf einen Schlag als friend deklarieren, indem Sie die ganze Klasse als friend deklarieren.
Verweise Siehe Kategorie Operatoren, Datenzugriff
Zugriff auf Basisklassenelemente Beschreibung Für abgeleitete Klassen muß man zwischen den eigenen und den geerbten Elementen sowie dem Zugriff aus den Methoden der abgeleiteten Klasse oder dem Zugriff von außen unterscheiden.
Zugriff aus der abgeleiteten Klasse Die Methoden, die in der abgeleiteten Klasse deklariert sind, können sowohl auf die eigenen als auch die geerbten Elemente direkt über den Elementnamen zugreifen. Während der Zugriff auf die eigenen Elemente jedoch keinerlei Beschränkungen unterliegt, wird der Zugriff auf die geerbten Elemente durch die Zugriffsspezifizierer aus der Basisklassendeklaration geregelt. Eine Methode der abgeleiteten Klasse hat Zugriff auf public- und protected-Elemente, nicht aber auf private-Elemente. Geerbte private-Elemente können in den Methoden der abgeleiteten Klasse nur auf dem Umweg über geerbte public- oder protected-Methoden angesprochen werden. class Basis { int basis_priv; protected: int basis_prot; public: int basis_pub; }; class Abgeleitet : private Basis { int abg_priv; public: void abg_func(int n) { basis_pub = n; basis_prot = n; //basis_priv = n; // Fehler, kein Zugriff } };
181
Sprachkonzepte und Syntax
Zugriff von außen Nach außen gibt es keinen Unterschied zwischen den eigenen und den geerbten Elementen einer Klasse. Wichtig ist nur, welche Zugriffsrechte die abgeleitete Klasse für die geerbten Elemente nach außen weitergibt. Grundsätzlich gelten die Zugriffsrechte, die in der Basisklasse, aus der die Elemente stammen, spezifiziert wurden. Man kann aber durch die Zugriffsspezifizierer der Vererbung die Zugriffsrechte verschärfen (siehe Abschnitt Vererbung und Polymorphie, Zugriffsbeschränkung bei der Vererbung). Für einzelne Elemente kann man die bei der Vererbung eingeengten Zugriffsrechte durch Redeklaration oder mit Hilfe des Schlüsselworts using wieder auflockern.
Zugriff auf verdeckte/überschriebene Methoden Wird eine geerbte Methode in der abgeleiteten Klasse durch Deklaration einer gleichnamigen Methode mit gleicher Signatur überschrieben oder durch die Deklaration einer gleichnamigen Methode mit abweichender Signatur verdeckt, kann man mit Hilfe des Gültigkeitsbereichsoperators weiterhin auf die Version der Basisklasse zugreifen. Dies funktioniert ebenso gut von innerhalb wie von außerhalb der Klasse. class Basis { public: void func(int) {cout << "Basis-Version" << endl;}; }; class Abgeleitet : public Basis { public: void func(char*) { // Zugriff auf verdeckte Basisklassenversion Basis::func(3); cout << "abgel. Version" << endl; }; }; int main() { class Abgeleitet abg; // Zugriff auf verdeckte Basisklassenversion abg.Basis::func(3); // ok return 0; }
Verweise Siehe Kategorie Vererbung und Polymorphie, Zugriffsbeschränkung und Zugriffsrechte
182
Der Konstruktor
Der Konstruktor klassenname()
{}
Beschreibung Jede Instanzbildung eines Klassenobjekts ist mit dem Aufruf eines Konstruktors verbunden.
Anwendung Der Programmierer kann im Anweisungsteil des Konstruktors für die Initialisierung der Datenelemente und für Eingangsarbeiten (beispielsweise der Reservierung von dynamischem Speicher) sorgen. Durch Definition eines oder mehrerer eigener Konstruktoren (Überladung) kann die Instanzbildung gesteuert und an die Übergabe verschiedener Argumenttypen angepaßt werden.
Der Standardkonstruktor Um sicherzustellen, daß jede Klasse über einen Konstruktor verfügt, weist der Compiler Klassen ohne explizit deklarierten Konstruktor einen Standardkonstruktor zu (Konstruktor, der ohne Argument aufgerufen werden kann). Wird ein Konstruktor explizit definiert, steht der Standardkonstruktor des Compilers nicht mehr zur Verfügung; der Programmierer kann aber einen eigenen Standardkonstruktor definieren. Der Standardkonstruktor vereinfacht die Einrichtung eingebetteter und geerbter Objekte sowie von Arrays von Klassenobjekten, die ansonsten nur durch Angabe von Konstruktorlisten bzw. Initialisierungslisten zu bewerkstelligen ist.
Warnung ●
Ein Konstruktor hat immer den gleichen Namen wie seine Klasse.
●
Ein Konstruktor hat keinen Rückgabewert (auch nicht void).
●
●
Konstruktoren können nicht als virtual, static, const, mutable oder volatile deklariert werden. Konstruktoren können überladen werden. Die Überladung wird meist dazu genutzt, dem Konstruktor Argumente für die Initialisierung der Datenelemente der Klasse zu übergeben.
●
Konstruktoren werden nicht vererbt.
●
Es können keine Zeiger auf Konstruktoren definiert werden.
●
Konstruktoren können nach der Instanzbildung nicht mehr aufgerufen werden.
183
Sprachkonzepte und Syntax ●
Konstruktoren, die mit genau einem Argument aufgerufen werden können, können implizite Konvertierungen vom Argumenttyp in den Typ ihrer Klasse durchführen. Der Compiler nimmt diese Konvertierung in das Repertoire seiner Standardkonvertierungen auf. Um ungewollte Standardkonvertierungen zu verhindern, kann ein solcher Konstruktor als explicit deklariert werden.
Beispiele class demo { int privat; public: demo(int i) {privat = i;} explicit demo(float f) {privat = (int) f;}; }; int main() { class demo instanz(100); class demo instanz2 = 100;//implizite Konvertierung class demo instanz3(3.2); class demo instanz4 = 3.2;//Fehler: explicit class demo instanz5; //Fehler, Standardkonstruktor //nicht mehr verfügbar return 0; }
Verweise Siehe Kopierkonstruktor Siehe Destruktor Siehe Klasseninstanzen als Datenelemente Siehe Kategorie Vererbung und Polymorphie, Vererbung und Konstruktor
Der Kopierkonstruktor klassenbezeichner(klassenbezeichner&)
Beschreibung Der Kopierkonstruktor ist ein Konstruktor, der Instanzen seiner Klasse auf der Grundlage anderer Instanzen der Klasse erzeugt. Die Instanz, die als Grundlage für die neu anzulegende Instanz verwendet werden soll, wird dem Konstruktor als Parameter übergeben.
184
Der Kopierkonstruktor
Anwendung Der Kopierkonstruktor wird aufgerufen, wenn Sie eine Klasseninstanz mit einer anderen Klasseninstanz (der gleichen Klasse) initialisieren. Wurde innerhalb der Klasse kein eigener Kopierkonstruktor deklariert, wird der Standardkopierkonstruktor aufgerufen, der die Klassenelemente 1:1 kopiert. Der Kopierkonstruktor ist eine von zwei Möglichkeiten, Klasseninstanzen zu kopieren. Die zweite Option ist der Zuweisungsoperator =, der ebenfalls als 1:1Kopierer vordefiniert ist und bei Bedarf überladen werden muß. Beachten Sie, daß Kopierkonstruktoren implizit auch bei der Übergabe von Klassenobjekten an Funktionen/Methoden und bei der Zurücklieferung von Klassenobjekten als Rückgabewerte von Funktionen/Methoden aufgerufen werden (sofern nicht Zeiger oder Referenzen verwendet werden).
Warnung 1:1-Kopieren wie vom Standard-Kopierkonstruktor vorgenommen, führt meist zu Fehlern, wenn die Klasse Zeiger enthält. Dies liegt einfach daran, daß für einen Zeiger die in dem Zeiger aufbewahrten Adressen kopiert werden, so daß nach der Kopierinitialisierung die beiden Zeiger der Vorlage und der Kopie auf das gleiche Objekt verweisen.
Beispiele #include using namespace std; class demo { int i; int* p; public: demo(int wert) { i = wert; p= &i; } demo(const demo& inst) { i = inst.i; p = &i; } }; int main() { demo a(3); demo b(a);
// Kopierkonstruktor
// Verwendung des Kopierkonstruktors
return 0; }
185
Sprachkonzepte und Syntax
Verweise Siehe Konstruktor
Der Destruktor ~klassenname()
{}
Beschreibung Destruktoren werden automatisch aufgerufen, wenn Klasseninstanzen ihre Gültigkeit verlieren (beispielsweise bei Verlassen des Gültigkeitsbereichs oder bei delete-Aufrufen für Zeiger auf Klasseninstanzen). Sie lösen die Instanz auf und geben den reservierten Speicher frei.
Warnung Für Destruktoren gelten ähnliche Regeln wie für Konstruktoren: ●
● ●
●
Destruktoren tragen immer den Namen ihrer Klasse, jedoch mit vorangehender Tilde. Ein Destruktor hat keinen Rückgabewert (auch nicht void). Destruktoren können nicht als static, const, mutable oder volatile deklariert werden. Eine Klasse ohne explizit deklarierten Destruktor bekommt einen Standarddestruktor zugewiesen.
●
Destruktoren werden nicht vererbt.
●
Es können keine Zeiger auf Destruktoren definiert werden.
Destruktoren unterscheiden sich von Konstruktoren: ●
Destruktoren können keine Argumente übergeben werden, woraus folgt:
●
Destruktoren können überschrieben, aber nicht überladen werden.
●
Destruktoren können als virtual deklariert werden.
●
Basisklassen sollten im Zweifelsfalle einen virtuellen Destruktor definieren.
●
●
Destruktoren von Klassen, die von Basisklassen mit virtuellem Destruktor abgeleitet sind, sind automatisch virtuell. Destruktoren können explizit aufgerufen werden, was aber selten notwendig ist.
Verweise Siehe Konstruktor
186
Standardmethoden
Standardmethoden Es gibt 5 Methoden, für die der Compiler automatisch Standardversionen erzeugt, wenn die Klasse keine eigenen Implementierungen vorsieht. ●
Standardkonstruktor
●
Destruktor
●
Kopierkonstruktor
●
Zuweisungsoperator
●
Adreßoperator (liefert den Wert des this-Zeigers zurück).
Der this-Zeiger this
Beschreibung Der this-Zeiger ist ein vom Compiler definierter Zeiger, der in allen nicht-statischen Methoden einer Klasse verfügbar ist und jeweils auf das aktuelle Objekt der Klasse verweist, über das die Methode aufgerufen wurde.
Anwendung Werden mehrere Instanzen einer Klasse gebildet, bekommt jede Instanz eine eigene Kopie der Datenelemente der Klasse. Die Methoden der Klasse stehen aber nur einmal im Arbeitsspeicher und werden von allen Instanzen einer Klasse gemeinsam benutzt. Da die Methoden üblicherweise auf den Elementdaten der Klasse operieren und diese instanzspezifisch sein sollen, muß sichergestellt werden, daß die jeweilige Methode nur auf die Daten zugreift, die zur aufrufenden Instanz gehören. Der Compiler weist aus diesem Grunde jeder Instanz einen Zeiger auf sich selbst zu – den this-Zeiger. Bei jedem Aufruf einer Methode wird dieser Zeiger automatisch an die Methode übergeben. Somit ist der this-Zeiger in jeder Methode verfügbar (mit Ausnahme statischer Methoden).
Beispiele #include using namespace std; class demo { public: void func() };
{ cout << this; }
187
Sprachkonzepte und Syntax int main() { class demo d1; cout << "This-Zeiger der Instanz d1 = "; d1.func(); return 0; }
Verweise Siehe Klassen, Methoden
Instanzbildung klassentyp obj;
Beschreibung Als Instanzbildung bezeichnet man die Einrichtung von Klassenobjekten (Variablen eines Klassentyps). Bei der Instanzbildung wird ein konkretes Objekt einer Klasse gebildet, indem den Datenelementen Werte zugeordnet werden. Für die Einrichtung des Speichers und die Initialisierung der Datenelemente ist der Konstruktor verantwortlich, der automatisch bei der Instanzbildung aufgerufen wird.
Bildung konstanter Instanzen Wird eine Instanz bei ihrer Einrichtung als const deklariert, bedeutet dies, daß alle Datenelemente, die in der Klasse nicht explizit als mutable deklariert sind, const sind, d. h., ihr Wert kann nicht verändert werden. Auch können nur als const deklarierte Methoden aufgerufen werden.
Instanzbildung und Arrays Bei der Initialisierung von Klassen-Arrays (Arrays von Klassenobjekten) muß jedes Array-Element durch einen Konstruktor eingerichtet werden. Dies kann durch einen Standardkonstruktor (Konstruktor ohne Parameter) geschehen oder durch eine Initialisierungsliste und expliziten Aufruf für jedes Array-Element:
Beispiele class demo { int privat; public: demo() {}; demo(int i) }; int main() {
188
// Standardkonstruktor {privat = i;}
Vererbung class demo d[2] = {demo(10), demo(100)}; // Einrichtung mit // Initialisierungsliste const class demo e[200]; // Einrichtung mit Standardkonstruktor return 0; }
Verweise Siehe Konstruktor
Vererbung und Polymorphie Vererbung Beschreibung Vererbung ist ein weiteres, ganz wesentliches Konzept der objektorientierten Programmierung, welches es ermöglicht, neue Klassen auf der Basis bereits vorhandener Klassen zu definieren.
Anwendung Die einfachste Form der Vererbung besteht aus einer Basisklasse und einer abgeleiteten Klasse. Durch die Vererbung wird festgelegt, daß die abgeleitete Klasse – zusätzlich zu den in ihrer Definition aufgeführten Eigenschaften (Datenelemente und Methoden) – über sämtliche Elemente der Basisklasse verfügt. Auf diese Weise kann praktisch jede Klasse von jeder anderen Klasse abgeleitet werden, was jedoch nicht immer sinnvoll ist. Zusätzlich zu den mit dem Klassenkonzept und der Kapselung verbundenen Vorteilen bringt die Vererbung weitere Annehmlichkeiten mit sich: ●
●
Gemeinsame Eigenschaften von Klassen brauchen durch Verlegung in eine Basisklasse nur einmal implementiert zu werden, wodurch auch die Wartbarkeit des Quellcodes erleichtert wird. Umgekehrt gewährt die Ableitung von Basisklassen ein einheitliches Erscheinungsbild verwandter Klassen.
Verschiedene Konzepte erlauben es, die Gestaltung von Klassenhierarchien flexibel zu gestalten: ●
Polymorphie (virtuelle Funktionen)
●
Friends
189
Sprachkonzepte und Syntax
Andere Konzepte dienen dazu, die Klassen vor Mißbrauch zu schützen und Klassenhierarchien strenger zu organisieren: ●
Zugriffsmodifizierer
●
abstrakte Klassen
Syntax class klassenname : BASISKLASSENLISTE { Liste der Klassenelemente }; ●
BASISKLASSENLISTE: In der Basisklassenliste werden, durch Kommata voneinander getrennt, die Basisklassen aufgeführt, deren Eigenschaften geerbt werden sollen. Ein Eintrag in dieser Liste hat folgende Syntax: [virtual] [public, protected, private] Basisklassenname
●
●
Die Zugriffsspezifizierer public, protected und private regeln bei der Vererbung nicht den Zugriff aus den Methoden der abgeleiteten Klassen auf die geerbten Elemente (dies wird schon durch die Zugriffsspezifizierer in der Deklaration der Basisklasse geregelt), sondern welche Zugriffsrechte die abgeleitete Klasse für die geerbten Elemente nach außen weitergibt (beispielsweise, wenn über eine Instanz der abgeleiteten Klasse auf diese Elemente zugegriffen wird, oder wenn die abgeleitete Klasse selbst als Basisklasse benutzt wird). Wird kein Zugriffsspezifizierer angegeben, wird die Klasse private vererbt. Das Schlüsselwort virtual ist nur für die Mehrfachvererbung von Bedeutung.
Warnung Eine Basisklasse muß deklariert sein, bevor sie zur Vererbung herangezogen werden kann! Sinnvolle Klassenhierarchien gründen darauf, daß ●
●
●
in Basisklassen die gemeinsamen Eigenschaften der abgeleiteten Klassen zusammengefaßt sind, abgeleitete Klassen natürliche Erweiterungen und/oder Spezialisierungen ihrer Basisklassen sind, man Objekte der abgeleiteten Klassen immer auch als Objekte ihrer Basisklassen ansehen kann.
Beispiele class Basis { public: void func1() {cout << "Basisklasse" << endl;} }; class Abgeleitet : public Basis {
190
Vererbung versus Einbettung public: void func2() {cout << "Abgeleitete Klasse" << endl;} }; int main() { Abgeleitet obj; obj.func1(); obj.func2(); return 0; }
Verweise Siehe Zugriffsbeschränkung bei der Vererbung
Vererbung versus Einbettung Beschreibung Um in einer Klasse die Eigenschaften einer anderen Klasse nutzen zu können, gibt es neben der Möglichkeit der friend-Deklaration zwei Möglichkeiten: die Vererbung und die Einbettung. Vererbung
Einbettung
class X
class X
{
{
...
...
};
};
class Y: public class X
class Y
{
{
...
class X var;
};
... };
Vererbung bringt Ihnen die Vorteile des Polymorphismus, der Zugriffsbeschneidungen und die Möglichkeit, Instanzen der abgeleiteten Klasse wie Objekte der Basisklasse zu behandeln.
Die Verwendung von Elementobjekten ist dagegen einfacher und besser zu überschauen.
Tip Sind Sie sich einmal unschlüssig, ob Sie die Funktionalität einer Klasse auf dem Weg über die Vererbung oder durch die Deklaration eines Elementobjekts zur Verfügung stellen, versuchen Sie sich klar zu machen, in welcher Beziehung die
191
Sprachkonzepte und Syntax
Objekte, die die Klassen repräsentieren, zueinander stehen. Stellen Sie sich einfach die Frage: »Kann man ein Objekt der Klasse Y auch als ein Objekt der Klasse X betrachten, oder sieht es vielmehr so aus, daß zu einem Objekt der Klasse Y auch ein Objekt der Klasse X gehört« Auf einen kurzen Nenner gebracht, heißt das » ist ein Y ein X «(Vererbung) oder » enthält Y ein X «(Einbettung) Für drei Klassen Auto, Motor und Sportwagen würde dies beispielsweise bedeuten, daß man die Klasse Sportwagen von der Klasse Auto ableiten würde (denn jeder Sportwagen ist auch ein Auto). Dagegen enthält jedes Auto einen Motor, man würde also in der Klasse Auto ein Objekt der Klasse Motor einbetten (und damit auch an die Klasse Sportwagen vererben).
Verweise Siehe Praxisteil, Kategorie Vererbung und Polymorphie
Zugriffsbeschränkung bei der Vererbung public, protected, private
Beschreibung Die Zugriffsspezifizierer public, protected und private können dazu benutzt werden, bei der Vererbung die Zugriffsrechte der geerbten Elemente zu verändern.
Anwendung Die folgende Tabelle zeigt wie sich die Zugriffsrechte für die geerbten Elemente bei Verwendung der Zugriffsmodifizierer public, protected und private ändern. Die Zugriffsspezifizierer der Vererbung regeln dabei nicht den Zugriff aus den Methoden der abgeleiteten Klassen auf die geerbten Elemente (dies wird schon durch die Zugriffsspezifizierer in der Deklaration der Basisklasse geregelt). Die Zugriffsspezifizierer der Vererbung betreffen nur die Zugriffsrechte, die die abgeleitete Klasse für die geerbten Elemente nach außen weitergibt (beispielsweise, wenn über eine Instanz der abgeleiteten Klasse auf diese Elemente zugegriffen wird, oder wenn die abgeleitete Klasse selbst als Basisklasse benutzt wird). Ausgangsbasis sind die Zugriffsrechte, die in der Basisklasse vorgesehen wurden. Diese können durch die Zugriffsspezifizierer der Vererbung verschärft werden.
192
Zugriffsbeschränkung bei der Vererbung
Zugriffsspezifizierer der Vererbung
Basisklasse
abgeleitete Klasse
public
public
public
protected
protected
private
private
public
protected
protected
protected
protected
private
private
private
public
private
protected
private
private
private
Warnung Wird kein Zugriffsspezifizierer angegeben, wird die Klasse private vererbt.
Beispiele #include using namespace std; class Basis { int w_priv; public: int w_pub; };
// private
class Abgeleitet: private Basis { public: void func() { w_priv = 2; // Zugriff verweigert, private-Element w_pub = 2; // ok } }; int main() { class Abgeleitet obj; cout << obj.w_pub << endl;
// Zugriff verweigert, private-Vererbung
return 0; }
Verweise Siehe Kategorie Klassen, Zugriffsspezifizierer Siehe Zugriffsrechte für einzelne Elemente auflockern
193
Sprachkonzepte und Syntax
Zugriffsrechte für einzelne Elemente auflockern Beschreibung Zugriffsbeschränkungen durch die Vererbung können in der abgeleiteten Klasse explizit wieder aufgelockert werden. Einer ererbten Eigenschaft kann jedoch nie eine weitreichendere Zugänglichkeit zugewiesen werden, als sie in der Basisklasse aufweist.
Beispiele class Basis { int w_priv; // private public: int w_pub1; int w_pub2; int w_pub3; }; class Abgeleitet: private Basis { // Redeklaration der Zugriffsrechte public: Basis::w_pub1; // ist wieder public using Basis::w_pub2; // ist wieder public }; int main() { class Abgeleitet obj; cout << obj.w_pub1 << cout << obj.w_pub2 << cout << obj.w_pub3 << cout << obj.w_priv << ...
endl; endl; endl; endl;
// Zugriff auf geerbte Elemente // kein Zugriff // kein Zugriff
Tip Der neue ANSI-Standard empfiehlt die Redeklaration durch die using-Anweisung.
Verweise Siehe Kategorie Klassen, Zugriffsspezifizierer
Vererbung und Konstruktor Beschreibung Konstruktoren und Destruktoren werden nicht vererbt. Daraus folgt, daß bei der Instanzbildung einer abgeleiteten Klasse die ererbten Datenelemente vom Konstruktor der jeweiligen Basisklasse eingerichtet werden müssen. Tatsächlich bilden die von einer Basisklasse geerbten Elemente in der abgeleiteten Klasse eine Art Unterobjekt mit eigenen Konstruktoren, Destruktoren,
194
Vererbung und Konstruktor
Zugriffsrechten, etc. Dieses Konzept ist die Grundlage für die in der objektorientierten Programmierung sehr gebräuchlichen Reduzierung eines abgeleiteten Objekts in ein Objekt einer seiner Basisklassen.
Anwendung Basisklassenobjekte einrichten
Standardkonstruktor
Ist in der Basisklasse ein Standardkonstruktor vorhanden, braucht sich die abgeleitete Klasse überhaupt nicht um die Einrichtung und Initialisierung der geerbten Elemente zu kümmern, sondern kann dies dem Compiler überlassen, der automatisch im Zuge der Instanzbildung für Objekte der abgeleiteten Klasse den Standardkonstruktor der Basisklasse aufruft. class Basis { public: int b_wert; Basis() { b_wert = 333;} }; class Abgeleitet : public Basis { public: int abg_wert; Abgeleitet(int i) { abg_wert = i;} }; int main() { class Abgeleitet obj(11);
Basisklassenobjekte einrichten
Spezieller Konstruktor
Um einen Konstruktor mit Argumenten für die Einrichtung der Basisklassenobjekte aufzurufen, muß man eine Konstruktorliste anlegen. Abgeleitet(int b1, double b2) : Basis1(b1), Basis(b2)
{ }
Die Konstruktorliste folgt mit einem Doppelpunkt auf den Namen des abgeleiteten Konstruktors. Um Argumente aus der Instanzbildung an einen Basisklassenkonstruktor weiterzugeben, muß für das jeweilige Argument im Konstruktor der abgeleiteten Klasse ein Parameter für das Argument definiert werden. class Basis { public: int b_wert; Basis(int par) { b_wert = par;} }; class Abgeleitet : public Basis { public:
195
Sprachkonzepte und Syntax int abg_wert; Abgeleitet(int i) Abgeleitet(int i, int n) : Basis(n)
{ abg_wert = i;} { abg_wert = i;}
}; int main() { class Abgeleitet obj(1,2);
Basisklassenobjekte einrichten
Reinitialisierung
Schließlich kann man im Konstruktor der abgeleiteten Klasse den geerbten Elementen neue Werte zuweisen. Auf diese Weise kann man die durch die Basisklassenkonstruktoren vorgesehene Initialisierung an die Bedürfnisse der abgeleiteten Klasse anpassen oder den Code übersichtlicher gestalten. Beachten Sie aber, daß ●
●
dies nichts daran ändert, daß die geerbten Elemente zuvor von einem Konstruktor ihrer eigenen Klasse eingerichtet werden, der Konstruktor der abgeleiteten Klasse im Gegensatz zum Konstruktor der Basisklasse auf Elemente, die in der Basisklasse als private deklariert sind, nicht zugreifen kann.
class Basis { public: int b_wert; Basis() }; class Abgeleitet : public Basis { public: int abg_wert; Abgeleitet(int i)
{ b_wert = 333;}
{ abg_wert = i; b_wert = 0;}
}; int main() { class Abgeleitet obj(11);
Mehrfachvererbung und Konstruktoren Klassen mit mehreren direkten Basisklassen Wird eine Klasse von mehreren direkten Basisklassen abgeleitet, beispielsweise class B1 {...}; class B2 {...}; class A: public B1, public B2 {...};
werden deren Konstruktoren in der Reihenfolge ihrer Auflistung bei der Vererbung abgearbeitet. Zuletzt wird der Anweisungsteil des Konstruktors der abgeleiteten Klasse ausgeführt.
196
Nicht vererbbare Methoden
Klassen mit direkten und indirekten Basisklassen Wird eine Klasse von einer Basisklasse abgeleitet, die selbst wieder von einer anderen Klasse abgeleitet ist, beispielsweise class B1 {...}; class B2: public B1 {...}; class A: public B2 {...};
wird zur Einrichtung des Basisklassenobjekts B2 der Konstruktor von B2 aufgerufen. Da das B2-Objekt selbst wieder ein Basisklassenobjekt der Klasse B1 enthält, wird als nächstes dessen Konstruktor aufgerufen und ausgeführt. Dann wird der Anweisungsteil des B1-Konstruktors und zuletzt der Anweisungsteil des Konstruktors der Klasse A ausgeführt.
Verweise Siehe Kategorie Klassen, Der Konstruktor Siehe Mehrfachvererbung
Nicht vererbbare Methoden Nicht vererbbar sind: ●
Konstruktor
●
Destruktor
●
Zuweisungsoperator
●
Auch Friend-Deklarationen, die ehedem keine wirklichen Elemente darstellen, werden nicht vererbt.
Polymorphie Beschreibung Der Begriff der Polymorphie ist eng an das Konzept der Vererbung gebunden. Er bezeichnet das Phänomen, daß eine Methode in verschiedenen abgeleiteten Klassen einer Hierarchie unterschiedlich implementiert sein kann.
Anwendung Auf diese Weise können einander ähnliche Klassen über eine Reihe gleichlautender und gleichartig zu benutzender Funktionen verfügen, die es dem Programmierer erlauben, Instanzen dieser Klassen gleich zu behandeln, ohne auf klassenspezifische
197
Sprachkonzepte und Syntax
Details, die in der Implementierung der Methoden versteckt sind (Information hiding), zu achten. Es liegt nahe, Methoden, die von mehreren Klassen benötigt werden, über eine gemeinsame Basisklasse zu vererben. In den abgeleiteten Klassen werden die Methoden überschrieben, um eine klassenspezifische Behandlung zu implementieren. Methoden, die überschrieben werden sollen, werden üblicherweise in der Basisklasse mit dem Schlüsselwort virtual deklariert. Gelegentlich wird auch das Überladen von Funktionen und Operatoren als Polymorphie bezeichnet.
Beispiele class ZeichenObjekt { protected: struct Punkt referenzpunkt; virtual int zeichne(struct Punkt p) { // zeichne Referenzpunkt an Koordinate p ... } }; class Rechteck : public ZeichenObjekt { protected: struct Punkt ecken[4]; virtual int zeichne(struct Punkt p) { // zeichne Rechteck mit linker unterer Ecke in // Koordinate p ... }; }; class Kreis : public ZeichenObjekt { protected: float radius; virtual int zeichne(struct Punkt p) { // zeichne Kreis mit Mittelpunkt in Koordinate p ... } };
Verweise Siehe Überschreibung und virtuelle Methoden Siehe Basisklassenzeiger
198
Überschreibung und virtuelle Methoden
Überschreibung und virtuelle Methoden Beschreibung Überschreibung bedeutet im Gegensatz zum Überladen, daß es nur eine Implementierung der Methode innerhalb einer Klasse gibt. (Die überschriebene Basisklassenversion kann allerdings über den Namen der Basisklasse und den Gültigkeitsbereichoperator aufgerufen werden. Dies wird häufig benutzt, um innerhalb der Implementierung der überschreibenden Methode die Basisklassenversion aufzurufen.)
Anwendung Der Compiler erkennt das Überschreiben einer Methode daran, daß neben dem Methodennamen auch Anzahl und Typen der Parameter übereinstimmen. Überschriebene Methoden bzw. Methoden, die überschrieben werden sollen, werden in der Basisklasse sowie in der abgeleiteten Klasse üblicherweise als virtual deklariert. Das Schlüsselwort virtual modifiziert dabei die Art und Weise, in der der Compiler eine Bindung zwischen einer Instanz und ihren Elementen herstellt..
Späte Bindung
virtual
Das Schlüsselwort virtual ist notwendig, um die Namen von überschriebenen Methoden in Klassenhierarchien richtig aufzulösen. Das Problem der Namensauflösung entsteht überhaupt erst dadurch, daß ein Objekt einer abgeleiteten Klasse praktisch auch ein Objekt der Basisklasse(n) ist. Dieser Umstand macht es möglich, einem Zeiger oder einer Referenz vom Typ der abgeleiteten Klasse die Adresse einer Instanz der Basisklasse zuzuweisen. Wird dieser Zeiger (oder die Referenz) nun dazu benutzt, eine Methode aufzurufen, die in Basisklasse und abgeleiteter Klasse deklariert ist (wobei die abgeleitete Klasse die Deklaration in der Basisklasse überschreibt), stellt sich die Frage, welche Version aufgerufen werden soll: ●
Die Methode der abgeleiteten Klasse, die dem Typ des Zeigers (der Referenz) entspricht? Diese Art der Namensauflösung wird als frühe Bindung bezeichnet, da sie zur Kompilierzeit vorgenommen wird.
Oder ●
Die Version der Basisklasse, die dem Objekt entspricht, auf das der Zeiger (die Referenz) verweist? Diese Art der Namensauflösung wird späte Bindung genannt, da sie erst zur Laufzeit erfolgt. Späte Bindung wird durch die Aufnahme des Schlüsselwortes virtual in die Funktionsdeklaration (zumindest der Basisklassen-Version) vereinbart.
199
Sprachkonzepte und Syntax
Beispiele #include using namespace std; class Basis { public: virtual void identifziere() {cout << "Dies ist die Basisklasse\n";}; }; class Abgeleitet : public Basis { public: virtual void identifziere() {cout << "Dies ist die abgeleitete Klasse\n";}; }; int main() { Abgeleitet abgeleitet_klasse; Basis *zeiger = new Basis;
// Zeiger vom Typ 'Zeiger auf Basis'
zeiger = &abgeleitet_klasse; zeiger->identifiziere(); }
Ausgabe: »Dies ist die abgeleitete Klasse«
Warnung ●
Für Klassen, die virtuelle Methoden enthalten (polymorphe Klassen) legt der Compiler eine virtuelle Tabelle an, in der alle virtuellen Methoden eingetragen sind. Zudem erhält die Klasse einen Zeiger auf ihre virtuelle Tabelle. Wird für ein Objekt der Klasse eine virtuelle Methode aufgerufen, greift der Compiler über diesen Zeiger auf die virtuelle Methode zu und entnimmt dieser die korrekte Adresse für den Aufruf der virtuellen Methode. Auf diese Weise ist sichergestellt, daß auch für Zeiger auf Klassenobjekte immer die zu dem Objekt passende Implementierung aufgerufen wird. Die späte Bindung erzeugt damit zusätzlichen Objektcode und bedingt längere Laufzeiten.
●
Konstruktoren können nicht als virtual deklariert werden.
●
Statische Methoden können nicht als virtual deklariert werden.
Verweise Siehe Vererbung und Destruktor
200
Vererbung und Destruktor
Vererbung und Destruktor Beschreibung Bei der Auflösung von Klassenobjekten wird automatisch der Destruktor aufgerufen. Dies ist unter anderem auch der Fall, wenn für einen Zeiger auf ein Klassenobjekt der delete-Operator aufgerufen wurde. Dabei sollte sichergestellt sein, daß der Destruktor der Klasse aufgerufen wird, dem das Objekt (und nicht der Zeiger, der auf das Objekt verweist) angehört.
Anwendung Aus den vorangehenden Ausführungen zur späten Bindung und den oben aufgeführten Gründen sollten Klassen, die Teil einer Vererbungshierarchie sind, über einen virtuellen Destruktor verfügen. Da der Destruktor, den der Compiler standardmäßig zuweist, nicht als virtual deklariert ist, sollte man in Basisklassen von Klassenhierarchien einen eigenen virtuellen Destruktor definieren, auch wenn man keinen Code in den Anweisungsteil schreibt.
Beispiele class Basis { ... public: virtual ~Basis() {} };
Abstrakte Methoden und Klassen Beschreibung Abstrakte Methoden sind Methoden ohne Anweisungsteil, die erfordern, daß sie in abgeleiteten Klassen überschrieben und mit einem Anweisungsteil versehen werden.
Abstrakte Methoden Abstrakte oder "rein virtuelle" Methoden werden als virtual deklariert und auf 0 gesetzt. Sie dienen lediglich als Vorgabe einer Schnittstelle für abgeleitete Klassen und haben keinen Definitionskörper. Sie müssen daher in den abgeleiteten Klassen definiert werden. virtual Typ name (PARAMETERLISTE) = 0;
201
Sprachkonzepte und Syntax
Abstrakte Klassen Klassen, die über abstrakte Methoden verfügen, werden automatisch zu abstrakten Klassen, mit der Konsequenz, daß von ihnen keine Instanz erzeugt werden kann. Sie stehen in Klassenhierarchien stets ganz oben, um geeignete Schnittstellen vorzugeben und so der Hierarchie ein einheitliches Gepräge zu verleihen.
Warnung Es ist zu beachten, daß Klassen, die von einer abstrakten Klasse abgeleitet wurden, und für die es versäumt wurde, die abstrakten Methoden zu überschreiben (und damit mit einem Definitionskörper zu versorgen), selbst zu abstrakten Klassen werden.
Beispiele class Basis { public: virtual void func() = 0; ... }; class Abgeleitet: public Basis { public: virtual void func() { ... } };
Basisklassenzeiger Beschreibung Da in einer abgeleiteten Klasse die Elemente der Basisklassen als Unterobjekte enthalten sind, kann man ein abgeleitetes Objekt wie eines seiner Basisklassenobjekte behandeln, es quasi auf sein Basisklassenunterobjekt reduzieren. Insbesondere in Verbindung mit Basisklassenzeigern und virtuellen Methoden ist dies ein äußerst nützliches Konzept, das in vielen Fällen äußerst effektive und elegante generische Lösungen erlaubt.
Anwendung Ein Basisklassenzeiger ist wie ein generischer Zeiger, dem man die Adressen abgeleiteter Objekte zuweisen kann. (Unter der Voraussetzung, daß das Basisklassenunterobjekt in dem abgeleiteten Objekt eindeutig identifizierbar und zugänglich ist.) Mit Hilfe dieser generischen Zeiger kann man
202
RTTI – Laufzeittypidentifizierung
●
Objekte verschiedener abgeleiteter Klassen mit gemeinsamer Basisklasse in einem Array (oder dynamischen Feld) verwalten, class ZeichenObjekt { ... }; class Rechteck : public ZeichenObjekt { ... }; class Kreis : public ZeichenObjekt { ... }; int main() { class ZeichenObjekt *geomFig[10]; geomFig[0] = new Kreis; geomFig[1] = new Rechteck; ...
●
generische Funktionen/Methoden implementieren, denen man Objekte verschiedener abgeleiteter Klassen mit gemeinsamer Basisklasse als Argumente übergeben kann. void markieren(const ZeichenObjekt& objekt)
{
...
}
RTTI Laufzeittypidentifizierung Beschreibung Um sinnvoll mit Basisklassenzeigern auf abgeleitete Objekte arbeiten zu können, muß es eine Möglichkeit geben, wieder auf die volle Funktionalität des abgeleiteten Objekts zuzugreifen. Das eigentliche Problem bei der Rückverwandlung ist die Typidentifizierung: welchem abgeleiteten Klassentyp gehört das Objekt an, auf das der Basisklassenzeiger verweist.
Anwendung Für die Typidentifizierung gibt es in C++ verschiedene Möglichkeiten: Virtuelle Methoden
Über einen Basisklassenzeiger soll für ein abgeleitetes Objekt eine in der abgeleiteten Klasse überschriebene Methode aufgerufen werden. base_ptr = &abg_obj;base_ptr->ueberschriebeneMethode(); Die Typidentifizierung erfolgt automatisch durch den Compiler. Soweit möglich und sinnvoll sollte man diese Form der Typidentifizierung nutzen.
203
Sprachkonzepte und Syntax
dynamic_cast
Über einen Basisklassenzeiger soll für ein abgeleitetes Objekt eine in der abgeleiteten Klasse neu definierte Methode oder ein neu definiertes Datenelement aufgerufen werden.
base_ptr = &abg_obj; if(abg_ptr = dynamic_cast(base_ptr)) { abg_ptr->neueMethode(); abg_ptr->Datenelement(); } Der Programmierer benutzt den dynamic_cast-Operator zur Umwandlung des Basisklassenzeigers in einen Zeiger auf Objekte einer abgeleiteten Klasse und überläßt dem Operator d