Jürgen Wolf
C++ von Abis Z Das umfassende Handbuch
......... ~
Galileo Press
liebe Leserin, lieber Leser, das ist bereits das vierte Buch, das Jürgen Wolf bei Galileo Computing veröffentlicht. Sie kennen vielleicht eines seiner Bücher, etwa .. C von Abis 20<, "Shell-Programm ierung« oder .. Un ux-UNIX-Programmieru ng«? All diese Werke sind von
hoher fac hJjcher Qualität, vermitteln das j eweilige Themengebiet sehr gründlich
und umfassend un d sind zudem seh r unterh altsam geschrieben. Diese Merkmale treffen auch auf sein neuestes Buch zur C++-Programmierung zu.
Auf den ersten Blick scheint es doch ungewöhnli ch zu sein, der Vielzahl an C++Literatur noch ein Buch hinzuzufügen. Aus Verlagssicht zum in dest ein riskantes
Unterfangen; es sei den n, wir haben es mit einem We rk zu t un , das sch licht besser ist als viele andere Bücher zum sei ben Thema. Und das ist hier der Fall : Sowoh l ein Programmieranfänger (mit guten PC-Kenntnissen) als auch ein »ausgewachsener« C++-Entwickler werden dieses Werk mit Gewinn lesen . D ie ein en werden Schritt für Sch ritt vorgehen, die anderen suchen gezielt nach Themen oder Lösungen. Und beide werden nicht enttäuscht werd en, denn sie finden h ier eine prakt ische Einfüh r ung in die Sprache ebenso wie ausfüh rli ches Fachwissen zur Standard Template Library, Boost, zur Socket- oder GUI-Programm ie rung. Doch selbst auf 1000 Seiten kann C++ nicht erschöpfend erklärt werd en. Vielleicht fehl t Ihnen Gru nd lagenwissen zu C? Auch das fi nden Sie im Buch, nä m lich auf der beiliegenden Buch-CD. Dort stellen w ir das komplette Werk »C von Abis b. zur Verfügung, als leicht navigierbare HTML-Version. Und j et zt wü nsche ich Ihnen viel Spaß beim Lesen !
Judith Stevens-lemoine lekt orat Galileo Computing
j ud ith
[email protected] www.galileocom puting.de Galileo Press· Rheinwerkallee 4 . 53227 Bonn
Auf einen Blick Vorwort ......... ... .. ........ .. .. .. ..... ...
17
Vorwort des Fachgutachters ....
23
1
Grundlagen in C++ ...................................................
25
2
Höhere und fortgeschrittene Datentypen ............... 129
3
Gültigkeitsbereiche. spezielle Deklarationen und Typumwandlungen .................................................. 219
4
Objektorientierte Programmierung ........................ 259
5
Templates und STL .................................................. 473
6
Exception-Handling .. ........ .. ..... ....................... ......... 657
7
C++-Standardbibliothek ... .. ..
691
8
Weiteres zum C++-Guru ...... .
817
9
Netzwerkprogrammierung und Cross-PlattformEntwicklung in C++ ................................................. 899
10
GUI- und Multimediaprogrammierung in C++
11
Anhang .. .. .. .... .... ... .. .. .. .... .. .. .. .. .. ..... ... .. .. ...... .. .. ... .. ... 11 89
973
Index ..... ,.. " .... ,... ,.. " .. ,..... " ..... ,... ,.. ,... ,..... " ..... ,.. " .. ,.. 1207
Der Name Galileo Press geht auf den italienischen Mathematiker und Philosophen Galileo GaJi lei (1564- 1642) zurück. Er gilt als Gründungsfigur der neuzeitlichen Wissenschaft und wurde berühmt als Verfechter des modernen, heliozentrischen Weltbilds . Legend är ist sein Ausspruch Eppur 5t' muove (Und sie bewegt sich doch), Das Emblem vo n Galileo Press ist der Jupiter, umkreist von den vier Galileischen Monden. Galilei entdeckte die nach ihm benannten Monde 1610.
Gerne stehen wir Ihnen mit Rat und Tat zur Seite: judith,
[email protected] bei Fragen und Anmerkungen zum Inhalt des Buches service@galileo -preS5.defürversandkostenfreie Be5tellungen und Reklamationen
[email protected]ür Rezensions - und Schulungsexemplare Fachgutachten Martin Conrad Lektorat Judith Stevens-Lemoine , Anne S<:heibe Korrektorat Susanne DÜweJl. Bonn Cover Barbara Thoben, Köln Titelbild Barbara Thoben, Köln Typografie und Layout Vera Brauner Herstellung SteHi Ehrentraut Satz SatzPro, Krefeld Druck und Bindung Bercker Graphischer Betrieb, Kevelaer Dieses Buch wurde gesetzt aus der linotype Syntax Serif (9 ,25/13,25 pt) in FrameMaker. Gedruckt wurde es auf chlorfre i gebleichtem Offsetpapier.
Bibliografische Information d er Deutschen Bibliothek Die Deutsche Biblioth ek verzeichn et diese Publikation In d er Deutschen Nation albiblIografi e: detaillierte bibliografische Daten sind Im Internet über hup:/Idnb .ddb.de abrufbar. ISBN
3-89842-816-8
ISB N 13 97 8-3·89842 ·816-3
Cl Galileo Press, Bonn 2006 1. Auflage 2006 Da> ...,til~geode Wen.: I.t In ..11. elnen Teilen u,heberr«htlkh gnchulrt. Al le RKhte vorbehalten. ,nsbe>Ondere das lI",ht der Ot,,,,,,wung. de. Vortrag •. der Reprod uktio n, der Vervi.lflltigung auffutomechanf«he m " der and"M Wegen un d der Spel<~ u ng In eJe klronlKhen Mt
de r irgendeine H. ftu ng übernehmen. Oie in d i ~m Wen.: wit
Inhalt Vorwort . Vorwort des Fachgutachters
1
Grundlagen In 1 .1
Die Entstehung von C++ ................................................ .
1.1.1
1 .2
e++ ...................................................................
Aufbau von C++
Erst e Schritte der C++·Programmierung ............... ,....... ........ ..... ..
1.2.1
Ein Programm erzeugen mit einem
1.2.2
Ausführen des Programms .... ........ .......... ,.................... .
1 .2.3
Ein Programm erzeugen mit einer IDE ... .
1.4
30
Bezeich ner ............. .
1 .3.2 1 .3.3
Schlüsselwörter. Literale
1 .3.4
Einfache Begrenzer .
36
Basisdatentypen ........ ........ ...... .
37
1 .4.1
Deklaration und Definition
1.4.2
Was ist eine Variable?
38 38
1.4.3
Der Datentyp bool
1.4.4
Der Datentyp char
39 39
1.4.5 1.4.6
Die Datentypen int
42
Gleitkommazahlen ffoat , double und long double
45
1.4.7
limits für Ganzzahl- und Gleitpunktdatentypen .
48 49 50 51
Konstanten . .... .... .... .... .... .... ...... .. ... ... .... .... .... .
1 .6
Standard Ein -/Ausgabe- Streams 1.6.1
1 .8
25 27
1 .3.1
Symbole von C++ .
1.5
1.7
25
31 33 33 34 34 34 34
Kommandozeilen -Compiler ..... .
1.3
17 23
Die neuen Streams - cout, ein, eerr, clog
1.6.2
Ausgabe mit eout .
1 .6.3
Ausgabe mit cerr
52 52
1.6.4
Eingabe mit ein
53
Operatoren .
55
1 .7.1
Arithmetische Operatoren
1 .7.2
Inkrement- und Dekrementoperator
56 59 60 64 64
1.7.3
Bitoperatoren .
1 .7.4
Weitere Operatoren
Kommentare ..
5
Inhalt
1.9
Kontrollstrukturen .............................. ........ ...... .. 1 .9.1 1 .9.2
1.10
1.11
2
65 65 84
1.9.3 Sprunganweisungen .. Funktionen ....... ........ ........ ........ ........ .................................
91 94
1 .10. 1 1 .10.2
95 97
Deklaration und Definition .. . Fun ktionsaufruf und Parameterübergabe ........ ....
1.10.3
Lokale und globale Variablen
104
1.1 0.4 1. 10.5
Standardparameter Funktionen überladen
105 1 08
1. 10.6 1. 10.7
Inline- Funktionen Reku rsionen
1. 10.8
main - Funktion
112 115 116 117 11 8
Präprozessor-Direktiven 1 .11. 1 Die #tdefine-Direktive. 1 .1 1.2
Die #tundef- Direktive
1 .1 1.3
Die #tinclude-Direktive ... .
1 .11.4 1 .1 1.5
Die Direktiven /terror und #pragma ... Bedingte Kompi lierung
121 122 123 124
Hohere und fortgeschrittene Datentypen ............................... 129 2. 1
2.2 2.3
Ze iger 2.1.1 2.1.2
Zeiger deklarieren Adresse i m Zeiger speichern
2.4
129 130 131
2.1.3
Zeiger dereferenzieren .. ........ ............
2.1.4
Zeiger, die auf andere Zeiger verweisen ..... ... ................. 137
2.1.5
Dynamisch Speic herobjekte anlegen und zerstören - new und delete
2.1.6
void- Zeiger ..
2.1 .7 Konstante Zeiger Referenzen ..... Arrays .
133
138 143
144 145 147
2.3.1
Arrays de klar ieren
147
2 .3.2
Arrays initialisieren
148
2.3.3 2 .3.4
Bereichsüberschreitung von Arrays .. .... .... .... .... .... .... .... . . 150 Anzahl der Elemente eines Arrays erm itteln ..... ... ..... ... . . 151 152 Arraywert von Tastatur einlesen ....................... .
2.3.5 2.3.6
6
Verzw eigu ngen (Selektionen) Schleifen (It erationen) .... .. ...... .. ...... .. .... .. ...... .. ...... .. ...... .
Mehrdimensionale Arrays .... .
152
Zeichenketten (C-Strings) - char-Array .......... .............................. 154 2.4 .1 C-String deklarieren und initialisieren ............................ 155
Inhalt
2.5
2.4.2 C-String einlesen ....................... . 155 2.4.3 C- Strings Bibli otheksfu nkt ionen .. 156 Arrays u nd Zeiger .. ........ .. ....... ..... ... ..... ... .... .. .. .... .. .. .... .. .. .... .. .. .... .. 160 2.5.1
C-Strings und Zeiger .. Mehrfache Indi rektion .................... .
165
2.6.2
Call by reference - Zeiger als Funktionsparameter
177
2 .6.3 2.6.4
Call by reference mit Referenzen nachbilden .... Arrays als Fun ktionsparameter .
178
2.6.5
Meh rdimensionale Arrays an Funktionen übergeben
182
166 C- String-Tabellen . 168 2.5.4 Arrays im Heap (Dynam isches Array) .... """ .... "" .... "" ... 170 ParameterObergabe mit Zeigern, Arrays und Referenzen 175 2.6. 1 Call by value .. 176
2.5 .2 2.5.3 2.6
2.7
2.8
3
179
2.6.6 Argumente an die main-Funktion übergeben Rückgabewerte von Zeiger, Arrays und Referenzen
1B3 185
2.7.1
Zeiger al s Rückgabewert
185
2.7 .2 2 .7.3 2.7.4
Referenz als Rückgabewert ................... ." ..... ." ..... ." .... . 188 190 const- Zeiger als Rückgabewert Array als Rückgabewert 190
2.7.5
Meh rere Rückgabewert e .. .
Fortgeschrittene Typen .. ." ..... ." .............. " ...... " ...... " .... .. 2.8.1 Strukt uren .. .... .. . 2.8.2 2.8.3
Unions .. " ....... .. " .... .. " ...... " ...... " ...... " .... .. Aufzählungstypen
2.8.4
typedef .. .. .
191 192 192 213
215 216
Gültigkeitsbereiche, spezielle Deklarationen und Typumwandlungen ................................................................... 219 3.1
Gültigkeitsbereiche (Scope) .. .. ....... .......... " ...... " ...... " .. .. .... " .. .... ". 219 3.1.1 Lokaler Gültigkeitsbereich (Local Scope) .. 220 3.1.2 3.1.3 3.1.4
3 .2
Gültigkeitsbereich Funktionen .. 220 Gültigkeitsbereich Namensraum (Namespaces) ............ . 222 Gültigkeitsbereich Klassen (Class Scope) ... ...... .... . . 222
Namensräume (Namespaces) ." .... .......... " ...... " ...... " ...... " .......... .. 222 3.2.1 Neuen Namensbere ich erzeugen (Defin ition) 222 3.2.2 3.2.3
Zugriff auf die Bezeichner im Namensraum using - einzelne Bezeichner aus ei nem
225 228
3.2.4
Namensraum importieren ............... " ...... . using - alle Bezeichner aus einem Namensraum importieren.
230
7
Inhalt
3.3 3.4
3.5
3.6 3.7
4
234 234 235 236 238 239 243 244 244 244 245 247 247 248 248 249 249 250 254
Objektorientierte Programmierung ......................................... 259 4.1
4.2
4.3
4.4
8
3.2 .5 Nameosauflösung Aliasnamen fü r Namensbereiche .... 3.2.6 Anonyme (namenlose) Namensbereiche 3.2.7 Namensbe reich und Headerdateien 3.2.8 (-Funktionen bzw. Bibli otheken in einem (H-Programm 3.3.1 (-Fun ktion en aus einer (-Bibliothek aufrufen Speicherklassenattribute ................................................... . 3.4.1 Speiche rklasse auto. 3.4.2 Speicherklasse register 3. 4.3 Speiche rklasse stat ic . 3.4.4 Speicherklasse ext ern . 3.4.5 Speiche rklasse mutable Type nqualifikatore n .............................................................. 3.5 .1 Qualifizierer coost .. . 3.5.2 Qualifiziere r volatile . Funktionsattribute ..... .. .... .. ...... Typumwandlung ............................................................. 3.7.1 Standard-Typumwandlung 3.7.2 Explizite Typumwandlung ....................................
OOP.Konzept ve rsus prozedu rales Konzept 4.1.1 OOP-Parad igmen ........................................................ . Klassen (fortgesch rittene Typen) .... . 4.2.1 Klassen deklarieren .......................... . Elementfunktion (Klassen methode) defi ni eren 4.2.2 4.2.3 Objekte deklarieren ..... 4.2.4 Kurze Zusamm en fassung. 4.2.5 private und public - Zugriffsrechte in der Klasse 4.2.6 Zugriff auf die Elemente (Mem ber) einer Klasse ....... .... 4.2.7 Ein Programm organisie re n 4.2.8 Konstruktoren ..... . 4 .2.9 Destru ktoren . Mehr zu den Klassenmethoden (K lassenfun kt ionen) 4 .3.1 Inline-Methoden (explizit und im plizit) 4.3.2 Zugriffsmethoden . 4.3.3 Read-only-Methoden 4.3.4 th is-Zeiger Verwenden von Objekten 4.4 .1 Read-only-Objekte ....................................................... .
259 260 261 262 263 264
265 266 268 275
279 286
289 289 293
296 299 301 301
Inhalt
4.4.2 4.4.3 4.4.4
Objekte als Funktionsargument e .................................. . 301 Objekte als Rückgabewert ... . 30B Klassen-Array (Array von Objekten) .............................. . 309
4.4.5 4.4.6
312 Dynamische Objekte Dynamische Klassene lemente ....... ............................... . 31B
4.4.7 4.4.8
Objekte kopieren (Kopierkonstruktor) ... .. Dynam isch erzeugte Objekte kopieren (operator:O)
4.4.9
Standardmethoden (Ü berblick)
323 325
4.4. 10
Objekte als Elemente (bzw. Eigenschaften) in anderen Klassen .
325
4.4.11 4.4. 12
Teilobjekte initiaJisieren ..... Klassen in Klassen verschachteln
332
4.4. 13
Konstante Klasseneigenschaften (Datenelemente)
4.4.14 4.4.15
St atische Klasseneigenschaften (Datenelement e) St atische Klassenmethoden .......................... . friend-Funktionen bzw. friend-K lassen
Operatoren überladen .. Grundlegendes zur Operator-Ü berladung . 4.5.1
335 337 341 343 346 352 352
4.5.2
Überladen von arithmetischen operatoren
356
4.5.3
Überladen von unären Operatoren.
4.5.4 4.5.5
Überladen von ++ und -Überlad en des Zuweisungsoperators
365 36B 370 371 375 37B 3B1 3B6 3B7 3BB 390 392 395 399 402 405 405 408 411
4.4.16 4.4.17
4.5
4.6
Zeiger auf Eigenschaften einer Klasse ....
4.5.6
Überladen des Indexoperators [] (Arrays überladen)
4. 5.7 4.5.8
Shift- Operatoren überladen O-Operator überladen .
4.5.9 new- und delete-Operator überladen. Typenumwandlung für Klassen 4 .6.1
4.7
322
Konvertierungskonstruktor
4.6 .2 Konvertierungsfunktion . Vererbung (Abgeleitete Klassen) 4.7.1
Anwendungsbeisp iel (die Vorbereitung)
4.7.2 4.7.3 4.7.4
Die Ablei t ung einer Klasse Redefinition von Klassenelementen . Kon struktoren .
4.7.5 4.7.6
Zugriffsrecht protected ..
4.7.7 4.7.8
Typen umwandlung abgeleitet er Klassen .. Klassenbibliotheken erweitern
Destruktoren ................................. ..
333
9
Inhalt
4 .8
4 .9
Polymorphismus ...................................... . 412 413 4 .8.1 Statische bzw. dynamische Bindung .... 4 .8 .2 Virtuelle Methoden ...................................... ........ ........ . 413 Virtuelle Methoden redefinieren Arbeitsweise von virtuellen Methoden .....
4 .8.5
Virtuelle Destruktoren bzw . Destruktoren abgeleit eter Klassen ............................ .. ....... ........ ........ ........ 429
4.8.6
Polymorphismus und der Zuweisungsoperator
4.8.7 4.8.8
Rein virtuelle Methoden und abstrakte Basisklassen . Probleme mit der Vererbung und der
4.8.9
dynamic_cast- Operator .. Fallbeispiel: Ve rkettete Listen Indirekte Basisklassen erben .. Virtuelle indirekte Basisklassen erben .
Templates und STL ................................................................... 473 5.1
5.2
5.3
Funktions-Templates .................... ................................ .. 5.1.1
Funktions-Templates definieren
5.1.2 5.1.3
Typenübereinstimmung ................................... . Funktions-Templates über mehrere M odule .
5.1.4 5.1.5
Spezialisierung von Funktions-Templates Verschiedene Parameter .. ...... .... .... ... ... ....... .. .
5.1.6 Explizite Template-Argumente Klassen-Templates .. .. Definition. .
5.2.2 5.2.3
Methoden von Klassen -Templates d efinie ren Klassen-Template gener ieren (I nstantiierun g)
5.2.4
Weitere Template-Parameter
5.2.5 5.2.6
Standardargumente von Templates ..... . Explizit e Instantiierun g .................................................. 502
STL (Standard Template library) 5.3.1 Konzept von STL . 5.3.3 5.3.4
Hilfsmittel (Hilfsstrukturen) Allokator . Iteratoren
5.3.5
Contai ner ...... .. .... ...
5.3.6 5.3.7
Al gorithmen .... .... ... ..... .. . Allokatoren .
.......... ... .
............
473 475 478 479 479 483 484 485 486 487 492 497 500
5.2.1
5.3 .2
10
431 433 437 439 461 465 469
Mehrfachvererbung .. ... .. .... . . 4.9.1 4.9.2
5
418 424
4 .8.3 4 .8.4
503
504 508 521 521 526 577 640
Inhalt
6
Exception-Handling ................................................................. 657 6. 1 6.2
6.3
Except ion-Handling in
Eine Exception auffangen - Handle einrichten.
6.3.1 6.3 .2
6.4
Stack-Abwicklung (Stack-Unwinding) ....... .
6.3.4
try- Blöcke verschachteln .
6.3.5
Exception weitergeben
6.7
7
............
Ausnahme-Klassen (Fehlerkl assen) .. ...... .. ...... .... ... . Klassenspezifische Exceptions ............... .
Standard-Exceptions .
6.5.1 6.5.2 6.6
Reihenfolge (Auflösung) der Ausnahmen Alternatives catch(. ..) ............................ .
6.3.3
6.4.1 6.5
eH
Eine Exception auslösen ................. .
Virtuelle Methode whatO . Anwenden der Standard-Except ions ...... ........ .. .
System- Exceptions
6.6.1
bad3 110c
6.6.2
bad_cast
6.6.3 6.6.4
bad_typei d . bad_exception
Exception-Spezifikation
6.7.1
unerlaubte Exceptions ..
6.7 .2
terminate-Handle einricht en
658 658 659 662 662 664 666 669 672 674 676 677 677 682 683 683 683 683 684 685 687
C++-Standardbibliothek ........................................................... 691 7.1
691 693 7. 1.2 Datentypen ... .... .... . 693 7. 1.3 Strings erzeugen (Konstruktoren) 694 696 7. 1.4 Zuweisungen ........................................... . 7.1.5 Elementzugriff .... .. ....... ... .............................. 698 7. 1.6 l änge und Kapazität ermitte ln bzw. ändern . 699 7. 1.7 Konvertieren in einen C-String ...................................... 702 7.1.8 Manipu lation von Strings .............................................. 703 7.1.9 Suchen in Strings . 706 7.1.10 Strings vergleichen 713 Die (überladenen) Operatoren 715 7.1.11 7.1.12 Einlesen ein er ganzen Zeile 717 Ein-/Ausgabe Klassenhierarch ie (l/O-Streams) 718 7.2 .1 Klassen für Ein- und Ausgabe-Streams 72 1 Die String-Bibliothek (string-Klasse) .
7.1.1
7.2
Exception-Handling ............................................. .
Inhalt
Klassen für Datei-Streams (File-Streams)
744
7.2.4 7.2.5
Klassen für String-Streams ... .. . Die Klasse streambuf Die Klasse filebuf ..
759 766
7.2.6
Die Klasse stringbuf .... .. ... ... ...
77 1
7.3
7.2.7 Die Klasse stdiobuf . Nu merische Bibliothek(en) .... .. .... ....... ...
772 774 774
7.4
776 799 7.3.4 Grenzwerte von Zah len typen . 803 7.3.5 Halbnumerische Algorithmen. 808 Typenerkennung zur laufzeit ................................................... .. . 811
7.2.2 7.2.3
7.3 .1 7.3 .2 7.3.3
8
Komplexe Zahlen (co mplex-Klasse) valarray Globale numerische Funktionen (cmath un d cstdli b)
Weiteres zum C++-Guru .......................................................... 817 8. 1
Module 8.1.1 Aufteilu ng ..... 8.1.2 8.1.3 8.1.4 8.1.5
8.2
Di e öffentli che Schnittstelle (Headerdatei) ... Die private Datei Die Client-Datei .... ................................................ . Speicherklassen extern und static
Werkzeuge ..................... . 8.1.6 Von C zu C++ 8.2.1 8.2.2
Notizen. Kein C++
8.2.3
Kein C .
malloc und free oder new und delete setj mp und longj mp oder catch und throw 8.2.5 ,.Altes« C++ .... . 8.2.4
8.3
8.4
819
820 822 823
82 5 826 826 827 829 830 831 831
Headerdateien mit und ohne Endung .. Standardbibliothek nich t komplett oder veralte t
831 832
8.3.3
Namespace (Namensbereiche) SchleifenvariabJe von tor
832
UML 8.4.1 8.4.2 8.4.3
8.5
8 17 8 18
8.3.1 8.3.2 8 .3.4
12
770
832
833 833 835 Diagramme erst ellen 835 Klassendiagramme mit UMl .... ..... ... ..... ... .... ... ..... ... .... .. 836
Wozu UMl? UMl- Komponenten
8.4.4 Programmierstil 8.5.1 Kommentare .......................... .
876 877
Inhalt
8.5.2 8.5.3
8.6
9
879 Code ........................................................... . 879 8enennung Codeformat ieru ng ....................................................... . 880
8.5.4 8.5.5
Zusammenfassung
Boost . 8.6.1
Boost.Regex (Reguläre Ausdrücke)
881 881 883
Netzwerkprogrammierung und Cross-PlattformEntwicklung in C++ .................................................................. 899 9.1
9.2
Begriffe zur Netzwerktechnik 9.1.1 9. 1.2
IP- Nummern Portnummer
9.1.3 9.1.4
Host- und Domainname .... ..... .... .... ..... . Nameserver
9.1.5 9.1.6
Das IP-Protokoll . TCP und UDP .
9.1.7 Was sind Sockets? Headerdateien zur Socketprogrammierung 9.2.1
9.3 9.4
9.5
9.6
Linux!UNIX ...................
...............
900 900 901 902 903 903 903 904 905 905
9.2.2 Windows .......................... .................................. CI ient -ISe rver -Pri nzi p 9.3.1 Loopback-Interface ........... .. ........................ .
907 908
905
Erstellen einer Client-Anwendung .
908
9.4.1 9.4.2
socketO - Erzeugen eines Kommunikationsendpunkts ... 909 connectO - Client stellt Verbindung zum Server her 910
9.4.3
Senden und Empfangen von Daten
9.4.4 doseO. dosesocketO ........................................ . Erstellen einer Server-Anwendung ..
916 918 919
9.5.1
bindO - Festlegen einer Adresse aus dem Namensraum ....
9.5.2
IistenO - Wartesch lange für eingehende
9.5.3
Verbindungen einrichten .............................................. . 921 acceptO und die Server-Hauptschlei fe .. 922
919
Cross-Plattform-Development .................................................... . 924 Abstraktion layer ........... . 9.6.1 925 9.6.2 Headerd atei (socket.h) 925 9.6.3
Quelldatei (socket .cpp) ...
927
9.6.4
TCP- Echo-Server (Beispiel) ............................
937
9.6.5 9.6.6
Exception-Handling integrieren . 940 Server- und Client-Sockets erstellen (TCP) ..................... 946
13
Inhalt
9.7 9.8
9.6.7 Ein UDP-Beispiel ....................................... ........ ............ 955 Mehrere Clients gleichzeitig behandeln .... .. . 958 Weitere Anmerkungen zur Netzwerkprogrammierung ...... .. ..... .. ... 967 9.8.1 9.8.2
Das Datenformat Der Pu ffer
9.8.3
Portabilität . Von I Pv4 nach IPv6 .............. ...................... ..
9.8.4 9.8.5 9.8.6 9.8.7
...... ....... ..
RFC-Dokumente (Request tor Comments) . Si cherheit Fertige Bibliotheken .
968 968 969
969 971 971 972
10 GUI- und Multimediaprogrammierung in c++ ........................ 973 10.1
10.2
10.3
GUI-Programmierung - Überblick
973
10.1.1
Low-Level ....
973
10.1.2 10.1.3
High -level ................................... ........ ........ ........ ........ . 974 Überblick zu plattformunabhängigen Bibliotheken 975
10.1.4 Überblick zu plattformabhängigen Bibliotheken Mu ltimedia- und Grafikprogrammierung - Überblick
977 977
10.2.1 10.2.2
978 980
Überblick zu plattformunabhängigen Bibliotheken Überblick zu plattformabhängigen Bibliotheken .
GUI-Programmierung mit w XWidgets .
981
10.3.1 10.3.2
Warum wXWidget s? .. Das erste Programm - Hallo We lt .
981
10.3.3
Die grund legende Str uktur eines wxWidgets+Programm .
10.3.4
Event -Handle (Ereignisse behandeln)
10.3 .5 10.3 .6
Die Fenster-Grundlagen .... . Übersicht zu den wxWidgets-(Fenst er-)Klassen ... .... .... .. 1001
10.3.7
wxWindow, wxControl und wxControlWithltemsDie Basisklassen . ......... . 1002
10.3 .8
Top-Level-Fenster .
981 985 991 999
... ...... .... .. ..... ... ..... ... . 1006
10.3 .9 Container Fenster ...... . 1030 10.3 .10 Nicht statische Kontroll-Elemente ... ... .. ... .... .... .... .... ...... 1057 10.3 .11 Stat ische Kontroll- Elemente .... ... 10.3 .12 Menüs 10.3 .13 Ein Beispiel- Text-Editor ...... .. ...... .. .... . 10.3 .14 Standarddi aloge 10.3 .15 Weitere Elemente und Techniken im Überblick
'4
.... 1116 ... 1121 ... 1137 ... 1152 ... . 11 76
Inhalt
11 Anhang ..................................................................................... 1189 11 .1
Operatoren in CH und deren Bedeutung (Übersicht)
11 .2 11 .3
Vorrangtabelle der Operatoren Schlüsselwörter von eH ................ .
.......... 1 189 ......... 1191 ............ 1 192
11 .4
Informat ionsspeicherung
.... 1 192
11.5
Zahlensyst eme 11 .4 .1 Zeichensät ze .. . 11 .5 .1 ASC II ~Ze i che n satz
.... 1193 .. 1200 ... 1201
11 .5.2 11 .5.3 Index ....
ASCI I ~Erweiterungen
Unicode .. ... .
............... .
............................ 1202 .. 1204 ..1 207
'5
Vorwort Mittlerweile gibt es eine ganze Menge an Büchern zur e H -Programmiersprache. Dies allein zeigt schon die Popularilät, welche diese Sprache genießt. Natürlich habe ich es mir nicht nehmen lassen, auch etwas vom großen Kuchen zu bekommen. Wieso mache ich mir aber die Mühe, gute acht Monate auf das Schreiben eines weiteren Buches über diese Sprache zu verwenden, wenn es schon unzählige davon gibt? Es gibt schließlich viele hervorragende eH-Bücher fUf Einsteiger. Und profis greifen dann zum viel zitierten Stroustrup ("Oie e++ Programmiersprache«) . An welche Zielgruppe richtet sich also dieses Buch, und was bietet dieses Buch, was andere C++-Bücher nicht bieten?
Zielgruppe Dieses Buch richtet sich an fast jede Zielgruppe und kann auch als Nachschlagewerk verwendet werden. Es ist geeignet für den absoluten Einsteiger in die Programm ierung bis hin zum fortgeschriltenen Programmierer. Auch .. UmsteigeH von C dürften hier keine Probleme haben, und auch das Deja-vu-Erlebnis sollte sich hier in Grenzen hahen (speziell auch für die Leser mei nes Buches "C von A bis Z..). Es ist auch nicht nötig . dass Sie bereits über Kenntnisse irgendeiner anderen Programmiersprache verfügen . Nach diesem Buch können Sie ohne Bedenken auf das Stroustrup-Buch zurückgreifen und sich selbst den letzten Schliff geben. Natürlich darf man bei einem Buch zur Programmierung (egal welcher Programmiersprache) immer erwarten. dass der Leser grundlegende Kennmisse über di e Arbeiten mit einem PC mi tbri ngt. Aber was bietet dieses Buch, was andere Bücher zu C++ nicht bieten? Eine ganze Menge: Neben dem üblichen ANSI-C++-Standard geh t dieses Buch auch aufThemen wie STL, Boost, Socket- oder die GUI-Programmierung ein.
C und C++ Dass C++ eine Erweiterung von C ist, bringt so manchen Buchautor zum Grübeln. Soll manjetzt ein Buch zweiteilen und somit C und C++ verwenden oder C ganz ignorieren. Zugegeben, wer reines C++ programmieren will, benötigt kein C. Aber C völlig zu ignorieren und als unnötigen Ballast zu bezeichnen, ist ein weiterer fo lgensch werer Fehler. Wer das Glück hat und ein neues "leeres« Projekt anfangen darf. dem ka nn C egal sein. Aber oft hat man als Programmierer die
17
Vorwort
undankbare Aufgabe . ..alte .. Programme zu pflegen bzw. zu verbessern. Und häu· fig sind solche Programme noch in C geschrieben. Hierbei erhält man dann meistens den Auftrag. das Programm objektorientiert und flexibler zu machen. d .h .. man soll aus einem in C geschriebenen Programm ein CH-Programm machen. Wie dem auch sei - Ober das Pro und Contra ließen sich noch viele Zeilen schreiben. und eben aus diesem Grund finden Sie auf der Buch-CD als kOSlenlose Beigabe die HTMt·Version des Buches ~ C von Abis z,. (2. Auflage).
Betriebssystem Da dieses Buch über den gewöhnlichen Standa rd-C++-Umfang hinausgeht. stellen Sie sich sicherlich die Frage. ob Sie das alles auch auf Ihrem Betriebssystem benutzen können. Hierzu ein ganz klares ja. Alle Themen sind so gut wie plau formunabhängig und wurden auf Linux. UNIX (BSD) und MS Windows getestet. Und wenn es trotzdem mal die ein oder andere .. Ungereimtheit" gibt wird daraufhingewiesen und entsprechende Alternativen demonstriert. Natürlich ist dieses Buch so aufgebaut, dass zuerst auf die in CH standardisierten Dinge eingegangen wird.
Übersicht Im ersten Kapitel wird auf die reinen Grundlagen von CH eingegangen . Dies beinhaltet einfache Dinge wie den BezeichneT. Basisdatentypen, Konstanten, einfac he Ein-/Ausgabe. die grundlegenden Operatoren, Kommentare. Kontrollstrukturen wie Verzweigungen oder Schleifen. Funktionen und Prä prozessor-Direktiven. Kapitel zwei geht auf die höheren und fortgeschrittenen Datentypen wie Zeiger, Referenzen, Arrays. Zeichenketten (C-Strings) und Strukturen ein.
Das dritte Kapitel behandelt unspektakuläre. aber sehr wichlige Themen wie die Gültigkeitsbereiche. Namensräume. Speicherklassenauribute. Typenqualifikatoren und Typenumwandlungen. Das Kapitel vier ist das wichtigste, aber auch schwierigste Kapitel in diesem Buch. Darin werden alle Themen behandelt. die die objektorientierte Prog ram· mierung betreffen. Das Verstehen dieses Kapitels ist die Grundlage fü r die weiteren Kapitel im Buch und für C++ generell. Dabei geht es um die Klassen und wie man diese in der Praxis anwenden kann. Natürlich wird hierbei auch auf die Vererbung. Polymorphismus und Mehrfachvererbung eingegangen .
•8
Vorwort
Kapitel fiinf geht dann auf die Erstellung eigener Funktion s- und Klassentempla[es ein. Daraufbasiertja auch d ie sn (Standard Template Library), weshalb auch sehr umfangreich auf STL eingegangen wird. Da in den vorangegangenen Kapiteln häufig die Rede von Exceptions (Ausnahmebehandlungen) war, behandelt Kapitel sechs dieses Thema sehr umfassend. Gewöhnlich besitzt jede Sprache einen Standardumfang. So natürlich auch C++ mit ihren Standardbibliotheken. Zunächst wird sehr ausführlich auf die StringBibliothek eingegangen. Anschließend folgen die Klassen für die Ein-/Ausgabe. Dabei wird nebe n den gewöhnlichen Klassen für die Ein- bzw. Ausgabe-Streams auch auf die Klassen für die Datei- und String-Streams eingegangen. Auch für Mathematiker hält C++ mit den Klassen valarray und complex einiges bereit. Natürlich wird auch auf andere numerische Bibliotheken eingegangen. Am Ende des siebten Kapitels finden Sie auch noch eine Beschreibung, wie Sie eine Typenerkennung zur Laufzeit d urchfüh ren können. In Kapitel acht finden Sie viele Info rmationen, über die man als C++-Programmierer verfUgen sollte. Neben ein fa cheren Dingen - zum Beispiel. wie Sie eigene Module erstellen - wird hierbei auch auf die fei nen, aber sehr wichtigen Unterschiede zwisc hen C und C++ eingegangen. Des Weiteren finden Sie eine sehr umfasse nde Einfüh rung in UML und die Erstellung von Klassen-Diagrammen. Natürlich wird auch ein wenig auf den Programmierstil eingegangen. Sehr selten findet man etwas über Boost, wes halb in Kapitel acht mit der Bibliothek BoosLRegex (fOr reguläre Ausdrücke) darauf eingegangen wird. Kapitel neun behandelt die Netzwerkprogrammierung. Da die Netzwerkprogrammierung nicht mehr zu den portablen Sachen gehön , wird in diesem Kapitel u.a. auf die Crossplattform-Entwicklung eingegangen. Hierzu werden Sie eine eigene Socket-Klasse schreiben. Im letzten Kapitel erhalten Sie zunächst einen Überblick über die gängigen GUIund Multimedia-Bibliotheken. Anschließen d wird sehr umfassend auf das wxWidgets-Framework eingegangen. Auch hierbei müssen Sie sich keine Gedanken bezüglich de r Portabilität machen . wxWidgets gibt es auf allen gängigen Plattformen, und (noch besser) die Quelltexte lassen sich oh ne Änderungen auf den verschiedensten Systemen übersetzen. Im Buch finden Sie an manchen Stell en grau hinterlegte Kästen, in denen Sie wei- [« ] terführende Informationen zu bestim mten The men erhalten. Einige Kästen sin d am Rand mit einem speziellen Icon gekennzeichnet. Dieses [con verweist auf einen Hi nwe is-Kasten. Hier fin den Sie z. B. Informationen über Unterschiede zwischen C und C++, Hinweise auf Fehlerquellen un d klei ne Tipps und Tricks, die Ihnen das (Programmierer-)Leben erleichtern .
19
Vorwort
Buch-CD Natürlich finden Sie auf der Buch-CD sämtliche Quellcodes aus dem Buch wieder. Ebenso wurden einige Anleitungen gängiger Compiler (Entwicklungsumgebungen) erstell t, wie Sie aus einem Quelltext ein au sführbares Programm machen. Sofern Sie also absoluter Neuling sind, sollten Sie zuerst einen Blick auf die BuchCD werfen. Damit auch MS Windows-Anwender gleich loslegen können, finden diese auf der Buch-CD die Bloodshed-Dev-C++-Emwicklungsumgebung wieder. Als Linuxbzw. UNIX-Anwender hat man es da leichter. Hier befindet sich gleich alles an Board des Betriebssystems und muss nicht extra besorgt werden. Gegebenenfalls kann es sein, dass man einzelne Pakete nachinstallieren muss (abhängig von der verwendeten Distribution). Neben der berei ts erwähmen HTML-Version meines Buches »C von Abis Z.. finden Sie auch noch eine weitere HTML-Version des Buches - Handbuch für Fachinformatik er«. Dieses Buch ist übrigens keine gedanken lose Zugabe des Verlags, sondern wurde ausdrücklich auf meinen Wunsch auf die Buch-CD gepresst. Der Grund ist einfach, viele angehende Programmierer überspringen häufig einfach die Grundlagen der Informatik. Solche Wissensdefizite machen sich irgendwann bemerkbar. Ich verwende außerdem selbst immer wieder gerne dieses Buch fLir meine Recherchen.
Danksagung Ein Buch mit einem solchen Umfang schreibt man nicht einfach so, und häufig steckt darin monatelange Arbeit (und vom »Autor-sein" kann man nicht leben). Schlimmer mu ss es aber für die Personen sein, die mit dem Autor in dieser Zeit zusammenleben. Meine Frau hat wohl schon die Hoffnung aufgegeben, mit einem normalen Menschen zusammenleben zu dürfen. Auch mein Sohn (vier Jahre) erkennt und protestiert sofort. wenn ich mich wieder zum PC »wegschleichen .. will. Einfach ausgedrückt. beim Schreiben von Büchern geht einfach die Harmonie flöten, Daher ist hier mal wieder ein riesiges Dankeschön an Euch
beide nötig. Ihr seid die wichtigsten Perso nen in meinem leben. Die wichtigste Person im fachlichen Bereich ist Manin Conrad, der stelS für Fachlesungen meiner Bücher zur VerfLigung steht. Er ist auch Maintainer _meiner.. Webseite und mi n lerweile brüten wir beide auch unser erstes Projekt aus. Obwoh l ich Manin mittlerweile mehrere Jahre ken ne, haben wir uns noch nie
'0
Vorwort
im realen Leben gesehen. Wer sagt da, dass virtuelle Freundschaften nicht funk tionieren. Auch dir, lieber Martin, vielen Dank für deine tolle Unterstützung. Bücher zu schreiben ist das eine, aber einen Verlag, der so flexibel ist und dem Autor so viele Freiheiten läss t, findet man kein zweites Mal. Natürlich gibt es hier auch immer eine Person, die hinter den Kulissen steht und diesen Prozess koordiniert. In diesem Fall ist es mei ne Lektorin Judith Stevens-Lemoine. die mich jetzt bereits beim vierten gemeinsamen Buchprojekt unterstützt. Vielen Dank, Judith, für die toll e Zusammenarbeit )ürgen Wolf
21
Vorwort des Fachgutachters C++, nur eine Erweiterung von C? Der Geruch eines neuen Buches zieht durch den Raum, Ihr Rechne r ist hochgefahren. und Sie sitzen in den Startlöchern. um sich mit e++ zu beschäftigen? Ich hoffe, Sie haben den Kaffee nicht vergessen.
Die meisten Leser werden sich vorher mit C beschäftigt haben. werden also eher Urnsteiger auf eine objektorientierte Sprache sein. Hierbei war die Auswahl. was erlern t werden soll , sicher eine schwierige Entscheidung. Wenn erfahrene Programmierer gefragt werden. welche Programmiersprache e mpfehlenswert ist, gibt es annähernd so viele Antworten wie Personen, d ie befragt werden. Falls es Einwände gegen C++ gegeben hat. wird das meist die Nähe zu C gewesen sein. Es ist durchaus möglich, CH genauso wie C als prozedurale Sprache zu verwenden und dabei die Bibliotheken e her als Zusatzfunktionen zu nuuen , un d leider wird C++ auch von vielen Leuten auf diese Art genutzt. Um wirklich gUi wartbaren objekto riemierten Code zu e rhalten, ist ein komple t-
tes Umdenken nötig, da die Schwerpunkte und Schwierigkeiten von beiden Ansätzen an komplett anderen Stellen liegen. Bei C war es noch möglich, kleinere Programme einfach zu beginnen und sie wachsen zu lassen - bei objektorientierten Sprachen fUhrt di ese Vorgehensweise schnell zu einem Neusch reiben des gesamten Codes, wenn der Code nicht prozedural sein soll, was ja Sinn der Sache ist. Es ist bei der Planung notwendig, den objektorientierten Ansatz zu verstehen und die Möglichkeiten zu kennen, die CH bieteL Fehlendes Wissen kann hier zu groben ärgerlichen Designfehlern und viel überflüssiger Arbeit führen. Namensräu me, Klassen, Templates, die STL, ... - es ist gut, dies alles zu kennen und in die Planung der Programme einzubeziehen. Aus diesen Gründen möchte ich Ihnen nahelegen, Ihr neues Buch möglichst kompl ett durchzuarbeiten und zu vermeiden, mit Anfangswissen PrOjekte zu starten. Ich wünsche Ihnen viel Spaß mit Ihrem neue n Buch un d der Welt der objektorientierten Programmierung!
Martin Conrad
23
I Dieses Kapitel geht auf die Grundlagen der C++ -Programmienmg
bzw. auf die grundlegenden Themen der meisten Programmiersprachen überhaupt ein. Angefa ngen von den BaSisdatentypen, 5tandard-J/O-
Streams, Konstanten, lexikalischen Elementen, Operatoren, Begrenzern. verschiedenen Kontrollstrukturen, Funktion en und dem Präprozessor finden Sie hier vieles, was Sie in anderen Programmiersprachen recht ähnlich (oder fast gleich) wiederfinden. Natürlich soll hierbei auch kurz
auf die Geschichte \Ion C++ und die Frage, wie man eigentlich ein Program m erstellt. eingegangen werden.
1
Grundlagen
1.1
Di e Entstehung von C++
In
C++
Ursprünglich wurde e++ von Dr. Bjarne Stroustrup 1979 entwickelt. um Simulation sprojekte mi t geringem Speicher- und Zei tbedarf zu programmieren . Auch hier lieferte (wie schon bei () kein geringeres Betriebssystem als UNIX die Ursprungsplauform dieser Sprache. Stroustrup musste (derzeit noch bei den Bell Labs beschäftigt) den UN IX-Betriebssystemkern auf verteihe Programmierung analysieren. Für größere Projekte verwendete Strous lru p bisher die Sprache Simula - die allerdings in der Praxis recht langsam bei der Ausführung ist - und die Sprache BCPL, die zwar sehr schnell ist, aber sich fur große Projekte nicht eignete. Simul a gilt als Vorgänger von Smalltalk. Viele der mit Simula eingeführten Konzepte finden sich in modernen objektorientierten Programmiersprachen wieder. BCPl (Kurzform für: Basic Combined Programming Language) ist eine um 1967 von Martin Richard~ entwickelte, kompilierte, ~y~temnahe Programmier5prache, abgeleitet von der Combined/Cambrige Programming Language CPL. Es ist eine Sprache aus der ALGOL-Familie. Ho rn page vo n Stroustrup: Wer mehr über Bjarne Stroustrup erfahren will, findet unter der URL http: //pu blic. researc h.att.com/-bs/ ho rne page.ht ml seine Hompage.
Stroustrup erweiterte die Sprache C um ein Klassenkonzept, wofür er die Sprache Simula-67 (mit der Bildung von Klassen, Vererbung und dem Entwurf virtueller
'5
1
I
Grundlagen In C·....·
Funktionen) und später dann Algol68 (Überladen von Operatoren; Deklarationen im Quelltext frei platzierbar) sowie Ada (Entwicklung von Templates, Ausnahmebehandlung) als Vorlage nahm . Heraus kam ein »C mit Klassen .. (woraus etwas später au ch CH wurde). C wurde als Ursprung verwendet, weil diese Sprache schnellen Code erzeugte, einfach auf andere Plattformen zu portieren ist, fes ter Bestandteil von UN IX ist. Vor all em ist ( auch eine stark verbreitete Sprache, wenn nicht sogar d ie am stärksten verbreitete Sprache überhaupt. Natürlich wurde auch weiterhin auf die Kompatibilität von C geachtet, damit auch in C entwickelte Programme in CH-Programmen liefen. Insgesamt wurde »C mit Klassen.. zunächst um fo lgende Sprachelemente erweitert, auf die später eingegangen wi rd: ..
Klassen
..
Vererbung (ohne Polymorphismus)
..
Konstruktoren, Destruktoren
..
Funktionen
..
Friend·Deklaration
..
Typüberprüfung
Einige Zeit später, 1982, wurde aus >OC mit Klassen .. die Programmiersprache (H. Der Inkrementope rator ++ am Ende des ( sollte darauf hinweisen, dass die Programmiersprache C++ aus der Programmiersprache C entstanden ist und erweitert wurde. Folgende neue Features sind dann gegenüber "C mit Klassen.. hinzugekommen: ..
Virtuelle Funktionen
..
Überladen von Funktionen
..
Überladen von Operatoren
..
Referenzen
..
Konstanten
..
veränderbare Freispeicherverwahung
..
Verbesserte Typenüberprüfung
..
Kommentare mit 11 an das Zeilenende anfügen (von BCPL)
1985 erschien dann die Version 2.0 von CH, die wiederum fo lgende Neuerungen enthielt : .. Mehrfachvererbung ..
Abstrakte Klassen
Die Entstehung von C++
.. Stalische und konstante Elementfunktionen ..
Erweiterung des Schutzmodells um das Schlüsselwort protected
Relativ spät, 1991, fand das erste Treffen der ISO {International Organisation for Standardization)-Workgroup statt, um CH zu standardisieren, was 1995 zu einem »Draft Standard« führte . Draft Standard ist die Vorstufe zum Standard. Das Protokoll hat die Analyse- und Testphase bestanden. kann jedoch noch modifiziert werden .
Allerdings dauerte es wiederum drei weitere Jahre bis 1998 CH dann endlich von der ISO genormt wurde (lSO/IEC 14882:1998). Erweitert wurde C++ in dieser Zeit um folgende Feamres: .. Temp lates .. Ausnahmebehandlung .. Namensräume .. Neuartige Typumwandl ung ..
Boolesche Typen
Natürlich entstanden während der Zeit der Weiterentwicklung von CH auch eine Menge Standardbibliotheken wie bspw. die Stream- I/O-Bibliothek. die die bis dahin traditionelle n C-Funklionen pr int f {) und scanf() abgelöst haben. Ebenfalls eine gewaltige Standard bibliothek wurde von HP mi t STL (Standard Template Library) hinzugefUgt. Im Jahre 2003 wurde dann die erste überarbeitete Version von ISO/IEC 14882:1998 verabschiedet und ISO/IEC 14882:2003 eingeftihrt. Allerdings stellt diese Revision lediglich eine Verbesserung von ISO/IEC 14882:1998 dar und keine ~ne ue« Version. Eine neue Version von C++ (auch bekannt unter C++Ox bzw. CH200x) soll angeblich noch in diesem Jahrzehnt erscheinen - zumindest deu tet der Name dies an. 1.1.1
Aufbau
von
eH
CH ist im Gegensatz zu Sprachen wie Smalltalk oder Eiffel keine reine objektorientierte Sprache - genauer, Small talk bzw. EitTel wurden ohne Wenn und Aber als objektorientierte Sprachen entwickelt. CH hingegen entstand ja aus C womit es sich hierbei um eine Programmiersprache mit objektorientierter Unterstützung handelt, was zum eine n Nachteile. aber auch Vorteile mit sich bringt (Siehe "Stärke n von C++« und ~Sc hwächen von CH«).
27
I
1.1
I
1
I
Grundlagen in C++
[»]
Hinwe is Sofern Sie bereits mit C vertraut sind, können Sie die g leich fo lgenden Zeilen als .. Update« betrachten - eben als »was kommt alles Neues auf mich zu mit C++ (als C-U msteiger) zu «.
Erweiterungen vo n C
Neben der objektorientierten Unterstützung bietet CH auch sprachliche Erweiterungen von C an . Zu den bekanntesten Erweiterungen gehören folgende Schwerpunk te: ...
InHne- Funktionen
...
Defau lt-Argumente
..
Referenztypen
..
Überladen von Funktionen
... Überladen von Operatoren ... Templates ... Ausnahmebehandlung Objekt orientiert e Unterstützung
Die objekwrientierte Unterstützung von CH beinhaltet fo lgende Möglichkeiten: ..
Klassen bilden
..
Zugriff auf Daten und Elementfunk tionen mi t pub 1 i C-, pr i va te- oder prote c ted-Spezifikationen steuern
..
Klassen vererben (auch mehrfach)
..
Polymorphe Klassen bil den
St ärken von e ++
Wie bereits erwähnt, hat C++. wie jede andere Sprache auch, einige "schwache. und _starke« Seiten. Zu den Stärken von C++ gehören folgende Punkte: ..
Maschinennahes Programmieren
...
Erzeugung von hocheffizientem Code
..
Hohe Ausdrucksslärke und Flexibilläl
...
Für umfangreiche Projekte geeignet
... Sehr weite Verbreitung ...
,8
Keine Organisation (wie z. B. bei Java) hat hier die Finger mit im Spiel (Standard erfolgt durch die ISO)
Die Entstehung von C++
..
Viele Möglichkeiten für die Metaprogrammierung
.. C-kompatibel - Dadurch kann das Programm, das in C erstellt wurde, weiterhin unverändert verwendet werden. Außerdem braucht sich ein C-Programmierer beim Umstieg nur mit den Erweiterungen und der objektorientierten Programmierung auseinanderzusetzen. Schwächen von
e++
Die .. Schwächen" von ü+, die hier erwähnt werden, sind allerdings häufig auch schwächen, die viele andere Programm iersprachen auch aufweisen. ..
C-kompatibel - Wie schon erwähnt hat die Kompatibiltät zu C zwar Vorteile. aber leider muss C++ auch den Balast von C mitschleppen. Dies hat den Nachteil . dass somit einige Details der Sprache compilerspezifisch si nd. obwohl sie es nicht sein müssten. Dies erschwert die Portierung von C++-Programmen auf die verschiedenen Rechnertypen, Betriebssysteme und Compiler.
..
Kaum ein Compiler erfüllt die komplette Umsetzung der ISO-Norm (ein ähnliches Problem gi bt es auch mit C-Compilern und dem . C99-Standard«).
..
C++ gilt als relativ schwierig zu erlernen bzw .. es ist eine längere Einarbeitungszeit nötig.
..
Es gibt (noch) keine Standardbibliotheken zu viel benutzten Themen wie Multithreads, Socket-Prog rammierung und den Dateisystem-Verzeichnissen. Dies erschwert die Portabilität von C++ nochmals erheblich. Zwar wird hierbei auf viele externe Bibliotheken zu rückgegriffen, doch dies vermittelt wohl eher ein uneinheitliches und nicht ausgereiftes Bild von C++. Zwar exis tiert schon seit längerem eine Sammlung von Bibliotheken, die unter dem Namen .. Boost« zusammengefasst werden, aber diese Bibliotheken sind (noch) kein Standard.
Vergleich mit anderen Sprachen Der Vergleich verschiedener Programmiersprachen ist häufig unsinnig, da jede Programmiersprache generell als Mittel zum Zweck dient und somit auch ihre Vor- bzw. Nachteile hat. Somit will ich C++ nicht im Vergleich zu anderen Sprachen auf Vor- bzw. Nach teile prüfen. sondern eher auf Ähnlichkeiten. Die Programmiersprachen Java und C# (gesprochen .. C Sharp«) haben zum Beispiel eine ähnliche Syntax wie C++. Allerdings sind beide Sprachen .. intern« komplett anders aufgebaut und som it auch ganzlieh inkompatibel. Ganz unumstritten ist immer noch. dass C++ (mit seinem Ursprung C) eine der am häufigsten eingesetzte und geforderten Programmiersprache ist. In vielen
29
I
1.1
I
1
I
Grundlagen In C·....·
Bereichen kann man allerdings mit C# oder Java besser (bzw. auch schneller) ans Ziel kommen. Wenn C++ nicht die generische Programmierung beherrschen würde. so wäre die Sprache wohl stark rückläu fig.
1.2
Erste Schritte der C++-Programmierung
Zur Entwick lung eines C++-Programms sind im Grunde nur drei Dinge nötig, ein Editor, mit dem Sie die Textdatei mit dem Code erstellen, ein Compiler, der das Programm in die Masch inensprache des entsprechenden Rechne rs libersetzt. und ein Linker, der daraus eine ausfuhrbare Datei macht. Gewöhnlich werden die Quelldateien mit der Endung ».cpp" oder ».cc« gespeichert (auch mit ».C .. (großes C) und ».cxx" kommt g++ zurecht). Die Headerdateien bekommen entweder die Endung ».h«, lI .hpp .. oder überhaupt keine Endung.
[»]
Hinweis Da Textverarbeitungsprogramme wie »MS Word .. oder »Word Perfect« zusätzlichen Formatierungsbalast beim Speichern hinzufügen, eignen sich diese weniger zur Erstellung der Quelldatei. Sie benötigen auf jeden Fall einen Texteditor mit dem Sie ASCII-Dateien editieren kÖnnen.
Mit dem Compiler erzeugen Sie aus einer Quelldatei und den inkludierten Headerdateien eine Objektdalei
Der Linker bindet anschließend diese übjektdatei(en) zu einer ausfUhrbaren Datei. Diese Datei enthält neben den erzeugten Objektdateien auch den StartupCode und die Module mit den verwendeten Funktionen und Klassen der Standardbibliothek (Siehe Abbildung 1.1). Häufig wird zur Erzeugung eines Programms auch auf lDEs (Entwicklungsumge bungen) zurückgegriffen. Eine IDE enthält schlicht und einfach alle Werkzeuge wie den Editor, Compiler und Linker unter »einem Fenster.. . Neben den üblichen Werkzeugen zur Entwicklung enthalten solche Entwicklungsumgebungen noch eine ganze Menge mehr, wie zum Beispiel einen Debugger, Prot1l er oder einen Hexedi lor.
30
Erste Schritte der C++- Programmierung
Editor
iI.
• "• ,••
•"
00
•~
~
0' s<;'
i
•
! Comp iler
ggl. weHere Bibliothek (en)
- ,\
•0
\
"~
\ 11\11. weil.. ,e Objektdalelen
\
- - _\
~
\
••• 5" -. 0"
... 0;
-a ~
\
\
\
~ Linker
Aus!iihrbare Dal";
Abbi ldung 1.1 Vom Quellcode zur ausführbaren Datei
1.2.1
Ein Prog ramm erzeugen mit einem Ko mmandoze il enCompiler
Als Buchautor steht man immer vor der Frage, welchen Compiler soll man hier mi t aufnehmen bzw. beschreiben. Und neben de r Frage des Compilers kommt auch noch das Betriebssystem dazu - also kein leichtes Unterfangen, jeden Leser zufrieden zu stellen. Zum Glück macht es C++ mit ei nem Standard hier recht leicht. sodass die Beispiele im Buch nich t ab hängig vom System oder Com piler sind. Und wenn Dinge eingesetzt werden, die nicht dem Standard entsprechen, so finden Sie auch in diesem Buch für jedes System ei ne Lösung, um das Programm dennoch auszuführen. Aber darüber brauchen Sie sich zunächst nicht den Kopf zu zerbrechen .
3'
I
1.2
I
1
I
Grundlagen In C·....·
Zunächst müssen Sie den Quelhext in einen ASC II-Editor eintippen und mit der Endung » .cp p ~ oder ».cc" speichern. Natürlich werden wir hierzu das klassische »Hall o Welt ..-Programm verwenden. 11 hallo . cpp lIinclude
int main(void) 1 cout « "Hallo Welt!\n" : return 0: Ungeachtet dessen, was diese Zeilen bedeuten, soll hieraus ein ausführbares Programm erstellt werden. Hierzu müssen Sie lediglich den Compiler (und Linker) aufrufen. I »~ l
Hinweis Was ist mit dem Linkerlauf, mag sich manch einer fragen? Dieser wird in der Praxis automatisch mit ausgeführt, sofern dies nicht explizit anders gewollt (genauer, über Compiler-Flags angegeben) ist . GNU g++-Compiler
Der GNU g++-Compiler ist Bestandteil der freien GNU Compiler Collection (kurz GCC). Häufig wird dieser Compiler auch in integrienen Entwicklungsumgebungen wie zum Beispiel unter Linux in »KDevelop.. und »Anjuta .. oder unter Windows mit "Dev-C ++ ~ ausgcfuhn. Unter Windows ist der g++ auch als MinGWCompiler bekann t. Neben Linux und Windows ist g++ auch unter UNIX und MacOS X (kostenlos unter http://www.gou .org!) erhältlich . Folgendermaßen können Sie »hallo.cpp .. mit g++ in der Kommandozeile übersetzen: S g++ -Wall -0 hall o ha l l o.epp
Mit dem Schalter · 0 (outPUt) teil en Sie dem Compiler mit, dass die ausfllhrbare Datei den Namen »hallo .. haben soll . Mit ·Wall (Warnings all) schalten Sie alle Warnungen ein, wodurch der Compiler viele Extrahinweise mit ausgibt. »hallo.cpp .. lautet der Name der QueJldal.ei. die zu einer ausführbaren Datei übersetzt werdell soll . Microsoft Visual C++ (Kommandozeile) Der Microsoft Visual C++-Kommandozeilen-Compiler »cl .. ist Teil der Visua! Studio-Entwicklungsumgebung - er ist aber auch bei Microsoft kostenlos erhältlich (besser bekannt unter »Visual e++ 2003 Too lki t~ ). Allerdings enthält die kostenlose Version vom Microsoft-Compiler keine !DE. »hallo.cpp .. übersetzen Sie also mit dem Microsoft-Compiler wie folgt:
32
Erste Schritte der C++-Programmierung
C: ) cl /Fe hallo I Wall hal l o.cpp
Mit IFe (Fi le(name) executable; Name der EXE-Datei) teilen Sie dem Compiler mit, dass Sie das ausführbare Programm .. hallo.exe« nennen wollen. Mit I Wa 11 (Warnings all) schalte n Sie die Warn meldungen ein (optional). Auch hier gilt. »hallo.cpp .. ist der Name der Quell datei. aus der eine ausführba re Datei erstellt werden soll. Borlands Free Command Line Tools
Der Borland C++-Kommandozeilen-Compiler ist ebenfalls kostenlos für Windows erhältlich. Der Compiler ist ein freigegebener Bestandteil von Borlands C++-Compiler, nur ohne grafische Oberfl äche. Der Compiler ist bei vielen C++Aniangern sehr beliebt, sodass es mittlerweile einen Instal ler sowie zahlreiche 1DEs (bspw. VlDE) dafür gibt. Das »Hallo Welt,,-Programm übersetzen Sie mit Borlands Kommandozeile n-Compi ler wie folg t: C: ) bcc32 -w -tWC -e ha l lo hall o . cpp
Die Warnmeldungen schalten Sie mit der Option -w ein . Mit - t WC teilt man dem Compiler mit. dass man eine Konsolenapplikation erzeugen will. Eine »Konsolenapplikation" ist ein Programm, das die Standard-C++-AP1 und ein MS-DOS-Fenster für die Ein - und Ausgabe verwendet. Mit -e geben Sie an, dass ein Programm mit dem Namen »hallo.exe" erzeugt werden soll . »hallo.cpp" ist der Name der Quelldatei.
1.2.2
Ausführen des Programms
Ausfuhren könn en Sie das Programm mit einem Namen, den Si e in der Kommandozeile bei der entsprechenden Option angegeben haben. 1m Beispiel wurde hier immer der Name ~hallo« verwendet. Somit genügt ein einfaches "hallo« (gegebenenfalls auch »hallo.exe«) bzw. unter LinuxlUNIX »./hallo« in der Kommandozeile. um das Programm zu starten. Anschließend wird die Meldung "Hallo Welt! " auf dem Bildschirm ausgegeben . Natürlich wird davon ausgegangen. dass Sie sich im Augenblick im aktuellen Verzeichnis befinden. worin das Programm enthalten ist. 1.2_3
Ein Programm erzeugen mit einer IDE
Natü rlich ist die Erstellung und übersetzung mit ei ner integrierten Entwicklungsumgebung wesentlich komfortabler und häufig mit einigen Mausklicks schneller erledigt. Schließlich findet man alles . was man benötigt, auf einem »Fleck,,_ Allerdings gibt es mittlerweile eine Menge interessanter !DEs, sodass an dieser Stelle nich t darauf eingegangen wird.
33
I
1.2
I
1
I
Grundlagen in C++
[»]
Hinweis Wenn Sie ein einfaches C++-Programm mit so großen Entwicklungsumgebungen wie beispielsweise MS-Visual CH/CH übersetzen wollen, gleicht dies dem Prinzip, mit Kanonen auf Spatzen zu sChießen. Immer noch sind viele Anfänger verwundert, wenn ich Ihnen sage, dass Sie theoretisch alles ohne eine solche Entwicklungsumgebung programmieren können . Lassen Sie sich nicht verunsichern, wenn Ihnen jemand sagen will, Sie benötigen diese oder jene Entwicklungsumgebung, um Programme zu erstellen. Entwicklungsumgebungen können einem das Leben erheblich erleichtern, aber einem Anfänger kann solch eine Software schnell das (Programmierer-)Leben vermiesen.
[)]
Hinweis Da viele Leser ihre Programme gerne mit einer Entwicklungsumgebung erstellen wollen, finden Sie auf de r Buch-CD einige kurze Einfüh rungen zu den gängigsten Entwicklungsumgebungen.
1.3
Symbole von C++
Wenn man sich mit einer Sprache befassen muss/will, dan n sollte man sich auch mit den gültigen Symbolen auseinandersetzen . 1.3.1
Bezeichner
Den Begriff »Bezeichner« verwendet man fü r Namen von Objekten im Programm. Dazu gehören Variablen, Funktionen, Klassen usw. Ein gültiger Bezeichner darf aus beliebigen Buchstaben, Ziffern und dem Zeichen _ (Unterstrich) bestehen. Allerdings darf das erste Zeichen niemals eine Ziffer sein. Man so llte au ßerdem beachten. dass CH zwischen Groß- und Kleinbuchstaben (englisch: case sensitiv) unterscheidet. Somit sind »Hallo«, ,.hallo« und ,.HALLO« drei verschiedene Bezeichner. 1.3 .2
schlüsselwörter
Schlüsselwörter sind Bezeichner mit einer vorgegebenen Bedeutung in CH und dürfen nicht anderweitig verwendet werden. So dürfen Sie zum Beispiel keine Variable mit dem Bezeichner ,.int« verwenden, da es auch einen Basisdatentyp hierzu gibt. Der Compiler würde sich ohnehin darüber beschweren. Eine Liste der Schlüsselwörter in eH finden Sie im Anhang des Buches. 1.3 .3
literale
Als Uterale werden Za hlen, ZeichenkeUen und WahrheilSwerte im Quelllext bezeichnet. die ebenfalls nach einem bestimmten Muster aufgebaut sein müssen,
34
Symbole von
eH
I
1·3
das heiße Literale sind von ei ner Programmiersprache defi nierte Zeichenfolge n zur Darstellung der Werte von Basistypen. GanzzahJ en
Man untersch eidet bei Ganzzahlen zwischen Dezimal-, Oklal- und Hexadezimalzahlen, wofür folgen de Regeln gehen: ..
Dezimalzah len (Basis 10) - Eine Dezimalzahl besteht aus einer beli ebig langen Ziffern reihe aus den Zeichen 0 bis 9. Die erste Ziffer darf allerdings keine 0 sein.
..
Oktalzahlen (Basis 8) - Eine Oktalzahl hingegen beginn t imm er mi t einer O. gefolgt von einer Reihe von Oktalzahlen (0-7).
..
Hexadezimalzahlen (Basis 16) - Ein Hexadezimalzahl beginnt immer mit der Sequenz Ox bzw. OX gefolgt von einer Reihe von Hexadezimalzahlen (O-F = 0 12 34567 89A B C D E F(oder Kleinbuchstaben: abcdeO>. Hinweis Mehr zum jeweiligen Zahlensystem entnehmen Sie bitte dem Anhang dieses Buches.
Man kann hinter den Dezimal-, Oktal- und Hexadezimalzahlen noch ein Suffix an hä ngen, mit dem der Wertebereich einer Zahl genauer spezifiziert werden kann. Das Suffix u bzw. U deutet zum Beispiel an, dass es sich um eine vorzeichenlose (uns i gn ed) Zahl handelt. 1 bzw. l gibt an, dass es sich um eine 10ngZahl handelt. Hierzu einige Beispiele, die in de r Reihe immer gleichwertig si nd: D~llmallahl
Dktallahl
HexadezImalzahl
123
0173
Ox7B
1234567L
04553207L
OX12D6B7L
",
0102U
Ox42u
Tabelle 1.1
Beispie le tür gül tige Ganuahlen
Fließ kommazahlen
Wie eine korrekte Fließkommazahl dargestellt wird , wird in Abschnitt 1.4.6 genauer beschrieben, wenn es um die Basistypen von Fließkommazahlen geht. Wie bei den Ganzzahlen kann man den Fließkommazahlen ebenfalls ein Suffix hinzufugen . Mit dem Suffix f oder F kennzeichnet man eine Fließkommazahl mit einer ein fachen Genauigkei t. Das Suffix 1 oder L hingegen deutet auf eine Fließkommazahl mit erhöhter Genauigkeit hin.
35
[« ]
I
1
I
Grundlagen In C·....·
Einzelne Zeich en
Ein Zeichen-Literal wird zwischen einfache Hochkommata (Si ngle Quores) eingeschlossen (, A· •. B· .. C' , .... ' S', . & •• usw.). will man nicht druckbare Zeichen wie beispielsweise einen ,.Tabu lator.. oder "Ze il envorschub ~ darstell en. muss man eine Escape-Sequenz (auch Steuerzeichen genanm) verwenden. EscapeSequenzen werden mit einem Backslash (\ ) ei ngeleitet (bspw. Tabul ator", . \ t ' , oder neue Zeile", · \n ·). Mehr zu den Escape-Sequenzen erfahren Sie in Abschni tt 1.4.4. Zeichen ketten Eine Zeichenkette ist eine Sequenz von Zeichen. die zwischen doppelte Hochkommata (Double Quores) gestellt wird (bspw. "I ch bi n ei ne lei c h enkette ~). Sehr wich tig im Zusammenhang mit einer Zeichenkelte ist es zu wissen. dass jede dieser Ketten um ein Zeichen länger ist als (sichtbar) dargestellt. Gewöhnlich werden Zeichenkelten durch das Zeichen mit dem ASCII-Wert 0 (nicht d ie dez imale 0) abgeschlossen (OxOO oder als einzelnes Zeichen • \0 '). Diese ASCII·O kennzeichn et im mer das Ende einer Zeich enkette. Somit e nthält zum Beispiel die Zeichenkette "e++" vier Zeichen. weil am Ende auch das Zeichen OxOO (oder auch . \0 ') abgelegt ist.
1.3.4
Einfache Begrenzer
Um die Symbole voneinander zu trennen, benötigt man auch Begrenzer in C++. In diesem Abschnitt wird nur auf einfache Begrenzer hingewiesen. Weitere »Begrenzer.. werden Sie im Verlaufe des Buches näher kennen lernen . Das Semikolon
Der wichtigste Begrenzer dorfte das Semikolon : sein (Plural: Semikola und Sem ikolons. auch Strichpunkt genannt). Dieser dient als Abschluss einer Anweisung. Jeder Ausdruck, der mit einem solchen Semikolon endet, wird als Anweisung behandelt. Der Compiler weiß dann, hi er ist das Ende der Anweisung und f.ihrt nach d er Abarbeitung der Anweisung (Befehl) mit der nächsten Zeile bzw. Anweisung fort. Natürlich hat das Semikolon keine Wirkung. wenn es in einer Stringkonstante verwendet wird: "Ha ll o ; Welt " Komma
Mit dem Komma trennt man gewöhnlich die Argu mente einer Funktionsparameterliste oder bei der Deklaration mehrere Variablen desselben Ty ps.
Basisdatentypen
I
1·4
Geschweifte Klammern
Zwischen den geschweiften Klammern (eng!.: braces (ameri kanisch) oder curry brackets (bri tisch» wird der Anweisungsblock zusammengefasst. In diesem Block befinden sich alle Anweisungen (abgeschlossen mit einem Semikolon), die in einer Funktion ausgeführt werden sollen. Bei dem Usting »hallo.cpp" i nt main(voidl { co ut « "Ha l lo Welt!" ; retu r n 0; finden Sie alle Anweisungen der main{ )· Funktion zwischen den geschweiften Klammern zusammengefasst. Hier wird lediglich die Textfolge ,.Hallo Welt! .. auf dem Bildschirm ausgegeben und mit re t urn der Wert 0 an den aufrufenden Prozess zurückgegeben. Mehr zur mai n{ l-Funktion und dessen Rückgabewert erfa hren Sie e twas später. Das Gl eichhe itsze iche n
Mit dem Gleichheitszeichen trennt man die Variablendeklaration von den Inilia!isierungslisten oder bei Parameterlisten einer Fun ktion den Vorgabewert eines Parameters. Typ name - wer t: 11 ode r als vo rga bewert ei ne r Funkti on typ fu nction ( t yp name - we rt ) ; Hinweis Wenn Sie eine Variable initialisieren, so ist dies noch lange keine Zuweisung. Ist das nicht ein und dasselbe? Nicht ganz, zwar erfolgt die Zuweisung und die Initialisierung mit dem Gleichheitszeichen, aber zum einen handelt es sich ja hier um den Begrenzer - und zum zweiten um den Operator - - also um zwei verschiedene Dinge, die mit ein und demselben Zeichen einhergehen. Eine Initialisierung geht immer erst beim Anlegen einer Variable vor sich, während eine Zuweisung lediglich in Bezug auf ein bereits existierendes Objekt ausgeführt werden kann.
[« )
Hinweis Wenn Sie in einem Programm auf zwei aufeinanderfolgende -- stoßen, so handelt es sich hierbei nicht mehr um eine Zuweisung, sondern um eine Prüfung.
[« )
1.4
Basisdatentypen
Als Basisdatentypen werden einfache vordefinierte Datentypen bezeichnet. Dies umfasst in der Rege l den WahrheitSwert (boa 1), die Zahlen (; nt . s hort i nt, lan g l nt , floa t, dou ble und la ng double), d ie Zeichen (char, wc har_t ) und den (Nich IS-)Typ (vo; d).
37
I
1
I
Grundlagen In C·....·
1.4.1
Deklaration und Definition
Den etwas unbequemeren Abschnitt zuerst. Deklaration und Definition werden oft durcheinander geb racht oder auch als ein und dasselbe verwendet. Mit einer Deklaration machen Sie den Compiler mit einem Namen (Bezeichner) bekannt und verknüpfen diesen Namen mit einem Typ. Der Typ wiederum beinhaltet die Informationen über die Art der Bezeichner und bestimmt somit implizit die Akti· onen, die auf das Speicherobjekt zulässig sind. Bei einer Ganzzahl zum Beispiel sind die arithmetischen Operationen +, . , *, I usw. als Aktionen zuläss ig. Die Syn tax einer einfac hen Deklaration sieht somit immer wie folgt aus: Typ name : Typ namel . name2 . name3 :
Mit _Typ" geben Sie immer den Datentyp an, und _name" ist immer der Bezeichner. Natürlich können Sie. wie im zweiten Beispiel gesehen, auch mehrere Bezeichner eines Typs durch Kommata vo neinander trennen . Mit einer Deklaration geben Sie dem Compiler nur Informationen zu m Ty p bekannt. Bis dahin wurde noch keine Zeile Maschinencode erzeugt. geschwe ige denn ei n Speicherobjekt (Variable) angelegt. Für das konkrelC Speicherobjekl im Programm bzw. den ausfuhrbaren Code wird die Defini tion vereinbart. Somit ist jede Definition gleichzeitig auch eine Deklaration. Gleiches gih auch häufig andersrum. Die Deklaration einer Variable von Daten typ 1on9 in der folgenden Art ausgeführt 10n9 i :
gibt zum Beispiel den Namen des Speicherobjekts bekannt und vereinbart somit auch den Speicherplau für das Objekt. Ebenso kann der Name einer Variablen vereinbart werden, ohne dass ein Objekt erzeugt wird (Speicherklassenauribut exte rn). Damit kann es fü r jedes Objekt im Programm zwar beliebig viel Deklarationen geben, aber nur eine einzige Definition. [ ))]
Hinweis Im Gegensatz zu C können in C++ Deklarationen überall im Quelltext vorgenommen werden - worauf man allerdings, wenn möglich, wegen der Lesbarkeit des Codes verzichten sollte, indem man die Deklarationen noch vor den _arbeitenden« Anweisungen setzt.
1.4.2
Was ist eine Variable?
Eine Variable ist eine Stelle (Adresse) im Hauptspeicher (RAM), worin Sie einen Wert ablegen und gegebenenfalls spä ter wiede r darauf zurückgre ifen kön nen. Neben einer Adresse hat eine Variable auch einen Namen, genauer einen Bezeichner, mit dem man auf diesen Wert namentlich zugreifen kann. Und
Basisdatentypen
I
1·4
natürlich belegt eine Variable auch eine gewisse Größe des HauptSpeichers, was man mit dem Typ der Variablen mitteilt. Rein syntaktisch kann man das wie folgt betrachten : long lv ar; Hier haben Sie eine Variable mit dem Namen (Bezeichner) 1var vom Typ lo ng, der üblich erweise vier Bytes (auf 32 -Bit-Syslemen) im HauptSpeicher (RAM) belegt. Wo (Speicheradresse) im Arbe itSspeicher Speicherplau fur diese Variable reserviert wird - hier vier Bytes -, können Sie nicht beeinflussen .
1-4-3
Der Datentyp bool
Mit dem Datentyp boo 1 können Sie WahrheitSwen e beschreiben . Dabei kann bool die Werte t r ue (für " wahr«) und fal se (fOr »falsch«) ausdrücken. bool flag : f lag - t r ue
11 Schalter auf wahr se t ze n
flag - false 11 Schal t er auf fa l sch setze n Der Sinn dieses Datentyps wird genauer erläutere wenn es um Logikanweisungen geht. Hinweis Auch in C - wenn auch erst im C99-Standard (lSOIIEC 9899:1999) - gibt es mittlerweile einen Datentyp boo 1, nur dass dieser hier _Boo 1 lautet.
[« )
Hinweis 800lesche Variablen sind nach dem englischen Mathematiker George Boole benannt, der mit seiner Theorie der booleschen Algebra einen Grundstein für die formale Logik und die Rechentechnik legte.
[« )
1_4.4
Der Datentyp char
Der grundlegende Datentyp für Zeichen lautet char und belegt gewöhnlich ein By te an SpeicherplaLZ, was somi t meistens 2 8 = 256 Ausprägungen entSpricht. Allerdings muss ein Byte nich t zwangsläufig aus acht BitS bestehen. Es gab früh er auch Maschinen, die zum Beispiel neun Bi t als kleinsten adressierbaren Typ hatten. Des Weiteren gibt es zum Beispiel DSPs , bei denen ein Byte 32 Bit ist. Damit kann ein char auch von 2 31 •. 211 - 1 gehen. DSPs sind digitale Signalprozessoren und haben eine für rechenintensive Anwendungen optimierte Architektur. Sie können komplexe Algorithmen, wie sie bei der Verarbeitung digitalisierter Analogsignale notwendig sind, sehr schnell abarbeiten. Anwendungen ergeben sich in der Signalfilterung, Signalkodierung und -dekodierung, aber auch in der Bildverarbeitung.
39
I
1
I
[»]
Grundlagen in C++
Hinwe is Wie viel Bits ein char auf Ihrem SY5tem denn nun hat, ist im Makro CHAR_ BIT (in der Headerdatei alias <1 imits . h» definiert. Aber egal, wie viel Bits ein cha r dabei hat, ein si zeof( c ha r ) muss immer eins (ein Byte) ergeben! Im Zeichencyp cha r findet de r ko mplette ASCI I-Zeichensatz Platz. Da die ASCIIZeichen geordnet sind, kann man diese auch miteinander vergleichen. Natürlich kann man hierbei auch Umwandlungen von Zeichen zu Zahlen machen. Beispielsweise entsprich t das Zeichen' A' (Iam ASCII·Tabelle) dem Zahlenwert 65, ' B' entspricht 66 usw.
[» 1
Hinwe is Den Zeichentyp c har kann man zwar auch mit s i gned oder uns igned spezifizieren, aber man sollte beachten, dass char , un sig ned char und signed char drei verschiedene Typen sind! Des Weiteren hängt es von der Compiler-Implementierung ab, ob cha r auch negative Zahlen aufnehmen kann. Zeich en werden bei cha r zwischen einfach e Anführungsstriche eingeschlossen: char chI - ' A': cha r ch2 - 'B': Hierbei handelt es sich immer um Zeichenkonstanten . Natürlich können Sie auch, wie schon erwähnt, den entsprechenden ASCII-Zahlenwert verwenden. So entspricht die eben gezeigte Syntax den folgenden Zahlenwerten: char chI - 65 ; char chZ - 66 ;
// laut ASCII -T abelle das Zeichen ' A' // laut ASCII -Tabelle das Zeiche n ' B'
Hierzu ein einfaches Beispiel: 11 cha r 1.cpp llinclude using namespace std ;
i nt main(voidl char ehl char eh' char eh) char eh' CQVl
«
---
chI
'A· ; 'B · ; 68 ; I/laut ASCII -T abelle ' C' 69 : I/laut ASCII -Tabelle ' D'
«
ch2
«
ch3
«
retu r n 0 , Die Ausgabe des Programms lau tet: ABCD
40
ch 4
«
· \n ·;
Basisdatentypen
Neben den darstellbaren Zeichen können Sie auch Sonderzeichen (nicht druckbare Zeichen) bzw. Escape-Zeichen (oder auch Fluchtzeichen) verwenden, die mit einem Backslash (\) eingeleitet, aber als einzelnes Zeichen interpretien werden. So erreichen Sie mil dem Escape-Zeichen ' \n ', dass ein Zeilenvorschub "ausgegeben .. wird, oder mit' \ t ' wird das lITabulator«-Zeichen ausgegeben meistens wird dabei in der Zeile um acht Zeichen eingerückt (abhängig vom System bzw. der Shelleins tellung). Hienu ein überblick zu den nicht druckbaren Sleueneichen und deren Bedeutung (Tabelle 1.2): Steuerzeichen
Bedeutung
\a
BEL (bel{) - akustisches Warnsignal
\b
BS (backspace) - setzt Cursor um eine Position nach links
\f
FF Iformfeed) - ein Seitenvorschub wird ausgelöst. Wird hauptsächlich bei Programmen verwendet, mit denen Sie etwas ausdrucken können
\n
N L (newline) - Cursor geht zum Anfang der nächsten Zeile
\r
CR (carriage return) - Cursor springt zum Anfang der aktuellen Zeile
\t
HT (horizontal tab) - Zeilenvorschub zur nächsten horizontalen Tabulatorposition (meistens acht leerzeichen weiter)
\ y
VT (verticaJ tab) - Cursor springt zur nächsten vertikalen Tabul atorposition
\"
" wird ausgegeben
\'
, wird ausgegeben
\?
? wird ausgegeben
\\
\ wird ausgegeben
\0
Ist die Endmarkierung eines Strings
\nnn
Ausgabe eines Oktalwerts (z. B. \033 '" ESCAPE-Zeichen)
\xhh
Ausgabe eines Hexadezimalwerts
Tabelle
1.2
Steuerzeichen (Escape·Sequenzen) in Zeichen kon stanten
Hienu ebenfalls ein recht einfaches Beispiel: 11 char2 , Cpp llinclude us i ng namespace std :
int main(void) char chI - ' A': char ch2 - 'B ': char ch3 - ' \t ': char ch4 - '\n ': cout « ch I « eh3 re t urn 0 :
«
eh2
«
eh4
«
chI
«
eh2
«
ch4 ;
41
I
1·4
I
1
I
Grundlagen In C·....·
Die Ausgabe des Programms lautet: A
B
AB [)) ]
Hinweis Beachten Sie bitte, dass einzelne Zeichen bei C++ in einfache Anführungsstriche geschlossen werden, Zeichen ketten (Strings) hingegen von doppelten Anfüh rungsstrichen umschlossen werden.
Breite Zeichen - wchar_t char reich t fLir die englischen und westeuropäischen Sprachen (oder genauer ZeichenSälZe) zwar aus, aber für Sprachen die mehr Zeichen haben als man zu zählen vermag (z. B. Chinesisch mit mehreren tausend Zeichen), benötigt man den Datentyp wchar _t. mit dem _breite ... Zeichen ausgegeben werden können. Allerdings wird bei der Deklaration von .. breiten .. Zeichen noch vor dem Anführungszeichen das Präfix ,.L.. verwendet: wchar _t omega - L":
// g r iec hisches ' Z' (Qmegal
Zeichensatz nennt man die Zuordnung der alphanumerischen Zeichen zu einer Zahl. Die wichtigsten in der Informatik bekannten Zeichencodierungen sind der ASC JIund der EBCDIC-Code, insbesondere letzterer hat aber stark an Bedeutung verloren. Zunehmend in den Vordergrund getreten sind Zeichensätze mit international notwendigen Zeichen, die über das Englische hinausgehen, zum Beispiel diesbezügliche Zeichensätze gemäß ANSI , vor allem der international anerkannte Standard " Unicode.:.
1·4·5
Die Datentypen int
Der Datentyp fü r Ganzzahlen lautet int. Ein int hat somit laut Standard die natürliche Größe, die von der Ausführumgebung (e nglisch: Execution-Environment) vorgeschlagen wird, oder einfach die Größe eines Maschinenworts des entsprechenden Rechnersystems. Das wären dann zum Beispiel auf einer PDP10Maschine 36 Bit, auf einem Pentium 4 32 Bit und auf einem beliebigen 64-Bil' Prozessor-System 64 Bi t. POP ist eine Rechnerfamilie der Firma DEC. PDP steht als Abkürzung für Programmable Data ProceS50r oder Programmed Data Processor.
Auf 16-Bit-Systemen lässt sich hiermit in den zwei By tes (2 16) ein Zahlenbereich von -32768 bis +32767 beschreiben. Auf 32-Bit-Rechnern sind dies schon vier By tes, mit denen man einen Zahlenraum von (2 32) - 2147483648 bis +2147483647 verwenden kann. In der aktuellen 64-Bit-Generation bel egt ein int schon beachtliche acht Bytes, mit denen ein Zahlenbereich von
Basisdatentypen
I
1·4
- 9.223.372.036.854.755.807 bis +9.223.372.036.854.755.807 dargestell t werden kann. Hinweis Für 32-Bit-Rechner gibt es den Datentyp lang lang, mit dem sich ebenfalls dieser 64-Bit (acht Byte) breite Wert beschreiben lässt.
[« )
Hinweis Die Konstanten I NT_ MIN und I NT_ MAX der Headerdatei (in C auch bekannt als (1 imi ts . h» sind mit den Werten deklariert, die der Datentyp int auf Ihrem System besitzt.
[« ]
Hierzu ein einfaches Beispiel: /I intl.cpp flinclude flinclude us ing namespace std ;
i nt main(voidl I in t wertl - 10 ; in t wert2 - 20 ; cout« "we rt ! : " « wertl« ' \n ': cout « "we rt 2 : " « wert2 « ' \n ': cout « " int·Zahlen bere i ch von " « I NT_HIN « " bis " « INT_HAX « retu r n 0 :
' \ n ':
Die Ausgabe des Programms lautet (hier auf eine m Pentium 4 (32 Bit)): werti : 10 wer t2 : 20 i nt · Zahl enbe r eic h von - 214748]648 bis +21474B]647
Die int-Typen long und short Ebenfalls zur Familie der i nt-Typen gehören die Datentypen sho rt und long . Hinweis Um das hier gleich richtig zu stellen, die korrekten Typennamen heißen eigentlich short int und 10ng i nt . In der Praxis werden üblicherweise short und 1ong verwendet.
[« J
Wie man am Namen schon ablesen kann , h andelt es sich um eine kurze (short) und eine lange ( 1ong) Version von i nt. Hinweis Die Größe der Typen i nt, short und 1ong sind nicht festgelegt. i nt weist die Größe eines Maschinenworts auf und ist mindestens so groß wie s hor t . Der Datentyp 1ong hingegen hat mindestens die Ausdehnung eines i nt.
43
[« ]
I
1
I
Grundlagen In C·....·
Der Datenty p short hat somit mindestens den Wertebereich von char und maximal den von einem i nt. Gewöhnlich kann man mit short (2 Bytes) einen Wertebereich von -32768 bis +32767 (oder 0 bis 65535) beschreiben (216) . Der Daten ty p 10ng hingegen wird gewöhnlich verwend et. wenn der Zahlen bereich von i nt zu ge ring isc 1ong deckt mindestens den Bereich eines i nt ab. Auf 16- und 32-Bit-Rechnern kann man mit 10ng so mit immer einen Zahl enbereich von -2147483648 bis +2147483647 (bzw. 0 bis 4294967296) beschreiben (2 3 2). Natürlich soll hier nicht der Datentyp 10ng 10ng vergessen werden. Bei den neueren C++-Compilern ist dieser Datentyp gewöhnlich schon integriert und deckt mit seinen acht Bytes Breite einen Zah lenbereich von -9.223.372.036.854.755.807 bis +9.223.372.036.854. 755.807 ab (2 64 ). signed und unsigned
Jeden dieser i nt-Typen können Sie noch zusätzlich mit si gned bzw. uns i gned spezifizieren . Zwar weisen beide Spezifizierungen dieselbe Ausdehnung auf. aber die Typen verfügen über ei nen anderen Wertebereich. Abgesehen viell eicht von cha r (compilerabhängig) ist s i gned die Voreinstellung der Datentypen, mit der auf eine explizite Angabe verzichtet werden könnte. Folgende Schreibweise hätte somit dieselbe Bedeutung, wobei die zweite Variante die Lesbarkeit eines Quelltexts erhöhen kann: i nt a : s i gned int a :
11 11
gleich wie : s i gned int a gleic h wie : int a
Hierzu ein Überblick, wie sich das Schlüsselwort si gned bzw. uns i gned auf den Wertebereich eines 32-Bit-Systems auswirkt (Tabelle 1.3). Name
Wertebereich
ellar , slgned char
- 128 .. . +127 bzw. 0 ... 255 (compilerabhängig)
unsl gned char
0 .. .255
short , slgned short
-32768 .. +32767
unslgned short
0 ... 65535
Int, slgned Int
- 2147483648 .. +2147483648
unsl gned Int
0 .. .4294967295
long , slgned long
- 2147483648 ... +21 47483648
unslgned 10ng
0 .. .4294967295
Tabell e 1.] Wertebereich in Verbindung mit signed und unsigned
44
Basisdatentypen
1.4 .6
I
1·4
Gleitkomm azahlen float, double und long double
Für reelle Zahlen wird de r Basis-Fließkommadatentyp f1 aat verwendet. Dargestellt werden solche Zah len durch eine ganze Zahl, die Mantisse, und den Exponenl. de r die Lage des Dezimalpunkts fes tlegt (Genaueres dazu gle ich). Mantisse ist ein lateinischer Begriff und die mathematische Bezeichnung für die Nach kom mastelien. Die Genauigkeit der Gleitkommadatentype n ergibt sich aus der "Mantisse« und ist abhängig vom Compiler. Wenn d ie Genauigkeit von f l aat nicht mehr reicht. greift man auf doubl e zu rück. daub I e weist mindestens die Genauigkeit von fl aat auf. Wenn dou b1e auch nicht mehr ausreich t, verwendet man den Typ I ong doub I e - der w iederum mindestens die Genauigkeit von doubI e aufweist. Beach ten Sie bitte, dass C++ standa rdmäßig kein Dezimalkomma verwendet. wie Sie es gewöh nt sind. sondern einen Dezimalpunkt. Dies hat allerdings eher her· kunftsspezifische Gründ e der Programmierung. fl oat a - 1.5 : fl oat b - 5. 1:
11 Komma ist falsch 11 ric hti g
We nn einer der Werle vor oder hinte r dem Dezimalpunkt 0 ist. beispielsweise 0.5 oder 1.0 , dann können Sie die Ziffer 0 auch weg lassen. Der Compiler macht hieraus automatisch eine O. float c - . 5; float d - 5 .:
11 entspric ht 0 . 5 /1 entspric ht 5 . 0
Natürlich könne n Sie für ei ne Fließkommazahl auch eine n Exponenten verwende n. Beispielsweise ist 1.2e34 die Abkürzung fü r 1.2 . 1034 . Hin weis Theoretisch ist es möglich , ein Komma anstatt eines Dezimalpunkts für Gleitpunktzahlen zu verwenden. Hierzu kann man ein I oca l e-Objekt von der gleichnamigen Headerdatei verwenden. Mit eine m soichen Objekt können Sie die Sprachumgebung für das laufende Program m festlegen bzw. ändern. Auch das Mische n von Integer und Gleitkommazahl ist erlaubt und möglich. Ist dabei der Divisor oder der Dividend eine Gleitkommazahl. wi rd automatisch eine Gleitkommadivis ion durchgeftihrt - also wird bei der Zuwe isung einer Gleitkommazahl automatisch der Integer zu einer Gleitkom mazahl konverlierl. Ein Beispiel : 11 fl aatl.cpp lIinc lu de
45
[« J
I
1
I
Grundlagen In C·....·
i o t main(voidl I f 10at a - 3. 0 ; f 10at b - 2. 0 ; i nt c - 3, i nt d - 2, cout « "3 . 0 I cout « "3 I cou t « "3 . 0 I I cout « "3
2.0 2 2 2.0 -
« alb « « cld « « ald « « c/b «
· \ 0' ; · \ 0' ;
· \n ' ; • \n ' ;
retu r n 0 ,
Das Programm bei der Ausführung: 3. 0 3 3.0 3
I I I I
2.0 2 2 2 .0
- 1. 5 - I
- 1.5 - 1.5
Es ist natürlich auch erlaubt, einen Integerausdruck einer Gleitkom mavariablen zuzuweisen. Dabei wird der Integerausdruck automatisch zu einer Gle itkommavariablen konvertiert. Das Gleiche gilt auch fur die andere Richtung, nur dass Gleitkommazahlen, die an In teger zugewiesen werden, ab dem Dezimatpunkt abgeschni tten werden. Rechnen mit Gleitkommazah len
Vielleich t verwirrt Sie das Fragezeichen in dieser Überschrift. Computer sind doch die besten Rechner. und ich wage es hier. dies in Frage zu stellen. Der Umgang mit lntegern ist fLir Computer kein allzu großes Problem. aber Gleitkommazahlen? [ )}]
Hinweis Ziel dieses Abschnitts ist es keineswegs. Ihnen nahezulegen. Gleitkomm azahlen nicht in der Praxis zu verwenden. sondern ich will Sie nu r auf einige Probleme hinweisen. Der Programmierer sind Sie selbst un d somit lastet die Verantwortung auch auf Ihren Schultern.
Zunächst rnüchw ich Ihm:1l da:; Glt:Hkommafo rrllat
bt'~chrt'ibCIl. da~
aus einCIIl
Vorzeichen, einem Dezimalbruch und einem Exponenten besteht.
Zunächst finden Sie mit +- das Vorzeichen, gefolgt vom Dezimalbruch mit vier Stellen f.fff und am Ende den Exponent mit einer Stelle (..~). Die Zahlen werden gewöhnlich im E-Formal (+-f.fffp·' ) geschrieben. Zwar hat das IEEE das Gleitkommaformat standardis iert. aber leide r halten sich nicht alle Computer daran.
Basisdatentypen
I
1·4
So wird die Zahl 1.0 wie im E-Format mit +1.000E+0 beschrieben oder -0.006321 mit -6.321 E-3 und die 0 .0 mit +O.OOOE+O. Soweit, so gut. Wenn Sie beispielsweise 2/(, + 2/(, rechnen, kommen Sie wohl auf das Ergebnis 4/ 6 . Richtig rur Sie. aber hier fangt das Dil emma Gleitkommazahlen und Rundungsfehler schon an. 2/(, ist im E-Formal gleich +3.333E-l. Addieren Sie nun +3.333E-l mit +3.333E-l erhalten Sie als Ergebnis +6.666E-1 (bzw.0.6666). Gut, aber leider fa lsch, denn 4/(, sind im E-Format +6.667E-l aber nicht wie berechnet +6.666E-1 . Mit derartigen Rundungsfehlern haben viele Computer ihre Probleme . Daher schon der erste Ratschlag. Sollten Sie eine Software entw ickeln. die mit Geldbeträgen arbeitet. und dabei auf keine anderen Bibliotheken bzw. BCDArithmetiken zurückgreifen wollen/können. dann sollten Sie die Beträge niemals im Gleitkommaformat verwenden (hierzulande also niemals in Euro und Cents. wie z. B. 1,99 t). Hier empfiehlt es sich. zur Berechnung die Geldbeträge in Cents als Integerzahl anzugeben, da bei immer intensiveren Berechnungen Rundungsfe hler gemacht werden und somit eine falsche Berechnung garan tiert ist. Man kann nicht mal ganz genau sagen. wie genau eine solche gebrochene Zahl ist. weil dies von der Art der Berechnung abhängt. Beispielsweise führen Subtraktionen mehrerer ähnlicher Zahlen zu einem ungenaueren Ergebnis . Um die Genauigkeit von fl oat zu erhöhen, können Sie zum Beispiel doub I e verwenden. dam it erhalten Sie häufig die doppelte Genauigkei t. Auf vielen Rechnern hat f lOal eine Genauigkeit von mindestens sechs Dezimalziffern und doubl e häufig eine 15-stellige Genauigkeit. Stellt man doub 1e noch das Schlüsselwort long voran, erhalten Sie gewöhn lich mindestens eine 19-slell ige (auf HPUX-Systemen (eine UN IX-Version von Hew[eu Packard) gar eine 33-s tellige) Genauigkeit. Allerdings sind diese Angaben immer co mp ilerabhäng ig und die Rundungsfehler können trotzdem noch auftreten. Um sich also mit den Problemen der Fließkommazahlen auseinanderzusetzen müssen Sie sich mit Themen wie numerischer Analyse oder BCD-Arithmetik befassen. Allerdings sind dies Themen, die weit über dieses Buch hinausgehen würden. Hinweis BCD steht für Binary Coded Dezimal5 und bedeutet, dass die Zahlen nicht binar, sondern als Zeichen gespeichert we rden. Beispielsweise wi rd der Wert 56 nic ht wie gewöhnlich als Bitfolge 0011 1000 gespeichert. sondern als die Werte der Ziffern im jeweiligen Zeichensatz. In unse rem Fall dem ASC II -Code-Zeichensatz. Und dabei hat das Zeichen ,.5« de n Wert 53 und das Zeichen . 6. den Wert 54. Somit ergibt sich folgende BitsteIlung: 00 11 0101 (53) 0011 0 110 (54). Damit benötigt der Wert 53 allerdings 16 anstatt der möglichen acht Bit. Für die Zahl 12345 hingegen
47
[« )
I
1
I
Grundlagen in C++
benötigen Sie schon 40 Bits. Es wird zwar erheblich mehr Speicherplatz verwendet, doch wenn Sie nur die Grundrechenarten für eine Ziffer implementieren, können Sie mit dieser Methode im Prinzip unendlich lange Zahlen bearbeiten. Es gibt keinen Genauigkei tsverl ust.
Limits für Ganzzahl - und Gleitpun ktdat entypen
In Abschniu 1.4.5 (beim LiSling ,. int1. 0 :) haben Sie bereilS erfahren, dass es für die einzelnen Datentypen eine Headerdatei gibt. wo Sie die minimalen bzw. maximalen Werte auf dem laufenden System abfragen können. Die Limits für die Ganzzahltypen befinden sich in der Headerdatei (unter C bekannt als <1 i m; ts .h » . Voraussetzung, um die einzelnen Werte in dieser Headerdatei abzufragen, ist natürlich, dass Sie diese Headerdatei mi t einbinden (Tabelle 1.4). l
Erklarung
CHAR_BIT
Bitzahl für ein Byte
SCHAR_HIN
min. si gned cha r
SCHAR_MAX
mal(. si gned char
UCHAR_MAX CHAR_MIN
m~.
min. char
CHAR_MAX
m~.
char
m~.
Byte für ein Viel-Bytezeiehen
uns 1gned char
MB_LEN_MAX SHRT_MIN
min. short lnt
SHRT_MAX
mal(. short ln t
USHRT_MAX INTj1IN INT_MAX
min.
UINT_MAX
m~.
LON~M IN
min. 10ng Int
LONG_MAX ULONG_MAX
m~.
10ng Int
m~.
unslgned 10ng Int
mal(. unslgned short
m~.
,0' '0' uns I gned I nt
Tabelle 1.4 Limit-Konstanten für ganzzahlige Datentypen in
Die Limitwerte für die Gleitkommazahlen finden Sie in der Headerdatei (cf 1oa t > (un ter C bekannt als (f 1oa t. h» . Darin finden sich alle Konstan ten und Eigenschaften, die für die Fließkommazahlen von Bedeutung sind. Natürlich müssen Sie hierbei auch die Headerdaw j im Programm mh ei nbinden , wenn Sie einzelne Werte abfragen wollen (Tabelle 1.5).
48
Konstanten
I(onstant~
Bed~utung
FlT_RADIX
Basis für Exponentendarstellung
FlT_MANT_DIG
Anzahl MantissensteIlen ( float )
DBL_MANT_DIG
Anzahl MantissensteIlen (doub I e)
LDBl_MMIT_DIG
Anzahl MantissensteIlen (1ong doubl e)
FlT_DIG
Genauigkeit in Dezimalziffern ( float )
DBL_DIG
Genauigkeit in Dezimalziffern (double)
LDBL_DI G
Genauigkeit in Dezimalziffern (I ong double)
FlT_MIN_EXP
Minimalster negativer Fl T_RADI X-Exponent ( fl Od t )
DBL_MIN _EXP
Minimalster negativer Fl T_RADI X-E)tponent (doub I e)
LDBL_MHCEXP
Minimalster negativer FL T_RAD! X-Exponent (l ong doub 1e)
FLT_MIN_IO_EX P
Minimalster negativer Zehnerexponent (f 1oat)
DBL_MIN 10 EXP
Minimalster negativer Zehnerexponent (double)
LDBL_MI N_l O_EX P
Minimalster negativer Zehnerexponent (l ong doub I e)
FlT_MAX_EXP
Maximaler Fl T_RAD I X-Exponent (f I oat)
DBL_MAX_EXP
Maximaler FL T_RAD I X-Exponent (doub I e)
LDBL_MAX_EXP
Ma)timaler FL T_RAD I X-E)tponent (long doub I e)
FLT_MAX 10 EX P
Maximaler Zehnerexponent ( f! Od t)
DBL_MAX_I O_EXP
Maximaler Zehnere)tponent (d oub 1e)
LDBL_MAX 10 EX P
Maximaler Zehnere)tponent (long double)
FLCMAX
Maximaler Gleitpunktwert ( fl oa t )
OBL_MAX
Maximaler Gleitpunktwert (dou bI e)
LDBL_MAX
Ma)timaler Gleitpunktwert (1ong doubl e)
FLT_E PSI LDN
Kleinster fl oat -Wert x, für den 1.0 + )t ungleich 1.0 gilt
OBL_E PSILDN
Kleinster doub! e-Wert x, für den 1.0 + x ungleich 1.0 gilt
LDBl_EPS I lDN
Kleinster I ong doub I e-Wert x, für den 1.0 + x ungleich 1.0 gilt
FLCMIN
Minimalster normalisierter Gleitpunktwert (f loat )
OBL_MIN
Minimalster normalisierter Gleitpunktwert (doub 1e)
LDBL_MIN
Minimalster normalisierter Gleitpunktwert (l ong doub I e)
Tabelle \.5
1.5
Limit-Konstanten für Gleitpunkt- Datentypen in «f1oat>
Konstanten
Benöligt man einen unveränderbaren Wen, können Sie eine Konstame verwenden. Der Sinn und Zweck einer solchen KonSlame ist es, dass der Wen zur Laufzeit des Programms nich t mehr verändert werden kann. Eine solche Konstante
49
I
1·5
I
1
I
Grundlagen In C·....·
können Sie definieren, wenn Sie vor den eigentl ichen Daten typ das Schlüsselwort cons t setzen: const int var - 365 ; const char dquote - "'; const flo a t pi - 3 . 141592 ;
I1 In t egerkonstante JI Zeichenkonstante 11 Fl ießkommakonstante
Wenn nun aus Versehen versuch t wird, den Wert dieser Konstante zu verändern, gibt der Compiler zur Obersetzungszeit einen Fehler aus, da es sich bei dieser Variablen um eine »read only.. (nur lesbare) Variable handelt. Somit können cons t-Werte auf gewöhnlichem Wege der direklen Zuweisu ng n icht mehr verändert werden. In der Praxis werde n Konstanten mit const recht häu fig bei Variablen, Objekten, Zeigern, Parameter von Funktionen usw. verwendet, also immer dann , wenn ein Wert nicht mehr verändert werden darf.
[»]
Hinweis Wer sich mit der C-Programmierung beschäftigt hat, hat häufig auch Konstanten mit lide fine erzeugt. leider werden diese Konstanten noch vor dem Compilerlauf vom Präprozessor durch den entsprechenden Text ersetzt. Das Problem dabei ist, dass der Ersetzungsvorgang nicht während der C++-Ü bersetzung stattfindet und somit die Syntax- und Typenprüfung umgeht. Wo sich doch gerade C++ gegenüber C mit einer verbesserten Typenprüfung rü hmt, sollte man in C++ die Verwendung von lidefine tur Konstanten ganz unterlassen und stattdessen const dafür verwenden.
1.6
Standard Ein-/ Ausgabe-Streams
In den vorangegangenen Beispielen wurde mehrfach COLJt zur Ausgabe auf dem Bildschirm verwendet, ohne je näher darauf eingegangen zu sein. Daher folgt jetzt eine Einfüh rung in die Ein-/Ausgabe-Streams von C++. Wer bereits Kenntnisse in C gesammelt hat, dürfte die Funktionen scanf() zur Eingabe und pr i nt f ( ) zur Ausgabe auf dem Bildschirm kennen. Neben den Ein/Ausgabefunktionen von C verlugt C++ über ein neues Ein-/Ausgabesystem, das (natürlich) auf der Basis von Klassen aufbaut. Aber keine Sorge, da bisher noch nicht auf die Klassen e ingegangen wurde, benötigen Sie hi erüber noch kei ne Kenntn isse, um diese Streams zu verstehen. Auf die Klassenhierarchie wird erst viel spät er bei der Erklärung der iostream-Bibliothek eingegangen.
[»]
Hinweis Wer sich für die Ein -/Ausgabefunktion en von C (u nd der Programmiersprache C überhaupt) und deren Verwendung interessiert, die Sie ja auch in C++ verwenden können, dem sei mein 8uch . C von Abis Z" empfohlen, das Sie auch online unter http://www.pronix.de/oder auf der Buch-CD find en
50
Standard Ein-/Ausgabe-Streams
I
1.6
Auch wenn man in C++ (fas t) alles aus der C-Welt verwenden kann , so hat doch das neue ,. St rea m- Konzept ~ (basierend auf Klassen) von C++ einen erheblichen Vorteil. nämli ch Typensicherheit. Probleme. wie ei n fals ch es Formatzeichen bei der Ausgabe von printf() . die zu mitunter gefahrlichen Laufzei tfeh lern geftihrt haben, gibt es mi t den neuen Streams nicht mehr. Hierbei entscheidet nun der Compiler anhand des Argumenttyps. welche Ein- bzw. Ausgabefunktion aufzurufen ist. 1.6.1
Die neuen Streams - cout, cin, cerr, clog
Die neuen Streams sind in der Headerdatei fo lgendermaßen deklariert (siehe Abbil du ng 1.2): ~
COut - Standardausgabe (basiert auf dem C-Standard-Stream s tdou t)
~
cerr - Standardfehle rausgabe (basiert auf dem C-Standard-Stream s tde r r)
~
clog - Standardfehlerausgabe (wie ce r r nur gepuffen)
~
ci n - Standardeingabe (basiert auf dem C-Standard-Stream stdi n) cout
0
1\
ein
Tastatur
.
Programm
Ab bildung 1.2 Ein- und Ausgabe-Streams in
~"
clog
•
.
•
DI \ Bildschirm
eH
Hinweis Auch wenn die neuen Streams auf de n C-Standard-Streams basie ren. sollten man möglichst nicht die »al ten« C-Streams mit de n neuen Streams mischen.
[« l
Hinweis Diese Streams basieren alle auf dem Datentyp char. Wollen Sie _breite« Zeichen (basierend auf wehar _tl ausgeben. stehen Ihnen die Streams wout. werr . wlog und wi n zur Verfügung.
[« )
Damit Sie die in der C++-Standardbibliothek defi nierten globalen Bezeichner zum Namensbereich s td direkt verwenden kön nen, milssen Sie di e Direktive
us i ng namespace st d: angeben (gewöhnlich am Anfang der Datei hin ter der Angabe der Headerdateien). Ohne Angabe des Namenshereichs std könnten Sie anstatt COLlt
«
" Hallo Welt!\n ";
5'
I
1
I
Grundlagen In C·....·
nur mit dem Namensbereich s td und dem Bereichsoperator über dem Stream cout machen: s t d :: cou t «
eine Ausgabe
"Hallo We lt ! \ n";
Auf den Namensbereich und den Bereichsoperator wird allerdings erst in einem späteren Abschnitt eingegangen. Ausgabe mit cout
1.6.2
Die Ausgabe mit cout wurde schon des öfteren hier verwendet. Damit die Aus· gabe realisiert wird, wurde der Operator « (Shift·Operator) _überladen .. , wodurch er für die Speicherobjekte dieser Klasse einen neuen Sinn bekommt (zum Überladen auch später mehr). cout
«
"Hallo Welt!\n" ;
Mit solch einer Anweisung schieben Sie den String . Hallo Welt!« in die Standardausgabe (cout alias stdout). Mit dem Operator « zeigen Sie an, wohin der Text geschoben wird. Natürlich können Sie dahinter noch einen weiteren Text ~schie ben« lassen: 11 Bspw . e i nen Text Ober zwei Ze i len cout « "Ha ll o Welt!\n" « "Noch e i ne Zei l e\n ";
Sie können sich dies gerne wie bei einer Laufschrift vorstellen. Abgesch lossen wi rd die Ausgabe mit einem Semikolon . Bei den Datentypen muss man hier nicht mehr ( -ty pisch auf das richtige Forma tzeichen achten, sondern hier ist cout clever genug, den Typ dieser Daten selbst zu erkennen, wie Sie dies bereits im Abschnitt zu den Basisdatentypen selbs t fes tstellen konnten. cout kann also alle ele menta ren Datentypen ausgeben.
1.6.3
Ausgabe mit cerr
ce rr entspricht im Grunde demselben wie cou t und sendet seine Ausgabe auch auf den Bildschirm. Gewöhn lich verwendet man den Fehlerausgabe-Stream cer r, wenn man den Standardausgabe-Stream cout umgeleitet hat, oder wenn man nur Fehler in eine Datei leiten will. Nur so kann man sicherstellen, dass nur noch die Fehlermeldungen auf dem Bil dschirm ausgegeben werden. Entscheidend ist auch zu wissen, dass cerr ungepuffert arbeitet. also die Ausgabe hier ungepuffert erfolgL Aus Effizi enzgründen ist die Ausgabe auf cout gewöhnlich gepuffert, das bedeutet, dass das System erst mal x Zeichen zwischenspeichert und diese dann alle auf einmal ausgegeben werden .
52
Standard Ein-/Ausgabe-Streams
Hinweis Man kann die Ausgabe aber mittels (1 us h bzw. eau t . ( 1ush ( ) erzwingen. 1.6 .4
Ein gabe mit cin
wollen Sie etwas von der Standardeingabe (Tastatur) einlesen, so erfolgt dies mit ein und dem Operator » . Ein Beispiel hierfür: 11 einl . cpp lIinelude us i ng namespace s t d :
int mai n(voi d l 1 float wert : cou t « "B itte ei n Fließ kommaz ahl : ": ein» wer t : cout « "Die Ei ngabe wa r " « we r t « ' \ n': re t urn 0: Das Prog ramm bei der Ausiuhrung: Bit te ein Fl ieBkommazahl Di e Eingabe war 5 . 5
5. 5
Der Wen wird hierbei von e in in die Variable "wert« geschoben. Auch hierbei entfallen Fehler, die mit fa lschen Formatelementen bei s ca n f ( ) schnell unterlaufen konnten. Am Typ der Variablen legt ci n nämlich selbständig fest, was akze ptiert wird und wie die Konvertierung erfolgt. Beim Einlesen mit e i n und dem Operator » gelten außerdem folgende Regeln: ...
Die Verarbeitung der Eingabe bricht an der Stelle ab, wo das erste Zeichen nicht mehr verarbeitet werden kann. Geben Sie im Listing ,.ön1.cpp« als Wert ,. 5.5 Euro« an, so wird in der Variablen ~ wert« nur ~5.5 .. gespeichert.
...
Führende Leerraumzeichen wie ein Tabulator, Newline oder ein Whitespace werden überlesen . Befindet sich also vo r dem eigentlichen Wert ein solches ",Zeichen«, so wird der Wert dennoch richtig eingelesen.
In der Praxis sollten Sie sich aber nicht darauf verlassen. dass der An wender schon das Richtige eingeben wird. Hierzu sollte man im mer den Rückgabewert von e i n und dem Operator » überprüfen. Bei einer richtigen Angabe wird "'wahr« und bei einer falschen Angaben ~ fal sch . zurückgegeben. Auf das Beispiel "önl.cpp« bezogen sieht eine solche Überprüfung wie folgt aus: if (
! (e i n» we r t) ) ( ce r r « "Fehler bei der Ei ngabe!\n" ;
53
I
1.6
[« J
I
1
I
Grundlagen In C·....·
el se I eou t
«
"Di e Eingabe war "
«
we r t
«
"\n ";
Auf das i (·e l se-Konstrukt wird später noch eingegangen, sofern Ihnen das noch nicht bekannt ist Einlesen von Zeichen und Zeichen kette
Wozu einen Extraabschni tt hierzu, werden Sie sich frage n? Weil es ein Problem sein kann, wenn beim Einlesen einzelner Zeichen ein fa hrendes Leerzeichen eingegeben wurde und dies tatsächlich so gemeint war. Sie wissenja, dass führende Leerzeichen vom Operawr » ignoriert werden. Wenn Sie also eine Eingabe zeichen weise einlesen wollen - also mit den fa hrenden Leerraumzeichen, dann benötigen Sie die cin-Methode get() (gleichwertig zur (-Anweisung getcha r { J) . 11 cin2 . c pp llinclude us i ng namespace std ;
int main( voidl { i nt wert ; (out « "Bitte ein Zeic hen wert - ein.get() : (ou t « "Die Ei ngabe war return 0;
.. " «
(char )we r t
«
' \n ';
Es nHlt auf, dass c i n mit der Methode get{ ) als Rückgabewert ein i nt zurückgibL Dies muss so sein, weil ci n . ge t{ ) den Wen EOF rur Daleiende od~ r einen Feh· ler, zurückgibt. EOF kann auch mit der Tastenkombination ~ + I]] (unter Windows/DOS) und ~ + [[) (unter Linux/UNIX) ausgelöst werden. Daher muss, sofern Sie nicht den ASCII-Wert des eingegebenen Zeichen haben wollen, der Wert mit (haI' "gecastet« werden. Mehr zum Typencasting in e inem gesonderten Abschnitt. [ ))]
Hinweis für Anfänger: Leider lässt es sich oft nicht vermeiden, das ein oder andere Thema hier schon mit einzubeziehen, das e~t in einem späteren Abschnitt behandelt wird. Allerdings sollte Sie das nicht entmutigen, da hierauf noch zu gegebener Zeit eingegangen wird.
Hier haben Sie ein klassisches Grundgerüst (Listing: cin3 .cpp), wie diese häufig bei einem FHterprogramm eingesetzt wird, das zeichenweise Text abarbeiten (oder gar verschlüsseln) soll.
54
Operatoren
11 cin3 . CPP llinclude
int main{void) int we r t : 11 EüF kann mit STRG+Z bzw. STRG+D ausgelös t werden whil e l (wert - cin . geU» !- EOF ) I co ut « (char)we rt: re turn 0: Wenn Sie dieses Beispiel ausführen, haben Sie im Grunde nichts anderes als einen Papagei, der alle Zeichen (beim Betätigen von B ) ausgibt, die ei ngegeben wurden . Ein ähnliches Problem entsteht übrigens auch, wenn Sie versuchen, einen String mit ci n einzulesen. Befindet sich hier ein Zwischenraumzeichen. wie beispielsweise ~C ++ von Abis 2« , so befinden sich im String nur die Zeichen ~ C++ • . Das gle iche Verhalten kennen die ( -Programmierer von der Funktion scan f ( l. Auch hier hat ci n eine weitere Methode. wie schon beim Einlesen einzelner Zeichen mit ge t ( ), und zwar die Methode get 1i ne ( ). Da auf die Strings noch gar nicht eingegangen wurde. nur schnell ein Codeausschniu dazu. wie die Methode get 1i ne( ) in der Praxis verwendet wird. cha r buc h[20J : cin . getli ne( buch . 20 ) ; Beim Aufruf der Methode get l ine() müssen Sie auch die Größe mit angeben. Auf das Thema Strings wird noch in eine m späteren Abschnitt (7.1) eingegangen.
1.7
Operatoren
Damit Sie anschließend nicht ins Straucheln geraten. sollen hier einige Begriffe zu den Operatoren im Voraus erläutert werden. Zunächst unterscheidet man Operatoren anhand der Anzahl ihrer Operanden: ..
Unärer Operator - dieser Operator ha t einen Operanden
..
Binärer Operator - dieser Operator hat zwei Operanden
.. Ternärer Operator - dieser Operator hat drei Operanden
55
I
1·7
I
1
I
Grundlagen In C·....·
In der Praxis werden Sie vorwiegend mit unären und binären Operatoren zu tun haben. Allerdings gibt es in CH mit ? : auch einen ternären Operator, aber dazu später mehr. Neben der Anzahl von Operanden unterscheidet man auch die Position des rators. Dabei verwendet man gewöhnlich drei verschiedene Begriffe: ..
Präfix - der Operator steht vor dem Operanden
..
Poslfix - der Operator steht hinter dem Operanden
..
Inflx - der Operator steht zwischen den Operanden
Op e~
Zum Sch luss werden die Operatoren auch noch nach der Assoziativi tät differen· ziert. Als Assoziativität wird die Auswertungsreihenfolge bezeichnet, in der Operanden in einem Ausdruck ausgewertet werden. Dabei gibt es folgende Assoziativitäten der Operatoren: ..
lin ksassoziativität
..
Rechtsassoziativität
Der Großteil der Operatoren in C++ istlinksassoziativ. Das bedeutet. dass zum Beispiel bei folgendem Ausdruck varl + var2 - var3 ; zuerst varl mit var2 addiert und die Summe var3 anschließend von der Summe subtrahiert wird . Wären die Operatoren rechtsassoziativ, würde zuerst var2 mit var3 subtrahiert und danach erst mit varl addiert. Ist dies erwünscht, müssen Klammern gesetzt werden: varl + (var2 - var3) : 1.7 .1
Arithmet ische Operatoren
Folgende arithmetische Operatoren sind in CH vorhanden (Tabelle 1.6): Operator
,
Bedeutung Addiert zwei Werte (va r I +va r2) Subtrahiert zwei Werte ( va rl · va rZ) Multipliziert zwei Werte ( varl ' var2 )
I
Dividiert zwei Werte ( varl/var2)
%
Modulo (Rest einer Division) (var1 1var2)
Tabell e '\.6
Arithme t ische Operatoren in CH
Operatoren
I
1·7
Es gelten für arithmetische Operatoren fol gende (üblichen mathematischen) Regeln: ..
Die klassische Punkt-vor-Strich-Regelung - * und 1 binden also stärker als + und -. In der Praxis heißt dies: ,.5 + 5" 5. ergibt ,.30. und nicht, wie eventuell erwartet, ,. 50 •. Wenn zuerst ,.5 + 5. berechnet w erden soll, verwenden Sie Klammern. Diese binden dann stärker als die Rechenzeichen , also »(5 + 5) .. 5 = 50 •.
..
Arithmetische Operatoren sind binäre Operatoren und haben so mit immer zwe i Operanden, also . Hinweis Grundsätzlich sollten Sie eine Division durch 0 vermeiden. Dies gilt sowohl für eine Division mit 1 also auch für den Modulooperator 't . Der Programmabsturz ist Ihnen hierbei garantiert.
Hierzu ein recht ei nfaches Beispiel, das die arithmetischen Operatoren be i ihrer Anwe ndung in einem C++-Programm demonstriert, 11 a r ith1.cpp lIinclude using namespaee std :
int main(voidl I int va r l, va r 2 , var3 : eou t « ~Ope r and I : ein» va r l : eou t « ~Ope r and 2 : ein» va r 2 : 11 Berechnung direkt in cout cout « "Multipl ika t ion : • « varl « « va r 2 « " .. " « (varl*var2)
" «
11 Berechnung in var3 zw;schenspeichern var3 - varl + var2 : cout « "Addition • « ~ arl « " « va r 2 « " - " « var3 « ' \n ':
Di vi sion durch 0 vermeiden (!var2) ?var2-1 : var2-va r 2 : 11 Berechnun g direk t in cout cout « "Divis i on • « vor3 « « va r 2 « " - " « (var3/var2) :
.. ' \n ':
I
"
11
"
1 •
57
[« )
I
1
I
Grundlage n In C·....·
11 Den Rest der Division ermitteln cout « " (Rest . « (var]%varZ)
«
" )\n" :
11 Neuen Wert von varl zuweisen var1 - var] - varZ : COut « "Subt raktion " « var] « " « va rZ « • - " « va r1 « ' \n ': return 0:
Das Programm bei der Ausführung:
Operand 1: a Operand 2: 3 Mult i plikation: Add i tion Di vi sion Subtra kt i on
8 * 3 - 24 8 + 3-11 11 I 3 - 3 (Rest 11 - 3-8
2)
Ein Anmerkung noch zur Zeile
( !varZ) ?va r Z-1 : varZ-va rZ: Hierbei ( ? :) handelt es sich um den einzigen ternären Operato r in C++, der sich wie folgt beschreiben lässt:
(Ausd ruck) 1 Anwe i sungl : Anweisung2 In Wonen: Ist »Ausdruck« wahr, wird die »Anweisung1 . ausgeführt, ansonsten die »Anweisung2«. Hierauf wird aber noch genauer eingegangen. Der Sinn dieser Ze ile ist es letztendlich zu vermeiden, dass Sie eine Division durch 0 ausfUhren. Wenn also der Ausdruck »var2. fa lsch ist ( ! = Negationsoperator), bekommt »var2« den Wert 1, ansonsten bleibt alles beim Alten. Allerdings wird auf diese n Operator noch extra eingegangen. Die arithmetischen Operatoren können Sie außerdem noch in einer erweiterten Darstellung verwenden (Tabelle 1.7). Erweiterte Darstellung
Bedeutung
+-
v~
r l +- v~ r2 ist gleichwertig zu
v~
r I - v~ r 1+ v~ r2
va r 1 · -va r2 ist gleichwertig zu va r I-va r 1· va r2
'-
v~
1-
va r li-va r2 ist gleichwertig zu va r I-va r 11 va r2
,Tabe ll e 1.7
SB
r l · - v~ r2 ist gleichwertig zu
v~
r I-v~ r 1*va r2
va r I :t·va r2 ist gleichwertig zu va rl-var I :tva r2
Erweiterte Darstellung arithmetischer Operatoren
Operatoren
Diese Schreibweise mit dem Operator und darauf fo lgendem - ist somit eigemlieh nur eine kürzere Schreibweise und hat ansonsten gegenüber der üblichen Verwendung der arithmetischen Operatoren keine nennenswerten Vor- bzw. Nachteile. 1.7.2
Inkrement- und Dekrementoperator
Beim Inkrement- bzw. DekrememoperalOr handelte es sich um unäre Operatoren mit nur einem Operanden . Die Operatoren machen nichts anderes als den Wert einer Variablen um 1 zu erhöhen bzw. zu reduzieren. Beide Operatoren haben ihr Haupteinsatzgebiet in Schleifen. In CH werden diese Operatoren wie folgt beschrieben (Tabelle 1.8): Operator
Bedeutung Inkrement (Variable um 1 erh öhen) Dekrement (Variable um 1 verringern)
Tabelle 1.8
Inkrement- und Dekrementoperator
Zur Verwendung der Operatoren gibt es zwei verschiedene Schreibweisen. In der Praxis ist es von Bedeutung, welche man davon verwendet (siehe Tabelle 1.9). Verwendung
Bezeichnung
v~r ++
Postfix-Schreibweise
++ ~~r
Präfix-Schreibweise
v~r-
Postfix-Schreibweise
.
'~ ~r
Tabelle 1.9
Präfix-Schreibweise Pos1fix- und Präfixsc hreib weisen
Folgende Umerschiede gibt es zwischen der Postfix- und der Präfix-Schreibweise: ..
Die pos tfix-Schreibweise erhöht bzw. erniedrigt den Wert von va r, gibt aber noch den allen Wert an den aktuellen Ausdruck weit er.
..
Die Präfix-Schreibweise erhöht bzw. erniedrigt den Wert von var und gibt diesen Wert sofort an den aktuellen Ausdruck weiter.
Hierzu ein einfac hes Listing, das die beiden Operatoren im Ein satz demonstriert: // incr1.cpp Ifi nclude
59
I
1·7
I
1
I
Grund lage n In C·....·
i nt main(voidl I int var- l ; 11 Inkrementope r ator cout « "var- " « vu « ' \n ': var++ : « '\n' : cout « "var- " « « "va r « var++ « ' \n' : cout '\n' ; « "var" « « cOut cout « "var- " « ++var « " \n\n" :
.
'" '"
11 var-l
1/ 1/ 1/ 1/
var-2 var- 2 va r-3 va r - 4
11 analog dazu mit dem Dekreme nt operator var · . : « ' \n ' : cout « "v ar- " « 1/ va r- 3 cOut « "var- " « var · . « '\n' : 1/ va r- 3 « ' \n ' : cOut « "var- " « 1/ va r- 2 cout « "var- " « ·· var « ' \n ' : 1/ va r- l return 0,
'" '"
Das Programm bei der Ausftlhrung: va r - l var - 2 va r - 2 var- 3 va r - 4 va r - 3 va r - 3 va r - 2 var-l
1.7.3
[»]
Bi toperatore n
Hinweis Der Abschnitt setzt voraus, dass de r Leser mit dem Dualsystem (bzw.
Binärsystem) zur Speicherung vertraut ist. Mehr zum Dualsystem finden Sie im Anhang des Buchs.
Wenn auf die binäre Darstellung von (Ganz-)Zahlen zugegriffen werden muss, dann kann dies mithilfe der Bi loperatoren gemacht werden. Folgende Bitoperatoren werden dazu angeboten (Tabelle 1.10): 81toperator
8edeutung
&. &-
Bitweise AND-Verknüpfung
Tabelle 1.10
60
Übersicht der bitweisen Operatoren
Operatoren
BItoperator
Bedeutung
I· 1-
Bitweise OR- Ve rknopfung (inkl.) Bitweise XOR (exkl.) Bitweises Komplement
» .
»-
Rechtsverschi ebu ng
« . «Tabelle 1.1 0
linksverschiebung Übersicht der bi tweisen Operatoren (Forts.)
Sie finden hier neben der üblichen Schreibweise auch die erweiterte Zuweisungsschre ibweise wieder. Natürlich sollte es klar sein, dass d ie Operatoren nur auf Ganzzahlen und nicht auf Gleitkommatypen anzuwenden sind. Bitweises UND Steht der So-O perator zwischen zwei Operan de n, so handelt es sich um den bi tweisen UND-Operator. Dieser wird gewöhnlich dazu verwendet. einzelne Bits gezie lt zu löschen: i nt var - 55 : 'IM - va r &. 7 :
Nach der Durchfüh rung dieser Bitoperatio n beinhaltet war'" den Wert 7. Um dies zu verstehen, sollten Sie die Regeln des bitweisen UND-O perators kennen ffabelle 1.1 1): var1&var2
var1
var2
o
o
o
o
o o
,
o
, ,
Tabell e 1.11
Regeln einer bitweisen UND-Verlm üpfung
Es werden all e Bits gelöschl, wenn nicht beide Operanden gesetzt sind . Sehen wir uns. dazu die interne Bitdars.tell ung der Berech nung von oben an (als 16-BilDarstellung) und verwenden hierzu die entsprechende Tabelle 1.10:
0000 0000 0011 0111 0000 0000 0000 0111
[ 55 J
0000 0000 0000 0111
7 ]
[
7
J
61
I
1·7
I
1
I
Grundlagen In C·....·
Bitweises ODER Um geziell ei nzelne BilS zu setzen, wird der bilweise ODER-Operalor verwendet. Für den ODER-Operator gehen folgende Regeln in der Verknüpfungstabelle (Tabelle 1.12): (BltAIBltB)
o o
o
1
o
1 Tabell e 1.12
o
1 Regeln einer bitweisen ODER-Verknüpfung
In der Praxis sieht die Verwendung des bitweisen ODER-Operators wie fo lgt aus:
i nt var - 1: va r - var I 126 : Wendel man hierauf die Verknüpfungslabelle des bilweisen ODER-Operators an, komm t man folgend ermaßen zum Ergebn is 127:
0000 0000 0000 0001 00000000 011 1 1110
[1] [126]
00000000 011 1 1111
[127]
Bitweises XOR
Dieser exklusive ODER-Operator liefen nur dann 1 zurück. wenn beide Bits unterschiedlich sind. Dies wird gewöhnlich dazu verwendet, einzelne BilS umzuschalten - gesetzte BilS werden gelöscht und gelöschte gesetzt. Für die XOR·Yerknüpfungen gelten folgende Regeln (Tabelle 1.13):
o
o
o
1
o
o 1 Tabelle 1.13
1 Regeln einer bitweisen XOR-Verknüpfung
Auch hi erzu ein einfaches Beispiel: int va r - 20 ; var - var ~ 55 :
6,
o
Operatoren
Wenden wir dazu die Regeln der Verknüpfungstabelle an, erhalten Sie als Ergeb· nis 35: 0000 0000 0001 0100 0000 0000 0011 0111
[20 [55
0000 0000 0010 0011
[35
Bitweises Komplement
Der NOT-Operator H wirkt sich auf Zahlen so aus, dass er jedes einzelne Bit invertiert. Bei vorzeichenbehafteten Datentypen en tspricht das einer Negation mit anschließender Subt raktion von 1: int va r - 20 : x - - x:
I! )(.- - 21
Für den NOT-Operator gilt fo lgende Verknüpfungstabelle (Tabelle 1.14):
o
o Tabe1\e
'1.14
Regeln einer bitweisen NOT-verknüpfung
Links- bzw. Rechtsverschiebung
Bei einer Linksverschiebung mit « werden alle Bits einer Zahl um n-Stellen nach links gerückt. Der auf der rechten Seite entstehende leere Teil wird mit 0 aufgefüllt. Aber Vorsicht, wenn der Datentyp s i gned ist, dann ändert sich das Vorzeichen, wenn eine 1 in die BitsteIle des Vorzeichens gerückt wird. Wenn der linke Operand eine n negativen Wert hatte, ist das Ergebnis compilerabhängig. Solche Linksverschiebungen werden gerne verwendet, um Za hlen zu potenzieren, den n rein mathematisch bedeutet eine solche Linksverschiebung eine Multiplikation mit 2. Bei Einrückung um zwei Stellen nach links wird mit 4 multipliziert , bei drei mit 8, bei vier mit 16 usw. Solche Bitverschiebungen laufen erheblich schneller ab als normale arithmetische Berechnungen im Stil von 4*x. Allerdings sind die meisten Compiler heute selbst schlau genug, dies gegebenenfalls auch selbst zu tun. Die Rechtsverschiebung mit dem »-Operator ist das Gegenstück zur Linksverschiebung (<< ). Damit können Sie statt einer Multiplikation mit 2 eine Division durch 2 bewirken. Ansonsten gilt dasselbe wie ruf die Linksverschiebung.
I
1·7
I
1
I
Grundlagen In C·....·
1.7.4
W eitere Operat o re n
Natürlich gibt es noch ein e Menge weiterer Operatoren, die zu gegebenem Ze itpunkt behandel t werden. Vorab ein kurzer Überblick zu den weiteren Operato· ren (Tabelle 1.15). Operator
Bedeutung logisches Nicht logisches Und
"
logisches Oder (inkU
,11
Kleiner
)
Größer Gleichheit
!-
Ungleichheit
<-
Kleiner-Gleich
) -
Größer-Gleich
Tabelle 1.15
1.8
Weitere Operatoren im Überblick
Kommentare
Mit Kommentaren können Sie Ihren C++·Quelltext ein wenig beschreiben und lesbarer machen. Der Compiler ignoriert solche Kommentare und entfernt diese bei der Übersetzung vom Quelltext in die Maschinensprache. Sie müssen sich also keine Gedanken machen, dass ein viel kommentierter Quelltext den Umfang des ausführbaren Programms erhöht. Es stehen Ihnen in C++ zwe i Möglichkeiten zur Verfügung, den Quelltext zu kommentieren: ..
Kommentare in einer Zeil e werden mit der Zeilenfolge 11 eingleitel. Alles, was dahinter in dieser einen Zeile geschrieben wi rd, wird nun vo m Compiler ignoriert.
..
Kommentare über mehrere Zeilen werden zwischen der Zeichenfolgen 1* und * 1 eingesch lossen. Das heißt, der Kommentar beginnt an der Ste lle im Quellcode. wo die Zeichenfolge I~ steh t und endet bei der Zeichenfolge ~/ . Diese Methode, Kommentare über mehrere Zeilen zu verwenden, wird gerne benutzt. um einen kompl etten Block Quellcode lOauszukom mentieren«.
Was Sie kommentieren , bleibt Ihnen se lbst überlassen. Allerdings sollte man nicht jede Zeile Code kommentieren oder Teile. die ohnehin klar sind. Wenn Sie versuchen, einen schwe r verständlichen Code zu kommentieren, überlegen Sie vorher, ob es nicht vielleicht möglich ist, den Code zu vereinfachen.
64
KontrolJslrukturen
1.9
Kontrollstrukturen
Bisher sind die Programme immer nur sequenziell abgelaufen - also immer Zeile für Zeile. In CH haben Sie folgende drei Möglich keiten, diesen sequenziell en Programm fluss zu verändern: ..
Verzweigungen (oder auch Selektionen)
..
Schlei fen (oder auch Iterationen)
..
Sprunganweisung
1.9.1
Verzweigungen (Selektionen)
Mit Verzweigung können Sie den weileren Ablauf des Programms von bestimmten Zuständen abhängig machen. Dabei wird im Programm eine Bedingung definiert, die entscheidet, an welcher Stelle das Programm fortgesetzt werden soll. Die if ... else-Anweisung
Bei der i f-Anweisung handelt es sich um eine einfache Verzweigung. Hier die Syntax:
if ( Bedingung) { Anweisung(en) : Beim Eintreten in die i f-Anweisung wird zuerst die »Bedingung« überprüft. Wenn die Bedingung wahr (ungleich 0 oder auch true) ist, werden die »Anweisung(en)« im darauf folgenden Anweisungsblock ausgefuhrt, der durch geschweifte Klammern begrenzt wird, sofern er mehr als eine Zeile beinhaltet. Wenn die Bedingung falsch ist (fal se), wird hinter dem Anweisungsblock mit der Programmmausiuhru ng fortgefahren. Hierzu ein einfaches Beispiel. Sie werden aufgefordert, zwei Ganzzahlen einzugeben, mi t denen anschließend eine Division durchgeführt wird. Zunächst wird bei der Eingabe jeder Zahl überprüft, ob überhaupt eine gültige Zahl eingegeben wurde. Falls die Eingabe fa lsch war, gibt die Bedingung false (also unwahr/falsch) zurück. Anschließend wird noch überprüft, ob der Wert der Variablen »var2« gleich 0 ist - was bei einer Division niemals der Fall sein darf. Ist der Wert der Variab len »var2« gleich 0, ist die i f-Anweisung wahr. In allen Fällen, in denen sich die Bedingung als wahr erweisen sollte, wird das Programm mit exi t und dem Rückgabewert 1 beendel (natürlich mil entsprechender Ausgabe).
65
I
1·9
I
1
I
Grundlagen In C·....·
it (Bedingun g •• wahf)
Nein
An"""isung("n)
Abbildung 1.3
Programmablaufplan zur If-Anweisung
11 ifl . cpp Ihnclude
i nt main(vo i dl I int varl , var2 ; cout « "Bi tte ei ne Zahl ". if ( (ein » varll -- f alse ) cerr « " Fehler bei der Eingabe ! \n~ e:dt(l) :
cout
:
« "Bitte den Teile r eingeoen : ":
if { (c i n » va r 21 -- false ) 1 cerr « "Fehle r be i der Eingabel\n" : ex i tl I) :
if(var2-- 011 cerr « "Teile r darf nich t 0 sein!\n" : ex il ( I) : cout « "Ergebnis der Division " « varl « " I • «va r 2« " « (varllvar21 « ' \n ': return 0:
Das Programm bei der Ausführung: Bi tte eine Zahl : 10 Bi tte den Teiler eingeben
66
a
KontrolJslrukturen
Fehler bei der Eingabe ! Bit te eine Zah l : 10 Bi tte den Teiler eingeben Teiler darf nicht 0 sein!
0
Bit te eine Zah l : 10 Bitte den Teiler eingeben : 2 Ergebnis der Div i sio n 10 1 2 - 5 Jeder i f -Anweisung kann optional auch ein el se-Zweig folgen . Dieser Teil wird immer dann ausgeführt, wenn d ie Auswertung der i f- Bedingung fa lsch (fa l se) war. Die Syntax dazu: if ( Bedingung) I Anweisung(en) ; el se I Anweisung(en) ; Allerdings kann el se niema ls fur sich alleine stehen - else fo lgt immer e inem vorausgehenden i f.
if (Beängungl
~~
wah r)
Nein
Anweisu ng (en )
Abbildung 1.4
Programmablaufplan mit else
Ein einfaches Beisp iel hierzu: 11 if2 . Cpp #include
Anwllisung(lIn)
I
1·9
I
1
I
Grundlagen In C·....·
us iog oamespaee std ; i ot mai o(void) I iot var : cout « " [0) Ich will zu i f\n ": eou t « "[1 - 9] Ich will zu else\n ": eou t « "\nIhre Wahl bitte if( (e i n» var) -- f alse ) I eerr « "Fehler bei der Eingabe l \n" : exit(}) ; i f( var cout
0
« "Du hast i f gew
el se I
cou t
« "Du has t ni ch t [ 0] gewahlt daher else\n" ;
re t urn 0; Das Programm bei der Ausführung: [0 ]
Ich will zu if
(1 - 9] Ich will zu else
Ihre Wahl bitt e 0 Du hast i f gewahlt! [0 ]
Ich will
'" bi tte '"
[ 1- 9] Ich will
Ihre Wahl Du has t nicht
[0 ]
if
else 4
gewa hlt daher else
Im Beispiel wurden die Blockanweisungen um den i f - und e I se-Zweig vet"\'lendel:
if (var -- Oll cout « "Du has t if gew
e I se I co ut
«
"Du hast nicht [ 0) gewahlt daher else\n " ;
Besteh t der jeweilige Zweig aus nur einer Anweisung. wie in diesem Beispiel. so ist der Anweisungsblock nicht unbedingt erforderlich. Also könnte man diesen Zweig auch wie folgt angeben:
68
KontrolJslrukturen
if(var cout else co ut
«
0) "Du hast if gew3h l t l \n ";
«
"Du hast nicht [0] gew
Rein ,.maschinell " sind beide identisch - also hat keine der beiden Methoden einen programmtechnischen Vorteil . All erdings ist die Variante mit den Blockanweisungen einfacher verständlich und lesbarer. Außerdem birgt die Methode ohne die Anweisungsblöcke die Gefahr, Anweisungen an der fa lschen Stelle zu schreiben und som it Fehler zu machen : if (
'"
cout else cout cout
«
0 ) · 0, hast if gew
« «
· 0, has t nic ht [0 J gew
Wo gehört nun die Anweisung mit der Ausgabe ~ Wo gehöre ich hin? .. hin? Rein programmiertechnisch gehört diese Anweisung nicht zum else-Zwe ig, aber war dies vom Programmierer auch so beabSichtigt? Anweisungsblöcke können in solch einem Fall Klarheit schaffen. Eine andere gut lesbare Möglichkeit wäre, die Anweisung direkt hinter di e Bedingung zu schreiben: i f (va r--O) cout « "Du hast i f gewahltl\n" : else cout « "Du hast ni cht (OJ gewahlt daher else\o" : (out « "Wo gehOre ich wohl hi n?\n" ; Wenn Sie mehrere Bedingungen überprüfen wollen/müssen, können Sie auf el se i f zuruckgreifen. Die Syntax: i f ( Bedingung) I Anweisung(e n) : el se if ( Bedingung) I Anweisung(en) ; 11 Optional e l se I Anweisung(en) :
Oplional können Sie natürlich auch noch einen else-Zweig hinzuftigen. Hierbei wird überprüft, ob die Bedingung der i f-Anweisung wahr (t rue) ist. Wenn ja, wird de r else i f-Zweig nicht mehr ausgeführt. Ansons ten , wenn die Bedingung der i f -Anweisung fa lsch (fal se) ist, wird die Bedingung des el se i f -Zweigs ausgewertet. Trifft die Bedingung des else ; f-Zweigs zu (wahr/t r ue), so werden die
69
I
1·9
I
1
I
Grundlagen In C·....·
Anweisungen des entsprechenden Anweisungsblocks ausgeführt. Trifft keine der Bedingungen zu, wird das Programm hinter den Verzweigungen weite rgeführt. Wenn eine else-Verzweigung vorhanden ist, so werden die Anweisungen der el se-Verzweigung ausgeführt (und anschließend wird mit der Programmausführung hinte r den Verzweigungen fo rtgefahren). Natürlich können hierbei mehrere solch er el s e if-Verzweigungen folgen, wobei man in der Praxis oft auf die swi te h-Anweisung zurückgreift.
Nei n
A ~ rog ( a n)
iI (Bed iogung2 __ wa r..)
Arrweisu rog(an)
Abbildung 1.5 Programmablaufplan if ... el se if
Hierzu das Programm . if.2.cpp« - erweiten mit else i f -Zweigen: /I i f3 . cpp lI i nclude us i ng namespace std ;
i nt main ( voi d ) I i nt va r: cout « "[0] cou t « "[1] cou t « " [2) cout
«
cout
«
I eh I eh I eh " [3-9) Ich
wi 11 wi 11 wi 11 wi ll
i f\n " ; " else ( 1 ) \ n' ; " else ifi f ( 2)\ n"; "zu c l s c\n " :
" \nl hr e Wa hl bit t e if( (c i n » va r) -- f alse ) ( cerr « "Fe hler bei der Eingabe ! \n " ; ex i t( 1) ;
i f( var
cout
70
«
oI
I
"Du hast i f gewa hlt ! \ n";
KootrolJslruktureo
e l se i f ( var - 1 ) I cout « "Du hast else if (1) gewahlt\ n"; e l se i f ( var - 2) f cout « "Du hast else if (2) gewahlt\ n"; e l se I cout
«
"Du has t nicht [0 . 1. 2] gewahlt daher else\n" :
re t urn 0: Das Programm bei der Ausführung: [OJ [lJ [2J [3 - 9J
Ich Ich Ich Ich
wi 11 wi 11 wi 11 wi 11
if '" else '" else '" else '"
if (1) if (2)
Ihre Wahl bitte , 2 Du hast else if (2) gewahlt i f -Anweisungen kö nnen natürlich auch »verschachtelt .. werden - worauf man allerdings. wenn möglich. der Übersich tlichkeit halber verzichten soll te. Hier ein Ausschnitt vom Listing »iG.cpp .. . nur »verschachtelter.. : if( (ein» va r ) ) I
if(var cout
«
Oll "Du hast if gewahlt!\o" :
el se i f ( var -- 1 ) I (out « "Du hast else if (1) gewahlt\n ": else i f cout
«
{ "Du hast else if (2) gewahlt\n" :
el se { cout
«
"Du hast nicht [0 . 1. 2] gewahlt - ) else\n" :
else cerr
«
el( it( 1) :
(var=~ 2)
" Fehler bei der Eingabe ! \n ":
I
1·9
I
1
I
Grundlagen In C·....·
Immer noch lesbar, sagen Sie? Das Ganze kann man natürlich noch weiter treiben: ifC (ein » var) ) if( var !- 0 ) if(var -- I cout « "Du hast e l se i f (I) gewah l t\n " ; else if ( var - - Z 1 cout « 'Du hast else i f (Z) gewahlt\n" ; else cout« 'Du hast nieht[O , l , ZJgewahlt . > else\n" ; else cout « "Du hast if gewah l t ! \n" ; el se f eerr « "Feh l er bei der [ingabe!\n" ;
ex i t{ 1) ; Hi er wurde das Beispiel wei ter »verschachtelt« und zusätzlich wurden noch die nicht unbedingt erforderlichen Anweisungsblöcke entfernt. Mir persönlich graust es vor so einem Code, aber auch das ist eine Frage des persönlichen Stils. Als Erweiterung von C wurde in C++ bei den Kontrollanweisungen i f , swi teh , for und whi 1e ein eigener Gültigkei tsbereich eingeführt. Zwar wird auf di eses Thema noch geson dert eingegangen, aber es sollte hier schon mal erwähnt wer· den. Dieser Gültigkeitsbereich erstreckt sich vom Anfang der i f~Anwei sung bis zum Ende des Anweisungsblocks. Es kann also im Gegensatz zu C direkt in der i f -Anweisung eine Variable deklariert werden: if { (int var - tage/stunden) > 9 ) f // Anweisungen . > hier i st va r gOltig // Ab hier i st var nicht mehr gOltig Verg le ichso pe ra toren
In den Beispielen wurden bisher schon häufig die Ve rgleichsoperatoren verwen· det, um bestimmte Ausdrucke auszuwerten. Da in der Programmierung häufig reger Gebrauch von diesen Operatoren gemacht wird, finden Sie hier eine Auflistung aller Vergleichsoperatoren in C++ (Tabelle 1.16). Verglelchso perator
Bedeutung
Wahr, wenn a kleine r als b
a
Tabelle 1.16
72
Wahr, wenn a kleiner oder gleich b Ober5icht zu Vergleichmperatoren (Relationale Operatoren)
KontrolJslrukturen
Verglelchso perator
Bedeutung
0>,
Wahr, wenn a größer als b
a >-
t:>
Wahr, wenn a größer oder gleich
a --
t:>
Wahr, wenn a gleich b
a !-
t:>
Wahr, wenn a ungleich b
Tabelle 1.16
I
1·9
t:>
Übersicht zu Vergleichsoperatoren (Relationale Operatoren) (Forts.)
Hier werden oft Fehler mit dem Vergleichsoperator -- gemacht. indem man ein - vergiSSt. if(
va r- IOO ) I 11 Anw e isung{en)
Rein syntaktisch besteht hier nämlich gar kein Fehler, weshalb sich Ihr Pro~ gramm auch anstandslos ausfuhren lässt. Allerdings ist der Ausdruck ..var=loo.,; immer wahr. schließlich bekommt di e Variabl e ..var.,; den Wert .. 100.,; zugewie· sen. Allerdings bin ich mir sicher, dass dies zu 99.9% nicht so gewünscht wurde. Es ist recht einfach, sich davor zu schützen, indem man den Ausdruck einfach umdreht: if{
100 -- var ) I 11 Anwe i sung(e n )
Würden Sie jetzt das "-Zeichen vergessen, läge ein Syntaxfehler vor und der Compiler würde sich melden, da man einer Zahl keine Variable zuordnen kann. Hinweis Wer sich fragt, warum man ausgerechnet das - ·Zeichen zum vergleich eines Ausdrucks velWendet, dem sei gesagt, dass die gute alte Bourne-Shell (sh) genau dies tut. Das Programm ..test.,; alias .. [.,; nutzt diese Syntax. Man kann diese Syntax somit theoretisch in jeder Shell nutzen, wenn »test« installiert ist (hier ist die Rede von linux-!unixartigen Systemen). Bedingungsoperator 1:
Den ternaren Operator ? : haben Sie Ja schon einmal verwendet. Dieser stellt letztendlich nur eine Kurzform der i f ... el se·Anweisung dar. Die Syntax: (Ausd ru ck)? (Anweisung 1)
(Anweisung 2)
Ist der ,.Ausdruck« wahr, dann wird die .. Anweisung 1" ausgefühn - ansonsten wird zur ..Anweisung 20c verzweigt. Will man zum Beisp iel den größeren Wert von zwei Zahlen ermitteln, so lässt sich dies mit dem Operator ? : wie fo lgt realisieren :
7l
[«)
I
1
I
Grundlagen In C·....·
11 tern.c pp llinclude us i ng namespace s t d :
iot main{) I int varl . var2 . ma xval : cout « "Bi t t e eine Zahl ein» varl : cout « "Noch eine Zahl ein» var2 : maxval - (varl > varZ) 7varl : varZ; cout « "Die gröBere Zahl lautet " « maxval return 0:
«
"' n":
Die äquivalente Schreibweise mit der i f .. . e I se-Anweisung sieh t folgendermaßen aus: if(a>b) 1 maxval - a ; else I maxval - b:
In solch ein em Fall ist der ternäre Operator noch ganz vertretbar. Aber es lässt sich schon erkennen, dass dieser Operator nicht unbedingt dazu geeignet ist. einen klaren und gut leserlichen Code zu schreiben . Natürlich ist das auch wieder Ansichtssache und abhängig vom Codesti!. Folgender Ausschnitt soll zeigen, was gemeint ist: bignum - ( a ) bl ?((a > cl ?a : c) : {( b > cl ?b : c) ;
Hier wurde der ternäre Operator »verschachtelt. , um den größten Wert von drei Za hlen zu ermi u eln . logische Operatoren
Die logischen Operatoren in CH sind && (UND), 11 (ODER) und ! (NiCl·lTI. Gewöhnlich werden logische Operatoren mit baa l -Werten verwendet, aber praktisch kan n man diese auch mit Zahl en und sonstigen Ausdrücken verknüp· fen. In der Praxis lassen sich diese Operatoren auch überladen (später mehr dazu) , wovon man aber Abstand nehmen sollte. um die Dinge nich t unnötig komplizien zu gestalten.
74
Kontrollstruk t uren
Bedeutung
Operator
logisches UND logisches ODER
11
logisches N ICH T T. belle 1.17
Die logischen Operatoren in CH
Ausdrücke. die durch den logischen ODER·Operator miteinander verknüpft sind, geben dann wahr ( t rue) zurück. wenn mindestens einer der Ausdrücke wahr ist.
if « Bedingung}) 11 (Bed i ngung2)) I 11 mindestens ein Ausdruck i st wa hr - true el se I 11
keiner der AusdrOcke i st wahr - false
Hierzu der Programmablau fp lan des logischen
ODER~Opera[Qrs:
..
,
Beding..-.g l . . ..ah r
ArlIII'
Abbildung 1.6
Programmablaufplan des logischen ODER-Operators
Anhand des Prog rammablaufplans (Abbildung 1.7) lässt sich auch erkennen, dass der zweite Ausdruck gar nicht mehr ausgewertet wird, sobald der erste Ausdruck wahr ist. Daraus e rgibt sich folgende logische Verknüpfungstabelle (Tabelle 1.18): Bedingung1
Bedingung2
Bedlngung1
t rue
t rue
true
t rue
false
true
11
Bedlngung2
Tabelle 1.18 Mögliche Ergebnisse einer logischen ODER-Verknüpfung
75
I
1·9
I
1
I
Grundlagen In C·....·
8edingung1
8edingung2
8edlngung1
fa1 se
t rue
true
fa1 se
fa1 se
false
Tabell e 1.18
11
8edingung2
Mögliche Ergebnisse einer logischen ODER·VerknÜpfung (Forts.)
Hierzu ein einfaches Beispiel: 11 logiCor . cpp lIinclude us i ng namespace std ;
int main(void) I int varl . var2 ; cout « "Bitte eine Zahl ". e in » var l ; cout « "Noc h eine Zahl ". e i n» var2 ; if{ (var} ~= 0) 11 (var2 ~ 0) cerr « "Einer der Zahlen ist 0 ! !! \n "; el se I cou t
«
"Ke i ne der Zah l en hat de n Wert O\n" ;
retur n 0: Sobald Sie im Beispiel eine Zahl mit dem Wert 0 belegen , gibt die logische ODERVerknüpfung true zurück und es erfolgt die en tsprechende Ausgabe. Anders hingegen sieht dies beim UND-Operator aus, der nur dann wahr (t rue) zurückliefert, wenn all e Ausdrücke wahr (true) sind. Die Syntax: if ( ( Bedingungl)
&& (Bedingung2» I II beide AusdrOcke sind wahr - true
else I II einer oder beide Ausdrücke sind fa lsch - f alse
Auch hierzu der Programmabtaufplan des logischen UND-Operators (Abbildung 1.7). Sofern der erste Ausdruck schon falsch (fa 1se) ist, wird der zweite Ausdruck gar nicht mehr ausgewertet, und es wird fa 1se zurückgegeben. Hierzu die logische Ve rknüpfungstabelte des UND-Operators (Tabel le 1 .19).
Kontrolls trukturen
Badi,,!/u,,!/1 •• wahr
8&dingung2 •• wahr
NeOn
Abbildun g 1.7
Nein
Programmablaufplan des logischen UND-Operators
Bedlngung1
Bedlngung2
Bedlngung1 && Bedlngung2
tr ue
t rue
true
t rue f~l
se
fal se Tabelle ,.19
f
f
t rue
f
f
false
Mögliche Ergebnisse einer logischen UNDNerknüpfung
Auch hierzu e in einfaches Beisp iel: 11 logic_and.cpp llinclude
int main(void) 1 int va r; cou t « "Bitte ei ne Zahl von 1 und 10: " . ein» var; if( (var )- 1) && (var <- 10) ) I cOu t « "Danke! !!\n" ; else 1 cerr « "F alsche Wert -Eingabe !! !\n" ; return 0:
77
I
1·9
I
1
I
Grundlagen In C·....·
[n diesem Beispiel werden Sie aufgefordert eine Zah l zwischen 1 und 10 einzugeben. Die logische UND-Yerknüpfung überprüft. ob der Wertebereich der Zahl größer oder gleich 1 ist UND klei ner oder gleich 10. Trifft beides zu. wird true zurückgegeben, wenn einer der Ausdrücke falsch ist, f a 1se . Selbstverständlich können Sie auch mehr als nur zwei Ausdrücke mi teinander verknüpfen bzw. den &&- und den li -Operator miteinander vermischen. All er· dings sollten Sie hierbei immer die Lesbarkeit des Quellcodes im Auge behalten. Mit dem logischen NICHT-Operat.or (!) kann ein Ausdruck negiert werden. Man kann also aus ,.wahr« "falsch .. machen und umgekehrt. Die fol gende Tabelle demonstriert das (Tabelle 1.20) : IBedmgung
Bedmgung
t rue
f~lse
f~l
true
se
Tabelle 1.20
Ergebnisse des logischen NICHT-Operators
Häufig wird dieser Operator verwe ndet, um einige Dinge abzukürzen. Anstalt zu überprüfen, ob etwas 0 oder false ist, lässt sich der N[CHT-Operator verwenden. Beispielsweise haben Sie schon oft Folgendes verwendet, um die richtige Eingabe des Typs zu überprüfen: // gleichwertig zu i f ( (ein » var) -- 0 ) (ein» var) -- false ) [ ce r r « "Fa l sche Eingabe - Keine Zahl\n " :
if (
exitcll :
Dies läss t sich jetzt mil dem logischen NICHT-Operator wie folgt abkürzen: if ( ! (cin » var) ) I ce r r « "Fa l sche Eingabe - Keine Zahl\n" ; exit(l);
Natürlich ist der logische NlCHT-Operator kein Operator, ohne den man nicht auskommen würde. wie die folgende Tabelle (1.2 1) zeigen soll: Mit logischem NICHT
Ohne logisches NICHT
if ( ! (a usdruck) )
if ( (ausdrUCK) -- false)
Iloder if ( ( ausdrUCK) -- 0 ) Tabelle 1.21
Vorgang mit dem und ohne den logischen NICHT-Operator
KontrolJslrukturen
Mit logischem NICHT 1f ( ! (varl ( -
1f ( ! (var (- I) Tabell e 1.21
y ~ r2)
&&
Ohne logisches NICHT
)
!(va r >- l U)
H ( H(
v~rl
(v ar
> v~r2)
>-I)
U
(v~r
<- 10»
Vorgang mit dem und ohne den logischen NICHT-Operator (Forts.>
Es ist woh l immer ein Frage des Geschmacks und des ProgrammierstHs. Die switch-Anweisung
Die switch-An weisung wird der i f-Anweisung häufig vorgezogen, wenn einfache Abhängigkei ten von mehrere n Zahlen oder Zeichen zur Verzweigung benÖ· tigt werden. Alles. was man mit der sw ; tch-Anweisung machen kann, könnte alternativ auch mit i f-Anweisungen erledigt werden, wobei swi tch bei vielen Verzweigungen komfortabler und besser lesbar ist. Die Syntax zur sw i t chAnweisung: swi tch ( Bed i ngung ) I (ase Ausdruck 1 // Anwe i sung(e n) br eak : ca se Allsd rllck_2 : 11 AnweiS lI ng(en) break :
case Ausd ru ck_n : // AnweiS lI ng(en) break ; defaul t // AnweislIng(en ) Zunächst wird beim Eintreten der swi tC h-Anweisung die Bedingung ausgewertet - dies muss ein integraler Ausdruck se in. Der nun folgende Anweisungsblock besteht aus einer Reihe von ca se-Marken. Jede dieser ca se-Marken scellt einen )OEinsp rungpunkt~ dar. Hat swi tch die Bedingung ausgewertet, wird zu einem (falls vorhanden) passenden Einsprungpunkt verzweigt. Nun werden alle Anweisungen, die sich in diesem Einsprungpunkt befinden, abgearbeitet. Ist die letzte Anweisung des Einsprungpunkts (noch vor dem nächsten Einsprungpunkt) ein break , so wird aus dem swi tch .Anweisungsbiock herausgesprungen (was gewöhnlich der Fall ist). Sollten Sie kein break verwe nden. so werden die dahinter folgenden Einsprungmarken ebenso ausgeführt (was durchaus gewollt sein kann).
79
I
1·9
I
1
I
Grundlagen In C·....·
Trifft keiner der Einsprungpunkte zu, wird en tweder hinter der swi tch-Anweisung mit dem Programm fortgefah ren oder, sofern ein e Sp rungmarke de fa ul t vorhanden ist, zu dieser verzweigt Natürlich da rf es nur eine Sprungmarke mit dem Namen defau 1 t geben. In der Praxis verwendet man in sw i tch-Anweisungen immer einen de fau l t -Zweig für unvorhersehbare Fehler .
...
""",
a uswerten
Konstanter
Ausdrud<
t
>-~-I
Konstante.
Ausdruck n
Anweisung(e n)
break
Anweisung(enl
break
>---1-1 M","",sung(en)
1---+1
Anv;e;SUng(en)
Abbitdung 1.8 Progra mmablau fp lan zur switch-Anwelsung
Hierzu ein einfaches Beispiel: 11 swit chl. cpp lli nclude
using names pdce std :
80
KontrolJslrukturen
int main(voidl 1 int var : cou t « " -1- Europa\n" ; cou t « " - 2 - Asien\n ": cou t « " -3- Afr i ka\n" ; cou t « " - 4 - Amerika\n" ; cou t « " - 5 - Australien\n ": cout « "Ihre Wahl bi tt e : ": if ( l( cin » var) ) ! cerr « " Fals che Eingabe - Keine Zahl\ n": exit(1 ) : cout « " Ih re Wahl is t ". switch ( var ) ! ca se 1 : cout « "Euro pa \n ": break : case 2 : cout « "Asien'n ": break : case 3 : cout « "Afrik a 'n ": break : case 4 : cout « "Amer i ka\n ": break : case 5 : cout « "Austral i en\n " : break : defau l t : cout « "\nFehle r bei der Auswah l 1\n" : return 0 :
Das Prog ramm bei der Ausführung:
+
[uro pa
-2- Asien -J- Afrika - 4 - Ame r i ka
-5- Aust r alien
,
Ihre Wahl bi t te 3 [ hre Wahl is t Afr i ka
81
I
1·9
I
1
I
Grundlagen In C·....·
- I · [uro pa - 2 · Asien -3· Afrika -4· Ame r ika - 5 · Aust ralien Ihre Wahl bitte : 8 Ihre Wahl ist Fehle r be i der Auswa hl!!! Wie bereits erwähnt, ist es auch durchaus gängig keinen ltAussprung" mittels break zu machen usi ng namespace std :
int main(void) I int var : cout « "·1· Produkt von A abholen\n" ; cout « "·2· Produkt auf Ba nd 1egen\n " : cout « " · 3· Produkt vom Ba nd abholen\n" ; cout « " · 4· Produkt nach B br i ngen\n " : cout « " Ihre Wahl bi tte if ( !( cin » var) ) { cerr « "Falsche Eingabe Keine Zah l \n" ; e)(it(l) ;
..
cout « " Ihre Wah l ist switch ( var ) I case 1 : cout « "Produk t von A abholen\n" : case 2 : cout « "Produk t au f Band legen\ n" : case 3 : cout « "Produk t vom Band abhole n\ n"; case 4 : cout « "Prod uk t nach B br inge n\n " ; break ; defaul t : cout « "Fehle r be i der Auswa hl\n" ; return 0:
.,
KontrolJslrukturen
Das Programm bei der Ausführung:
-1- Produkt von A abholen - 2- Produkt auf Band leg en -3 - Produkt vom Band abholen -4- Produkt nach B bringen Ihre Wahl bitte : 1 Ihre Wah l ist Produkt von A abholen Produkt auf Band legen Produkt vom Band abholen Produkt nach B br ingen
,,"
-[- Produkt A abholen Band legen - 2- Produkt - J - Produkt Band ab holen - 4 - PrOdukt nach B br i ngen Ihre Wa hl bi tte : 3 Ihre Wahl ist Produk t vom Ba nd abholen Produkt nac h B bringen
,,' "m
In diesem Beispiel wird ei n automatischer Vorgang si muliert. Hierbei wird ein Gegenstand zunächst vom On ,.A ~ abgeholt. aufs Band gelegt. vom Band abgeholt und zum On "B« gebracht. Da es durchaus sein kann. dass die Maschine während eines Vorgangs angehalten wurde. kann nun mitten im Prozess darauf zugegriffen werden. Liegt ein Produkt bereits auf dem Band und der Prozessvorgang wurde unterbrochen. dann kann dieser mit der Auswahl "3 ,, von Hand zu Ende geführt werden. Gerne und häufig wird die swi tch-Anweisung auch in Verbindung mit dem Auswenen (oder auch als Filter) einzelner Zeichen verwendet. Hier ein solches Beispiel. welches alle leichen einliest. die Sie in der Standardeingabe eingeben, und die Zeichen . $« und -#« ausfilten. 11 switeh3 .epp If i nel ude using namespace std ;
i nt main(void) i nt eh ; 11 EOF kann mit STRG+Z bzw . STRG+D ausgelöst werden wh i le( (eh " ein . get() ! - EQF 1 I sw it eh ( (eharl ch) I ca se ' $ ' : 11 Hie r auf das Dollarze i chen reagie r en ... cout « "[Doll arzeichen) " :
83
I
1·9
I
1
I
Grundlagen In C·....·
break ; ca se ' 11 ': 11 Hier auf das '·Zeichen reagieren ... cout « "( Hash · Zeichen]" : break : 11 ... usw default cout « Ccha r lch ;
re t urn 0: Das Programm bei der Ausführung: Hallo Welt Hallo Welt
Hier ein $-Zeichen Hier ein (Dollarze i chen] · Zeichen Und hier ein '·Zeichen Und hi er ein (Hash · Ze i chen] ·Zeichen lliiil +[I) bzw . ~ + [[) 1,9 .2
Schleifen (Iterationen)
Bei den Schleifen (bzw. Ilerationen) wird ein bestimmter AnweisungsbJock so oft wiederholt. bis eine bestimm te Abbruchbedin gung e intrifft (was auch nie der Fall sein kann). Die whiJe-Anweisung
Zunächst das Grundgerüsl zur wh i 1e-Anweisung: wh i le ( Bedingung) I 11 Aba r beiten der Anweisu ngen Die wh i I e-Schleife führt die Anwe isungen, die im folgenden Anweisungsblock zusammengefasst sind, solange aus, wie der Ausdruck der whl1 e-Anweisung wahr (tr ue) ist. Ist der Ausdruck in der while-Anwe isung hingegen falsch (fa I se). so wird der Anweisungsblock nicht (mehr) ausgeführt und die Ausfüh· rung des Programms wird hinter dem Block fortgesetzt. Üblicherweise durchläuft eine whi I e-Schleife fo lgende Schriue: ~
Initialisierung - die Schleifenvariable bekommt ihren Anfangswert
KontrolJslrukturen
..
Bedingung - d er Ausdruck d er Schleifenvariable w ird auf ei ne bestimmte Bedingung überprüft
..
Reinilialisieru ng - der Wen der Schleifenvariable wird veränden
B&dlngung __ wahr
N.. in
Anwei !IUng(enl
Abbildung 1.9
1------'
Programmablaufplan der while-Schleife
Rein programm techn isch sieht dieser Vorgang folgende rmaßen aus: int va r - 12 ; while ( var < 12 ) // Anweisungen var++;
1/ Initialisierung // Ausdruck auf Bedingung überp r üfen
// Rein i tialisierung
Ein einfaches Beispiel zur whi le -Schleife: /I while1.cpp llinclude
int main(voidl I int va r - l ; while( var <- 6 ) cout « var «
Schl e ifendurchlauf\n ";
ViJ l'++ ;
re t urn 0; Das Programm bei der Ausführung: 1. Schleifendurchlau f 2. Schleifend urchlau f
85
I
1·9
I
1
I
Grundlagen In C·....·
3. 5c hlei t endu rchla ut 4 . 5c hl eitendu rchla ut
5. 5ch l eifendu rchl au f 6. 5c hleifendurchla uf
[»]
Hinweis Ein häufiger Fehler ist das Vergessen der Reinitialisierung für das Abbruch~ kriterium einer wh i le-Schleife. Meistens kommt es dann ungewollt zu einer Endlosschleife, die nur noch ..gewaltsam« unterbrochen werden kann.
Manchmal ist eine Endlosschleife durchaus erwünscht. Diese wird gerne einge· setzt, wenn etwas dauerhaft überwacht werden muss ode r man auf ein bestimmtes Ereignis warten will. Ähnlich wird dies übrigens bei Bibliotheken mit einer grafischen Oberfläche gemacht, um aufTa statu r- oder Maus-Ereignisse zu warten oder bei einer Server·Anwendung in der Netzwerkprogrammierung - einfach überall, wo ein bestimmtes Server-/Clientprinzip vorhanden ist. Im Prinzip läuft eine solche Schleife imm er wie fo lgt ab: wh i le ( t ru e ) { bool ende - fals e; t ue_et was ( ) : if (ei neBedi ngung()) switc h ( welcheBedin gung ) { ca se Di es : 11 Anweisun gen t or Dies break : ca se Das : 11 Anweisu ng en t or Das break : ca se Ende : 11 Anweisungen t or En de e nde- t r ue ; break;
11 Endlossc hl ei t e abbrechen if ( ende -- tru e ) I br'ed k ;
Oie do ... w hile-Anweisung
Die do ... whi 1e-Anweisung wird ebenso wie die wh i 1e-Anweisung ausgeführt, nur mit dem Unterschied, dass die Überprüfung der Bedingung erst nach dem Ende des Anweisungsblocks des Schleifenrumpfes stattfindet. Die Sy ntax:
86
KontrolJs lrukturen
do I
// An weisungen ) while ( Bedingung ) :
Zunächst werden die Anweisungen im Schleifen rumpf ausgefühn. Anschließend wird der Ausdruck auf eine bestimmte Bedingung hin uberpruft. [st diese wahr (true), fo lgt ein erneuter Schleifendurchlauf. Trifft die Bedingung nicht mehr zu, wird die ProgrammausfUhrung hinter dem Schleifen rumpf fortgeführt. Achten Sie auch darauf, dass Sie eine da while-Schleife am Ende von while mit einem Semikolon abschließen.
1..-----,
Anweisung(en)
J,
Bedingung •• wallf
Nein
Abbildung 1.10
Programmablaufplan der do while-$chleife
Da die da ... whi 1e-Schleife den Schleifenrumpf mindestens einmal durchläuft, sollte man diese Schleife auch nur dann verwenden, wenn dies mindestens einmal der Fall sein soll. Die do ... whi 1e-Schleife lässt sich zum Beispiel hervorragend für einfache Benutzermenüs in der Konsole einsetzen, da eine solche mindestens einmal durchlaufen wird. / / do_whi 1el. cpp lI i nclude (iostream> using namespace std ; i nt main(voidl i nt va r : do
cou t cout cout COLlt
« « « «
· 21 ·3 · ·+ ~
~
~
~
~
~
Europa\n" : Asien\n "; Afr i ka\n "; Amerika\n" ;
8,
I
1·9
I
1
I
Grundlagen In C·....·
cout cout cout
« « «
" · 5· Austral i en\n" ; " · 6 · Ende\n" ; "Ihre Wahl bitte ". ff ! (ein» var) ) { cerr « "Falsche Eingabe Kei ne Zahl \n "; exit(}) ;
COut « "Ihre Wahl i s t switch ( var ) I ca se 1 : cout « " Europa\n" ; break : tase 2 : cout « "As i en\n" : break : case 3 : cout « "Afrika\n ": brea k : case 4 : cout « "Ame r ika\n ": break : case 5 : cout « "Austra l ien\n" ; break ; ca se 6 cout « "\nProgramm wi r d beendet . .. !\n" ; break : default : cout « " \nFehler bei der Auswahl !!! \n" ; while( var !- 6 ) : return 0: Das Prog ramm bei der Ausfuhrung: -1 - Europa - 2- Asien - ) - Afrika - 4 - Ameri ka - 5- Australien - 6 - Ende Ihre Wahl bit te 2 Ihre Wahl ist As i en - 1- Europa
88
KontrolJslrukturen
-2- Asien -3- Afrika -4- Ame r i ka -5- Aust r alien -6- Ende Ihre Wah l bitte 6 Ihre Wahl is t Programm wird beendet ... !
,
Die tor-Anwei sung Die for-Anweisung ist im Prinzip e ine »verkilrzte" whi I e-Anweisung - mit dem Unterschied, dass bei der fü r-Anweisung die Initialisierung, die Auswertung der Bedingung und die Reinitialisierung schon in der fo r-Anweisung e rfolgen können. Die Syn tax: f or e l nitial i s i erungeen) : Bed i ngung ; Reinitia l is i erungeen) I 11 Anweisungen Zunächs t können bei der for- Schleife e ine oder mehrere Variab len initialisiert werden. Dieser Vorgang geschieht allerdings einmalig - egal. wie oft der Schleifenrumpf anschließend ausgeftihn wird. Nach der Initialisierung der Schleifenvariable wird typischerweise der Ausdruck der Bedingung darauf überprüft, ob wahr ( t ru e) zurückgeliefert wird. Wird t r ue zurückgegeben, werden die Anweisunge n im Anweisungsblock der for- Schleife ausgeführt. Wenn all e Anweisungen ausgeführt wurden, werden die Reinitialisierungen der Schleifenvariable ausgefuhrt. Anschließend w ird erneut die Bedi ngung überprüft, und der Schle ifendurchlauf beginm gegebenenfalls von vorne. Der Rumpf der f or-Sch leife wird so oft durchlaufen, bis die Bedingung falsch (fa I se) zurückgibt. Hierzu ein ein faches Beispiel: 11 f o r l . cpp If i ncl ude using namespace std ;
int main(voidl [ i nt i; f or e i - I ; <- 6 ; i ++ ) I cout « i « Schle i fend urchlauf\n "; r eturn 0;
89
I
1·9
I
1
I
Grundlagen In C·....·
Inilialislerull9
Bedir>gung == wahr
Nein
Anweisung(en)
R.· Inilialisierung
Abbildung , ."
Programmablaufplan der for·Schleife
Das Programm bei der Ausführung : 1. Schleifend urchlau f
1. Schleifendu rchlau f Schleifendurchlau f Schleifendurchlauf Schleife nd urchlauf Schleifendurchlauf
3. 4. 5. 6.
Das Programm macht also nichts anderes als schon das Beispiel ~ w h j)e1.cp p " . Es ist übrigens (in ü+) durchaus üblich, d ie Schleifenvariable lokal zu deklariere n:
for ( f nt 1-1 : i <- 6 : i++ ) I cout « i « •. Schleifendurchlau f \n" : 11 Hi er ist i ni ch t mehr gültig
Die Variable ist somit nur innerhal b des Schleifenrumpfs gültig und verschwendet somit keinen unnötigen Speicherplatz. Es ist außerdem auch möglich, mehrere Variablen in der for ·Schleife zu initialisieren und gegebenenfalls auch zu reinitialisieren, um som it gleichzeitig zwei
90
KontrolJslrukturen
Indizien zu behandeln . Dabei werden die einzelnen Variablen mit ei nem einfa· ehen Komma voneinander getrennt: 11 for2 . c pp lIinclude us i ng namespace std :
int main{void) 1 for ( 1nt nI-I. n2-2: nl <- 10 : nl++ . n2*-2 ) I cout « l nl* n2) « ' \n ': return 0 : Natürlich müssen Sie keineswegs alle Angaben der for-Schleife ausfll11en. Lassen Sie die Auswertung der Bedingung weg, so haben Sie praktisch eine End losschleife, die immer wahr ist. 11 Endlo ssch l eife
f or ( ;; ) I 11 Anweisungen Sol! die Schleife nur eine Bedi ngung auswerte n, so lässt sich das auch mit for ohne Probleme bewerkstelligen : for ( : Bedingung -- wahr; ) I 11 Anweisunge n Allerdings greift man in solchen Fällen doch eher zur whi 1e-Anweisung. 1.9.3
Sprunganweisungen
Bei Sprunganweisungen wird die Programmausführung mit Hil fe von Sprungmarken an einer anderen Position fortgesetzt. Obwohl nach wie vor möglich, werden Sprünge in einem Programm mittlerweile als schlechter Stil angesehen, und sie sind auch nicht notwendig. Die Rede ist von direkten Sprüngen. Die direkten Sprünge mil der gele-Anweisung werden in diesem Buch nicht mehr behandelt, da es wohl ke in Problem dieser Welt gibt, das nicht ohne gete zu lösen wäre. All ein schon die Möglichkeit, Dek larationen mit Initialisierungen zu überspringen, ist für mich ein Argument, hierauf zu verzichten. Mit Schlüsselworten wie r eturn , break , cont i nue und exi t könnenjedoch auch kontrollierte Sprünge ausgeführt werden . Natürlich muss man dazu anmerken. dass return und exi t eigentlich keine schleifentypischen Anweisungen sind. daher werden diese auch erst später behandelt.
9'
I
1·9
I
1
I
Grundlagen In C·....·
Die break-Anweisung Die brea k·Anweisung haben Sie bisher schon öfter angewandt. Mit break fuhren Sie einen Sprung aus der direkt umfassenden Schleife(im Anweisungsblock) oder der swi tch-Anweisung heraus. Oder einfacher: Mit break wird generell ei n Anweisungsblock beendet - unabhängig von Schleife oder Verzweigung. Das folgende Beispiel liest solange Zeichen von der Tastatur ein und gibt diese auf die Standardausgabe aus, bis ein Punkt eingegeben wird. Wurde ein Punkt eingegeben, wird die Schleife mit br eak beendet. und die Ausführung des Programms wird hinter dem Sch leifenrumpf fortgeführt. 11 breakl.cpp llinelude us i ng namespace s t d ;
int main(void) i nt eh ; cout « . ) ' . wh i le( (ch - ci n. get()) ! - EQF ) I if ( (cha r )ch -- ' .' ) I brea k; i f( (cha r )c h -- ' \n ' ) I cou t « '\n > ". else I cou t
«
(ehar) eh ;
eout « ' Pr ogr ammende er reicht!\n' ; return 0 ; Das Program m bei der Ausführung:
> Hallo
Welt Hallo Welt
> Jetzt folgt dann das Ende Jetzt folgt dann das Ende > H1er der Punkt. Hier der Punkt Programmende erreicht! Beachten Sie bitte, dass beim Setzen vo n brea k in einer verschachtelten Schleife immer nur die inne rste Schleife abgeb rochen wird.
92
KontrolJslrukturen
Die continue -Anweisung Im Gegensatz zu break beendet die conti nlle-Anweisung nur die aktuelle Schleifenausftihrung. Das heißt, es werden alle noch hinter cont i olle folgende n Anweisungen .. ausgelassen«, und es wird mit der Programmausftihrung zum nächsten Schleifendurchlauf gesprunge n. Bauen Sie die continlle-Anweisung anstelle der break-Anweisung im Beispiel ,.break1.cpp« ein, wird zurück zum Schleifenanfang gesprungen, statt de n Schleifendurchlauf zu beenden, der Punkt wird also einfach ignoriert und nicht ausgegeben. 11 continuel . cpp #i nclude lIsing namespace std :
Int main(void) i nt ch ; cout « . > " . wh i le( (ch - cin . get(» ! - EOF ) r if ( (char)ch ) I eonti nue: if(
(ehar)ch - ' \n ' ) I cou t « " \n >
else I cout
«
(e ha r) eh :
cout « "Programmende erre i ch t! \n ": return 0 : Das Programm bei der Ausführung:
> Ha ll o
Welt
Hallo Welt
> Wie gehts. Wie gehts > Ein t ol le r Tag. Ein toller Tag )
> lliI2J+~ oder ~I+I]] Pr ogrammende e rr eicht!
93
I
1·9
I
1
I
Grundlagen In ( .....
Gerade in Verbindung mit Schleife nvariablen sollten Sie bei der Verwendung von cont i nue Vorsicht walten lassen. Das folgende Beispiel wird sich nicht mehr ohne äußere Gewalt beenden lassen. weil die Schleifenvariab le nicht mehr inkrementiert wird: // conti nu e2 . cpp lI i nclude us i ng namespace std ; int main(void) I int va r-O ; wh i le( va r < 10) if( va r t2) conti nu e : va r++: retu r n 0 : Sobald der Wert von ~ va r« mit dem Modulooperator (t ) durch zwei geteilt wird und die Bedingung der i f·Anweisung true ist. wird sich die Schleife nicht mehr von selbs t beenden können, weil die Zeile >lvar++« niema1s erreicht wird und ~va r % 2« immer wahr ist. Wenn die Bedingung true zurückgibt, bedeutet dies, dass die Zahl ungerade ist. da die Division einen Rest beinhaltet.
1 . 10
Funktionen
Funktionen sind praktisch kleine Unterprogramme (oder auch benutzerdefinierte Operationen), mit denen Sie Daten verarbeiten oder Teilprobleme lösen können . In den Be ispielen zuvor haben Sie auch schon im mer eine Funktion verwendet, nämlich die ma i n-Funktion. Egal wie viele Funktionen Sie schreiben, es wird immer eine ma i n-Funktion benötigt. Die ma in-Funktion ist außerdem immer die erste Funktion. die beim Programms tart aufgerufen wi rd. Den ersten Funktionsaufruf werde n Sie daher immer von der mai n-Funktion aus machen , alle weiteren Aufrufe können dann auch von den aufgerufenen Funktionen aus gestartet werden. Wenn Sie zum Beispiel eine komplizierte Berechnung erstell en müssen, werden Sie dies wohl kaum in der ma i n-Funktion machen. Dazu schreiben Sie eine gesonderte Funktion . Aus der mai n-Funktion rufen Sie dann die Funktion (gegebenenfa lls mit den Argumenten) auf. Die Fun ktion berechnet dann die Aufgabe oder zumindest einen Teil davon und gibt den Wert emweder an den Aufrufer zurück
94
Funktionen
oder ruft gegebenenfalls eine weitere Funktion auf, um beispielsweise das Ergebnis der Berechnung in einer Datei zu speichern usw. In der Praxis solhen Sie möglichst die Aufgaben auf viele kleine Funktionen aufteilen - eine Funktion soll ihre Aufgabe erfüllen und fertig. Wenn weitere Funktionalitäten benötigt werden, kann eine weitere Funktion geschrieben werden. So lässt sich der Code besser lesen, verbessern und gegebenenfalls auch wiederverwenden. Leider gibt es in C++ keinen standardisierten Weg, Funktionen parallel auszufü hren (Stichwort: Multithreading). Hierzu müssen Sie auf externe Bibliotheken zurückgreifen. Die Fun ktionen in C++ laufen in der Regel sequenziell ab, also nacheinander.
1.10.1
Deklaration und Definition
Bevor Sie eine Funktion verwenden bzw. aufrufen können, müssen Sie diese deklarieren und dann erst definieren . Mit der Deklaration teilen Sie dem Compiler den Namen der Funktion, den Typ des zurückzugebenden Fun ktionswerts und die Parameter der Funktionen mit. Die Deklaration einer Funktion wird als Prototyp bezeichnet. Die Syntax einer solchen Deklaration sieht wie folgt aus:
(Spezifizierer] Rockga betyp Funk t ionsname( Parameter) ; Somit ist eine solche Deklaration wie folgt gegliedert: ~
Rückgabetyp - Hier legen Sie den Datentyp des Rückgabewem fes t. Dabei dürfen all e bisher kennen gelernten Datentypen verwendet werden. Eine Funktion ohne Rückgabewert wird als void deklariert.
~
Funktionsname - Dies ist ein eindeutiger Funktionsname, mit dem Sie die Funktion von einer anderen Stelle aus im Programmeode aufrufen können. Für den Funktionsnamen selbst gehen dieselben Regeln wie für Variablen. Außerdem sollten Sie keine Funk tionsnamen der Laufzei tbibliothek verwenden .
~
Parameter - Die Parameter einer Funktion sind optional. Diese werden durch den Datentyp und den Namen spezifiziert und durch ein Komma getrennt. Wird kein Parameter verwendet, können Sie zwischen die Klammern en(weder voi d oder gar nichts schreiben.
~
Spezifizierer - Außerdem lassen sich bei Funktionen auch so genannte Speicherklassen-Spezifiz ierer verwenden. Mehr dazu finden Sie im entsprechenden Abschnitt.
95
I
1.10
I
1
I
Grundlagen In C·....·
Hier eine solche Funktionsdeklaratlon: i nt check Point ( j nt xPos , int yPos ) :
Beim Prototyp der Funktion müssen Sie bei den Funktionsparametern allerdings den Namen nicht zwangsläufig angeben, nur die Datentypen: i nt check Point ( i nt. int ) :
Allerdings lässt sich an der ersten Deklaration wesentlich schneller ablesen , was die einzelnen Parameter der Funktion bedeuten, wenn man troudem einen Namen verwendet. Der folgend e Codeausschnitt soll eine solche Funklionsde klaration näher demonstrieren: 11 funcl.cpp lIinclude
1nt
chec~Po1 nt
( 1nt xPos, 1nt yPos ) ;
int ma i n(void) // An weisu ng en r eturn 0 ;
[»]
Hinweis Jede Funktion muss vor der Anwendung deklari ert werden. Im Gegensatz zu den Variablen stellt eine Funktionsdeklaration noch keine Definition dar - hierzu fehlt noch der eigentliche Code der Funktion.
Mit der jetzt gleich fo lgenden Definition teilen Sie dem Com piler die eigentliche Arbeitsweise der Funktion mit. Eine Funktionsdefinition beinhaltet neben den Daten vom Prototyp auch den Funktionsrumpf (bzw. den Anweisungsblock), worin die Anweisungen (die eigentliche Arbeit) der Funktion geschrieben werden. Außerdem benötigt eine Funktion kein abschließendes Semikolon mehr. Die Syntax der Funktionsdefinition sieht demnach wie folgt aus: [Spezifizierer] RQckgabetyp Funktionsname(Parameter) { 11 Anweisungen
Somit sieht eine komplette Deklaration und Definition einer Funktion folgendermaßen aus: 11 funcl .c pp llinclude using namespace std :
96
Funktionen
11 Funktions-Prototyp (Deklarat i on) 1nt chec kPo1 nt ( 1nt xPos , 1nt yPos ) ;
i nt ma i n(voidl 11 Anweisungen re t urn 0;
11 Funktio nsdefin i tion
1nt ch e ckPo1 nt ( 1nt xPos , 1nt yPos ) { 11 Anwe1 s un gen )
Die Pos ition de r Fun ktionsdefinilion muss allerdings nicht zwangsläufig hinter der ma in -Funktion stehen, sondern darf natürlich auch davor platziert werden: 11 funcl . cpp llinclude
i nt chec kP o1 nt ( i nt xPos , 1nt yPos ); 11 Funktionsdefin i tion i nt chec kPoi nt ( int xPos . i nt yPos ) ( 11 Anweis ung en )
i nt ma i n(voidl ( 11 Anweisu ngen return 0 ; Dies ist wohl w ieder eher eine Frage des Programmierslils. Mir persönlich iaHt es mittlerweile leich ter, die Funktionsdefinition hin ter der main-Funktion zu setzen, weil ich hierbei, angefangen bei ma i n( ) . den Programmablaufbesser verfolgen kann, sofern d ie Funktionen und main() in derselben Datei stehen, was in der Praxis eher selten der Fall ist. Aber dazu in e inem späteren Abschnitt mehr.
1.10.2
Funktionsaufruf und Parameterübergabe
Wenn Sie den Code fu r die Fun ktion definiert haben, können Sie diese aufrufen. Eine Funktion, die Sie aufrufen, hat neben den Funktionsnamen, mindestens noch den Funktionsaufrufoperator () , der immer hinter dem Funktionsnamen nollert wird :
97
I
1.1 0
I
1
I
Grundlagen In C·....·
11 ein f acher Funktionsau f r uf ohne Pa r ameter
f une ( ) ; In diesem e infachen Beispiel wird die Funktion o hne irgendwelche Parameter aufgerufen . Di es setzt natürlich voraus, dass diese Funktio n auch o hne irgendweIche Parameter deklariert und definiert wurde. Das Ganze in der Praxis: 11 f une2.epp lIinclude
11 Funktions · Prototyp ( Deklaration) vo1d fune ( vo1d ) ;
int main(void) { cout « ~ Vo r dem Funktionsaufruf\n ~ : 11 Funkt i onsauf r uf f une() ; cout « ~Nach dem Funktio nsau fru f \n~ ret urn 0:
:
11 Funktionsdefini tion
vo1d fun e ( vo1d ) cout « "f une {) ist aktiv\n" : Das Programm bei de r Ausführung: Vor dem Funktionsaufruf ist aktiv Nach dem Funktionsaufruf funcr)
Die hier verwendete Funktion ,.fune()« hat weder einen Rückgabewert noch irgendwelche Paramete r - weshalb hier auch voi d al s Rückgabewe n und Parameter angegeben wurde.
[»]
Hinweis Funktionen, die keinen Rückgabewert (also void) haben, werden in vielen anderen Programmiersprachen auch als Prozeduren bezeichnet . Dies gilt zwar nicht fijr C++, sollte aber trotzdem hier erwähnt werden ,
Parameterübergabe an Funktionen (call-by-value) Meistens werden Sie allerdi ngs kaum ,.naekte« Funktionen ohne Parameter bzw. einen Rückgabewert schrei ben. Abhängig davon, wie Sie die Funktion deklariert und definiert habe n, können bzw. müssen Sie auch Parameter be im Aufrufen an die Funktion übergeben. Eine solche Parameterübergabe kann wie fo lgt aussehen:
98
Funktionen
I
1.1 0
11 func3 . cpp llincl ud e
// Funk t io nsde f i n i tion void fun e ( i nt var ) I cout « " f unc() : Wert vo n var - "
«
var
«
" \n" ;
Das Programm bei der Ausführung: f unc( ) func( )
Wert von va r - 10 Wert von va r - 20
Anhand der Deklaration kann man scho n erkennen, dass diese Funk[ion einen Parameter vom Ty p i nt erwartet. Verwenden Sie kei nen Parameter oder ein en anderen Typ, meldet sich der Compiler mi t ei nem Fehler. Aufrufen können Sie diese Funktion nun aus jeder anderen Funktion mit f unc( Intege r _Wert ) ; Hinweis In der Informati k bezeichnet man die beim Funktionsaufruf angegebenen Parameter als Aktualparameter. Die Parameter, die bei der Deklaration angegeben wurden, werden a ls Formalparameter bezeichnet.
Wenn Sie die Funktion aufgerufen haben, wird hierfür ein Stack-Frame (StackRahmen = dynamischer Speicher) angelegt. In diesem Bereich wi rd Speicher für die einzel nen Parameter reserviert. die die Funktionen beinhalten. Der Para meter, den Sie der Funktion als Argument übergeben haben, wird hierbei auch gleich initialisiert (alles im Stack-Rahmen wohlgemerkt). Damit steht der Wert, den Sie der Funktion beim Aufruf mitübergeben haben, als Kopie in der Funktion zur Verfügung. Man spricht hi erbei von einer call-by-value-Obergabe dies ist also immer eine Ko pie vom Original.
99
[« )
I
1
I
[»]
Grundlagen in C++
Hinweis Neben call-by-value existiert auch call-by-reference, womit statt einem Wert eine Adresse kopiert wird. Diese Art des Aufrufs wird im Zusammenhang mit Zeigern näher besprochen.
Da also formal e Parameter als Kopie der aktuellen Parameter fungieren, könn en Sie diese beliebig verändern, ohne dass dies Auswirkungen auf die Aktualparameter hat. Selbst wenn Sie beim Funktionsaufruf Variablen mit demselben Namen verwenden würden wie die formal en Parameter, hat dies keine Auswirkung auf die Werte der aktuellen Parameter. Hierfür ein Beispiel: 11 fune4 . epp llinelude
i nt ma i n(voidJ I i nt var - 20 : 11 . .. vor dem Funktionsaufruf eout « "main{ J : Wert von var IJ Funktio nsaufru f - eall·by - value f une ( var ) ; 11 .. . naeh dem Funktionsaufruf (out « "main() : Wert von var - • re t urn 0:
II Funktionsdefin i tion void fune ( int var 1 I va r - va r *2 ; (out « "fune() : Wert von var - •
«
var
«
"'n" ;
«
var
«
"'n" :
«
var
«
"'n" ;
Das Programm bei der Ausführung: ma i n( ) f une( ) ma i n{ )
Wert von var - 20 Wert von var - 40 Wert von var - 20
Dieses Beispiel zeigt recht gut, dass beim Aufruf von lO funcO " j eweils eine Kopie von »var« und ei n Original von »var" in der mai n· Funktion ex istiert. Beide haben zwar denselben Namen, aber unterschiedliche Adressen. Natürlich bedeutet dies auch, dass die Kopie der Variable in lOfun cO« genauso lokal ist wie die lokale Variable in der ma i n-Funktion. Beide Variablen sind nur innerhalb ihres Anweisungsblocks gültig und verwendbar.
100
Funktionen
I
1.10
Natürlich können Sie auch mehrere Parameter (auch unterschiedl ichen Typs) in einer Funktion verwenden. Wichtig ist nur, dass die Aufruf- Reihenfolge der Parameter mit der Deklaration und Definition der Funktion übereinstimmt. Rückgabewert v on Funktionen
Wenn Sie Funktionen schreiben, die ein Teilproblem lösen, werden Sie wohl einen Wert an den Aufrufer zurückgeben woUen, um mit diesem Wert (gegebe. nenfalls mit weiteren Funktionen) weiterarbeiten zu können. Welchen Wen und von welchem Typ eine Funktion etwas zurückgibt, geben Sie ja bereits bei der Deklaration und Definition der Funktion vor, zum Beispiel: i ot a rea ( fn t 1. fnt b ) : Hier haben Sie eine Funktion, die zwei Parameter vom Typ i nt erwartet und als Rückgabewen ebenfalls einen Integer-Wen zurückgibt. Um einen Wen aus einer Funk tion an den Aufrufer zu rückzugeben, müssen Sie die retllrn-Anweisung verwenden. Mit der Angabe von re t urn n: beenden Sie sofon die Funktion, egal, was sich dahinter noch fu r Code befin det. Der Rückgabewert. den Sie an die aufrufende Funktion zurückgeben, wurde hier mit ,.n" angegeben. Wollen Sie mehr als einen Wert aus einer Funktion zurückgeben. können Sie Struktu ren (struct) verwenden, worauf in Abschnitt 2.8.1 eingegangen wird. Hin weis return können Sie auch ohne weitere Parameter bei Funktionen verwenden, die kei nen Rückgabewert zurückgeben (void). Hierbei wird die Funktion an der Position beendet, wo return aufgerufen wird.
[« )
Hin weis Verwenden Sie return in der main-Funktio n, bedeutet dies somit auch das Ende des Programms - aber dazu ein paar Seiten später meh r.
[« )
WoUen Sie, dass der Aufrufer diesen Wert speichert, benötigen Sie eine Variable mit entsprechendem Typ und weisen dieser Variablen den Rückgabewert der Funktion mll dem Zuweisungsopera lor " zu: int ret ; ret .. area ( 10, 20) ; Die Variable »ret+< erhält nun den Rückgabewen, den die Berechnung der Funktion »areaQ+< zurückgibt. Dies bedeutet allerdings nicht. dass der Rückgabewert einer Variablen zwingend benötigt wird. Ein Beispiel: cout
«
area(lO . 20) ;
101
I
1
I
Grundlagen In C·....·
Hierzu wieder ein Listing, das zum besseren Verständnis beitragen soll: 11 func5 .cpp llinclude us i ng namespace std :
Funktions · Prototyp ( Deklaration) int area ( int 1. int b ) ;
11
i nt main{void) int varl . va r2. ret : eout « -Bitte die L~nge angeben if { ! (ein » varl) ) { cerr « "F ehle r be i der Eingabe!\n" : ex i tC I) :
..
..
eout « "B itte die Breite angeben if { ! (ein» var2l ) { eerr « "Fe hler bei der Einga be!\n" : exit(l) : ret - area ( va r l, var2 ); cout « "Die Fl~ehe bet ragt" « ret « - ' n" : return 0:
11 Funkt i onsdefinit i on
int area ( int 1, i nt b) I int fl aeche ; flaeche ~ 1 ." b; re tur n flaeche; Das Programm bei der Ausführung:
Bi t te die L~ng e angeben Bi t te die Breite angeben Di e Flache betragt 132
12 11
Die Funktion "area« könnten Sie noch um eine lokale Variable ,.erleichtern«, in dem Sie die Berechnung gleich in die return-Anweisung verpacken:
i nt a rea (int 1. int b) ( retu rn ( 1 * b ) :
102
Funktionen
[n der Praxis sollle man noch eine Funktion einbauen. die überprüft, ob einer der Werte gleich 0 war. [n solch einem Fall lohnt keine weitere Berechnung mehr. Hier ein solches Beispiel, wie dies in der Praxis aussehen kann: /I func6 . cpp If i ncl ude using namespace std:
11 Funktions- Prototyp (Dekla r ation)
int area ( i nt 1. i nt b ) : baal not_null ( lnt var ) ; int main(void ) int va r l , va r2. ret ; cou t « "Bitte di e Lange angeben if ( ! (ein» warll ) [ cerr « "Fehler bei der Eingabe ! \n" : ex i tO) ;
« "Bitte die Breite angeben : " : Ucin » var2) ) [ cerr « "Feh l er bei der Eingabe !\n" ;
cout if (
exit(l) ; 1f ( not _ nulle varl ) U noCnull( var2 ) ) {
ret - area ( varl, var2 ); else { cou t
«
"E1ne der Angaben 1st Ol\n " ;
ex1t(1);
cou t « "Die Flache be tr agt" return 0:
«
ret
«
"\n ":
/I Funkt i onsdefinit i on int arca ( i nt 1 . i nt b ) I
return
(l
* b) :
bool noCnull( fnt var ) { 1f ( var > 0 ) { return true; else {
103
I
1.1 0
I
1
I
Grundlagen In C·....·
return false : )
Sie sollten grundsätzlich eine Funktion schrei ben, mit der Sie Daten auf ihre Richtigke it überprüfen. Ganz besonders dann, wenn die Daten vom Anwender eingegeben werden . Man darf sich einfach niemals darauf verlassen, dass der User schon das Richtige eingeben wird. Auch hier könnten Sie die Funktion "nocnull .. auf eine Codezeile begrenzen, indem Sie die Auswertung, ob der Wert der Variablen größer als 0 ist, gleich wieder in die ret urn-Anweisung verpacken. boal not_null( int var ) { ret urn ( var ) 0 ) :
Achten Sie aber darauf, dass bei all den Optimierungen und Kürzungen, die Sie am Quellcode vornehmen, die l esbarkei t und vor allem die Verständlich keit des Codes erhalten bleiben. 1.10.3
lokale und globale Variablen
Wie Sie bereits im Beispiel des Listings ,.func5.cpp .. bei der Funktion "areaü" gesehen haben, kann man an einer Funktion nicht nur Va riablen übergeben, sondern auch Variablen darin deklarieren. i nt a r ea (int 1 . int b ) r
tnt flaeche : flaeche - 1 * b :
ret urn f1 aeche :
Bei dieser Variablen handelt es sich um eine lokale Variable. Dass heißt, diese Variable ist nur innerhalb des Funktionsrumpfs gültig. Es ist auch möglich, Variablen global zu deklarieren . Eine globale Variable hat daher auch einen globalen Gu ltigkeitsbereich und kann von jeder Funktion auch der ma i n-Funktion - verwendet werden . In der Praxis sollte man allerdings eine Variable so lokal wie möglich und so global wie nötig deklarieren, denn globale Variablen bringen leider auch Probleme in Bezug auf die Übersichtlichkeit des Codes mit sich. Wenn Sie eine Variable global angelegt haben und bei einem mehrere tausend Zeilen langen Code darauf zurückgreifen, erscheint das Ganze nicht mehr so übersichtlich, besonders wenn jetzt auch noch ein Dritter diesen Code überarbeiten muss.
104
Funktionen
Eine Frage liegt Ihnen aber sicherlich am Herzen: Was iS1, wen n Sie eine globale und lokale Variable mit demselben Namen verwenden? Welche Variable erhält den Zuschlag? Hier gih, dass immer die lokals1e Variable bei gleichnamigen Vari~ ablen den Zusch lag vom Programm erhält - wie das folgende Beispiel demonstrieren soll: 11 fune7 . epp llinelude
Globale Variable int va r - 123 ;
11
11 Funktions -P rototyp (Deklaration) void funel ( int var ) ; void fune2 ( void ) ;
i nt ma i n(voidl ( int var - 321 ; eout «"main() ; va r - " 11 ... per Pa rameter f unel ( var ) ; fune2 ( ) ; return 0;
« var « "\n" ;
11 Funktions de fin i tion void funel ( int var ) tout « "f un c!() : va r - " « var « "\n" :
void fune2 ( void ) eout «"fune2()
va r - " « var
« "\n" ;
Das Programm bei der Ausführung:
ma i n ( ) funcl( ) f une2{ ) 1.10.4
var - 321 va r - 321 va r - 123 Standardparameter
Bei der Deklaration der Prototypen einer Funktion ist es in C++ (nicht C) auch möglich. die Funktionsparameter mit einem bestimmten Wert zu belegen. Man
105
I
1.1 0
I
1
I
Grundlagen In C·....·
spricht dabei auch von Standard parameter CDefault Parameter). Hal man bei· spielsweise einen Prototyp wie folg t deklariert
vo i d f unc ( int var ) : kann man den Parameter "var« folgend ermaßen mit einem Standardwert belegen:
void f une ( i ot var - 66 ) : Wird j etzt beim Fun ktionsaufruf kein Argument mit übergeben, so wird der Standard wert ,,66« vom Compiler verwendet. Andernfalls wird - wie sonst auch - der Wert verwendet, der der Funktion als Argument übergeben wurde. Hierzu das Beispiel: 11 fun e8 .epp lIinelude
int main{void) I !! Funktion mit dem Argument 99 aufrufen fune ( 99 ) ; 11 Funkt i on Ohne Argument aufrufen fune ( ) : return 0:
Funkt i onsdefinition void func ( i nt var eout «"funel() var - • « var « "'n " ;
11
Das Programm bei der Ausführung :
f unel{) : var - 99 f unel{) : var - 66 Um keine Missverständn isse aufk ommen zu lassen, der Name des Prototyp muss nich t mit dem Namen der Fu nktionsdefi nition übereinstimmen, da die Standardzuweisung nach Pos ition und nicht nach Name n gemacht wird. Der Prototyp könnte demnach auch wie folgt aussehen:
vo i d f une ( int - 66 ) :
,o6
Funktionen
Natürlich lässt sich dies auch mi t mehreren Parametern machen - allerdings müssen Sie dabei beachten , dass die Zuordnung der Paramete r von links nach rechts erfolgt. Wenn praktisch ei n Parameter keinen Standardwert besitzt, kann auch der vorherige Parameter (links davon) keinen Standardwert haben. Am besten demonstriert man dies an Prototypen mit drei Parametern.
vo id f une ( int parI - 11. i nt par2 - 22 . int par3 - 33 ) : Es stehen Ihnen nun vier verschiedene Möglichkeiten zur Verfügung, diese Funktion aufzurufen: 11 fune9 . epp lIinelude us i ng namespaee std :
11 Funk t ions·Proto t yp mit Standardparamet er void fune ( i nt parI - 11 , in t par2 - 22 , int par3 - 33 ) ;
i nt ma i n(void) 11 Funk t i on ohne Argume nt au fru fen fune ( ) : 11 Funktio n mit e i nem Argument aufru fen fune ( 44 ) : 11 Funkt i on mit zwei Argumenten aufrufen fune ( 44 . 55 ) : 11 Funkt i on mit dre i Argumenten aufrufen fune ( 44 , 55 , 66 ) ; return 0:
Funkti onsde f in i tion void fun e ( int parI, int par2 . jnt pa r3 ) ( eout « "( pa ri/par2/p ar3 ) - • « par I « « par2 « « par3 « ' , « ' \n ':
11
Das Program m bei der Ausfiihrung: (pa rl/ par2/par31 (pa rl / par2/par31 (pa rl/ par2/par31 (pa rl / pa r2/par31
-
11 22 33 44 22 33 44 55 33 44 55 66
An ders sieht dies schon bei folgend em Prototyp aus:
void fune ( i nt par I . int par2 - 22 , i nt par 3 - 33 ) :
I
1.10
I
1
I
Grundlagen In C·....·
Hier stehen Ihnen nur noch drei verschiedene Möglich keiten zur Verftigung, da imme r mi ndestens ein Argument für den ersten Parameter "parl " angegeben werden muss (immer daran denken , die Zuordnung der Paramete r erfolgt von links nach rechts). Die möglichen Funktionsaufrufe hierfür lauten : // Funk tion mit ei nem Argument a ufrufen func( 44) : // Funk tion mit zwei Argumenten auf r ufen f unc ( 44. 55 ) : // Funk tion mit drei Argumenten auf r ufe n f une ( 44 , 55 . 66 ) ;
Verwenden Sie hingegen nur einen Standardparameter, so haben Sie nur noch zwei mögliche Aufrufe dieser Funktion, da hier immer Argumente fü r die Parameter "parl .. u nd "par2 « benötigt we rden. 11 Prototyp mit ei nem Standardwe r t void func ( int parI. int par2 . i nt par3 - 33 ) ;
JJ Funktion mit zwei Argu mente n a ufrufen
fune ( 44 , 55 ) ; 11 Funk t ion mi t drei Argumen t en auf r ufen
fune ( 44 . 55 . 66 ) ;
Somit sind folgende drei Prototypen falsch. weil hier der vorangegangene Parameter keinen Standardwert enthält u nd der Compiler die Auswertung hierbei von links nach rechts ausführt. // dreimal falsche Pro t otypen void f unc ( i nt parl - 11 , int pa r 2 . i nt par3 ) ; void f unc i nt pa r I - 11 . int pa r 2 . i nt pa r3 - 33 ) ; void f unc i nt parI . int par2 - 22 . i nt par3 ) ; 1.10.5
Funktionen überladen
In c++ können Sie auch meh rere Funktionen mit dem gleichen Namen verwenden. Dabei können sich die Funktionen sowohl durch unterschiedliche Parametertypen als auch durch eine unterschiedliche Anzahl von Parametern unterschei· den. Dieser Vorgang wird als ü berladen von Funktion en bezeichnet (oder auch als Funktionspolymorphie). So können Sie z. B. meh rere Funktionen mit dem Namen ,.do_stuffO" vereinbaren: int do _st uff i nt do s t uff
108
int par ) : double pa r
Funktionen
Beide Prototypen haben zwar den gleichen Namen ..do_stuffO .. , unterscheiden sich aber durch unterschiedliche Daten type n als Parameter. Solange die Funktionsparameter unterschiedlich sind (und nur dann). können Sie auch den Rückgabewert verändern: int do_stuff ( int par ) : double do_st uf f ( double par) : Wie berei ts erwähnt, können Sie au ch eine unterschied liche Anzahl von Funktionsparametern verwenden: i nt do_stuff ( i nt pa r ) : i nt do_stuff ( i nt pa r l . int par2 ) : double do_stuff ( dou ble par) : double do_stuff ( dou ble part . double par2 ): Dass dies überhaupt funktioniert, liegt daran. dass der Compiler die Funktionen nicht anhand von deren Namen iden tifiziert, sondern an hand ihrer Signatur. Die Signatur entsteht bei der Dek laration der Fun ktion aus einer Kombination der Funktionsnamen und der Parameterliste. Neben dem überladen von Funktionen ist die Signatur auch beim überschreiben von vi rtuellen Funktionen sehr wichtig . Durch diese Signatur sind allerdings die überladenen Funktionen fü r den Compiler nichts anderes als die üblichen Funktionen , wie Sie diese bisher auch verwen det haben. Dank dieser Technik können Sie Funktionen mit ähnlichen Aktionen mit gleichen Namen benennen. Dies ist auch beim Vereinbaren eines eigenen Datentyps sehr nützlich. Die Technik, die der Compiler verwendet, um die richtige Funktion zu find en, ist in der Tat beeindruckend . Hier durchläuft der Compiler der Reihe nach folgende runf Schritte: ..
Der Compiler findet eine Funktion, die vollständig auf die Signatur passt und muss keinerlei Improvisationen vornehmen.
..
Der Compiler versucht ei ne Funktion zu finden, mit der sich eine integrale Promolion durchführen lässt. Beispielsweise aus bool (true =1 und f a 1se=O) wird int.
.. Jetzt versuch t der Compiler eine Standard-Typenumwandlung durchzufuhren. um eine entsprechende Funktion zu finden . ..
Klappt es nicht mit einer Standard-Typen umwand lung sucht der Compiler nach einer benutzerdefinierten Typenumwandlung für den Funktionsaufruf.
..
Zum Schluss sucht der Compiler noch nach einer Funktion mit den drei Punkten ( .. . ) als Parameter - auch Ellipse genannt. Mit solc hen Funktionen kön-
109
I
1.1 0
I
1
I
Grundlagen In C·....·
nen theoretisch beliebig viele Parameter verwendet werden. In der Praxis verwende ich diese letzte Form der Suche nach einer Funktion gerne als den »else«-Zweig der Funktionsüberladung, um eine Fehlermeldung auszugeben .
[»]
Hinwe-is Wenn Sie der letzte Punkt interessiert, wie man eine variabel lange Argumentliste für Funktionen auswerten kann, dann möchte ich Sie auf mein Buch >Oe von Abis Z« hinweisen. Dieses Buch finden Sie außerdem als openbook unter http://www.galileocomputing.deloder auf der Buch-CD. Hierzu das Prinzip der Funktionsüberladung in der Praxis: 11 funclO . cpp f/include using namespace std :
Funktions · Prototyp int do_stuff ( in t par ) : int do_stuff ( int parI . i nt par2 ) ; double do_stuff ( double par ) ; double do_st uff ( double parI. double pa r 2 ) ; ~o i d do_stuff ( ); 11
int
main(~oid) [ int ~arl ; double va r2: 11 Au f ru f von Mint do_stuff (int) " ~arl - do_stuff ( 10 ) : cout « ~arl « ' \n '; 11 Au f ru f von "doub l e do stuff (double) " var2 - do_stuff ( 10 . 2 ) : caut « va r2 « ' \n '; 11 Aufruf von Mi nt do_stuff (int , i nt) varl - do_stuff ( 10 . 11 1: cout « va r l « ' \n '; 11 Aufruf von "double da_stuff(dauble , double) " var2 - do_stuff ( 10 . 11 . 11 . 22 ) : cout « var2 « ' \n '; 11 Falsch e r Funktionsaufr uf , daher . ) 11 . ) Au fr uf von Ovoid da_stuff( ... ) " da_stuff ( ' a ', ' b', ' e' ) : return 0:
Funktionsdefinit i on int do_stu f f ( i nt pa r ) [
11
Funktionen
return (par * 2) ;
int do_stu ff ( int pari . int par2 ) [ return (parI * par2) :
double do_stuff ( double par) [ return (par '" 2) :
double parI . double par2 ) [ double do_stuff return (parI * par2) ;
void do_st uff ( ... ) I cout « "Feh l er : Falsc her Funk ti onsaufruf\n ": Das Programm bei der Ausführung: 20
20 . 4 110 113 . 434
Fehler : Falscher Funktionsa ufruf Das überladen von Funktionen ist sicherlich eine tolle Sache in e++. aber Sie solllen Bedenken, dass Sie häufig denselben Effekt mit vorbelegten Standardwerten erreichen. Da der Compiler ja auch die Slandard-Typenumwandlung übernimmt, können Sie das Beispiel oben (»func10 .cpp") bis auf zwei Funktionen kürzen oder bis eigentlich auf eine - die zweite Funktion mit der Ellipse muss ja nicht unbedingt sein (Slilfrage). Hierzu dasselbe Beispiel nochma ls. nur mit der Verwendung von Standard parametern: /I func1l.cPP lI i nclude
us i ng namespace std : 11 Funktions -Prototyp double do_stuff ( double parI . double par2 - 2.0 ) : vo i d do_stuff ( .. ) :
int main(void) [ int va r l ; double va r2;
111
I
1.10
I
1
I
Grundlagen In C·....·
11 Au f ruf von Mi nt da_ stuft (int) " varl - do _ stu f f ( 10 ) : cout « va r l « ' \n ': 11 Aufruf von "double do_st uff (double) " var2 - do_stuff ( 10 . 2 ) : cout « va r 2 « ' \n ': fI Aufruf von Mi nt do_stuff (int . i ntl varl - do_stuff ( 10 , 11 ) : cou t « varl « ' \n ': 11 Au f ru f vo n "double do_s t uff(double , double) " var2 - do_stuff ( 10 . 11 , 11 . 22 ) : cou t « var2 « '\n '; 11 Falscher Fu nktionsaufruf . daher . ) 11 . ) Au fr uf von ·void do_st uff( ... ) " do_stuff ( ' a '. ' b ', ' c ' ) : retu r n 0 :
double do_stuff double parI . double pa r 2 ) I return (parI * pa r 2l :
void do_ stuff ( ) { cout « "Feh l er : Falsc he r Funkt i onsau f r uf \n" ;
Ich denke, der Voneilliegt auf der Hand. Wollen Sie zum Beispiel die Funktion
»do_stuffO« verändern bzw. erweitern, müssen Sie dies nur einmal - statt wie im Beispiel zuvor viermal - tun. Sofern sich also die einzelnen Parameter nicht grundlegend voneinander umerscheiden, sollten Sie immer die Standardparame· ter der Funktionsüberiadung vorziehen. 1.10.6
Inline·Funktionen
Beim Aufruf von Funktionen wird immer ein gewisser zusätzlicher Rechenaufwand benötigt. Ohne zu sehr ins Detail zu gehen, soll dies hier kurz erläutert werden. Wenn ein Programm eine Funktion aufruft, wird (wie bereits erwähnt) ein sogenannter Stack-Rahmen <Stack-Frame) eingerichtet. Der Stack ist eine zusätzliche Speichereinheit. die das Programm fur die aufgerufene Funktion verwendet. Der genaue Ablauf ist zwar architekturspezifisch, aber im Grunde gehen alle Architekturen ähnlich vor.
112
Funktionen
Zunächst wi rd die Rücksprungadresse auf dem Stack abgelegt. damil das Pro· gramm weiß. wohin es nach der Ausführung der Funktion zurückkehren kann. Dann wird für den Rückgabetyp platz auf dem Stack geschaffen. la Ufet beispielsweise die Deklaration ,.ißt do_stuff( ... .. )*', so wird Speicher für den Datentyp i nt reserviert. Neben dem Rückgabetyp wird selbstverständlich auch Speicher fUr die einzelnen Parameter der Funktion (falls verwendet) auf dem Stack bereitgestellt. Erst jetzt wird gewöhnlich die Funktion ausgeführt und somit auch die Defin ition . Das bedeutet. dass noch weiterer Platz au f dem Stack für die Definition der lokalen Variablen der Funktion benötigt wird. Für viele ei nfache Funktionen, die Sie bisher geschrieben haben , stellt sich die Frage, ob sich der Aufwand lohnt und man nicht gleich die paar Zeilen in die ma i n-Funktion schiebe Für solche Zwecke wurden in C++ die in 1i ne-Fun ktionen geschaffen. Stellen Sie das schlüsselwort in 1i ne vor eine Funktion, so wird der Compiler angewiesen, diese Funktion nicht als ei ne aufrufbare Funktion in den Maschinencode zu übersetzen, sondern den Code an der Stelle zu setzen, wo di e Funktion aufgerufen wird. Damit sollte der Aufwand , der bei einem Funktionsaufruf betrieben wird, verm ieden werden. Beispielsweise sei fo lgender Code gegeben: 11 func12 .cpp lIinclude us i ng namespace std : 11 Funktions· Prototyp
inline int max_int ( i nt a . int b l : i nt main(voidl I int va r 1 - 100 . var2 - 200 . ma !\ : max - max_i nt ( varl . var2 ) : cou t « max « " ist de r gröBere Wert\n ": re turn 0 :
11 Funkt j onsde f i nition
i nline i nt max_int ( j nt a , in t b ) { if(a)- b return ( a ) : else { return ( b ) :
113
I
1.10
I
1
I
Grundlagen In C·....·
Der i nl i ne-Theorie nach würde der CompHer aus diesem Quelleode nun Folgendes machen : 11 funcl2 . cp p lIinclude us i ng namespace std :
int main(voidl 1 int va r l - 100 . var2 - 200 . max : 1f ( varl >- var2 ) { max - varl; )
el se ( max - var2; )
cout « ma x return 0 ;
« •
ist de r grOBere Wert\n" ;
ob der Co mpiler dies aHerdings in der Praxis auch so macht. wie Sie wollen, entscheidet er doch letztendlich selbst. Wenn Sie eine Funktion mit in 1i ne notieren, schlagen Sie dem Compiler vor, diese als in 1i ne zu verwenden. Eine Garantie gibt es dafür allerdi ngs nicht. Dennoch haben Sie sehr gute Chancen , dass die Funktion als i nl i ne vom Compiler nominiert wird , wenn der Code der Funktion recht gering ist.
[» J
Hinwe is Sofern Sie eine Funktion als in l ine notieren und diese im Programm häufig an unterschiedlichen Stellen aufrufen, müssen Sie wissen, dass sich dadurch natürlich auch der Umfang des Maschinencodes erhöhen kann . Am sinnvollsten setzt man in 1 i ne-Funktionen in Schleifen ein, die sehr häufig ein und dieselbe Funktion aufrufen. Rich tig eingesetzt lässt sich damit die Lau fzeit verbessern un d der Objekteode gegebenenfalls reduzieren.
[» J
Hinwe is Mittlerweile sind die Compiler so klug, dass sie solche i nl i ne-Optimierungen selbst durchführen, auch wenn Sie eine Funktion vielleicht gar nicht als i nl i ne notiert haben . Bei alt der Hyste rie um das Thema "Opti mierung ~ sollte man nie das Ziel aus den Augen verlieren. Optimi erungen sollten immer nur dann durchgeführt werden. wenn es bei der Ausflihrung des Programms zu Engpässen (z. B. Wartezeiten) kommt.
Funktionen
( -Program mierer sollten nicht den Fehler machen, die in 1 i ne- Funktionen mit den defi ne-Makros zu vergleichen. Die defi ne-Makros werden vom Präprozessor expandiert, die i n1 i ne-Funktionen vom Compiler. Das bedeutet. dass bei den ( -Makros keine Typenüberprüfung stattfindet. Genau aus diesem Gru nd wurde n ja die i n1 i ne-Funktionen eingeführt. Mittlerweile steht auch dem (-Programmierer seit dem (99-Standa rd i n1 i ne zur Verfügung.
1.10.7
Rekursionen
Rekursionen sind Funktionen, die sich immer wieder selbst aufrufen und somit neu defi nieren, bis eine bestimmte Abbruchbedingung eintrifft. Fehlt diese Abbruchbedingung, wird sich die Rekursion äh nlich wie eine ungewollte Endlosschleife verhalten und den Rechner eventuell stark belasten. Schließlich for dert jeder Funktionsaufruf Platz auf dem Stack (Rücksprungadresse sichern; Rückgabewert; Platz fü r die Funktionsparameter; lokale Variablen; usw.). Da hierbei keine Funktion an dem Aufrufer zurückkehrt - bis die Abbruchbedingung gültig ist - , kann es passieren , dass der zur Verfugu ng stehende Stack für das Programm voll- bzw. überläuft (Stack Overflow). Ein klassisches Beispiel dafür dürfte die Berechnung der Fakultät einer Zahl f1 sein . So lau tet zum Beispiel die Fakultät der Zahl »6 .. 720 (errechnet aus 1* 2*3*4 .... 5*6). Somit kann man solange eine bestimmte Zahl um eins dekrementieren, bis die Abbruchbedingung 0 erreicht wurde. Das Beispiel dazu : 11 funcI3 . cp p lIinclude (iost ream) us i ng namespace std : 11 Funkt i ons- Prototy p
lang f akul( l ang n ) : int main(void) 1 lang val : va l - fakul( 6 ) : cout « "Faku ltat aus 6 ist va l - fakul( 10 ) : cou t « "Fakultat aus 10 ist return 0 :
«
val
«
' \n ':
«
val
«
' \n ':
11 Funkt i ons-Defint i on
lang f akul ( la ng n ) 1 if( n ) 1 return n * f akul(n -l) :
115
I
1.10
I
1
I
Grundlagen In C·....·
return 1· Das Programm bei der Ausführung:
Fakul t at aus 6 is t 720 Fakul t at aus 10 is t 3628800 Die Funktion »faku IO« ruft sich solange mit n*n-1 selbst auf. bis n gleich 0 ist. Hierbei könnte man auf n~ 1 auch verzichten. weil sich das Ergebnis nicht mehr verändern wird. Das hört sich in der Theorie ganz gut an, aber in der Praxis ist man mit der iterativen ProbJemJösung immer noch wesentlich effizienter. Hierzu die iterative lösung zur Fakultät eine r Zahl:
10ng f akul( ; nt n) i nt x - n ; wh ; 1e( .. x) n *- x ;
return n; Diese Funktion erfüllt denselben Zweck und ist auch noch einfacher und verständlicher. Wozu also Rekursionen verwenden. wen n zum einen der Code dadurch häufig schwerer zu verstehen ist und der Aufwand mit dem Stack enorm sein kann? Es gibt durchaus Dinge. die man mit Rekursionen einfacher und schneller lösen kann (etwa binäre Bäume. Sortierroulinen wie Quicksort usw.), troudem gibt es häufig auch eine iterative Lösung . Um sich allerdings mit dem Pro- und Contra ausei nanderzusetzen. müsste man sich schon mehr mit den ~ AIgorithmen~ befassen - aber das Thema ist schon sehr spezifisc h und vor allem umfangreich. weshalb man hie r bei Beda rfmit spezieller Literatu r gut beraten ist.
I»J
Hinweis Einen umfassenderen Einblick zu diesem Thema (»Rekursionen« und »Algorithmen«) finden Sie in meinem Buch »C von Abis l«, das Sie als openbook unter http://www.proni lt. de/oder auf der Buch-CD finden.
1.10.8
main-Funktion
Nach dem eH-Standard muss jedes ausführbare Programm maximal eine Funktion mit dem Namen ma i n() besitzen . Diese Funktion ist auch immer die erste Funktion. die beim Programmstan ausgefLlhn wird.
Präprozessor-Direktiven
Ebenfalls fordern nach dem e++-Standard di e ma in-Funktionen den Rückgabewert i nt. Zwar findet man in (zumeist älteren) Buchern immer noch folgende Schreibweise: void mai n( voi d) I // Anweisungen Aber nach neuem Standard is t dies nicht mehr richtig. Der Compiler dürfte sich bei dieser Verwendung sowieso bei Ihnen beschweren. Richtig ist also immer: int main( void) I // Anwe i sungen ret ur n 0 ; Weiterhin ist auch eine Variante mit zwei Parametern erlaubt: i nt mainlint arge , eha r **argvl f return 0 ; Damit können Sie der ma in-Funktion beim Programmstart auch einige Argumente mitgeben. Die Namen der Bezeichner sind hier zwar fre i wählbar, aber in der Praxis werden gewöhnlich die Namen a rge und argv verwendet. über den Rückgabewert der mai n-Funktion en sind schon regelrechte ,.Flamewars« ausgebrochen. Generell ist dieser Wert abhängig von der Umgebung des Betriebssystems . Unter Linux/UNIX bedeutet ein Rückgabewert von 0, dass ein Programm erfolgrei ch beendet wurde, alles andere bedeutet, dass etwas fehlgeschlagen ist. So können Sie unter Li nux/UNIX mit Syou@host
> echo
$1
o ermitteln, welchen Rückgabewert das zuletzt gestartete Programm (bzw. Kommando) zurückgegeben hat. Andere Betriebssysteme können auch einen anderen Rückgabewert als erfolgreiche Beendigung erwarten , was bedeutet, dass es hierbei keinen ,.ponablen« Standard gibt, aber der Konvention nach wird hierfür immer 0 verwendet.
1 . 11
Präprozessor-Direktiven
Bevor das C++-Programm vom Compiler übersetzt wird, wird der Präprozessor aktiv . Dieser fasst u.a. String-Literale zusammen, entfernt Zeilenumbrüche mit
117
I
1.11
I
1
I
Grundlagen In C·....·
vorgestelltem Backslash, löscht die Kommentare des Quelltexts sowie die Whi tespace-Zeichen zwischen den Tokens . Zusammengefasst fühn der Prä prozessor rein textuelle Manipulationen am C++-Quelilexl durch. Ist d er Präprozes-
sor fenig, erhält der Com piler den so angepassten Quelltext zum Übersetzen. Diese für den Präprozesser gedachten Zeile n werden Direktiven (oder auch Präprozessor-Direktiven) ge nannt und beginnen immer mit dem Hash-Zeichen ,,1/'" am Anfang einer Zeile und enden beim Zeilenende . Beachten Sie, dass im Gegen· satz zu einer e H -Anweisung eine Direktive nicht mit einem Semikolon beendet wird . Fügen Sie hierbei ei n Semikolon am Ende ein, kann dies zu unerwarteten Ergebnissen führen. Sollten Ihre Präprozessor-Direktiven länger werden, können Sie diese in der nächsten Zeile fonsetzen, wenn Sie ein Backslash an das Zeilenende setzen. Da die Direktiven nicht vom Gültigke itsbereich abhängig sind, können diese irgendwo bis zum Ende des Quelltexts stehen. Pro Zeile ist eine Direktive erlaubt. Die Hauptanwendungsgebiete von Präprozessor-DirekLiven sind das Ein kopieren von Header- und/oder Quelldateien, das Einbinden symbolischer Konstanten sowie die bedingte Kompilierung. 1.11 .1
Die #define-Direktive
Mit der /fdefi ne-Direktive lassen sich einfache Makros realisieren. Die Syntax: lIdefine makroname ersetzungs·name Mit dieser Direktive veranlassen Sie den Präprozessor, überall im Quelltext »Ma kroname« durch den Ersetzungstext "Ersetzungsname", zu ersetzten. In der Praxis schre ibt man den Makronamen gewöhn lich groß . Ei n ei nfaches Beispiel hierfür: 11 definel.cpp llinclude I def1ne NUHBE R 5 ' d c f 1nc STRING "H al lo"
using namespace std : i nt main(vo i dl 1 for ( int i - 0 : i < NUHB ER: i ++ ) 1 co ut « STRING « ' \n ': retur n 0;
".
Präprozessor-Direktiven
I
1.11
Im Beispiel wurden zwei ei nfach e Ersetzungsmakros NUMBER mit dem Wert ,.5« und STRl NG mit der Zeichenfolge ,.Hallo« definiert. Somit werden vom Präpro· zessor alle Makros mit entsp rechenden Namen du rch den entsprec henden Wen ersetzt. Der Compil er würde praktisch vereinfac ht folgenden Quellcode zum Übersetzen erhalten: 11 de f inel.c pp llinclude
int main(void) I fore i nt i .. 0 : ; < 5: i++ 1 cout « "Hallo · « "\n" : retu r n 0 :
Hinweis Dies ist natürlich sehr vereinfacht dargestellt, da nach dem Präprozessorlauf erheblich mehr zu finden ist, als man annehmen würde. Jeder Compiler bietet einen Schalter an, der Ihnen den Quelltext nach dem Präprozessor und vor dem Compilerlauf ausgibt (bei der GCC ist dies bspw. der Compilerflag - E).
Neben der Möglichkeit, einfache Makros zu definieren, können Sie auch parametrisierte Makros velVlenden. Ein einfac hes Beispiel: #de f ine SQRE(xl ( (xl · (xl)
11 Quadrieren
Im betrach teten Fall haben Sie den formal en Parameter x. Dieser kann auf der rechten Seite des Makros belieb ig oft velVlendet werden. Dabei muss beachtet werden. dass diese r forma le Parameter ebenfalls auf der rechten Seite in Klammern stehen muss. Folgendes Beispiel demonstriert Ihnen, was passiert, wenn Sie ke ine Klammerung verwenden: 11 de f ine2.cPP lIinclude lIdefine SQ REl(x) «(x) (xl) lIdefine SQ RE2(x) (x * xl us i ng namespace s t d :
I/Quadrieren I/Quadrieren
int main(voidl I int wert .. 5 : cou t « "SQ REl(wertl : «SQREl(wertl« ' \n '; cout «'SQRE2(wert) : «SQRE2(wert)« ' \n ': cou t «'SQRE}(wert+}l : «SQREl<wert+}J« '\n ': cou t « "SQ RE2(wert+l) : " « SQRE2(wert+lJ « '\n ': return 0 ;
119
[«l
I
1
I
Grundlagen In C·....·
Das Programm bei der Ausführung : SQREl(we r t) : 25 SQRE2(we r t) : 25 SQRE1(we r t+ll : 36 SQRE2(we r t+ll : 11 Der Präprozessor versteht nämlich keine C++-Syntax und aus SORE2( x ) (x " :1()
wird (wert+l " wert+l1 und das Ergebnis ist nicht das gleiche wie «wert+l1 " (wer t +l11 Natü rlich kön nen auch mehrere Argumente als Parameter eines Makros verwendet werden: #de f ine MAX(x . y) ( ( x)<- (yl ?(y) : (x) ) Das Makro e rmittelt den größeren Wert von )IX« und »y«. Wie bei einer Funktion, werden hierbei die einzelnen Argumente mit einem Komma getrennt. Und Sollte sich mal ein Makro über mehrere Zeilen strecken, so wird die nächste Zeile auch noch als Makro betrachtet, wenn die vorherige Zeile mit einem Backslash beendet wurde: 11 Zwei Werte tauschen lIdefine TAUSCHE{ x. y) i nt j ; \ j- x;
x- y: y- j :
1 \
\ \ \
}
#define oder co nst
Ganz klar, die define-Direktive ist unverzichtbar, wenn es um Dinge wie die bedingte Kompilierung geht, aber in der Praxis w ird häufig cons t vorgezogen. Für einen Einsteiger in die Programmiersprache CH ist der Unterschied zunächst nicht ganz einsichtig, aber mit fortschreitenden Kenntnissen werden die folgen den Punkte klarer erscheinen:
.. CH überprüft die Syntax einer const-Anweisung sofort. Bei lIdef i ne findet erst eine Übe rprüfung stalt, wenn das Makro verwendet wird.
120
Präprozessor-Direktiven
..
Eine const-Anweisung kann auf so zie mlich j eden CH-Typ defi niert we rden (also auch Klassen und Strukturen). I/defi ne hingegen beschränkt sich nur auf einfac he Konstanten.
..
cons t verwendet die üblichen CH-Regeln ftir die Gühigkeitsbereiche (siehe Kapitel 3). #defi ne-Konstanten sind immer und überall güllig.
..
cons t verwendet die CH-Syntax - #defi ne hat eine eigene Syntax.
In li ne- Funktionen oder pa rame trisierte Makros
Das parametrisierte Makro Udefine SQ RE2(x) ( x * xl
11 Quadrieren
hat bereits gezeige dass solche Konstrukte unangenehme Nebeneffekte haben können. Daher werden auch hierin der Praxis meistens i n1 i ne-Funktionen bevorzugt. in1ine int SORE( const int x ) f ret urn ( x • x ) : Jetzt hat man allerdings den . Nachteil.. , dass man die in 1i ne-Funklion nur für de n Datentyp i nt verwenden kann. Beim Makro hingegen war man nich t vom Datentyp abhängig. Man kann jetzt zwar für jeden Datentyp eine in1ine-Funktion schreiben, oder aber man verwendet eines der besten Features von C++, den Funktions-Template (siehe Abschnitt 5.1).
1,11 .2
Die #undef-Direktive
Der Geltungsbereich von symbolischen Konstanten bzw. Makros reicht vom Punkt der Deklaration mit #def i ne bis zur Aufhebung mit #undef . Die Aufhebung mittels I/ undef ist aber optional. Wird I/undef nicht verwendet, reicht der Geltungsbereich bis zum Dateiende. Das fo lgende Listing wird sich nicht übersetze n lassen, weil beim Compilerlauf der zweite Makroname . WERT« nicht mehr definiert ist, da dieser zuvor mit lIundef aufgehoben wurde. 11 undefl . CPP lI i nclude 11 Hakronamen WERT definieren #defl ne WERT 5 using names pace std :
121
I
1.11
I
1
I
Grundlagen In C·....·
i nt main(voidl [ cout « WERT « "\n " : // Definie r te n Makronamen aufheben #unde f WERT 11 ll! Comp i lerfehler !!! cOut « WERT « "\n ": return 0:
1.11.3
Die #indude-Direktive
Mit der Präprozessor·Direktive i ncl ude kann ein Programm Quellcode aus einer anderen Datei einkopieren. Die Syntax: lIinclude tokens Diese Angabe bewirkt. dass die Zeile durch den kompletten Inhalt der in ,.tokens .. angegebenen Datei ersetzt wird. Hat die Direktive folgende Form lIinclude so wird gewöhn lich im include·Verzeichnis des Compilers bzw. in ei nem spezifizierten Verzeichnis nach dieser Datei gesucht. Auf UNIX-Systemen finden Sie dieses Verzeichnis gewöhnlich in /usr/include oder lusrllocallinclude - unter MS Windows ist dies abhängig vom Installationsverzeichnis des Compilers. Meistens handelt es sich bei der Angabe mit den spitzen Klammern um Standard· Headerdateien wie beispielsweise: llinclude Hier gleich ein Hinweis fü r Einsteiger in die Programmierung, da häufig die Headerdateien mit Bibliotheken gleichgestellt werden. Die Standard-Headerdateien wie i ostream werden verwendet. um die von den Bibliotheken-Funktionen verwendeten Datenstrukturen und Makros zu definieren. Natürlich kann man auch eigene Headerdateien schreiben , um beispielsweise Konstanten und Datenstrukturen darin zu speichern. Dies ist besonders hilfreich, wenn sich ein Programm über mehrere Dateien erstreckt - was gerade beim Programmieren im Team der Fall ist. Solche Angaben zu ,.lokalen.. Headerdateien werden zwischen doppelte Anftihrungsstriche gestellt: lI i nclud e "date i . h" Natürlich können Sie auch einen relativen Pfad zur Datei angeben Ifi nclude " .. / .. / .. /datei . h"
122
Präprozessor-Direktiven
I
1.11
oder aber auch ei nen absoluten pfad Ifnclude " /home/user/datef.h" Hinweis Mittlerweile unterstützt auch MS Windows die normalen Schrägstriche als Verzeichnistrenner. Fruher musste man immer den rückwärts gerichteten Schrägstrich alias Backslash dafür verwenden. Gewöhnlich werden Headerdateien flir Definitionen, Deklarationen, Makros, Konstanten und InHne-Funktionen verwendet. Es ist aber möglich, Code in eine Headerdatei zu schreiben und zu verwenden . Bei einem guten Programmierstil würde man aber den Code in cpp-Dateien schreiben und den Rest in Headerdateien. Natü rlich ist es auf der anderen Seite auch möglich. cpp-Dateien von ei ner anderen Headerdatei aus einzubinden (mit include), was aber auch nicht unbedingt als »stilvolle" Programmierung gilt. In den nächsten Abschnitten werden Sie ohnehin nur die Standard- Headerdateien verwenden, aber mehr darüber, wie Sie zum Beispiel eigene Headerdateien erstellen können, werden Sie in Abschnilt 4.2.7 erfahren. 1.11.4
Die Direktiven #error und #pragma
Mit der lIerror-Direktive können Sie den Obersetzungsvorgang des Compilers mit einer eigenen Fehlermeldung abbrechen. Dies ist sinnvoll. wenn zum Beispiel eine Funktion oder ein Programm nicht fertig ist oder man einen wichtigen Hinweis hinterlassen will. Ein einfaches Beispiel: 11 e r rorl . cpp llinclude
int main{void) I 11 ... viele Anweisungen
"
.. .
I/error "Bitte neue Headerdatei besorgen ! " 11
return 0 :
11 viele De f i nitionen
123
[« )
I
1
I
Grundlagen In C·....·
Der Compiler beendet die Übersetzung mit der Fehlermeldung, dass man sich doch bitte die neue Headerdatei besorgen möchte. Anschließend kann man diese Direktive auskommentieren ode r gleich ganz entfernen. Ifpragma sind compilerspezifische Direktiven und von Compiler zu Compiler verschieden. Wenn ein Compile r eine bestimmte IIpragma-Direktive nicht kennt. wird diese ignoriert. Mithilfe dieser Pragmas können Compiler-Optionen definiert werden, ohne mit anderen Compilern in Konflikt zu geraten. Da das Verhal· ten von Ifpragma-Anweisungen stark systemabhängig ist, soll darauf nich t näher eingegangen werden. Welche Pragmas Ihr Compiler unterstützt, entnehmen Sie bitte dem C++·Manuallhres Compilers. 1.11 . 5
Bedingte Kompilierung
Oft steht man vor dem Problem. einen Code schreiben zu müssen. der auf meh· reren Betriebssystemen laufen soll. Zwar ist dies, dank Standard. heutzutage kein un lösbares Problem mehr, aber dennoch gibt es immer wieder kl einere Differenzen . Umjetzt nicht gleich fli r jedes Betriebssystem einen eigenen Code zu schreiben, gibt es die Möglichkeit der bed ingten Kompilierung. Neben den unterschiedlichen Betriebssystemen gibt es außerdem noch un ter· schiedliche Compiler, die auch einige kleine Eigenheiten mitliefern. Auch hier kann man mit einer bedingten Kompilierung dafli r sorgen . dass diese ,.Eigenheit .. auch verwendet wird. wenn der Quellcode mit dem entspreChenden Compiler übersetzt wird. Folgende drei Direktiven von Bedingungsanweisungen für den Prä prozessor stehen Ihnen zur Verfügung:
'if Konstanter_Ausdr uck 'lfdef Konsta nter_Ausdruc k 'l f ndef Ko nstante r-Ausdruck Mit der fli f- Direktive überprüfen Sie. ob der Ausdruck (es muss ein Konstantenausdruck sein) einen Wert ungleich 0 zurückgib t. lIi fdef überprüft, ob ein Bezeichner »Konstanter_Ausdruck« bereits definiert ist, und mit /fi fndef wird gepruft, ob der Bezeichner ,. KonstanteCAusdruck .. dem Prä prozessor nicht bekannt ist - also das Gegenteil von lIi fdef . Bei den Bedingungen dürfen mit den logischen Operatoren 11 bzw. && mehrere Ausdrücke miteinander verknüpft und somit überprüft werden :
/f ifde f Ausdruckl 11 Ausdruck2
124
Präprozessor-Direktiven
Einer dieser drei Bedingungsanweisungen für den Präprozessor können nun beliebig viele Zeile n Code fo lgen und weitere Direktiven: 'e11f Konstanter_Ausdruck 'el se 'e nd1 f Mit der Direktive Hel i f können beliebig viele weitere Ausdrücke überprüft werden. Ei ne fiel se-Direktive ist option al und kann am Ende von beliebig vielen lIel i f-Direktiven oder mindestens einer lIi f , lIi fde f oder fli fnde f-D irektive fo lgen. Am Ende muss immer eine Ilend i f-Direk tive stehen, wodurch der Präprozessor weiß, dass hier die bedi ngte Kompil ierung zu Ende ist. In der Praxis sieht ein solches Konstrukt wie folgt aus : lI ifde f DIES define FUNCTION(param) linux_func(pa ram) lIeli f J ENES 11 define FUNCTION(param) dOLfunc(param) lIelse 11 define FUNCTION(param) unknow_ func(param) lIendi f 11
Oder: lli f LANG - - 1 include "german_heade r.h " lIel i f LANG - - 2 11 include "eng l ish_heade r. h" lIe 1se 11 er ror "Es wird nur Englisch und Deu t sch lIendi f 11
unterstOtzt~
Ein einfach ausfUhrbares Beispiel in der Praxis sieht demnach wie folgt aus: 11 ifde f 1.cp p lIinclude us i ng namespace std :
lIi fde f _MS DOS_ 11 Programm lauft unter einem echten HS - DOS 11 Hie r kommt der Code dafQr hin 11 define CODE "MS-DOS " lIel if _ WIN32_ 11 _HSCVER 11 Programm lauft i n einer Win32 Konsole . ode r 11 wurde mit dem Microso f t -Compi l er Obe rsetzt 11 define CODE "Win32 ode r MS -VC++\n" lIelif _ unix_ 11 _ linux_
125
I
1.11
I
1
I
Grundlagen In C·....·
Progra mm wi rd unter einem Li nux/U NIX· System Obe rse tzt 11 define CODE "Linux /U NIX\n " He 1se 11 Pr3pro zessor konnte keines der Systeme ausmache n lIdefine CODE "Unbekanntes System\n " lIendi f 11 11
int main(lIoid) cou t
«
CODE ;
ret ur n 0 ; Je nachdem, auf welchem System das Programm übersetzt w ird , erhält man eine entsprechende Ausgabe. Im Grunde ähnelt das Ganze den gewöhn lichen i fAbfragen \Ion C++. nur fü r den Präprozessor. Wie schon erwähnt, umfasst die bedingte Kompilierung neben maschinen·spezifischen Abfragen auch comp ilerspezifische. Daher fo lgt nun e in Überblick zu den gängigen Compilern und Systemen und den entsprechenden Konstanten, die hierfür verwend et werde n könn en. Zunächst die Konstanten für die Com piler (Tabelle 1.22): I
Compiler
_oc
Microsoft C ab Version 6.0 Microsoft Quick C ab Version 2.51
_T URBOC
Borland Turbo C, Turbo C++ und BC++
_BORLANQC
Borland C++
_lTC
Zortech C und CH Symantec CH
sc _IIATCOHC_ _GN UC_
WATCOMC Gnu C
_EHX
Emx Gnu C
Tabelle 1.22 Konstanten fur bestimmte Compiler Und jetzt n och Konstanten, die das Betriebssystem betreffen (Tabelle 1.2 3) : I
Betnebssystem
_ unix_ oder _uni x
UN iX-System
_ MS_DOS
MS·DOS
Tabelle 1.2] Konstanten fiir bestimmte Betriebssysteme
126
Präprozessor-Direktiven
I
Betriebssystem
_W I N32
Windows ab 95
_OS2
052
-
Wl ndows
Ziel5ystem Windows
_N T_
Windows NT
1 j nux
linux
_freeBSD_
FreeBSD
_OpenBSD_
OpenBSD
SGLSOURCE
SGI- IRIX mit Extension ·. 5g1
_MI PS_J SA
SGI-IRIX
_hpu x
HP- UX
Tabelle 1.23
Konstanten für bestimmte Betriebssysteme (forts.)
Die meisten Projekte bestehen gewöhnlich aus mehreren Header- und Codedateien. All diese Dateien werden beim übersetzen zunächst zu Objekldateien kompiliert und anschließend von einem Linker zu einer ausfUhrbaren Datei zusammengefasst. Nehmen wir mal an, Sie haben zwei Headerdateien .file1. h. und ,.file2.h ... die die Headerdatei »myfile. h« inkludieren. Dann würde praktisch zweimal be im Präprozessorlauf die Datei . myfi1e.h« eingelesen. Damit würden Konstanten mehrmals definiert, was noch kein Fehler wäre, aber sobald eine Datenstruktur oder eine Union mehrmals definiert würde, ist dies definitiv ein Fehler. Um dieses Problem zu lösen, muss man nur in der ents prechenden Headerdatei - bei der Gefahr besteht, dass sie mehrmals inkludiert wird - fo lgenden Code am Anfang einfügen: lIi f ndef MYFILE_H 11 define MYF I LE_H 11 Hie r kommt der Inhalt der Code- Da te i hi n lIendi f In diesem Fall, wenn die Headerdatei . myfile.h« bereits eingebunden wurde, we rden alle weiteren Definitionen bzw. der Code bis zur !lend; f -Direktive versteckt, sodass es nicht mehr zu Problemen kommen kann. Wurde die Headerdatei »myfile.h« noch nich t defin iert (lfi fnde f), wird di ese mit I/defi ne definiert und der entspreche nde Code einkopiert.
127
I
1.11
I
In diesem Kapitel werden die höheren Datentypen beschrieben. Dabei handelt es sich einfach um Typen, die aus den Basisrypen gebildet werden. In e++ sind dies Zeiger, Arrays (oder auch Vekto ren), Referenzen und Strukturen. Klassen gehören eigentlich auch in dieses Kapitel, aber damit ich hier nicht ständig vor- und zurückgreifen muss, werden die Klassen zu gegebener Zeit behandelt.
2
Höhere und fortgeschrittene Datentypen
2.1
Zeiger
Zeiger werden in C/C++ immer noch als das ,.komplizieneste« Thema hingestellt. Dabei sind Zeiger eigentlich gar nich t so schwer zu verstehen, wie dies auf den ersten Blick scheint - wohl aber für einen Anfänger eine immer noch recht große Hürde. Das Problem des Verstehens von Zeigern ist zunächst nicht die Funktionalität, sondern das »wozu«. Aber leider muss ich hierbei erwähnen, dass sich der Aha-Effek t erst später einstellen wird. Hier geht es zunächst nur um die Grundlagen für die Zeiger. Es wurde ja bereits erwähnt, dass alle in diesem Kapitel beschriebenen Typen aus den Basisdatentypen gebildet werden - also auch ein Zeiger. Alle Basisdatentypen müssen vom Rechner verwaltet werden. Dies geschieh t, indem das System den Wert, die Größe und die Adresse im Arbeitsspeicher ablegt: int va r ,. 10 0:
Hiermit wird praktisch die Variable "var« mit dem Wert ,.100.. und der Größe (abhängig von der Rechnerarchitektur) von vier Bytes im Arbeitsspeicher an einer bestimmten Adresse abgelegt. Im Arbeitsspeicher ergibt sich dadurch fol gendes Bild (Abbildung 2.1). Bei der Adresse (hier 0000) handelt es sich um eine erfundene Adresse. Welche Adresse »var« bei Ihnen im Arbeitsspeicher wirklich belegt. können Sie mit dem Adressoperator ,,&« vor der Variablen ermitteln: 11 zeiger1.c pp llinclude
"9
I
2
I
Höhere und fortgeschrittene Datentypen
us ing namespace std ; int main(void) I int va r - 10 0; cout « "Adresse von va r : " return 0 ;
0000
100
«
&var
«
' \n ';
,oe
0004
0006
oooc
0200 11 _-I I _________ 1 I 1
Abbildung u
Speicherbelegung einer Variablen im Arbeitsspeicher
Das Programm bei der AusfUhrung: Adresse von var : 0..:22ff74
Beachten Sie aber, dass sich die Adressierungen vom Rech nertyp unterscheiden können . Wie viel Platz die Variable »var« im Arbeitsspeicher benötigt, geben Sie ja bereits bei der Deklaration der Variablen durch den Datentyp an.
[»]
Hinweis Der Compiler unterscheidet zwischen dem unären Adressoperator ,.&.. und dem binären bitweisen UND »&«, da der unäre Adressoperator einen Operanden, der binäre hingegen zwei benötigt.
2.1,1
Zeiger deklarieren
Die Syntax der Deklaration eines Zeigers sieht wie folgt aus: Typ *zeiger :
Der »Typ« des Zeigers muss immer vom selben Typ sein wie der Datentyp des Speiche rpl atzes, auf den der Zeiger verweist. Zeiger sind also typengebunden.
[»]
Hinweis Es sollte erwähnt werden, dass es mehr gibt als Zeiger auf einfache Spe i ~ cherobjekte - wie hier am Anfang mit den Basisdatentypen gezeigt wird.
130
Zeiger
In der Praxis sieh t diese Deklaration so aus: 11 Dekla ration
char *cptr ; 11 Dekla ra tion t nt *ipt r; 11 Dekla ration double *dptr :
"" ""
Zeige r Zeige r
"" Zeige r
,,' ,,' ,,'
char-Variable int·Variable double-Variable
Sie können zur Deklaration eines Zeigers zwei Schreibweisen verwenden: 11 Sternchen vor dem Zeiger - Namen
t nt "ipt r l : // Sternc hen nach dem Typ en"Namen i nt * ipt r2 : Welche Variante Sie auswählen, bleibt Ihnen überlassen. We r zuvor bereits in C programmiert hat, wird sich mit der ersten Schreibweise recht schnell als (-Geübter "oute n ~ , da diese vorwiegend in (eingesetzt wurde. Die meisten (++Programmierer verwen den die zweite Variante (vielleicht um sich nicht als al[er ( -Programmierer enttarnen zu lassen) - soviel zum ,.Handlesen« eines Programmierers. Beachten Sie allerdings, dass Sie mit der folgenden Zeile int * iptrl , iptr2 ; nur einen Zeiger deklariert haben, So erscheint der ( -Stil einer Zeiger-Deklaration in puncto Überblick besser geeignet tnt *ip trl , *i ptr2 : Das .. Sternchen ~ , das bei einem Zeiger verwendet wird, wird als (unärer) Indirek· tionsoperator bezeichnet.
2 .1.2
Adresse im Zeiger speichern
Es wurde bisher immer noch nicht erwähnt, wozu ein Zeiger verwendet wird. Ein Zeiger iSI im Grunde eine einfache Variable, die eine Speicheradresse im Arbeitsspeicher aufnehmen kann. Das hört sich zunächst nich t spe ktakulär an, aber im Verlauf des Buches werden Sie Ihre Meinung ändern. Um die Adresse einer Variablen in einem Zeiger speichern zu können, benötigt man wiederum den unären Adressoperator ,. & ~ . Beispiel: int va r - 100 ; int iptr ; 11 tpt r zeigt auf var 1ptr - har;
131
I
2.1
I
2
I
Höhere und fo rtgeschrittene Datentypen
Hiermit weisen Sie dem Zeiger »iptre die Adresse von Abbildung soll diesen Vorgang näher demonstrieren. 0000
~ vare
zu. Die fo lgende
HXl
000' 000'
f-----1 oooc 1----1,
,, ,, ""'" 11_0000--11, ,
ip"
1_________ 1
Abbildung 2.2
Adresszuweisung an Zeiger
In dieser Abbildung sehen Sie, dass der Zeiger »iptre die Adresse der Variablen »var« (hier 0000) bein haltet Man spricht hierbei auch von einer Indirektion, weil
auf das eigentliche Speicherobjekt (hier »var«) nicht direkt. sondern indirekt über einen Zeiger (»iptr«) zugegriffen wird. Wollen Sie nun ausgeben . welche Adresse ein Zeiger beinhaltet, so benötigen Sie im Gegensatz zu den normalen Basisdatentypen keinen Adressoperator, da ein Zeiger j a eine Adresse speichert. Verwenden Sie dennoch den Adrcssoperator, so wird die Speicheradresse des Zeigers selbst ausgegeben. 11 zei ge r2 . Cpp
#include using namespace std ; int main(void) 1 int var - 10 0 : int * i ptr ; iptr - &var ; (ou t « "Ad re sse von va r rout « " ipt r VPfWP ; st auf (out « "Adresse von ipt r return 0 ;
Das Programm bei der Ausführung: Adresse von var i ptr ve rweist auf Ad resse von i ptr
132
Ox22 ff 74 Ox22 ff 74 Ox22ff70
« «
«
&var « ' \n ' ; iptr « ' \n ': &iptr « ' \n ';
Zeiger
2.1·3
Zeiger dereferenzieren
Jetzt können Sie zwar Adressen in einem Ze iger von anderen Variablen speichern, aber in der Praxis werden Sie wohl kaum mit Adressen arbeiten wollen, sondern mit echten Werten . Um di es zu realisieren, wird der unäre lndirektionsoperator (*) verwendet. Hierzu das Beispiel: 11 zeiger3 . cpp lIinclude (iostream) us i ng namespace std :
int main{void) 1 int va r - 100 : int tmp ; int* iptr ; iptr - &var ; 11 Dere fe renzie r ung tmp - *1ptr;
cout « cout « cout « cout « return
"Ad resse von var "We r t von var "Adresse von tmp -Wert von tmp
« &var « ' \ n':
«
var
«
' \n ':
« &tmp « '\ n'; « tmp « ' \n ':
I) ;
Das Programm bei der Ausftihrung:
Adresse vo n var Wert von var Adresse vo n tmp Wert von tmp
Ox22ff7 4 100 Ox22ff70 100
Nachdem Sie dem Zeiger "iptr« die Adresse von "var« zugeo rdnet haben, wurde indirekt über
tmp - *iptr : .. iptr« auf den Wert von "var« zurückgegriffen . Diese Zeile entspricht somit fol· gender Anweisung:
tmp - va r; Die Auflösung der Adresse von der Ze igervariablen wird als Dereferenzierung bezeichnet. Natürlich lässt sich eine solche Dereferenzierung auch anders verwenden: 11 zeige r4 .cPP llinclude (iostream)
'33
I
2.1
I
2
I
Höhere und fo rtgeschrittene Datentype n
us iog namespace std ; iot maio(void) I i ot va r - 100 : iot tmp : iot * i ptr : iptr ~ &var: 11 Dere f e renzie rung
tmp - *;ptr ; cou t « "Adresse von var cou t « "We r t von va r cou t « "Ad resse von tmp cou t « ~W e r t von tmp
« « « «
&va r « var « &tmp « tm p «
« « « «
&va r « ' \n ' : var « ' \0 ' : &tmp « ' \n ' : tm p « " \n\n" ;
« « « «
&va r « var « &tmp « tm p «
' \n ' ; ' \n ': ' \n '; " \n\n" ;
11 Dere f e renzie ruog
*i ptr cou t « cou t « cou t « cou t «
200 ; "Ad resse von "We r t "Ad resse ,," tmp
"" '"
~W e r t
,,"
'"
tmp
11 Oere f erenzie r ung
*iptr -- 5 ; cout « "Ad resse von var cout « "We rt von var cout « "Ad resse von tmp cout « "Wert von tmp re turn 0:
Das Prog ramm bei der Ausfuhrung: Adresse von var Wert von var Adresse vo n tmp Wert von tmp
Ox22 ff7 4
Adresse vo n var Wert von var Adresse vo n tmp Wert von tmp
Ox22ff7 4
Adresse vo n var Wert von var
Ox22 ff7 4
' 34
100
Ox22 ff7 0 100
200
Ox22ff7 0 100
195
' \n ' ; , \n ' ; ' \n ' ; " \n\n " ;
Zeiger
Ox22ff70
Adresse von tmp Wert von tmp
100
Mit der Anweisung
*ipt r - 200 : wurde im Beispiel der Wert de r Variablen »var« indirekt über die Adresse, die aus dem Pointer »iptr« bekannt ist und auf denselben Speicherbereich verwe ist, verändert. Der Inhalt von ~ tmp " bleibt unverändert, da diese Variable einer anderen Speicheradresse zugeordnet ist. 0000
200
'"
0004
100
tmp
1------1
0008
oooe I I I I I
020e
I
I I I I I
0000
I
l
iplr
I
1______ - - - ,
Abbildung 2.3
Auflösung der Adresse einer Zeigervariablen
Die nächste Dereferenzierung mit *; pt r ' · 5 :
soll zeigen , dass mithilfe des Indirektionsoperators auch arithmetische Operationen möglich sind. Folgende Rechenoperationen können mit einem Zeiger u nd auf dessen Adresse verwendet werden: ..
Ganzzahlwerte erhö hen
.. Ganzzahlwerte verringern ..
Inkrementieren
..
Dekrementieren
Des Weiteren sind bei Verwendung eines Zeigers natürlich auch die Vergleichs-
operatoren <, >< , ! - . --. <- und -> erlaubt. Die Verwendung ist aber nur si nnvoll , wenn die Zeiger auf Elemente eines Arrays (Siehe Abschnitt 2.3) zeigen oder bei den Parametern von Funklionen, um Arbeitsspeicher zu sparen .
135
I
2.1
I
2
I
Höhere und fortgeschrittene Datentypen
Ein Problem im Umgang mit Zeigern entsteht wenn Sie einen Zeiger dereferenzieren, der auf kein gültiges Objekt zugreift - also keinen speziellen Wert erhalten hat. Beispiel: int * iptr: *ip t r - 100 ; Hier wurde dem Zeiger indirekt der Wert 100 zugewiesen. Da dem Zeiger zuvor aber noch kein Speicherobjekt übergeben wurde. auf das dieser verweist, wird hierbei auf eine Zufallsadresse (meistens ein zufälliges Bitmuster, das vom Linker erzeugt wurde) zugegriffen. Befindet sich diese Speicheradresse außerhalb des Speicherbereichs vom Programm. so haben Sie eine Speicherschutzverletzung. Hierfür wird der Zeiger gewöhnlich mit 0 initialisiert, Damit wird angezeigt, dass dieser Zeiger noch auf kein gültiges Speicherobjekt zeigt. und somit darf dieser Zeiger auch nicht dereferenziert werden : 11 zeiger5 .cPP lIinclude (iostream> us i ng namespace std :
int main{void) I i nt * ipt r - 0 : if ( ipt r -- 0 ) I // iptr verweist noch au f ke i n Objekt return 0: C-Programmierer werden sich hier fragen. wo die Konstante NlJ LL geblieben ist. Da nicht genau gesagt werden kann. ob NlJLL mit 0 oder {void *lO definiert ist. kann letzteres bei C++ zu Problemen führen, weil man ein Typencas ting vornehmen muss. um den void*-Wert einem Zeiger zuzuweisen. Um jetzt ein wenig das Tempo herauszunehmen, hier eine kurze Zusammenfassung. Jede Variable besitzt eine Adresse im Speicher. Um auf die Adresse einer Variablen zuzugreifen. wird der Adressoperator »&." verwendet. Diese Adresse kann man auch in einem Zeiger speichern. Ein Zeiger besteht aus dem Typ des Speicherobjekhgefolgt vom Indirektionsoperator »' " und dem Zeigernamen. Um auf den Wert, der in der im Zeiger gespeicherten Adresse abgelegt ist, zuzugreifen, muss der Indirektionsoperator . "" verwendet werden. Man spricht hierbei von einer Dereferenzierung. Überblick
136
Zeiger
Zeiger, die auf andere Zeiger verweisen
Natürlich können Zeiger auch auf andere Zeiger verweisen:
int *iptrl : int *iptr2 : iptrl - iptr2 ; Hiermit verweisen heide Zeiger auf dieselbe Adresse . Ein einfaches Beispiel dazu: 11 zeiger6.c pp lIinclude (iostream) us i ng namespace std :
int main{void) 1 int* iptrl - 0: i nt* iptrZ - 0: i nt wert - 10 0: i ptrl &wert : i ptrZ - i ptrl : cout « "i ptrl verlole i st auf cout « "i ptr2 verwe i st auf cout « "~dress e von wert : return 0:
-
« « «
iptrl iptrZ &wert
« « «
' \n ': ' \n ': ' \n ':
Das Programm bei der Ausführung:
i ptrl verweist auf OxZZff6c i ptrZ verweist au f OxZZff6c Adresse von wert : OxZZff6c Beide Zeiger verweisen also auf die Adresse von )twen«. Das bedeutet in der Praxis, wenn einer dieser Zeiger den Wen mit dem Indirektionsoperator .. **< manipuliert, so bezieht sich die Veränderung immer indirekt auf die Variable )twen «: 11 zeige r 7.CPP lI i nclude (iostream)
using namespace std :
int main(voidl I int * i ptrl - 0: int ' i ptrZ - 0: int wert - IOD : iptrl - &wert : iptrZ - ipt r l :
I
2.1
I
2
I
Höhere und fortgeschrittene Datentypen
cout «'we r t " « we r t « ' \0 '; 11 iodirekte r Zugr ; ff Obe r * « ~; pt r l « . \n ' ; cout « ' we r t cout « 'we r t : " « *;pt r 2 « ' \n\o ": 11 Dere f e renzierung
*ip t rl - 20 0: cout « ' we rt : " « wert « ' \0 ': 11 indi re kter Zugrif f Ober ~ cout « ' wert « *iptrl « ' \n ': cout « "wert : " « *iptr2 « ' \n ': re t urn 0:
Das Programm bei der Ausführung: wert wert wert
100 100 100
wert wert wert
200 200 200
Besti mm t ist Ihnen aufgefallen, dass bei der Übergabe von der Ad resse eines Zeigers zu einem anderen kein AdressoperalOr verwendet wurde. Dies ist bei den Zeigern nicht nötig, da der ,.Wert« eines Zeigers schon eine Adresse im Speicher darstellt und ein Zeiger auch einen . Wert« als Adresse erwartet. 2.1 .5
Dynami sch Speicherobjekte anlegen und zerstörennew und delete
Bisher haben Sie Variablen verwendet. die beim AusfUhren Ihres Anweisungsblocks automatisch angelegt und beim Verlassen wieder gelöscht wurden. Der Vorteil hierbei ist natürlich, dass man sich um nichts kümmern muss. Was ist aber, wenn Sie nicht genau sagen können, wie viele Objekte Sie für das Programm benötigen. Zwar gibt es hierbei Dinge wie Arrays. aber diese sind ebenfalls wieder auf eine bestimmte Größe beschränkt. Zwar kann man ein Array mit einer enormen Größe belegen. aber man sollte bedenken. dass das Programm dann auch diese Größe an Speicherplalz benötigt. Hier haben Sie auch gleich einen Hauptnutzen der Zeiger. Da Sie bei den Zeigern mit Adressen arbeiten . kön nen Sie Speicherobjekte zur Laufzeit des Programms dynamisch anlegen und auch wieder zerstören. All erdings sollten Sie an dieser Stel le gewarnt sein. den n wenn Sie die Verwaltung von SpeiCh erobjekten selbst
138
Zeiger
I
2.1
übernehmen, massen Sie auch entsprechend Platz im Speicher dafür reservieren und diesen auch wieder freigeben (zerstören), wenn er nicht mehr benötigt wird. Allerdings ist Speicher nicht gleich Speicher. Generell unterscheidet man bei einem laufenden Programm folgende Speicherbereiche: ..
Codespeicher - Dieser wird in den Arbeitsspeicher geladen, und von dort aus werden die Maschinenbefehle der Reihe nach in den Prozessor (genauer in die Prozessor-Register) geschoben und ausgeführt.
..
Datenspeicher - Darin befinden sich alle statischen Daten, die bis zum Programmende verftigbar sind (globale und statische Variablen).
.. Stackspeicher - Im Stack werden die Fun ktionsaufrufe mit ihren lokalen Variablen verwaltet. Auf den Stack wurde ja bereits eingegangen. ..
Heapspeicher - Dem Heapspeicher steht der verbl eibende Speicherplatz zur Verfügung und diesem gebührt auch das Hauptin teresse in diesem Abschn itt. Mit ihm funktioniert auch die dynamische Speicheranforderung. Der Heap funktioniert ähnlich wie der Stack. Bei einer Speicheranforderung erhöht sich der Heapspei cher, und bei Freigabe wird er wieder verri ngert. Wenn Speicher angefordert wurde. wird die Anfangsadresse des Speicherblocks zurückgegeben, wenn genug Speicher vorhanden wa r. Und wenn von Adressen die Rede ist. dann ist auch ein Zeiger nicht weit entfern l. Der Heap hat außerdem den Vorteil, dass ein einmal reservierter Speicherplatz verfügbar bleibt, bis dieser wieder fre igegeben wird. Der Vorte il kann allerdings auch zu eine m erheblichen Nachteil werden. Reservieren Sie zum Beispie l Speicher vom Heap in einer Funktion und beenden diese Funktion. sollten Sie immer die Adresse des reservienen Speichers in einem Zeiger speichern, da, and ers als beim Stack, dieser reservierte Speicher nach dem Ende der Funktion erhalten bleibl. Haben Sie keinen Zeiger, der auf diesen Speicherbereich verweist, dann haben Sie ein schönes Speicherloch (englisch: Memory Leak) in den Heapspeicher »geschosse n ~, der nicht mehr verwendet werden kann. Eine andere Lösung des Problems ist es natürlich diesen Spe icher wieder freizugeben. Hinweis Die l:Ieschreibung des speicherkonzepts wurde hier natürlich erheblich vereinfacht dargest ellt. letztendlich ist es den Programmierer nur wichtig ZU wissen, dass das dynamische Speichermanagement im Heap stattfindet.
new - dynamische Objekte an legen
Um Speicherobj ekte dy namisch zur Laufzeit des Programms anzul egen, wird der Operator new verwendet. Hinter new wird der Typ des anzulegenden Objekts angegeben . Das System forden daraufhin soviel Speicherplatz an. wie der ange-
'39
[« )
I
2
I
Höhere und fortgeschrittene Datentypen
gebene Ty p benötigt. Bei Erfolg erhalten Sie als Rückgabewen eine Adresse vom erfolgreich reservierten Speicherplatz. Diesem Rückgabewert weisen Sie natürlich einen Zeiger zu. Die Syntax:
Typ * ze ig er " new Typ ; Oder in der Praxis:
i nt *iptr ; i ptr " new in t; Bei alten C++-Compilern liefert new im Falle eines Fehlers (wenn kein Speicher für das Objekt reserviert werden konnte) 0 zurück. Also sollten Sie dabei auch überprüfen. ob der Zeiger nicht 0 ist.
i nt *iptr ; i ptr - new int ; jf
11
i ptr - 0 ) Fehler bei der Speicherrese rv ierung
el se f 11
Spe i che rre serv i erung er folg reich
Neuere C++-Compiler arbeiten hierbei auch schon mit Ausnahmen (Exceptions; siehe Kapitel 6. Exeption Han dling . Dabei gibt es drei verschiedene Varianten des Operato rs new (bzw. auch new []):
vo i d* ope r ator new( size_t l throw(bad_allocl ; vo i d* operator new( siz e_t . const nothrow_t &l throw() ; vo i d* operator new( si ze_t . void * ) th row(l ; Lassen Sie sich jetzt nicht von der Syntax dieser drei new-Versionen abschrecken. Die erste Variante ist Standard und wird immer verwendet, wenn Sie keine andere Variante angeben. Bei der ersten Variante wird die Ausftihrung des Programms beendet, wenn kein Speicherplatz im Heap angefordert werden konnte. Die zweite Variante gibt im Falle eines Fehlers 0 als Rückgabewen zurück. das entsprich t dem Verhalten älterer C++-Compiler. Mit der dritten Variante haben Sie einen weiteren Parameter, und dieser wird verwendet, wenn Sie pe rsistente Speicherobjekte anlegen wollen (P!acement-new-Operator). Darauf wird allerdings in diesem Buch nicht eingegangen, weil es recht selten in der Praxis verwendet wird. Welche Variante Sie verwenden wollen. bleib t natürl ich Ihnen überlassen. Meistens wird die erste Variante verwendet, in der bei fehlendem Heapspeicher das Programm beendet wird:
Zeiger
I
2.1
11 Speicher fOr ei ne int-Zah l reservie ren
i nt * ipt r - new int : 11 Speiche r fOr e i ne do ub le Zahl reservie r en
double ' dptr - new double : Wollen Sie die zweite Variante verwenden, bei der die Ausführung des Programms nicht beendet wird, um es zum Beispiel nochmals zu versuchen oder sonstige Aufräumarbeiten durchzuführen, so wird dies nach neuem eH-Standard wie folgt realisiert: i nt* ip tr - new (no t hrow) int ; if (
i pt r ) I 11 Spe i cher reserv i e r ung e rfolgreich
else { // Fehler bei der Speicherreservierung Mit der Verwendung der vordefinierten Konstante nothrow geben Sie an, dass Sie di e Ausnah mebehandlung des new-Operators nicht verwenden wollen. Natürlich sollte Ihnen auch klar sein, dass Sie auf ein mit new reserviertes Speicherobjekt nur über den Zeiger selbst (also mit dem Indirektionsoperator) zugreifen bzw. einen Wert ablegen können: // Spe iche r fCir ei ne int-Zah l reservie ren
i nt * ipt r - oew i nt; 11 We r t 100 am Ze i ger iptr ablegen *i ptr - 10 0:
Allerdings sollte man niemals die Adresse des Speicherobjekts _verlieren« - was in einem einfachen Beispiel eigen tlich kaum möglich ist, aber bei umfangreichen Datenstrukturen recht schnell passieren kann. Hinweis Bedenken Sie immer, wenn Sie die Adresse verlieren, haben Sie keine Möglichkeit mehr, das Speicherobjekt zu verwenden, obwohl es im Heapspeicher weiterhin vorhanden ist.
delete - dynamische Objekte zerstören
Wie bereits mehrmals erwähnt, werden dynamisch erzeugte Objekte nicht mehr von selbst freigegeben und müssen bei Nicht-Gebrauch explizit wieder freigegeben (oder auch zerstört) werden. Hierzu wird der Operator de I ete (bzw. de lete [ ]) verwendet. delete zeige r:
[«)
I
2
I
Höhere und fo rtgeschrittene Datentype n
Der Operator de 1ete gibt immer das Speicherobjekt auf der rechten Seite, das ein Zeiger sein muss, wieder frei. Beachten Sie aber, dass nicht festg eschrieben ist, wie de 1ete das Objekt freigi bt. Es muss also nicht sein, dass der Wert der Adresse, de r freigegeben wird, auf 0 gesellt wird. del ete _markiert« den Speicher im Heap lediglich als frei für die erneute Verwendung. Zerstören Sie mit delete einen Zeiger, der auf keinen gültigen Speicherplalz zeigt, so ist das Verhalten undef'iniert (also nicht vorhersehba r).
[»]
Hinweis Andere Programmiersprachen, wie zu m Beispiel Java, verfügen über eine automatische Freispeicherverwaltung, der _Garbage Collection«, bei der Sie sich nicht mehr um das Freigeben von angefordertem Speicher kümmern müssen - nat ürlich geht so etwas auf Kosten der Performa nce.
Hierzu ein einfaches Beispiel, das die Verwendung der Operatoren ne .... und de 1et e demonstriert: 11 zeiger8 . c pp llincl ude
int main ( void) 1 int * i ptrl - new int ; int * i ptr2 - ne .... int ; i nt tmp ; " lpt rl - 100 ; *iptr2 - 200 ; cOu t « "We r t von *i ptrl cou t « ~W e r t von *i ptr2
«
*i ptrl «
«
~ i pt r2
«
' \n '; "\n\n " ;
11 Werte ta usc hen
tmp - *ip t r2 ; *ip t r2 = *i ptrl : *iptrl - tm p; cou t « "W er t von *i pt r l cou t « · Wer t von *i pt r2 11 Speicher wieder freige ben delete i ptrl ; dele t e i pt r2 : return 0;
Das Programm bei der Ausführung: Wert von ~ ipt r l We r t von *iptr2
100 200
« *i pt r l « ' \n ' : *i ptr2 « ' \n ';
«
Zeiger
Wert vo n *i ptrl Wert vo n *iptr2
I
2.1
200
100
Die meisten modernen Betriebssysteme geben reservierten, aber nicht fre igegebenen Speicher bei Beendigung eines Programms wieder frei, obwohl dies nicht vom ANSIIISO-Standard gefordert wird. Sicherlich stellen Sie sich auch die Frage, was passiert, wenn Sie für andere Objekte als Basisdatemypen Speicher reservieren, woher weiß delete , wie groß der Speicherblock im Heap ist, der zerstört werden muss. Darum müssen Sie sich nicht kümmern, das ist wiederum die Aufgabe der Speicherverwaltung des Betriebssystems. Hinweis Die Funktionen ma 11 oe( 1, rea 11 oc (1. ca 11 oc{ 1 bzw. free( 1 (zum Freigeben von Speicher) in einem QueUcode sind die Standard-C-Funktionen zur dynamischen Speicherverwaltung, die in der Headerdatei (in C bekannt als <stdlib .h » definiert sind. Da C eine Untermenge von CH ist und einige Programme auch diese Funktionen noch verwenden, sei für weitere Recherchen auf mein Buch . C von Abis Z.. verwiesen, das Sie von Galileo Press beziehen können und auch als openbook auf der Buch-CD finden (oder auf http://www.proni lC.del).
2.1 .6
[«)
void·Zeiger
Hinweis Wer keine Kenntnisse in C besitzt, kann diesen Abschnitt überspringen, da es sich vorwiegend an C-Programmierer richtet.
void-Zeiger (vo i d *) wurden in C gerne verwendet. um Funktionen zu implememieren, die verschiedene Arten von Dalemypen verarbeiten können. void alJeine hat relativ wen ig »Nutzen« und zeigt nur an , dass kein speziel1er Typ verwendet wird. vo i d-Zeiger hingegen können wie normale Zeiger eine gültige Adresse speichern und sind von keinem typabhängig. In C konnte ein voi d-Zeiger ohne Schwierigkeit dereferenziert werden - C++ hingegen schreibt eine Typenumwandlung vor, wenn man den Zeiger dereferenzieren wi ll . Zwar wird auf eine solche Typenumwandlung noch extra (Abschnitt 3.7) eingegangen. aber hier soll schon mal ein solcher Vorgang in der Praxis gezeigt werden: 11 zeige r9 cpp Ifi nclude using namespace std :
i nt main(voidl r void *vptr : i nt i var - 100 :
143
[«)
I
2
I
Höhere und fortgeschrittene Datentypen
vpt r - &ivar ; 11 Umstand l iche Dereferenzie ru ng : cout « -(static_cast(int")(vptr)) return 0 ;
«
' \n ':
Da e++ mit den .. Templates« bessere Alternativen zu den voi d-Zeigern bietet, um generische Datenstrukturen zu erzeugen, und weil die Typenprüfung bei der Verwendung von void-Zeigern vom Compiler fast unmöglich ist, sei von void-Zeigern in der C++-Programmierung abgerate n (wen n möglich). 2 .1.7
Konstante Zeiger
Auch bei den Zeigern kann mit dem Schlüsselwort cons t ein Read-only-Zeiger verwendet werden . Aber ab wann haben wir einen konstanten Zeiger? Das ist auf den ersten Blick nicht so leicht zu durchschauen. Ein einfaches Beispiel hierfür: const c ha r *cptr - "ABC ": cptr "XY Z": 1I Ok , kein Fehler!!! II Fehler *cpt r ist e i ne Konsta nt e *cptr - ' X': Warum kann .. cptr« verändert werden und .. ·cptr« nicht? Ganz einfach, .. cptr. ist nur ein Zeiger, der auf ein const char-Array zeigt. Die Daten, auf die »· cptr« zeigt, können dagegen nicht verändert werden, weil diese konstant sind. Ve rändern wir nu n die Position des Indirektionsoperators: const *char cptr - "ABC· : cpt r ·XYZ ": 1I Fehler cptr i st konstant *cp tr - 'X': /J Ok. "c ptr ist ei n char Jetzt erhalten Sie ein umgekehrtes Bild . Nun ist der Zeiger konscanr und die Daten, auf die er zeigt (die Zeichen), können geändert werden. Somit so ll ten Sie immer die Position des Indirektionsoperators beachten . Halten Sie hierbei immer die Begriffe »Daten « und »Zeiger. auseina nder - dies sind zwei verschiedene Dinge! Wolten Sie letztendlich, dass sowohl der Zeiger als auch die Daten konstant und somit nicht mehr veränderbar sind, dann müssen Sie nur Folgendes schreiben : const char *const cptr - "ABC " : ·XYZ " : 11 Fehler cpt r ist konstant cpt r *cpt r - 'X': 11 Feh l er *cptr ist konstant
[»]
Hinweis Bevor sich jetzt einige (-Fanatiker aufmachen, um Protestmails zu verfassen, dass man den Schreibschutz sehr wohl umgehen kann, soU hier noch darauf hin-
Referenzen
gewiesen werden, dass man theoretisch den Zeiger immer irgendwie verbiegen kann, um den Schreibschutz zu unterlaufen. Aber darauf wird hier nicht eingegangen.
2,2
Referenzen
Eine Referenz in C++ ist nichts anderes als ein anderer Name (Alias-Name) fü r ein Speicherobjekt. Beim Anlegen einer Referenz muss diese m it einem bereits bestehenden Objekt initialisiert werden. Die Deklaration von Referenzen erfolgt. indem nach d em Typ und vor dem Namen das Begrenzungszeichen ,.&" steht. Auch hierbei kann, wie schon bei den Zeigern, das Zeichen ,.&" unmittelbar nach dem Typ oder vor dem Namen stehen . int var - 100 ; int &rvarl - var : int& rvar2 - var : Allerdings sollte man bei der zweiten Verwendung nicht den Fehler machen zu glauben, dass durch die Angabe »Typ&" die weiteren Objekte ebenfalls Referenzen sind . In diesem Beispiel ist nur "lVar1" eine Referenz. "lVarb ist eine normale i nt-Variable: int va r - 10 0: int& rvar1 - var , rvar2- var ; Eine gewisse Ähnlichkeit von Referenzen und Zeigern kann man nicht leugnen. Kein Wunder, Referenzen werden ja intern mit Zeigern realis iert. Dennoch sollte man sich immer vor Augen halten. dass Referenzen nur Alias-Namen auf andere Speicherobjekte sind. Zeiger hingegen können Adressen von anderen Speicherobjekten aufnehmen. Alias
(lateinisch für ,.ein anderer«) bezeichnet einen Ersatznamen (Pseudonym).
Hierzu ein einfaches Beispiel. das die Referenzen demonstrieren soll: 11 r efl . cpp #include us ; ng namespace std :
int main(void) int va r: int & rvar - var ; var - 100 : cout « "var
:. «
var
«
' \n ':
145
I
l.l
I
2
I
Höhere und fo rtgeschrittene Datentypen
cout
«
"r va r
• « rvar «
rv ar - 200 : cout « Ova r cout « "rv ar return 0:
' \ 0 ';
«var« ' \n ' : rvar « ' \ n':
«
Das Programm bei der Ausführung: var rva r var rva r
100 100 200 200
An der Zeile rva r - 20 0 ;
kann man eindeutig den Unterschied von Referenzen und Zeigern erkennen. Bei einem Zeiger häuen Sie hier nur mit dem Indirektionsoperator auf die Variable »varj( indirekt zugreifen können. Bei Referenzen genügt der Name, da dieser nichts anderes als ein Synonym fti r die eigentliche Variable ist. Am besten lässt sich dieser Sachverhalt noch demonstrieren, wenn man den Adressoperator verwendet. 11 ref2 .cpp #i nclude using namespace std :
i ot mai n(vo i d l i ot var ; ; ot& rvar - var ; cout « "&va r cout « "& r var return 0:
« «
&va r « ' \n ': &r var « ' \0 ';
Das Programm bei der Ausftihrung : &va r &rva r
Ox22 ff 74 Ox22 ff74
Da Referenzen bei ihrer Erzeugung initialisiert werden, arbeiten diese immer als Synonyme ftir das eigentliche Ziel. Daher gibt es auch keine Möglichkei t, die Adresse einer Referenz zu ermitteln.
Arrays
I
2·3
Referenzen können aber nur einmal zugewiesen werden. Der Versuch einer erneulen Zuweisung hälte nur den Effekt. dass Sie den Wen der Zielreferenz, mil dem Sie die Referenz am Anfang initialisien haben, verändern.
i nt varl .. 100 ; i nt var2 .. 200 ; i nt& rvar .. varl : 11 neue Zuweisung?
rvar .. var2: Der Versuch einer zweiten Zuweisung hier hat lediglich den Effekt , dass der Wen von "var1 ~ auf 200 verändert wird. Das Anwendungsgebiet von Referenzen liegt vorwiegend in der ParamelerObergabe und der Wertrückgabe von Funktionen. Im Beispiel des Rückgabewerts muss so kein Speicherobjekl daflir angelegt werden, und es wird nur ein Alias für ein bereits bestehendes Objekt zurückgegeben. Mehr dazu erfahren Sie in den Abschnitten 2.6.3 und 2.7.2.
2.3
Arrays
Mit den Arrays können Sie eine geordnete Folge von Werten eines bestimmten Typs abspeichern und bearbeiten. In vielen Büchern werden Arrays auch als Felder oder Vektoren bezeichnet. Die gleich artigen Objekte werden sequenziell im Speicher abgelegt - darauf können Sie sich immer verlassen. Das bedeutet natürlich, dass ein Array mit fünf lang-Werten auf einem 32-Bi l-Rechner 20 Bytes (5 x 4 BYles ( für long) = 20 Bytes) belegt. Hinweis Auch wenn sich die Beispiele hier zunächst nur auf die Basisdatentypen beziehen. so soll nicht der Eindruck entstehen, dass sich Arrays nur auf diese anwenden lassen. Aber hierauf wird im Verfauf des Buchs noch explizit eingegangen. 2.3.1
Arrays deklarieren
Hier die Syntax zur Deklaration eines Arrays:
Typ
Ar r ay~ame[n] ;
Mit "Ty p~ geben Sie an, von welchem Ty p die Elemente des Arrays sind. Der "ArrayName« ist frei wählbar nach denselben Vorschriften wie ein Bezeichner für Variablen. Mi t "n« geben Sie die Anzahl der Elemente an, die dieses Array vom ..Typ« aufnehmen kann, also den Indexwert. Arrays mit unterschiedlichen Typen gibt es nicht in C++.
147
[« )
I
2
I
Höhere und fortgeschrittene Datentypen
Die einzelnen Elemen te werden mit dem Arraynamen und dem entsprechenden Indexwen in eckigen Klammern angesprochen . Der [ndexwen muss eine Ganzzahl sein und fangt immer bei 0 an zu zählen! Hierzu ein ige Beisp iele von Arrays: int ia rray(1 0] : fl oa t farray[50] : dOuble darray[100J :
II 10 i nt - Werte II 50 float-Werte I1 10 0 dOuble -Werte
Noch ein einfaches Beispiel: int va r (5] :
Im Speicher ergibt sich som it folgendes Bild: varjO]
0000
var['] var[2]
0008
"ar[3] 00,.
varl4]
,, ,, !..._------- ~ Abbi ldune 2.4
Array mit funf Elementen
Anhand des Typs erkennt der Comp iler von selbst, wie viel Speicher für das Array benötigt wird. 2.3.2
Arrays initi alisieren
Die einzelnen Werte eines Array könne n Sie nun mit dem Arraynamen und dem entsprechenden Indexwen zuweisen. int va r (5] : var(O ) var(I ] var(2] var(3 ] var(4 ]
"
- 4:
- 6, - 8, - 10 :
Oder auch in einer Schleife: JJ arrayl .cpp lI i nclude
Arrays
us i ng namespace std ; int main(void) I int var[5] : for( i nt i- O: i < 5 : i ++ ) 1 var[i] - (i+l) * 2: far( int i - O: i < 5 : i++ ) 1 cou t « "var[ " « i «"] :"
«
var[i)
«
"\n ":
retu rn 0 : Bei diesem Beispiel wurde an alle fLinf Elemente ein Wert mithilfe des In dizie~ rungsoperators ([ J) übergeben und anschließend auch wieder ausgegeben:
, ,• , "
0000
oooc
,,,.
varja] varjl] varj2] varjl]
•arj4]
,, ,, !.._-------~ Abbi]dung 1.5
Ein initialisiertes Array mit fünf Elementen
Arrays lassen sich auch, anders als eben gezeigt, direkt bei der Deklaration, in itialisieren. Die Werte müssen dabei wie fo lgt zwischen geschweiften Klammern slehen: int var[5] - I 2 , 4 . 6 . 8 . 10 I ; Bei einem so iniLialisierten Array können Sie die Indexgröße des Arrays auch weglassen: i nt var[] - I 2, 4 . 6 . 8, 10 I : Der Compiler kümmert sich dann darum, dass genügend Speicherplau zu r Verfügung steht. Die einzelnen Werte werden imm er mit einem Komma getrennt und stehen in geschweiften Klammern. Schreibt man hingegen Folgendes i nt var[ 5] - { 2 . 4 I :
149
I
2·3
I
2
I
Höhere und fortgeschrittene Datentypen
so ist dies kein Fehler. Hiermit werden praktisch die ersten beiden Array-Elemente mit den Werten 2 und 4 initialisiert - alle anderen Elemente erhalten automatisch den Wert O. Sofern Sie also bei einem Array alle Werte mit 0 vorbelegen wollen, müsse n Sie hierfür nicht extra eine Schleife verwenden
Unnöti 9 fore int i- O: i < 5: i++ ) 1 var[i] - 0
1/
sondern es genügt folgen de Initialis ierung:
int va r [5] - ( 0 I ;
2.3.3
Bereichsüberschreitung von Arrays
Hier komm en wir nun zu einem Punkt, der in der Verga ngenheit schon vie le Feh[er beschert hat. Anhand der Indexnummer können Sie erkennen, dass hier bei 0 mit dem "zä hlen ~ angefangen und mit 4 das letzte Element beziffert wird. Genau betrachtet si nd es ja fünf Zahlen (0 , 1, 2. 3, 4) - aber meistens entspricht dies nicht der logischen Denkweise (erster Schultag, erster Platz, erster Sieger, erster Tag usw.). Abe r gerade in Schleifen wird hierbei gerne Folgendes geschrieben: 11 ! !! Be r eichsObersch reit ung !!! f ore i nt i - O; i (- 5: i++ ) 1 var[i] - ( j+1) * 2 ;
Hier wurde anstatt auf "kleiner« 5 auf "klei ner-gleich « 5 geprüft. Viele Compiler machen dabei anstandslos mit, und im Speicher würde sich folgende r Zustand finden:
,
0000
,, ,,
•ar{O]
,• , " "
•ar{l ] •ar{2] •ar{3] •.., [4]
•arlS]
~--------~ Abbildung 2.6
'50
Bereichsuberschreitung eines Arrays
Arrays
Das wei tere Verhalten des Program ms ist in diese m Fall undefinien. Sobald zum Beispiel eine andere Variable diesen Speicherbereich (hier 0018) bekommt und verwen det. wird dieser Wen gnadenlos überschrieben. Besonders fata l wirkt sich das bei char-Arrays aus, da hi er oft die Nullterminierung übersch rieben wird. Somit liegt es praktisch in Ihrer Verantwortung den Bereich eines Arrays nicht zu überschreiten . Auf manchen Systemen gibt es eine Compiler-Option (range-checking), mit der ein solcher Über- bzw. Unterlauf eines Arrays zur Laufzeit des Programms ge prüft wird. Das fertige Programm sollte allerdings nich t mehr mit dieser Option übersetzt werden, da dies zu einem sch lechten Laufzeitve rhahen führt.
2.3.4
Anzahl der Elemente eines Arrays ermitteln
Die Anzahl der Elemente eines Arrays können Sie mit dem si zeo f-Operator ermitteln. Diese r liefert Ihnen die Größe in Bytes eines Typs zurück. Auf das Array alleine angewandt liefert der si zeof-Operator allerdings nur die gesamte Größe des Arrays zurück. Teilt man diesen Wert durch die Größe eines einzelnen Typs, dann erhält man die Anzahl der Elemente zurück. 11 ar ray2 . cpp lIinclude
int mai n(void) 1 in t va r [] - 1 1 . 2 , 3 , 4, 5, 6, 7, 8 I ; cou t « "Gesamt gröBe « sizeo f (va r ) « " Bytes\n "; « sizeo f (va r (O]) cou t « "EinzelgröBe « " Bytes\n" ; cout « "Anzahl Elemente « s1zeof(va r )/s1zeof(var[O]1 « ' \n ': return 0;
Das Programm bei der Ausführung: Gesamtg rOBe : 32 By t es Ei nzelg rOBe : 4 By t es Anzahl Elemente 8
151
I
2 ·3
I
2
I
Höhere und fo rtgeschrittene Datentypen
2.3.5
Arraywert von Tastatur einlesen
Das Einlesen von Arraywerten von der Tastatur funktioni ert genauso wie schon bei normalen Variablen. Es muss lediglich das Indexfeld (mithilfe des Indizierungsoperators) verwendet werden, für das Sie den Wert vorgesehen haben . 11 array3 . cP P llinclude
int main(voidl 1 int var[5] ; fore i nt i - O; i < 5 ; i++ 1 eou t « "Wert e i ngeben ei n» var [i] ; cout « "Di e eingegebenen we r te waren ; \n" ; for{ i nt i - O; i < 5 ; i ++ 1 1 cou t « var[i] « ' \n '; return 0:
Das Prog ramm bei der Ausführung: Wert ei nge ben 55 Wert ei ngeben 43 Wert ei nge ben 45 Wert ei nge ben 11 Wert ei nge ben 15 Di e einge geben en Werte waren 55 43 45 II 15
2.3.6
M ehrdimensionale Arrays
Selbstverständ lich kann man in e++ auch ein Array mit mehr als nur einem Index verwenden, also ein mehrdimensionales Array. Bei einem zweidimensionalen Array hat man dann zwei Indizierungsoperatoren statt - wie bisher - einen. int spielfeld[2][4] :
Hier wurde ein zweidimensionales Array mit dem Namen »spielfeld .. deklariert, das 2x4 Felder besitzt. Man kann sich das Ganze wie bei einer Tabellenkalkula-
'5'
Arrays
tion vorstellen . Der erSte Index diem als ..Zeile« und der zweite als .. Spalte«, Dasselbe hätten Sie übrigens auch mit int spielfeld[8J ; realisieren können - nur ist es hierbei nicht so offensichtlich, worauf das Ganze hin aus laufen soll. Die Anzahl der Dimensionen ist hier all erdings nicht auf zwei beschränkt. doch machen weitere Dimensionen, zum Beispiel eine Dritte, in der Praxis kaum noch Sinn. Die lnitialisierung von mehrdimensionalen Arrays erfolgt im Prinzip genauso wie bei einem ei nd imensionale n Array über die Indizierungsopera loren. Woll en Sie zum Beispiel auf dem ..spielfel d« in der erSten Zeil e und dritten Spalte einen Wert zuweisen, können Sie dies wie fo lgt machen
11 , 22 , 33 ,44 I , 55. 66 , 77 . 88 ) J;
Tatsächlich werden aber die inneren geschweiften Klammern vom Com piler ignoriert und dienen lediglich der Übersichtlichke it. Der Compiler macht aus dieser Initialisierung Folgendes: int spielfeld[2][4] - 111 , 22 , 33 ,44, 55 , 66 , 77 , 88 J; Auch fIlr die mehrdimensionalen Arrays gil t, dass alle nicht initialis ierlen Felder mit dem Wen 0 vorbelegt werden - vorausgesetzt, es wurde mindestens ein Feld mit ei nem Wert belegt: int spielfeld[2][4] -
11 I. SS . !ifiJ I :
Hier wurden nur die Felder [OJ[OJ , [1][0 J und [1][ 1J mit Werten belegt. Alle anderen Felder haben den Wert O. Das folgende Beispiel demonstriert Ihnen, wie Sie von der Tastatur Werte an ein zweidi mensionales Array zuweisen kön nen. // arr ay4 . cpp llinclude
153
I
2·3
I
2
I
Höhere und fo rtgeschrittene Datentypen
us ing namespace std ; i nt main(voidl I int spielfeld[2][ 4 ] ; for ( i nt i - O; i < 2 ; i++ ) I for ( int j - O; j < 4 ; j++l cou t « "E i ngabe : " : ein » sp i e lfe ld ( i )(j) ;
cout « " Folg ende We rte wu rden ei ngegeb en : \n " : for (int i " O; i < 2 ; i ++ ) I f or ( int j ~ O : j < 4 : j++) I cout « spielfeld[i ](j] « cou t
«
' \n ';
return 0 :
Das Programm bei der Ausführung: Eingabe : Ei ngabe : Eingabe : Eingabe : Ei ngab e : Eingabe : Eingabe : Eingabe : Folgende 11 22 33
11 22 33 44 55
66 77
88 Wert e wurden eingegeben 44
55 66 77 88
In der Praxis werden mehrdimensionale Arrays bei verschiedenen Arten von Berechnungen benötigt oder auch bei 2-D-Darstellungen von Grafiken.
2.4
Zeichenketlen (C-Strings) - char-Array
(-Programmiere r sind mit den klassischen (-Strings bestens vertraut. Allerdings verwendet man bei modernen C++-Programmen keine C-Strings mehr, sondern die C++-Klasse »string". Wozu also »alte Geister" aufwecken, werden Sie sich fragen? Der Hauptgrund ist, dass viele (-Programme mittlerweile in C++-Programme umgeschrieben wurden, und dabei wurde d ie Verwendung von
154
Zeichenketten (C-Strings) - char-Array
I
2 ·4
C-Strings häufig beibehalten. sofern die Ausführgeschwindigkeit eine Rolle spielen sollte, sind die C-Strings erheblich effizien ter in ihrer Ausführung, da hier der Verwalmngsaufwand geringer ist. Hinweis Auf die Verwendung der C++-Klasse ,.string" wird in Abschnitt 7.1 noch ausführlich eingegangen.
2.4.1
C-String deklarieren und initialisieren
Ein C-String ist auch ein Array, nur eine Kette von einzelnen char-Zeichen mit einer abschließenden 0 (Stringterm inierungszeichen \0 oder OxOO). Daher ist die Deklaration eines char-Array identisch mit der bisher bekannten Form der Array-Deklaration: cha r carray(IOO] : Dam it wird ein Array angelegt, das 100 einzelne Zeichen speichern kann: const char carray[100] - I 'H'. ' a '. ' 1 '. '1 ', ' 0 '. ' '. 'W'. ' e'. ' 1 '. ' t ', ' \n '. ' \0 ' I : In solch einem Fall ist es nicht nötig, die Größe des Arrays mitanzugeben:
const char carray[] - I ' H', ' a ' , ' 1'. ' 1'. ' W'.
' e ',
' 1 '.
' t '.
'0' .
' \n '.
'
'.
' \0 ' ) :
Allerdings ist diese Schreibweise mit den einzelnen Zeichen recht ums tändlich, Daher erlaubt (fC++ auch die folge nde gängige Kurzform: const chaf carray[] - "Ha l lo Wel t \n ": Hier hat man auch gleich den Vorteil, dass das Stringterminierungszeichen (\0) nich t hinzugefügt werden muss. Bei der Verwendung von doppelten Hochkommala macht der Compiler dies automatisch. Somit besitzt das char-Array "carray" Plao: fOr zwölf Eleme nte. Hier liegt jedoch eine mögliche Fehlerquelle und somit auch wieder eine der Schwächen der C-Strings, Da man elf Zeichen sieht. vergisst man gerne das zwölfte, das Stringlerm inierungszeichen, das das Ende des Slring anzeigt. Wenn Sie also Speicherplatz fli r einen C-String reservieren müssen, bedenken Sie immer, dass Sie Speicher für n+ 1 Zeichen reservieren ! 2.4,2
C-String einlesen
Es wurde bereilS erwähnt. dass man nich t ohne weiteres mit ci n einen String einlesen kann. Man kann schon, aber es besteh t zum einen das Problem, dass alles was evemuell hinter einem Leerzeich en (bzw. Tabulator-Zeichen) steht, nicht
155
[« )
I
2
I
Höhere und fortgeschrittene Datentypen
mehr miteingelesen wird (es sei denn dies ist beabsichtigt), und zum anderen findet keine Längenübe rpfÜfung stau. Man ist nicht vor eine r Bereichsüberschreitung (Pufferü beriauO geschützt. Deswegen wird gewöhn lich zur Methode getli ne von ci n gegriffen. Diese Funktion deckt beide Problemfalle ab : 11 car r ay1.c pp lIinclude using namespac e std ;
i nt main(voidl I cha r ca rray [8 0) : cOu t « "Bitte Ei ngabe machen : "; cin . getline( ca r ray , sizeof(carray) 1: cout « " I hre Eingabe : \n" « carray « ' \n ': return 0 ; Das Program m bei der Ausführung : Bit te Einga be machen : Soluti on to the probleml Ihre Einga be: Solution to the problem!
2,4.3
C-Strings Bibliotheksfunktionen
Falls Sie schon probien haben «(-Programm iere r wissen das sowieso schon), einem Array ein anderes Array (oder ( -String) zuzuweisen, werden Sie fe ststellen, dass dies nicht funktioniert: char carray}[80) ; char carray2[) - "Ein C- String" ; carray} - carray2 ; I! Geh t niCht - Fehler ca rrayl - "E in C-St r ing" 11 Geht auch nicht - Fehler Bei den (-Strings können Sie dann auf die Standardfunktionen der (-Bibliothek zugreifen . In diese m Fall sind dies die Funktione n st r cpy() und strncpy(), wobe i strc py () hier nicht beha ndelt wi rd, da es ohne KontroHe de r Länge die Getahr eines PutlefÜberiaufs e rhö ht. Die Syntax; lIinclude 11 in C: <str i ng . h> char* st rn cpy ( char * dst , const char * src , size_t n ) : Hiermit werden vom String, der in ,.src« steht, n Zeichen nach ,.dst .. kopien. Als Rückgabewen erhalten Sie einen Zeiger auf den Anfang von ,.dest« oder, im FaHe eines Fehlers, NU LL. Die Definition dieser Funktion finden Sie in der Headerdatei (ohne .h). In ( ist dies die Headerdatei <s tr i ng . h> .
Zeichenketten (C-Strings) - char-Array
Hinweis Die C-Standardbibliothek-Funktionen finden Sie natürlich weiterhin in den Headerdateien s t di o. h, stdl i b . h, st ri ng . h, mat h. h usw. und können diese auch so verwenden. In C++ sind sie aber auch unter den Namen cst diO. cstdl i b, cstri ng. cmath usw . vorhanden, nur mit dem Unterschied, dass der Inhalt im std Namensbereich einkopiert ist. Mehr zum Namensbereich und s t d in Abschnitt 3.2. Hierzu das Beispiel, wie Sie einen C-String in einen anderen kopieren können: 11 car r ay2 . c pp lIinclude llinclude us i ng namespace s t d :
int main(void) I cha r carrayl[80J : char carray2[] - ~Ein C - String ~: strncpy ( carrayl. ca rr ay2 . sizeof(car rayl) -1 ) : cout« ~carrayl : . « carrayl « ' \n ': re t urn 0: Die Größe des C-Strings lassen wir uns mit dem s i zeof-Operator minus 1 ermitteln. Minus 1 deshalb, weil aueh noch Platz für das Stringende-Zeiehen benötigt wird. Wollen Sie j eUl an den C-String einen weileren C-String anhängen, können Sie leider nicht mehr strnepy() verwenden, da hiermit der alte Inhalt komplett überschrieben wird. Deshalb wird eine der C-Funktionen streat<) , oder strncat ( ) benötigt, wobei hier wiederum auf s treat{ ) nicht eingegangen w ird: lIinc l ude <estring> cha r* s t rnca t ( char* dst . cons t
char~
src . size_t n ) :
Mit dieser Funktion hängen Sie n Zeichen des Strings lO sre" an das Ende von ,.dst.. . Hierzu ei n weiteres Beispiel, das das Aneinanderhängen von C-Strings demonstriert 11 carray3.cpp llinclude us i ng name space s t d :
int main(void) I cha r name[60) : cha r vn ame[30] : cha r nname[30] :
157
I
2·4
[«J
I
2
I
Höhere und fo rtgeschrittene Datentype n
cout « "Vorname c i n . getline{ vname , sizeo f ( vname) ) ; co ut « "Na chna me : " . c i n . getlin e ( nname , si zeof{ nnameJ l ; strncpy{ name . vname , sizeof{name) - } ) ; strncat{ name , " ", I } ; strncat{ name . nname . sizeof{ namel - strlen{name) - }) : name[sizeof{namel-I] - ' \0 ': cout « "Vollst
"«
name
«
' \n ' :
Das Programm bei der Ausführung: Vo r name ; Jiirgen Nachname : Wol f Vollst
=
' \ 0' :
Bestimm t ist Ihnen in der Zeile s t rncat( na me . nna me . sizeof(name) - st r le n( name) - !l : die Subtraktion mit der Funktion strlen{) und »name" als Parameter aufgefallen. Die Funktion st r len() zählt die Anzahl der Zeichen eines C-String bis zum Terminierungszeich en. Wenn Sie strlen{) also auf e inen nicht terminierte n String anwenden, kommt es zu einem Programmfehler. Die Syntax von st r l e n{ ) lautet: llinclude size_t strlen ( const cha r* str i ng ) ; Neben diesen Funktionen gibt es noch eine Menge weiterer Funktionen rur C-Strings, die Sie weiter unten aufgelistet finden. Zuvor aber noch ein paar (Grund-lSälZe zu den C-Strings. Ihnen ist sicherlich aufgefallen, dass die Verwendung von C-Strings recht umständlich ist, und häufig kann eine falsche Verwe ndung zu gefah rlichem Code , wie zum Be ispiel Pufferüberläufen (Buffer Overflow), führen. Funktionen wie strcpy{) oder strcat{) (ohne n dazwischen) bieten keine U1ngenüberpTÜ-
158
Zeichenketten (C-Strings) - char-Array
fung an. Somit kann eine solche Funkeion. wenn man nicht richtig aufpasst. ungehinden in einen unerlaubten Speicherbereich schreiben. Leider hat die Vergangenheit gezeigt. dass sich viele Programmierer einfach nicht an diese Regeln gehalten haben, sodass heute kein Tag vergeht, an dem nicht wieder eine Sicherheitslücke in einem Programm gefunden und gegebenenfalls ausgenutzt wu rde. Gerade im Bereich Pufferüberlauf werden wohl die meisten »Feh[er" gemacht. In einer Welt. wo jeder Computer einen Zugang zu einem Neuwerk hat. ist dies ein nicht mehr zu übersehendes Problem. Und aus dem Grund, dass die C-Strings zum einen rech t umständlich zu bedienen sind und der Sicherheitsaspekt häufig vernachlässigt worden ist, wurde in C++ die string-Klasse eingeführt, die zum einen erheblich einfacher zu verw-enden ist, mehr Funktionalität bietet und vor all em erheblich sicherer ist. Aber wie bereits erwähnt. da es noch viele Zeilen C-Code gibt, müssen Sie sich als ernsthafter C++-Programmierer auch mit den C-Strings befassen. Und wenn die Ausführgeschwindigkeit auch noch von Bedeutung ist (aber nur dann). sind die C-Strings immer noch wesentlich effizienter. Jeut noch die restlichen C-Funktionen (Tabelle 2.1), die Sie in Verbindung mit C-Strings verwenden können (a lle Funktionen benötigen die Heade rdatei
BeschreIbung
char 'strchr ( const char * s . lnt eh ) :
Diese Funktion gibt die Position im C-String >OS« beim ersten Auftreten von _ch _ zurOck. Tritt das Zeichen »eh« nicht au f, wird NUl l zurOckgegeben.
char * strrchr ( const c har * 5 . lnt eh ) :
Diese funktion ähnelt der Funktion strc hr( ). nur dass hier das erste Auftreten des Zeichens von hinten bzw. das let zte ermittelt wird.
lnt strClllp( constc har ~ sl . const c har "52 ) ;
Sind beide C-String5 identisch, gibt diese Funktion 0 zurück. Ist der C-String »51 « kleiner als »52«, so ist der Rückgabewert kleiner als 0, und ist ..51 .. grOßer als _52 «, dann ist der Rückgabewert größer als O.
lnt st rn Cllp ( const char * sl. const c har " s2 . s l ze_t n ) ;
Hiermit werden die ersten . n.. Zeichen von »51 « und die ersten "n« Zeichen von »52 .. le.ikografi5ch miteinander verglichen . Der ROckgabewert ist derselbe wie schon bei st r cmp( ).
lnt strcspn ( const char rsl . const c har * 52 ) ;
Sobald ein Zeichen, das in . 52« angegeben wurde, im CString »51- vorkommt, liefert diese Funktion die Position dazu zurück.
Tabell e 2.1 Standard-C-Funktionen in (bzw. in C <string.h»
159
I
2 ·4
I
2
Höhere und fortgeschrittene Datentypen
FunktIon
Beschreibung
char ' strstr { constchar · sl . const char ' s2 ) ;
Damit wird der C-String »51« nach einem C-String mit der Teilfolge .52... ohne' \0 ' durchsucht.
char * strtOk { char * 51. const char ' s2 ) ;
Damit wird der C-String »s1 « durch das Token getrennt, das sich in .s2« befindet. Ein Taken ist hier ein C-String, der keine Zeichen aus . s2« enthält.
vo ld ' lIIelJchr { const vo id * buffer . 1nt c. slze_tn) ;
Diese Funktion sucht in den ersten IO n« Bytes in . buffer ... nach dem Zeichen . C«. Sollten Sie den ganzen String durchsuchen wollen, können Sie die Funktion st rC hr() verwenden, Tritt dabei ein Fehler auf oder wird das Zeichen nicht gefunden, gibt die5e Funktion NULL zurück,
lnt memclJp C const vold * 51 . const vold ' 52 . slze_t n) ;
Mit memcmp() werden die ersten IO n« Bytes im Puffer . s1« mit dem Puffer . s2<, lexikografisch verglichen, Der Rückgabewert ist derselbe wie schon bei strcmp().
vo l d *lIIelJcpy { vold *de st . const vold · src , sl ze _t n ) ;
Mit der Funktion memcpy() können Sie IO n « Bytes aus dem Puffer .src« in den Puffer .dest« kopieren. Die Funktion gibt die Anfangsadresse von . dest« zurück.
vo l d ~ l!IelJlJove ( vold 'desto const vo id ' src , slze_t n ) :
Die Funktion erfüllt denselben Zweck wie die Funktion mem · cpy (). mit einem einzigen, aber gravierenden Unterschied. memmove() stellt sicher, dass im Fall einer Oberlappung der Speicherbereiche der Überlappungsbereich zuerst gelesen und dann überschrieben wird. Auch die Rückgabewerte sind bei memmove() dieselben wie bei memcpy().
vo ld 'llelJset { vold *dest . 1 nt eh. unsigned int n);
Mit dieser Funktion füllen Sie die ersten IO n« Bytes der Adresse . de5t... mit den Zeichen »eh« auf.
Tabell e 2.1 Standard-C·Funktionen in <:c5trin8> {bzw. in C <:string.h>} (Forts.) [ )) ]
Hinweis Mit den mem ... -Funktionen können Sie ganze Speicherblöcke kopieren, vergleichen, initialisieren und durchsuchen.
2.5
Arrays und Zeiger
Arrays und Zeiger haben in CH eine enge und gute Beziehung zueinander. aber sind nicht identisch oder ähnlich. Ein Array belegt nämlich zum Programmstart einen konstanten Speicher mit n Elementen, dessen Anfangsadresse nich t mehr verschoben werden kann. Man kann auch sagen, ein Array besitzt einen konstanten Zeiger auf das erste Element.
,60
Arrays und Zeiger
Einem Zeiger hingegen muss man zunächst erSt mal einen Wen zuweisen, damit dieser auch auf einen belegten Speicher zeigt. Außerdem kann der ~ Wert" eines Zeigers später nach Belieben ei nem anderen >JWen" (Speicherobjekt) zugewiesen werden. Ein Zeiger muss außerdem nicht nur auf den Anfang ei nes Speicherblocks zeigen. Hierzu ein einfaches Beispiel: 11 za r rayl .cpp llinclude us i ng namespace std :
int main(void) I int ia r ray[) - ( 11 . 22 . 33 I : 11 ipt r zeigt au f das e rste Element von iarray int* i ptr - iarray : for(int i- O: i <s i zeof(iarrayl/sizeo f (iarray[O]l : i++ ) { cout « *fptr « ' \n ': fptr++ : // n3chstes Element re t urn 0:
Das Programm bei der Ausführung: 11
22
33
Zunächst bestimmen Sie ein i nt-Array ,.iarray ... mit drei Elementen. Anschließend weisen Sie dem Zeiger lIiptr... die Adresse des ersten Elements zu. 0000
f------:'c'__1IarraY10]
0004
22
I8.rray[l ]
oooc
0000
I--- -----i 0008 33 Iarray[21 I--- -----i iplr 0014
00"
1---
---1
,, ,, L________ ..
Abbildung 2. 7
_iptr. verweist auf das erste Element von . iarray.
,6,
I
2·5
I
2
I
Höhere und fortgeschrittene Datentypen
In der folgenden Schleife wird der Wert mithilfe des Indirektionsoperators '" . auf den ,.iptr.. indirekt verweist, ausgegeben (im Beispiel ,.11«). Anschließend wird der Zeiger inkrementiert bzw. die Adresse wird inkrementiert Da hier nicht der Indirektionsope rator verwendet wurde, bezieht sich diese Inkrementierung auf die Adresse des Zeigers. Und ei ne Erhöhung eines Zeige rs um den Wert n hat denselben Effekt, wie eine Erhöhung eines Arrayelements mithil fe des lndizierungsoperators um den Wert n. Somit wird also der Wert der Adresse nicht um eins erhöht, sondern um die Größe des Typs, die der Zeiger repräsentiert. Im Beispiel eines Integers sind dies gewöhnlich vier Bytes (abhängig von der Architektur). Somit bedeutet also ~ Typ + n .. in der Praxis: Typ + sizeof(Typ) * n Häuen Sie im Beispiel statt ,.i ptf+H hier ,.*iptf+H mit dem Indirektionsoperator verwendet, dann häuen Sie tatsächlich den Wert des ersten Arrayelements um ei ns erhöht. Nachdem also die Adresse von »iptn< inkrementiert wurde, ergibt sich fo lgendes Bild: Iarray[O]
0004
" 22
0008
33
I8.rray[2]
oooc
"""
iplr
0000
Iarray[l]
0018 I I I I
!.._------_ ... Abbildung 2.8
~iptr.:
verweist auf das zweite Element von . iarray.:
Nach der Ausgabe wird die Adresse erneut um eins inkrementiert, wodurch der Zeiger ,.irrT« nun auf da _~ letzte Element im Array zeigt. was auch wieder ausgegeben wurde . Die Abbruchbedingung der Schleife wurde mit i < sizeof(iarrayJ/s i zeof(iarray[O]) ;
angegeben. Die Schleife wird somit solange durchlaufen , bis alle Elemente ausgegeben wurden (Siehe auch Abschnitt 2.3.4) . Wie bereits in diesem Beispiel kurz erwähn t, ist es auch möglich, mithilfe eines Zeigers und dem Indirektionsoperator indirekt den Wert der einzelnen Arrays zu
,62
Arrays und Zeiger
verändern bzw. einen (neuen) Wert zuzuweisen. Genauso wie es schon im Abschnitt über Zeiger mit den Variablen gemacht wurde. Hier das Beispiel dafür, wie Sie einem Array indirekt über einen Zeiger Werte zuweisen bzw. diese ändern. 11 zarray2.epp lIinelude ( i ostream> using namespaee std : int main(voidl 1 int i a rray[5] : 11 iptr zeigt auf das e rste Element von ia r ray int* i ptr - ia rray: fore i nt i-O : i(sizeo f (ia rra yJ/s i zeof(ia r ray[O]J : i++ ) ! cou t « "Bitte Wert einge ben : ": ein» * iptr : i ptr++ : 11 n
Das Programm bei der Ausführung: Bit te We r t eingeben: Bit te We r t eingeben: Bit te We r t e ingeben : Bit te We rt e ingeben : Bit te We r t e i ngebe n : Oi e Wer te 1 au ten
,
11
22 33 44
55
II
22 JJ 44
55
Die Eingabe von Werten mittels e i n» *iptr :
entspricht also de r folgenden Eingabe: ei n
»
iar r ay[ i ] :
I
2·5
I
2
I
Höhere und fo rtgeschrittene Datentypen
Sie wissen jetzt. dass man mit Zeigern auch auf ein beliebiges Element im Array verweisen kann. Wollen Si e zum Beispiel direkt mit dem Zeiger auf das dri11e Element im Array zugreifen, so gehen Sie wie fo lgt vor:
iptr ze igt auf das dritte Element von iarray i ptr - &iarray(2] ; 11 Neuer We rt für das dritte Element im Array *i ptr - 1000 ; 11
Es gibt aber auch noch eine weitere Möglichkeit, um den Wert des dritten Arrays indirekt zu verändern:
ipt r zeigt au f das e rste Element von i arr ay int * iptr - iarray ; 11 Neuer Wer t für das dr itte Element im Array *(iptr + 2) - 1000 ; 11
Damit der Zeiger ta15ächlich auf die nächste Adresse zeigt, muss ,.iptf+2« zwischen Klammern stehen, weil Klammern eine höhere Bi ndungskraft als der Dereferenzierungsoperato r haben und som it zuerst ausgewertet werden. Sollten Sie die Klammern vergessen, würde nicht auf die nächste Adresse verwiesen, sondern auf den Wert, auf den der Zeiger ,.iptr« weist. und di eser würde dann um den Wert zwei erhöht. Genau betrachtet. stellt der Indizierungsoperator [] eine Schreibweise für die Adressierung mittels Zeigerarithmelik dar, wie das folgende Beispiel demonstrieren soll: 11 zarray3.cpp llinclude
i nt main(void) 1 int iarray[] - 1 11 , 22 , 33 I ; 11 zweites Element ausgeben cou t « iarray(l] « ' \n ' : 11 als Zeigerarithmetik ... cou t « *(iarray + 1)« '\n ': // drittes Element andern *(iarray + 2) - 44 ; // dr i ttes Element ausgeben cout « i ar ray[2 ] « ' \n '; return 0;
Arrays und Zeiger
I
2·5
Das Programm bei der Ausführung: 22 22 44
Beide Ausdrucke haben somit dieselbe Bedeutung: iar r ay[2] .. 0 ; "* (ia r ray + 2) - 0 ;
Im ersten Beispiel wird über den Indexoperator auf das dritte Element vom Array lIiarray" zugegriffen. Im zweiten Beispiel wird dasselbe gemacht, nur durch ei ne Addition der Zahl 2 zur Adresse des ersten Elements vom Array lIiarray". Die Klammerung hat hierbei eine höhere Bindungskraft als der Indirektionsoperator und wird daher zuerst ausgewertet. Erst daraufhin wird durch den Indirektions· operator * der entsprechende Wert dereferenz iert. Um also auf das n-te Elem en t eines Arrays zuzugreifen, haben Sie die fo lgenden (direkten und indirekten) Möglichkeiten: i nt ia rray [10] : i nt *i ptrl . *iptr2 : iptrl - iarray ; i ptr2 - i array + ) ,
11 i ptrl 11 i pt r 2
iar r ay[Ol - 99 , i ptr l[ll *(iptrl+2J - 77 : .. 66; * i ptr2
11 11 11 11
- aa ,
11 Dek larat i on
auf An fa ng s ad resse auf 4 . Element ,,"
Zuweisung Zuweisung Zuweisung Zuweisung
ao ao ao ao
,,"
iarray iarray
i array[O] i array[l] i array[2] i array[3]
Hinweis Auch wenn Sie bei den Arrays in der Praxis den Indirektionsoperator "* staU des Indizierungsoperators [] einsetzen können. ist davon abzuraten, da dies den Code nur unnötig verkompliziert. Auch in puncto Codeoptimierung lässt sich dadurch kein Vorteil erzielen.
2.5.1
C-Strings und Zeiger
Rt>i dt>r Vt>rwt>ndllng von C-Strings lind Zeigern g ibt es eigentlich nichts Nelles
7. 11
berichten, was nicht auch für Arrays und Zeiger gilt. Letztendlich sind C-Strings nichts anderes als Arrays aus einzelnen Ze ichen sta tt Zah len. Som it gilt alles zuvor Beschriebene auch in Verbindung mit den C-Strings und Zeigern. Aber hierzu vielleich t noch ein Beispiel einer Subtraktion von Zeigern. Solch eine Subtraklion sollte allerdings nur dann verwendet werden , wenn (logischerweise) zwei Zeiger auf ein Elemen t im selben Array bzw. ( -String zeigen. Wenn Sie zwei
[« )
I
2
I
Höhere und fortgeschrittene Datentypen
Zeiger subtrahieren , erhalten Sie gewöhnlich die Anzahl der Array- bzw. CString-EIemente, die zwischen den beiden Zeigern liegen: 11 zar ray 4. cpp fli ncl ude
int main(\(oidl { cha r carray[] - "Ein einfacher String ": cha r *cptrl . *cp tr 2 : cp tr } - carray : whi l e{ *cptrl !) I cptrl++ ; cpt r 2 - ++cptrl; while( *cptr2 !- ' . ) ( cptr2++ ; cout « cptr2 cptr l return 0;
«
' \n ';
Die Ausgabe des Programms beträgt »9.. - was bedeutet. dass zwischen der Adresse von Zeiger »cptr2 .. und »cptr1 .. 9 Elemente vom Typ char liegen. Im Beispiel wurde jeweils nach einem Leerzeichen gesucht und der Abstand von einem zum nächsten Leerzeichen ausgegeben.
M ehrfache Indirektion
2_5.2
Zugegeben die Überschrift ist ein wenig verwirrend, und im Grunde will ich auch gar nicht allzu sehr auf dieses Thema eingehen, aber es ist auch möglich, Zeiger auf Zeiger zu deklarieren: Typ "
name :
Hier haben Sie einen Ze iger deklariert, der auf einen Zeiger verweisl. der wiederum auf eine Variable verweist und somit auf di ese Variable zugreifen kann . Das Ganze wird als mehrfache Indirektion bezeich net. Theorelisch können Sie noch mehr als diese zweifache Indirektionen verwenden: Typ **~'
name :
Aber irgendwo ist die Grenze erreicht. Gewöhnlich werden Zeiger auf Zeiger mit zwei Indirektionsoperatoren verwendet. Das Haupteinsatzgebiet liegt gewöhnlich bei der dynamischen Erzeugung von mehrdimensionalen Arrays (daher
,66
Arrays und Zeiger
wurde dieser Abschn itt auch bei den Arrays verwendet und nicht bei den Zeigern). wie dies zum Beispiel bei einer Matrizenberechnung verwendet wird. In der "Praxis« lässt sich ein Zeiger auf ei nen Zeiger wie fol gt einsetzen (wobei das Beispiel relativ wen ig Sinn mach t):
// pt rpt r l .cpp /Ji nclu de using names pace std ; i nt ma i n(voidl I i nt iwe rt - 111 : 11 i pt r ve rweist auf die Ad r esse von wert i nt *ip tr-&iwert : 11 i pt r ptr verweis t auf die Adresse von i ptr i nt **i pt r ptr- &i ptr : 11 Dereferenzi erung cout « ·* ipt r cout « · **iptrptr :
«
*iptr
«
' \n ' :
« *-iptrptr « ' \ n':
11 doppelt in d irek t e Zuwe is ung ; - ) **i ptrptr - 222 : 11 Dere fe ren zierung « *i pt r « ' \n ': cou t « "*i ptr cou t « " **i ptr ptr : « **iptrptr « ' \n ':
11 ei nfache indi rekte Zuweis ung
*iptr - ]]] ; // De refe renzi erung cout « "*i ptr cout « "* *i ptrp t r return 0;
« «
*ip t r « ' \n ': **i ptrp t r « '\n ':
Das Programm bei der Ausführung:
. ; ptr **ipt rpt r *; ptr **ipt rpt r *; ptr H ipt rpt r
111 111 222 222 333 333
Hätten Sie im Beispiel anstatt der Dereferenzierung von "iptrptr« mit der doppelten Indirektion eine einfac he Indirektion verwendet. so hätten Sie nicht den
I
2·5
I
2
I
Höhere und fortgeschrittene Datentypen
Wert der Variablen _iwert« verändert. sondern der Zeige r lIiptrptr« würde auf eine ungültige Speicheradresse zeigen. Somit lässt sich zusammenfassen (auf das Beispiel bezogen), dass ,.iptrptr« (ohne einem Indirektionsoperator) eine Variable vom Typ char *" ist, die die Adresse von ,.iptr« beinhaltet. ,.·i ptrptr« (mi t einem Indirektionsoperator) ist eine Variable vom Typ char * welche wiederum die Adresse von ~iwert« beinhaltet, und ,..... iptrptr« (mit zwei Indirektionsoperatoren) ist eine Variable vom Typ cha r mit dem eigentlichen Wert von ,.iwert« - also am Anfang 111.
2.5.3
C-String-Tabellen
C-String-Tabellen si nd den Zeigern auf Zeiger nicht unähnlich, aber - wie schon die Relation von lIArray und Zeiger.. - nicht dasselbe, sondern mehrdimensionale char-Arrays. Hier eine solche C-String-Tabelle: char* cptrptr[] - I "Stringl ". "Stri ng Z". "String3 " I ; Von den Ze igern wissen Sie ja noch, dass die Schreibweise ""ptr. und "ptrIO]« auf dieselbe Adresse verweist. Dasselbe lässl sich auch hier über ""cplrptr« und ,,·cptrptrIOJ« sagen. Somit können Sie auch hier wie folgt auf die einzelnen Elemente zugreifen : cout cou t cou t
« « «
*cptrptr « ' \ n'; *(cptrpt r + 1) « ' \n '; *(cptrptr + 2) « ' \n ';
/I Stringl // StringZ // String3
Aber auch hie r sollten Sie der Lesbarkeit zuliebe den Indizierungsoperator [] verwenden: cou t cou t cou t
« « «
cptrp tr[O] cptrptr [1] cptrpt r[2]
« « «
' \n ': ' \n ': ' \n ':
J/ Str i ngl J/ J/
Str ing2 Str ing3
Natürlich können Sie auch auf die Zeichen der einzelnen C-Strings zugreifen. Bei den folgenden drei Beispielen greifen Sie automatisch auf den dritten Buchstaben des zweiten Slrings zu. Drei verschiedene Möglichkeiten stehen Ihnen hier zur Verfügung: JJ 2 . St r ing "String2" -) 3. Buchstabe ' r ' cout « ' (*(cptrpt r+ 1 )+2) « ' \n ': cout « *(cptrptr[l]+2) « ' \n ': cout « cptrptr[I](2) « ' \n ':
Die Tabelle zeigt (2.2), wie Sie noch auf den n-ten C-String und das rn-te Zeichen zugreifen können :
Arrays und Zeiger
Zugnff auf ...
Mogllchkelt 1
Mogllchkeit 2
Mogllchkelt l
1.String, 1.Zeichen
**marray
' marray[O]
marray [ O][O]
LString,l.Zeichen
** (marray+i)
*marray[i]
marray [ i][O]
1.String, LZeichen
*(*marray+i)
*( marray[O] +i)
marray [ O][ i ]
i.String, j .Zeichen
*(
'( marr ay[ l] +j)
marray [ l][ j ]
·( mar ra y+l )+j )
Tabell e 2.2 Äquivalenz zwischen Zeigern auf Zeiger und mehrdimensionalen Arrays
Natürlich können Sie solche C-String-TabelJ en auch dynamisch anlegen . Die Deklaration einer solchen Tabelle sieht wie folgt aus: char ·words[256) : Hiermit haben Sie ein char-Array mit 256 char-Zeigern deklariert. Sie können damit jedem dieser 256 Zeigern eine Adresse auf einem C-String zuweisen. Das fo lgende Beispiel demonstriert nochmals den gesamten Vorgang zu den CString-Tabellen in der Praxis: 11 ptrptr2 . CPP llinclude
int main(void) 1 char* cpt rpt r [] - 1 "Stringl" . ·Str i ng2 ". "S tring3 " ): char * cst r i ng [3] : char carray [ ] - " und " : cou t cou t cout
« "- cpt r ptr « ' \n ': « "-(cptrptr + }) « ' \ n': « *(c ptl' ptr + 2) « ' \n ':
cout cout cout
« cp tr ptr[O] « ' \n ' : « cp trp tr[1 ] « ' \n ' : « cptrptr[2] « ' \n ' :
2. St r ing "S tring2 * - ) 3 . Buchs t abe cout « • ( • (c pt rptr'f'} H'2) « . \n ' : cout « *(cptl' ptr[I]+2) « ' \n ' : cout « cptrptr(l][2] « ' \ n' : 11
"
.
cstring[O) - cptrp t r(O) : cstring[l) - carray : cstring[2) - cptrptr[l) :
169
I
2·5
I
2
I
Höhere und fo rtgeschrittene Datentypen
cout « cstr i ng(O] re turn 0:
«
cstr i ng[l]
«
cstr i ng[2]
«
' \ n ':
Das Programm bei der Ausführung: Str i ngl Str i ng2 Str i ng3 Str i ngl Str i ng2 Str i ng3 c c c St ri ngl und Str i ng2
2.5.4
Arrays im Heap (Dynamisch es Array)
Häufig will man zum Programmstart so wenig Ressourcen wi e nötig verbrauchen und die Speicherobjekte zur Laufzei t dynamisch anlegen. Wenn Ihnen das Reservieren von Speicher einzelner Variablen zur Laufzeit in Abschnitt 2.1.5 etwas sinn los erschien, so dürfte Ihnen der jetzt vorliegende Grund sinnvoller erscheinen. Die Syn tax, um Speicherplatz vom Heap fü r ein Array zu reservieren, lautet: Ze i ger - new Typ [E lemente ] :
Hier ist ,.Zeiger« das erste Objekt in einem Array von »Elemente«-Objekten, Beispiel: int * iptr - new i nt (100) :
Hiermit zeigt der Zeiger »iptr« auf das erste Objekt von 100 i nt-Objekten. Jetzt können Sie mi thilfe des Indizierungsoperators Werte an das Array zuweisen. iptr[O] - 100 : iptr[l] - 200 : iptrr99 1 - 9900 :
Hierzu ein Programm beispieL das eine Abfrage ausfUh rt. wie viel Speicherplatz vom Heap Sie reservieren wollen. Anschließend wird Platz für die von Ihnen angeforderte Menge reserviert, und Sie können Werte eingeben. Alle Werte werden addi ert, sodass am Ende die Summe aller eingegebenen Werte ausgegeben wird. Zum Schluss wird der angeforderte Speicher vom Heap mitlels delete wieder freigegeben. Hierbei müssen Sie die eckigen Klammern mit angeben, um
170
Arrays und Zeiger
I
2·5
dem Compiler zu signalisieren, dass ein Array zu zerStören ist. Ohne die eckigen Klammern wurden Sie nur das erste Element freigeben! 11 dynarr1.cpp lIinclude us i ng namespace std :
int main(voidl { i nt *dyna rray ; int el ements ; i nt sum- O; eout « "W i e viele Werte wollen Sie speichern : ein » elements : // Speicher reservieren dynarray - new int [e l ements] ; for(int i-O ; i<elements ; i++) " . cout « ;+1 « " . Wert cin » dynarray [i] : cout « "Oie Summe aller We r te l autet for(int i- O: i<elements : i++J I sum +- dynarray[ i ) :
".
cout « sum « ' \n ': // Speicher wieder freigeben del ete (] dynarray : return 0: Das Programm bei der Ausführung:
Wie viele Werte wollen Sie speichern : 5 1. Wert 11 2. Wert ZZ 3. Wert 33 4. Wer t 44 5. Wert 55 Die Summe alle r Werte l au t et : 165 Hinweis
Die Operatoren new und delete wurden bereits in Abschnitt 2.1.5 behan-
delt. Hier folgt wohl die unausweichliche Frage. was zu tun ist. wenn einem der Spei· cherplatz nicht ausreicht und man ein dynamisches Array weiter vergrößern will? Leider ist das nich t so einfach. Wer als ( -Programmierer denkt, es gibt in C
[«)
I
2
I
Höhere und fortgeschrittene Datentypen
eine rea l loc( )-Ahernative wie zum Beispiel »renew«, der wird bitter enttäuscht werden. Wenn Sie ein bereits alloziiertes Array vergrößern wollen , müssen Sie folgende Schritte ausführen: .. Einen neuen größeren Speicherplatz im Heap anfordern ..
Den Inhah des allen Arrays ins neue Array kopieren
..
Das alte Array mit de 1ete löschen
..
Den alten Zeiger auf das Array auf das neue Array verweisen lassen
Das Ganze könnte zum Beispiel in einer Schleife verpackt und endlos durchlaufen werden. Aber aus Übersichtlichkeitsgründen soll dieser Vorgang gleich in eine Fun ktion verpackt werden, die als ersten Parameter das alte Array erwartet, als zweilen Parameler di e Größe des alten Arrays und als drilten Parameter die Größe, die das neue (+ahe) Array danach haben soll. Als Rückgabewen gibt diese Funktion einen Zeiger auf die Anfangsadresse des neu reservierten Speichers im Heap zurück. In diesem Beispiel wurde außerdem die Übergabe von Arrays bzw. Zeigern an eine Fun ktion als Parameter vorgezogen, was in Abschnitt 2.6 näher erläutert wird. 11 dynarr2 . CPP llinclude
i nt * renew( i nt * old . int sizeo l d . i nt sizenew ) I 11 GröBere n Speicher alloziie ren i nt · tmp - new i nt [s izenew] : 11 Alten Inha lt nach tmp kop i eren fore i nt i- O: i < s i zeold : i ++ ) I tmp[ i ] - old[il; 11 Alten Inhalt l öschen dele te [] old : /1 Neues Ar ray zu rO ckgeben return tmp :
int main(voldl I int *dyna r ray - new int[3] ; fore i nt 1-0; i ( 3; i++ ) ( dynarra y[i] - i ; 11 Array vergr öBern au f fanf El emente dynarray- r enew{ dyna rray. 3. 5 I ; for e int i-3 ; i < 5; i++ I (
172
Arrays und Zeiger
dynarray[i] - i ; fore i nt i - O: i < 5 : i ++ ) 1 cout « dynarray[i] « ' \n ': return 0 :
Das Programm bei der Ausführung:
o I
2
J 4
Zweidimensionale dynamische Arrays Bei den Zeigern auf Zeiger wurde bereits erwähnt, dass sich hiermit mehrfach dimensionierte Arrays dynam isch erstellen lassen. Im Folgenden soll ein solches zweidimensionales Array mit n Zeilen und m Spalten erstellt werden. Aus int ~'
mat r i x :
soll so mit i nt mat r i x [zei l e](spalte) :
werden. Um ein solches zweidimensionales Array zu realisieren . müssen Sie zunächst Platz fü r die Zeilen oder genauer Zeilenadressen reservieren. Den Speicher fur eine Zeile können Sie wie folgt reservieren: int~ ~
ma t r i x ;
mat r ix - new i nt* [z e il e ] ;
Damit haben Sie schon mal Platz fur eine bestimmte Anzahl von Zeilen (bzw. für die erste Dimension) reserviert (hier drei Zeilen).
Iint" matri)(; ~
matri)([OI malrix[1] malri)([2j
Abbildung 2.9 Reservierung des Speichers für die Zeile (erste Dimension)
I
2·5
I
2
I
Höhere und fo rtgeschrittene Datentypen
Im nächsten Schritt können Sie rur die einzelnen Zeilen (Zeile für Zeile) Speicherplatz für die Spalten (oder genauer für die zweite Dimension) reservieren . Dies wird gewöhnlich in einer Schleife, die Zeile für Zeile durchläuft, realisiert for ( int i - 0 : i < zeile : i ++) I matrix[i] - new int [spalte ] : Jetzt haben Sie Speicherplatz rur das zweidimensionale Array reservien und können es mit Wenen belegen (hier mit zwei Spalten).
Iint" matrix; ~
Ab bildung 2.10
matrix[Oj
f------o-
matrix[OJ[O] matrix[Oj[1j
matrix[1j
f------
matrix[1][O] matrix[1](1 J
matrix[2j
f------o-
matrix[2]COj matrix[2][1]
Nach der Reservierung des Speichers rur die Spalte (3x2)
Beim Freigeben des Speichers mittels delete müssen Sie all erdings darauf achten, dass dies in umgekehrter Reihenfolge geschieht Hierzu nun das komplette Beispiel: 11 dynarr3 . CPP llinclude us i ng name space std ;
int main ( voi d ) I int i . j . zei l e . spalte : int . .. matrix ; cout « " ~ ie vi ele Zeilen ei n» zeile ; cout « " ~ie vi ele Spalten : " . ei n» spalte ;
..
11 Speiche r fO r di e einzelnen Zeilen rese r vieren matrix - new int * [zeile] ;
11
S~elthe r
f ür oie einzel nen Spillten in oe r i-ten Zeile
for(int i - 0 ; i ( zeile ; i++) ( matrix(i) - new int [spalte] ; 11 Mit beliebigen Werten initi al is i eren for (i - 0 ; i < zeile : i++ ) [ f or (j - 0 : j < spalte : j ++) ( ma tr i x[i][j] - i + j ; 11 matrix[zeile][spalte )
174
Parameterübergabe mit Zeigern , Arrays und Referenzen
Inhalt aus geben for (i - 0: i < zeile : i++) r for (j - 0: j < spalte : j++) cout « matrix(i)(j) « "
11
cout 11 11
« ' \ n':
Spe i che r platz wieder freigeben Wich t ig! I n umgekehrter Reihenfolge
11 Spa I t en der i -ten Zei 1e frei geben for(i - 0: i < ze i le : i++) dele t e matrix[ i ) : 11 Jetzt können die leeren Zeilen fr eigegeben werden delete [l matr i x: return 0:
Das Programm bei der Ausführung :
Wie viele Zeilen 5 Wie viele Spalten : 4
o1
2 3 234 234 5 3 4 5 6 456 7
2.6
Parameterübergabe mit Zeigern, Arrays und Referenzen
Sie kennen bereits die Möglichkeit. Daten zwischen Funktionen auszutauschen durch die Parameterübergabe und durch globale Variablen . Globale Variablen sollten aber aufgrund der Fehleranflilligkeit möglichst vermieden werden un d sind in der Praxis selten sinnvoll. In den Abschnitten zu den Fun ktionen haben Sie Parameter auch als forma le Parameter kennen gelernt. Solche formalen Parameter di enen immer als Platzhalter für die aktuellen Parameter.
175
I
2.6
I
2
I
Höhere und fortgeschrittene Datentypen
2 .6 .1
Call
by value
Bei der Übergabe von Parametern unterscheidet man zwischen zwei Arten , dem »call by value« und dem »call by reference«. Sie haben bisher im Kapitel über Funktionen die Werte nach "call by value« an die Funktionen übergeben. /! callbyval . cpp llinclude
void callbyva lu e( int a , int b 1; i nt ma i n(voidl I i nt iwertl - 100 . iwert2 - 5 ; callbyvalue{ i wer tl . iwert2 ) ; cout « i wer tl « ' . « i we rt2 return 0;
«
' \n ';
..
void callby va lue{ inta . intb) { « b « · _ . « (a*bl« ' \n '; cout « a « .
a - 0; b - 0;
Das Programm bei der AusfLihrung: 100 * 5-500 100 5
Am Ende der Funktion »call by value« wurden die beiden übergebenen Werte auf o gesetzt, was auf die Originalwerte in der mai n-Funktion keinen Einfluss hat, wie die Ausgabe bestätigt. Somit können Sie al so sicher sein, dass beim ,.call by value« immer nur eine Kopie an die Funktion übe rgeben wird. Bei einer Übergabe als Kopie muss bei jedem Funktionsaufruf ein e Kopie des oder der aktuellen Parameter erstellt werden. Dies ist bei einfachen Basisdatentypen kein Problem. Aber wenn die Objekte komplexer und umfangreicher werden - was in der objektorientierten Programmierung gewöhnlich der Fall ist müssen oft umfangreiche und komplexe Objekte an die Funktion übergeben werden. Muss bei jedem Funktionsaufruf ein solches Objekt erzeugt und wieder zerstört werden, so kann sich dies auf das Laufzeitverhalten des Programms sehr negativ auswirken.
Parameterübergabe mit Zeigern , Arrays und Referenzen
2.6.2
Call by reference - Zeiger als Funktionsparameter
Um solche Schwierigkeiten zu vermeiden, wird das "call by reference« verfahren bevorzugt. Dabei geben Sie nicht die Objekte selbst an eine Funktion , sondern nur die Adressen der Objekte. Dadurch können die Objekte durch eine Dereferenzierung des Zeigers direkt in der Funktion bearbeitet oder verändert werden. Natürlich bedeutet die übergabe einer Adresse als Parameter auch, dass sich jede Veränderung (mittels Dereferenzierung) auf das Original bezieht, weil keine Kopie mehr übergeben wird, Natürlich müssen Sie auch die Deklaration und Definition der Funktion anpassen, und beim Aufruf der Funktion milssen Sie selbstverständlich die Adresse und nicht mehr die Variable selbst übergeben.
vo id call byreference( int * a . int * b ) : // Funktionsau f ru f callbyr efe rence( &i we rtl . &iwert2 ) : Wenn Sie den Wert auch verwenden wollen, ist zu beachten, dass Sie diesen in der Funklion mit dem Indi rektionsoperator verwenden müssen. Obwohl das selbstverständlich sein sollte. führt es immer wieder zu Fehlern beim "call by reference« Verfahren: /I callby r ef , cPP llinclude
vo i d call byvalue( i nt a . i nt b ) : vo i d call byreference( i nt * a . int* b ) : int ma;n(vo;d) 1 int i wertl - 100 . iwert2 - 5: cou t « "call by value : \n" : ca l lbyvalue( ; we rt l. iwert2 ) : cout « iwe r tl « .. « iwe rt2 « "\n\n" : cout « "ca ll by re ference : \ n": c;rllbyrefe re nce( &iwertl . &iwert2 ) ; cout « i wertl « . , « i wert2 « "\n\n ": re t urn 0:
void callbyvalue( int a , int b ) { [out « a « " • " « b « " - • d - 0;
«
(a'b)
«
' \n ':
I
2.6
I
2
I
Höhere und fortgeschrittene Datentypen
b - 0:
void callby r eference ( int* a . in t * b 1 cout « *a « • * • « *b « « ( *al * ( *bl « ' \n ': *a - 0 : *b - 0 :
Das Programm bei der Ausftihrung: call by value : 100*5 - 500 100 5 call by reference : 100*5 - 500 o0
Anhand de r Ausgabe des ,.call by reference .. verfahren lässt sich erkennen, dass sich eine Veränderung der übergebenen Adresse gleich auf das Original bezieht. Die Klammerungen in der Funktion ,.call by reference .. wurden nu r wegen der Obersichtlichkeit verwendet.
2.6.3
Call by reference mit Referenzen nachbilden
Vielleich t finden Sie die mehrfache Dereferenzierung mit dem Indirektionsoperator beim ,.call by refe rence~ Verfahren in den Funklionen auch etwas umständlich. Außerdem ist die Anwendung nicht sehr lesefreundlich und recht anfallig für Fehler. Dann kommt noch die umständliche Übergabe der Werte mit dem Adressoperator hinzu. Viele kleine Dinge. die zum einen mehr Arbeit machen und zum anderen zu Fehlern führen können. Der Auffassung waren auch die Erfinder von C++, und sie haben vorwiegend deshalb die Referenzen (S iehe Abschnitt 2.2) ei ngeführt. Mi t den Referenzen können Sie das ,.call by reference .. nachbilden. Das bedeutet, kein Adressoperator beim Funktionsaufruf un d keine wilden Dereferenzierungen mehr in den Fun ktionen selbst und trotzdem alle Vorzüge des ,.call by reference .. Verfahrens . Es ist alles wie beim »call by value .. Verfahren, nur dass der Funktionskopfbei der Deklaration und Definiti on anders aussieht: void callbyreference2( i nt & a , ; nt& b ) ;
178
Parameterübergabe mit Zeigern, Arrays und Referenzen
Hierzu die nachgebildete ,.call by reference.. Funktion mit Referenzen in der Pra· xis: 11 call byref2 . cpp Ifi nclude using namespace std : void callbyreference2( in t & a . i nt& b ) : lnt main(void) I int i wert l - 100 . i wert2 - 5 : cou t « "call by re f erence (aber Re f erenzen) : \n ": ca l lby r e f ere nce2 ( i wertl . iwert2 ) : cou t « iwe r tl « . , « iwe r t2 « " \n\o" : return 0 :
void callby r efereoce2( ;ot& a. ; nt& b ) I cout « a « " * " « b « " - " « (a *b)
« . \n ':
a - 0: b - 0,
Das Programm bei der Ausftihrung: call by r eference (Ober Referenzen) : 100 * 5 - 500
o0 Wollen Sie an die Funktion ,.call by reference2 « statt zwei einfache Variabl en zwei Zeiger übergeben, müssen Sie den Indirektionsoperator beim Funktionsaufruf verwenden: int ivall - 100 , iva12 - 5 : iot *;wertl - &ival1. *iwe r t2 - &iva12 : callby r e f erence2( *i wertl . *i wert2 ) :
2.6-4
Arrays als Funktionsparameter
Arrays werden an Fun ktionen als Parameter wie Zeiger im . call by reference.. Verfahren übergeben. Zum Glück, denn man stelle sich vor, dass ein Array mit 1000 Objekten an eine Funktion kopie rt wird. Die Deklaration und Defi ni[ion des Funktionskopfs kann demnach so void callarray( int* a r r ay ) :
179
I
2.6
I
2
I
Höhere und fortgeschrittene Datentypen
oder auch so aussehen
void callarray( int arr ay[] ) ; Sie können beim Array also die Array-Deklaration mi[ der Zeiger-Deklaration als formalem Parameter einfach vertauschen. Beachten Sie allerdings , dass dies eine Besonderheit ist, denn bei Funktionen »zerfallt.. ein Array sofort in einen Zeiger, und somit wird niemals wirklich ein Array an eine Funktion übergeben. Dieser Umstand ist für AnHinger etwas verwi rrend, weil dadurch der Eindruck entsteht. dass Zeiger und Arrays dasselbe sind. Aber diese Umwandlung eines Array zu einem Zeiger gilt wirklich nur für die formalen Parameter einer Funktion. Dass ein Array hier zu einem Zeiger zerfallt, macht es außerdem unmöglich anzugeben, wie viele Elemente das Array beinhaltet. Folgendes wäre also falsch:
vo i d call a rray( i nt array[lOJ ) ;
11
f alsch! !!
Daher ist es empfehlenswe rt, die Elemente eines Arrays immer als Extraargument mitanzugeben:
vo i d callar ray( i nt array[J. in t elements 1; 11 ode r auch vo id callarray( i nt* ar r ay . int elements ) ; Aufgerufen wird diese Funktion wie folgt
callar r ay( i array , sizeof(iarrayl/s i zeof(iarray[O]» : oder aber auch mit
callar r ay( &iarray[O] . sizeof{ i array)Jsizeof(iarray[O]» ; Theoretisch müssen Sie hierbei nicht zwangsläufig die Adresse des ersten ArrayElements übergeben. Würden Sie ,,&iarray[21 " als Argument verwenden. würden Sie die Adresse des dritten Elements an die Funktion übergeben. Beach ten Sie allerdings dann den zweiten Parameter. Denn solhen Sie alle Elemente durchlaufen, kann es passieren, dass Sie den Speicherbereich überschreiten. Hierzu nun das Programmbeispiel. wie Sie ein Array an eine Funktion als Parameter übergeben: 11 callar r ay . cpp llinclude
vo id callar ray(
i nt ~
ar ray , int e l ements ) ;
int main(void) I int iarray(] - I 11 . 22 , ]] ) ;
,80
Parameterübergabe mit Zeigern , Arrays und Referenzen
callarray( iarray , sizeof(iarrayl/sizeof(iarray[O]ll ; return 0;
void calla r ray( int * array , int elements 1 I fore int i- O: i < elements : i++) I cout « array [ i ] « ' \n ':
Das Programm bei der Ausführung: II
22 33
Natürlich gilt auch hi er, dass sich j ede Veränderung des Arrays in der Funktion auch auf das Original bezieht, daja auch mit Adressen gearbeitet wird. Wenn Sie unbedingt ein Array als Kopie an eine Funktion übergeben wollen, können Sie sich eines Tricks bedienen. indem Sie ein Array in eine Struktur verpacken (siehe Abschnitt 2.8.1) . Zu Referenzzwecken hierzu das Listing (ohne hier näher darauf einzugehen): 11 callar ray2.cPP lIinclude
struct sa r ray I i nt i[3] : I : typedef struct sarray SARRAY : void calla rr ay( SARRAY a rray , int elemen t s ) ; int ma i n(voidl I SARRAY iar ray : i array . i[O] - 11 : i array.i[}l - 22 : i array.i[21 - 33 : callarray(i array . sizeo f (ia r ray . i)/sizeof(iarray . i[O]) ; re t urn 0;
void calla r ray( SARRAY a r ray . int elements) I fore int i- O; i < elements : i++l I cout« array . i[i]« '\n ';
,8,
I
2.6
I
2
I
Höhere und fortgeschrittene Datentypen
2.6 .5
Mehrdimension ale Arrays an Funktionen übergeben
Natürlich ist es auch möglich. mehrdimensionale Arrays an ei ne Funktion als Parameter zu übergeben. Wenn man ein zweidimensionales Array an eine Funktion übergibt. ~zerfallt« das zweidimensionale Array (Array auf Array) zu einem Zeiger auf Arrays (nicht etwa zu einem Zeiger auf einen Zeiger). Somit kann der Funktionskopf wie folgt deklariert und definiert werden
void callrnarray( i nt ar ray[][elernents] ) ; oder. was auch der Compiler daraus machen würde :
vaid callrnarray( i nt ar raY(*PtrJ (el ernents] ) ; Wie schon bei den einfachen Arrays als Parameter stellt die aufgerufene Funktion keinen Speicher für ei n Array in der ersten Dimension bereit, was Sie gegebenenfalls selbst übernehmen müssen. Die zweite Dimension (und alle wei teren) müssen Sie immer mit angeben. Hierzu wieder ein einfaches Beispiel, das demonstriert, wie Sie ein zweidimensionales Array an eine Funktion übergeben können: 11 callrnarray . cpp Ifi nclude
const i nt DIM l-4 : const i nt DIM2- 3: void callmarray( i nt marray[][DIM2] . int di rn} ) ; int main(vaid) I int imarray[D IMI][0IM2] ; far(int i - O: i < OIMl ; i++) (ar(int j - O; j < DIM2 ; j ++) irn array[i][j] ... i+j :
ca Ilmarray( imarray . DIM1) ; return 0:
void callmarray( int rnarray[ ](0I M2] . i nt di rn}) I f or(int i"'O ; i < dirnl ; i++ ) I f or(int j - O; j < 01M2 : j ++) I cout « ma r ray[i ][j] « '
,82
Parameterübergabe mit Zeigern , Arrays und Referenzen
cout
«
I
2.6
' \n ':
Das Programm bei der Ausführung :
o1
2
2 3 234 3 4 5
2.6 .6
Argumente an di e main-Funktion übergeben
Auch der ma in-Funktion können beim Programmstart Parameter übergeben werden, Standardmäßig hat main() zwei Parameter: i nt main( int arge . ehar "'*argv ) : »argc« gibt die Anzahl der Argumente zurück. die der mai n-Funktion übergeben wurden, Da der Name des Programms automatisch als erstes Argument verwendet wird, ist dieser Wert immer mindestens mit 1 belegt. "argv« hingegen ist eine String-Tabelle welche die einzelnen Argumente als (-Strings beinhaltet. Beispielsweise enthält »argv« gewöhnlich folgende Einträge:
argv(O] argv(l] argv(2]
- Programmname - e rstes Argument - zweites Argume nt
argv[argc] - 0 Das letzte Element in .. argv« enthält also immer den Wert 0 und kann daher ebenso als Abbruchbedingung verwendet werden wie "argc«. Hinweis Die Namen .. argc« und _argv .. sind nicht so vorgeschrieben und können genauso gut anders heißen. Aber in der Praxis werden diese Namen fast immer so verwendet
Dem Programm werden Argumente beim Aufrufübergeben. Unter MS Windows kann dabei das alte MS-DOS verwendet werden. Die MS-DOS-EingabeaufTordcrung von MS Windows ist dafür aber auch geeignet. Unter linux genügt eine einfac he Konsole bzw. ShelL Die einzelnen Argumente, die dem Programm per Kommandozeile übergeben werden, müssen immer mit mindestens einem Leerzeichen getrennt sein. Beispiel:
Prompt> programmname argumentl argumente2 arg ument 3 argv[O] - pr ogrammname argv[l] - arg umentel
183
[« J
I
2
I
Höhere und fortgeschrittene Datentypen
argv[2] - argumentel argv[3] - arg umentel
Sofern Sie eine Entwicklungsumgebung ODE) verwenden, müssen Sie Kommandozeilen-Argumente anders übergeben. Viele Entwicklungsumgebungen bieten hierfü r beim Menü »Ausführen« noch ein Untermenü ,. Parameter« oder Ähnliches (abhängig von der IDE) an, um die Argumente noch vor dem Programmstart festzu legen .
[»]
Hinw ~ i s Wie die übergabe von Kommandozeilen-Argumenten bei einer Entwicklungsumgebung genau funkt ioniert, finden Sie auf der Buch-CD zu den gängigeren Compi lern .
Hierzu ein Programmbeispiel. das die Argumente auswertet, die Sie dem Programm beim Start mit übergeben : 11 a r gmai n. epp lIinclude
int main( int arge , char *a rgv[] } I cout « ·D i e Argumente der Kommandozeile an main ; \n · ; eOut « ·P rogrammname : " « argv[O] « ' \n '; for ( int i - I ; i < arge ; i++} I eout« · argv[ ~ « i «"] : " « argv[i]« ' \n '; return 0:
Das Programm bei der Ausführung: Prompt) argma1n Hallo Welt w1e ge hts Di e Argumente der Kommandoze il e an main : Progr ammname : C: \ Dev -Cpp \a rgma in . exe argv[I] : Hallo argv[2] ; Welt argv[3] ; wie argv[4] ; gehts
In »argvIOl .. befindet sich meistens der Programmname, dies muss aber nicht zwangsläufig so sein , Ein Beispiel: ehar * argv~for _n ew_aDP[] - I • ga nzAndere rName" , .... argumente I,
cha r *app l ication - ·programmname · ; execve(a pplication, argv _ for _new~ap p. envp) ;
Rückgabewerte von Zeiger, Arrays und Referenzen
Somit ist in lO argvIO]« von ,.programmname« nun lOga nzAndererName« zu lesen. Das ist u,a. ein effektiver Workaround ftir DOSlWindows Plattformen, die keine symbolischen Links haben (d.h. manche Programme erkennen ihre Funktion an lO argv[O]«).
2.7
Rückgabewerte von Zeiger, Arrays und Referenzen
Bei einfachen Basisdatentypen ist es nich t unbedingt nötig. fü r den Rückgabewen einen Zeiger oder Referenzen zu verwenden. Aber bei etwas umfangreicheren oder dynamisch erzeugten Objekten sollten Zeiger oder Referenzen verwendet werden. 2.7.1
Zeiger als Rückgabewert
Sofern der Rückgabewen ein bereits bestehendes Element ist, empfiehlt es sich. Referenzen stau Zeiger zu verwenden. Bei neuen (dy namisch erzeugten) Elementen sollte man auf einen Zeiger zurückgreifen. Die Syntax eines solchen Funktionskopfs sieht wie folgt aus:
Typ* functio n( paramete r ) ; Aufgerufen wird eine solche Funktion folgendermaßen:
Typ* ptr ; ptr - function (argumente) ; Die Verwendung von Zeigern als Rückgabewert aus Funktionen wird gerne bei Arrays oder Strukturen verwendet. Gerade bei Arrays bzw. C-Strings ist (war) dies die einzige Möglichkeit . ganze Felder bzw. Zeichenketten aus einer Funktion zu rückzugeben. Natürlich wird auch hier immer nur eine Adresse auf das erste Element zurückgegeben. Hierzu ein einfaches Beispiel: 11 dynamicarray . cpp lIincl ud e
using namespace std ; int ~ dynamic_a rray( int elements ) : int " renew ( int* old . i nt sizeold . i nt sizenew ) :
int main(void) 1 11 Reservie r t Speicher fO r ze hn El emente int 'i pt r - dynamic_ar ray( 10 ) : 11 Wert an die einzelnen Elemente zuwe i sen
185
I
2 .7
I
2
I
Höhere und fo rtgeschrittene Datentypen
fore in t i - O: i ( 10 : i++ ) I i pt r[i) - i : weitere fOnf Elemente reservieren i ptr - renew( iptr , 10 , 15 ) : 11 .. , und initialis i eren fore in t i - 10 : i ( 15 : i++ ) ( ipt r [i] - i: 11
I
11 Alles Ausgeben fore i nt i - O: i ( 15 : i++ ) cou t « i ptr [ i ] « ' \n ': I 11 Speic her wi eder fre igeben
delete (] i ptr : return 0:
i nt *dynamic_a rr ay( int elements) ( i nt *array = new int [elements] : 11 An f angsad resse zurOckgeben return ar ray :
i nt * renew( i nt - oId , int sizeold , i nt sizenew ) ( 11 GröBere n Speicher alloziie ren i nt * tmp - new in t [s i zenewJ : 11 Alten Inha lt nach tmp kopieren fore i nt i- O: i < s i zeol d: i++ ) ( tmp[i] - old[i] : I
11 Alten Inhalt löschen delete (] old : 11 Anfangsad resse zurOckgegeben return tmp : Beide Funktionen, "dynamic array« und "renew«, alloziieren jeweils Speicherplatz auf dem Heap und geben die Anfangsadresse des Speicherblocks an den Aufrufer zurück , Leider sind es gerade Funktionen, die Zeiger oder Referenzen zurückgeben, die häufig Fehlerquellen beinhalten , Sehen Sie sich dazu folgendes Beispiel an : 11 badar ray ,cpp lI i nclu de
,86
Rückgabewerte von Zeiger, Arrays und Referenzen
us ing namespace std ;
i nt main(void) 1 i nt *i pt r - a_ten_array( ) : for( i nt i - O: i < 10 ; i ++ ) 1 iptr[ i ] - i : for{ i nt i - O; i < 10 : i ++ ) cou t « i pt r[ i]« ' \n '; r e t ur n 0:
i nt wa_te n_array( voi d ) 1 in t ar r ay[lO] = I 0 I : re turn a r ray ;
Viele Compiler geben schon eine Warn ung bei der ü bersetzung aus, dass eine Variable mit einer lokalen Adresse zurückgegeben wird. Bei mir macht das Programm folgende Ausgabe:
o 2293424 2
3 1
4469696 6
,
5456754 9
Nicht ganz das gewünschte Ergebnis. Wenn Sie sich noch an die Beschreibung des Stack bei den Funktionen (s iehe Abschnitt 1.10) erinnern können, wissen Sie, dass alle benötigten Daten einer Funktion (Parameter, lokale Variablen. Rücksprungadresse) auf diesem, beim Funktionsaufruf, angelegt werden. Diese Daten bleiben solange erhalten, bis sich die Funktion wieder beendet. Die Funktion "a_ten_arrayO" gibt einen solchen lokalen Speicherbereich zurück - was somit ein ungültiger Speicherbereich ist. Dasselbe Problem tritt auch auf, wenn Sie eine Referenz auf ein lokales Objekt zurückgeben wollen.
18,
I
2,7
I
2
I
Höhere und fortgeschrittene Datentypen
Wenn Sie also etwas von einer Funktion zurückgeben lassen wollen, haben Sie drei Möglichkeiten: ~
Einen statischen Puffer (s tat ; c) (siehe Abschnitt 3.4.3)
..
Einen (mit new) dynamisch reservierte n Speicher vom Heap
..
Einen be im Aufruf der Funktion als Argument übergebenen Puffer
Hier alle drei Möglichk eiten auf das Beispiel _a_ten_arrayO .. bezogen: // I . Speic he r vom Hea p zurOckgeDen i nt* a_ten_arrayl( void ) I i nt * array - new int [1 0J ; retu r n array ;
/I 2 . Ei nen Static -Speicherbereich zu rOckg eDen int * c_ten_a rr ay2( void ) { stat i c i nt a r ray[lO] ; return ar r ay ;
// 3 . Ei nen Puffe r als Argument mitgeben i nt * a_ten_array3( int *Duffer ) I in t ar ray[IOl - 10) ; Duffe r - array ; retu rn buffer : Die Verwendung e ines statischen Puffers funktioniert allerdings ni cht mehr, wenn eine Funktion rekursiv aufgerufen wird. 2.7 ,2
Referenz als Rückgabewert
Auch Referenzen können als Rückgabewert eingesetzt werden. Damit können Sie praktisch e inen Fu nktionsaufruf wie ein Objekt verwenden - also so, als würden Sie direkt auf eine Variable zugreifen. Ha ben Sie zum Beispiel folgenden Prototyp einer Funktion int& test_ referenz( void ) ; so stellt jeder Funktionsaufruf von _tescreferenzO .. ei ne i nt-Variable dar. Somit bedemet ein Aufruf wie ++ tesCreferenz() ; dass eine referenzierte Variab le inkrementiert w ird. Das Hau ptanwendungsgebiet ist hierbei das Überladen von Operatoren - ein Thema, das allerdings erst
,88
Rückgabewerte von Zeiger, Arrays und Referenzen
später im Buch (Abschnitt 4.5) behandelt wird. So eine Operatorüberladung haben Sie mit dem Operator « und cout schon mehrmals verwendet. Zum Beispiel ist der Ausdruck cout
«
"Ein schOner Tag ":
eine Referenz auf das Objekt co ut . was bedeutet. das er selbst wieder das Objekt cou t darstellt. Daher können Sie mit dem folgenden Ausdruck den Operator << erneut anwenden: cout
«
"Ei n schOner Tag "
«
"i st heute\n ":
Aber jede weitere Erk lärung würde den Anfanger jeut überfordern. Dies wird alles erst im Abschni tt zur Oberladung von Operatoren behandelt (Abschnitt 4.5), Hierzu noch ein Beispiel. wo der Funktionsaufruf eine Referenz auf ein i ntObjekt darstellt bzw, wie eine i nt-Variable verwendet werden kann. 11 returnref . cpp lIinelude us i ng namespa ce std :
int& test_referenz( void ) : i nt main(void) [ int* i ptr . wert: 11 Wert von "iwert" an "wert" zuweisen wer t - test_referenz() : 11 " iptr " au f "iwert " zeigen l assen iptr - &test_referenz() : cou t eou t
«
«
"ipt r "wer t
«
«
*ip tr « ' \n ': we r t « "n':
11 .. . entsp ri eht ++ iwe r t
++ test refe renz{) . cou t « "ipt r « *ipt r « ' \n ': " « we r t « ' \ n '; eout « 'we r t 11 iwert ve rdoppeln
tes tJ ef e renz{) - test_re ferenz() ... 2 : cou t « "ipt r cout « "we r t return 0 :
« «
*ipt r « ' \n ': wert « ' \n ';
189
I
2.7
I
2
I
Höhere und fo rtgeschrittene Datentype n
i nt& test_ ref erenz( void ) static i nt i wert - 10: re tu r n iwe r t : Das Programm bei der Ausfuhrung: i ptr wert i pt r wert i ptr wert
10 10 11 10 22 10
2.7.3
const - Zeiger als Rü ckgabewert
Sie haben gesehen, wie man auch Zeiger als Rückgabewene verwenden kann. Wollen Sie verhindern, dass ein Aufrufer mithilfe dieses Zeigers die Daten veränden, so können Sie einen cons t -Zeiger verwenden. Dann kann der adressierle Speicherbereich von der aufrufenden Funktion nur gelesen werden. Die Syntax eines solchen PrololYpS lautet: cons t ty p'" f unct i on (pa r ameter ) ;
2.7.4
Array als Rückgabew ert
Ein Array selbst können Sie nicht als Rückgabewen einer Funktion verwenden. Hier können Sie entweder auf Zeiger zurückgreifen (unter Berücksichtigung des Gühigkeitsbereichs) oder Sie verwenden denselben Trick wie schon bei der Übergabe eines Arrays als Parameter )lby value .. (also Kopie) . Sie packen das Array in eine Struktur (Abschnitt 2.8.1) und geben diese dann so verpackt an den Aufrufer zurück. Das Programmbeispiel dazu: 11 reta rray . cpp lIincl ude using namespace std ;
struct arr ay I i nt wertE)} ; 1; struc t arr ay init_array(voidl : i nt ma i n(voidl ( "t.rur t.
~ r r~y
npw~rr~y
-
init._~rr~y(l ;
f or(int i- O; i < sizeo f (struct arrayl/sizeof(intl ; i++) t cout «newarray .wert[i] « ' \n ':
190
Rückgabewerte von Zeiger, Arrays und Referenzen
re t urn 0:
struc t array init_array{voidJ I struct ar ray arr : f or{int i - O: i < sizeo f {struct arrayl!sizeof{ i nt l : i ++) I ar r .wert[i) - i: return ar r;
2.7.5
Mehrere Rückgabewerte
Es ist standardmäßig nicht möglich, mehrere Werte auf einmal zurückgeben zu lassen . Auch hier können Sie mehrere Werte in ei ner Struktur verpacken un d diese anschließend an den Aufrufer zurückgeben. Aus Effizienzg ründen sollte man allerdings eine Adresse auf diese Stru ktur zurückliefern lassen. Eine weitere Möglichkeit ist die Verwendung von Zeigern bzw. eine Parameterübergabe ,.by reference ... Sie wisse n ja noch, dass sich hierbei eine Veränderung des Werts tatsächlich auch auf das Original bezieht. da mil derselben Adresse gearbeitet wird. Ein Beispiel. worauf das hinausläuft: 11 retmisc.c pp llinclude
int ret_moreC int& a . f loat & b ) ; int main(void) I i nt retl . ret2 : float ret3 : 11 Funktionsaufruf retl - ret_more( ret2 . ret3 I , cou t « -Die Ergebn i sse doe Be rechnungen la ut en : \n ": « retl « · \n ' ; cou t « -Rechn ung 1 « ret2 « · \n ' ; cou t « -Rechnung 2 « ret3 « · \n ' ; cout « -Rechnung 3 return 0:
int re t _more( int& a . f loa t & b ) ( 11 Wichtige r Code hi er . . . bs pw. mehrere Berechnungen a - 10 : b - 11.11 :
191
I
2.7
I
2
I
Höhere und fortgeschrittene Datentypen
retu r n ( a * a); Das Programm bei der Ausführung: Di e Erge bn isse der Be rechnungen lauten Rechnung 1 100 Rechnung 2 : 10 Rechnung 3 11 . 11 In diesem Beispiel erhalten Sie drei ,. Rückgabewerte~ - im Grunde nur einen, aber durch das Nachbilden des "call by reference.. mit Referenzen werden auch die anderen Werte »bearbeitet ...
2.8
Fortgeschrittene Typen
Da Sie jeut alle Basisdatentypen in C++ kennen, ist es an der Zeit, die Sprache um ,.eigene.. Typen zu erweitern. Statt von fortgeschrittene n oder erweiterten Typen könnte man auch von struburierten Ty pen sprechen. Das Prinzip ist ähnlich wie bei den Arrays. Nur dass Sie jetztü, statt einer Zusammenfassung des gleichen Typs, Elemente beliebiger Typen zusammenfassen können. C++ kennt verschiedene solcher fortgeschrittenen Typen. Neben den in diesen Abschn itten beschriebenen Typen von Strukturen (s truct), Un ions (union) und Aufzählungen (enum) sind die Klassen (class) eigentlich das zentraJe Thema dazu in der C++-Programmierung. Und da in C++ die Klassen ein Hauptthema sind, wird darauf gesondert in Kapitel 4 eingegangen . Hier werden zunächst die Strukturen, Unions und Aufzählungen behandelt. 2.8. 1
Strukturen
Mit den Strukturen haben Sie jetzt die Möglichkeit, mehrere Typen zu einem neuen Typ zusammenzufassen . Die Syntax einer solchen Zusammenfassung mit einer Struktur sieht wie fo lgt aus: s t ruct strukturName Typl : Typ2 :
TypN : VariablenBezeichner : Eingeleitet mit dem Schlüsselwort struct . werden all diese Daten eine r Struktur unter dem Namen ,.strukturName .. zusammengefasst. Der Inhalt (auch Strukturmitglieder genan nt) dieser Struktur wird nun in den geschweiften Klammern
192
Fortgeschrittene Typen
zusammengefasst. Am Ende können Sie optional einen Variablenbezeichner dieser Struktur deklarieren, mit dem Sie ansch lie ßend auf die einzelnen Strukturmitglieder zugreifen können. Und wie es bei der Deklaration von Variablen auch schon der Fall ist, massen Sie de n erweiterten Strukturtyp auch mit einem Semikolon abschließen. Strukture n de kla rie re n Hierzu die Deklaration ein er einfachen Zusammenfassung von Artikel (was natürlich e rheblich erweitert werden kann), wie sie bei diversen Programmen zur Verwaltung von Gegenständen verwendet werde n kann. s t ruct artikel 1 int sachn ummer ; cha r beze ichnung[IOO] ; i nt anzah l ; I,
Hiermit haben Sie einen erweite rten Ty p namens »artikel« erstellt, der die Daten - sachnummer« vom Typ i nt , ,.bezeichnung«, ein C-String mit 100 Zeichen, und ,.anzahl« vom Ty p i nt aufn e hmen kan n Nach dem Sie den Strukturtyp definiert haben, können Sie einen neuen Artikel defini eren: s t ruct artikel pulver ; Dam it haben Sie zunächst einen neuen Artikel "pulver« definiert. In Sie im Gegensatz zu C auch das schlüsselwort st ru ct weglassen:
eH könn en
11 in C++ e r laubt - nicht aber in C artikel pu lver :
Dies ist alle rdi ngs wieder eine Stilfrage - ich persönlich bevorzuge das Schlüsselwort struct , wodurch ich gleich weiß, um was es sich handelt. Die obige Anweisung st ruct artikel pulver : hätten Sie mit fo lgender Schreibweise gleich bei der Deklaration erreichen können: s t ruct artikel 1 int sac hnummer ; cha r bezeichnung[IOO] ; int anza hl : pu l ver :
'93
I
2 .8
I
2
I
Höhere und fo rtgeschrittene Datentype n
So könnten Sie auch gleich mehrere Typen auf einmal dekl arieren:
struct artik el r i nt sachnummer ; char bezeichnung[IOO) : i nt anzahl ; pulve r , stahl , rohre ; Hier haben Sie gleich drei erweiterte Variablen vom Ty p ..artikel« deklariert Zugriff auf die Strukturmitglieder
Um j etzt auf die einzelnen Strukturmitglieder zuzugreifen . wird der operator wie folg t verwendet:
Punkte~
Vari abl en Bezei chner , StrukturMi tg l i ed 1m Beispiel des Artikels "pulver« greifen Sie auf die Strukturvariable "sachnummer« wie folgt zu:
pulver,sac hnummer Ansonsten erfolgt die weitere Behandlung und Verwendung wie bei den Basisdatentypen, was das folgende Beispiel demonstriert: 11 structl ,cPP llinclude 'include us ing namespace std :
struct artikel I int sac hnummer : char bezeichnung(IOO] : int anza hl : I,
int main(void) I struct artikel pulve r : pu l ver . sac hnummer - 1234: strncpy ( pulver .bezeichnung , "E i senpulver O,3mm (lkg) ", sizeo f (pulve r.beze i chnung)-1 ) : pulver . anza hl - 10 : cout cout
194
« «
pulver ,sachnummer « ' \n ' : pul ver. bezei chnung « ' \ n' ;
Fortgeschrittene Typen
cout « pulver . anzahl return 0:
«
' \1'1 ';
Das Programm bei der Ausführung : 1234
Ej senpulve r 0. 3mm (Ikg) 10 Strukturen können natürlich ebenso wie die Basisdatentypen direkt bei der Deklaration mit Werten initialisiert werden:
strlJc t artj kel I i nt sac hnummer : char be zejchnung[lOOJ : jnt anzahl ; pulver - I 1234 .
"Ei senpulve r 0.3mm (lkg)" . 10 I,
Oder auch erst bei der Definition der Variablen, zum Beispiel in der ma i n( )Funktion:
struc t art ikel pulver - I 1234,
"Eis enpu lv e r O. 3mm (Ikg)" , 10 I,
Parameterübergabe mit Strukturen an Funktionen Die Parameterübergabe von Strukturen an Funktionen lässt sich im Prinzip genauso wie schon mit den Basisdaten ty pen realisieren. Auch hier steht Ihnen die Möglichkeit zur Verfügung. die Daten per ,.call by value.. als Kopie oder mit »cal1 by refe rence.. als Adresse zu übergeben. Es wurde bereits erwähnt, dass man umfangreichere Typen nicht als Kopie an die Funktion übergeben sollte, um aufwendiges Kopieren auf dem Stack zu vermeiden. Umfangreiche Strukturen kommen zum Teil auf enorme Datenmengen. Somit empfiehlt sich also bei Funktionen das . cal1 by refe rence ... Verfahren. Wenn zum Beispiel folgender Prototyp gegeben ist
void printe cons t str uc t astruct *p ) :
195
I
2.8
I
2
I
Höhere und fortgeschrittene Datentypen
stellt sich die Frage, wie man in der Funktion die einzelnen Elemente der Struktur dereferenzieren kann. Wenn Sie Folgendes schreiben
*p.struk tu rVa r i able würde sich der Compiler beschweren. Der Grund daftir ist, dass der Punkteoperator eine höhere Bindungskraft hat als der Indirektionsoperator. Dieses Problem kann man mit einer Klammerung umgehen . Diese wiederum ha t eine höhere Bindungskraft (siehe auch Anhang des Buchs) als der Punkteoperalor.
(*pl . st ruk t urVariable Solch eine Schreibweise ist allerd ings auch nicht einfach zu lesen und leider auch sehr fehleranfa ll ig. Daher wurde der Operator - ) - in Form eines pfeils oder Zeigers (also recht passend) - eingeführt. Somit sieht ei ne Dereferenzierung hiermit wie folgt aus;
p-)strukt urVa riable In der Praxis sie ht dies so aus: 11 struct2 . Cpp llinclude
struct ast ru ct [ i nt iwe rt ; fl oa t fwert ;
void printe const struct as t ruct *p J ; i nt ma i n(void) [ struc t ast ru ct atest ; atest .i wert - 10 : atesL fwert - 11.11 : printe &atest ) ; re t urn 0;
void print ( const struct astr uct * p ) [ « p-)iwert « ' \n ': cout « "iwert cout « "fwert : " « p-)fwert « ' \n ';
Fortgeschrittene Typen
Das Programm bei der Ausführung:
iwert fwe rt
10 11 , 11
Natürlich lässt sich das auch mit den Referenzen nachbilden. Allerdings muss man dann wieder mit dem PunkteoperalOr auf die einzelnen Elemente zugreifen. und der Funktionsaufruf erfolgt ohne den AdressoperalOr:
pri nt( at est ) ; void print eonst struet astruet& p ) I « p. l wer t « ' \n '; eout « "i wert cout « "fwert : " « p. fwer t « ' \n ':
Rückgabewert von Strukturen aus Funktionen
Wie schon bei der Parameterübergabe von Strukturen an Fun ktionen gilt auch bei der Rückgabe, dass "call by reference« dem "call by value« vorzuziehen ist. um den Aufwand auf dem Stack zu minimieren. Im folgenden Beispiel wird der Speicher fLir die Struktur erst zur Laufzeit in der Funktion angefordert, dann werden die entsprechenden Werte zugewiesen und am Ende dem Aufrufer zurückgegeben. 11 struet3 .epp lIinelude lIinelude <estrlng> us i ng namespaee std :
struc t ast ruet ( Int iwe rt : fl oa t fwert : I,
void print( eonst st ruet astruet* p ) ; struct olst r uct* input( int i . f l aolt f
):
i nt ma i n(void) ( struet astruet * atest : atest - in put t 10 . 11 . 11 ) : print< atest ) : re t urn 0:
197
I
2.8
I
2
I
Höhere und fortgeschrittene Datentypen
void print ( const struct astruct * p ) ( cout « "iwer t « p-) i wert « ' \n ' ; cout « "fwert : • « p-) fwert « ' \n ' :
struc t ast ruc t* in pu t ( i nt i , float f ) r // Spe i che r anf ordern fOr die St ruk tur struct astr uct * p - new (slr uct astruct) ; 11 Werte zu weisen p-)iwert - i : p-)fwert - f 11 Adresse zur Oc kgeben return p; Das Programm bei der Ausführung:
i wert fwert
10 11 . 11
Vergleichen von Strukturen
Ein direkter Vergleich zweier Strukturen ist gewöhnlich nicht möglich, da der Compiler aus Optimierungsgründen die einzelnen Elemente nicht immer direkt aufeinander folgend im Speicher anordnet Da hilft auch kein Vergleich auf niedriger Ebene Byte fur Byte. da auch hier die Ergebnisse durch zufallig gesetzle BiLS in diesen »lücken .. verfa lscht sein könnten. Zwar besitzen viele Compiler einen Schalter, mit dem man die lücken en tfernen kann, aber wie schon herauszuhören ist, ist dies compilerabhängig und somit nich t portabei. Für einen direkten Vergleich von Strukturen werden Sie also nicht darum herumkommen, die einzelnen Elemente einer Struk tur miteinander zu vergleichen. In der Praxis könnte ein solcher Vergleich wie folgt aussehen: 11 struct4 . cpp #i nclude
struct ast r uct ( i nt i wer t ; f loa t fwert : I,
198
Fortgeschrittene Typen
bool cmp_struct ( cons t struct astruct* sI . cons t str uct astruct · s2 ) : in t ma i n(voi dl ( struc t astruct atestl - I 10. 11. 11 I : struc t astruc t ates t 2 - I 10. 12 . 11 I : i f ( (cmp_struct( &atestl. &a test2 )l -- t r ue ) ( cout « "Inha lt i st gleich\n" : else ( cout
«
"In ha lt i st nicht gleich\n ":
return 0;
baal cmp_5truct ( cons t 5truct astr uct ~ 51. const struct as truct ~ 52 ) if ( (sl - >iwert .. - s2 >iwertl && (sl->fwert -- s2->fwertl ) I re turn true : 11 Inhalt bei der Sturkturen gle i ch else I retu rn fa l se : 11 Strukturen si nd nicht gle i ch
Arrays von Strukturen
Natürlich können Sie auch ganze Arrays von Strukturen verwenden. Arrays sind schließlich nicht nur auf die Basistypen beschränkt und lassen sich auch ohne Schwierigkeit mit den fortgeschritlenen Typen wie den Strukturen oder Klassen kombinieren: s t ruct astr uct int iwert : f loat fwe r t : I,
Wollen Sie jeut ein Array aus zehn Elementen definieren, gehen Sie fo lgendermaßen vor: struc t astruct s tr array [lO] : Hiermit häuen Sie ein Array mit zehn Elementen definiert. wo jedes Element aus der Struktur ,.astruct« besteht. Werte können Sie den einzelnen Elementen so zuweisen :
'99
I
2.8
I
2
I
Höhere und fo rtgeschrittene Datentypen
i nt i - 0: II Ele men te
mi t de m Index 0 stra rr ay[i) . i wert - 10 : strar ray[i].fwert - 11. 11 : II nac hst es Elemen t i++:
Natürlich können Sie solch ein Array auch direkt bei der Deklaration mit Werten initialisieren. Dies geschieht ähnlich wie bei den mehrdimensionalen Arrays:
s t ruct astr uct strarray[3) 10 . 11.11 I .
~
I
11 . 12 . 12 I . 12 . 13 . 13
Das folgende Beispiel demonstriert die Verwendung der Arrays von Strukturen in der Praxis. Dabei wird eine einfache Geburtstagsverwaltu ng mit folgender Struktur erstellt:
s t ruct ge bu rtstage cha r name[50) : int ta g : i nt mo nat : i nt ja hr; }
,
Im Prog ramm wird ein Array deklariert. mil dem Sie zehn Geburtstagsdaten speichern können:
struc t geb urtstage geb[OATA) : Im Grunde ein einfaches Listing ohne besondere Extras: 11 st ruct5 .CPP l/include us i ng names pa ce std ;
st ruct ge bur tstage char name [50) : i nt t ag; i nt mo nat ; i nt ja h r; }
,
const i nt DATA - 10 :
200
Fortgeschrittene Typen
void input( int& zaehler , struct geburtstage* b ) ; void output{ int zaehler , struct geburtstage* b ) ; int ma i n(void) ( struct geburtstage geb[DATA) : i nt zaehler - 0 , wahl :
do I cout « " \nGeburtstage - verwaltung\n" : cout « " -- --- --- ---- ------- --- \n "; cout « " . 1 . Eingeben\n" ; cout « "-2- Ausgeben\n" ; cout « " - 3- Ende\n" : " cout « "I hre Auswahl bi tte ein» wahl ; switch( wahl) ( case 1 : i nput( zaehler . geb ) : break ; case 2 : output( zaehler . geb ) ; break ; ca se 3 : break ; defaul t : cout « "Falsche Eingaben!\n" : while( wahl return 0:
!~
3 );
void input( int& zaehler , struct geburtstage* b ) ( " . cout « "Name ein» b[zaehler) . name ; " . cout « "Tag e i n » b[zaehlerJ .t ag ; cout « "Monat : "; e i n » b[zaehlerJ . mona t: " . COlJt « "Ja hr c i n » b[zaehler) . j~h r; // Wichtig!!! Index e r hOhen!!! zaehler++:
void output( int zaehler . struct geburtstage b ) ( fore int i - 0 : i < zaehler : i++ ) I cout« b(il . name « «b[il.tag « ' .'
20'
I
2.8
I
2
I
Höhere und fortgeschr ittene Datentypen
« b[i] .monat « «b[i] . jah r
«
' \n ';
cout « "\ n\n "; Das Programm bei der Ausführung:
Geburtsta ge -Verwaltung - [ -
Ein gebe n
- 2- Ausgebe n -l- Ende
Ihre Auswa hl bi tte Zapf Name Tag
Monat Jahr
1
11 11 1988
Geburtstage -Verwaltung - 1- Eingebe n - 2- Ausgebe n - 3 - Ende Ihre Auswahl bitte
2
Wolf - 12 . 11 . 1974 Zapf - 11 . 11 . 1988
Auch wenn sich die Verwendung von Arrays kombiniert mit Strukmren ganz toll anh ören mag, so wird in der Praxis doch eher auf dynamische Datenstrukmren zurückgegriffen. Denn bei den Arrays von Strukturen ist irgendwann Schluss. Man kan n zwar den Indexwert ausreichend groß dimensionieren, aber man sollte bedenken, dass dieser Speicherplatz vom Programm verwendet wird. Bei umfangreichen Strukturen mit ein paar hundert Elementen sind schnell mal ein paar Megabytes für ein einfaches Programm verbrauch t. will man die Daten dann auch noch sortieren ode r einige Daten löschen (und die Lücken rullen), wird aufwändiges llin - und llerkopieren mit einem temporären Speicher nötig. was einen weiteren Performanceverlust zur Folge hat.
Solche Probleme können Sie zum Beispiel durch verkettete Listen minimieren. Hier wird Speicherplatz auf Anfrage vom Heap erzeugt. und die Date n werden gleich richtig einsortiert. Gelöschte Daten werden ohne großen Aufwand »ausgehängt« und zerstört.
Fortgeschrittene Typen
Strukturen in Strukturen
Natürlich lassen sich auch Strukturen innerhalb von Strukturen verwen den - es wird hierbei auch von »Nested Struetures« gesprochen. Dies lässt sich recht gut an unserem Beispiel der »Geburtstagsverwahung .. demonstrieren. Nehmen wir an, Sie haben diese Struktur ein wenig erweitert:
struct gebu r tstage char name[50] : char vname(50) : char email[IO O) : char wohno r t[lOO) : 11
usw
i nt t ag : i nt monat : i nt jahr : I:
Bei umfangreichen Strukturen macht es häufig Sinn, einzelne Daten in gesonder~ ten Strukturen abzu legen, um das Program m wieder lesbarer zu machen. Das Beispiel »Geburtstage .. könnte man demnach wie folgt zerlegen (mit denselben Daten):
struct i nt i nt i nt
datum I tag : mon at : jah r:
I:
struct person I char name[ 50] : char vname[50) : char email [100] : char wohno r t[lOO) : 11 .. _ usw
struct gebu r tst aQe { st ruct datum geb_datum : st ruct person geb_person : Eine Variable können Sie jetzt folgendermaßen erzeugen:
struc t geb urt stage geb ;
203
I
2.8
I
2
I
Höhere und fo rtgeschrittene Datentype n
Das Ganze lässt sich natürli ch auch als Array verwenden. Der Zugriff auf die einzelnen Elemen te funk tioniert j etzt etwas anders. Wollen Sie zum Beispiel den Namen der Struktur "person .. eintragen. so müssen Sie folgendermaße n vorgehen:
s t rncpy{ geb.geb_person . name. "Wolf " . 50 ) ; Oder das Geburtsjahr lässt sich so zuweisen: geb .ge b_datu~.jahr
- 197 4;
Natürlich lässt sich auch wieder eine direkte Zuweisung bei der Definition durchfUhren:
st ruct geburtstage ge b - [ 12 . 11. 1974 J . [ "Wol f" . "JOrgen" . "email@email .de " . "/ home/mer i ng" I In der Praxis wird man so etwas dynamisch mit einem Zeiger machen wollen, dafür muss man auf den Pfeiloperator zugreifen:
Speiche r vom Heap an fordern struct geb ur tstage *geb - new (struc t geb urtstage) ;
11
strncpy{ geb ->geb_person . name . "Wolf ". 50) : geb->ge b_da t um. jahr
~
1974 :
Einen wei teren Vorteil der ,.Verfeinerung" von Strukturen besteht darin, dass Sie die einzelnen Strukturen jederzeit auch in ganz anderen Strukturen oder fO r sich alleine verwenden können - deshalb erzeugen Sie ja einen fongeschriuen en Typ. So können Sie jederzeit im Programm die Struktur _datum " fOr sich verwenden:
st ruct datum d - I 12 . 11 . 1974 J; Aber auch die mehrfache Verwendung einer Struktur in einer Struktur ist möglich und oft recht sinnvoll:
st ruct personal st ru ct datum e1 nstel lungs_da t um;
st ru ct datum ge burt s_datum ; st r uct person pe rson_da te n; I,
Auch hier gilt natürlich, wie schon am Ende von Arrays und Strukturen erwähnt, dass man in der Praxis eine verkettete Liste einsetzen sollte, wenn man mehrere Daten erfassen will . Hierzu ist eigentlich nicht mehr viel nötig - es bedarf nur noch eines Zeigers auf die Adresse einer Struktur. die demselben Typ entspricht:
204
Fortgeschrittene Typen
I
2.8
struct persona l struct datum e instellungs_datum ; struct datum geburts_datum : struct person person_daten ; str uct personal ""next; I,
Hier haben Sie praktisch ein en Zeiger vom Typ "struct personal.. definiert. der auf ein Objekt vom selben Typ zeigen kann. [n der Praxis ist dies das nächste Element in der Kette. Man hängt hiermit die Daten aneinander wie bei einer Perlenkette. Verkettete Listen werden in C++ gewöhnlich mit dem OOP-Ansatz erstellt (siehe auch Abschni u 4.8.9). Dennoch gehören sie zu den grundlegenden Dingen, die ein Programmierer zumindest kennen sollte, weil man dadu rch wi rkliche Optimierungen im Hinblick auf Geschwindigkeit. Speicherverbrauch u.s.w. durchfUhren kann. Daher finde n Sie hier einen kleinen Exkurs darüber, was zu den Grundlagen einer verketteten Liste gehört. Hinweis Detailliert und umfangreicher wird auf die verketteten listen im Buch »C von Abis Z" eingegangen . Sie finden dieses Buch als openbook auf der Bu ch-CD. Zwar wird hierbei als Sprache C verwendet, aber bezuglich der Implementierung gibt es keine Unterschiede. In C werden lediglich andere Fu nktionen fUr die Speicherverwaltung (ma 11 oc { ) und Co.) und für die Ein- bzw. Ausgabe verwendet. Exkurs: Verkettete Listen
[m Gegensatz zu den Arrays gehären verketteten Listen zu den dy namische n Datenstrukturen, die eine Speicherung von einer unbestimm ten Anzahl zusammengesetzter Datentypen erlauben. Bei den Arrays müssen die einzelnen Speicherzeil en im Speicher immer der Reihe nach abgelegt sein . Nich t so bei den ver· ketteten Listen, hier sind die Speicherorte absolut referenzierl.
Verkettete Listen oder Armys Häufig wird gefragt, was man denn nun verwenden soll . Dies hängt vom Anwendungsfall ab und kann nicht pauschal beantwon et werden. Der Voneil von verketteten Listen besteht darin. dass man sich Keine Gedanken machen muss, wie viele Elemente maximal gespeichert werden. Man erzeugt einfa ch ein El ement und fügt es in der Liste ein. Benötigt man es nicht mehr, kann man es wieder löschen. Die Anzahl der möglichen Elemente ist nur durch den verfügbaren Speicher beschrän kt. Alle rdings ist die Zugriffszeit auf die Elemente an einer bestimm ten Position umfangreicher als in einem Array (sofern es sich n icht um das erste Element handelt).
205
[«l
I
2
I
Höhere und fortgeschrittene Datentypen
Die Daten (der Knoten) Als Basis ftir die verkettete Liste wird ein Knoten (engl. node) verwendet. Dieser Knoten wird als Element in der Liste bezeichnet, der die Daten und einen Zeiger auf seinen Nachfolger enthält. Ein so lcher Knoten wird als einfache Struktur realisiert:
st ru ct Knoten I int daten ; Knoten* ne xt ; I,
Natürlich kann ein solcher Knoten mehr Daten, als hier mit »daten« verwendet, enthalten. Für den AnHinger ist die Verwendung des Typs »Knoten« innerhalb der Deklaration der Struktur »Knoten « ein wenig verwirrend. Hierbei wird lediglich ein Zeiger auf ein Knoten-Element definiert. Und da ein Zeiger immer dieselbe Speichergröße hat (egal von welchem Typ der Zeiger ist). kann es auch fur den Compiler irrelevant sein, wie der Typ ,.Knoten« genau aussieht. Anhand des Bezeichners vom Zeiger lässt sich schon erkennen. dass dieser ftir das Nachfolgerelement bestimmt ist.
Der Anfang der Liste (der Anker) Ein weiteres Element, das man bei einer verketteten Liste benötigt, ist ein Anfang der Liste, was häufig auch als Anker (eng!. anchor) oder als Start bezeichnet wird. Über diesen Anker erfolgt der Zugriff auf das erste Element im Knoten. Nach dem Zugriff auf das erste Element der Liste werden die weiteren Elemente mit dem Zeiger »next" (ein Verweis auf den nächsten Listenknoten) erreicht. Das Ende der Liste Der letzte Knoten in der Liste zeigt auf einen Nul!wert. Im Beispiel kann man sich so lange von einem Element zum nächsten hangeln, bis ,.next " 0 ist. Mit 0 zeigen Sie das Ende der Liste an . Nach dem Starten des Programms ist die Liste gewöhnlich auch leer, dah er muss zu Beginn auch der Anfang der Liste (der Anker) 0 enthalten. Eine verkettete Liste sieht also etwa so aus, wie es in der folgenden Abbildung schematisch dargestellt ist:
A",,,
--1 I. I ·1 1·1 ·1 I+--o
Abbildung 2.11
,o6
K"",
K'O'"
",0'"
Schematische Darstellung einer einfach verketteten Liste
Fortgeschrittene Typen
Ein einfa ches Beispiel
Das fo lgende listing zeigt ein einfaches Beispiel einer solchen verketteten liste. Hierbei wurden lediglich die Funktionen zum Einfügen von Elementen am Ende, das Anzeigen aller Elemente und das Löschen einzelner Elemente implementiert: 11 list .cpp lIinclude
struc t Knoten I i nt daten ; Knoten* next ; I,
Anfang der Liste Knoten* Anfang - 0: 11
11 Funktionsprototypen Knoten* insertKnoten( int& val) : void showKnoten( const Knoten* n ): Knoten· deleteKnoten( int dat ) :
int main(void) I Kno ten* nod e; int auswahl, ival ; do I
cou t « "Eine e infache verkettete Liste\n" ; - - - - - -- - - - - - - - - -- -------- \1'1 "; cout « · cout « · 1 . Neues Element hi nzu f agen\n" ; cout « · 2· A11 e Elemente ausgeben\n" ; cout « " ·3· Einzelnes Element löschen\n " ; Programm beenden\n\n " : cout « cout « "Ihre Auswah l cin » auswahl ; switch( auswahl) I case 1 : cout « "D~ ten e i ngeben : ": ein» ival : node - insertKnoten( ival ) : break : ca se 2: showKnoten( node ) : break : case 3: cout « 'Wert zum Löschen eingeben : " .
·
· ·
·..-
207
I
2.8
I
2
I
Höhere und fo rtgeschrittene Datentypen
e i n» ival ; node - deleteKnoten( i val ) ; break ; case 4; break ; default : cou t « "Falsc he MenOauswahl?\n "; wh i l e( auswahl !- 4) ; return 0: 11 Funktion zum EinfOgen neuer Elemente
Knoten" i nsertKno ten( i nt& val) I 11 Ist noc h kein Element in der Liste , 11 dann f Ogen wir das erste am Anfang ein if( Anfang - 0 ) I Kno t en ' node - new Kno t en ; nOde ->daten - val ; nOde ->next - 0; Anfang - node ; return Anfang ; Es sind be r eits Eleme nte i n der Li ste . dann soll das neue hinten angehan gt werden el se I Knoten node - An f ang ; Knoten " newNode ; while( node ->next ! - 0 node- nod e->ne xt ; newNode - new Knoten ; newNode -)daten - val ; newNode -) next - 0: node ->ne xt - newNode : return Anfang ; 11 11
T
11 Alle Elemente der Liste anze i gen void show Kn oten( cons t Knoten" n ) I i f ( Anfang - - 0 ) I cou t « "Die Li ste ist l eer\n ";
else { cout
20B
«
"I . Element : "
«
n->daten
« . \ n';
Fo rtgeschrittene Typen
fore i nt i - 2; n-)nex t 1- 0: i++ ) I n=n-)ne xt : cout « i « Element : " « n-)daten
« ' \n ':
11 Das erste El ement mi t dem Wert dat aus der Lis t e l öschen
Knoten * dele t eK noten( i nt da t ) I if ( An fang -- 0 ) I cou t « "D i e Liste ist lee r\n " ; I
11 1st das ers t e Elemen t das von uns gesuchte? if(
Anfang -)daten - da t ) Kno t en * deI - Anfang : i f( Anfang-)nex t ! ~ 0 Anfang - Anfang-)ne xt : dele te deI :
Die kom plette Liste nac h dem gesuchten 11 Element durchlaufen el se I Knoten * node - Anfang : wh i le( nOde-)next 1- 0 && nOde-)nex t -)daten 1- dat ) nOde-node -)next : i f( nOde -) next -- 0 cout « "Elemen t zum Lösc hen kommt nic ht" « in der Liste vor!\n" : el se I 11 das zu lösc hende Element an deI zuweise n Knoten * deI " node-)next : 11 Einen Hilfszeiger hi nte r da s zu lösc hende Element Knoten * help .. del-)next : 11 das zu lösc hende Element "aushangen" node -)nexl - help : delete deI : 11
retu r n Anfang : Das Programm bei der Ausführung:
Ei ne einfache verke t te t e Liste
209
I
2 .8
I
2
I
Höhere und fortgeschr ittene Datentypen
- }- 2-3-4-
Neues Element hinz ufOgen Alle El emente ausge ben Einzelnes Element löschen Programm beenden
Ihre Auswahl Anhand des Listings lassen sich all erdings auch die Nachteile ei ner solchen verketteten Liste erkennen. Der Aufwand, den man betreiben muss, um nach Daten zu suchen. Knoten einzufügen oder zu löschen, die Liste zu sortieren eIe. , ist sehr groß, da aber jedes Element gegangen werden muss. Natürlich hat man auf der anderen Seite immer den Vorteil , dass verkettete Listen einen sehr geringen Speicher bedarf haben . Das Einfügen am Anfang der Liste hingegen geht wieder relativ schnell. Verwendet man einen weiteren Zeiger fur das Ende der Liste, de r immer auf das letzte Elemen t zeigt, können die Daten auch sehr schnell an das Ende der Liste angehängt werden . Natürlich benötigt die Liste dadurch wieder etwas mehr Speicherplatz.
DoppeJt verkettete Listen Bei den doppelt verkelteten Listen hat jedes Element der Liste nicht nur einen Zeiger auf das noch folgen de, sondern auch einen zusätzlichen zweiten Zeiger auf das Vorgänger-Element. KnOlen KOlOlen Knolen
Abbitdung 2.12 Schematische Darstellung einer doppelt verketteten Liste
Auf unser Beispiel bezogen sieht die Struktur des Kno tens fo lgendermaßen aus: struct Knoten I int daten : Knoten " next : 11 Zeiger auf den Nachfolge r Knoten" previous : 11 zeiger au f den Vorg3nger I,
210
Fortgeschrittene Typen
I
2 .8
Gewöhnlich zeigt der Vorgängeneiger des ersten ElementS auf ein Dummy-Element, ebenso wieder der Nachfo lgerzeiger auf das letzte Element. Diese beiden Dummy-Elemente werden verwendet zum Auffinde n von Anfang und Ende der doppelt verketteten Liste. Der Vorteil einer doppelt ve rketteten Liste ist, dass jetzt die Elemente auch von hinten nach vorne durchlaufen (iteriert) werden können, Ist die Liste sortiert, dann können die Elemente in der zweiten Hälfte noch schneller gefunden werden. Natürlich benötigt die doppelt verkettete Liste mehr Speicherplatz, da ein zusätzlicher Zeiger verwendet wird . Hinweis Den OOP-Ansatz, der das Prinzip der Kapselung verwendet fin den Sie in Abschnitt 4.8.9. Des Weiteren wird auch auf die von STL angebot ene (objektori entierte) Schnittstelle zu den verketteten Listen eingegangen (Abschnitt 5.3.5).
Bitfelder (gepackte Strukturen) Bitfelder sind nichts anderes als gepackte Strukturen. Ziel ist. dass eine Struktur so wenig Speicherplatz wie nötig verwendet. Beispielsweise fo lgende Struktur: strlJc t data tinS i gned unsigned uns i gned uns i gned uns i gned J,
i nt i nt i nt i nt i nt
1 i nks : rechts : vo r: zu r uec k: s t at us :
Jedes einzelne Strukw relement dieser Struktur belegt auf eine m 32-Bit-Rechner gewöhnlich vier Bytes. Die komplette Struktur mit allen fün f Elementen benötigt somit 20 Bytes an Speicherplatz. Die Strukwr soll einen Roboter im Raum steuern. Bei der Richtungssteuerung benötigt man lediglich die Werte 1 und O. 1 ftir »Rich tung aktiv..- und 0 fü r »nicht aktiv«_Außerdem soll noch ein Wert für den Status verwendet werden, der einen Fehlercode (sagen wir von 0- 16) zurückgibe dessen Bedeutung jetzt allerdi ngs von sekun där ist. Hier bietet Ih nen e++ die Möglichkei t, die einzelnen Strukturelemente mi t Bitarrays zu verwen den. Dazu müssen Sie lediglich hinter den Strukturelementen einen Doppelpunkt einfugen , gefolgt von der Anzahl der Bits , die das El ement verwen den soll: struct da ta unsig ned i nt l i nks un signed int rec hts
11;
[« ]
I
2
I
Höhere und fortgeschrittene Datentypen
unsigne d i nt
1· 1·
4,
I,
Hiermit fordern Sie den Compiler auf, für die Rich tungen "links«, "rechts«, »vo r« und "zu rück« jeweils ein Bit zu verwenden, und für das Element "status« vier Bits. Dies wären insgesamt acht Bits und somit ein Byte. Gewöhnlich werden fUr diese Struktur aber dennoch vier By tes verwendet, da, wie bereits erwähn t. der Compiler aus Optim ierungsgründen ein bestimmtes »Alignment« (bei 32~Bit~Rechnern meistens ein Vie r-Byte~Alignment) verwendet. Nicht verwen deter Speicher (hier die drei Bytes) wird auch hier wieder "aufgefüllt« und bleibt ungenutzt. Es wird auch vom "Padding .. (Aufftillen, Polsterung) des Speichers gesprochen. Viele Compiler besitzen daher einen speziellen Schalter, mit dem diese Lücke entfernt werden kann. Mit dem Schalter
attrl but können dem Compiler mehrere Informationen zu einer Funktion, zu Variablen oder Datentypen übergeben werden. Um damit eine lückenlose Speich erbelegu ng zu erreichen, könnten Sie das Attribut pac ked verwenden:
struct da ta unsig ned i nt li nks :}; unsig ned i nt rechts :! : uns i gned i nt vor : 1 : unsigned i nt zurueck : l : unsigned int status :4 : I__ att r ib ute __ ({packed» ; Sollte dieser Schalter bei Ihrem Compiler nicht funktionie ren, können Sie auch das Pragma pack verwenden:
IIpragma pack(n) Fü r Tl kalm hier der Wen 1, 2, 4, 8 oder 16 arr~e~ebe n werden. Je nachdeuI, welche Angabe Sie dabei machen, wird jedes Strukturelement nach dem ersten kleineren Elementtyp oder auf n Byte abgespeichert. Allerdings entferne ich mich jetzt ziemlich vom Thema, da dies compiler-spezifisch ist und kein C++-Standard. Ebenso ist nicht garantiert, dass die gepackte Strukmr auch tatsächlich vom Compiler gepackt wird, wie hier beschrieben. Der C++-Standard schreibt nämlich
Fortgeschrittene Typen
I
2.8
nicht vor. wie die gepackten Strukturen gepackt werden sollen. Es kann somit auch sein , dass Ihr Compiler die gepackte Struktur genauso behandelt, wie eine ungepackte und alles so belässt. wie es ist. Außerdem sollte man nur gepackte Strukturen verwenden, wenn Speicherplatz knapp ist. da dies di e Effizienz der Program mausführun g beeinträchtigen kann . Hinweis Gepackte Strukturen sind nur mit dem Variablen ; nt und enum erlaubt und funktionieren nicht mit anderen Basis- oder komplexeren Typen.
2.8.2
Unions
Neben den Strukturen können Sie auch mit Unions, auch Variante genannt, Daten unterschiedlichen Typs strukturieren. Abgesehen von einem anderen Schlüsselwort (union) besteht zwischen Unions und Strukturen zunächst kein allzu großer Unterschied. Auch die Zuweisung und der Zugriff erfolgt wie bei den Strukturen. Erst beim Umgang mit dem Speicherplau wi rd der Unterschied deutlich, wie das Beispiel zeigt struct da ta 1 int iwert; fl oa t fwert : Auf einem 32-Bit-Rechner belegt diese Stru ktur gewöhnlich acht Bytes (i nt (vier Bytes) + f l oat (vier Bytes) = acht Bytes). Seuen Sie nun das Schlüsselwort union davor: union data I int i we rt : float fwert : Nun belegt diese Struktur nur noch vier Bytes an Speicher. Durch das Schlüsselwort uni on wird Speicherplatz gespart, indem manjeut immer nur auf eines der Elemente in der Union zugreifen kann: union data test ; test . iwe r t - 10 ; Hier haben Sie zum Beispiel dem Strukturelement »iwert_ den Wert 10 zugewiesen. Würden Sie anschließend einen Wert von »fwert" zuweisen union data test ; test.iwe r t - 10 ; tes t. f wert - 11 . 11 ;
11 Fehler!!!
[« )
I
2
I
Höhere und fortgeschrittene Datentypen
ist dies unzulässig. Zwar würde das Programm zunächst anstandslos laufen, aber wenn Sie jetzt zum Beispiel eine Wertzuweisung vornehmen würden wie
SO
union data test : i nt i: test . iwe r t - 10 : test . fwe r t - 11.11 ; i - test . iwert :
JJ Feh l er!!! JJ undef ini ert
so wäre das weitere Verhalten undeflniert. das heißt es ist nicht vorauszusagen . was passiert. Anders herum können Sie auch nicht mehr auf ,.iwert .. zugreifen, wenn Sie "fwert .. bereits einen Wert zugewiesen haben. Eine Union lässt sich hervorragend einsetzen. um Daten zu organisieren. Besonders wenn man mehrere umfangreiche Strukturen hat. von denen man weiß (oder will), dass diese niemals gemeinsam verwe ndet werden . Haben Sie zum Beispiel zwei Strukturen mit Adressdaten definiert, wovon eine Struktur für den privaten und die andere für den geschäftlichen Bereich verwendet wird, können Sie eine Union wie folgt verwenden: un i on adressen 1 st ruc t daten pr i vat : st ruc t daten beru f;
Hiermit läSSt sich das Berufliche vom Privaten trennen. Wenn Sie eine Union mit gleichen Datentypen definieren , ist bei folgender Verwendung immer standardmäßig das erste Element der Initialisierer: union da ta i nt a : int b : int c ;
11 In iti ali s i e r t das Struktu re l ement a union data t es t - I 10 I :
Hier wird automatisch das Stru kturelement ,.a.. initialisiert. Eine interessante Mixtur lässt sich mit den Unions und Arrays erstellen. Wenn Sie ein Array von Unions erstellen, müssen Sie keineswegs für die weiteren Elemente den Typ verwenden, den Sie für das erste Element verwendet haben. So können Sie für jedes Array-Element wieder ein anderes Strukturelement verwenden. Ein Beispiel dazu:
Fortgeschrittene Typen
11 unionl , Cp p
lIinclude (iostream) us i ng namespace s t d : un i on data ! cha r ewe r t ; i nt i wer t ; float fwert: I,
const i nt C const i nt I const ; nt F
--
0, I .
-"
i nt ma i n(voidl I
union da ta t es t [3] : t est[C] , cwer t - ' A ': test[I] .i wert - 10 ; test[F) . fwert - 11 . 11 ; co ut
« « «
tes t [C] . cwert tes t [I] . i wert tes t [F] . f wert ret ur n 0:
« « «
' \t ' ' \t '
' \n ';
Auch hier entsteht zunächst wieder der Eindruck, dass diese Methode, Daten zu organisieren, prak tisch ist. Das ist im Grunde auch zutreffend, aber in C++ verwendet man hierfür in der Praxis die Basis- und abgeleiteten Klassen.
2.8.3
Aufz ählungstypen
en um(Aufzählungstypen) können Sie als Alternative fur cons t verwenden. Diese Aufzählungstypen sind dazu gedacht. dass nur eine begrenzte Anzahl von Werten aufgenommen wird. Wenn Sie zum Beispiel konstante Werte für einen Monat verwenden , können Sie dies statt mit cons t const ; nt JAN - 0, const ; nt FEB - 1; const ; nt HA R - 1
,
const i nt DEC - 11 :
i nt monat - FE B;
"5
I
2,8
I
2
I
Höhere und fortgeschrittene Datentypen
einfacher und kürzer mit enum machen: enum mona t 1 JAN , FES. MAR , APR , MAI , JU N, JUL , AUG , SEP , OKT , NOV, DEC }
,
enum monat ak tuell - FES ; Somit gilt für eine enum-Anweisung folgende Syntax : enum name ( namel . name2 .
, nameN I bezei chner ;
Die Namen zwischen den geschweiften Klammern von enurn werden auch als Tag bezeichnet und wird gewöhnlich in Großbuchstaben geschrieben. Der Bezeichner ist auch hier w ieder optionaL Der erste Tag bekommt immer standard mäßig den Wert 0 zugewiesen. Die weileren Werte werden j eweils um eins inkremenllert. Dies kann aber auch geändert werden: enum monat 1 JAN- l. FES . MAR . APR . MAI. JUN . JUL . AUG . $EP . OKT . NOV . DEC }
Hier haben Sie den Tag "JAN ~ mi t 1 vorbelegt. Somi t erhöhen sich die weiteren Werte bis "DEC .. (=12) um eins. Natürlich können Sie auch mittendrin einen Tag verändern: enum far be WEISS. ROT. GRUEN . GELB-lO . BLAU , SCHWARZ }
Angefangen wird bei der Farbe _WE ISS" wie gewöhnlich mit O. Weiter wird inkrementiert bis ,.G ELB" - das hier den Wert 10 e rhält. Somit werden die weiteren Tags dahinter wiederum wie gewöhnlich mit eins in krementiert, sodass »BLAU« den Wert 11 und »SC HWARZ" den Wert 12 besitzen. Natürlich können Sie auch alle Tags mit einem Wert vorbelegen. 2.8.4
typedef
Mit dem Sch lüsselwort typedef kann ein neuer Bezeichner für einen Datentyp verwendet werden. Die Syntax einer einfachen Typendeflnition sieht so aus: typedef Ty pendefinition Bezeichner ; Dam it lässt sich die Lesbarkei t eines Program ms erheb lich verbessern . Natü rlich erzeugt man mit typedef keinen neuen Typ, sondern nur ein Synonym (Abkürzung) dafür.
Fortgeschrittene Typen
Wenn Sie zum Beispiel nicht immer .. unsigned int« im Programm eintippen wollen, brauchen Sie nur folgende Typendefinition vorzunehmen: typedef unsigned int uint : Jetzt können Sie im Programm statt unsigned int i wer tl. i wert2 . iwert3 : Folgendes schreiben uint iwertl , i wert2 , i wert3 : Das schlüsselwort typede f wird ebenfalls dazu benutzt. sogenannte primitive Datentypen zu erzeugen. Wozu soll das gut sein? Nehmen wir als Beispiel den primitiven Datentyp »ichbinprimitiv_t« der folgendermaßen definiert ist: t ypedef 10ng i chbinp r imitiv_t: Auf einem anderen System kann diese primitive Variable wie fo lgt definiert sein: typedef uns i gned int ichb i nprimitiv_t : Die primitiven Daten typen machen ein Programm somit portabler. Dadurch müssen Sie sich nicht mit den Datentypen bei Portierung auf andere Systeme auseinandersetzen. Wenn Sie ein Programm beispielsweise auf einem 32-Bit-System programmiert haben und dies anschließend auf einem 16·Bit-System getestet wird, ka nn die Suche nach dem Feh ler einer fa lschen Werteausgabe frustrierend und zei taufwendig sein.
217
I
2.8
I
Das Kapitel behandelt Themen, die zwar recht trocken und sehr theoretisch, aber für die Praxis unverzichtbar sind. In den ersten beiden Kapiteln haben Sie viel über verschiedene Datentypen (Basistypen, Zeiger, Referenzen, strukturierte Typen etc.) und Funktionen eifahren. In diesem Zusammenhang wurden aber noch einige wichtige Aspekte vernachliissigt, und zwar die Gü/tigkeitsbereiche, Namensräume und die Sichtbarkeit. Auch die Deklarationen wurden nur recht einfach erklärt. Hierbei gibt es aber noch besondere Speicherklassenattribute, Typenqua lifikatoren und Funktionsattribute. Und dann fehlen noch die Typum wandlungen, bei denen man zwischen einer Standard-Umwandlung und einer expliziten Umwandlung unterscheidet.
3
Gültigkeitsbereiche. spezielle Deklarationen und Typumwandlungen
3.1
Gültigkeitsbereiche (Scapel
Zunächs t solhe man nicht den Fehler machen, den Begriff »GühigkeilSbereich« mit dem Begriff ,.Sichtbarkeit« gleichzusetzen. Ein Speicherobjekt ist sichtbar, wenn man innerhalb eines Bereichs darauf zugreifen kann. Damit ein solches Objekt sichtbar ist. muss es güllig sein. Sie können zum Beispiel ein Objekt ,.ungültig« machen, indem Sie es mit einem Objekt mil gleichem Namen überdecken: 11 cU . cpp llinclude
int main(void) \ II iwert ist hier ·Sichtbarint iwert - 100 ; // iwert Oberdec kt das außere iwert i nt i wer t - 200 ; (out « iwe rt « ' \n ';
219
I
3
I
Gottlgkeltsbere lche, spezielle Deklarat io ne n und Typumwandlungen
// hier i st i nne res iwert nicht meh r gOltig // und somit ist :!uße res i wert wieder Sicht bar cout« i wert « ' \n '; return 0: Das Programm bei der Ausführung: 200 100
Jetzt aber zum eigentlichen Thema, den Gültigkei tsbereichen in C++. Der Gültigkeitsbereich ist ein Abschnitt im Quellcode, wo ein deklarierter Bezeichner verwendet werden kann. Häufig wird auch von einem ..Scope .. (=G ültigkeitsbereich des Bezeichners) gesprochen.
3.1 .1
Lokaler Gültigkeitsbereich (Local Scope)
Wenn Sie einen Anweisungsblock verwenden, haben Sie automatisch einen lokalen Gülligkeitsbereich (Local Scope) eingeführt. In einem lokalen Gültigkeitsbereich gelten die don deklarierten Bezeichner immer nur im Anweisungsblock (zwischen den geschweift en Klammern). Hierzu gehören alle Arten von Kontrollstrukturen wie i f, for, swi tch und whi 1e . die auch als ... Substatement Scope« bezeichnet werden. Wenn mehrere gleichnamige Variablen vorhanden si nd , erhält immer die lokal sie Variable den Zuschlag. Eine Ausnahme bildel die Ver- aber dazu in wendung des Scope-Operators (oder auch Bereichsoperator) Kürze mehr. int i wer t - 100 ; I
11 Verwendet :!uBeres i wert cout « iwe r t « ' \n' : int i wert - 200 : 11 Verwendet i nneres i wert cout « iwe r t « ' \n ':
3.1.2
Gültigkeitsbereich Funktionen
Der Gültigkeitsbereich eines Funktionsprototypen gilt bis zum Ende der Deklaration (mitsamt Funktionsparameter), und der GültigkeitSbereich der Funktionsdefinition erstreckt sich über die gesamte Funktion (also dem Anweisungsblock der Funktion). Ansonsten gilt auch hier, dass bei der Verwendung einer globalen
220
Gültigkeitsbereiche (Scopel
und einer lokalen Variable mit demselben Namen. die lokalste Variable den Zuschlag erhält. Eine Ausnahme gibt es jedoch wieder, wenn Sie den Scope-Operator : : verwenden. Mil dem Scope-Operator haben Sie die Möglichkeit auf den globalen Gültigkeitsbereich zuzugreifen (egal von wo). Haben Sie zum Beispiel eine lokale und globale Variable m it dem Namen ,.iwen«, können Sie mi t ,.::iwen« auf den globalen und wie immer mit _iwen« (ohne den Scope-Operator) auf das lokale Objekt zugreifen. Dieser Operator wird noch näher im Kapitel 4 bei den Klassen besprochen. Trotzdem hierzu ein Beispiel: 11 scope . cpp llinclude
gl obale Variable i nt iwe rt - 11 :
11
void func{ void ) : i nt ma i n(voidl func{ ) : re t urn 0:
void func( vo id ) { i nt iwe rt - 22 : 11 i wert der Funktion erhalt Zuschlag cout « iwert « ' \n ': I 11 Nochmals iwe rt der Funkti on cout « iwe rt « ' \n ': int iwer t - 33 : 11 Inneres iwe rt erhal t Zuschlag cout « iwe rt « ' \n ': 11 Globales i we rt erhal t den Zuschlag cout « :: iwer t « ' \n ': 11 nochmals globales iwe r t cout « : : i wert « ' \n ':
Das Programm bei der AusfGhrung: 22
22
I
3 ·1
I
3
I
Gottlgkeltsbere lche, spezielle Deklarat io nen und Typumwandlungen
33 11
11
3.1.3
Gültigkeitsbereich Namensraum (Namespaces)
Ein Bezeichner, der in einem Namensraum deklariert ist, existiert vom Anfang seiner Deklaralion bis zum Ende des Namensraums. Wegen de r Wichtigkeit der Namensräume (englisch: Namespaces) in eH wird in Abschnitt 3.2 gesondert darauf eingegangen.
3.1.4
Gültigkeitsbereich Klassen (Class Scope)
Klassenelemente, die in einer Klasse deklariert werden, gelten von dieser Deklaration an bis zum Ende der Klassendeklaration . Auf Klassenelemente kann nur in Verbindung mit einer Variablen des Klassen typs zugegriffen werden. Darauf wird speziell bei den Klassen in Kapitel 4 eingegangen .
3.2
Namensräume (Namespacesl
Mit Namensräumen (Namespaces) können Sie einen Gültigkeitsbereich erzeugen , in dem Sie beliebige Bezeichner wie Klassen, Funktionen, Va riablen, Typen und sogar andere Namensräume deklarieren können. Die Verwendung von Namensräumen ist besonders hilfreich bei sehr umfangreichen Projekten wo für gewöhnlich mit mehreren Modulen und Klassenbi bliotheken gearbeitet wird. Hier kann es passieren. dass es bei der Vergabe von Namen zu Konflikten mit gleichen Namen kommt. Zwei Klassen oder Funktionen mit demselben Namen lassen sich zum Beispiel ohne Namensräume nicht verwenden.
3.2.1
Neuen Namensbereich erzeugen (Definition)
Einen neuen Namensbereich können Sie entweder global oder innerhalb eines bereiLS definierten Namensbe reichs einfiihren. Dieser wird deklariert, indem hlmer dem SchlOsseIwort namespace der Name des Namensraums folgt. Die Deklarationen, die anschließend zu diesem Namensbereich gehören, werden zwischen geschwei ft en Klammern angegeben: namespace meinBereich int iwert : f loat fw er t : " De f init i on von f unkti on () voi d funk tion ( void ) I
m
Namensräume (Namespaces)
11 Anweisungen von f unktion() 11 Defin i tion von e i neKlasse cl ass e i neKlasse 1 11 Anweisu ngen fOr e i ne Klasse
Hier haben Sie eine i nt-Variable . iwert_. eine fl oa t -Variable ,.fwert_, die Funktion ,.funktion .. und die Klasse ,.eine Klasse .. als Elemente für den Namensbereich »meinBereich .. deklariert. Natürlich können Sie einen solchen Namensbereich jederzeit um weitere Elemente erweitern. Im Beispiel wurde die Funktion ,.funktion .. und die Klasse »eineKlasse .. bereits definiert. In der Praxis wi rd alle rdings meistens eine Deklaration im Namensbereich gemacht. Die eigentliche Definition wird für gewöhnlich außerhal b des Namensbereichs durchgeflihrt. Hierzu muss allerdings der Namensbereich mit dem Scope-Operator (bzw. Bereichsoperator) verwendet werden. Die Fun ktion ,.funktion .. und die Klasse »meine Klasse .. wird dann wie folgt defi niert: namespace me i nBere i ch int i wert : fl oat f we r t ; /I Deklaration von funktion{) void f unktion ( vo i d ) ; 11 Deklaration von eineKlasse cl ass e i neKlasse :
/I Def i nition von funktion() void meinBereich : : funktion{ void ) I 11 Anweis un gen von funktion()
11 Def i nition von eineKlasse cl ass meinBereich :: e i neKlasse 11 Anweis ungen für eineKlasse
Ein en bereits eingeführten Nam ensbereich kön nen Sie außerdem jed erzei t wieder öffnen und um weitere Elemente erweitern oder die Definition darin machen. Beispielsweise: namespace meinBereich i nt i wert :
I
3· 2
I
3
I
Gottlgkelts bere lche, spezielle Deklaratio ne n und Typumwandlungen
fl
oat fw e rt;
11 Deklaration von f unktion{)
void f unktion ( void ) ; 11 Deklaration von eineKlasse class ei neKlasse ;
11 Erneut fm Hamensberef ch mefnBerefch namespace melnBerelch ( 11 De f lnltlon von melnBere l ch::funktlon( vold ) vold funktlon ( vold ) ( 11 Anwelsungen von funkt lo n()
11 De f inition von e i neKl asse class meinBe r eich : :eineKlasse 11 Anweis ungen fOr eineK l asse
11 Na~ensbere fc h neu öffnen und erweftern namespace mefnBe r efch { 11 Namen sberefc h melnBerefch um dwert erweftert double dwert: )
Jetzt könnten Sie theoretisch einen neuen Namensbereich mit denselben Funktionen erzeugen, ohne dass es hierbei zu Konflikten kommt:
namespace meinBereich int iwert ; fl oat fw ert ; 11 Deklaration von funk ti on{) vo i d f unktion ( vo id ) : 11 Deklaration von eineKlasse cl ass ei neKlasse : namespace mel n NEUER_Berelch i nt iwe rt ; float fwert : 1/ Dekla r ation von funktion() void fun kt ion ( void ) ; 1/ Dekla ration von eine Klasse cl ass eineKlasse :
Namensräume (Namespaces)
Hier haben Sie zwei Namensbereiche (~mei nB ereich" un d .. mein_NEUEIL Bereich«) mit denselben Elementen. Das kommt zwar in der Praxis selten in dieser Form vor, soll aber hier demonstrieren, wofü r Namensbereiche dienen. 3.2. 2
Zugriff auf die Bezeichner im Namensraum
Verwenden Sie einen Bezeichner innerhalb eines Namens raums, so können Sie diesen. wie gewöhnlich. über den Typ und den Bezeichner ansprechen.
Sezei ehner : Wollen Sie allerdings einen Bezeichner außerhalb des Namensbereichs verwenden, müssen Sie den Namensbereich mit dem Scope-Operator mitangeben.
Namen s be r ei eh : : Beze i e hner ; Auf globale Bezeichner, die Sie außerhalb eines Namensbereichs definiert haben, können Sie mit dem Scope-Operator (Bereichsoperator) alleine zugreifen.
Auf r uf von glObalen Beze i chner : : Sezei chner :
11
Hierzu ein Be ispiel, das die Zugriffe auf die einzelnen Bezeichner eines Namensraums demonstrieren soll. Im Beispiel wurde dreimal eine Funktion ,.funktionO« und dreimal der Wert . iwert« verwendet. ohne dass es zu Konfli kten kommt 11 namespacel.cpp llinclude
namespace e rsterBere i ch i nt iwe rt - 11 ; 11 Dekla ration von Funktion i m // Namensbereieh erste r ßereieh void funktion( vo i d ) ;
namespaee zweiterBereieh I i nt iwe rt - 22 ; 11 Dekla r ation von Funktion i m 11 Namensbereieh zweiterBere i eh void funktion( yo i d ) :
225
I
3· 2
I
3
I
Gottlgkelts bere lche, spezielle Deklarat io ne n und Typumwandlungen
int main(void) I int gesamt : 11 Au fr uf von funktion() aus Namensbe r eich erste rBe r eich ersterBereich : : funktion() : 11 Au f ruf von funkt i on() aus Namensbe r eich zwe iterBere i ch zwe i terBerei ch : : funkt ion ( ) : 11 Aufruf der globalen Fu nkt ion funktion() fu nkt ion() ; 11 ... ode r auch ;; funktion() : 11 Zugriff auf den Wert iwe rt aus ersterBere i ch erster8e reic h: : iwer t - 66 ; 11 Nochmals die Funktion aus dem 11 Namensbereic h ersterBereich ersterBereich : : f unkt i on() : 11 Rechnung mit Werten aus meh reren Namensbereichen
gesamt - ersterBereich : : iwert + zweiter Bere i ch ::i wer t + :: iwert : cout « "Summe aus al len Namensbe r eichen « gesamt « ' \n ': return 0;
11 globale Funktio n def i nieren void funktion( void ) 1 eout « "f unk t i on{) : Globale\n ": • « iwert eout « "Wert von iwe r t
« "\n\n " :
void erste rBereich :: funklion{ void ) 1 eout « "f unkt i on() : Namens bere i ch erste rB ereich\n" : cout « "Wert von iwe r t : • « i wert « "\n\n" :
void zweiterBereich : : funktion{ void ) I cout « "f unkt i on() : Namens bere i ch zweite r Bereich\n ": cout « "Wert von iwert " « i wer t « "\n\n ":
".
Namensräume (Namespaces)
Das Programm bei der Ausführung:
I
3· 2
I
f unktion() : Namensbereieh e r sterBereieh Wert von i wer t : 11 funktion() : Na mensbereieh zweiterBereieh Wert von iwer t : 22 funk ti on ( ) : Glo ba le Wert von iwe rt : 1 funkt i on() : Glo ba le Wert von iwe r t : 1 funkt i on ( ) : Namensbere i eh ersterBereieh Wert von iwe r t : 66 Summe aus allen Namensbereie hen : 89 Die Verwendung der globalen Funktion aus der mai n-Funktion heraus kann auch ohne de Scope-Operator verwendet werden. Wollen Sie aber zum Beispiel die globale Funktion "funktionü " aus einem anderen Namensbereich aufrufen, benötigen Sie wieder den Scope-Operator. Wenn aus dem Namensbereich "ersterBereich" die globale Funktion "funktionO " verwendet werden soll, müssen Sie wie folgt vorgehen: void erste rB ere i eh : :f unktion( vo i d ) I cout « "funkt i on() : Namensbere i eh ersterBere i eh\n "; cout « "Wert von iwe r t : " « i wert « "\n\n "; 11 Ruft die globale Funktion funktion() auf ::funkt i on () ; Den Scope-Operator zu vergessen, hätte in diesem Beispiel außerdem noch den negativen Effekt, dass ei ne nicht mehr o hne Gewalt zu beendende Rekursion gestartet würde. In der Praxis sollte man es sich angewöhnen. den Scope-Operat.or zu verwenden. Werden (wie im Beispiel gesehen) mehrere Funktionen mit demselben Namen deklariert sowie definiert, und Sie importieren einzelne oder alle Bezeichner aus einem Namensbereich, so wird dem Bezeichner aus dem imponierten Bereich der Vorzug gegeben, da dieser dann eben der globalere Bezeichner ist (siehe au ch den nächsten Abschnilt 3.2.3).
227
3
I
Gott lgkeltsbere lche, spezielle Deklarat io ne n und Typumwandlungen
3.2.3
using - einzelne Bezeichner aus einem Namensraum importieren
Will man nicht ständig den Namensbereich mi tangeben, kann man auch einzelne Bezeichner aus einem Namensbereich ,.im portieren«. Hierzu werden Deklarationen mit d em schlüsselwort using gemacht. Mit folge nder us i ng · Deklaration können Sie zum Beispiel aus dem Namensbereich " e rsterBereich ~ die Funktion »funkrionO« imponieren: using ersterBereic h: : funktion : 11 Achtung . ohne Klammern (I Jetzt können Sie die Funktion ,.funktion()« aus dem Namensbereich ,.ersterBereich« mit einem einfachen Aufruf des Bezeichners aufrufen: funktion(} :
11 Funktion aus Namensbereich erste r Bereich
Hierzu nochmals unser Beispiel ,.namespace1.cpp. , verändert mi t dem schlüsselwort us i ng und einigen importierten Bezeichnern:
11 names pace2 . cpp Ifi nclude using names pace std : 11 Dekla ra t i on globale Funktion void funktion( vo i d ) ; 11 gl obale Va r ia bl e i wert i nt i wert - I ; namespace e r sterBere i ch i nt iwe r t - 11 ; 11 Dekla r ation von Funktion 11 i m Namensbereich e r sterBereich void fun ktion( wo i d ) ;
namespace zweiterBereich I i nt iwe r t - 22 ; 11 Dekla r ation von Funktion 11 im Namensbe reich zweiterBereich wo i d f unktion{ void l ;
int main(voidl I us ; ng erste r Be re ich : : funktion ; us ; ng zweit er Ber eich :: iwer t; int gesamt ;
228
Namensräume (Namespaces)
Aufruf von fu nkt ion {) aus Namensbereich ersterBereich funk t ion() : 11 Aufruf von funk t ion{) aus Namensbereich zweiterBe reic h zwe it erBereic h: : funktion() : 11 Aufruf der gl obalen Fu nk tion funktion() 11 Jet zt unbedi ngt mit dem Scope-Operator :: :: funktion() : 11
11 Rechnung mit Wer t en a us mehreren Name ns bereichen gesamt ~ erste rB ere i ch : : iwert + i wer t + : : i we r t : cou t « ~S u mme aus allen Namens bere i chen : " « gesamt « . \n ': return 0 :
«
3· 2
I
11 Zugri ff auf den We r t iwe rt aus lwei t e rB ere i ch i wer t - 66 : 11 NOChmals die Funktion aus dem 11 Name nsbere i ch lwe i t erB ere ich lwe i t erBere i c h: : funkt i on ( ) :
11 globale Funktion de f inieren void funktion( vo i d ) ! cout « "f unkt i on() : Glo bale\n" : cout « "Wert von iwe r t : " « i wer t
I
"'n\ n" :
void ers t e r Be re ich ::f unktio n{ vo i d ) 1 cout « "f unkt i on() : Namens bere i ch erste rB ere i ch\n" : cout « "Wert von iwe r t : " « i wert « "\n\ n" :
void lweiterBereich :: funktion( void ) I cout « "f unkt i on() : Namens bere i ch lweite r Bereich\n ": cout « "Wert von iwe r t : " « i wer t « "'n\ n" : Das Programm bei der Ausführung: f unktion() : Namensbereich e r sterBereich Wert von i wert : 11 funktion() : Namensbereich lwe i terBe reic h Wert von i wert : 22
229
3
I
Gott lgkeltsbere lche, spezielle Deklarat io ne n und Typumwandlungen
f unk ti on() : Namensbere i ch erste rBe r eich Wert von iwe r t 11 funkt i on() : Namensbere i ch lweite r Bereich Wert von iwe r t 66 Summe aus alle n Namensbereichen [» l
78
Hinweis Eine us i ng·Deklaration ist die Deklaration eines Namens im Gültigkeitsbereich und keine Definition!
3.2.4
using - alle Bezeichner aus einem Namensraum importieren
Wollen Sie alle Bezeichner eines Namensraums sichtbar machen, so ist dies zum Beispiel mit folgender Angabe möglich:
using namespace e rsterBereich ; Jetzt sind alle Bezeichner aus dem Namensbereich ,.ers terBereich .. einfach mit dem Bezeichner ansprechbar - es ist keine explizi te Angabe des Scope-Operators mehr nötig . Natürlich kann es trotzdem sinnvoll sein, den Scope-Operator zu verwenden, um Mehrdeutigkeiten zu vermeiden. Beachten Sie bitte, dass eine us i ng-Direktive, die den kompletten Namensbereich ,.importiert«, nicht einer Deklaration all er Namen eines Namensbereichs entspricht, sondern ledig lich einer Bekanntgabe des Namensraums, Dies bedeutet, dass eine usi ng-Direktive alleine noch nicht zu einem Konflikt fUhrt, wenn sich im aktuel len und importierten Namensbereich gleiche Bezeichner befinden. Das Problem der Mehrdeutigkeit tritt erst dann auf, wenn die Bezeichner angesprochen werden. Daz.u sollte man wissen, wie C++ nach einem Bezeichner sucht (siehe Abschnitt 3.2.5, NamensaujIösung) .
[»1
Hinweis Beim ,.Importieren« einzelner Bezeichner aus einem Namensraum spricht man von einer using -Deklaration. Bei der Verwendung aller Namen aus dem Namensraum ohne Angabe des Scope-Operators wird von einer us i ng-Direktiven gesprochen.
Ein Beispiel. das die Verwendung von us i ng-Direktiven demonstriert: 11 namespace3 . cPP llinclude us i ng namespace std ;
namespace ersterBereich int iwert - 11 ; 11 Dek l arat ion von Funktion fu nktionl
23°
Namensräume (Namespaces)
11 i m Namensbereich e rster Bereich
3· 2
I
void fun kt ion}( void ) :
namespace zweiterBereich 1 fl oa t fwert - 11.11 : 11 Dekla r ation von Funktion funktion2 11 im Namensbere i ch zweite rBereich vo i d funk t io n2( vo id ) :
int main(voidl 1 us i ng na mespace ersterBereich; us i ng na mespace zwe i te rBe re i ch: Ruft fu nkt ionl() aus Namensbere i ch ersterBere i ch auf funk t ionl<) : 11 Ruft fun kt i on2() aus Namensbere i ch zwei terBereich auf funktion2 ( ) : 11
"n "n
I
11 Gibt den W ert iwe rt iwert Gi bt den W er t fwe rt 11 fwe rt « fwe r t « . \n ' : cout « re t urn 0,
,. ,.
void ers t e rBe rei ch ::f unktio nl{ void ) I cout « "funkt i on() : Namensbere i ch erste rBe re i ch\n" : cout « "Wert von iwe r t : " « i wert « "\n\n" :
void zweiterBereich : : funktion2( vo i d 1 1 cout « "f unk t ion() : Namensbereich zweite r Bereich\n" : cout « "Wert von fwe r t : " « fw er t « "\n\ n" : Das Prog ramm bei der Ausführung:
f unktion() : Namensbereich er sterBereich Wert von i wer t : 11 funktion() : Namensbereich zweiterBereieh Wert von fwer t : 11 . 11
231
3
I
Gott lg keltsbere lche, spezielle Dekla rat io ne n und Typumwa ndl unge n
main{) -) iwert main {) -) fwert
11
11 . 11
Wenn ein Namensraum auch eine us i ng-Direktive en thält, di e Bezeichner importiert, so wird automatisch der zweite Namensbereich mit importiert:
// names pace4 . cPP Ifi nclud e
namespace zwei ter Ber eich 1 us1ng namespace ersterBere1c h; f l oa t fwer t - 11 . 11; // Dekla rat i on von Funktion f unk ti on2 11 im Namensbe reich zwe i t er Bereic h vo i d funktio n2 ( vo id 1:
i nt main(voidl I 11 Importi er t aut oma ti sC h auch e rs te rB e reic h us1ng namespace zwe1terBe reich: 11 Ru f t funktion1{ ) aus Namens bere i ch erste rB ere i ch au f fu nkt ionl C) : 11 Ru f t funk tion2( ) aus Namen sb ere i ch zweite r Bereich auf fu nkt ion2 ( ) ; 11 Gibt den W ert von iwe r t aus Bereich erste rBereich aus cout « "main!) -) iwert ; " « i wert « ' \ n'; 11 Gibt
den Wert von fwe rt aus BereiCh zweite r Bere i ch aus
cout« "mai n() -) fwe rt : " «fwert« ' \ n': re tu rn 0:
void erste rBe reich ; ; funk t io nl( void ) I cout « "fu nkt i on() : Namens bere i ch erste rBer eich\n " : cout « "Wer t von iwe rt : " « iwer t « "\ n\ n":
232
Namensräume (Namespaces)
vo i d zweite rB ere i ch ::funktio n2( void ) I cout « "funktion() : Namensbereich zweiter Bere i ch\n ": " « fwert « "\n\n" ; cout « "Wert von fwert
I
3· 2
I
Sie können die Namensbereiche aber auch verschachteln, das heißt in einem Name nsbereich können noch mehrere andere Namensbereiche deklariert werden. Hier ein solches Beispiel:
namespace ers terBereich_aussen int iwe r t - 11 ; void fu nk tion( void ) ; names pace ersterBe reic h_ innenl int iwert - 22 : void fun ktionl( void ) ; namespace er sterBereich_innen2 int iwert - 33 : vo i d funktion2( void ) ;
11 Aufru f von funktion}() ers te rBe r ei Ch_dussen : :ersterBere i ch_i nnenl : : f unkt i on 1( ) ; 11 Aufr uf von f unktion2() ers te rBe r ei Ch_dussen : :ersterBere i ch_i nnen2 : : funkt i on2( ) ; 11 Aufr uf von f unktion() ers te rBe re i Ch_d us sen : : f unk t i on ( ) ;
Wenn Sie Namensbereiche verschachteln, kann dies sehr verwirrend. daher sollten Sie sich folgende Zugriffsregel n merken: ~
Ein Bezeichner im inneren Namensraum verdeckt Bezeichner gleichen Namens aus den äußeren Bereichen.
•
rm inneren Namensraum können Sie die Bezeichner aus den äußeren Namensbreiche n verwenden, ohn e den Scope-Operator daftlr zu benutzen.
~
Alle Bezeichner eines inneren Namensraums sind für den äußeren Namensbreich nicht sichtbar und müssen mit dem Scope-Operator verwendet werden.
•
Bei verschachtelten Namensräumen, die mit der us i ng-Direktive ,.importiert.. werden, sind nur die Bezeichner im Namensraum selbst sichtbar! Bezeichner in nerhalb eines Namensraums sind hi er noch nich t verfügbar.
233
3
I
Gottlgkeltsbere lche, spezielle Deklarat io ne n und Typumwandlungen
3,2.5
Namensauflösung
Hier der versprochene Abschniu, der kurz beschreibt, wie CH nach einem Bezeichner sucht (wird auch als Name Lookup bezeichnet). Hier ein Beispiel, mit dem Sie die anschließend beschriebenen Namensauflösungen in der Praxis demonstrieren können , indem Sie die einzelnen Bezeichner »iwert" auskommen· tieren bzw. wieder anwenden. 11 namespace5 . cpp llinclude
namespace einBereich 11 Deklaration im NamenSbereich e i nBereich int iwert • 11 : globale Deklaration int iwert - 22:
11
i nt main(voidl I us i ng names pace einBereich ; 11 lokale Deklaration ; nt iwert - 33 : cOut « iwert « ' \n ': return 0: Stößt der Compiler auf einen Bezeich ner, versucht er diesen Namen nach fo lgen· dem Schema aufzulösen : .. Zuerst wird nach der lokalen Deklaration eines Bezeichners gesucht. ..
Wird keine lokale Deklaration des Bezeichners gefunden, werden Bereiche abgesucht, die im aktuellen Gültigkeitsbereich enthalten sind. Dies umfasst theoretisch auch den globa len Bereich.
.. Als letztes wird nach dem Bezeichner im importieren Namensraum gesucht. Dies bein ha ltet auch die namenlosen Namensbereiche (siehe Abschni tt 3.2. 7).
3.2.6
Aliasnamen für Namensbereiche
Wenn ei n Namensbereich bereits definiert wurde, dürfen Sie dafür auch einen neuen Namen (genauer Aliasname) verwenden.
namespace neuerName alterName :
'34
Namensräume (Namespaces)
Hiermit können Sie auf die Bezeichner des Namensbereichs sowohl mit »neuerName j( als auch mit ~ aherName .. zugreifen. Diese Techn ik wird gerne beim Zugriff auf verschachtelte Namensberciche verwendet. Beispielsweise: namesp ace er s te r Bereich_au s s en int iwert - 11 : void fu nkt i on ( void ) : names pace e r sterBe r eich_ innen f lo a t f wert - 11. 11 : void funktionl ( void ) :
Wenn Sie nun folg ende Definition machen namesp ace e inBere i ch e r st e r Bere i ch : : e r s ter Bereich_innen ;
können Sie zum Beispiel mit e i nBe r eic h : : fwert - 22 . 22 ;
auf die Vanable ,.fwert« Im Inneren Bereich des Namensbereichs ,.ersterBerelch innen« zugreifen . Ohne den Aliasnamen müSsten Sie umständlich Folgendes verwenden: erste r Be r eich_aussen : :erster _Bereich_i nn e n: : fwert - 22 . 22 :
3.2.7
Anonyme (namenlose) Namensbereiche
Es kann auch ein Namensraum ohne einen Namen verwendet werden. Beispielsweise: namespace 11 ...
Dieser Namensbereich wird als Namensraum mit einem systemweit eindeutigen Namen mit einer us i ng-Direktive verwendet und entspricht im Prinzip folgendem Konstrukt: na me space e i nBerei ch /I
using namespace einBereich :
Auch wenn Si e dem anonymen Namensbereich keinen Namen gegeben haben. so wird dennoch einer vom Compiler vergeben. Durch die automatische Verwendung der anschließenden us ; ng-Direktive können Sie Bezeichner eines anony-
'35
I
3· 2
I
3
I
Gottlgkelts bere lche, spezielle Deklarat io ne n und Typumwandlungen
men Namensbereichs im aktuellen Gültigkeitsbereich (siehe Abschnitt 3.1) direkt (ohne Scope-Operator) ansprechen. Außerhalb dieses Gültigkeitsbereichs können die Bezeichner des anonymen Namenbereichs nicht mehr verwendet werden, wei l es nicht möglich ist eine us i ng-Direktive für einen Namensbereich zu verwenden, der keinen Namen hat. Damit hat man ein effektives Mittel. wenn man globale Variablen verwenden wilVmuss (warum auch immer). Da der Gültigkeitsbereich nur fur die aktuelle Übersetzungseinheit (beispielsweise der Datei) gültig ist, entstehen keine Konflikte, wenn in einer anderen Datei der übersetzungseinhe it eine globale Variable mit demselben Namen existieren sollte. Natürlich stellt dies nach wie vor keinen Freischein für globa le Variablen dar. Nebenbei stellen anony me Namensbereiche eine gute Alternative für staticDeklarationen da. Beispielsweise: stat;c int ;wert : Hier haben Sie eine Variable vo m Typ int mit einer statischen (siehe Abschn itt 3.4.3) Lebensdauer deklariert. Auch diese Deklaration hat ihren Gültigkeitsbereich in der aktuellen Obersetzungseinh ei t. Dasselbe realisieren Sie auch mit einem anonymen Namensraum wie folgt: ndmespdce 1 int iwert :
3.2.8
Namensbereich und Headerdateien
Das beste Beispiel zum Thema Namensbereich finden Sie in der C++-Standardbibliothek selbst. Im ANSI-C++-Standard sind alle Klassen , Objekte und Funktionen in der C++-Standardbibliothek im Namensbereich std defin iert. Durch die Verwendung vo n using namespace std : im Programm ersparen Sie sich den Scope-Operator und können auf alle Klassen, Objekte und Funktionen ohne den Scope-Operator : : auf die Bezeichner zugreifen, Ohne die Verwendung der using-Direktive und das ,.Impon ieren« des Namensbereichs s td müssten Sie auf die Bezeichner der HeaderdalCi wie folgt zugreifen: // namespace6 . cpp #i nclude
Namensräume (Namespaces)
int main(void) 1 std : :cout « "Ohne using namespace std\n ": retu rn 0 : Ich könn te es jetzt hierbei belassen und davon ausgehen. dass schon alles in Ordnung gehen wird. Aber wenn Sie sich vielleicht schon einige ältere Quelleodes angesehen haben. werden Sie feststel1en. dass es Programme gibt. die iostream folgendermaßen einbi nden Ifi nclude
int main(void) 1 cou t « · Ohne us i ng names pace std\n" ; retu rn 0 : Das dies hier ohne Bekanntgabe des Namensbereichs std funktioniert. liegt daran. dass C++ vor 1997 noch gar keine Namensbereiche kannte. Die Verwendungen der Headerdateien mit der Endung ".h « wie zum Beispiel
I
3· 2
I
3
I
Gott lgkeltsbere lche, spezielle Deklarat io ne n und Typumwandlungen
11 namespace8 . c pp llinclude
int main{void) 1 std :: cout« "Aktueller Stand ard\n" : return 0 : Oder auch mit der usi ng-Direktive (mit der Sie auch gleich die Kompatibilität zu älteren Program men bewahren, die noch keine Namensräume kennen): 11 namespace9 . cPP llinclude us i ng namespace s t d :
int main(voidl 1 cout « "Aktueller St anda rd\n" : retu r n 0 : Die Version, die Sie im Beispiel »namespace 7.cpp« gesehen haben, ist somit nur noch geduldet, aber entspricht nicht mehr dem aktuellen Standard. Leider kann man nicht einfach den neuen Standard verwenden. Auf manchen Implementierungen (beispielsweise auf Uralt-Systemen) schlägt nämlich die Version ohne eine Endung der Standard-Headerdat eien bei der Obersetzung fehl. Dann bleibt einem meistens nichts anderes mehr übrig, als die alte Version mi t der Endung ».h« zu verwenden.
3.3
(-Funktionen bzw. Bibliotheken in einem (++-Programm
Es wurde bereits zuvor erwähn t, dass sich der Weg, eine Headerdatei einzubinden mit dem neuen CH-Standard geändert hat. Dies gi lt auch für die Verwendung der Standardbibliomeks-Funktionen von C. Neben der Neuerung, dass die Headerdateien nicht mehr mit der Endung ».h« (siehe Abschnitt 3.2.8) eingebunden werden, bekommen die Headerdateien vo n Standard-C das Zeichen »c« vorangestellt. Damit werden diese als C-Heade rdateien gekennzeichnet und gleich sichtbar. Aus <std i o.h> wird also . Hierzu eine Liste von C-Headerdateien (siehe Tabelle 3.1), wie sie im ANS I CH Standard eingebunden werden soll ten, und das Gegenstück dazu in ANS! C:
238
(-Funktionen bzw. 8ibliotheken in einem (++-Programm
ANSI (
ANSI C++
<err no.h >
<math .h >
<setJmp. h>
<slg n~ l.h>
<std ~ rg.h>
<stddef. h>
<stdl0 . h>
<stdli b. h)
<str1ng . h>
h>
<wtype h>
<wch ~ r
3.3
I
Tabell e 3.1 Das ANS1 C++ Gegenstück von ANS1 C
3.3.1
I
C-Funktionen aus einer C-Bibliothek aufrufen
Dieser Abschnitt richtet sich an die schon etwas erfahreneren Programmie rer, die bereits Programme in C geschrieben haben. Wenn Sie zum Beispiel ei ne C-Funktion aus einer C· Bibliothek aufrufen wollen, müssen Sie nicht den C-Quell code neu kompilieren. Wollen Sie also in einem C++-Programm eine C-Funktion aus einer C-Bibliothek aufrufen, die mit einem C-Compil er übersetzt wurde. müssen Sie dies dem CHCompiler bekannt geben:
extern -CO typ funktion( parameter ) : Damit teilen Sie dem CH-Compiler mit, dass die Fun ktion .fun ktionO. mit einem (-Compiler übersetzt wurde. Hier sollte man vielleicht noch darauf hinweisen, dass ein CH-Compiler Funktionsaufrufe anders übersetzt, weil dieser gegenüber C einige Spracherweiterungen (beispielsweise Überladung) besi tzt. Natürlich können Sie hierbei auch mehrere C-Funktionen einer Bibliothek verwenden. Hierzu müssen Sie die Funktionen nur in geschweiften Klammern zusammenfassen:
239
3
I
Gott lgkeltsbere lche, spezielle Deklarat io ne n und Typumwandlungen
ext ern typ typ typ
· C· [ fu nktion1 { parame t er ) ; funktion2< parameter) ; funktion3{ parameter ) ;
Sind diese ( -Funktionen bereits in einer Headerdatei deklariert, so müssen Sie nur noch diese Datei einkopieren: extern ·C· I lIi nc l ude · CFunkt ionen . h· Wo llen Sie C-Funktionen oder Headerdateien sowohl für C als auch CH-Programme zur Verfügung stellen (was d urchaus üblich ist), müssen Sie eine bedingte Kompilierung w ie folgt verwenden: 11 Haben wir e i n C++-Programm #1fdef __ cp l usplus ex t ern "e" ( JJ FOr das e++ -P rogramm #end1f JJ Fü r C und C++- Prog r amme typ funktion!{ parameter ) : typ funktion2{ pa r ameter ) ; t yp funk t ion3( parameter ) 11 fdef __ cplusplu s ) JJ Für das C++-Programm
,
lend1 f
Mit der vordefinierten symbolischen Konstante __ cp 1 uspl us können Sie feststellen , ob ein C oder C++-Compiler verwendet w ird. Wenn es sich um e inen C++(ompiler handelt. wird die e xtern · C· -Deklaration mit den geschwe iften Klammern verwendet. Wenn Sie ( -Funktionen in einem (++-Programm neu erstelle n woll en, wie dies zum Beispiel nötig ist. wenn Sie eine (-Fu nktion aufrufen, die als Argument eine weitere (-Funktion erwartet (wie dies etwa bei den Standard-(-Funktionen bsea r"ch( ) und qsort{) der Fall ist), dann können Sie auch hier den (.1"T-(ompiler mi t ex t ern · C· anweisen, diese Funktion als ( -Fu nktion zu übersetzen. Hierzu ein reines (- Programmbeispiel: 1* bsearch . c *1 lIinclude <std i o.h> llinclude <std l ib . h> llinclude <st ri ng . h> J- Anzahl der Str i ngs * /
C-Funktionen bzw. Bibliotheken in einem C++-Programm
lIdefine MAX 5
3,3
I
/' Vergleichsf~nkt i on fOr zwei Strings */ int cmp_str(const void *sl , const vo i d *s2) return {strcmp( *(char **)sl . *(char **)s2»:
i nt main(void) 1 char *daten[MAXl. puffe r [80] , *ptr . *key_ptr . w*key_ptrpt r: int count : / * WOrter eingeben * / pr i nt f (-Geben Sie Id WOrte r ein\n ". MAX) ; for (count - 0; count< MAX ; co~nt++) 1 pr i nt f ("Wort td : •. co~nt+l) ; fgets(puffer, 80 , stdin) ; / * Speicher fOr das Wort N~mer count reservieren */ daten[co~ntl - (char *) malloc(strlen{p~ffer)+l) : strcpy(daten[countl, st rtok{puffer . "\n") ) : /* Die einzelnen Wörter so rtieren * / qsort(daten, MAX , s i zeof(daten[O]) . cmp_st r l ; 1* Sortierte Date n ausgeben */ for (count = 0: count< MAX: count++) printf{ "\nWort Id : 15 ". co~nt+l , daten[co~nt])
I
:
1* Jetz t nach einem Wort suchen */ printf( "\n\nNach welchem Wort wollen Sie suchen : "1: fgets(pu ff er . 80 . stdinl : / * Zur Suche ~ bergeben Sie zuerst den puffer an key , * danach benOtigen Sie einen weiteren Zeiger , der * auf diesen Such -Schlüssel ze i gt ,/
key_ptr - strtok{puf f er . "' n" ) ; key_ptrptr - &key_ptr; 1* ptr bekommt die Adresse des Suchergebnisses *; ptr - (ehur * ) bseureh(key_ptrptr . duten . MAX . sizeof(daten(O)l , cmp_str) ; if(NULL - - ptr) printf{"Kein Ergebnis für %s\n" , puffer) ; el se prin t f( "Ss wurde gefunden\n" , puf fe r ): return EXIT_SUCCESS ;
241
3
I
Gott lgkeltsbere lche, spezielle Deklarat io ne n und Typumwandlungen
Dies hier ist reiner (-(od e_ Wenn Sie nun die »d ankbare« Aufgabe haben, dieses Programm in e in (++-Programm umzuschreiben, haben Sie imme r noch das Prob lem, dass die beiden Fu nktionen bsearch() und qsort() (-Funktionen als Argume nte erwarten, Mit dem bisherigen Wissen und extern "C O können Sie dies durchaus selbst realisieren, wenn Si e wollen, Hier das Beispiel umgeschrieben in einen CH-Code, / * bsearCh,Cpp */ llinclude llinclude Uinclude Uinclude us i ng namespace std :
11 Anzahl der Str ings const i nt MAX~5 : const i nt BUF~80 :
I/ Vergle i chs funktion für zwei St r ings extern "CO I i nt cmp_st r (const ~o i d *sl , const ~o i d *s2) I re t urn (strcmp(*{char"Jsl. *(char '*)52» :
i nt main{voidJ I char *daten[MAX1 , puffer[BUF1 , *pt r, *key_pt r, **key_p t rptr : // Wörter eingeben cout « "Geben Sie' « MAX « • Wö r ter ein\n" ; for (int count - 0 ; count< MAX ; co unt++) I cout « 'Wort • « (count+l) « ": ': c i n , getline(p uffer , BUF) ; // Spe i che r f ür das Wort Nume r count reservieren daten[count] - new char [BUF+ll : strcpy(daten[countJ . strtok{ puffer , ' \n') }; // Di e einzelnen Wörter sortieren qsort
242
SpeicherkJassenattribute
11 Je t z t nac h einem Wort suchen
' /
key_pt r - strtok{puf fer . "\n") : key_p t rptr - &key_ptr : 11 ptr bekommt die Adresse des Suchergebnisses ptr -(char *) bsearch(key_ptrptr . daten , MAX , sizeof(daten(O)l . cmp_strl : cout
«
"Ke i n Ergebnis fOr "
«
pu ff er
«
' \n ':
else I cout « puffer « • wurde gefunden\n ": return 0: Das Programm bei der Ausfiihrung:
Geben Sie 5 Wörter ei n Wort I , Vogel Mensch Wort Wort ] , Auto Wort 4 , Baum Wort 5 , Pfe rd
"
Wort Wort Wor t Wor t Wort
I , Auto
" 8aum Mensc h ], 4 , Pfe rd
5, Vogel
Nach welchem Wort suchen Sie 8aum wurde ge funden
3.4
3·4
I
cout « " \n\n ~ ach welchem Wort suchen Sie : ": ci n. getline(p li ffer . 8UF) : 1* Zur Suche ~bergeben Sie zuerst den puffer an key. * danach benötigen Sie einen weiteren Zeiger , der ~ auf diesen Such ' SchlOssel ze i gt
if
I
, Baum
Speicherklassenattribute
Mit Speicherklassenattribulen geben Sie die Speicherklasse eines Bezeichners an und bestimmen somit die Lebensdauer und Bindung eines Bezeichners.
243
3
I
Gottlgkeltsbere lche. spezielle Deklarat io ne n und Typumwandlungen
3.4.1
Speicherklasse auto
Die Speicherklasse auto haben Sie bisher fast immer (unbewusst) verwendet. Durch das Voranste llen des schlüsselworts au to bei der Deklaration einer Variablen wird diese Variable automatisch angelegt und am Ende des Gültigkeitsbereichs (Blocks) wieder gelöscht. Der Gültigkeitsbereich von au to ist somit derselbe wie bei einer lokalen Variablen (siehe Abschn itt 3.1.1): auto int i wer t - 11 ; ist dasselbe wie int iwert - 11 : Da also auto die Standard-Speicherklasse ist. kann die Angabe auch ganz entfallen. Verwenden Sie aut o für globale Bezeichner. bekommen Sie eine Fehlermeldung. da auto nur für lokale Objekte gültig ist - und somit auf Funktionen überhaupt nicht anwendbar, weil diese immer global sind .
342
Speicherklasse register
Alles was zu au to gesagt wurde, trifft auch auf die Speicherklasse regi ste r zu. Allerdings weisen Sie mit dem schlüsselwort reg i s t e r den Compiler an, diese Variable möglichst lange im Prozessorregister zu halten - wodurch ein schnellerer Zugriff möglich ist als auf dem Arbeitsspeicher. Allerdings gilt für das Schlüsselwort reg i ster mi t Variablen dasselbe wie für in 1i ne bei den Funktionen der Compiler entscheidet selbst welche Variable er in den schnellen Prozessorregistern ablegt. Somit kann der Compiler diese Speicherklasse auch ignorieren.
3.4 .3
Speicherklasse static
Wenn Sie eine Variable mit der Speicherklasse s ta t i c deklarieren, ex istiert diese Variable von ihrer ersten Verwendung an bis zum Ende des Programms. Bei skalaren Objekten bewirkt static. dass dieses Objekt automatisch mit 0 initialisiert wird. Ein Beisp iel: / ' static . cpp ' / lIinclude
SpeicherkJassenattribute
3·4
I
re t urn 0:
void f unktion( vo i d ) { static int iwer t ; eout « iwert « ' \n ': i wert++ ; Das Prog ramm bei der Ausfiihrung:
o 2
[n der Funktion .. funkt ionO .. finden Sie eine statische Variable .. iwen.. , deren Wert bei einem Funktionsaufruf zunächst ausgegeben wird. Beim ersten Aufruf ist dieser Wert automatisch 0 - außer man initialisiert diesen mit einem anderen Wert. Am Ende der Funktion wird dieser Wert um eins inkrementiert. Beim Beenden der Funktion wird die statische Variable ,.iwert.. nicht wie üb lich zerstört, sondern bleibt zu Lebenszeit des Programms erhalten, was die erneuten Funkrionsaufrufe auch demonstrieren. Allerdings bleibt der Gühigkeitsbereich nach wie vor nur innerhalb der Fu nktion .. funktionO.. erhalten. Es kann also nicht außerhalb der Funktion auf diese statische Variable zugegriffen werden. Ein erneuter Funktionsaufruf von ,.fu nktionO.. hat außerdem den ,.Vorteil .. , dass diese Variable nicht mehr erneut angelegt werden muss. Natürlich können Sie auch Funktionen mit der Speicherk lasse statie versehen. Dies wird zum Beispiel verwendet. dami t Funktionen (und natürlich auch Variablen) ei ne lokale Gültigkeit haben - also ausschließlich in der Datei gültig sind. In CH ist es allerdings mittlerweile üblich, interne Objekte nicht mehr mit der Speicherklasse s tat i c zu versehen, sondern es wird ein anonymer Namensraum dazu verwendet (siehe Abschnitt 3.2.7) .
3.4,4
I
Speicherklasse extern
Standardmäßig können alle global in einer Datei definierten Bezeichner in anderen Dateien benutzt werden, die nicht als stat i e ausgewiesen wurden. Somit ist also die Verwendung der Speicherklasse extern zunächst optional wenn man genau ist. Mit dem Schlüsselwort extern teilen Sie dem Compiler mit, dass die Definit ion von Variablen oder Funktionen in einer anderen Datei, oder
245
3
I
Gott lgkeltsbere lche, spezielle Deklarat io ne n und Typumwandlungen
einem externen Modul definiert wurde. Mit extern deklarierte Objekte geben nur den Namen bekannt und belegen zunächst noch keinen Speicherplatz. Dies geschieht erst bei der Definition in eine r andere n Datei. Hierzu ein Beispiel: 11 abc . cP P llinclude using names pa ce std ;
extern vaid pr i nt1( vai d ) ; extern vaid pr i nt2( va i d ) ; extern int MAX ; i nt ma i n(voidl print1{) ; print2{) ; caut « "Wert van MAX : • return 0:
«
MAX
«
' \n ';
Hier fi nden Sie die Deklaration von zwei Funktionen und einer Variablen, die als ext ern deklarien sind. Somit weiß der Compile r hier schon mal, dass sich die Definitionen dieser Bezeichner in einer anderen Datei befinden. Im Falle der Funktionen könnten Sie das Schlüsselwort exte r n auch weglassen. Bei der Variablen -"MAX«. hingegen würde dies zu einer Fehle rmeldung führen, wenn in der externen Datei ebenfalls eine Va riable mit demselben Namen vorhanden wäre (worauf dieses Beispiel ja auch hinaus will) - wobei wir auch gleich wieder bei der Warnung wären, möglichst auf globale Variablen zu ve rzichten. Die Datei, in de r sich die Funktionen und die Variable befinden , heißt in diesem Fall -"xyz.cpp" und sieht so aus: 11 XYZ . cp p lIinclude uS i ng namespace std ;
void printl( void };
void print2{ void ) : i nt MAX -
10 ;
waid print1{ waid cout « " Ich bi n prin t1 { )\n" ;
Typenqualifikatoren
void print2( vo i d ) cout « "Ich bi n print2( )\n" ; Sofern Sie dieses Beispiel in der Praxis ausführen woll en, müssen Sie die Datei "xyz.cpp" bei m überSelzen mit angeben bzw. dem »Proj ekt" hinzufügen. Bei der Verwendung von cons t -Beze ichnern lässt sich das hier Beschriebene allerdings nicht so realisieren, da cons t- Bezeichner nur in der Date i gültig sind, wo diese definiert werden. Wollen Sie auch const-Bezeichner in verschiedenen Dateien verwenden, müssen Sie diese schon bei der Definition als exte rn definieren. Dies sähe, bezogen auf die Datei " abc.c p p ~ und die Variable ,.MAX" wie fo lgt aus: 11
abc . cpp
extern const i nt MAX ; Und bei der Datei »xyz .cpP'" in der gang wie fo lgt aus:
»MAX~
auch defi niert wurde, sieht der Vor-
11 xyz.Cp p
exte rn const i nt MAX - 10 ; Eine weitere Verwendung der Speicherklasse ex t ern haben Sie ja bereits gesehen, als es darum ging, C-Code in einem CH-Programm einzufügen (siehe Abschnitt 3.3).
3.4.5
Speicherklasse mutable
Die Speicherklasse mutab 1e wird nur auf Klassenelemente angewendet, doch sei hier schon mal der Vollständigkeit halber da rauf hingewiesen. Mit diesem Schlüsselwort spezifiz ieren Sie ein Klassenelement als »ma n ip u lierbar~ . auch wenn dieses Element mit (nicht explizit) co ns t vereinbart wurde!
3.5
Typenqualifikatoren
Es gibt zwei Typenqualifikatoren mit co nst und volati le . Sie haben const schon näher kennen gelernt (Sie he Abschnitt 1.5). Die beiden Qualifikatoren Jassen sich auch miteinander verwenden, wodurch sich insgesam t vier Angaben von Typen ergeben:
247
I
3·5
I
3
I
Gott lgkeltsbere lche, spezielle Deklarat io ne n und Typumwandlungen
11 unqua 1 i fi zierte Typenangabe usigned in t reg; 11 eons t ~Objekt eonst uns i gned i nt reg ; 11 volatile-Objekt volatile unsigned int reg ; 11 eons t volatil e -Objekt eonst vol at i le unsign ed i nt reg ;
Qualifizierer const
3.5.1
Wie bereits erwähnt, wurde eons t bereits in Abschnitt 1.5 beschrieben. Hiermit qualifizieren Sie einen Typ so, dass dieser nicht mehr geändert werden darf. Dies bedeutet. dass er nicht mehr auf der linken Seite vor einer Zuweisung stehen darf: eonst int MAX - 5 ; 11 Fehle r !!! MAX - 10 ;
Qualifizierer volatile
3.5.2
vo 1at i 1e gibt eine Variable an. die im Programm zum Beispiel vom Betriebssystem, der Hardware oder einem gleichzeitig ausgeführten Thread (Parallelprozess) geänden werden kann.
Der Qualifizierer vol a t i 1e kann nur auf Variablen angewendet werden und bewirkt, dass diese Variablen außerhalb des normalen Prog rammablaufs den Wert verändern können. Mit volatile modifizieren Sie eine Variable so, dass der Wert dieser Variablen vor jedem Zugriff neu aus dem Hauptspeicher eingelesen werden muss. Das Hauptanwendungsgebiet hierfür ist die System-, Hardware- und Treiberprogrammierung. Beispielsweise soll in einer Schleife überp rüft werden, ob ein bestimmter Zustand im Register des Prozessors vorzufinden ist:
while { reg ~( ZUST~KO_~IZUST~~O_B) 1 II Ein Hardware-Gerat wird überprüft cout
«
"Status Ge rat X
(OK)\n ";
An dieser whi 1e-Sehleife sehen manche Compiler, dass immer auf dieselbe Adresse überprüft wird und optimieren diese Schleife weg. Dies bedeutet, die Überprüfung des Gerätestatus wird nur einmal ausgeführt, weil hier die Schle ife weg ist. Der Compiler kann einfach nicht wissen. wie wichtig diese Überprüfung
Typumwandlung
für den weiteren Programmablauf ist. Somit gilt fü r Variablen. die mit vo 1at i 1e deklariert sind, dass diese ohne jede Optimierung neu aus dem Hauptspeicher geladen und neue Werte auch sofort wieder dort abgelegt werden.
3.6
Funktionsattribute
Auch rur Funktionen gibt es Atlribute. Mit in 1i ne haben Sie bereits eines kennen gelern t. Neben i nl ine (Siehe Abschniu 1.10.6) gibt es noch die Attribute virtual und exp l ic it, die allerdings nur im Zusammenhang mit Klassen und Methoden verwendet werden können, weshalb auch erst in Kapitel 4 darauf eingegangen wird.
3.7
Typumwandlung
Bevor man sich mit der Typumwandlung beschäftigt, sollte man sich zunächst vor Augen halten, dass ein Typ auch nichts anderes als eine Kette einzelner Bits mit 0 und 1 darstellt. Wie Sie bereits bei den Basisdatentypen erfahren haben, ist die Länge dieser Bitkette abhängig vom Datentyp. So hat der Datentyp char gewöhnlich meistens ei n Byte, was meistens auch acht Bits sind (aber nicht sein müssen). Der Datentyp i nt repräsentiert auf ei ner 32-B it-Maschine meistens vier Bytes und hat somit gewöhnlich 32 einzelne Bits mit 0 und 1. Beispi elsweise sieht die Zahl 100 als cha r-Wert wie folgt aus: 01100100
11
1 Byte - ) 8 Bi ts - ) Wert - 100
Bei einem Typ i nt sieht diese Bitkette wie folgt aus (32 Bit): 00000000 00000000 00000000 01100100 Im Grunde haben Sie hier dieselbe BitsteIlung im ersten Byte. Somit könnten Sie für den Wert 100 sowohl den Typ char also auch i nt verwenden. Nehmen wir aber mal den Wert 256 als i nt: 00000000 00000000 000000001 00000000 Dieser Wert passt nicht mehr in den Typ char , da hierbei das zweite Byte mit einem Bit besetzt ist. Worauf ich hinaus will ist, dass bei einer Typumwandlung (beispielsweise hier von i nt nach char) einige Stellen »verloren« gehen kÖnnlen. Häufig trelen solche Probleme (auch unsichere Konvertierung genannt) beim Konvertieren von Gleitpunktzahlen zu ganzzahligen Werten auf.
249
I
3·7
I
3
I
Gottlgkeltsbere lche, spezielle Deklarat io ne n und Typumwandlungen
Eine Typumwandlung kann implizit - also durch den Compiler automatisch - erfolgen oder explizit vom Programmierer mit einem Cast-Ausdruck "erzwungen « werden. Bei den Klassen können Sie außerdem noch eine solche Typumwandlung selbst definieren - worauf aber erst in Abschnitt 4.7.7 eingegangen wird,
3,7.1
Standard- Typumwandlung
Eine implizite Standard-Typumwandlung wird vom Compiler vorgenommen, wenn der Typ eines Ausdrucks nicht mit dem Typ übereinstimm t, der erwartet wurde. Dies trifft häufig in folgenden Fällen zu: ..
Funktionsaufrufe - Wurde bei einem Funktionsaufruf zum Beispiel ein double-Wert erwartet, als Argument aber ein Typ i nt übergeben, wird das i nt-Argument in einen doub 1e-Wert konvertiert.
..
Arithmelische Ausdrücke und Vergleiche - Werden bei Berechnungen zwei verschiedene Typen verwendet, so wird immer der "kleinere« der Typen in den Typ des ,.größeren« konvertiert.
..
Initialisierung (Zuweisung) - Der Wert, der auf der rechten Seite einer Zuweisung angegeben w ird, wird automa tisch in den Wert konvertiert, der auf der linken Seite steht.
Integral - Promotion Um es gleich klar zu stellen, wir sprechen hier von einer automatischen Promotion, die der Compiler mit den Datenty pen mach en ,.kann", und nicht von einer automatischen Typumwandlung. Bei einer integralen Promotion wird beispielsweise häufig ein Typ in einen anderen integralen Typ umgewandelt. Ein Beispiel: char cwer t - OxOO : 11 vergleich von zwei int'Werten ! ! !
if ( cwert !- Ox80 ) [ 11 ...
Obwohl Sie hier einen Typ char verwenden. vergleich t das ausführende Programm zwei i nt-Werte. Dass dies so ist, liegt daran, dass vor der Ausftihrung einer Operation immer der Typ des Operanden angepasst wird. Diese Promotion wird immer so ausgefOhrt. dass der ,. kleinere« Typ auf den ,.größeren" erweitert wird - natürlich nur dann, wenn auch keine Werte verloren gehen. Es ist also tatSächlich so, dass die Typen char , uns i gned cha r , signed char , short , uns i gned short vom Compiler auf den Typ i nt erweitert werden können. Kann der Wertebereich nicht von int abgedeckt werden. so kann der Compiler
250
Typumwandlung
versuchen, eine Anpassung auf uns i gned i nt vorzunehmen. Dasselbe wird auch mit baal , enum und wcha r_t gemacht. Die folgende Tabelle (3.2) zeigt, welche integralen Promotionen der Compiler vornehmen kann. Auf der linken Seite steht der integrale Typ, der aULOmatisch in den integralen Typ der rechten Sei te umgewandelt wird, wenn der Wertebereich abgedeckt wird. Integrale PromotIon zu
char , slg ned char, unslgned c har
l nt
short , un slgned s hort
ln t
wchar~t
l nt . unsigned In t. 10ng , unslg ned 10 ng
enum
l nt. unslgne d lnt. 10ng , unslg ned long
bool
10t
class (Bitfelder)
unslgned l nt
Tabell e 3.2
integrale Promotion
GI e it kom m a- Pro motion
Neben der integralen Promotion gibt es noch die Gleitkomma- Promotion. Hier versucht der Compiler, ei nen Ausdruck vom Typ fl aat in einen Ausdruck vom Typ doubl e zu konvertieren. Integral-Typum wandlu ng
Zunächs t macht man sich um die integrale Typumwandlung recht wenig Gedanken . Sobald man aber dann die i nt-Wene mit und ohne si gned und un s i gned verwendet und vermischt, sind Fehler garantiert. Bei einer integralen Typumwandlung werden entweder i nt ·Ausdrücke in einen entsprechenden uns i gned- oder eben einen s i gned-Typ umgewandelt. Wichtig ist es zu wissen, dass eine Konverti erung eines signed-Ty p in einen unsignedTyp immer undef'iniert ist, das heißt das Ergebnis lässt sich nicht vorhersagen. weil es vom Compiler abhangt, wie dieser eine solche Konvertierung durchführt. Ein Beispiel hierzu: s i gned int s i wert - - 100 : i nt iwe r t - 100 : // iwert wird zu unsigned int konve r tiert unsigned int uiwer t l - iwert ; 11 unde fi nie r t - comp i lerabhangig
'5'
I
3·7
I
3
I
Gott lgkeltsbere lche, spezielle Deklarat io ne n und Typumwandlungen
unsigned int uiwert2 - siwert ; 1/ Ok , i wert spe i chert signed i nt -We r t
i nt iwert2 - siwert : 11 unde f i nie r t - comp i lera bhangig
i nt iwert3 - 4294967290 ; 1/ Ok
unsigned int uiwert3 - 4294967290 : Wenn der Ziehyp baal ist , wird e ntsprechend nach 0 oder 1 konvertiert: i nt iwe r tl - IOD ; i nt iwe r t2 - 0 : 1/ int - ) bool - Wird nach 1 konve r tie r t
bool bwe r tl - i wertl : 11 int ->bool Wird nach 0 konvert i ert
bool bwert2 - iwert2 : Gleitkomma-Typumwandlung
Natürlich kann auch der Typ einer Gleitkommazahl in einen anderen Gleitkommatyp umgewandelt werden (beispielsweise float nach double oder umgekehrt). Sofern allerdings der Wert einer Gleitpunkuahl nicht in eine r andere n ganz abgedeckt werden kann, ist der Wert auf der linken Seite der Zuweisung undefiniert - also auch wieder compilerabhängig. da sich nicht sagen lässt, was der Compiler damit macht. Jntegral - Gleit komma-Typu mwand I ung
Natürlich kann auch einem integralen Typ ein Gleitkommatyp zugewiesen werden. Allerdings kann der integrale Typ keine Nachkommastelle speichern, sodass diese Informationen verloren gehen. Lässt sich der Gleitkommawert nicht als integraler Wert darstellen, ist das Ergebnis wieder undefiniert und abhängig davon, was der Compiler damit macht. float fwert - 3. 14567 : 11 Gleitkomma-Integral-umwandlung - ) aus 3 . 14567 wi r d 3
int iwert - fwert : Der umgekehrte Fall, wenn ein integraler Typ in einen Gleitkommatyp umgewandelt bzw_ zugewiesen wird, ist weniger problematisch. Das Ergebnis entspricht dann ei ner Gleitkommareprasen tation.
252
Typumwandlung
8001-Typumwandlun g
Jeder Wert vom integralen Typ, vom Enumerations- oder vom Zeigenyp, kan n automatisch in einen bool -Wen umgewandelt werden. 0 wi rd somit zum bool Wen fa 1se und all es andere zum boa I-Wen t r ue. in t i wert l - 100 ; in t i wert 2 - 0: i nt * ip tr - 0 : 11 i nt eg r ale r Ty p zu bool a us 100 wird tr ue ( I) boa I bwe r t - i wertl ; i f( bwe rt -- true ) I cout « "i wertl ist nic ht O!\n ":
II i nt eg r ale r Typ zu bool a us 0 wird f alse ( 0) bwert - iwe r t2 : if ( bwe r t - false cout « "i wer t2 is t 0 1\ n" ;
If Zeige rt yp zu baal aus Nu llz ei ger wi r d f a l se (0) bwer t - i pt r; if( bwe rt -- f a l s e ) caut « "i ptr i s t ungOlti g\ n";
i ptr - &iwe r tl ; II Zeige r ty p zu bool aus gO ltige Adresse wi r d true ( I)
bwe rt - i ptr : i f( bwert -- t r ue ) I cou t « "iptr ist gOlti g \ n" ;
Zeiger- und Basisklassen-TypumwandJung
Es gibt auch zwei Fälle de r aUlomatischen Typumwandlung von Zeige rn, und zwar kann jeder Zeigerwert. der den Wert 0 enthält. auch in einen Nullzeiger (NU LL) umgewandelt werden. Außerdem kann jeder Zeiger eines bestimmten Typs in einen Zeiger vom Typ ~ oid umgewandelt werden. Hie rbei erhalt man ebenfalls die Anfangsadresse des Speicherobjekts. Auch bei den Klassen gibt es die Möglichkeit von Typumwandlungen im Zusammenhang mit der Vererbung. Aber hierauf wird erst in Abschnitt 4.7.7 eingegangen .
253
I
3·7
I
3
I
Gottlgkeltsberelche, spezielle Deklarationen und Typumwandlungen
3.7.2
Explizite Typumwandlung
Nachdem Sie erfahren haben wie vom Compiler automatisch implizite Umwandlungen vorgenommen werden, erfahren Sie jetzt, wie Sie als Programmierer selbst solche Typumwandlungen realis ieren können. Sie sollten sich allerdings hierbei immer vo r Augen halten, dass Sie den Compile r hiermit . zwingen«, etwas zu tun, was dieser eigentlich nicht so machen würde. Somit sollte auch klar sein, dass eine gedankenlose Typumwandlung das eine oder andere Problem mit sich bringe Dies beginnt bei falschen Ergebnissen und kann auch zum Absturz des Programms fUhren. [))]
Hinweis Bevor Sie sich auf das Abenteuer der expliziten Typumwandlung einlassen, sollten Sie sich zunächst immer überlegen, ob dies nicht zunächst mit der impliziten Umwandlung möglich ist, das heißt Sie sollten nur eine explizite Typumwandlung vornehmen, wenn dies nicht mehr anders möglich ist. Typumwandlung mit dem C-Cast
Zwar werden Sie in der Praxis kaum noch eine Ty pumwandlung mit den C-Casts machen, aber da es noch viel C-Code (oder eine Mischung aus C und CH) gibt, sollten Sie auch den C-Cast-Operator und dessen Verwendung kennen. Die Syntax hierzu ist recht einfach : (Typ) Aus dru ck ;
Hiermit wandeln Sie das Ergebnis des Ausdrucks in den entsprechenden Typ um: fl oat f wert : i nt i we r tl - 100 . iwe rt 2 - 33 ; II Ergb nis der Divis i on von Ganz zah len II umwandeln nach Gleitpun ktzah l en f wert - (float) iwe r tl I iwert2 ;
Ohne die Typu mwandlung mit dem C-Cast (floa tl würden Sie als Ergebnis den Wert 3 zurückbekommen. Die NachkommastelIen würden abgeschniuen wer· den. Was ist also so "sch li mm" am (-Cast-Operator? Verwenden wir hi erfür doch mal folgendes Beispiel: fl oat fwert : i nt i we r tl - 100 . iwe rt 2 - 33 : II Erge bnis der Di viSion von Ganzzahle n II umwande ln nach Gleitp unktzahlen f wert - (char) iwe rt l I i wert2 :
Eigentlich handelt es sich um dasselbe Beispiel, nur haben ich hier statt eines Casts nach fl oat einen Cast nach cha r gemacht - sinnlos, aber möglich.
254
Typumwandlung
I
3·7
Leider ist ein C-CaSt auch möglich, wenn das Ergebn is des rechten Ausdrucks grö· ßer als die Zielgruppe ist. Wird zum Beispiel versucht. ei ne fl oat -Berechnu ng in einem short-Wert mithilfe eines (-(asts zu speichern, so lässt sich dies auch mit »Gewalt« realisieren:
I
sho rt swe r t ; floa t fwe r tl - 1000000 . f wert2 - 33 : swe r t - (short) fwertl I fwert2 : Der Compiler dürfte (sollte) hier zwar eine Warnung ausgeben , aber leider gibt es immer wieder Programmierer, die das ignorieren. Also ist das . Problem .. an den (·(asts. dass man fast jeden belieben Datentyp ohne Sicherhei tsabfrage umwandeln kann. (++
hat hierzu noch eine alternative Notation des alten (-(asts mit eingeftihrt:
Typ (Ausdruck) : Diese Notation stellt eine lesbare Alternative zu den alten ( -(asts dar und wird wie ein Funktionsstil ausgeführt und auch so bezeichnet: fl oat f wert : i nt iwe r tl - 100 , iwer t 2 - 33 ; 11 Ergebnis de r Divi si on von Ganzzahlen 11 umwandeln nac h Gle i tpunktzahle n fwe r t - float(1wertl) 1 float(1wert2); Hinweis Man kann es nicht oft genug erwähnen. Wenn Sie ein C++-Projekt starten, sollten Sie die neuen Operatoren zur Typumwandlung verwen den. Neue C++-Typumwandlungs-Operatoren
In C++ wurden vier neue Operatoren zur Typumwandlung eingeführt. deren Verwendung erheblich sicherer ist als die von C-(asts (aber zunächst auch komplizierter). Hier die vier neuen Cast-Operatoren: const cast(ausdruck) static_cast(ausd r uck) dynamic_cast(au sd r uckl reinterpret_cas t {a usdruck) _TYP« ist hier der neue Ty p, in den Sie den Ausdruck konvertieren wollen. const_cast- Typumwandlung
Mit der Typumwandlung cons t _cas t wandeln Sie einen Typ mit den Qualiflkatoren cons t oder vo 1at ; 1e in einen Ausdruck desselben Typs um. nur ohne
'55
[« )
3
I
Gott lgkeltsbere lche, spezielle Deklaratio ne n und Typumwandlungen
den Qualifizierer - der Qualifizierer (cons t oder va 1at i 1e) wird also vorübergehend entfernt. Ein einfaches Beispiel: Eine Funktion gibt einen const ehar * Wert zurück, Sie wollen (oder können) den Rückgabewert aber nicht in einem eons t char ' Zeiger speiche rn , son dern in einem ein fach en ehar-Zeiger. Hierzu müssen Sie mit einem const_cast vorübergehend den Qualifizierer auße r Kraft setzen. Das fo lgende listing demonstriert ein solches Beispiel. Di e Fun ktion ,.substring« sucht im String ,.sl « nach einem Unterstring »s2« und gibt die Anfangsadresse auf diesen Unterstring zurück (falls vorhanden). Ohne den co ns t _cast würde sich das Programm nicht übersetzen lassen .
11 const_east . epp llinclude
const cha r * substr( eonst char * 51 . eonst char* s2 ) ! return st r str( 51 _ 52 l ; Natü rlich bedeutet das jetzt nicht, dass Sie consLcast auch auf echte Objekte anwenden können. die bei ihrer Deklaration als co ns t vereinbart wurden_ Der Grund ist ga nz einfach, da manche Compiler bei ei nem Typ , den Sie mit cons t qualifizieren , bestimmte Optimierungen vorneh men. Beispielsweise könnte der Compiler di ese Konstante in einem Read-only-Speiche r wie Flash oder EEPROM ablegen. Ein schreibender Zugriff darauf hat wieder undefi nierte Folgen . Folgendes ist also recht kritisch zu betrachten: eonst int ciwert - 100 : I1 Nicht erlaubt !! ! i nt iwe r t - const_cas t (ciwertl : Die meisten Compiler sollten hierbei sowieso eine Fehlermeldung ausgeben_
Typumwandlung
st atic_cast-Typ u m wan d lu ng
Ein s ta t i ecas t könn en Sie überall einsetzen, um zum Beispiel Ganzzahltypen und Gleitkommatypen hin- und herzukonvertieren. Natürlich bedeutet dies auch wieder, dass zum Beispiel bei einer Konvenierung von i nt nach char nur d ie Bits mit n iedrigen Werten übernommen werden. Auch hier wird der Rest wieder verworfen . double dwert .. 9 . 1; 11 explizit e Ty pumwandlung von double nach int int iwert .. stat i ecast(dwertl ; 11 explizite Typumwandlung von int nach float
floa t fwe r t .. statiecast(iwertl ; I1 Konve r tiert das Ergebnis in einen int -Wert
i nt i doppelt .. static_cast( dwert * dwert ) ; Der s t at i ecas t -Operator wird auch dazu verwendet, um typenlose Zeiger (voi d*) in einen beliebigen anderen Zeiger zu konvertieren. Beisp ielsweise: void ' vptr .. 11 zeige r- umwandlung von void * nach char*
char* c ptr .. sta ti c_cast(vpt r ) ; Natürlich kann der s ta t i eca s t -Operato r auch zur definierten Umwandlung von Objekten (genauer Objektzeigern) einer Klasse auf Obj ekte einer Basisklasse (die in einer Beziehung zueinander stehen) verwendet werden. rei nte rpret3as t - Typu mwand Iung
Die Typumwandlungen, die Sie nicht mehr mit conSCcast oder staticcast realisieren können, müssen Sie mit reinterp ret_cast umwandeln. Der reinter pr et_cast ist also fGr alle anderen Fälle. Wenn Sie sich die Fälle ansehen, die consLcast und staticcast bereits abdecken, so scheint reinter pr et_cast für die »exotischeren« Umwand lungen zuständig zu se in. Vorwiegend wird dieser Operator dazu verwendet, verschiedene Zeigertypen und Ganzzahlen in Zeiger bzw. umgekehrt zu konvertieren . Beachten Sie, dass bei einer solchen Umwandlung auch die Bitkette anders interpretiert wird, was zu einem komplett neuen Resu ltat führen kann. Dieser Operator gibt eine alte Sichtweise aus der ( -Programmiersprache wieder, indem eine Variable nur noch als eine Ansammlung von Bytes gesehen wird, die man beliebig interpretieren kann. Wenn Sie diesen Operator verwenden. machen Sie zumindest deutlich, dass Sie sich der Risiken bewusst sind. Ein solches Beispiel ist im Umgang mit Dateien gegeben. die im Binärformat gespeichert
257
I
3·7
I
3
I
Gottlgkeltsbere lche, spezielle Deklarat io ne n und Typumwandlungen
sind. Wenn Sie den Inhalt einer solchen Datei einlesen, verwenden Sie gewöhn[ich die Methode ist ream: : read ( ) aus der Standardbibliorhek: ist ream ::read(cha r
~p tr .
streams;ze n) :
Als erstes Argument müssen Sie einen cha r-Zeiger auf den Speicher übergeben , in dem der Dateiinhalt geschrieben werden soll . Wenn Sie allerdings eine Reihe von i nt-Werten binär ei nlesen wollen, benötigen Sie einen rei nte r pr eLcast , um den Zeiger »umzuinterpretieren«. Hier ein solcher Codeausschniu: 1nt* buffer ; 11 Puffer in der Größe r eserv i eren bu f fer - new i nl[size] : 11 Datei zuvor noch öffnen
if stream in(dateiname_zum_lesen_ ios :: in I ios :: Dinary) ; 11 Da t en lese n
i n . read( r el nterp ret_c ast( Duffer ), siz e* sizeo f ( i nt » ; Beachten Sie bitte außerdem, dass Sie eine teilweise plattformabhängige Typumwandlung vornehmen. dynam i c_cast-Typ umwand lu ng Die letzte Art der Typumwandlung, der dynamiccasl-Operator, steht in einem sehr engen Zusammenhang mit dem Ty pensyslem von C++ (RITI = Runtime Type Information System) und wird auch erst in Kap itel 4, wo die Klassen und Ve rerbungen näher behandelt werden, erläulert, da dieser Opera tor nur in diesem Zusallllllt:llhauK VOll Bt:de u t ullK ist.
258
Häufig wurden Sie bereits auf dieses Kapitel verwiesen. Hier wird jetzt auf modeme objektorientierte Programmierung eingegangen, die sich hervorragend dazu eignet, aufhohem Abstraktionsnivea u die Komplexität umfangreicher Projekte zu realisieren.
4
Objektorientierte Programmierung
4. 1
OOP- I
Bevor wir uns mit der objektorientierten Programmierung auseinandersetzen, soll nochmals das ,.neue« OO P-Konzept mit dem ,.alten« prozeduralen Konzept. wie dies zum Beispiel in der Programmiersprache C verwendet wird, verglichen werden. Bei der klass ischen prozeduralen Programmierung war es immer so, dass Daten und Funktionen keine Einheit bildeten (Abbildung 4.1). Probleme traten hierbei häufiger bei falschem Zug riff auf (z. B. nicht initialisierte) Daten auf. Musste dann noch ein Programm umgeschrieben bzw. erweitert werden, war der (Zei t-)Aufwand häufig enorm, und weitere Fehl er waren so gut wie sicher, wie es die Praxis in der Vergangenheit immer wieder bewiesen hat.
I
Funktion
I
I I Dalen
Funktion
I
I I'-------' Dalen I
Abbildung 4.1 Klassisches prozeduales Konzept
Bei der objektoriemierten Programmierung hingegen bilden die Objekte eine echte Einheit aus Daten und Funktionen. Wobei in der OOP die Daten als .Eigenschaften« und die Funktionen als "Methode« bezeichnet werden (Abbildung 4.2). Der Vorteil vom OOP-Konzept ist für einen AnHi.nger bzw. Umsteiger zunächst noch nicht nachvollziehbar, aber dies wird sich auf den nächsten Seiten noch ändern. Es lässt sich allerdings nicht vermeiden, dass die Komplexität im verlauf des Kapitels immer mehr zunimm t. Man sollte sich also Zeit neh men, di e OOP-
259
I
4
I
Objektorientierte Programmierung
Paradigmen zu verstehen - es lohnt sich auf jeden FalL Damit wird Ihnen auch das Erlernen von Java oder C# bzw. jeder anderen OOP~ Sprach e leichte r fall en. Eigenschaften
Eigenschaften
Methoden
Methoden
Objekt A
Objekt B
Abbildung 4.2 Objektorientiertes Konzept
4.1.1
OOP-Paradigmen
Unabhängig von der Programmiersprache bieten alle OOP-Prog rammiersprachen zur vollen Unterstützung folgende OOP-Paradigmen an: ,.
Datenabstraktion - Es werden fortgeschrittene Datentypen (in diesem Fal1 »Klassen«) defin iert, die di e Eigenschaften (Daten) und Methoden (Funktionen) von Objekten beschreiben - oder einfach ausgedrückt: Wenn man einen Typ i nt für Ganzza hlen oder ein doub 1e für Gleitpunktzahlen verwenden kann, warum sollte man dann nicht einen Typ »Festplatt.e« mit. seinen Fähigkeiten definieren und verwenden.
,.
Datenkapselung - Einzelne Elemente eines Objekts einer Klasse können vor einem fa lschen Zugriflvon außen geschü tzt werden. Zum Datenaustausch mit anderen Objekten besitzt jedes Objekt eine »offene« Schnittstelle - oder einfache r ausgedruckt: Wenn Sie einen Typ »Festplatte.,; haben und verwenden wollen, brauchen Sie nicht zu wissen, wie diese funk tioniert
,. Vererbung - Neue Objekte können aus bereits vorhandenen Objekte abgeleitet werden . Damit ,.e rbt« das neue Objekt Eigenschaften (Daten) und Methoden (Funktionen) des vo rhandenen Obj ekts - oder einfach ausgedrückt: Wenn Sie schon ein Objekt »Festplatte« haben, können Sie auch ei n Objekt ,. DVDBrenner«, »C D-Brenner« usw. mit den Methoden (Funktionen) »Dat en lesen«, »Daten s uch e n ~ , »Daten schre iben ~ usw. und den Eigenschaften (Daten) »Schnittstelle« (z. B. S-ATA oder SCSI), ~ D atendurc h satz« , »Farbe« usw. beerben und müssen di ese Angaben nicht immer wieder neu schreiben . ,.
Polymorphie - Die Vererbung ist auch auf ganze Fam ilien gleicher Objekte anwendbar. Je nach Typ des Objekts kann der Aufruf zur Laufzeit dan n eine andere Eigenschaft (Daten) verwenden oder ei ne andere Methode aufrufen oder einfach ausgedruck t: Wenn Sie ein Objekt "Tier« defi niert haben , dann haben viele Tiere eine ähnliche Eigenschaft (Daten), aber die Methoden
,60
Klasse n (fortgeschrittene Typen)
I
4·2
(Funktionen oder hier besser Fähigkei ten) dieser Tiere sind oft andere - die Kuh zum Beispiel macht »Muh..: und nicht »Wau« (es sei denn die Gentech niker arbeiten dra n).
I
Auch wenn sich die objektorientierte Programmierung in der Theorie erschre· ckend kompliziert anhört - in der Praxis erweist sie sich als sehr einleuchtend. Hinweis Vielleicht sind Sie manches mal etwas genervt, wenn die Autoren der Bücher immer als Objekte einfache Dinge aus dem l eben verwenden (wie Auto, Tiere, lebewesen, etc.) . Dies ist häufig der einfachste Weg, die OOP näher zu bringen. Natürlich könnte man als Objekt auch einen Typ »Otto-Motor" oder "Kernreaktor« definieren und verwenden, aber dies setzt dann noch weitere (Fach-1Kenntnisse aus diesen Gebieten voraus.
4.2
I«J
Klassen (fortgeschrittene Typen)
Dieser Abschnitt schließt an Abschnitt 2.8 über »Fortgeschrittene Typen " an. Zunächst werden Sie enttäusch t und vielleicht auch überrascht sein, dass Klassen nichts anderes als einfache Strukturen (st ruc t ) sind, die abgesehen davon auch Funktionen (Meth oden) enthalten dürfen und üblicherweise das schlüsselwort cl ass staU str uc t verwendet wird. Wenn Sie den Abschniu zu den Strukturen gut durchgelesen haben, wird Ihnen der Einstieg in die Welt der Klassen leichter fall en. Hinweis Zwar wird gewöhnlich das Schlüsselwort cl ass für die Klassen verwendet, aber Sie können ohne weiteres auch das Schlüsselwort struct verwenden. Wenn Sie in Ihrer Klasse nicht das Schlüsselwort pub 1 i c oder pri va te verwenden, besteht der Unterschied zwischen struct und c l ass darin, dass bei einer mit str uct definierten Klasse alle Daten publ ic (also öffentlich zugänglich) und beim Schlüsselwort c l ass alle Daten pr; va te (nur zur Klasse gehörende Daten) sind. Auf pub 1 i c und pr i va te wird noch eingegangen.
Der Begriff »Klasse« ist zunächst ein wenig unklar, aber wenn Sie drau ßen spazieren gehen und sich umsehen, sehen Sie Bäume, Häuser, Person en, Hunde und Autos. Auf den ersten Blick sehen Sie zum Beispiel einen Baum, aber wenn Sie diesen Baum etwas länger fixie ren, werden Sie feststell en, dass Sie anfangen, diesen Baum zu klassifizieren. Jeder kann noch zwischen einem Nadelbaum und einem Laubbaum unterscheiden (zwei Klassen von der Oberklasse Baum). Wer jetzt auch noch einige botanische Kenntnisse hat. kann die ei nzelnen Bäume noch genauer klassifizieren (z. B. Birke, weiße Baumrinde, grOne Bläuer, ca. 20 Mete r hoch usw.). Ebenso verläuft dies bei einer Person. die Sie sehen. Eine Person ist ein Lebewesen der Gattung Homo sapiens. Diese "Menschen« unterscheiden sich nun durch die Haarfarbe, die Augenfarbe, die Hautfarbe. die Größe usw.
,6,
[« )
4
I
Objektorientierte Programmierung
- so oder so ähnlich geht das menschliche Gehirn auch vor. Was zunächst unbedeutend erscheint, erg ibt beim näheren Betrachten mehr Sinn. 4.2.1
Klasse n deklariere n
Wir bleiben bei unserer Spezies, dem Homo sapiens, und spielen ein wenig Gott - oder denken Sie Gott ve rwendet noch eine prozedurale Sprache? Zunächst benötigen Sie einen Rahmen für eine neue Klasse . Eine leere Klasse ..Mensch .. sieht demnach wie folgt aus:
cl ass Mensch I 11 Hier kommen die Eigenschaften und ra hig ke iten hi n I,
Ve rgessen Sie das Semikolon am Ende dieser Klassendeklaration. kann dies zu seltsamen Fehlermeldungen fUhren. Für den Klassennamen gelten dieselben Regeln wie bei den Bezeichnern (Abschnitt 1.3.1). Allerdings sollte ein Klassenname immer eindeutig sein. Gewöhnlich verwendet man einen großen Anfangsbuchstaben. was zwar kein Standard ist, aber gängige Praxis.
Als Nächstes werden die Ele mente (auch Member genannt) zur Klasse hi nzugefügt. Zu den Elementen (Member) einer Klasse gehören die Eigenschaften (Daten) und die Methoden bzw. Fähigkeiten (Funktionen), die zwischen dem Anweisungsblock der geschweiften Klammern einer Klasse geschrieben werden. cl ass MensCh I char name[30] : unsigned int alte r: boal geschlecht : 110 - mannlich : 1 - weiblich I,
Hier haben Sie die Klasse »Mensch« mit den Eigenschaften Namen, Alter und Geschlecht versehen. In der Rea li tät hat ein ..Mensch« mehr Eigenschaften, aber das sollte zunächst genügen. Die Eigenschaften der Klasse werden auch als Klassendaten bezeichnet. Die Eigenschaften alleine machen noch keine Klasse aus und das Beschriebene entspricht immer noch dem Niveau einer einfachen Struktur. Die Klasse (hier ..Mensch«) besitzt im Grunde immer noch einige Fähigkeiten (Funktionen). Auch hierbei habe ich mich auf einige grundlegende Fähigkeiten beschränkt.
class Mensch ( " Eigenschaften der Klasse Mensch char name [3D) ;
,6,
Klassen (fortgeschrittene Typen)
I
4 ·2
uns i gned int a l ter : 110 - mannl ic h; 1 - weibl ich bool geschlec ht : 11 Fah i gkeite n (Me th oden) der Klasse Mensch void sehen( const cha r* objekt ) : void hoeren( const cha r* geraeus ch ) : vo i d riechen( const char * ge r uch ) : vo i d schmecken( const cha r* geschmack) : vo i d t asten( co nst char* objekt ) ; 11 Ei nen Mensc hen mi t all en Daten erzeugen vo i d erzeuget const char * n , uns i gned i nt a , boo l g ) : vo i d pr i nt e void ) ; vo i d test_geschlecht< vo i d ) ;
I,
Hier haben wi r uns bei der Deklaration der Klasse »Mensch« auf die Fäh igkeiten der fünf Sinne beschränkt. Natü rlich habe n Sie mi t dieser Klasse . Mensch« noch lange keinen Speicher reservierl - es handelt sich lediglich um e ine Anweisung für den Rechner, was die Klasse ,.Mensch .. alles darstellt. Der Rechner weiß hie rbei. wie vie l Speicherplatz e r fü r e in Objekt . Mensch« reservieren muss. Allerdings wird nur wieder Speicher Tur die Eigenschaften (Daten) reserviert. Wie Sie die Elementfunktionen (Method en, Fähigkeiten) definieren können, erfahren Sie weiter unten.
4·2.2
Elementfunktion (Klassen methode) definieren
Die Methoden (Funklio nen). die Sie in der Klasse deklariert haben, müssen Sie nun auch definieren , um eine Klassendefinition voll ständig zu machen. Theoretisch könnten Sie die Definition einer Elementfunktion auch gleich in der Klasse selbs t vornehmen. aber in de r Praxis ist dies doch eher unü blich: class Mensch r 11 Ei genschaft en der Klass e Mensch cha r name[30) ; unsigned int alter : 110 - m3nnlich : 1 - we i blich bool geschlecht : 11 F3higkeiten (Methoden) der Klasse Mensch 11 De f inition der Elementfunktion sehen in der Klasse
vo1d se hen( const cha r* objekt) { 11 Anwe1sungen für d1e Funkt10n I,
I
4
I
Objektorientierte Programmierung
In der Praxis wird gewöhnlich, wegen der Übersichtlichkeit, die Definition einer Elementfunktion außerhalb der Klasse vorgenommen . Allerdings genügt es dann nicht mehr, bei der Definition nur den Funktionsnamen anzugeben. Hierbei müssen Sie also zunächst den Klassen namen, ge fol gt vom Scope-Opera tor (BereichsoperalOr) ;; und dann den eigentlichen Funktionsnamen verwenden. Die Syntax hierzu: Typ Klassenname : : funktionsname( pa r ameter) I II Anweisungen de r Funktion
So te ilen Sie dem Compiler mil. dass die Funktion .. funktions name« eine Methode (Fähigkeit) der Klasse .. Klassen name_ ist. Würden Sie den Klassennamen nicht verwenden, so hätten Sie nur eine einfache globale Funktion definie rt. Bezogen auf die Klasse .. Mensch« und die Elementfunktion _sehen ~, sieht eine Definition au ßerhalb der Klasse wie folgt aus: void Mensch : : sehen( const char '" objekt) I I I Anwei sungen
Die Definition der Elementfunklion ..sehenO_ sieht bei der Klasse fo lgt aus:
_Me nsch~
wie
void Mensch : : sehen( const char * obj ekt ) I cou t « name « • sie ht " « obj ekt « · 'n " ;
Hier können Sie auch gleich erkennen, dass Sie auf die Eigenschaften (Daten) einer Klasse in nerhalb der Methode direkt zugreifen können - also ohne irgendwelche Scope-Operatoren. Dies funktionie rt natürlich auch, wenn Methoden einer Klasse an dere Methoden derselben Klasse aufrufen. Auch hierbei kann ohne weiteren Bezug auf diese Klasse zugegriffen werden. Somit kann man sagen, dass alle Eigenschaften und Methoden innerhalb einer Klasse ohne besonderen Bezug zueinander harmonieren, sodass Elementfunktionen (Methoden) ohne Umweg auf die Eigenschaften (Daten) und anderen Eleme ntfunktionen zugreifen können.
4.2.3
Objekte dekl arieren
Jetzt haben Sie zwar eine Klasse _Mensch« geschaffen, aber eine solche Klasse entspricht eher der Vorstellung eines Menschen. Und Gedanken an einen Menschen machen diesen noch nicht real. Sie müssen also ein Objekt (auch als Instanz einer Klasse bezeichnet) deklarie re n. Klassen und Objekte? Bevor Sie etwas durcheinander bringen: Eine Klasse ist
Klasse n (fortgeschrittene Typen)
zunachst der bloße Gedanke an einen Gegenstand. Wen n wir von ei nem Objekt selbst reden, dann handelt es sich um einen realen Gegenstand, der vor Ihren Augen ist (natürlich nicht wirklich, aber von der IT-Welt auf unser Gehirn proj iziert). Die Dekla ration eines Objekts verläuft wie eine beliebige andere Typendeklaration:
Klasse nname Objekt ; Auf unser Beispiel "Mensch« bezogen, sieht dies wie folgt aus:
Mensc h frau ; Natürlich können Sie auch mehrere Objekte deklarieren:
Mens ch frau . mann ; Mit einer solchen Deklaration wird jeut Speicher für die Eigenschaften (Daten) des Objekts .. frau « oder .. mann .. reserviert. Im Beispiel sind dies die Datenelemente .. name«, .. alter.. und -geschlecht... Natürlich gilt dies für jedes Objekt. Allerdings arbeiten beide Objekte mit denselben Methoden, mit denen der Code diese r Klassenmelhoden nur einmal im Speicher abgelegt wird - und das ist wiederum unabhängig davon, ob und wie oft ein Objekt dieser Klasse existiert. Wie auch bei den anderen Typen, die Sie bisher kennen , ist der Inhalt der Daten zunächst undefi niert. Wenn Sie das Objekt als st at ic oder gar global (nich t ra tsam) defi nieren, so wird der Inhalt auch hier stand ard mäßig mit 0 belegt. 4.2.4
Kurze Zusammenfassung
Eine Klasse besitzt mehrere Elemente (auch als Member bezeichnet). Diese Elemente (Member) eine r Klasse werden aufgeteilt in deren Eigenschaften (die Daten) und die Methoden bzw. Fahigkeiten <Memberfunktionen oder auch Elementfunklionen). Diese Elementfunktionen sind in der Regel die offenen Schni ttstellen (Interfaces) einer Klasse nach außen. über diese Funktionen werden die Eige nschaften (Daten) ei ner Klasse angesprochen . Wenn ein e Klasse erzeugt wurde, kann ein Objekt erzeugt werden. Ein Objekt wird auch als Instanz ei ner Klasse bezeichnet. Hier nochmals die allgemeine Syntax hierzu:
class Klass enname I 1/ Zus ammenfassung de r Klassenelemente - Anfang Eige nschaften eine r Kl asse typ date nl ; typ da ten 2;
11
I
4 ·2
I
4
I
Objektorien tiert e Programm ierung
/I Methoden bzw . Fa higkeiten e i ner Klasse typ funkt i onsnamel( parameter ) ; typ funktionsnam e2 { parameter) ;
11 Zusammen f assun g der Klassenelemente - Ende
I, 11 De fi nitionen der Methoden Typ Klassenname : ; funk ti onsname{ parameter) I 11 Anweis ungen der Funktion
11 Objekt deklarieren - Instanz eine r Klasse Klassenname objektname ;
4.2.5
private und public - Zugriffsrechte in der Klasse
Mit dem bisherige n Wissen Ober Klassen kann noch kein funktionierender Quellcode erstellt werden, denn bei der Deklaration von Klassen müssen auch die Zugri ffs rech te auf deren Elemente mit den Schlüsselwörtern pub l ie ooer pr i vate erteilt werden. Bisher s ieht unse re Klasse »Mensch" wie folgt aus : class Mensch [ 11 Ei ge nschaften der Klasse Mensch cha r name(30) : unsi gn ed int al t e r ; //0 - mann l ich ; I - wei bl ich boo l gesch l ec ht ; 11 Fah i gk e i ten (Me thod en) de r Klasse Mensc h void sehen{ const char * obj ek t ) ; void hoe r en{ const char* ge r aeusch ) : void r i echen( const char ' geruch ) : void schmec ke n{ const char* geschmack ) ; ~oid t a 5ten( (on~t c har* obje kt ) : 11 Ei nen Menschen mit al len Daten e r ze ugen void e rzeuge{ cons t char * n , unsig ned int a , bool g ) ; void pr int( vo i d ) ;
I,
Würden Sie jetzt ein Objekt deklariere n und versuchen, auf die Klassenele mente zuzugreifen, würde sich das Programm nicht übersetzen lassen. Der Grund sind die Standardzugriffsrechte, die jede Klasse ohne expliZite Angaben besilZt. Stan-
,66
Klasse n (fortgeschrittene Typen)
dardmäßig ist der Zugriff bei Klassen (im Gegensatz zu Strukturen (struct» von außen verboten. Der Grund solcher Rechte ist. dass der Zugriff auf die Elemente einer Klasse nur noch kontrolliert erfolgen soll. So werden Probleme wie die fal~ sehe Übergabe von Argumenten oder nicht initialis ierte Variablen bei richtiger Anwendung ausgeräumt. Der Standardzugriff aller Elemente ei ner Klasse ist pri vate . Dies bedeutet, die Eigenschaften und Methoden einer Klasse können nur innerhalb der Klasse angesprochen werden. Diese "Sperrung« gilt außerhalb der Klasse. Methoden einer Klasse können immer auf die Eigenschaften der eigenen Klasse zurückgreifen. Es leuch tet aber ein. dass die Standardeinstellung pr i vat e alleine für die Verwendung von Klassen sinnlos sind, da überhaupt nicht von außen auf die Elemente einer Klasse zugegriffen werden kann . Aus diesem Grunde muss mindestens für ein Element die Voreinstellung aufgehoben werden, um die Klasse von außen ansprechen zu können. Aufh eben kann man eine solche Sperre mit dem Sch lüsselwort publ i c. Alle Elemente. die das Zug riffsrecht pu bl ie haben. können von außerhalb der Klasse angesprochen werden. Hier die Syntax: cl ass Kl assenname I 11 Au f Eleme nt e kann nur in ne r hal b eine r Kl asse 11 zugegri f fen werden prh ate: 11 Eigensc haft en e i ner Klasse typ da t en }: typ da t en2 :
11 lugri ff von außen auf die Elemente mOgl ic h publ1 c : 11 Methoden bzw . F3higkeite n einer Klasse typ f unktio nsnamel( paramete r ) : typ f unktio nsname2( parameter ) ;
I,
Alle Elemente, die hier hinter private (Doppelpunkt muss angegeben werden) stehen, sind nur innerhalb der Klasse ..sichtbar«. Von außen kann darauf nicht zugegriffen werden. Diese Rechteerteilung gilt bis zum Ende der Klasse - oder bis zum Schlüsselwort pub 1i c (auch hier ist der Doppelpunkt dahinter wichtig). Ab dem Schlüsselwort pub1i c ist alles dahinter Geschriebene ..öffentlich« außerhalb der Klasse zugänglich . Es ist im Grunde egal. ob Sie zuerst die Klassenelemente
I
4·2
I
4
I
Objektorien tiert e Programm ierung
pub 1i c oder pri va te oder beide gemischt verwenden. Es ist auch egal. wie oft Sie publ ie oder privat in einer Klasse verwende n und ob Sie dabei Eigenschaften und/oder Methoden öffentlich oder privat zugänglich mache n. In der Praxis we rden aber gewöhnlich als Entwurfsmus ter die Eigenschaften einer Klasse als pri va te vorbehalten und die Methoden (Funktionen) als publ i c (öffentlich). Damit lässt sich der Zugriff auf die Daten (E igenschaften) über die Schni ttstellen (Elementfunktionen) kontrollieren. Um auf die Daten einer Klasse zuzugreifen, wird eine sogenannte Zugriffsmethode (Funktion) geschrieben. Hiermit w ird praktisch auf die priva ten Eigenschaften einer Klasse zugegriffen. In der Klasse »Mensch« ist dies im Beispiel die Zugriffsmethode »erzeugeO", Hiermit sieht unser Klasse ,.Mensch« bisher wie folgt aus: class Mensch I private : 11 Eige nsc haft en der Klasse MenSch eha r name[30J; unsign ed int alter ; bool geschlecht ; 110 - mannlich ; 1 - we i blic h 11 Methode nu r i nnerhal b de r Kl asse ans prechba r vo i d test_geschlecht( void ) ; publlc: 11 Fahig keite n (Me thoden ) der Kl asse Mens ch vo i d se hen ( const c har~ objekt ) ; vo id hoeren { eonst char~ geraeusch ) ; vo i d riechen{ const char* geruch ) ; vo i d sc hme cken{ const c har* geschmack ) ; void t aste n( const cha r * objekt ) ; 11 Ei nen Menschen mit al len Da t en erzeu gen void erzeuge( const cha r* n. unsigned int a . bool 9 ) ; void pri nt{ vo i d ) ; }
,
4.2.6
Zugriff auf die El emente (Member) einer Klasse
Wie Sie bereits erfahren haben, können Sie von außerhalb der Klasse nur auf die public-El emente zugreifen. Ausnahme hierbei ist , wie auch bereits erwähnt wurde. wenn Sie auf die Eigenschaften ei ner Klasse mit einer Elementfunktion de rselben Klasse zugreifen wollen . Dann sind kein e Besonderheiten zu beachten. Hier benötigen Sie weder den Scope-Operator noc h den Punkt- bzw. Pfe iloperato r. Es ist gängige Praxis. die Eigenschaften (Daten) einer Kl asse nicht direkt. sondern immer nur über ei ne Elememfunktion (Zugriffsmethode) anzusprechen.
,68
Klassen (fortgeschrittene Typen)
I
4·2
Hierzu wird in der Klasse ,.Mensch .. die Funklion »erzeugeO« als Zugriffsmethode verwen det: lJoid Mensch :: erzeuge{const char * n . unsigned int a . bool g) I // Hier kOnnten/sollten d i e übergebenen Parameter // OberprOft werden . bevor d i ese ve rwendet werden st r nc py( name . n . sizeof(name) · 1 ) ; name[sizeo f (name)] - ' \0 '; alte r - a : geschlecht - g ;
I
Ansonsten erfolgt der Zugriff auf die publ i c-Elemente e in es Objekts von außen wie schon bei den Strukturen mit dem Punkt- und Pfeiloperator. Direkter Zugriff mit dem Punktoperator
Wenn Sie ein Objekt defin ien haben, können Sie den Punktoperator verwenden um auf die pu bl i c-Elemente direkt zuzugreifen (wie scho n bei den gewöhnlichen Strukturen mit struct). Im Gru nde können Sie auf d ie Klassenelemente immer nur mit einem Objekt der Klasse direkt zugreifen. Dieser direkte Zugriff auf ein Elemem erfolgt d urch di e Bezeichnung des Objekts, gefolgt vom Pu nktoperator und dem Bezeichner des Klassenelements. Die Syntax : // Objekt deklar i eren Klassenname Objek t: // Zug r i f f a uf pUbl i c - EigensChaften der Klasse Objekt.Eige nschaf t // Zug r i f f a uf publ i c -Hetho de der Klasse Objekt . Met hode{ parameter ) : Im Beispiel der Klasse »Mensch .. können Sie folgen dermaßen einen Mensch erschaffen: 11 Objekt mann der Klasse Mensch Mensch man n: 11 Zugriffsmethoden aufrufen mann . erzeuge( "Adam ". 18 . 0 ) :
Im Beispiel wurde gleich die Zugriffsme thode zum Erzeugen eines Menschen verwen det: void Mensch : : erzeuge{const char · n . unsigned int a . bool g) I // Hier kOnnten/sollten d i e Obergebenen Parameter // Ober pr Oft werden . bevor d i ese ve rwe ndet werden st r ncpy( name . n . sizeof(name) -1 ) ; name[s i zeof{n ame)] - ' \0 ';
'69
4
I
Objektorien tiert e Programm ierung
alte r - a ; gesc hlec ht - g ; Natürlich können Sie theoretisch auch auf die einzelne n Eigenschaften einer Klasse von außen zugreifen. Aber wie bereits erwähnt, sollte man diese Daten immer als pr i va t e-Elemente kennzeichnen, sodass eine Methode dafü r ve ran twortlich ist , dass die richtigen Daten eingegeben werden . Bei der Klasse ,.Mensch« würde folgender Zugriff außerhalb e iner Klassenfunktion auf die Eigenschaften zu e inem Fehler führen: 11 Objek t man n der Klass e Me nsch Mensch ma nn; 11 Fehl er !!! name is t pr iva t e strncpy( mann . name , "Adam ". sizeof(mann . namel - l l ; mann . name[sizeof(namel] - ' \0 '; 11 Fehle r I!! a lt e r ist pri va te mann . alter - 18 ; 11 Fehle r ! !! geschlecht ist pr i vate mann . ges ch lec ht - 0;
pri va t e-Elemente haben ihre Bedeutung aber nicht nur beim Beschreiben einer Eigenschaft, sondern auch beim Lesen. So können Sie auch nicht einzelne pr; va t e-Elemente zum Beispiel mit cou t auf dem Bildschirm ausgeben: 11 Fehle r ! !! name ist private cout « mann . name ;
Wollen Sie trotzdem direkt auf die Eigenschaften der Klasse »Mensch« über das Objekt ,. mann « zugreifen, müssten Sie diese Daten als pu bl ic- Elemente kennzeichnen, was aber aus Designgründen der Sicherheit vermieden werden soll. Dadurch würd en die Vo rteile de r OOP wiede r aufgehoben. Mi t folgender Klassendeklaration könnten Sie auf alle Elemente (Eigenschaften und Methoden) von außen zugreifen : class Mensc h I 11 Alle Klassenelemente nac h außen sich t ba r -> 11 > schlechtes Programmdeslgn pu bl l c : 11 Eigensc haften der Klasse Mensch cha r name[3 0) ; unsigned int alter ; bool geschlec ht ; 110 - mannlie h ; 1 - weiblic h vo i d test_geschlecht( void ) ; 11 Fahig keiten (Methoden) de r Kl asse Mensch voi d sehe n( const c h a r~ objek t ) ;
27°
Klassen (fortgeschrittene Typen)
vo i d hoeren( co nst char* geraeusch ) ; vo i d riechen( const char· ge ruch ) ; vo i d schmecken{ const char ' geschmack ) ; vo i d tasten( co nst char * objekt ) ; 11 Ei nen Menschen mi t allen Daten erzeugen voi d erzeuget const char * n, uns i gned i nt a , bool g ) ; vo i d pr i nt e void ) ;
4 ·2
I
I,
Ein am Anfang häufiger Fehler bei der Verwendung von Objekten auf Klassen ist, dass vergessen wird, das Objekt anzugeben, worauf eine Klassenmethode aufgerufen werden soll: 11 Fehler!!! Sucht nach eine r gl obalen Funktion erzeuget) erzeuget "Adam" , 18 , 0 ) ;
Wenn es hierbei keine globale Funktion ..erzeugeO'" gibt, fUhrt dieser Funktionsaufruf zu einem Fehler beim Übersetzen des Quelleodes. Indirekter Zugriff mit dem Pfeiloperator
Wie auch schon bei den Zeigern auf Strukturen, können Sie auch mit Ze igern auf Objekte von Klassen arbeiten - was bei dynamischen Datenstrukturen durchaus gängig ist. Der indirekte Zugriff wird mit dem Objektzeiger, gefolgt vom Pfeil operator und dem Bezeichner eines Elements (Eigenschaft oder Methode) realisiert. Natilrlich muss man auch hi er, vor der Verwendung des Objektzeigers, zu nächst auf eine gilltige Adresse verweisen, bevor man diesen verwendet. Der Vorteil von Objektzeigern ist auch, dass man diesen jederzeit wieder eine andere Adresse zuweisen kann. Di e Syntax hierzu :
Objekt de klarieren Klassenname Objekt ; 11 Objek tze ig e r dekla r ie re n Klassenname* ObjektPt r; 11
11 GDltig Adresse an Obje ktPtr zuweisen ObjektPt r - &Objekt ; 11 11
I
Indi re kter Zugrif f auf pll blic -Ei gensc haften der Klasse ->
-> in der Prax i s nicht empfehlenswe rt
ObjektPt r ->Eigenscha f t 11 Indi rekter Zugrif f auf publ ic -Methode der Klasse Objek t Pt r ->Methode( pa rameter ) ; Bezogen auf unser Objekt .. mann ... von der Klasse »Mensch ... sieht der indi rekte Zugriff wie folgt aus:
271
4
I
Objektorien tierte Programmierung
11 Objekt mann der Kl asse Mensch Me nsch man n ; 11 Objekt zeige r Mensch* Mensc hPtr ; 11 GOI t i ge Adresse an MenschPtr zuweisen MenschPtr - &man n : 11 Zug r iffsmet hoden au f r uf e n MenschPtr - >erz euge( "Ad am" . 18 . 0 )
,
Bevor hierzu ein listing fo lgt, das das bishe r Beschriebene in der Anwendung demonstrien, soll noch erläuten werden, wie man auf enum-Eigenschaften zugreifen kann. Im Beispie l fäl lt auf, dass hier für die Erzeugung eines Menschen o fur männlich und 1 fii r weiblich verwendet wird. Hierzu könnte man einen besser leserlichen en um-Typ wie folgt einfüh ren:
11 0 is t mannlich , 1 ist we i bl i ch en um Ge sc hl ech t I MAN N. FRAU I ; J etzt kann man natürlich diesen enum-Da lentyp global de klarieren. Somit würde beim Erzeugen von ,.Adam .. Folgendes genügen:
11 Er zeu gt einen Mann mann . e rzeu ge( "Adam ". 18 . MANN ) ; Aber wenn Sie richtig OOP programmieren wollen, dann sollten Sie diesen enumDatentyp auch in de r Klasse deklarieren, wo dieser hingehön. Diesen enumDatenty p sollten Sie als publ ic d eklariere n, wenn Sie außerhalb d er Klassenmethoden darauf zugreifen wollen. Wollen Sie die enum-Konstanten außerhalb der Klassenmethoden verwenden, müssen Sie alle rd ings de n Klassen namen und den Scope-Operato r mit angeben.
[»]
Hinweis Natürlich sollten Sie hier auch nur den enum-Oatentyp als pub 1i c deklarieren. en um-Eigenschaften sollten Sie möglichst wie alle Klasseneigenschaften als pr i ~ a t e deklarieren. Somit sieh t der Zugriff des (pub l i c) e num-Daten typs in d er Klasse "Mensch« außerhalb de r l(lasscnmcthoden folgendermaßen aus : mann . e r ze ug e( "Adam ". 18 . Mens c h::MANN ) ; Hierzu nun das komplette Listing, das alle bisher besprochenen Aspekte in der Prax is demonstrien: 11 classl . cpp J/include (i os trea m> # i nclu de ( cs t r i ng>
272
Klassen (fortgeschrittene Typen)
I
4 ·2
using namespace std ; cl ass Mensch 1 pub li c : enum Gesc hlecht 1 MANN . FRAU I : priva t e : II Ei genschaften der Klasse Mensch char name [30] ; uns i gned in t alter ; bool ges ch lecht : JJ 0 - mannlich: I - weiblich void tes t_geschlechte void ) : publ i c ; 11 Fahigkeiten (Methoden) der Klasse Mensch void sehen( const cha r* obje kt ) ; void hoehren{ const char* geraeusch ) : void r i echen( cons t char * geruch ) : void schmecke n( const char* geschmack ) : void t as t en( canst cha r* objekt ) : 11 Ei nen Menschen mit allen Daten erzeugen ggf . Überladen void erzeuge{ cons t char * n .. "Unbekannt" , uns i gned int a .. 0 , boo I 9 .. FRAU ) : vo i d print( void ) :
int main(voidl 1 11 Mehrere Objekte deklariere n Mensch mann. frau , person. dummy : 11 Ei n Obje kt ze i ger Mensch* Menschptr : 11 Zugriffsmethoden au fru fen mann . erzeuge{ "Adam ". 18. Mensch : : MANN ) : frau . erze uge{ "Eva" . 18 . Mensch :: FRAU ) : 11 einige Aktionen (Kl~ssenmethodenl mann . sehen( "Eva" ) ; frau . sehen( "Apfel " ): frau .taste n( "Apfel " ) : mann . hoe ren( "Warnung von Gott" ) : frau.hoeren( "n i chts" ) : frau . riechen{ "Ap fel" ) : fr au . schmec ken( "Apf el" ) ; mann . hoe ren( "Schl ange " ) ;
o~fr ~ fen
I
4
I
Objektorientierte Programmierung
So gehts auch - dank Funk tionsüberladun g person . erzeugel ) ;
11
11 Ode r indi re kt mi t einem Zeiger Ober den Pfeil -Operato r Menschptr - &dummy : Menschptr->e rz euge( "Jü rg en Wolf ". 30 . Mensch : :MANN 1;
11 Ausgabe all er erze ug t en Mensc hen cout « "\nMeine e rzeugten Menschen bisher : \nO ; mann. pr intc) ; f ra u. pr intc) ; person . print( 1: Menschptr->print{ l : return 0:
/1 Hi er begin nen die De fi nit io nen der Klassenmet hoden
void Mensch : :e rzeuge(const cha r* n.un signed in t a.boo l gl I 11 Hi er kOnnt en/sollten die übe rgebenen Paramet e r II überp rü ft werden , bevor diese verwende t werden strncpy{ name , n. sizeo f {namel- l ) ; name[si zeo f{namel] - ' \0 ': alter - a ; geschle ch t - g:
void Mensch : :p rint ( void 1 ( cout « name « " " « al t er test_gesch l ec hte) : cout « " )\n" ;
« " Ja hre (" :
void MenSCh : ;t esCgeschlecht( void ) ( if( geschl ech t FRAU 1 ( cout « "weiblich " ; else I cout
«
"mannl i ch " ;
void Mensch : : sehen{ cons t char * objekt 1 { cout « name « " sieh t" « objekt « ' \n ';
274
Klassen (fortgeschrittene Typen)
I
4 ·2
void Mensch: : hoeren( const char* geraeusch ) I cout « name « " hOrt" « geraeusch « ' \n ':
I
void Mensch: : riechen( const char ~ geruCh) cout « name « " riecht " « geruch « ' \n ':
void Mensch: : schmecken( cons t char* geschmack 1 cout « name « " schmeck t" « geschmac k « ' \n ' :
void MensCh: : tasten( const char* objek t ) cout « name « " nimmt" « objekt « ' \ n' : Das Prog ramm bei der Ausführung:
Adam sieht Eva Eva sieht Apfel Eva nimmt Apfel Adam hO r t Warnung von Gott Eva hOrt ni chts Eva rie cht Apfel Eva schmeckt Apfel Adam hO rt Schlange Meine er zeugten Menschen bisher Adam 18 Jahre (m3nnl i ch) Eva 18 Jahre (weiblich) Unbekannt 0 Jahre (weiblich) JDrgen Wolf 30 Jahre (m3 nnlichl Ein Programm organisieren
Bei kleineren Projekten und Programmen, wie sie in d iesem Buch zum Beispiel verwendet werden , ist es meistens nicht nötig, das Programm zu organisieren. Aber ein größeres Projekt sollte man immer in mehrere Module aufteilen. Hierbei wird gewöhnlich die Defi nition in eine separate Headerdatei gepackt. Auch die Definition der Klassenmethoden so llte man in ein anderes Modul packen dafür sollte allerdings keine Headerdatei mehr verwendet werden, sondern eine we itere Quelldalei (.cpp). Da Sie bisher n ur mit der Klasse ,.Mensch" gearbeitet haben, soll an hand des Listings demonstriert werden, wie Sie ein solches Programm sinnvoll organisieren. Zunächst also die Headerdatei ,.mensch .h". wie die Klasse defin iert wi rd:
275
4
I
Objektorientierte Programm ierung
11 mensch .h lIinclude
lli fndef _MENSCH_H_ I/define _MENSCH_H_ us i ng na mespace std ; cl ass Mensch I pub 1i c : enum Gesc hl echt { MANN . FRAU I ; private : 11 Ei genschaften der Klasse Mensch char name[30] : uns i gned i ot a l ter ; baal geschlecht ; 110 - m
I,
I/end i
f
Zwar können Sie die Quelldateien und Headerdateien nennen wie es Ihnen gefallt, aber aus ersichtlichen Gründen wird die Headerdatei einer Klasse auch ähnlich wie die Klasse bezeichnet. Das sollte auch bei der Beze ichnung der QueJldatei mit den Klassenmethoden der Fall se in. Im Beispiel habe ich die Methoden der Klasse in eine Quelldatei namens »mensch.cpp« gepackt und definiert: 11 mensc h. cp p llinclude llinclude llinclude "mensch . h" using na mes pa ce std ;
276
Klassen (fortgeschrittene Typen)
11
I
4 ·2
Hi er begin nen die De finitionen der Klassenmeth oden
void Mensch : :erzeuge(const cha r * n. unsig ned int a .bool gJ ( 11 Hier kOnntenlsollten die Obergebenen Parameter 11 Ober prOft werden. bevor diese ve rwendet wer den strncpy( name . n. sizeo f !nameJ-l J; name[si zeo f!nameJ] - ' \0 ' ; alter - a ; geschl echt - 9; vold Mensch : :p rlnt ! vold ) ( cout « name « " " « alter « " Ja hre I" ; test_gesc hlecht! J: cout « ")\nO ;
void MenSCh: :tescgeschlec ht( voi d ) I 1f ( geschlec ht -- FRAU) ( cout « "we i bl i ch" ; else I cout
«
"mannl i ch" ;
void Mensch: : sehen! const ch ar" objekt) I cout « name « " sieht" « objekt « ' \n ' : void Mensch : : hoeren! const char* geraeusch ) ( cout « name « " hOr t" « geraeusc h « ' \ n'; void MensCh: : riec hen( const char* geruc h ) cout « name « " riecht · « geruch « ' \n ' : void Mensch : : schmecken! const char * gesc hmack) cout « name « " schmeckt" « gesc hm ac k « ' \n ': void Mensch : : tasten( const char* objek t ) cout « name « " ni mmt" « objekt « ' \ n' :
I
4
I
Objektorien tiert e Programm ierung
Zum Sch luss können Sie das eigentliche Hauptprogramm in eine Quelldatei schreiben. Oft wi rd, wie in diesem Beispiel auch, das Hauptprogramm einfach als ~ main.cpp « bezeichnet: 11 main . cpp l/i ncl ude "mensch . h"
int main{ vo id) { II Meh r ere Objekte deklarieren Men sc h mann , f rau . person , dummy ; II Ein Objektzeiger Mensch~ Menschp tr ; I I Zug r i ffsmethoden au f r ufen mann .er zeuge( "Adam" , 18 . Mensch ; ;MANN ) ; fr au . erze ug e( "Eva " . 18 . MenSCh :: FRAU l : II einige Aktionen ( Kl assenmethode nl aufrufen mann . sehen( "Eva " ) ; frau . sehen( "Apfel" l : fra ll . t aste n( "Apfel " ) : mann . hoe ren( "Warnung vo n Gott" ) ; f rall . hoe r en( "nichts" ) ; frall . riec hen ( "Ap fel" ) ; frau . schmec ken ( "Apfel" ) ; mann . hoe ren( ·Schlange " ) :
So gehts auch - dank FunktionsOberladung person .e rzeuge( ) ;
II
11 Ode r indi rek t mi t einem Zei ger Ober den Pfeil -Operato r Menschptr - &dummy ; Menschptr->erzeuge( "JO rgen Wo lf ". 30 , Mensc h: :MANN ) : 11 Ausgabe aller erzeugten Menschen cou t « "\nMe i ne erz eug t en Menschen bisher mann . prfnt() : frau .p rint( ) : pe rso n. print() ; MenSChpt r ->print() ; ret urn 0;
\n ";
Vorausseezung dafür, dass dieses Beispiel auch funktioniert, ist, dass die Headerdatei ~ mensch. h « im sclben Verzeichnis wie die anderen beiden Quellcodes lie-
2,8
Klassen (fortgeschrittene Typen)
I
4·2
gen (ansonSten müssen Sie die Inkludierung dieser Datei mit dem pfad anpassen). Des Weiteren müssen Sie selbstverständlich beim Übersetzen des Quellcodes auch die Datei .. mensch.cpp.. mit angeben oder, bei einer GUI, im Projekt mit aufnehmen. Hinweis Wie Sie dies in die Praxis übersetzen können, find en Sie auf der Buch-CD. Da find en Sie An leitun gen sowohl für Kommandozeilen-Compiler als auch für gängige Entwick lungsumgebungen.
[«)
Dadurch haben Sie praktisch die Definition der Klassen so getrennt, dass sich ihre Wiederverwendbarkeit erheblich erleich tert. Wenn Sie Ihre Klasse weitergeben wollen , aber den Que][code nicht freigeben wollen/dürfen. so reicht es auch aus. nur die Headerdatei(en) und die übersetzte Dbjektdatei (.objl.o) weiterzugeben. Der Anwender muss dann nur noch die Headerdatei einbinden und die entsprechende Dbjektdatei (.objl.o) zu seinem Projekt hinzuftigen. Beachten Sie allerdings, dass die Dbjektdatei plauformspezifisch ist. Hinweis Um die Übersichtlichkeit in diesem Buch zu wahren und nicht Platz für seitenweise Code zu verschwenden, w ird die Kl asse ..Mensch« - so lange diese noch verwendet wird - in die gezeigten drei einzelnen Dateien (main.cpp ; mensch.cpp und mensch.h) aufgeteilt. Falls Sie den Überblick verlieren, finden Sie zu m jewe iligen Kapitel auch das komplette List ing (mit allen Dateien) auf der Buch-CD.
4.2.8
Konstruktoren
Bisher haben Sie noch nichts über die ..sicherere.. Ini tialisierung von Variablen erfahren. womi t sich e ++ und die DDP immer rühmen . Ein Objekt vom Typ ,.Mensch.. besitzt zum Beispiel so lange keinen gültigen Wert, bis die Klasse nmethode ..erzeugeOa aufgerufen wird. Die Klassenmethode ..erzeugeO" trifft den Sachverhalt schon rech t gut, aber Klassen bieten hierfür einen speziellen Mechanismus an, der dafür verantwo rtl ich ist, dass ein Objekt bei der Definiti on automatisch mi t einer Initialisierungsfunktion mit Werten für die Eigenschaften belegt wird. Einfach Ausgedrückt: Es werden Standardin itialisierungen vorgege· ben, falls eine Methode mi t feh lenden Parametern aufgerufen wurde, und zusätzlich lassen sich noch verschiede ne MögliChkeiten einzeln behandeln . Damit ist garantien, dass Sie immer mit gültigen Werten arbeiten. Die Rede ist von den Konstruktoren, die im Grunde den Methoden einer Klasse recht ähnlich sind. Doch in zwei Dingen unterscheiden sich Konstruktoren von Klassenmethoden: ..
Der Name des Kon struktors ist derselbe wie der Name der Klasse
..
Der Konstruktor besitzt keinen Rückgabewert
279
[« J
I
4
I
Objektorien tierte Programmierung
I
Damit der Konstruktor auch zur Erzeugung von Objekten von außen zur Verfügung steht. erfolgt die Deklaration gewöhnlich im pub 1 i c-Bereich der Klasse . Hierzu müssen Sie zunächst die Deklaration de r Konstruktoren in der Klasse »Mensch", vornehmen. Die einzeln en Kon Slruktoren (im Beispiel vier) unterscheiden sich durch ihre Parameter. 11 mensc h.h llinclude lli f nde f _MENSCH_H_ lIdefine _MENS CH_H_ us i ng namespace std ;
cl ass Mensch I priva t e : 11 Ei genschaften de r Klasse Mensch char name[30] : uns i gned i nt al ter : bool gesc hlecht : 110 - mannl i ch : 1 - weibl i ch void t est_geschlech t { void ) : publ i c : 11 Dek l aratio n der Konst r ukto r en Men sch( const char .... unsigned 1nt. bool Men sch( const char .... unsi gned 1nt ) ; Men sch( const char'" ); Men sch( ) ;
);
enum Geschlec ht I MANN . FRAU I : 11 Fah i gkeiten (Methoden) der Kl asse Mensc h void sehen( cons t char * obje kt ) : void hoe ren { cons t char* ge ra eusch ) : void r i echen( const char * geruch ) ; void schme cken { const c har~ geschma ck ) : void t as t en{ const char * objekt ) : 11 Ei nen Menschen mit allen Daten erzeugen ggf . Überlade n vOiQ er zeuge! const cha r"" n- -unbe kannt- . unsigned i nt a- O. baal g- FRAU ) : vo i d pr inte void ) ; I,
lIendi f
,80
Klassen (fortgeschrittene Typen)
I
4 ·2
Ko nstrukto ren d ef i nieren
Nach dem Sie die Deklaration der Konstruktaren in der Headerdatei geschrieben haben, müssen Sie diese. wie die Klassenmethoden auch, definieren. Die Definition ist ebenfalls den Klassenmethoden ähnlich , nur dass hierbei zweimal der Klassenname verwende! wird, gefolgt von den Parametern: Klassenname : : Klassenname( Parameter) 1
Natü rlich können die Konstruktaren überladen werden . Daher müssen sich die Konstruktoren (sollten es mehrere sein) durch Ihre Parameter (genauer Signatur) un terscheiden. Dadurch lassen sich die Obj ekte auf viele verschiedene Arten initialisieren. In der Praxis sieht die Definition der Konstruktoren, bezogen auf die Klasse ..Mensdl". wie folgt aus: 11 mensch . cp p llinclude llinclude "mensch . h" us i ng namespace std ;
11 Defln1t1on der Konstruktoren - Anfang
Mens c h: :Hensch( const char* n. unslgned lnt a . bool 9 ) ( strncpy( name. n. slzeof(name)-l ); name[s1zeof(name)] - ' \0 ' ; alter - a ; geschlecht - g; )
Mensch: :Hensch( co nst char* n. unsigned lnt a) strncpy( name, n, s1zeof(name)-1 ); name[slzeof(name)] - '\0'; ~1ter
if(
(
- a;
a S 2 ) { 11 Gerade oder ungerade Zahl geschlecht - FRAU;
)
else ( geschlecht - HANN; )
)
281
I
4
I
Objektorien tierte Programm ierung
Mensch: :Hensch( const char* n ) ( strncpy( name, n, s 1zeof(name) - 1 ): name[sizeof(namel] - ' \0 ': 11 Heu geboren ... :-) alter - 0: 11 mehr Frauen braucht das land ; -) geschlecht - FRAU; )
11 Der Standardkonstruktor Men sch: :Hensch( ) { 11 Raben-Vater oder Raben-Mutter oder Tragödie 1 strncpy( name, wUnbekannt" , s1zeof(namel-l ) : name[sizeof(nue)] - ' \0 ': 11 Heu geboren ... :-) alter - 0; 11 a l s Ausgleich wieder ein Mann ; . ) gesch l echt - MAHH; )
11 Def1nition der Konstruktoren - Ende
void Mensch : :erzeuge(const cha r* n, unsigne d int a, boo l g) f 11 Hi er kö nnt enIsollten die Dbergebenen Parameter II ilberp rilft werden , bevor diese verwende t werden strncpy( name , n , sizeo f (namel -l 1: name[sizeof(name l ] - ' \0 ' : alt e r - a ; gesch l echt - g ;
void Me nsc h; :p ri nt ( void ) ( cout « na me « •• « al t er tesCgesc hl echt() : cout « " )\n " :
« • Ja hre (" ;
void Mensch : : tes Cg esc hl echtC void ) ( if ( geschlec ht -- FRAU) I cout « "we ibl i ch ": el se I cout
2a2
« "ma nn l i ch ":
Klassen (fortgeschrittene Typen)
I
4 ,2
void Mensch: : sehen( const char * objekt) I cout « name « " sieht" « objekt « ' \n ':
I
void Mensch: : hoeren( const char* geraeusch ) I cout « name « " hOrt· « geraeusch « ' \n ':
void Mensch: : riechen( const cha r * geruCh) cout « name « " riech t " « geruch « ' \n ':
void MensCh: : schmecken( const char* geschmack) cout « name « " schmeckt" « geschmac k « ' \n ':
void Mensch : : tas t en( const char * objekt) cout « name « " nimmt " « objekt « '\n ':
Die Definition von Konstruktoren überprüft die übergebenen Argumente auf ihre Gültigkeit und kopiert die entsprechenden Eigenschaften in das Objekt. Wenn eine Eigenschaft eines Objekts nicht ini tialis ien wurde, so sollte man diese mit einem Standard wert (Null wert) initialisieren . Neben den In itialisierungen von Variablen werden Konstrukwren auch für andere Dinge verwendet. Gängige und häufig gebrauchte Anwendungen sind das Reservieren von Speicher, das öffnen von Dateien oder das Vorbereiten von Daten usw, Konstruktoren aufrufen
Da ein Konstruk tor keinen Rückgabewert besitzt, können Sie diesen nicht wie ei ne normale lO Klassenmethode .. aufrufen. Daher kann der Aufrufnur implizit bei de r Definition des Objekts erfol gen . Die Syntax daher: Klassenname Objekt ( Parameterl i ste ) ;
Als Nächstes sucht der Compiler nach einem passenden Konstruktor mit der passe nden Signatur. Gibt es einen, erzeugt der Compiler ein Objekt mit den entsprechenden Argumenten, die bei der Definition mit übergeben wurden. Gibt es keinen passenden Konstrukwr, bricht der Compiler die Übersetzung ab und gibt eine Fehlermeldung aus.
283
4
I
Objektorien tiert e Programm ierung
Im Beispiel der Klasse ,.Mensch. wurden vier Konstruktoren verwendet. Somit stehen Ihne n folgende Möglichkei ten zur Verfügung, ein neues Objekt mit de fi ~ nienen Eigenschaften anzulegen: 11 main,CPP l/i ncl ude "mensch, h"
int main(void) 1 Mensch mannl( "Adam " , 18 . Mensch ; ; MANN ) ; Mensc h fr aul("Eva" , 18 , Hensch :; FRAU); Mensc h personl("JOrgen" , 30 ) ; Mensch person2("Fatma" ) ; II Verwendet Standardkonstruktor Mensc h person3 ; 11 Auch mOg l i ch '" Mensch person4 - " Math i lda " ; 11 NatO r lich geh t es auch Indi r ekt 11 Verwendet Standardkonstruktor Mensch person5 ; Mensch* Mensch Ptr = &person5 ; mannl . print() ; fraul . print() : personI.print() ; person2.print() ; 11 Ohne Konstru ktor , wOrde hier ein (( undefiniertes Verhalten vorl iegen person3 . print() ; person4 . print() ; 11 Indi r ekter Zugriff auf person5 Mensch Ptr->print() ; return 0:
Die Ausgabe des Programms: Adam 18 Jahre (mannlieh) Eva 18 Jahre ( weiblich ) JOrgen 30 Jahre (mannl i ch) Fat ma 0 Ja hre (weib lich) Unbekannt 0 Jah r e (mannl i ch) Mathilda 0 Ja hre (weiblich) Unbekannt 0 Jah r e (mannl i ch)
Klassen (fortgeschrittene Typen)
I
4·2
Natürlich decken diese Konstruktoren noch lange nicht alles ab. Wenn Sie zum Beispiel ein Objekt mit der Eigenschaft ,.MANN " wie folgt erzeugen wollen
Mensch pe r son6{ Mensch : : MAN N ) ;
I
bekommen Sie eine Fehlermeldung zurück. da es keinen passenden Konstruktor dazu gibt. Aber so was ließe sich problemlos nachrüsten: 11 Deklaration in mensch . h Mensch( bool ) : 11 De f ini t ion i n mensch . cpp Mensch : :Mensc h( bool 9 ) ( 11 Raben-Vater oder Raben-Mutter oder Tr agödie? strncpy( name , "Unbekannt" , sizeof(name)-l ) ; name(sizeof(name») .. ' \0 ': /I Neu geboren __ . : -) alter " 0; 11 Geschlecht bestimmt der Anwender ... ; -) geschlecht .. g:
Sicherlich ist Ihnen auch folgendes Objekt ins Auge gestochen:
Mensch person4 .. "Ma t hilda ": Dies können Sie verwenden, falls Sie ein Objekt mit nur einer Eigenschaft initialisieren und natürlich ein entsprechender Konstruktor vorhanden ist. Das bedeutet auch, dass Sie fo lgendermaßen ein Objekt definieren können
Mensch person6 .. Mensch : ;MANN : wenn Sie den beschriebenen Konstruktor
Mensch{ bool ) : deklariert und defi niert haben . Hinweis Die verwendung mit der Initialisierung zwischen den Klammern ist übrigens nicht auf Klassen beschränkt, sondern lässt sich theoretisch auch auf die Basisdatentypen anwenden: i nt. i WRr t. ( S) anstatt i nt. i wprt. .. S
Standardkonstru ktar
Konstruktoren ohne Paramete r heißen Standardkonstruktoren
Verwendet Stand ardkonstrukto r Klassenname Obje kt :
11
285
[« )
4
I
Objektorien tiert e Programm ierung
In der Praxis schreibt man diesen Standardkonstruktor gewöhnlich so. dass alle Eigenschaften einer Klasse mit Standard werten versehen werden. Im Beispiel zuvor sah die Definition des Standardkonstruktors wie folgt aus: 11 Deklaration des Standard konstruktors in me nsch . h Mensch ( ) : 11 De f initio n des Standardko nst ruk tors in mensc h. cpp Mensch : :He nsch ( ) I 11 Rabe n· Va ter oder Rabe n· Hu tte r oder Tragö di e? st rncpy{ name , ·Unbekann t ", si ze of(name )'l ) ; name[sizeoff na me») - ' \0 ': 11 Neu ge boren ... : -) alter - 0 : 11 als Ausgleich wiede r ei n M ann ; . ) geschlec ht - MANN ;
Im Hauptprogramm wird di eser Standardkonstruktor zum Beispiel mit 11 Ve rwend et St andard konstr ukt or Mensch pe rson3 :
verwendet. Sofern Sie keinen Standardkonstruktor verwenden (was man in der Praxis allerdings immer machen sollte), stellt Ihnen der Compiler automatisch einen solchen zur Verfugung. Diese r Standardkonstruktor (ebenfalls von außen erreichbar als publ i c) beschreibt allerdings nicht die Eigenschaften einer Klasse mit Werten. Beachten Sie auch. dass der Compiler Ihnen keinen Standardkonstruktor mehr zur Verfügung stellt. sobald Sie mindestens einen Konstruktor verwenden. und Sie sich selbst darum kümmern müssen. Würden Sie den Standardkonstruktor im Beispiel entfernen, so würde folgende Objektdefinition zu einem Compilerfehler fuhren:
Mensch pe rson3 : Dies häue allerdings wieder den Vorteil. dass Sie bei der Definition eines Objekts eine Initialisierungsliste mit angeben müssen.
4.2.9
Destruktoren
Der Destruktor wird genauso wie der Kon struktor automatisch aufgerufen. mit dem Unterschied, dass der Destruktor nicht bei der Definition aufgerufen wird. sondern beim ,.Zerstören« des Objekts. Der Konstruktor wird gewöhnlich für
,86
Klassen (fortgeschrittene Typen)
I
4 ·2
Aufräumarbeiten wie das Freigeben von dynamisch reserviertem Speicherplatz verwendet. Man spricht auch vom Abbauen eines Objekts. Destruktor deklari eren
Der Destruktor beste ht. wie auch der Konstruktor. nur aus dem Klassennamen allerdings mit einem vorangestellten - (Tilde-Zeichen): " Deklaration eines Destruktors -Klassenname( ) : Also gilt auch für den DeSlruktor, dass dieser keinen Rückgabewen emhält, und außerdem besitz t dieser niemals einen Parameter, im Gegensatz zum Konstruktor. Darüber hinaus muss der Des truktor immer im publ ic -Bereich einer Klasse stehen. Destruktor defini eren
Sofern Sie keinen Destruktor definieren, erzeugt der Compiler eine Standard version davon (im publ ic-Be reich). Dieser Destruktor zerstört die Eigenschaften eines Objekts in umgekehrter Reihenfolge (da Stack) der Erzeugung. Dies gilt auch wenn die Eigenschaft selbst ein Objekt ist - nur wird dann der Destruktor (implizit oder falls vorhanden explizit) verwendet. Wenn Sie einen Destruktor selbst explizit definieren, wird dieser äh nlich w ie der Konstruktor definiert. Die Syntax hierzu: " Definition e i nes Destruk tors Klassen na me : :-Klassenname( ) { // Anweisungen In de r Klasse ,.Mensch .. wird die Deklaration des Destruk tors im publ i c-Bereich vorgenommen. Hierzu die Headerdatei ,.mensch.h .. ein wenig gekürzt: 11 mensch.h If i ncl ude If i fndef _MENSCH_H_ lidefine _M[NSCIUI_ uS i ng namespace std :
class Mensch { pr i vate : " Eigenschaften der Klasse Mensch cha r name[30) : unsigne d int alter ; baal geschlec ht : 110 - mannlich : 1 - weiblich
I
4
I
Objektorien tierte Programm ie rung
void test_geschlechte void ) ; pub 1 i c :
11 Dekla ration der Konstruktoren Mensch{ const char *, unsigned int. bool ) : Mensch{ const char* , unsigned int ) ; Mensch{ const char* ) ; Mensch( baal ) : Mensch( ) ; 11 Dekl arat ion der Des truktoren - Men sc h( ) ;
enum Geschl echt { MANN, FRAU I ; 11 Fahigkeiten (Methoden) der Kl asse Mensch 11 Dekl arationen der Methoden sehen{) , heeren() , 11 r iechen{) , schmecken() . tasten() hie r her schreiben
void printe void ) : lIendi f " Die Deflnition des Destruktors in der Quelldalei .. mensch.cpp« kann demnach wie folgt aussehen (auch gekürzt): 11 mensch.cpp llinclude lI i nclude Il i nclude "mensch . h" using namespace std ;
11 De f inition der Konstrukteren - An f ang 11 Hier kommen die Konstruktoren hin
11 De f inition der Konstrukto re n - Ende 11 De fin i tion de s Destruktors
Mens ch: :- Mensch ( ) { cou t « name « "1st von uns gegangen\n"; }
11 Hi er kommen die restlic he n Methoden hin
,88
Mehr zu den Klass enmethoden (Klassenfunktionen)
I
4 .3
Oestru ktor a ufrufe n (implizit ) Hierzu nun die ma i n-Funktion, die den Destruktor in der Praxis demonstrieren soll: 11 main . Cpp llinclude "mensch . h"
int main(void) I Mensch pe rso nl( "Adam" . 18 , Mensch ; ;MANN ) ; I Mensch person2< "Justus ". 30 . Mensch ;: MANN ) : staUc Mensch person3 ( "Johanna ". 100 . Mensch : : FRAU ) ; cou t « "Der Rest geht nach dem Programmende\ n": return 0; Das Programm bei der Ausführung: Justus ist von uns gegangen Der Rest ge ht nach dem Progr ammende Der Destruktor kann nicht explizit aufgerufe n werden. sondern wird immer implizit verwendet, wenn sich der Gühigkei lsbereich des ObjektS beendet. Für Objekte gil t dasselbe, was in Kapitel 3 für den Gültigkeitsbereich von anderen Typen beschrieben wurde. Ist ein Objekt ..
loka l deklarie n und gehört nicht zur Speicherklasse sta t ic , wird dieses am Ende des entsprechenden Anweisungsblocks zerstört.
..
global oder stat;c deklariert. wird es gewöhnlich am Ende des Programms zerstört (weshalb im Beispiel auch keine Ausgabe mehr auf dem Bildschirm erfolgte).
4.3
Mehr zu den Klassenmethoden (Klassenfunktionen)
In den nächsten Abschnitten soll auf typische Themen zu den Klassenmethoden eingegangen werden - was natürlich voraussetzt, dass Sie den Abschnitt zu den Klassen verstanden haben.
4 ,3.1
Inline-M ethoden (ex plizit und implizit)
Wie Sie bereits bei den Funktionen erfahren haben, stellt ihr Aufruf keinen unerheblichen Aufwand da (Stichwort: Sta cke-Frame»~. Dasselbe gil t auch in Bezug auf
289
I
4
I
Objektorien tierte Programm ierung
die Klassenmethoden, die im Grunde auch nur Funktionen (für e ine Klasse) sind. Im Gegensatz zu "gewöhnlichen« Funktionen ist der Aufwand, der fur den Aufruf von Klassen me thoden betriebe n werden muss, noch erheblich höher. Auch hier steht Ihnen, wie schon bei den Funktionen , die Möglichkeit zur Verfugung, eine Klassenmethode als ; n 1; oe zu de finieren (siehe Abschnitt 1.10.6). Damit könn en Sie d ie Vorte ile der Klassen wieder nutzen , o hne dass diese zu einem schlechten Laufzeitverhalten beitragen . Zur Verwendung von ; n I; ne stehen Ihnen bei de n Klassen zwei Möglichkeiten zur Verfügung, die explizite und die implizite. Inline implizit
Wenn kleinere Funktionen gleich innerhalb der Klasse definiert werden, werden sie auwmatisch zu i nl i ne- Klassenmethoden, auch wenn das Schlüsselwort in li ne nicht mit angegeben wird. Na türlich können Sie di ese M ethoden trotzdem, zur besseren Lesbarkeit, extra mit; nl; oe definieren. Zum Beispiel die Klasse ,.Mensch",: 11 mensc h . h llinclude <;ostream) 11; f ndef _MEN SCH_H_ lIdefine MENSCH_H_ us i ng namespace std :
class Mensch I pr i vate : " Eig e nschaften der Klasse Mensc h (har name[30) : uns ; gned i nt alter : baal gesc hlecht : 110 - mannl i ch : 1 - weiblich void t est_geschlech t{ void ) : pub 1i c : 11 Dekla r ation der Koostruktoren Mensch{ eons t ehar ' . un s ig ned i nt . boo l Mensch( eons t cha r* . uns igned i nt ) Menseh( eonst ehar * ) Mensch( baa l ) Mensch( ) 11 Dek l aration der Oest ruk toren -Mensch( ) :
,
,
,
en um Geschlecht ( MANN , FRAU I ;
290
,
),
Mehr zu den Klass enmethoden (Klassenfunktionen)
Fahigkeiten (Methoden) der Klasse Mensch 11 Di e folgenden Methoden im pliz i t als inline def i nieren vo1d sehen( const char· obJekt) ( co ut « name « .. s1e ht .. « objekt « ' \ n':
I
4.3
11
vo1d hoe ren{ const char · geraeusch ) ( cou t « name « .. hllrt " « geraeusch
I
« . \n ' ;
)
vo1d r1 echen{ const ch ar· ge ru ch) ( cou t « name « " r1 ec ht .. « geruch
« . \ n' ;
vo1d schmecke n{ const char· geschma ck) ( cou t « name « " schmeckt" « geschmack
« '\n';
)
vo1d tasten( const char· objekt ) ( cou t « name « " n1mmt .. « objekt
« '\n':
11 Einen Menschen mit allen Daten erzeugen ggf . Überladen void er zeuge{ const cha r* n-·Unbekannt" , unsigned int a- O, boo 1 g-FRAU ) : void pr inte ~oid ) ;
I,
lIendi f In diesem Beispiel gelten die Klassenmethoden »sehenO«, »hoerenO«, »riechenO ~ , »schmeckenO ~ und »tastenO« implizit als i nl ; ne . InHne explizit
Klassenmethoden können natürli ch auch explizit als i n1 i ne definiert werden. was durchaus die gängigere Methode darstell t Hierbei geht man genauso wie schon bei den i nl i ne-Funktionen vor und setzt bei der Definition vor der Klassenmethode das Schlüsselwort i nl i ne. Auf unser Beispiel der Klasse »Mensch ~ bezogen müssten Sie hierzu in der Datei »mensch.cpp« vor den entsprechenden Methoden das Schlüsselwort i n1 i ne setzen (na türlich setzt dies voraus, dass Sie in der Headerdalei »mensch.h« noch keine Definition, wie mit »Inline implizit« gesehen. vorgenommen haben). Hier das Beispiel dazu:
'9'
4
I
Objektorien tierte Programmierung
11 mensc h.cpp llinclude llinclude lli ncl ude 'me nsch . h' us i ng namespace std : 11 .. , alles wie gehabt - gekQrzt
1nl1ne void Mensch :: sehen{ co nst char* objekt) cout « name « ' sieh t ' « objekt « ' \ n' :
1nline void Mensch :: hoeren{ const char* geraeusch ) I cout « name « ' hör t ' « geraeusch « ' \n ':
1nline void Mensch : : riechen{ const char* ge r uch) cout « name « . riecht· « geruch « ' \n ':
1nline void Mensch : : schmecken{ const char* geschmack) cout « name « ' schmeckt' « gesc hmack « ' \n ':
i nl 1ne void Mensch : : tasten( const char * objekt) cout « name « ' nimmt' « objekt « ' \n ':
inline-Kon struktore n und -Destru ktore n Natürlich können Sie, wie auch die Klassenmethoden, die Konstruktoren und Destruktore n als inl ine definieren . Und das sowohl explizit als auch implizit. nur muss auf die "Merkmale .. von Konstruktoren und Destruktoren geachtet werden .
[»]
Hinweis Natü rlich gilt auch bei der Verwendung von in 1 i ne bei den Klassenmethoden bzw, den Konstruktoren/Destruktoren dasselbe wie bei den Funktionen , Mit dem Schlüsselwort i nl i ne (wenn es explizit erfolgt) oder mit der beabsichtigten impliziten i nl i ne-Definition fordern Sie den Compiler nur auf, diese wenn möglich als in 1 i ne einzubauen. Es handelt sich also um keine Vorschrift. Aber in der Praxis sind die Compi ler bereits selbst so »i ntelligent. und versuchen, eigenständig Klassenmethoden bzw. Konstruktoren/ Destruktoren als in 1 i ne einzubauen, besonders wenn eine Methode in der gleichen Datei verwendet wird.
292
Mehr zu den Klassenmethoden (Klassenfunktionen)
4 .3 .2
I
4 .3
Zugriffsmethoden
Das Thema der Zugriffsmethoden wurde bereits kurz erwähnt. Diese dienen dazu, um auf die pri va te-Eigenschaften (Daten) einer Klasse zuzugreifen. Man kann diese " Datenkapselu n g ~ zwar unterlaufe n. wenn man die Eigenschaften als publ i c deklariert, aber dies ist wohl eher nicht im Sinne der OOP. Daher werden in der Praxis die Zugriffsme thoden verwendet, die im Prinzip auch nur einfache Methoden sind, die auf die Eigenschaften einer Klasse zugreifen können. Im Beispiel der Klasse ..Mensch .. wurde die Methode »erzeugeO" verwendet, um die Eigenschaften der Klasse mit einem Wert zu initialisieren. Was hier allerdings noch fehlt. sind Methoden. um einzelne Eigenschaften dieser Klasse abzufragen bzw. zu (über)schreiben . In der Praxis wird hie rfü r oft die Syntax get_eigenschaft{) ; verwendet. um Daten zu holen (:get-> und set_eigenschaft() ; zum (über-)Schreiben bzw. Setzen (= set-> von Daten . Im Beispiel der Klasse ..Mensch« könnten Sie somit das Alter einer Person mit Mensch person ; person . geLa 1ter{ ) ; ermiueln und mit person .set_alter{ 23 ) ; (über-)schreiben bzw. setze n. Bezogen auf die Klasse ..Mensch.. müssen Sie zunächst d ie Headerdatei um die Deklarationen der neuen Zugriffsmethoden erweitern: 11 mensc h.h lIinclude
lIi f ndef _M ENSCH_H_ lIdefine _M ENSCH_H_ us i ng namespace std ; class Mensch ( pri va te : 11 Eigenschaften der Klasse Mensch char name[30] ;
293
I
4
I
Objektorien tierte Programm ierung
uns ig ned i nt al ter ; bool ge sch le ch t : /10 - mannl i ch : I - weibl i ch void t esCgeschlecht{ void ) ; pub 1j c : 11 Dekla r ation der Konstruktaren Mensch{ const char *, uns igned int . bool ) : Mensch{ const ehar* . uns i gned int ) ; Menseh( eonst ehar* ) ; Mensch{ bool ) : Mensch{ ) ; 11 Dekla rat ion der Des t ruk t oren -Mensch { ) : enum Geschlecht I MANN . FRAU I : 11 Fahigkeiten (Methoden) Klasse Mensch ) sehen( cons t char· obje kt void void hoe ren { eonst char * geraeusch ) void r i echen( const char * geruch ) void schmec ken { const char* ge schmack ) void t as t en{ eonst char * obje kt ) 11 Ei nen Menschen mit a11en Daten er ze ugen ggf , Übel'laderl void e rze uge t const cha r' n- "Unbeka nnt" . unsigned int a- O. boo I g- FRA U ) ;
'oe
,
,
,
,
,
11 Zugrlffsmethoden zum Abfragen der Elgenschaften const char* get_name< vold ); unslgned lnt get_alter( vold ) ; bool get_geschlecht{ vold ); 11 lugrlff$methoden zum Setzen der Elgen$choften vold set_na me< const char* n ); vold set_alte re unslgned lnt a ) ; vold set_geschlechte bool g ); 11 Ausgabe alle r Eigenschaften
void pr inte void ) ; I,
lIendi f Die einzelnen Zugriffsmethoden werden dann in der Date i folgt definiert (wie immer gekürzt): 11 menseh . epp #i nclude Ifi nelud e <estr i ng> #i ncl ude "mensc h.h"
294
»mensch . cpp ~
wie
Meh r zu den Klassenmethoden (Klassenfunktionen)
I
4.3
using namespace std ; 11
hie rh i n al l es wie gehabt
I
11 Zugr1ffsmethoden zum Abfragen der E1genschaften const char* Mensch ;; get_name( void ) re t u r n name:
uns i gned int Mensch ; ; get_alter( vo i d ) I return a1 ter :
boo l Mensch : ; g eL geschlecht( void ) f retu r n geschlecht ;
11 Zugr1ffsmethoden zum Setzen der E1genschaften void Mensch:: set _name( const char* n ) strncpy( name. n . sizeo f (n ame) - l ) ; name[si zeo f(name)] - ' \0 ';
voi d Mensch: : seLa lter( uns i gned i nt a ) I alt er - a :
vo i d Mens ch: : seLgeschlecht( bool 9 ) I gesc hlec ht ~ g :
Und wieder eine main- Datei, die die hier deklarierten und defi nierten Zugriffsmethoden in der Praxis bei de r Ausführung zeigen soll : 11 main , CPP "me ns ch _h"
11; ncl ude
int main( vo id) I Mensch pe r son l( " Adam" . 18 . Mensch: : MANN ) : Mensc h pe r son2 : personl.p rin t() : person2 .pr int( ) :
295
4
I
Objektorien tiert e Programm ierung
person 1. se t _a 1ter ( 20 ) : person2 . set_name( "Eva " ) ; person2 . set_alter( 19 ) ; person2 . set_geschlecht( Mensch ; : FRAU ) ; personl . prinU) ; person2 . prinU) ; i f(
personl. geL alt er() > person2 .ge L alter() ) I cout « personl . get_name() « " ist alte r als " « person 2. get_name() « "\n " ;
else cout
« person2.get_name() « " ist alte r als " «pe rson l . ge t _n ame ()« ' \n ':
re turn 0; Das Programm bei seiner Ausführung:
Adam 18 Jah re (mannlieh) Unbekan nt 0 Jah r e (mannlichl Adam 20 Ja hre (mannlic hl Eva 19 Ja hre (we i bl i ch) Adam ist alter als Eva Zunächst erscheinen einem die Zugriffsmethoden als enormer Mehraufwand, und der Quelleode wird dadurch auch n icht »kürzer«. Aber in der Praxis überwiegen die Vorteile eindeutig. Zum einen können Sie jetzt mit solehen Zugriffsmethoden fehlerhafte Zugriffe auf Variablen von vornherein auszusch ließen. Damit können Sie sicher sein , dass Objekte stets mit gü ltigen (definierten) Werten arbeiten. Und mit Zugriffsmethoden werden die implementierungsabhängigen Details einer Klasse versteckt Man kann jederzeit die Klassenmethoden verändern (etwa die Darstell ung von Daten), ohne dass eine Zeile des Anwendungsprogramms selbst geändert werden muss, dies gilt natürlich nur solange die öffentJichen Schnittstellen (die ja die Methoden sind) unverändert bleiben. 4.3.3
Read-only-Methoden
Klassenmethoden, die nur lesend auf die Eigenschaften zugreifen , werden in der Praxis auch speziell gekennzeichnet. Damit ist es möglich, dass diese mit (ons t Objekten aufgerufen werden. Eine Klassenmethode wird als Read-only deklariert
296
Mehr zu den Klass enme thoden (Klassenfunktio nen)
I
4.3
und defin iert, indem man an den Funhionskopf das schlüsselwort cons t anhängt: 11 Deklaration in der Klasse typ met hode( parameter) eo nst:
I
11 De f inition der Me t hode
t yp me thode( parameter ) eonst 1/ Anweisungen Im Beispiel de r Klasse ,.Mensch« würde man zum Beispiel die Zugriffsme lhoden geC als Read-only deklarieren und definiere n. Hierzu müssen Sie im Beispiel nur an den entsprechen den Stelle n das Schlüsselwort const anhängen. In der Heade rdatei ,.mensch.h« bspw. beträfe dies folge nde d rei Meth ode n: 11 mensc h . h lIinclude lli f ndef _MENSCH_H_ lIdefine _MENSCH_H_ us i ng namespace s t d :
class Mensch I private : 11 Ei gens cha ften de r Kla sse Mensch char name[30] : uns i gned i nt alter : 11 0 - man n lieh : 1 - wei blich bool geSC h leCht : void t est_gesch l echt C void ) : pub 1i c : 11 Alles wie gehabt 11 Zugrif fs methoden zum Abfragen de r Ei ge nscha f ten
}
,
cons t e har* get_name{ vof d ) eons t: uns1gned 1nt ge t _a l ter( vold ) const : bool get_gesc hlecht( vofd ) const:
lIendif Neben der Deklaration muss eine solche Methode natürlich aueh in der Definition mit eons t signiert werden. Hierzu müssen Sie in der Datei J>mensch.cpp« die fo lgenden drei Zugriffsmethoden anpassen:
297
4
I
Objektorientierte Programmierung
11 mensch . cp p lIinclude lIinclude llinclude "mensch . h" us i ng namespace std : 11 Alles wie bisher
11 Zugr i f fsmethoden zum Abfragen der Ei genscha f ten
co nst char * Mens ch :: get_name( vo1d ) const ( return name:
unslgned ln t Men sch::ge t _a lter( vol d ) cons t ( retu rn alte r;
bool Mensch: :get_ges ch lecht( vold ) cons t ( return geschlecht;
Der Vorteil solcher Read-only-Methoden besteht darin, dass aus diesen Methoden keine anderen Methoden aufgerufen werden können, die die Eigenschaften ei nes Objekts überschreiben. Zum Beispiel würde der Compiler folgenden Codeabschnitt bemängeln und das Programm nicht übersetzen: bool Mensch :: get_geschlecht( void ) const 11 !! ! Feh ler - Ni cht Erlau bt! !! se t _geschlechte FRAU ) : retu r n geschlecht ; Folgende Aspekte sollten allerdings in Bezug auf Read-only-Methoden noch erwähnt werden . Zum einen können diese Methoden natürlich auch für nicht konstante Objekte aufgerufen werden und zum anderen gehört das Schlüsselwort cons t zur Sign atur einer Funktion (Methode) . Dadurch ist es theoretisch auch möglich, dass Sie zwei Versionen derselben Funktion im plementieren. Für konstante Objekte eine Read-only-Version und für nich t konstante Objekte eine andere: 11 ... fOr konstan t e Objekte
typ methode( parameter) const:
298
Mehr zu den Klass enmethoden (Klassenfunktionen)
I
4.3
11 ... für nichtkonstante Objekte
typ methode( parameter ) ;
I
11 De finit io n typ methode( parameter ) co nst I 11 Anweisungen
typ methode( parame t er ) I 11 Anweisungen
4. 3.4
this- Zeiger
Sicherlich haben Sie sich schon gefragt. wie es möglich sein kann, dass man mit den Klassenmethoden auf die Eigenschaften eines bestimmten Objekts zugreifen kann, obwohl n iemals eine explizite Angabe zum Objekt gemacht wurde. Als Beispiel wieder die Klasse ,.Mensch .. . Wenn Sie fo lgendes Objekt ausgeben wollen Mensch person : person2 . print() ; wi rd hierbei ja die Klassenmethode . printO.. aufgerufen: void Mensch :: pr i nt ( void ) [ cout « name « " " « alter test_geschlechte) ; cou t « ")\n" ;
« "
Ja hre (" ;
Jetzt ist aber bei der Klassenmethode keine Angabe in Sicht, mit welchem Objekt diese Methode eigentlich arbeitet. Die Antwort lautet, dass beim Aufruf einer jeden Klassenmethode als nicht sichtbares Argument die Adresse des aktuellen Objekts mit übergeben wird. Diese Adresse steht in der Klassenmethode mit dem kons tanten Zeiger t hi s zur Verfügung. Die Syntax der Deklaration von diesem th i s-Zeiger sieht intern folgendermaßen aus: Klasse nname* const this - &Dbjekt ; Auf das Beisp iel . Mensch person.. bezogen sieht das so aus: Mensch~
const this - &pe rson :
Somit iSl also das Objekt .person" das Objekt, mit dem die Klassenmelhode aufgerufen wird.
'99
4
I
[»]
Objektorientierte Programmierung
Hinweis Wie Sie schon der Syntax des th i s·Zeigers entnehmen können, handelt es sich um einen konstanten Zeiger, den Sie nicht mehr verändern können. Der Zeiger verweist also immer auf das aktuelle Objekt - wobei naturlieh das Objekt selbst ver· ändert werden kann . this·> und -this
Verwenden Sie t hi s innerhalb von Klassenmethoden, erfolgt der Zugriff auf die ei nzelnen Elemente einer Klasse mit folgendem Ausdruck: this - )Eleme nt In der Tat handelt es sich hierbei tatsächlich darum, was der Compiler implizit daraus mache n würde, wenn Sie nur den Ausdruck Element verwenden. Somit entspricht bezogen auf die Klasse »Mensch.. und die Klassenmethode (bzw. hier Zugriffsmeth ode) lIsecnameü" folgende Definition: vOld Mensch : set name( const cha r * n ) st r ncpy( name , n , sizeof{name) - l ) ; name[sizeof(name)) - ' \0 ': Das ist dasselbe wie folgende Definition: void Mensch : : seLname{ const char* n ) I st rn c py f th1s-)name, n , s i zeof{ thls-)name )-l ) : thl s-) name (s i zeofe thls-)name J) - ' \0 ': Wenn der Compiler dies per Standard selbst so vornimmt, stel1 t sich natürlich die Frage nach dem Sinn von th i s · ) . In der Praxis kann man einen expliziten thisZeiger ven....enden, um die Bezeichner von lokalen Variablen einer Klassenmemode von den Eigenschaften (Klassenvariablen) mit gleichem Namen zu unterscheiden (wenn ein gleicher Name verwendet werden sollte), Bezogen auf die ZugrifTsmethode hen:
" secnameü~
würde dies in der Praxis so ausse-
void Mensch :: set name( const char /; name) ( st rn cpy( thl s-)name, name , s i zeofe thls-)name ) · l ) : thls-)name (s i zeo fe th1s-)name )] - ' \0 '; Hieran erkennt man gleich die lokale Variable (name) und die Klassenvariable (Eigenschaft) (th i s·)name). Natürlich lässt sich der thi s-Zeiger auch bei den Konstruktoren und Destruktoren einsetzen.
300
Verwenden von Objekten
Die zweite Verwendung des t hi s-Zeigers lautet " thi s. übe r diesen Zeiger können Sie auf ein Objekt komplett zugreifen (als ein Ganzes). Dies wird häufig verwendet, wenn aus einer Klassenmethode ein Objekt als Kopie oder Referenz zurückgegeben werden so ll (r etur n *thi s). Auf diese Art der Verwendung von * thi s wird auf den kommenden Seiten noch näher eingegangen.
4.4
Verwenden von Objekten
Dieser Abschnilt wird Ihnen fast so vorkommen wie eine Wiederholung des Umgangs mit den verschiedenen Datentypen - nur bezieht sich das Ganze nun auf die Objekte. 4.4.1
Read.only-Objekte
Wie jeden anderen Typ können Sie auch Objekte mit const deklarieren, damit dieses Objekt nur noch lesbar ist: const Mensch personl( "Adam" . 18 . Mens ch : : MANN ) ;
Aber seien Sie gewarnt, der Compiler kann nämlich oh ne zusätzliche Informationen nicht wissen, ob eine Klassenmethode nur lesen oder auch schreiben will. Dies bedeu tet in der Praxis, dass neben den set_-(Zugriffs-)Klassenmethoden zum Beschreiben der Eigenschaften auch die ge C-(Zugriffs-)Klassenmethoden nicht aufgerufen werden können. 4-4-2
Objekte als Funktionsargumente
Wie bei den bereits ken nen gelernten Typen stehen Ihnen bei der übergabe von Objekten als Argument an Funktionen drei Möglichkeiten zur Verfugung, und zwar per >tca ll by value«, >tcall by reference_ und »Referenzen«. (all by value
Der Aufwand bei der übergabe »by value« an ei ne Funktion kann sehr groß sei n. d;J eine komplette Kopie des Objekls eneugt wi rd. mit der die FunkTion ;J nschließend arbeitet. Das bedeutet, bei der übergabe "by value« wird der Konstruktor aufgerufen und erzeugt ein neues Objekt für die Kopie. Beim Beenden der Funktion wird das Objekt wieder zerstört - wobei auch hier der Destruktor aufgerufen werden muss. Ein ziemlicher Aufwand. Als Beispiel können Sie ja am Ende der Headerdatei ,.mensch.h« folgende globale Funktion einfügen:
30'
I
4·4
I
4
I
Objektorien tierte Programm ierung
11 mensc h.h I!include lli f ndef _MENSC H_H lIdefine _M EN SCH_H us i ng namespace std :
class Mensch ( 11 Hie r die Ei genscha f ten und Methoden wie gehabt I, 11 Globale Funktion
i nt vergleiche_altere const Men sch pl , const Men sch p2 ) ( return pI.get_alter() - p2.get_alter(); }
lIendif Anwenden können sie diese Funktion dann beispielsweise wie folgt: 11 main . cpp lIincl ude "mensch . h"
int main(void) I Mensch person1( "Ada m", 18 , Hensc h: : MANN) ; Mensch person2( "Eva" . 19 , Mensc h:: FRAU ) : int ret ; ret - vergleiche_alter< personl. personZ ); if{ret
« pe r son2. get_name() « • ist alter als " «personl. get_name()« ' \n ';
re t urn 0; Das Prog ramm bei der Ausfuhrung:
Adam ist von uns gegange n Eva i st vo n uns gegangen Adam i s t alte r als Eva
302
Verwenden von Objekten
Bei der Ausfü hru ng des Programms können Sie erkennen. dass der DeStruklor ak tiv war, und zwa r in der Funktion ,.vergleiche_alterO ... Beim Aufruf der Funktion ,.vergleiche_alterO" werden zwe i Objekte der Klasse ,.Mensch.. an die beiden Parameter kopiert, also vom Konstru ktor neu erzeugt. Beim Verlassen werden diese wieder zerstört, es wird also der Destruktor aufgerufen. call by reference
Dass die Verwendung des "call by value .. -Verfahrens eher kontraproduktiv für das Laufzeitverhahen eines Programms ist, dürfte wohl klar sein. Diesen Aufwand kann man ohne Probleme mit "call by reference« vermeiden (siehe Abschni tt 2.6.2). Da hier mit den (Origina l-)Adressen der Objekte gearbeitet wird, sollte natürli ch auch klar sein, dass sich jede Veränderung einer pub 1i cEigenschaft auf das tatsächliche Objekt auswirkt. Abe r in der Regel sind die Eigenschaften einer Klasse fas t immer pri ~ate. Bezogen auf das Beispiel der Klasse ,.Mensch", und der zuvor erstellten globalen Funktion ,.vergleiche_alterO« ist bei der Fun ktionsdefinition nicht viel zu verändern. Lediglich die Parameter dieser Funktion müssen als Zeiger angegeben werden, und der Zugriff auf die Klassenmethoden bzw. publ i c-Eigenschaften erfolgt über den Pfeiloperator (- » : int ve rg leiche_altert const Mensch* pI . const Mensch* p2 ) I return pI->get_alter() - p2 - >get_alte r{ ) : Beim Aufruf der Funktion müssen Sie natürtich auch die Adressen der Objekte mi thil fe des Adressoperators übergeben: ret - vergleiche_alter t &personl . &person2 ) ; Hiermit entfa llt der Aufwand, der beim Kopieren und Zerstören ein es Objekts benötigt wird. Daher empfiehlt es sich immer, Objekte an Funktione n über Zeiger ode r Referenztypen zu übergeben. Referenzen auf Objekte
Natürlich können Sie auch Referenzen auf Objekt e verwenden. Hierbei haben Sie
dieselben Vorzüge von Zeigern und den gleichen (einfacheren) Komfort von normalen Variablen (Zugriff und Verwendung) (Siehe auch Abschnitt 2.2 zu den Referenzen). Die Funktionsdefinition der Funktion lOvergleiche_alterO« sich t mit Referenzen auf Objekte wie fo lgt aus:
303
I
4·4
I
4
I
Objektorientierte Programmierung
int ve rgleiche_al terI(const Mensch& pI , const Mensch& p2 ) [ return pl,gecalterC) - p2,gecalterC) ; Der Funktionsaufruf und die Verwendung in der Funktion selbst auf die einzelnen lugriffsmelhoden und gegebenenfalls pub 1i c-Eigenschaften , erfolgt hingegen wie bei den normalen Variablen. Hier haben Sie außerdem durch die Deklaration von const (konstante Referenzen) der beiden Parameter einen Schreibschutz (Read-only) verwendet. Damit bleiben die Daten wie beim "ca ll by value«-verfahren geschützt.
1 lli f ndef _ME NSCH_H lIdefine _M ENSCH_H us i ng namespace std ;
304
Verwenden von Objekten
I
4·4
cl ass Mensch I priva t e : 1I Alles wie gehabt
I
pub 1 j c : II Alles wi e gehab t
1nt ve r gleiche_altere co nst Mensch& p2 ) co nst ; I,
lIendi f Die Definhion hingegen wi rd wieder auf die Date i . mensch.cpp« ausgelagert und hat folgendes Aussehen : 11 mensc h. cp p
i nt Men sch : : ve r gle i che_alter( const Me ns ch!. p2 ) const I ret urn alte r - p2 . alte r: Hieran lässt sich erkennen , w ie Sie in der Methode direkt auf die pri vate-Eigenschaften der Klasse zugreifen können. Aufgerufen wird diese Klassenmethode folgendermaßen: Mensch pe r sonH · Adam~ , 18 , Me nsch : :MANN ) : Mensch pe r s on 2{ • Eva · . 19 . Mensch :: FRAU ) : int ret ;
ret - personl.vergle1che_alter ( person2 ); i f (ret> O ll /I
*this zum Zweiten ...
Wenn Sie in einer Klassenmethode schreibend auf ein Objekt (als Argume nt) bzw. dessen Eigenschaften zugreifen wollen/müssen, muss dies natürlich ohne das Schlüsselw ort cons t geschehen . Gewöhnlich verwe ndet man hierzu alle rdings Zeiger stau Referenzen (für den schreibenden Zugrifi), weil immer eine Adresse des Objekts angegeben w erden muss. Wolten Sie zum Beisp iel die Eigenschaften eines Objekts kopieren oder zwei Objekte austauschen , können Sie mit dem * thi s-Zeiger das Objekt als ,.Ganzes« ansprech en oder die einzelnen Elemente (thi s - >ei genscha f t ) separat. Fügen Sie
305
4
I
Objektorientierte Programm ierung
zur Demonstration folgende zwei Klassenmethoden-Deklaratlonen in der Headerdatei ,.mensch. h" ein :
Ilmensc h. h class Mensch
}
,
I/ Mensch mit allen Eigenschaften ~opiere n vo i d kopie_Mensch( Mensc h *p ) ; I1 Mensc h mit allen Eigensc haften aus t ausche n vo i d tausche_Mensch( Mensch wp ) :
Die Definition in der Datei »mensch. cpp":
void Mensch : : ~opie _Mensch( Mensch ' p ) I Mensch tmp - 'th is : *p - tmD:
void Mensch : : t aus che_Mensch( Hensch *p ) I Mensch tmp - *p; *p - *this : *t his - tmp ; Im Beispiel wurden die einzelnen Objekte mit *t his als ,.Ganzes« verwendet. was Sie, wie bereits erwähnt, mit th i s - > auch au f die einzelnen Eigenschaften anwenden könnten . Beide Klassenmethoden ,.kopieflenschO" und ,.tauscheflenschO.. erwarten einen Zeiger, der hier immer neu beschrieben wird. Beide Methoden legen auch ein lokales Obj ekt ,. tmp" von der Klasse Mensch . Das bedeutet. dass in der Funktion jeweils Konstruktor und Destruktor aufgerufen werden. Bei der Methode _kopie_MenschO" erhält das Objekt »tmp" die Adresse des Objekts durch *t hi s. Damit haben Sie praktisch schon ein Objekt kopiert, das sich jetzt im lokalen Objekt »tmp~ befindet. Dam it nun auch di e aufrufende Funktion, mit dem aufgerufenen Zeiger als Parameter, diese Kopie erhält, greifen Sie mit dem Zeiger "p" indirekt auf das Objekt ,.tmp" als _Ganzes" zurück, wodurch auch das Argument des Aufrufers auf ein gültiges Objekt (zunächst indirekt) verweist.
306
Verwenden von Objekten
Ähnlich verläuft bei der Melhode .. tauscheßenschO .. . in der die Eigenschaften zweier Objekte ausgetauscht werden. Auch hier wird ein lokales Objekt verwendet, das die Adresse des austauschbaren Objekts erhält. Anschließend übergeben Sie an "PO( das Element des aktuellen Objekts (Siehe den Abschnitt .. Klassenmethoden mit Objekten als Argumente,,), was Sie mit dem *th i s-Zeiger gleich als »Ganzes« machen können . Am Ende erhält der *thi s-Zeiger wieder das Objekt (bzw. die Adresse auf das Objekt), das Sie als Argument an die Funktion übergeben haben. Das muss in diesem Fall ein lokales Objekt sein. In der Praxis lassen sich diese beiden Klasse nm ethoden wie folgt einsetzen: 11
main .Cpp
lli ncl ude "mensch . h"
int main{void) { Hensch personl{ "Adam ". 18 . Hensch : : HANN ) : Hensch person2 ; Hensch person3{ "Ev a ". 19 , Hensc h:: FRAU) : // Kopie erstellen personl . kopie_Hensch{ &person2 ) : personl . pr i nt() : person2 . pr i nt() ; person3 . print{ ) ; 11 Ei genschaften der Objekte tauschen person1 . tausc he_Hensch{ &person3 ) ; person1 . print{) ; person2.prinU ) ; person3.prinU ) ; return 0;
Das Programm bei der Ausführung:
Adam ist von uns gegangen Adam 18 Jahre {m3nnlichl Adam 18 Jahre {m3nnlichl Eva 19 Ja hre (weiblich) Eva i st von uns gegangen Eva 19 Ja hre (weiblich) Adam 18 Jahre {m3nnl i chl Adam 18 Jahre {m3nnl i chl In diesem Beispiel können Sie außerdem erkennen, dass es manchmal hilfreich ist (auch für das Verständnis), den Destruktor etwas ausgeben zu lassen . [n bei-
307
I
4·4
I
4
I
Objektorientierte Programmierung
den Fällen w ird dieser in de n Klassen methoden "kopie_MenschO« und "tausche MenschO" ausgeführt - wo beide Male lokale Objekte verwendet w urden.
4.4.3
Objekte als Rückgabewert
Natürlich können Sie auch Objekte, wie auch bei anderen Typen, entweder als Zeiger, als Referenz oder als Objekt selbst zurückgeben lassen. Allerdings gilt hierbei, wie auch sc hon bei der Parameterübergabe von Objekten an Funktionen, dass die Rückgabe als Kopie nicht unbedingt sehr sinnvoll erscheint - gerade bei größeren Objekten ist der Aufwa nd auf dem Stack nicht ganz unerheblich. [)]
Hinweis Auch bei der Rückgabe von Objekten sollten man auf die Lebensdauer achten. Gerade bei der Rückgabe von Kopien bzw. Referenzen darf das Objekt nicht lokal beschränkt sein und sollte mindestens als stat i c deklariert werden. Das kann man nicht oft genug erwähnen, Ein einfaches Beispiel, w ie Sie als Rilckgabewe rt ei ne Kopie des Obje kts zurilek· geben lassen können. Im Beispiel wird eine globale Funktion ,.kopie_aJ ter_ Me nschO" erstellt, die die ältere von zwei Personen als Kopie an den Aufrufer als Rückgabewen zurückgibt. Schreiben Sie di e Defin ition dieser Funktion entweder in die Datei "main .cpp« oder in der Headerdatei " mensch.h ~ außerhalb der Klassendeklaration. Mensch kopie_olter_Hensch( const Mensc h! pI. cons t Mensc h! p2 if( pI . vergl e i che_o 1ter (p2) ) 0 ) I ret urn pI :
)
else I re t u rn p2 :
Besser als diese Methode, bei der ein temporäres Obj ekt erzeugt und wieder zerstört werden muss, ist die Rückgabe von Zeigern oder Refe renzen - bezüglich der einfache ren Ve rwendung sollte man immer, wenn möglich, Referenzen bevorzugen. Somit sieht diese Funktion mit der Rückgabe einer Referenz wie folgt aus : const Mensch! kopie_alter_Mensch
const Mensch! pI . const Mensch! p2 ) I if( pl.vergle i che_alter(p21 > 0 1 I ret urn pI :
308
Verwenden von Objekten
I
4·4
else I retu rn p2 :
I
Die Verwendung dieser globalen Funktion kann wie folgt aussehen : 11 main.cp p lli ncl ude "mensch. h"
int main(void) I Mensch personl( "Adam· . 20 . Mensch : :MANN ) : Mensch person2( " Eva " . 19 . Mensch :: FRAU ) : Mensch person3 :
person3 - kop1e_a lter_Hensch ( personl, person2 ); person3 . print() ; return 0:
Das Ganze lässt sich auch noch komfortabler und objek torientiener ohne ein Objekt oder eine zusätzliche Referenz verwenden: Mensch personU " Adam ". 20 . Mensch : : MAN N ) : Mensch person2( "E va" . 19 . Mensch : : FRAU) ; cout
4.4-4
« kopie_al ter _Mensch( personl . pe r son2) . geLname() « " ist der Älte r e von be i den ! \n " : Klassen-Array (Array von Objekten)
Wollen Sie mehrere Objekte einer Klasse verwenden, so stehen Ihnen auch hier Arrays von Objekten zur Verfügung . Di e Deklaration erfolg t wie bei der berei ts kennen gelernten Syntax der Basistypen: Klasse Beze i chner[Anza hlObjek te l :
Deklarieren Wollen Sie zum Beispiel ein Array "Mensch .. mit zehn Objekten anlegen. können Sie dies folgendermaßen ausführen: Mensch personen[IOl ;
Wenn Sie keine explizite Initialisierung angeben, wird für jedes Obj ekt der Konstruk tor (oder, falls keiner vorhanden ist, der Default-Konstruktor) aufgerufen.
l09
4
I
Objektorientierte Programmierung
Initialisieren
Auch die Initialisierung kann, wie schon bei den Basistypen bzw, den Strukturen, über eine Initialisierungsliste erfolgen, Wenn Sie wieder verschiedene Kons truktoren erstellt haben, dann kann jedes Element theoretisch mit einem anderen Konstruktor erzeugt werden: II Ein Objekt der Klasse Mensch Mensch iCh("JOrgen ", 3D , Men sc h::MANN} : 11 10 Obje kte der Klasse Mensch Mensch pe r sonen[lO] - I MenSCh ( " Adam ", 20 . MenSch :: MANN ) , Mensch( "Eva ". 19 , Mensc h:: FRAU) , Mensch( Mensch : : FRAU ) , Mensch( "Ma r t i n" , 39 ) . Mensch : : FRAU , "Jenovo " , ich
I,
Wenn Sie in Ihrer Klasse Konstruktoren mit nur einem Parameter angegeben haben. so können Sie in der Liste auch nur ein Argument angeben. Das ist natürlich nur sinnvoll, wenn die Klasse eindeutige Parameter besitz l. Sie können auch ein bereits definienes Objekt (wie hier ,.ich,,) zur Liste hinzufügen , Die restl ichen nicht definienen Objekte werden vom Konstruktor mit entsprechenden Werten vorbelegl. Das Ganze lässt sich auch ohne die Angabe der Array-Länge verwenden, Nur werden dann so viele Elemente angelegt, wie in der Initialisierungsliste stehen: 11 Ein Objekt der Klasse Mensch Mensch ich(" Jürgen ", 30 . Mensch :: MANN ) ; 11 7 Objekt e der Klasse Mensch Mensch pe r sonen[ ) - I Mensch( "Adam ", 20 , Mensch: : MANN ) , Mensch( "Eva ". 19 , Mensc h :: FRAU) , MenSch( MensCh : : FRAU ) . Mensch( "Martin ". 39 ) . Mensch: : FRAU, "Jenovo" , ich
I,
Ohne eine Angabe der Array-Länge werden hier sieben Objekte der Klasse ,.Mensch .. angelegt.
30 '
Verwenden von Objekten
I
4·4
Zugriff auf Klassenelemente Der Zugrjff auf die Klassenelemente (Klassen methoden oder publ i c-Ejgenschaften) erfolgt ähnlich wie schon bej den normalen Objekten, nur muss hjerbej der entsprechende Index in den eckigen Klammern mit angegeben werden. Objekt[Index) . Methode Hjerzu ein kleines Programmbeispiel, das die VelVlendung von Klassen-Arrays jn der Praxis demons trieren soll : 11 main ,Cpp lli ncl ude "mensch. h"
int main{voidl I 11 Ein Objekt der Klasse Mensch Mensch iCh("JOrgen ", 30 , Mensch :: MANN ) ; 11 10 Objekte der Klasse Mensch Mensch pe rsonen[10] - I Mensch( "Adam" , 20 , Mensch :: MANN ) , Mensch( "Eva ", 19 , Mensch : : FRAU >Mensch( Mensch : : FRAU ) , Mensch( "Ma r tin ", 39 ) , Mensch : :FRAU . "Jenova " , i ch I,
Anzahl der Objekte ermittel n cout « "Anza hl der Elemen t : « (sizeof(personenl!sizeof(Mensch» 11 Zugrif f mit den Klassenmethoden 11
«
"'n ":
personen[7] . set~name( · Georg·l :
personen[7] . seLalter( 50 ) : personen[7] . seLgeschlecht( Mensch : : MANN ) : 11 Alles Ausgeben for(int i - O: i < 10 : i++ ) I personen[ i ) . pr i nt( ) : return 0: Das Prog ramm bei der Ausfiihrung: Anzahl der Element : 10 Adam 20 Jahre (m3nnl i chl Eva 19 Jah r e (weiblichl Unbekannt 0 Jahre (weiblic h )
3"
I
4
I
Objektorientie rte Programmie rung
Mar t in 39 Jahre (we i bli ch) Unbekannt 0 Ja hr e (we i bl i ch) Jenova 0 Jah r e (we i bl i ch) JOrgen 30 Jahre (man nli eh) Georg 50 Jah r e (ma nnl i ch) Unbekannt 0 Ja hr e (mannl i ch) Unbekannt 0 J ah r e (mannl i ch)
4.4.5
Dynamische Objekte
Das Erzeugen dyn amischer Objekte einer Klasse lässt sich. wie schon bei den bisher kennen gelernten Typen , mit dem Operator new realisieren. Mit dem Reservieren vo n Speicher ist es aber bei den Klassen noch nic ht getan . Hier wird zusätzlich noch der Konstruktor ausgeftihrt. Genauso sieht es beim Freigeben des Speicherplatzes aus. Neben der Freigabe des Speichers an den Heap wird auch noch der Destruktor rur jedes reservierte Objekt aufgerufen. Objekte dynamisch anlegen
Ansonsten ist die Syntax zur Reservierung von Speicher ftir Klassen dieselbe wie bei den Basistypen: 11 Speiche r fOr e i n Objekt reservie ren
new Klasse 11 Speiche r fOr e i n Objekt reservie ren und In i tialisieren
new Klasse( lni tialisier un gs li ste ) 11 Speiche r fOr n Objekte reserv i eren (dy n. Kl assen-Array)
new Klasse[ n] Im ersten Beispiel wird Speicher fti r ein Objekt mit dem Standardkonstruktor aufgerufen (der Konstruktor ohne Parameter, falls vorhanden - ansonsten eine Minimalversion des Compilers) . Im zweiten Fall wi rd ebenfalls Speicher fü r ein Objekt reserviert und ein entsprechender Konstruktor aufgerufen (abhängig von der Initialisierungsliste bzw. dem Vorhandense in eines solchen Kons truktors). Wird kein passender Konstruktor gefunden, wird eine Fehlermeld ung ausgegeben. Beim dritten Beispiel reservie ren Sie Speicher für n Obj ekte einer Klasse also Speicher fü r e in dynamisches KJassen-Array. Auch hier wird rur jedes einzelne Elem ent der Standardkonstruktor aufgerufen.
Bezogen auf die Klasse ,.Mensch.. sieht das Beispiel so aus: Mensch " MenschPtr : 11 Speic her fOr ein Obje kt (mit Initialis i erungsliste) MenschPtr - new Mensc h( "Adam" . 20 . MenSC h: :MANN ) :
312
Verwenden von Objekten
De r Zeiger "Mensch Pm e rhält nun bei erfolgreic her Speicherreservierung die Anfangsadresse eines Objekts der Klasse ,.Mensch .. . Im Beispiel werden die einzelnen Eigenschaften gleich mit Wenen initialisiert. Dies könnten Sie auch nachträglich (oder korrigierend) mit ,.MenschPtr.. und dem Pfeiloperator machen. Woll en Sie zum Beispiel die Eigenschaft ,.alter.. verändern, gehen Sie wie folgt vor: 11 Alte r verandern MenschPtr - >set~alter(
22 ) :
Mit dem Zeiger und dem Pfeiloperator können Sie über alle pub 1i c-Klassenmethoden und (falls vorhanden) publ ic-Eigenschaften zugreifen. Ähnlich können Sie auch Speicher für ein dynam isches Klassen-Array reservieren: Mensch * MenschPtr : 11 Speicher fQr Anzahl Objekte rese r viere n Mensch Pt r - new Mensch[anzahl] : Die Objekte können Sie jetzt. wie bei den gewöhnlichen Klassen-Arrays, mi t Wenen initialis ieren. Wichtig ist immer, dass Sie den (richtigen) Index verwenden. Dies können Sie entweder über die Klassenmethoden machen wie beispielsweise: MenschPtr[i] . seLname( " Adam" ) : MenschPtr[i] . set_al ter( 22 ) ; oder Sie verwenden einen Konstru ktor (der Standard konstruktor wurde ja bereits beim Anlegen mit new aufgerufen): Mensc hPtr[ i ] - Mensch( "Adam ", 22 , Mensch : :MANN ) : i++ -
MenschPtr[;] - Mensch( Mensch : : FRAU ) : Mehr dazu können Sie dem Abschnitt über Klassen-Arrays und Konstruktoren (Abschnitt 4.4.4) entnehmen. Speicher freigeben
Analog sieht dies beim Freigeben des Speichers mit de 1ete aus_ Die Syntax hierzu: 11 Speic her fOr ein Objekt wieder fr eigeben delete KlassenPtr : 11 Speic he r fOr das Klassen -Arr ay wieder f re i geben delete [] Kl asse nPtr :
313
I
4·4
I
4
I
Objektorientierte Programm ierung
1m ers ten Beisp iel wird der Speicher fü r ein reserviertes Objekt, auf das der Zeiger " KlassenPtr" verweist, an den Heap zurückgegeben . Damit auch alles zerstört wird, wird auch hier der Destruktor des Obj ekts aufgerufen. Im zweiten Fall geben Sie den Speicherplatz für ein dynamisch reserviertes Klassen-Array frei. Auch hierbei wird für jedes Elem ent einzeln der Destru ktor aufgerufen. Bezogen auf Objekte der Klasse . Mensch". ist das sehr einfach. Ein ei nzelnes Objekt geben Sie folgendermaßen frei : delete MenschPtr ; Ebenso einfach wird der Speicher für ein ganzes Klassen -Array wieder abgebaut: delete [] MenschPtr : Hierzu nun ein Programm beispiel. das das hier Beschriebene wieder in Praxis demonstrie ren so ll. Wir verändern nur die Datei »mai n.cpp", alles andere (»mensch .cpp". »mensch .ho:) bleibt unverände rt: 11 main . c pp /}include "mensch . h" int main(void) { Mensch * Mensc hPtr ; unsigned in t anzah l ; char na me [30] ; unsigned int a lter ; bool geschlec ht ; MenschPtr - new Mensch( "Adam ". 20 . Mensch : : MANN ) : 11 Alter verandern MenschPtr')set_a l ter( 22 ) ; MenschPtr-)print() : 11 Speich er wieder f reigeben delete Mensch Ptr : cou t « "Wieviele Pesonen sol l en erzeugt werden : ci n » anzahl : 11 Spe ic her fOr Anzahl Obj ek te reservieren MenschPt r - new Mensch[anzahl] ; 11 Elemente init i al i sie r en fore i nt ;- 0; i < anzahl ; i++ ) cou t « " \nPerson " « i « • eingeben\ n" ; cout « "Name e i n» name ; cout « "Alte r
..
Verwenden von Objekten
e i n» alte r; cou t « ~Gesc h lecht (m=O/w=ll : e i n » geschlecht : MenschPtr[i).set_name( name l ; MenschPtr[i).set_alter( alter ) : MenschP t r [i ) . set_gesch 1echt ( ges ch 1ech t ) : 1* Auch möglich ware hier be i : * MenschPtr[i) - Mensch( name . alter . geschlecht ) : 11 Element e ausgeben cout « ~ \nDi e er zeug t en Per sonen\n ~: fort int i - O; i < anzahl : i++ ) I MenschPtr[i] . pri nt<) :
I 11 Speicher wieder freigeben
delete [] MenschPt r: return 0; Das Programm bei der Ausführung :
Adam 22 Jahre (mannl i ch) Adam ist von uns gegangen Wiev i ele Pesonen sollen er zeugt werden: 3
Person 1 eingeben Name Alt er Geschlecht (m- O/w- l) Person 2 ei ng eben Name Al t er Geschlecht (m- O/w-})
4·4
I
,/
Person 0 ei ng eben Name Alte r Geschlecht (m- O/w-l)
I
Jllrg en 30
o Jo nathan 3
o Fetme 35 1
Di e e rzeu gten Personen JOrgen 30 Jahr e (mannlieh) Jonathan 3 Jah re (mannlie h) Fa tma 35 Ja hre (weiblic h)
315
4
I
Objektorientierte Programmierung
Fatma ist von uns gegangen Jo nathan ist von uns gegangen Jürgen ist von uns gegangen An der Ausgabe des Programms lässt sich gut erkennen. wie nach jedem de 1ete der Destruktor rur jedes Element aufgerufen wird. Kein Speicherplatz mehr vorhanden
Wie bereits im Abschnitt zu new und delete (Abschnill 2.1.5) erwähnt. ist es nicht immer ga rantiert, dass Sie einen angeforderten Speicher auch erhalten. Es kann passieren. dass der Heap irgendwann nicht mehr genügend zusammenhängenden Speicher reservieren kann . Wie man darauf reagieren kann, haben Sie auch bereits gelesen. Sofern Sie überhaupt nicht darauf reagie ren, wird standardmäßig bei einem Fehlschlag der Speicheranforderung mit new das Programm beendet (abgebrochen). Oft wird auch hier die »alte .. Möglichkeit verwendet. indem man C-typisch den Rückgabewert auf NU LL überprüft. Gibt eine Speicherreservierung NULL zurück. konnte kein Speicher mehr reserviert werden: Mensch* MenschPtr : for( ; nt ; - 0 : : i ++ ) I MenschPt r - new Mensch[lOOOO] : ; f( Hen sc hPtr NUll ) I // Feh l er be i m Speic herre serviere n
Gewöhnlich reagiert ma n darauf, indem man .. eine Fehlermel dung auf den Standardfehler-Kanal (ce rr) ausgibt .. gegebenenfalls die Daten sichert und das Programm beende( ..
das Programm beendet
..
nochmals versucht, Speicher vom Heap anzufordern
Fehler-Handle von new
Sofern Sie keine Vorkeh rungen in Ihrem C++-Programm bezüglich des new-Operatars treffen . wird ein Standard· New-Handler eingerichtet. der eine Exception (siehe Kapitel 6) aus löst. Wird diese Exception nicht abgefangen, wird das Programm beendet.
Verwenden von Objekten
Hinweis Eine Exception ist eine unvorhergesehene Ausnahme. Die ursachen einer solchen Exception können sehr vielfältig sein. Auf dieses Thema wird gezielt in Kapi tel 6 eingegangen .
Mit dem ,.neusten« ANSI-Standard bietet eH einen recht komfortablen Handler an, mit dem man festlegen kann, was nach einem erfolglosen Aufruf von newpassieren soll. Diesen Mechanismus können Sie mit der Funktion lIsecnew_handler«. die in der Heade rdatei defin iert ist (und daher auch inkl udiert werden muss). verwenden. Die Syntax lautet: llinclude new_handler seLnelc handle r { new_handle r _Pnew ) throw( ) : Der new-Handler, den Sie dieser Funktion als Paramete r übergeben, ist eine globale Funktion, die weder einen Parameter noch ei nen Rückgabewert besitzt. In dieser Funktion legen Sie fest, wie beim Fehlschlag einer Speicherreservierung fongefahren werden soll. Mit der Funktion lIsecnew_handler« und der globalen Funktion als Parameter installieren Sie den new-Handler und ersetzen den Standard-New-Handler durch Ihren selbst geschriebenen Handler. Hierzu das Programm beispiel, das den new-Handler de monstriert: /I mai n .c pp lIinclud e "men sch . h" llincl ude
vo l d heap_vol l { vo l d ); int main(void) 1 Mensc h* MenschPt r : 11 Eigenen new-H andler ins t alliere n seCne'lchand l er( heap_vol l ) : t or{ ; nt ; - 0 : ; ;++ ) I MenschPt r - new Mensch[ l OOOO) ;
ret ur n 0:
11 E;gene r new -Handl er vold heap_voll( vold ) { cou t « "Der Heap kann nicht
~ehr
genQgend"
« " zusa mm enh~ngenden Spei che r anbletenl\n" « "Das Progralllm w1rd beendetl\n "; 11 Hier noch ggf. wl cht 1ge Arbe ite n erledigen
317
I
4·4
[« J
I
4
I
Objektorientierte Programmierung
ex1t(1): Wenn Sie das Beispiel testen wollen , sollten Sie sich ein wenig gedulden, bis der Heap »voll", ist (abhängig von der Größe des virtuellen Speichers und davon, wie das Betriebssystem damit umgeh t). Sie können unter Windows in einem Taskmanager und un ter linux/*nix mit dem Befehl top beobach ten, wie der Speicher rhres Rechners immer voller wird (und der pe zwangsläufig langsamer reagiert). Wenn Sie keinen Speicher mehr bekommen, wird die Funktion .. heap_vollO.. ausgeführt und gibt die entsprechende Fehlermeldung aus.
[»]
Hinweis Natü rlich muss erwähnt werden, dass new- Handler noch relati v neu ist, sodass alte Compiler weiterhin mit NULL arbeiten müssen, weil diese das Feature noch nicht unterstützen. Auf vielen Testsystemen und Compilern hatte ich allerdings keine Probleme damit.
4.4.6
Dynamische Klassenelemente
Natürlich ist die dynamische Spe icherverwallUng nicht nur den Objekten vo rbehalten, sondern auch den einzelnen Klasse nelemelllen - man spricht auch von dynam ischen Elementen oder dynamischen Klasseneigenschaften. Zur Demonstration soll in der Klasse »Mensch.. die Eigenschaft .. name .. dynamisch angel egt werden. Also statt char name [ 30 ):
soll hier char · name :
verwendet werden. Die Länge dieser Variablen soll erst zur Laufzeit vergeben werden. Dazu benötigen Sie natürlich eine weitere Variable, um die Länge der CZeichen kette zu speichern. Wir verwenden hierfilr einen rnteger .. Ien ... Sie müssen also bei der Deklaration der Klasse in der Headerdatei .. mensch. h.. eine pri· va te-Eigenschaft verändern und eine hinzufligen: 11
mensch . h
cl ass Mensch priva te: 11 Ei genSChaften der Klasse Mensch cha r *name: 11 Spe1c her w1 rd zur 11 Linge des Namen 1nt len :
Lau fze1t re se rv 1e r t
uns i gned i nt alter : bool geschlecht : 1/0 -
318
m~nnl i ch :
1 - weiblich
Verwenden von Objekten
I
4·4
publiC : 11 Alle publ i c-Hethode n und Eigenschaften wie gehabt }
,
Es wurde bereits erwähnt, dass solche Umschreibungen von Programmen oft vorkommen. Um kompatibel zu älteren Programmversionen zu bleiben, sollten Sie, wen n möglich, die Schnitts tellen der Funklionsköpfe nicht verändern. So können Sie sicherstellen , dass andere. die ein Update Ihrer Bibliothek erhalten. diese auch noch bei den alten Hauptprogrammen verwenden können , ohne irgendwelche Veränderungen daran vorzunehmen. So werden auch keine Veränderungen der Schnittstellen notwendig. Sie müssen led iglich alle Funktionsdefin itionen, bei denen auf den C-String »name« bisher schreibend zugegriffen wurde, entspreche nd der dynamischen Version anpassen. Zunächst wären hier die Konstruktoren, die beim Anlegen eines Objekts immer den C-String mit einer ZeichenkeUe versehen . Betrachtet man hier zum Beispiel fo lgenden Konstruktor: 11
mensch.cpp
Hensch : :He nsch( canst char ' n. uns i gned int a . baal g ) ! strn cpy ( name. n, sizeof(name l- l 1; na me[s 1zeof (n ame l) - ' \0' ;
alte r - a : geschlecht - g: dann muss der fett helVorgehobene Teil nur wie folgt umgeschrieben werden: 11
mensch . cp p
Mensch : :Mensc h( const cha r* n. unsigned int a . baal 11 Lange des neuen C-$t r i ngs erm i tteln
9 ) I
len - s tr len( n): 11
Speic her mit l en+l Bytes r eservieren
name - new cha r [len+l];
// St r i ng in den neuen Spei cher sch rei ben s tr c py( na me. n 1;
alte r -a : gesch l echt - g: Diese Änderungen müssen Sie an allen Konstruktoren in der Datei lI mensch.cpp« bei deren Definition vornehmen . Hier di e restlichen Konstruktoren:
319
I
4
I
Objektorien tierte Programmierung
11
mensch.cpp
Mensch; ;Mensch( cons t cha r* n . uns igned int a ) len - s tr len( n); naMe - new char[ l en+l) : st rcpy( name. n ); alter - a ; 11 Zufallsgenerator ware sinnvoll er - aber i f( a % 2 ) I 11 Gerade oder ungerade Zahl geschlecht - FRAU: else I geschlecht - MANN :
MenSCh : :MenSC h( const char * n ) I len - st r l en(n ) ; naMe - new char(len+l); st r cpy( name. n ); 11 Neu geboren ;. ) alter - 0 : 11 meh r Frauen braucht das Land : . ) geschlec ht - FRAU :
MenSCh : ;MenSCh( ) I 11 Raben , Vater oder Raben ' Mutter ode r Tr agOdie ? len - s trlen ("Unbekannt" ) ; name - new char[len+l ) ; strcpy( name . WUnbekannt W ) ; 11 Neu gebo ren ... : . ) alter - 0 ; 11 als Ausgleich wi ede r ein Mann : .) geschlecht - MANN :
Mensch; :Mensch( bool 9 ) I 11 Rabe n, Va ter oder Raben ' Mu tter ode r Trag Odie ? len - s tr len("U nbekannt " ) ; na~e - new char[len+l ] ; s trcpy( name . "Unbeka nnt" ); 11 Neu geboren .. . : - ) alter - 0:
320
Verwenden von Objekten
I
4·4
11 als Ausgleich wieder ein Mann : -)
geschlecht - g :
Als Nächstes fol gen die Zugriffsmethode n, die auf den C-String . name .. bisher schreibend zugegriffen haben, was bei der Klasse .. Mensch " nur zwei sind: 11
mensch . cpp
void Mensch :: erzeuge(const char~ n . unsigned int a . baal g)( 11 . .. vom Konstruktor reservierten Speicher freigeben dele t e [] name: 11 Speicher fur neuen Namen anfordern len - st r len( n): name - new char[len+l]: strcpy( name. n ): aH er - a : geschlecht - g :
void Mensch :: set_name( const char ' n ) 11 vom Konstrukto r reservierten Speicher f reige ben de l ete [] name; 11 Spe icher für neuen Namen anforde r n len - st r len(n): name - new cha r [len+l1: strcpy( name. n ): Wird e in Objekt wieder frei gegeben. müssen Sie auch den dynamisch reservierten Speicher ftir das Klassenelement expliz it a n de n Heap zurückgeben. Ansonsten haben Sie ein Speicherleck (Memory Leak). Diese Aufgabe wird gewöhnlich in der Definition des Destruktors erledigt. In unserem Beispiel »Mensch" iSl das dynamische Klassenelement ein C-String (also cha r-Array), den Sie mit del e te [] freigeben müssen. De r Destruktor in der Datei »mensch.cpp" sollte demnach so aussehen (bzw. muss erweitert werden); Mensch : :-Mensch{ ) ( cout « name « " ist von uns gegangen\n" ; 11 dynamisches Element wieder freigeben delete [] name; Zum Testen kön nen Sie nochmals die Date i »main.cpp.. vom Abschnitt 4.4.5 verwenden, wo es darum ging, Objekte dynam isch anzulegen. An der Ausführung
321
I
4
I
Objektorien tiert e Programm ierung
im Vordergrund und der Verwendung der Methoden des Programms hat sich nichts geändert. Nur die Interna der Klasse wurden verändert. Hiermit wurde auch gleich ein weiterer Vorteil der OOP von CH demonstriert. Der Wartungsaufwand durch die Verwendun g von Klassen hält sich in Grenzen, da darauf geachtet wurde, dass die Schnittstellen (Kopf der Klassenmethoden) nicht verändert wurden.
4.4 ,7
Objekte kopieren (Kopierkonstruktor)
Sicherlich erinnern Sie sich noch an unsere Methode .. kopiere_MenschO". Diese Methode hätten wir uns auch sparen können, weil der Compiler schon von Haus aus, einen sogenannten Kopier- Konstruktor zur Verfügung stellt. Damit werden alle Eigenschaften (Daten) einer Klasse in das neue Objekt kop ien: Mensch pe r sonH "Adam- , 22 , Mensch : : MANN ) ; Mensch per son2{ personl) ; 11 Gl e i ch zu pers on2 - personl Damit wird das Objekt "person2 .. durch einen Aufruf des Standard-Kopierkonstruktors mit den Eigenschaften von "person1 .. initialisien. Natürlich können Sie einen solchen Kopierkonstruktor auch selbst dek larieren und definieren. Die Deklaration in der Headerdatei ,.mensch.h« als publ i c-Element der Klasse ,.Mensch« würde folgendermaßen aussehen: // men s ch . h publ i C; // Kopi er konstruktor Mensch( const Men sch & ) ; Die Syntax der Deklaration eines solchen Kopierkonstruktors lautet also immer: Klasse ( con st Klasse
& );
Eine Definition in ,.mensch.cpp .. ist nicht unbedingt nötig und kann auch in . mensch.h« mit zwei leeren geschweiften Klammern erfolgen. Wenn Sie den ~ Effekt« des Kopierkonslruktors testen wollen, können Sie auch folgende Definition dazu verwenden: Mens ch : :Mensch( con s t Mensch & ) I cout « "Kopie r - Ko nstrukto r auf ger ufen\n "; Allerdings können Sie diesen Kopierkonstruktor nur bei den Beispielen vor dem Abschni u 4.4 .6 (den dynamischen Klassenelementen) verwenden. Durch die
322
Verwenden von Objekten
Verwendung von dynamischen Klasse nelementen sind Probleme vorprogram· miert, weil Zeiger von verschiedenen Objekten auf denselben Speicherbereich zeigen würden . Im Beispiel der Klasse "Mensch .. würde beim Kopieren zweier Objekte auch dieselbe Adresse des Namens "kopiert .. . Sobald aber eines der Objekte zerstört bzw. verändert (vergrößert, verkleinert) wird, treten Probleme auf. Daher kommen Sie bei Objekten mit dynamischen Klassenelementen nicht drum herum, einen eigenen Kopierkonstruktor zu defini eren, der nicht den Zei · ger, sondern die Eigenschaften (Daten) mit kopiert. Bezogen auf unsere Klasse "Mensch", sieh t die Verwendung (mit Deklaration und Definition in den entspre· chenden Dateien) des KopierkonstruklOrs wie fo lgt aus: 11 mensch.h 11 Deklaration des Kop i erkonstruktors Mensch( const Mensch &person ) :
11
men sch. cpp
11 De f ini t io n des Kop i erkonstruktors Mensch : :Mensch( cons t Mensc h& person) I len - person . len : name - new cha r [len+l] ; strcpy( name , person . name ) : alter - person .alter : geschlecht - person . geschlecht :
11 ma1n.cpp
Mensch pe r sonl( "Adam" . 22 . Mensch : :MANN }: 11 Verwendung des Kop i erkonstruktors MenSCh pe r so n2( personl) : 4.4.8
Dynamisch erzeugte Objekte kopieren (operator={)
Im letzten Abschnin wurde kun erwähnt. dass eine Zuweisung wie
Objek t } - Objekt2 : etwa dasselbe ist wie
Klasse Objekt}( Ini t ialisierungs - Li st e ) : Klasse Objekt2(Obje ktl) :
323
I
4·4
I
4
I
Objektorientierte Programmierung
Bei dieser Methode (es ist tatsächlich eine echte Methode) handelt es sich um eine Standardzuweisung. Das Prinzip entspricht genau dem des Standard-Kopierko nstruktors. Dies bedeutet alle rdings au ch, dass hier die bekannten Probleme auftre ten , wen n ein Objekt dynamische Klasse nele mente verwendet. Daher muss auch die Standardzuweisung bei der Verwendung von dynamischen Elementen durch e ine selbst definierte Zuweisung ersetzt werden. Dabei muss auf das Prinzip der Opera lor-Überlad ung zurückgegriffen werden, das erst in Abschnitt 4.5 ausführlicher behandelt wird. Trotzdem soll kurz darauf eingegangen werde n, wie Sie den Zuweisungsoperator = der Klasse ,.Mensch.. überladen können. Zuweisungen werden immer von der Methode opera t o r- {) ausgeruhrt. Das bedeutet, dass eine Standardzuweisung wie Objekt2 - Obje ktl : dasselbe bedeutet wie Objek t 2 . oper ator- (Objek t l) ; Hierzu können Sie jetzt in der Praxis die Methode ope r ato r - () der Klasse "Mensch.. wie fo lgt überladen: 11
mensch . h
pub 1i c : 11 Zuweis un gsoperator übe rladen - Deklarat i on Mensch& M ensch :: operator~( const Mensch & person ) ;
11 mensc h. cPP 11 Zuweisungso perator überl aden - De f initio n
Mensch& Mensch ; ; operato r-( const Mensch& person) I 11 Au f Selbst - Zuwe i sung Oberprüfen if( this == &pg r so n ) I ret urn *th i s ; el se { l en - person . len ; name - new char[len+lJ : strcpy( name , pe r son . name ) ; al t er - pe r so n. alter :
324
Verwenden von Objekten
I
4 ·4
geschlecht - person . gesc hlecht ; return *this :
I
11 ma i n .cpp
Mensch pe r sonH "Adam" . 22 . Mensch : :MANN ) : 11 Zweisu ngsopera t or überlad en - Verwendung Mensch pe r son2 - personl ;
Das nur in Kürze, falls Sie den Zuweisungsoperator im Abschnitt 4.4.7 verwendet und sich gewunden haben, warum dies nicht funktioniert Die Operator-Überladung wird in Abschniu 4.5 erläutert. 4 .4 .9
Standardmethoden (Überblick)
Auf den letzten Seiten dürfte Ihnen aufgefallen sein, dass Ihnen der Compiler eine Menge Standardmcthoden zur Verfügung stellt. die jederzeit exp lizi t durch eigene Methoden ersetzt werden konnten. Diese vier Methoden sind: ..
Standardkonstruktor
..
Destruktor
..
Kopierkonstruktor
..
Standardzuweisung durch den Zuweisungsoperator
4.4.10 Objekte als Elemente (bzw. Eigenschaften) in anderen Klassen Es lassen sich neben den gewöh nlichen Datentypen na türlich auch Objekte ande· rer Klassen selbst als Eige nschaften (Daten) einer Klasse verwenden. Als Beispiel dient eine einfache Klasse "Haustier.. , di e auf das Grundlegende beschränkt ist und anschließend in der Klasse ..Mensch .. verwendet werden soll. Hier die Hea· derdatei ..htier.h «, bei der die implizite i nl i ne·Definition mit "eingepackt.. wurde, sodass alles zur Verfügung steht: /I htie r. h Ifi nclude Ifi nclude Ih fnde f HTI ER_ H Ifde f i ne _ HTI ER_ H using names pace std ;
3'5
4
I
Objektorien tierte Programm ierung
cl ass Haustier ( private : 11 Eigenschaften der Klasse Haustie r char so rte[30J : char rasse[30J : char name[25] ; pub l ; c ; 11 Kons tr ukto r en 11 De fault - Konstruktor Haus t ie r{ ) st rncpy( sorte , "? ", s i zeof("? " ) ) ; strncpy( rasse . "?" . sizeof( "?" ) ) : strncpy( name, "? ". sizeo f ( "?") ) ; Haustier( const char * s , const char * r, const char strncpy( sorte . s. s izeo f( so r te)-l ) ; strncpy( rasse , r , sizeofUasse)-l ) ; strncpy( name . n. sizeof(name) - l ) :
k
n ) f
11 Destruktor -Haust ier ( ) \
11 Fah i gkeiten (Me t hoden) der Klasse Haust i er
Hi er nu r au f Zugriffsmethoden besch rankt cons t char* geCsorte( 1 const { return sorte ; const char* geCra sse( 1 const { return rasse ; const char k get_n ame ( ) const ( re t urn name; I vOld set sorte( const char ' s ) I st rncpy( sorte . s , s i zeof (sorte l-l 1; 11
void set_rassel const char* r ) I strncpy( rasse. r. si7eofCrassel - l 1; void set_name( const char* n ) I strncpy( name, n. s i zeo f (name)-l ) ; I,
lIendi f In der Praxis könnten Sie diese Klasse »Haustier« wie die Klasse »Mensch« im Hauptprogramm einsetzen: 11 main_htier . cpp lI i nclude "ht i er . h"
326
Verwenden von Objekten
int main(voidl 1 Hau st i er tierl : Haust i er tier2 ( "Hund" . "Jack- Ru sse l". "Blin ki " l : tiert . set_so rt e( "Katze" l : tierl . set_rasse( "longhai r" ) : tierl.set_n ame( "M aunzi " l :
I
4·4
I
cout « t ierl . get_name ( ) « « tierl.geLs ortell « «tierl.geLrassell« ' \n ': cout « «
t ier2 . geLname( ) « ' , tier2 . geCs ortell « « t ier2 . get_rasse() « ' \n ': re tu r n 0:
Das Programm bei der AusfLihrung: Maunzi Ka tze longhai r Blinki Hund Jack - Russel
Allerdings entspricht dieses Beispiel nicht unseren Absichten in diesem Abschni tt. Wir wollen jetzt die Klasse .. Hausti er.. in einer anderen Klasse verwenden. Diese Klasse arbeitet dann mit dem Obj ekt .. Haustier.. - man spricht von einer »Hat-Beziehung.. . Natürlich verwende n wir wieder die Klasse ..Mensch«, da ein Mensch auch gerne ein .. Ha ustier.. hält. Hierzu müssen Sie zunächst be i den Eigenschaften (Daten) der Klasse ein neues Objekt vom Typ ~ H austier .. einfügen: // mensch.h # i ncl ude #1nclude " ht1e r, h" Ifif nde f _MENSCH_H Ifd e f ; ne _MENSCH_H using namespace std ; cl ass Hens ch 1 priva te; // Ei genschaften der Klasse Mensch char ' name ; i nt len ; uns i gned i nt a l ter ; bool gesc hl echt : Haustier tier;
327
4
I
Objektorien tiert e Programm ierung
publiC :
Hiermit haben Sie zunächst ein privates Objekt _tier« der Klasse »Haustier« zu den Klasseneigenschaften von -Mensch« hinzugefügt. Wenn Sie ein neues Objekt »Mensch. an legen, wird der Konstruktor (bzw. Default-Konstruktor, abhängig von der lnitialisierungsliste) aufgerufen. Befindet sich in einem solchen Objekt ein weiteres Teilobjekt (wie im Beispiel die Klasse _HaustieH), so wird auch der Default-Konstruktor des Teilobjekts aufgerufen . Wenn Sie also ein Objekt der Klasse »Menschoc wie folgt defin ieren Mensch person ;
wird neben dem Default-Konstruktor fi1r das Objekt "Mensch .. auch der DefaultKonstruktor für »Haustier.. aufgerufen. In unserem Beispiel sieht der DefaultKonstruktor flir "Haustier« so aus: Haustier( ) st r ncpy{ sorte . " ?" s i zeof{" ? " )) ; st r ncpy{ rasse . "?" s i zeof{" ?" )) ; st r ncpy{ name, "? ". sizeo f ( "?" ) ) ;
Alle Werte werden mit einem Fragezeichen belegt. Das können Sie natü rlich auch anders machen. Neben dem Default-Konstruktor wurde in der Klasse »Haustier.. noch ein weiterer definien, dem Sie die komplette Initialisierungslisle der Klasseneigenschaften übergeben können. Zurück zur Klasse »Mensch.. - wenn Sie ein Objekt der Klasse »Mensch .. anlegen, werden Sie dies auch mit einer Initialisierungsliste machen wollen. Somit benötigen Sie flir die Klasse -Mensch« einen Konstruktor, der das Teilobjekt der Klasse »Haustier« beachtet und es ebenfalls mit entsprechenden Werten versieht, also nicht mit den Defau lt-Konstruktor aufruft. Hierzu zwei Beispiele von expliziten Konstruktoren, die ein Teilobjekt auch exp lizit initialisieren . Dies lässt sich einfa cher realisieren, als es den Anschein hat: 11 menseh. h
pu b 1 i c ; 11 Konstruktoren Mensch( const char * o . unsigned int a . bool g . eonst ehar* "- eonst ehar* eonst ehar· Mensch( cons t char* n , unsigned int a . bool g . eo nst Haust1er t ) :
,.
328
"
)
,
Verwenden von Objekten
I
4·4
Sie haben nun die Möglichkei t, beim Anlegen eines Objekts das Teilobjekt " tie r~ der Klasse " H austier~ indirekt oder direkt mit Werten zu initialisieren. Hierzu die Definition der beiden Konstruktoren in der Datei "me nsch.cpp ~ :
I
11 mensch . cpp
Mensch : :M e nsch( const char * n . uns i gned i nt a , bool g . const c har* s , co ns t char* r, cons t cha r* na ) I len - strlen(n); name - new char[len+l] ; strcpy( name , n ) : alter - a ; geschl echt - g : tfer - Haustfe r ( s . r, na ) ;
Mensch : : Mensc h ( const cha r * n , unsigned int a , bool g , const Haustfer t } I len - strlen(n) ; name - new char[ l en+lJ : st rcpy( name, n ) ; alter - a ; ge schlech t - g ; t1er - t;
Hinweis Es sollte noch erwähnt werden, dass immer erst das Teilobjekt vor dem eigentlichen Objekt aufgebaut wird, damit der Compiler weiß, welche Arg um ente in der Initialisierungsliste zu welchem Teilobjekt gehören.
Gegebenenfalls werden Sie einige Methoden noch etwas ergänzen müssen. damit auch das Teilobjekt »tier« korrekt übernommen wird, das betrifft zum Beispiel den Kopierkonstruktor, da sonst beim Kopieren nicht die "Haustiere« übernommen werden, sondern lediglich neue die mit Fragezeichen erzeugt würden: Mensch : : Mensch( const Mensch& person) { len - pe r son.len ; name - new char[len+l] ; strcpy( name. person . name }; alter - person . alter ; geschlecht - person . geschlecht ; t1er - person. tier;
329
[« )
4
I
Objektorien tiert e Programm ierung
Jetzt fehlt nur noch der Zugriff auf die pu b1i c-Elemente der Klasse ,.Haustier« aus der Klasse ,.Me nsch .. heraus. Dieser erfolgt aus den Klassenmethoden der Klasse ,.Mensch .. , ind em zuerst der Name des Teilobjekts (hier ,.tier..) angegeben wird, dann folgt der Punkteoperator und zuletzt das entsprechende (publ i c-)Klassenclemem. [m Prinzip erfolgt der Zugriff genauso, wie Sie bisher auf die pub 1i cElemente einer Klasse von außen zugegriffen haben. Als Be ispiel habe ich der Klasse "Mensch ~ eine Methode "printaIlO .. hinzugefügt, die alle Daten mitsamt den "Haustier.. · Daten ausgibt. Die herkömmliche Funktion .. printO" ble ibt nach wie vor der Ausgabe de r Klasse "M e nsc h ~ vorbehalten: 11 mensch.h
publiC : 11 Ausgabe aller Eigenscha f ten mit der Klasse Haustier vaid printall( waid ) :
11 mensch . cpp
weid Mensch : : pri ntall ( wo i d ) [ cout « name « " " « alte r « " Jahre ( ": test_geschlec ht() : . ) Haust i er : cout « tie r. ge t _ sorte() « (R asse : « tie r. ge t _ rasse() "/Name :" « « tie r. geCname() " ))\n " : « «
.
[»]
.
.
Hinwe is Auch hier sei angemerkt, dass durch die Änderung der Klasse "Mensch« mit der Erweiterung ei nes Objekts der Klasse _Haustier« keine Probleme mit den Hauptprogrammen entstehen, da auf die Beibeha[tung der Schnittstelle geachtet wurde. Sie können also durchaus ein Beispiel der _ä[teren« Sorte testen, und es sollte ohne Probleme funktionieren.
Hierzu nun unser Hauptprogramm, das die Verwendung von Teilobjekten in anderen Klassen (hier de m Teilobjekt "tier.. der Klasse " Haustier .. in der Klasse "Me n s(h ~) demonstriert: 11 main . c pp lIi ncl ude "mensch . h"
int main( vo id) { Mensc h per sonl ( "Adam ". 22 , Mensch : :MANN . "Hund ". " Dackel" . "Wald ; " ) :
330
Verwenden von Objekten
I
4·4
Haust i er cat( "Katze" . "Pe r ser ". "Mi nz i" ) ; Mensch pe r son2( " Eva ". 19 . Mensch : : FRAU . cat ) : Mensch pe r son3 :
I
personl . p r int a ll() ; person2 . p r intall() : person3 . p r intall() : return 0 ; Das Programm bei der Ausführung: Adam 22 Jahre (m
int main(void) I Mensc h personl( "Adam" . 22 . Mensch : : MANN . "Hund" , " Dackel" , ·Waldi") ; Hausticr cat{ "l
« «
«
" Das Haust i er von " « personl . get_name() hei ßt ' « personl,tler.get_name() " und ist e i n · « personl.tler.get_s orte {) «
' \n ':
person2.tler.set_name( "M lnka " ):
331
4
I
Objektorien tiert e Programm ierung
cout « person2.tfer.geCname() return 0 ;
«
' \n ';
Auf der anderen Seite können Sie durch die private Defin ition eines Teilobjekts die Schn ittstelle des Teilobjekts nach außen »verstecken", sodass Sie bei einem Zugriff auf Elemente eines Teilobjekts gezwungen sind. in der Klasse hierftir eine eigenen Schnittstelle zur Verfugung zu steUen, was in der Regel zu empfehlen ist, da hi erbei nicht das OOP-Schema unterlaufen werden muss.
4.4.11 Teilobjekte initi alisieren Bisher wurde bei der Erzeugung von Objekten, die Teilobjekte enthielten, zunächst der Default-Konstruktor (der Teilobjekte) au fgerufen und das Teilobjekt mit den Standardwerten belegt. Erst anschließend erhielt das Teilobjekt bei der Zuweisung die richtigen Werte. Ein Vorgang mit doppelter Arbeit. der nicht unbedingt sein muss. e++ bietet mit einem speziellen Elementinitialisierer ei ne Möglichkeit an, Teilobj ekte einer Klasse explizit mit Werten zu initialisieren. Das soll am Beispiel des Konstruktors "Mensch", den Sie im leuten Abschnitt verwendet haben. gezeigt werden: 11 mensch.cp p
Mensch ; ;Mensch( cons t cha r"" n . un signed int a . baal g . const char · s . cons t char* r . const char ' na ) I len - strlen(n) ; name - new char[len+1 J: strcpy( name . n ) ; a lte r - a ; geschl echt - g : t ier - Haus t i er ( s . r. na ) :
Die Defi nition dieses Konstruktors können Sie nun wie folgt mit einer zusäuliehen Initialisierungsliste belegen: 11 mensch . cp p
Mensch :: Mensch( const cha r "" n . unsigne d int a . bool g . const cha r* s . const char* r . const cha r * na : tfer( s . r. na len - strlen(n) ; name - new char(len+ 1J : st r c py{ name . n ) :
332
Verwenden von Objekten
I
4·4
alter - a ; geschlecht - g;
Hinweis Bitte beachten Sie. dass bei der Verwendung von Elementinitialisierern wirklich nur die Definition verändert werden muss , Die Deklaration des Konstruktors bleibt gleich!
Durch den oben verwendeten Elememinhialis ierer wird jeut gleich der passende Konstruktor rur di e Eigenschaften aufgerufen und ni cht mehr der Defau h-Konstruktor. Die Liste enthält die Namen der Eigenschaften (Datenelememe) mit den emsprechenden Anfangswen en. Die Elememinitialisierer werden himer dem Funktionskopf getrennt mit einem Doppelpunkt angegeben. Die einzelnen Elemente werden durch ein Komma getrennt. Dies funk tioniert natürlich auch bei dem Konstruktor. bei dem Sie im Beispiel des Abschni tts zuvor als vienes Argumem das komplette Teilobjek t ,.Haustier.. übergeben haben: 11 mensc h.cpp
Mensch : : Mensch{ const cha r * n. unsigned int a . bool g , const Haust i e r t ) :t1er(t) I len - strlen( n); name - new char[len+l] ; strcpy( name , n ) ; alte r - a ; geschl ech t - g ;
Auch wenn Ihnen diese Möglichkeit, Teilobjekte zu in itialisieren, ein wenig überflÜSSig erscheinen mag. so hat diese Version doch große Voneile: ..
Das Laufzeitverhalten des Programms verbessert sich. Es muss nicht unnötig Speicherplatz angeforden (Default-Konstruktor) und wieder fre igegeben werden, um anschließend erneut Speicher anzufordern (Konstruktor). um das Teilobjekt mh Wenen zu initialisieren.
,..
Als Datenelemente können auch konstante Objekte und Referenzen verwendet werden.
4.4.12
Klassen in Klassen verschachteln
Zwar wird in der Praxis kaum davon Gebrauch gemacht, aber es sollte dennoch erwähn t werden, dass es auch möglich ist, Klassen zu verschachteln . Dies hieße
333
[«)
I
4
I
Objektorientierte Programmierung
dann, dass eine Klasse lokal innerhalb einer Klasse deklariert wird . Somit gilt die Klasse nur in ihrem Gültigkeitsbereich. /I
classA .cpp
class A I pub li c : private :
class B
II
l okale Kl asse
1/ }
,
Ein mögliches Anwendungsgebiet von verschachtelten Klassen wäre die Verwendung von Klassen, die mit gleichem Namen eine globale Gültigkeit haben. So könnten Sie in einer Klasse eine interne Klasse vereinbaren , ohne dass es zu Namenskonflikten kommt, weH die lokale Gültigkeit Vorrang hat. Im Grunde sollten Klassen, die innerhalb von Klassen verwen det werden, auch nur dort gelten, daher werden diese ja auch lokal vereinbart. Im Codeausschnitt . classA.cpp" kann, so definiert, keine der Klassen auf die El emente der anderen Klasse zugreifen. Also kann weder »A« auf »B« noch "B« auf Elemen te von »A« zugreifen. Wollen Sie, dass »A« Zugriff auf »B« hat, müssen Sie nur die Klasse »S« im pub l i c-Teil von "A« ve reinbaren und "A« nur zu einem fr i end (S iehe Abschnitt 4.4.16) von "B« erklären : /I
classA . cPp
class A I pu bl i c : class B II lo kale Klasse fr i end class A: pri~ ate :
}
,
Da sich der Umfang und die Unübersichtlichkeit von verschachtelten Klassen schnell ins Chaotische entwickeln können, empfieh lt es sich in der Praxis, d ie verschachtelte Klasse in der umgebenden Klasse zu deklarieren, aber außerhalb zu definieren. Bei der Definition der verschachtelten Klasse muss die innere Klasse über den Namen der äußeren Klasse und den Scope-Operator verwendet werden:
334
Verwenden von Objekten
11
I
4·4
classA . cpp
cl ass A { publ i C : // Dekla r ation der Klass e B class B: priva t e :
I
I,
11 De fi nition de r Klasse B cl ass A:: B I // Defi nition
4.4.13 Konstante Klasseneigenschaften (Datenelemente) Wolle n Sie in ei ne r Klasse Eigenschaften verwenden, die nicht mehr verändert werden können, so wird diese mit const deklariert. Beispielswe ise deklarieren Sie in der Klasse ... Mensch« das Teilobjekt ... tier« von der Klasse - Haustier« als konstantes Objekt: // mens ch . h class Me nsch private : // Eige nschaften de r Klasse Mensch char *name : i nt len : uns i gned i nt a l ter : boal gesc hlecht ; 110 - mannl i ch ; 1 - weibl i ch cons t Haust1er tier; pub 1i c :
Sie müssen allerdings darauf achten , dass gleich bei der Initialis ierung des Objekts der richtige Konstruktor aufgerufen wird, weil bei konstanten Daten keine späteren Zuweisungen mehr möglich sind . Das bedeutet, dass gleich bei der Defi ni tion des Konstruktors der Klasse jedes cans t- Element einen Elementinitialisierer benötigt. Folgender Konstruk tor würde zum Beispiel zu ei ner Fehlermeldung des Compile rs führen :
335
4
I
Objektorientierte Programmierung
Mensch : :Hensc h( const cha r* n, unsigned i nt a, baal g , const Haustie r t ) I len - strlen(n) ; name - new char[len+l) : s t rcpy( name , n ); alter - a : geschlec ht - g : 11 ! 11 Fehler 111 const-Elemente bereits 11 vom Default · Konstruktor vor belegt tier - t; Wie Sie be reits erfahren haben, wird beim Vereinbaren eines Objekts auch der Konstruktor eines gegebenenfalls vorhandenen Teilobjekts aufgerufen. In unserem Beispiel würde praktisch be im Erzeugen eines neuen Objekts ,.Mensch« schon der Default-Konstruktor aufgerufen, der alle Werte mit Fragezeichen belegt. Daher führt die anschließende Zuweisung zu einem Fehle r, weil das cons t -Element bereits mi t Werten belegt ist. Die Fehl ermeldung verschwindet, wenn Sie hierbei die Zeile t i er -
t;
entfernen, wodurch das Teilobjekt ,.tier" mit den Werten (Fragezeichen) des Default-Konstruktors vorbelegt wird. Dies ist allerdi ngs nicht im Sinne des Erfinders. Daher benötigt auch der Konstruktor einen Elementinitialisierer, um den Aufruf des Default-Konstruktors gegebenenfalls zu umgehen. In unserem Fall würde dies Fo lgendes bedeuten: Mensc h: :Mensch( const char* n , unsigned int a , bool g, const Haust i er t l :tier {t) I len - st r len(n) : name - new char[len+l) : st rcpy{ name . n ) : alte r -a : geschlecht - g : Dasselbe gill übrigellS auch CUr die elementaren Dawntypen , die Sie ebenfalls in einer Klasse als konstant defini eren können. Auch hier lässt sich der Elementinitialisierer einsetzen - und das rur alle Type n:
class A I pr i vate : eonst f nt f wert: pu bli c :
336
Verwenden von Objekten
I
4·4
/I Konstruktor
A{ lnt Icl wert . . . . l ;l wert I 1/ ...
I 4.4.14 Statische Klasseneigenschaften (Datenelemente) Wollen Sie bei Objekten einer Klasse nicht nur die Eigenschaften fü r ein Objekt allei n verwenden, sondern soll eine Eigenschaft gemeinsam (mit anderen Obj ekten) genu tzt bzw. geteilt werden . dann müssen Sie diese Klasseneigenschaflen als Hatie vereinbaren. Diese mit stat i e deklarien e Variable ist für alle anderen Objekte nur ein ma l im Speicher vorhanden. Vorwiegend werden solche statischen Eigenschaften verwe ndet. um bestimmte Informationen einer Klasse anzuzeigen, beispielsweise die Anzahl der bereiLS erzeugten Objekte einer Klasse oder zwischengespeichene Höchst- bzw. Niedrigwerte. Außerdem können statische Eigenschaften verwendet werden. um Daten temporär zwischenzuspeichern, sodass diese Daten wiederum den anderen Objekten zur Verfügung stehe n. Um ein statisches Element zu deklarieren, ist so vorzugehen, wie es schon in Bezug auf das Schlüsselwort s ta t i c beschrieben wurde: static ty p bezeichner ; Bei unserer Klasse .. Mensch .. wü rde sich das Beispiel ganz gut eignen. die Weltbevölkerung zu zählen: 11 mensch . h
cl ass Mensch private : // Ei gensch aften der Klasse Mensch char *name : i nt len ; uns i gned int alter ; bool gesc hlecht ; /10 - mannl i ch ; 1 - we ibl i ch Haustie r tier : statle lnt anzahlHenseh; pub 1i C: I,
337
4
I
Objektorien tiert e Programm ierung
Dieses so statisch vereinbarte Klassenelement einer Klasse belegt sofort einen Speicherplatz, auch wenn noch kein Objekt diese r Klasse existiert Deshalb müssen Sie diese Variable wie die Klassenmethoden auch, außerhalb der Klasse in einer Datei definieren und gegebenenfalls initialisieren_ Hierzu w ird auch die Klasse und der Scope-Operator : : benötigt. In unserem Beispiel sieht die Defi nition in der Datei »mensch.cpp .. so aus: 11 mensc h . cp p llinclude
int Mensch :: anzahlMensch - 0 : Beachten Sie außerdem , dass hierbei nicht mehr das Schlüsselwort s ta t i ( mitangegeben wird. Natürlich benötigen Sie jetzt auch Methoden, die auf den Wert diese r statischen Variable zugreifen. In unserem Fall ve rwenden wir die Konstruktoren, bei denen der Wert um eins inkrementiert wird, und den Destrukto r, der den Wert von »anzahlMensch.. um eins dekreme ntiert. Außerdem wird in diesem Beispiel bei der Verwendung des Kopierkonstruktors dem überlade nden Zuweisungsoperato r und der Klassenmethode »erzeuge« der Wen von anzahlMensch inkrementiert. Mensch : :Mensc h{ const cha r* n . unsigned int a . Mol g . co nst char · s . const char* r . const char* na ) ; tiere s . r . na ) len - strle n{n) ; name - new char [ l en+l] ; s trcpy( name . n ) ; alter - a ; geschlecht - g ; ++anzah1Henseh;
Mensch : :Mensch( const cha r* n , unsigned i nt a . bool g , (onst Haustier t ) : t i er(tl I 1en - strlen(n) ; name - new char[ l en+l] ; s tr cpy{ name , n ) ; alter - a ; gesc hle ch t - g ; ++a n Zll h1Hen s eh :
338
Verwenden von Objekten
Mensch : :Mensc hf cons t cha r* n. unsigned in t a . bool 9 ) I len - strlenfn ): name - new char[ l en+l) ; st rcpy( name, n ) ; alter - a ; geschl echt - g;
I
4·4
I
++~nzahlMe n sch;
Mensch : :Hensc hf const cha r* n. un signed int a ) len - strlen(n) ; name - new char[len+l) ; strcpy( name , n ) ; al ter - a ; II Zufallsgenerator ware si n n~oller - abe r iff a S 2 ) I ff Gerade oder ungerade Zahl geschlec ht - FRAU :
e I se I geschlec ht - MANN ; ++anzah l Mensch: Mensch : :Mensc h( const char* n ) r len - strlen(n) ; name - new char[ l en+lJ ; strcpy( name , n ) ; I I Neu gebo ren : -) alter - 0: 11 me hr Frauen bra ucht das Land ; - ) geschlech t - FRA U; ++anzahlMensc h; Mensch : :Mensc h( ) I II Raben -Va ter oder Raben -Mutter oder Trag Odie ? len - strlen( "Unbekannt " ) : name - new char[len+lJ : st rcpy( name . "Unbekannt " ) : 11 Neu gebo ren. ,. : - ) alte r - 0; 11 als Ausglei ch wieder ein Mann : - )
339
4
I
Objektorientierte Programmierung
geschlec ht -
MA~N ;
++~nzahlMensch;
Mensch : :Hensch( bool g ) I 11 Raben -Vater oder Raben-Mutter ode r Tr agöd ie? len - strlen( "Unbekannt " ) ; name - new char[len+l) ; strcpy( name, "Unbekann t " ) ; 11 Neu gebo ren : -) alter - 0; 11 als Ausgleich wi ede r ein Mann - ) geschlecht - g: ++anzahIMensch;
Mensch : :Me nsc h( cons t Hensch& person) I len - pe r son. le n; name - new char[ l en+l) ; str cpy( name. person . name ) ; alter - person . alter ; geschlec ht - person . gesc hlec ht; tier - person . tier ; ++anzahlHensch;
11 De f ini t io n des Destru ktors Mensch : :- Mensch( ) I cou t « name « • ist von uns gegangen\ n"; delete [] name ; -- anzahl Mensch;
11 Zuweisungs ope rator übe r la den Mensch& Mensch : : operato r- ( const Mensch& person) I 11 Au f Selbst-Zuweisung übe rp rü fen if( this -- &pe r son ) I return *this :
el se { l en - person.len : name - new char[ len+l J: strcpy( name . person. name ) : alter - pe rson. alter ; gesch lecht - pe rson . gesch l echt :
340
Verwenden von Objekten
I
4·4
++anz ah 1Mensch; re turn*th is :
I
void Mensch: :erzeuge(canst cha r* n . unsigne d int a . boal 9 ) ! l en - st r len( n ) ; name - new char[len+l] ; st rc py( name . n ) : alte r - a ; geschlecht - g : ++an za hl Hensc h ; Sofern Sie ein static- Element im pUblic-Bereich deklariert haben, ist es kein Problem mittels Mensch person : 11 Zugriff auf static-Element 11 ... wenn anzahl Mensch publi c ist cout « Mensch : : anzahlMensch « ' \n ' ; 11 .. . ode r auch so cout « person . anzahl Mensch « ' \n ': darauf zuzugreifen . Mir persönlich gefallt die Möglichkeit »Mensch ::anzahlMensch .. besser, da man hier sch neller erkennt dass es sich um eine statische Variable handelt, die unabhängig vom Objekt ist. Allerdings werden Sie auch in der Prax is relativ selt en ein stat i c-Ele ment im publ i c-Berei ch deklarieren, sondern eher mit Zug riffsmethoden auf das Element zugreifen. Um hierbei auf eine vom Objekt unabhängige Variable zuzugreifen. sollte man auch eine vom Objekt unabhängige Methode erstellen (siehe nächsten Abschni tt).
4_4.15 Statische Kl assenmethoden Im letzten Abschnitt wurde eine vom Objekt unabhäng ige statische Variable erzeugt und verwendet. Um auf den Wert dieser Variable n im pri va te-Bereich zuzugreifen, sollte man in der Praxis eine vom Obj ekt unabhängige Zugriffsmethode verwenden . Eine solche statische Methode realisieren Sie ebenfalls mit dem Schlüsselwort s tat i c. Wird die statische Methode außerhalb der Klasse definiert (was im anschließenden Beispiel der Fall ist), so erfolgt das Schlüsselwort s ta t i c nur be i der Deklaration der Methode. In unserem Beisp iel:
341
4
I
Objektorientierte Programmierung
11 mensch . h
cl ass Mens ch private : 11 Ei gensch aften der Klasse Mensch char *name ; i nt 1en ; uns i gned i nt alter ; baal gesc hlecht ; /10 - m
statle l nt get_anzahlMensch{ vold ): I, Bei der Definition der lugriffsmethode wird das Schlusseiwort s t at i c nicht mehr ben ötigt: 11 mensc h. cp p
i nt Mens ch: : anzahlMens ch - 0:
l nt Mensch::get_anzahlMen sc h{ vo1d ) { return anzahl Men sc h: Zwar können Sie diese Zugriffsme thode jetzt auch noch über jedes Objekt aufrufen, aber man sollte den direkten Aufruf über e ine Klasse und den Scope-Operator vorziehe n, da hierdurch auch der Eindruck entstehen soll, dass diese Methode nicht vom Objekt abhängig ist. Somit kö nnen Sie außerhalb der Klasse folgendermaßen die Anzahl von Menschen abfragen : Men s ch ; ; get_a nza h1Hen sc h ( ) Den Zugriff auf die neu implementierte statische Eigenschaft (»anzahlMensch«) und die statischen Zugri((smethoden (»geLanzahIMenschO«) soH das (olgende Hauptprogramm demonstrieren: 11 main . c pp llinclude "mensch . h"
int main( voidJ { Mensc h pe r sonI< "Adam ". 22 . Mensch : :MANN. "Hund ". " Dackel" . "W al d;" );
342
Verwenden von Objekten
I
4·4
Haust i er cat( "Katze" . "Per ser ", "Mi nz i" ) ; Mensch pe r so n2( " Eva ". 19 . Mensch ; : FRAU . cat ) ; Mensch pe r so n3 ; cou t
I
«
"Menschen : « Mensc h:: get_anzahlMensch() «
' \n ';
Mensch person4 : Mensch personS : cou t « "Menschen : «Mensch :: get_anz ah lMenSCh{) « cout « "Menschen: " « Mensc h: : ge t _anzahlMensc h() « return 0:
' \n ':
' \ n':
Das Programm bei seiner AusfQhrung: Menschen : Menschen : Unbekannt Unbekannt Mensche n:
3 5 ist ist 3
,oe ,oe
'"' '"'
gegangen gegangen
4.4.16 friend - Funktionen bzw. friend - Klassen Globale Funktionen und Methoden anderer Klassen dürfen normalerweise nicht auf die privaten Elemente einer Klasse zugreifen. Nur so ist sichergestellt, dass Zugriffe auf Objekte von fremden Objekten und Funktionen geschützt si nd. Mit dem Schlüsselwort friend können Sie diese Datenkapselung ,.aufbrechen •. Wenn Sie in eine r Klasse eine Funktion oder andere Klassen zum »Freund .. (fri end) erklären, hat dieser ,.Freund« Zugriff auf alle privaten Elemente. Natürlich bedeutet dies zum Beispiel bei Klassen nicht. dass in dem Fall . dass ei ne Klasse eine andere Klasse zum ,.Freund.. erklärt, diese Klasse ebenfalls Zugriff auf die privaten Elemente der anderen Klasse erhält - oder einfacher ausgedruckt. die »Freundschaft.. von Klassen beruht nicht auf Gegenseitigkeit. Sonst könnte man jede Datenkapselung beliebig unterlaufen, Hierzu ein e infaches Beispiel aus unserer Klasse ,.Mensch., Ausnahmsweise beginnen wir mal mit dem Hauptprog ramm: 11 main , CPP
lli ncl ude "me nsch . h" using name space std ;
343
4
I
Objektorien tierte Programm ierung
vo1d pr1nt_name( Mensch& p J{ cou t « wDer Name: W « p.name « ' \n ' ;
int main(voidl 1 Mensch pe rsonlt "Adam" . 22 . Mensch : :MANN . "Hund" . "Dackel" . "W aldi· ) ; pr1nt_name(personll; return 0: ohne irgendwelche Vorkehrungen würde dieses Beispiel nicht funktionieren, da hier in einer globalen Funktion . princ nameQ« versucht wird, auf eine private Eigenschaft (.nameoc) zuzugreifen. Dies ist in der Regel den Klassenmethoden vorbehalten . Wollen Sie aber jetzt dieser globalen Funktion trotzdem Zugriff auf die pr i va teEigenschaften der Klasse ~ Mensch oc gewähren, brauchen Sie diese Funktion nur in der entsprechen den Klasse mit dem Schlüsselwort f r i end zu deklarieren : 11
mensch.h
class Mensch private : 11 Eigens cha ften der Klasse Mensch char *name : i nt len : uns igned i nt alter : baal gesc hlecht : 110 - mannl i ch : I - weiblich Haus t ie r tier : static int anzahl Mensch ; void test_geschlechte void 1; pub li c : fr1end vo1d pr1nt _name( Mens ch& Men schPtr) ; I,
Zwar sp ielt es keine Rolle, ob Sie diese Deklaration im public- oder privateBereich vereinbaren, aber im Grunde soll eine solche Erweiterung als öffentliche Schn ittstelle dienen, deshalb sollte di e Vereinbarung sinnvollerweise im pub 1i cBereich vorge nommen werden. Mehr is t jetzt nicht nötig, um mit der globalen Funktion . princnameOoc auf private Elemente einer Klasse zuzugreifen.
[» J
Hinweis Eine f riend -FunKtion stellt keine Methode einer Klasse dar, weshalb Ih nen auch kein t hi s-Zeiger zur verfagung steht.
344
Verwenden von Objekten
Natürlich sind neben friend- Funktionen auch friend- Klassen möglich. Auch hierbei gilt, dass nur einseitige freundschaftliche Verhältn isse eingegangen werden können. Beispiel einer solchen fri end.Vereinbarung: 11 Vorwartsde klarat i on class B·
class A ! priva t e : i nt iwe r t : publ i C : fr l end c1ass 8: I,
cl ass B f private : pub 1 i C : void funcoCB( void ) :
11 Def i nition void funcof_ B( void ) 1 A a: 11 Möglic h, we il Klasse B ein Freund von A ist a .lltl er t - 100;
Eingesetzt wird die friend -Techn ik häufig, wenn man globale Funktionen verwenden will und keine Klassenmethoden geeignet sind. Als Beispiel dient hier das Überladen von Operatoren. Allerdings sollte man immer beachten, dass Sie mit der fr i end-Deklaration das Prinzip der Datenkapselung der OOP aufweichen. Daher sollte man es nur einsetzen, wenn es nicht anders möglich ist. Gerade in der Praxis hat sich schon oft gezeigt. dass der Zugriff von "fremden.. Funktionen auf private Daten einer Klasse zu Fehlern führ t Dies muss nicht zwangsläufig der Fall sein. aber spätestens wenn Sie eine Klasse ändern oder erweitern und dabei nicht aufpassen, treten Probleme auf. Besonders die Verwendung von friend-Klassen gilt al s schlechtes Design. In solch einem Fall soll te man den Entwurf der Klasse nochmals überprüfen.
345
I
4·4
I
4
I
Objektorientie rte Programmierung
4 .4 .17
Zeiger auf Eigenschaften einer 1
Neben den üblichen Zeigern. mit denen Sie bestimmte Variablen adressieren können, können Sie in e ++ auch Zeiger verwenden, die auf bestimmte Elemen te eines Objekts zeigen können. Dabei ist sowohl die Adress ierung von Eigenschaften also auch von Methoden möglich. Die Deklaration von solchen Zeigern sieht im Gegensatz zu herkömmlichen Zeigern wie folgt aus: 11 He r kömmliche Synta x Typ * Pt r; 11 Elementzeiger Typ Klasse : :* ElementPt r;
Es wird gleich deutlich, dass dieser Zeiger nur auf Elemente ei ner bestimmten Klasse verweisen kann . Natü rlich gelten hier nach wie vo r die üblichen Zugriffsrechte. Von außen können Sie aber nicht auf pr i va te -Eigenschaften oder Methoden einer Klasse zugreifen. Für solche Fälle müsse n Sie entweder Methoden oder friend -Fu nktionen (Siehe Abschnitt 4.4.16) verwende n. Nebe n der Klasse sind solche Elementzeiger auch an den Datentyp gebunden, den diese re ferenzieren. Außerdem gibt es einen weiteren ,.internen.. Unterschied zwischen Elemen tzeigern und herkömmlichen Zeigern. Denn im Grunde hat jedes Objek t eine Anfangsadresse. und die Adresse auf das Element, wohin der Elementzeiger verweist. stellt das Offset dar. Wenn Sie zum Beispiel ein ganzes Array von Objekten haben, zeigt ein einmal referenz ierter Zeiger daher immer auf dasselbe Element eines j eden Klasse n-Arrays - das heißt. es muss nicht immer wieder von neuem referenziert werden. Vor dem Zugriff auf den Elementzeiger müssen Sie diesen selbstverständlich auch inHialisieren. Hierbei komm t wieder die übliche Variante mithilfe des Adressoperators zum Einsacz : Element Ptr - &Klassenname ;; Element ; Da hierbei auf einzelne Elemente einer Klasse zugegriffen wird, gibt es mit * und - )* zwei neue Operatoren. Wobei der Zugriff analog zu den Gegenstücken des Punkt- und Pfeilope rators funktionie rt. Beispielsweise wird der Operator * zum Zugriff über ein Objekl wie folgt verwendet: Objekt .* El ementP t r Der Operator - ) * wird hingegen zu m Zugriff übe r eine n Zeiger auf das Objekt fo lgendermaßen verwendet: Objekt Ptr - )*ElementPt r
Verwenden von Objekten
Der Elememzeiger soll bei der Klasse ,.Mensch« verwe nd et werden. Um hier keinen allzu großen Aufwand zu betreiben. soll eine globale fri end-Funktion in das Beispiel eingebaut werden. Fügen Sie daher in d er Headerdatei "mensch.h" fol ge nde Deklaration der Funktion ,.d urchschnitcO" in den publ ic-Bereich ein:
11 mensc h.h cl ass Mensch priva t e :
pub l ; C: friend 1nt dur chs c hnitt( Mensch"" Hens c hPtr. int anzahl );
Diese Funktion soll den Altersdurchsch ni(t mehrerer erzeugter Menschen berech nen. Hierzu benötigen Sie nur noch die Haupt(unktion mit der globalen Funktion .durchschnittO":
11 main . c pp lIinclude "mensch . h" int durchschnitt< Mensch* Mensch Pt r . in t anzahl) 1 unsigned int gesamt - 0 ; unsigned int Mensch ::* alt erP tr ; 11 alterPtr zeigt i mmer auf das El ement alter in der 11 Klasse Mensch alterPtr - &H ensc h::alt e r; fore i nt i - O; i
i nt main(void) I Mensch pe r sonen[] - I Mensch( "Adam " , 22 , Mensch : : MANN ) , Mensch( "Eva" . 19 ) , Mensch( "Jorgen" , 30 , Mensch : : MANN ) , Mensch( "Jonata hn " . 3 , Mensch: : MANN ) , Mensch( "Fa tma" , 36 ) I, lnt al ter schnltt - durchschnitt( personen , (sizeof(personen)/s i zeof(Mensch» cout « "Durc hschnittalter der"
347
I
4·4
I
4
I
Objektorientierte Programmierung
«
(sizeo f (pe rsonenl/sizeo f (Menschll Personen ist . « al ter schnltt« ' \n '; return 0;
«"
Das Programm bei der Ausführung: Durchschnittalter der 5 Per sonen is t : 22 Natü rlich soll das Beispiel nicht darüber hinweg täuschen, dass hier nur ein lesender Zugriff auf die Klassenelemente über den Zeiger möglich ist. Sie können hiermit auch aufjeden anderen Typ der Klasse zugreifen, wenn dieser den Angaben entspricht: unsigned int Me nsch ; :* alterPtr ; Würden sich in der Klasse ~Me nsch " weitere El emente mit un signed int befinden, so könnten Sie diesen Zeiger auch ohne Probleme für diese Elemente verwenden. Außerdem kann hierm it ein Zeiger auf Methoden vereinbart werden, was aber den Code unnötig verkomplizieren kann. Das Beispiel mit der Klasse .. Mensch" demonstriert den Elemenueiger allerdings recht ungenau. Daher hierzu nochmals ein unabhängiges Beispiel. das auch die Verwendung über einen Zeige r auf ein Objekt, den Operator · >*, demonstriert: 11 elementzeiger . cpp lIinclude us i ng name space s t d ;
class Elementzeiger pub 1i c : int awert ; int bwert ; 11 Konstruktor Elemen t zeige r (int a , int bl I awert'"' d ; bwert '"' b: 11 Impl i zit inline
yoid pr i nt (yoidl I cout cout I,
348
« «
" \t awe r t ist " \t bwert ist
« awe r t « «bwe rt «
' \n ': ' \n ':
Verwenden von Objekten
int main (~oid) ! Elementzeige r objektl (1.2) . objekt2 (3 ,4) ; Elementzeige r *P trobjekt : cou t « ~ W e r te von objektl : \ n~: objektl ,pr i nt () : cou t « ~ Wer te von objekt2 : \ n~: objekt2 , pr i nt () :
4·4
I
11 zeiger auf int Elemen te in der Klasse Zeigertest
i nt (EI ementze i ger : : * I ntegerE l ementzei ger) : 11 Se t zen des Elementzeigers auf Ele ment awert
Int egerElementzeiger - &Elementzeiger : :awer t : 11 Zugri ff El ementzei ger : awert in obje ktl wi rd geaend ert
objektl.*IntegerElementzeiger - 11 : 11 Zugrif f Elementze i ger : awert in obje kt2 wird geaende rt objekt2 ,* Intege rElemen t zeiger ~ 33 : 11 Se t zen des Elementzeigers auf Element bwert
rntegerElementzeiger - &Elementzeiger : : bwert : 11 zugriff Elementze i ger : bwert in obje ktl wird geaende rt
objek t l .* IntegerElementzei ger - 22 : 11 zugrif f Elementze i ger : bwert in obje kt2 wird geaende rt objek t 2 *Integer Elemen tze i ger - 44 : cout « ~ \ nWerte von obje kt l : \n ~: objektl.print (l : cout « "Werte von objekt2 : \n ": objekt2 . print (l : 11 Zugrif f wie oben (au f bwert der bei den Objekte) 11 nur Obe r Zeiger auf ein Elemen t zeiger'Obje kt
Ptrobjekt Ptrobjek t Ptrobjek t Ptrobjekt
I
- &ob jek tl: >*I ntegerElementzeiger - 222 : - &obj ek t 2: >*l nteger Elementzeiger - 444 :
cout « "\nWerte von objektl : \n" : objektl .pr int (l : cout « "Werte von objekt2 : \n ": objekt2 . pr int (l : return 0;
349
4
I
Objektorien tiert e Programm ierung
Das Prog ramm bei der Ausführung: Werte von objektl : awert i st bwert i st Werte von obje kt2 : awert i st bwert i st Wer t e von obje ktl : awe r t ist bwe r t ist We r t e von obje kt2 : awe r t ist bwe r t ist Werte von objektl : awe r t ist bwe r t is t Wer t e von obj ekt2 : awe rt is t bwe rt is t
I
2 3 4
II
22
33 44
II
212
33 44 4
In der Praxis sind allerdings genau diese Beispiele weniger tauglich , da hierbei immer wieder die Daten kapselung unterlaufen wird, Einmal wurde eine globale f riend-Funktion verwendet und das zweite Mal wurden die Eigenschaften einfach pub1i c gemacht. Dass dies nicht im Sinne der aop ist, wurde bereits erwähn t. Wollen Sie dennoch diesen Vorgang mit dem Elementzeiger realisieren, sollten Sie ih n auf die Zugriffsmethoden anwenden. Im Folgenden wu rde das Beispiel ,.elementzeiger.cpp" um weitere Zugriffsmethoden erweite rt, die im Code mit Elementzeigern verwendet werd en sollen: 11 elementzeiger2 . cpp lIinclude
class [lementze i ger priva t e : i nt awe r t : i nt bwe r t : public : 11 Konst r uktor Elementzeiger (int a , int bl I awert - a ; bwert - b;
350
Verwenden von Objekten
waid print (wa id) I cout « "'t awert ist cout « "'t bwert ist i nt get_awerte wa i d i nt get_bwerte wa i d wa i d set_awerte i nt a wa i d set_bwerte i nt b
« awert « "' n" : « bwert « "' n":
4·4
I
return awer t; I return bwer t; I I awert - a ; I I bwert - b; I
I,
int main (woi d ) ( Elementzeig er objektl (1.2) . objekt2 (3 .4) : 11 Ze i ger auf Methode woid (int) in Klasse Elementze i ger woid (Elementzeiger :: *VoidMethodelntParamPtr){int) ; /1 Zeige r auf Methode int (vaidl in Klasse Elementzei ger i nt (EI eme ntz e i ger : : * J ntM et hod eVoi dParam Ptr) (woi d) ; Zeiger auf di e MethOde geCawert se tzen - &E 1eme nt ze i ge r : : get_dwer t : 11 Zeige r auf die Methode set_awert setz en Vai dMethod e Int Par amPt r - &E I ementzei ge r: : set_awert ; 11
! ntMet hodeVoi d Pa ra mPt r
11 set_awert f ar objek t l ausf ahren
(objek t l .' VoidMethodelntParamPtr)(ll) ; 11 set_awert f ar objek t 2 ausf ahr en
(objek t 2.*V oid MethodelntPa ramPtr)(33) ; 11 Au f ru f von get_awert f Or beide Objekte cout « "awert (objekt}) : " « (obje ktl .* IntMet hod eVoidParam Pt r )()« "'n "; cout « "awer t {objek t 2J : " « (Objekt 2.* In t Me th od eV oidParam Ptr)(J « "\n " : 11
I
Selbige r Vorgang jetzt f ar bwer t de r be i den Obj ekte
Zeiger auf die Methode get_bwert set zen IntMethodeVoi dParam?tr - &Elemen t zeige r: :geLbwert : 11 Zeiger auf die Methode seCbwert setze n Voi dMethode Int ParamPt r - &El ementzei ge r: : set_bwert : 11
se t _bwe rt fOr objektl ausfahren (objektl .* VoidMethodelntParamPtr)(22J : 11 set _bwe rt f Or objekt2 ausfOhren (ob je kt2 . *VoidMet hade In t Pa ra mPt r ){ 44J : 11
351
4
I
Objektorien tiert e Programm ierung
// Au f ru f von get_bwer t f Or beide Objekte cout « "bwe rt (objektl) : « (o bjektl . 'l ntMeth odeVoid ParamPtr)()« cout « "bwe r t (objekt2) : " « (ob jekt2 .* IntMethodeVo idParamPtr)()« return 0:
" \n~ :
" \n~ :
Das Programm bei der Ausführung:
awert awert bwert bwert
4.5
(Obj ekt!) : (Ob j ekt 2) : (ob j ekt!) : (objekt2) :
II
33 12 44
Operatoren überladen
Da Sie in CH mit Klassen bzw. Strukturen neue bzw. fortgeschrittene Typen erzeugen, ist es glücklicherweise auch möglich, die meisten vordefinierten Operatoren zu überladen. So lässt sich zum Beispiel der Operator + so überladen, dass hiermit komplizierte Be rechnungen mit komplexen Zahlen oder auch Manipulationen mit Strings gemacht werden können. Vor allem ist es dadurch auch möglich, mit « die neuen Typen auszugeben, mit » einzulesen und mit - eine Zuweisung vorzunehmen.
4.5.1
Grundlegendes zur Operator-Überladung
Dass in eH Ope ratoren überlad en werden können, verdanken sie den sogenannten Operatorfunktionen, die implementiert sind . Dies sind Funktionen, die mit dem Schlüsselwort ope rator und dem eigen tlichen Operatorsymbol vom Compiler aufgerufen werden . Die meisten Operatoren können überladen werden (siehe Tabelle 4.1 und 4.2). Operatoren
Bedeutung
+- '" /1: - (unär) ++--
Arithm etische Operatoren
+--- *-1-1:-
11 ! --! - « - »&&
Logische Operatoren Vergteichsoperatoren Zuweisu ngsoperatoren
&I~-»«
Bitoperatoren
&-1- ~ - » - « Tabelle 4.1
352
Operatoren. die überladen werden können
Operatoren überladen
Operatoren
Bedeutung
• -) -) * .
Sonstige Operatoren
( ) [1 & new dele t e ne w[ ] del ete(]
BedeutunG Zugriffsoperator Zugriffsoperator (Elementzeiger) Scope-O perator (Bereichsoperator)
?:
Ternärer Operator (bedingte Auswertung)
s 1zeof
Größe von Objekten
Tabell e 4.2
4 ·5
I
Tabell e 4.1 Operatoren, die überladen werden können {Forn.}
Operatoren
I
Operatoren, die nicht überladen werden können
Schlüsselwort operator Alle Operatoren, die in der Tabelle 4.1 aufgelistet wu rden , können Sie entweder in der üblichen Operatornotation verwenden oder aber in der Funktionsnotation. Beispielswe ise enthält der Ausdruck objektl + objekt2 den Operator +. mit dem hier zwei Werte addiert werden. Im Fall von numerischen Wenen ist das sehr sinnvoll. Aber es wird hierbei von Objekten eines bestimmten Typs ausgegangen. Somit entspricht dieser gezeigte Ausdruck fol gender Funktionsnotation: objekt l .operator+(objekt2) Somit werden überladene Operatoren wie normalen Funktionen definiert. Wobei die Syntax eines solchen Funktionsnamens wie folgt auszusehen hat: Klass enname operato r@( Par ameter) Das Ze ichen @ hinter dem Schlüsselwort ope rator muss durch einen Operator ersetzt werden, der naeürlich auch überladbar sein muss. Das ist in der Praxis einfache r, als es sich in der Theorie anhört. Die Regeln
Mit der Operator-Oberladung können Sie einiges ,.verbiegen ... abe r trotzdem gibt es einige Aspekte. die Sie hierbei beachten müssen oder nicht »verbiegen« können:
353
4
I
Objektorientierte Programmierung
.. Operator-Oberladungen finden immer im Zusammenhang mit Klassen statt. Das heißt. es ist nicht möglich, diese Operatoren bei den Basisdatentypen »umzubiegen... ..
Es lassen sich kei ne neuen Operatoren dami t erzeugen, das heißt. Sie können nur Operatoren überladen , die bereits existieren. Neu e Operatorsymbole (wie z. B. H ) lassen sich hiermit leider nicht einführen .
..
Die Operanden eines Operators können nicht verändert werden . Ein binärer Operator hat nach wie vor zwei Operanden und ein unärer einen. Der ternäre Operator ? : enfa llt hier, da diese r nicht überladen werden kann.
.. Auch die Priorität der Operatoren verändert sich nicht. Der Operator * zum Beispiel besitzt im mer noch eine höhere Priorität als der Operator + (Punk tvor-Strich·Rege!ung, siehe auch die Prioritätstabelle im Anhang des Buchs). .. Operatoren dürfen außerdem keine Default-Argum ente erhalten und müssen natürlich dieselbe Argumentenzah l wie der ursprünglich e Operator enthalten. Beispiele zu Demonstrationsz wecke n
Es ist schwierig, hierflir einfache Beispiele zu Demonstrationszwecken zu erstellen. Manchmal macht eine Operator-Überladu ng bei ei nem Beispiel keinen Sinn, daher habe ich mich entschlossen, zwei verschiedene Themen zu verwenden . Ein Beispiel verwendet eine Klasse mit komplexen Zahlen und ein anderes Beispiel verwen det eine Klasse mit Strings (mit der dynamisch Strings angelegt werden). Beide Beisp iele verwenden nichts, was in diesem Buch bisher noch nicht besprochen wurde . Wenn Sie etwas nicht verstehen, sollten Sie nochmals zum entsprechen den Abschnitt zurückblättern. Di e Beispiele werden außerdem in einer Datei zusammengefasst. was in der Praxis natürlich nicht so sei n soll te.
[»]
Hinweis Natürlich stellt diese String-Klasse nur eine Demonstration dar und sollte nicht gegen die bereits kurz erwähnte Standard-String-Bibliothek ef5etzt werden .
Zuerst das Beispiel mit der dynamischen String·Klasse (zum Testen auch immer gleich m it einer Hauptfunktion); "
String.cpp
llinclude llinclude us i ng namespace std : class String I pr i vate : cha r "- buffer : unsig ned i nt len :
354
Operatoren überladen
I
4·5
publ i c : 11 Konst rukt oren Str ; ng{ co nst char * s·"") ( len - strlen(s) : buf f e r - new char [len+ll : st r cpy( buffer . s ) ;
I
11 Destrukto r ~String()
( delete [] buffer : )
11 KopierkonstrLJktor - ex pliz it nöt i g da 11 dynamischer Spe i cherbe r eich ~erwendet wird
String( const String& s ) ( len • s.len : bu f fer - new char [len+l] : strcpy( bLJ ffer . s . bLJffe r ) : 11 Zugri f fsmethdoe char * get_String() const ( return bLJffe r: )
I, int main( String cout « retLJrn
'.'oid ) ( st r i ngl("Adam " ) : stringl . get_String() 0:
«
"\n" :
Das zweite Beispiel, das in diesem Abschnitt der Operator-Überladung zur Demonstration verwendet werden soll. benutzt komplexe Zahlen mi t ei nem realen und einen imaginären Anteil. Hier die Klasse lO myComplex« (ebenfalls mit einer Hauptfunklion zum Testen): Hinweis ALJch hier gilt. die selbst defin ierte Klasse »myComplexoc sollte nicht gegen die bereits bewährte Klasse der entsprechenden Standard bibliothek ersetzt we rden. 11 complex . cpp ffinclLJde
using namespace
~td :
cl ass myCom ple ;w; priva t e : double _ r eal : double _ image : pub 1 i c : myComplex( double vall-O . O. doubl e valZ-O . O ) (
355
[«)
4
I
Objektorientierte Programmierung
_real - va l l ; _ i mage - val2 ; vo i d print_Comple x( ) I (out« rea l« "+" « _image «
int main( void ) I myComplex val1(l . l , 2 . 2) ; myComplex vaI2(3 . 3 , 4.4); cout « "We r t von vall : cout « "We rt von val2 : return 0:
"'n ";
vall . printJomplex() ; vaI2 . print_Complex() ;
Auch hier haben wir uns zunächst auf das Nötigste beschränkt. Mit den beiden Klassen werden Sie in den nächsten Abschn itten den ein oder anderen Operator überladen. Das Beispiel mit den komplexen Zahlen stellt eine Erweiterung der reellen Zahlen dar. Damit sind komplexere Berechnungen möglich, die mit reellen Zahlen nicht machbar sind. Solche Zahlen werden vorwiegend be i Berechnungen von elektronischen Schaltungen verwendet. Jede dieser Zahlen besitzt einen reellen Anteil Lrea l ), den Realteil, der ei ne reelle Zahl ist, und einen Imaginäneil L ima ge), der ein reelles Vielfaches von i=(Wurzel),j ist. Im Grunde iS( es allerdings nicht so wichtig, wenn Sie sich nicht mit dieser Mathematik auseinan dersetzen wollen - im Beispiel geht es nur um die Operator-Überladung von Klassen. 4.5.2
Überladen von arithmetischen Operatoren
Zunächst soll mit dem Operator +- ein einfacher arithmetischer Operator überladen werden. Beispielsweise soll für die Klasse »S lring~ fo lgender Ausdruck stringl+-str i ng2 ; dazu führen, dass der Inhalt von »s tringb am Ende von »stringh hinzugefügt wird. Beachten Sie bitte, dass + und +- zwei verschiedene Operatoren sind. Operator-Üb erladung als Klassenmethode
Um also den In halt zweier Strings zu »addieren ... (so fern man davon sprechen kann), benötigt man zunächst den Rückgabetyp des überladenen Operators. Da Sie hier vorhaben, zwei Objekte der Klasse ,.String" zu add ieren, ist es logisch, dass der Rückgabewen vom Typ ,.String" ist. Somit könnte die Deklaration dieser Operator-Überladung wie fo lgt aussehen:
356
Operatoren überladen
I
4·5
String operator+- ( const String& strl ) ; In unserem Fall entsp rich t die Deklaration der Definition, weil aus Übersichtlichkeitsgründen gleich alles in eine Quelldatei geschrieben wird. Bevor Sie die komplette Definition und das kompl eue Listing in Aktion sehen, stellt sich noch die Frage, wie man diese (Operator-Oberladungs-)Methode aufrufen kann. Betrachtet man die Syntax der Operator-Überladung, wäre fo lgender Aufruf möglich: stri ngl . opera tor+-( s t r i ng2) : Dieser Aufruf der Operalorfunklion mit operator+- () hat dieselbe Bedeutung wie: stringl +- str i ng2 ; Sie sehen, dass man mit der Operator-Oberladung hervorragende Schnittstellen für d ie Klassen anbieten kann. Allerdings sollte man eine solche Operator-Oberladung auch sinnvoll einsetzen_ Zum Beispiel ist es in de r Praxis sinnlos. zwei Strings zu multiplizieren (»stringl *", string2 «). Hierzu das Beispiel de r Operalor-Überladung vom +- -Operator der Klasse ,.String«: // St r ing2 . Cpp lIinclude lIinclude us i ng namespace std : class String [ pr i va te : char "buffer : uns i gned int len ; pub 1i c : // Konst ruk toren Str i ng( const char* s .. ··) I len - strlen(s) ; buffe r - new char [len+ll : strcpyl buffer . s J: /I Destruktor -String() [ delete (] buffer ; ) /I Kopierkonstruktor - explizit nöt i g da 11 dynamischer Spe i cherbe re ich verwendet wird St ring( const String& s ) [ len - solen : buf f er - new char (len +ll :
357
I
4
I
Objektorien tiert e Programm ierung
s t rcpy( buffer , s . bu ff e r }; Zug r i f f smet hd oe char* get_St r i ng(} cons t I ret urn buffer ; 11 De f inition der Operato r ·O berl adung Strlng operator+-{ const Strlng& strl ) ( 11 tmp gleich mit aktuellen Objekt Inltlallsleren Strlng tmp(*thls); 11 Aktuelles Objekt lö sc hen delete [] butter; 11 Gesamtlange bel der Objekte ermitte l n len - tmp.len + strl.len: 11 Spei cher reservieren bufter - new char[len]+l: 11 linker Operand In buffer kopieren str cpy( bufter, tmp.butter ) : 11 rechten Operand anhangen str cat( buffer, str1.buffer ) : 11 ZurO ck und fertig return bufter: 11
I,
i nt main( voi d ) I String st r i ng l("Adam " ) ; String st r i ng2 ( "Eva " ) ; str1ngl+-" und ": str1ngl . ope rator+-(str i ng2 ) : cou t « str i ngl . get_Stri ng() « "\nO ; re t urn 0; Das Program m bei der Ausführung: Adam und Eva Operator·Überiadung als globale (friend-)Funktion
Die Op era tor~Oberlad ung lässt sich natürlich auch als normale globale Funktion verwenden. Allerdings werden Sie wegen de r Date nkapselung kaum Zugriff auf die Eigenschaften (Date n) der Klasse haben, weil diese gewöhnlich als private vereinbart sind. Daher wird eine solche Funktion gewöhnlich als f r i end deklariert.
Wenn Sie eine Operatorfun ktion als globale fr i en d-Funktion defi nie ren, müssen Sie jedem Operanden des Operators einen eigenen Parameter widmen . Be i un-
358
Operatoren überladen
I
4·5
ären Operatoren wäre dies immer ein Operand, u nd bei binären Operatoren wä· ren es zwei Operanden. Im Beispiel der Klasse ~ myComplex " wolle n wir nun den Operator + m it einer globalen fri end-Funktion übe rladen. Zur Demonstration wurde zum Vergleich eine Klassenmelhode zur Oberladung des Minusoperators - verwendet, die, wie schon beim Beispiel der Klasse »5tring.. gesehen, nur einen Parameter benötigt. Somit sieht die Deklaration der globalen Oberladungsfunktion im pub 1 i c-Bereich der Klasse »myComplex« wie folgt aus; 11 Operator-überladung a l s globale friend-Funktion friend mYComplex ope ra tor+(myComplex ya11 . myComplex va 121 :
Ansonsten können Sie diese Funktion wie gehabt mit complexSum - complexi + complex2 : aufrufen. Diese Anweisung ist gleichwertig mit dem Funktionsaufruf: complexSum - operator+( complexl . complex2 ) : Hierzu nun das komplette Beispiel, das neben der Oberladung des Operators + als globale fr i end-Funkti on auch den Minusoperator als Klassenmethode zum Vergleich demonstriert. 11 complex2 . cpp /jinclude us i ng namespace std :
class myComplex private : double _ r eal : double _ image : pub 1 i c : myComplex( double vall - O. O. double va12-0 . 0 ) I real - val! : _image - va12 : vo i d p r inCComplex( ) ! cou t « r eal« "+" «_image «
"\n ":
11 Operator-Oberladung als globale frlend-Funktl on frlend myComp l ex operato r+( mYComplex vI. mYComplex v2 ) ; 11 Operator-Ober lad ung als Klas senme t hode mYComp l ex operator-( mYComplex va 12 ) co nst {
359
I
4
I
Objektorien tierte Programm ierung
11 tmp gleich mi t akt . Objekt initia l isieren myCompl ex tmp(*th1s); tmp._real -- va12. _ real ; tmp._1mage -- va12. _1ma ge; return tmp: I,
myComp l ex operator+( myComplex val!, myComp l ex va12 ) ( myComplex tmp : tmp.Jeal - vall._real +- va12. _ real; t mp. _1mage - vall._1mage +- va12. _1 mage; retu rn tmp;
i nt main( void ) [ myComplex vallO . 1. 2 . 2) ; myComp1ex va12(3 . 3. 4 . 4) ; myComplex s um; cout « ·We r t von vall ; " . va11.print_Comp1ex( ) ; cout « ·We r t von va12 : " . va12 . print_Comp1ex( ) ; sum - val ! + val2: cout « · Wert von sum : " . sum . prin LC omp1ex( ) ; sum - val l - va l 2; cout « · Wert von sum : " . sum . prin LC omp1e x( ) ; retur n 0; Das Prog ramm bei der Ausführung: We r t We r t We r t We r t
'" '" '" '"
va 11 : 1 . 1+2 . 1 va 12 : 3. 3+4 . 4 s umo 4 . 4+6 . 6 s um; - 2. 2+- 2 . 2
Verg leic h der Implementierungen der O perat or-Überlad ung
J etzt haben Sie zwei mögliche Implementierungen der Operator-Oberladung kennen gelernt und werden sich wohl fragen, welche der beiden Varianten die bessere ist. Im Grunde bleibt es dem Prog rammierer übe rlassen, wie dieser die Operawr-Oberladung implementieren will. Dennoch gibt es Fäll e. wo es keine andere Möglichke it gibt, da Klasse nmethoden als linkes Argument immer ein Objekt ihrer Klasse haben müssen. Schließlich können Methoden auch nur an Objekte einer zugehörigen Klasse versan dt wer-
Operatoren überladen
I
4·5
den. Wenn dies nicht mehr möglich ist. müssen Sie eine Operator·überladung als globale fri end -Funktion impleme ntieren. Beispielsweise bei der String-Klasse sei Folgendes gegeben: St ring st r ingl{"Adam " ) : cha r name[30) : cou t « "Bitte Name einge ben : c in » name : if( name
cout e l se { cout
str1 ngl ) {
«
«
"B eide Namen sind identisch\n" :
"Beide Namen sind unterschiedl ic h\n " :
In diesem Beispi el wird de r ---Ope ralor (Vergleichsoperato r) velWendet, um die Parameter char* und Str i ng zu vergleichen. Es w ird hier eine Zeichenkette mit einem Objekt verglichen. daher ist es nicht möglich. eine Klassenmethode als Operator-überladung zu implementieren. weil die Methode -- ein cha r*-Objekt schicken würde. D man mit Basisdatentypen keine Operatoren überladen kann, müssen Sie diese Funktion als globale fri end-Funktion implementiere n. Die Deklaration dieser globale n f riend-Funktion im pu blic-Bereich der Klasse »S tring« sieht demnach so aus: fr i end baal operator --( const char * sI . const String s2 ) : Natürlich keine Theorie o hne Praxis. daher das zuvor beschriebene als Code: 11 St ring3 .cp p llinclude lIinclude us i ng names pa ce std :
class String { pr i vate : cha r *b uffer : uns i gned int Jen : pub 1 j c : 11 Konst ru ktaren Str i ng( co nst char * s·"") ! Jen· strlen(s) : bu f f e r - new char [le n+lJ : s tr c DY( bu ff er . s ) :
I
4
I
Objektorien tierte Programm ierung
11 Destruktor -St r ing() ( delete [] buffer : ) 11 Kopier kons truk t or - e xp l i zit nötig da 11 dyn ami sc her Spe i cher be r e i ch verwendet wi rd St ring( const String& s ) ( len - s . len : bu f fer - new char [len+ll : s t rcpy( bu ffer . s . buff er ) :
I 11 Zug r i f fsmet hdoe
cha r * get_St ri ng() cons t I ret ur n buffe r: ,
fr1end bool operator --(eonst ehar* sl , eonst Str1ng sZ) ; I,
bool operato r --( eonst ehar* sI, eonst Str1ng s2 ) ( 1f( strlen(sl) -- 0 ) { return fa l se ; } 1f( s2.len -- 0 ) { return fa l se; } 1f ( s tremp (s l . s2.buf fer ) -- 0 ) ( return true; return false;
i nt main( void ) I String st r ingl( 'Adam") ; ch a r name[30] : caut « 'Bitte Name e i ngeben : ein» name : i f{
name -- strl ng l ) ( cout
«
"Sei de Namen sind i dent i sch\n ' :
el se I cout
«
"Sei de Namen sind un te r sch i edlich\n" :
return 0:
Da die Operator-Elementfunktion im Konlext des li nken Operanden aufgerufen wird (das heißt, dass li nke Argument einer Klassenmethode muss ein Objekt der eigenen Klasse sein), ist es möglich, dass sich Folgendes wiederum als Klassenmethode implementieren ließe:
Operatoren überladen
Str i ng stringl( "Adam " ) ; char name[30] : cout « "Bitte Name eingeben : c i n » name : 11 Vertauscht !!!! if( str 1ngl
-
name ) I
cout
«
"Beide Namen sind ident i sch\n ":
el se 1 cout
«
"S eide Namen sind unterschiedlich\ n":
Beide Seiten sind jetzt vertauscht und der Methode - - w ird ei n Objekt St r i ng geschickt. Hier w ird also (umgekehrt) ein String (Klasse) mit e iner Zeichen kette (cha r*) verglichen. Die Klassenmethode hierzu will ich Ihnen nicht vorenthalten: baa l ope r ator -- ( const cha r *sl ) f Str i ng t mp(*th i S) : if ( strlen(sl) - - 0 ) ( r eturn f alse ; if ( tmp.len -- 0 ) I return false ; i f( s t rcmp(sl . tmp . buffe r ) -- 0) I retu r n true : retu r n false : Dasselbe Problem ist auch bei der Klasse »myComplex« gegeben, wenn Sie versu· chen. eine n doub 1e-Wert zu e inem myComp I ex-Wert zu addieren : ComplexSum - 3 . 3 + comple xl ; Auch hie r ist das llnke Argument des Operators + kein Objekt der Klasse ~myComplex«, sondern lediglich ein Basisdatentyp (double). Daher müssen Sie eine globale fri end-Funklion verwenden, die Sie wie fo lgt deklarie ren können: f r i end myComp 1ex ope r ator+( doubl e va 11 . myCompl ex va 12 ) : Hierzu das kompleue Beispiel zur +-Operator- überladung der Klasse »myComplex«, wo der linke operand wieder kein Objekt der Klasse ist. 11 complex3 . cPP llinclude
class myComplex pr i va te : double real .
I
4·5
I
4
I
Objektorien tierte Programm ierung
double _image ; pub I i c : myComplex( dOuble vall~O . O . dOuble vaI2- 0.0 ) I real" vall : _image" val2 : void print_Complex( ) I cout« real « "+" « _i mage « "\n" : Operator -Oberladung als gl obale friend - Funktion f riend myComplex operator+( myComple x vI . myComplex v2 ) :
11
}
,
frlend myComplex operator+( double vi. myComplex v2 ):
myComplex operator+( myComplex val! . myComplex va12 ) 1 myComplex tmp : tmp. _r ea l .. vall._real + vaI2 ._real ; tmp. _image " vall ._image + vaI2 ._image : return tmp :
mY Complex operator+{ double val l. mYComp lex val 2 ) ( my Complex tmp: tmp._real - vall + va12. _ re al : tmp. _image - vaI2. _1mage: return tmp: )
i nt ma i n( void ) I myComplex va l 1(1.I , 2. 2) ; myComp lex sum ; sum - 3.3 + vall: cout « 'Wert von sum: sum . printJomplex( ) : return 0; Wenn fo lgender Ausdruck gegeben ist
Compl exSum - complexl + 3, 3: kann wieder eine Klassenmethode verwendet werden, weil der linke Operand ei n Objekt der entsprechenden Klasse ist. Die Klassenmethode sieht demnach wie folgt aus:
myComplex operator+( double val2 ) const I 11 tm p gleich mi t akt . Objekt initialisieren
Operatoren überladen
I
4 ·5
myComplex tmp(*this) ; tmp . _real +- val2 ; return tmp : fr i end ·Funktion zu definieren. Die Operatoren - , [1. () und - ) müssen immer als Klassenmethoden implementiert werden! Außerdem ist es für einige binäre Operatoren wie +- , --. /- , 1-, » - und «- besser, sie als Methode zu implementieren, da diese Operatoren nicht »symmetrisch« sind und als linken Operanden stets ein Objekt benötigen (l-value).
Hinweis Es ist nicht bei jedem Operator zulässig, diesen als
*-.
4.5.3
Überladen von unären Operatoren
Wenn Sie unäre Operatoren mit einer Methode überladen, so muss der Operand natürlich auch ein Obj ekt der Klasse sein. Da eine solche Methode, wie jede andere auch, einen th i s-Zeiger auf das aktuell e Objekt enthält. wird diese Methode ohne Parameter defin iert. Für unsere Klasse "myComplex« soll der Vorzeichenoperator überladen werden. Beispielsweise: Comple xva l - -complexJ : 11 Co mp lexva l - complexl . operator -( ) Hiermit verändert der Vorzeichenoperator die Vorzeichen des Real- und des Imaginärteils. Im Gegensatz zum Minusoperator benötigt der unäre Vorzeichen-(Minus-) (H)Ope rator nur einen Operanden ( - val) (der binäre Minusoperator benötigt zwei (völl - val2» . Natürlich muss auch der Rückgabewert von der Klasse "my Complex. sein. Mit dieser Überladung ist auch fo lgender Ausdruck der Klasse zulässig : Complexval - -{ complexl + complex2l : Hierzu das Beispiel, in dem der Vorzeichenoperator überladen wird: 11 complex4 . cpp lIinclude (iostream) us i ng namespace std :
class myComplex pr i vöte : double real . double _image ;
[«J
I
4
I
Objektorien tiert e Programm ierung
pub l i C: myCompl ex { do uble vall - O. O, double vaI2- Q, O ) I real - va ll : _image - ~a1 2 : void pri nt_Complex{ cou t « rea l «
+ "
« _image « "\ n";
myCompl ex operator-{) const { retu r n myComp l ex{ -_ rea l, -_ 111lage );
I,
i nt main( voi d ) I myComplex va ll(l . l , 2 . 2) ; myComplex sum - -va ll; cout « "Wert von sum : " . sum . prinLComple x( ) : return 0: Das Programm bei der Ausführung:
Wert vo n sum: - 1 , 1 + - 2 , 2 Wenn Sie einen unären Operator als f r i end-Funktion implementieren wollen, benötigen Sie einen Parameter, da diese Funktion nicht mehr zur Klasse gehört. Der Parameter gibt dann den Operanden an und ist immer vom Ty p einer Klasse, deren Operator überladen werden soll, Zur Abwechslung soll der Operator ! mit der Klasse "String" überladen und als globale fr i end-Funktion implementiert werden, Damit soll eine Abfrage wie
i f( !stringl) I cout « "str in g1 i s t l ee r \n" ; el se I cout
« · str i ng1 i st nic ht leer\n " :
AusKunft darüber ~ebe n , ob ein Strill~ ,.leer« oder bereits initialisiert ist. Som it sieht die Deklaration dieser friend -Funktion wie folgt allS:
f r iend boo l opera t or ! ( const Str i ng 5 ) ; Hierzu das komple[te Listing: 11 St rin g4. cpp lIi ncl ude lI i nclu de ( cs t r i ng>
366
Operatoren überladen
I
4·5
using namespace std ; cl ass Stri ng I priva t e ; char 'buffer ; uns i gned int len ; pUblic: 11 Konstruktoren String( const char* s- "") I len - str l en(Sl ; bu f fer - new char (len +l] ; strcpy( buffer , s l ; I 11 Destruktor -String() I delete (] buffer ; ) 11 Kopierkonstruk t or - exp liz i t nö t i g da 11 dynamischer Spe icherbereich verwendet wird String( const String& s ) I
Jen - s . len ; bu f fer - new char [len+l) ; st rcpy( buffer , s . buffer l ; 11 Zug r iffsmethdoe char* geLSt r ing() const r return buffe r; 11 Operator I überladen fr1end baal operat orl( const Str1ng s );
baal operator!( con st Str1ng s ) { H( strcmp(s.buffer ..... ) - 0) return true : return fal se; )
int ma i n( void ) I Str i ng stringl; if{ lstr1ngl l I cout « 'stringl i st leer\n" ; el se I cout
« 'stringl i st nicht leer\n" ;
return 0;
I
4
I
[» l
Objektorientierte Programmierung
Hinweis Der eH-Standard schreibt außerdem vo r, dass der NOT-Operator (!) immer einen boo l-Wert zurückliefern muss - also e ntweder wahr (true) oder falsch
(false),
4.5.4
Überladen von ++ und --
Einen besonderen Fall haben Sie hier mit den unären Operatoren ++ und - - , die in zwei verschiedenen Schreibweisen auftreten können - und zwar in der Postfix- (val ++) oder der Präfixschreibweise (++va l). Daher müssen Sie für beide Varianten je eine Klassenmethode (oder fri end -Funktion) erstellen . Die Präfixmetbode (Hva 1) stellt wohl den einfacheren Fall dar. Wichtig ist hierbei, dass Sie in der Methode zuerst die Addition ausführen und dann das Ergebnis zurückliefern. Des Weiteren müssen Sie (logischerweise) das Objekt selbst verändern und benötigen kein temporäres Objekt. Hierzu die komplette Klassenmethode, wie man die Präfixschreibweise des ++-OperalOrs überladen kann: 11 Prafi x· üperator ++ Oberladen myCompl ex& operator++{ 1 ( _real++ : _image++ ; return *th i s :
Der andere Fall, die Oberladung der Postfixschreibweise (va 1++), benötigt einen Parameter - einen sogenannten Dummy-Parameter. f-lierbei müssen Sie allerdings die Regeln der Postfixschreibweise beachten . Und zwar muss zunächst der Ursprungswert des Objekts zurückgegeben werden, was man mit der Rückgabe eines temporären Objekts lOsimulle ren .. kann, da der Postfixoperator ja die Addition erst nach der Auswertung des Objekts durchfuhren soll. Hier die Methode zum Überladen des Operators ++ in der Postfixschreibweise: 11 Postfi x ·üperator ++ Ober l aden myComplex& operator++{i ntl ( 11 FOr den Rockgabewert myComplex tmp( *thisl : 11 Org inalwerte verandern _real ++ : _i mage++ : 11 ursprUng] i chen Wert zurOckgeben retu r n tmp :
Das folgende Beispiel demons triert den Einsatz des unären ++-OperalOrs in der Postfix- und in der Präfixschreibweise. Die Überladungen wurden als Methoden implementiert. Analog gilt dasselbe natürlich auch fü r den Operator _. :
368
Operatoren überladen
I
4 ·5
11 complex5 . cpp llinclude
I
class myComplex pr i vate : double _real : double _ image : pub l i C: myComplex( doub l e vall-O . O. double va12-Q . O ) { r eal - va ll ; _image - va 12 : void print_Complex( ) I cout« rea l « - +. « _ i mage «
"\n" ;
11 Präflx-Operator ++ überladen my Co mp l ex& operator++( ) { _ real++; _ lmage++; return *th ls ; 11 Post f l x-O perator ++ übe r laden myComplex& operator++(lnt) myComplex tmp ( *thls); _ r ea 1++; _ lmage++; return tmp;
I, int ma i n( void ) I myComplex valI(I . I . 2 . 2) ; ++vall : cout « "We r t va 11 : " . valI . print_Complex ( ) va l l++: cout « "We r t va 11 : " . va ll++ . print_Compl e x( va 11 : va 11 . pri nt_Comp 1ex( ) cout « · Wert retu r n 0,
"0 "0 "0
, ,
)
,
Das Programm bei der Ausführung : Wert von valI : 2 . 1+3 . 2 Wert von valI : 3 . 1+4 . 2 Wer t von valI: 4 . 1+5 . 2
369
4
I
Objektorientierte Programmierung
4.5.5
Überladen des Zuweisungsoperators
Das Überladen des Zuweisungsoperators - wurde ja bereits in Abschnitt 4.4.8 in der Praxis eingesetzt. Verwenden Sie zum Beispiel den Zuweisungsopera tor mit komplexen Zahlen wie myComplex va l l(l . l . 2 . 2) ; myComplex va 12 - val! ; so erhält nach dieser Zuweisung "vaI2« tatsächlich den Inhalt der komplexe n Zahl "vaI1«. Das dies funktionie rt. liegt da ran . dass C++ im Standard-Zuweisungsoperawr für Klassen ein "Shallow (opy. ausfUhrt. Somit ist im Grunde gar keine Oberladung des Zuweisungsoperators nötig. da der von C++ zur Verfügung gestellte Standard-Zuweisungsoperator gute Arbeit verrichtet. Anders sieht die Sache allerdings bei Klassen aus. die dynamische EigenSChaften en thalten. wie dies beispielsweise bei der Klasse "String« mit "buffer« der Fall ist. String str i ngl( "Adam") ; String str i ng2 - stringl : Wenn Sie d as Beispiel teste n. werden Sie auf den ersten Blick und bei der ersten Ausgabe keinen Feh ler bemerken. Allerdings täuscht dies. denn durch die Zuweisung eines Objekts. das dynamische Eigenschaften enthält. bes itzen beide Objekte Ze iger auf den gleichen Speicherbereich. Wird eines der be iden Objekte gelöscht. bleibt hier mindestens ein Zeige r erhalten. der auf einen nicht mehr allozi ierten Speicherbereich im Heap zeigt. Solche "wilden. Zeiger führen häufig bei Programmen zu einem Absturz. Somit benötigen Sie also für Objekte mit dynamischen Eigenschaften und bei der Verwendung des ZuweisungsoperawfS eine "tiefe« Kopie (Deep Copy). Sie müssen in diesem Fall den Zuweisungsoperator überladen . Eine solche Überladung geschieht in mehreren Schrillen. Zunächst müssen Sie überprüfen. ob eine Zuweisung wie ~obje k t"'objekt" vorliegt (was zwar keinen Sinn ergibt. aber rein syntaktisch auch kein Fehler ist) . Hiermi t würden Sie praktisch das Objekt selbst zerstören. was unter Umständen zu einem fehlerhaften Verhalten des Operators führen kann. Anschließend müssen Sie den bereits alloziierten Speicherbereich des Objekts fre igeben . Jetzt können Sie die Speicherbereiche mit der richtigen Größe neu anfordern. Danach können Sie den dynamisch alloziierten Speicher bereich verwenden (kopieren) und geben abschließend als Rückgabe eine Referenz auf das eigene Objekt (" thi s) zurück. [ )}]
Hinweis Eine Regel sollte es sein. dass ma n beim Vo rhandensein dynamischer Eigenschaften eine r Klasse nebe n dem Kopierkonstruktor auc h immer den Zuwei· sungsoperator selbst defin iert.
370
Operatoren überladen
I
4 ·5
Somit sieht die Überladung des Zuweisungsoperators mit der Klasse .. String« fol gendermaßen aus : String& operator-I const String& s ) I 11 Au f Selbst - Zuwe i sung OberprOfen if( t his -- &s ) I ret urn *th i s :
I
delete[ ] buffe r: buffer - 0 : len - s . len : buffer - new char( l en+l1 : strcpy( bu f fe r . s . buf f er ) : return *this :
Zuwei sung von Objekten verhind ern
Zwar ist es standardmäßig imme r erlaubt. einem Objekt einer Klasse ein anderes Objekt der gleichen Klasse zuzuweisen (auch ohne Oberladung des Zuweisungsoperators übernimm t dies der Compiler), Hierbei wird immer Eigenschaft für Eigenschaft kopiert. Fü r den Fall , dass Sie dieses verhalten unterbinden wollen, müssen Sie nur das Überladen des Zuweisungsoperators mit einer leeren Methodenfunktion innerhalb eines privaten Bereiches (private) definieren. Private Mitglieder sind von außen nicht zugänglich, so kann man außerhalb der Klasse keinem Objekt der Klasse ein Objekt gleiche r Klasse mehr zuweisen, weil es eine pr i va t e-Methode ist. 4 .5 .6
Überladen des Indexoperators [] (Arrays überladen)
Wenn Sie jetzt in der Klasse .. String" Folgendes ausführen wollen String stringl - "Adam ": cout « s tri ngl[O] « st r ingl[3]
«
' \ n':
würde der Compiler eine Fehlermeldung ausgeben . Schließlich handelt es sich bei "Slring« um eine Klasse und nicht (d irekt) um ein cll d r-Array. Wollen Sie also auf einzelne Elemente eines Arrays in einer Klasse zugreifen, müssen Sie den []Operator überladen. Die Verwendung einer solchen Klasse hat auch den Vorteil, dass man eine Indexprüfung mit einbauen kann. Natürlich hat solch eine Indexüberprilfung immer den .. Nachtei l", dass dies die Laufze it des Programms nega tiv beeinfl usst. Hier müssen Sie als Programmierer selbst einschätzen können, ob es Ihnen hierbei mehr auf die Laufzeit oder auf die Sicherheit ankommt - was sicherlich auch von der Art der Anwendung abhängt, die Sie erstell en.
371
4
I
Objektorientierte Programmierung
Da hier einzelne Zeichen verwendet werden, sollten Sie als Rückgabewen eine Referenz auf einen char-Ty p zurückgeben. Außerdem müssen Sie beim überladen des Indexoperators in Klassen fo lgende Punkte beachten : ..
Das Überladen des Indexoperators darf nur als Klassenmethode definiert werden - somit ist der linke Operand immer das Objekt de r Klasse (st r [O J).
..
Das rechte Argument wird als Argument an die Funktion übergeben und darf nur einen Parameter haben (5t r [ 0 ) .
..
Der Typ des rechten Operanden ist beliebig, ebenso wie der Rückgabetyp nicht festge legt ist.
Mit diesen Freiheiten, im Gegensatz zu herkömmlichen Arrays, können Sie den Indexoperator erheblich verändern. Allerdings sollte man in der Praxis den Indexoperator so verändern, dass dieser noch der ursprünglichen Verwendung von Arrays entspricht. Hierzu das Programmbeispiel. das den Indexoperator überlädt und einzelne Zeichen aus der Klasse ,.String" zurückgibt und außerdem noch eine Indexüberprüfung vorn immt. Im Falle eines Fehlers (BereichsOberschreitung), wird ein Fragezeichen zurückgegeben: 11 String5_cpp lIinclude /linclude us i ng namespace std ;
class Str ing I pr i vate : cha r *buffer: uns ig ned i nt len ; publ i C: 11 Konst ruktor en Str i ng{ const char * s="") I len - strlen(s) : buf fer - new char [le n+l) : strcpy{ buffer . s ) ; I 11 Des t ruktor
-St r ing() ( delete Cl bu ffer : ) 11 Kopierkonstruk t or - exp l i z i t nötig da 11 dyn ami scher Spe i cher berei eh verwendet wi rd St ring( const String& s ) I len - s . l en ; buffer - new cha r [le n+l) :
372
Operatoren überladen
I
4 ·5
strcpy( bu ffer . s . tJuf fe r ) ;
11 Zugri f fsmethdoe char* get_String() const r return buffer : 11 Oberladung des In dexo perators co ns t char& operator [](i nt index ) co nst { 11 Indexüberprüf ung 1f ( (i ndex )- 0) && ( i ndex< len ) ) ret urn buffe r [i ndex]; 11 1m Feh l erfa l l e ein Fragez ei chen zurü ckgeben re t urn ' 1' ;
I
I,
i nt main( void ) r String stringl - -Adam" : cout « str l ngl[O] « st rl ngl[3] « ' \n' : // !!! Be re l chsObe rschreitung !!! cout « str l ngl[99] « "\n ": retu r n 0: Das Programm bei der Ausführung:
Am 1
Da bei der Oberladung des [ndexoperators ein beliebiger Datentyp verwendet werden kann, können Sie hiermit auch assoziative Arrays realisieren. Dies sind Arrays, bei denen die einzelnen Elemente über Strings als Index angesprochen werden. Außerdem könnte man mit jedem einzelnen Array-Element Speicher dynamisch reservieren und somit eine ganze liste von dynamischen Objekten verwalten. Hierzu ei n einfaches Beispiel unserer Klasse " Stri ng ~ . das jetzt auch assoziative Arrays untersrutzL In unserem Fall verwenden wir dies. inde m wir im .. String« nac h einem Teilstring suchen. Den Teilstring geben Sie als Indexwert des Arrays an: /I String6.Cpp llinclude llinclude us i ng namespace std ;
class String private :
373
4
I
Objektorien tierte Programm ierung
char "' buffer ; uns i gned in t len : pub 1i c : 11 Konst r uktoren Str i ng{ const char '" s- "" ) I len - strlen(s) ; buffer - new char [len+l] ; strcpy( buffer , s ) ; I
11 Destruktor
-String() ! delete (] bu ffer : 1 11 Kopierkonstruktor - explizit nOtig da 11 dyn ami scher Speicher berei eh verwendet wi rd String( const String& s ) I Jen - s . len ; buf f er - new char [len+ll : strcpy{ buffer . s . bu ff er ) ; 11 Zugrif f smethdoe char'" ge t _String{) const ! r eturn bu ff er ; 11 Indexoperator überladen const chaf& operatof[]( i nt index} const 11 Indexüberprüfung i f( (index )- 0) && (index< len) ) return buffer[indexl ; 111m Fehlerfalle ein Fr agezeichen zu rü ckgeben return '?':
Assoz1at1ves Array char* operator[ ) ( const char *st r ) cons t { return strstr( buffer, str );
11
I,
int main( void ) I String st r ingl - "Adam und Ev a" : char "'ptr - st r 1ng l [~und~]; if( ptr 1- NUL L ) cout « ptr « "\n" ; ptr - st r1ngl["Ev if( ptr ! - NUL L ) cout « ptr « "'n" ; return 0; R
]
374
;
Operatoren überladen
I
4·5
Das Programm bei der Ausführung: und Ev a ,,,
I
Shift-Operatoren überladen
4.5.7
Natürlich ist es auch möglich, dass der Eingabeoperator » und der Ausgabeoperator « überladen werden. Damit können Sie eigene Datentypen wie beispielsweise der Klasse »myComplex« in das C++-System implementieren. Das bisherige Versuche wie cout
«
comp l exVal ;
fehlgesch lagen sind, liegt daran, dass (out den Typ des Werts nicht kennt, das heißt, dass es hierfur keine Ausgabefunkeion wie fur char , i nt, doub 1e elC. gibt. Für Ein- bzw. Ausgabefunktionen von Klassen si nd Sie selbst verantwortlich. Beginnen wollen wir mit dem Operator » und der Klasse ~ myCom plex « . Mit dem bisherigen Wissen wü rden Sie zum Einlesen einer komplexen Zahl eine Zugriffsfunktion schreiben. Durch das Überladen des Operators » können Sie hierbei nun ein Objekt der Klasse »myComplex« mittels ein
»
eomplexVal ;
einlesen. Links vom Ope rator steht hier ~cin«. was ein Objekt der Klasse ~istream« ist. "istream« ist wiederum in der Headerdatei defin iert. Hinweis Mehr zu den grundlegenden Streams von C++ entnehmen Sie bitte dem Abschnitt 7.2.
Da alle rdings »cin« ein Objekt der Klasse ,.istream« ist, haben Sie keinen Zugriff auf die publ ie-Eigenschaften <stehen auf der rechten Seite des Operators » ) der Klasse »myComplex«. Aus diesem Grund müssen Sie den Operator als globale fr i end -Funktion implementieren. Somit sieht die Syntax der Funktionsdeklaration (bezogen auf die Klasse »myComplex«) folgende rmaßen aus : fr i end istream& operato r »(istream& is o myComple x& val) ; Als erSten Parameter benötigen Sie ei ne Referenz der zu definierend en Operawr· funkt ion (also auf den Eingabestream) - daher auch eine Referenz auf die Klasse ~istream ... Der zweite Parameter (der rechte Operand) ist ein Objekt der Klasse »myComplex«. was ebenfalls eine Referenz darauf se in sollte. Der Rückgabewert ist eine Referenz auf ,.istream ... Dies ist nötig, damit Sie den Operator » mehrmals hintereinander aufrufen können:
375
[«]
4
I
Objektorien tiert e Programm ierung
11 Liest zwei komple xe Zahlen ein ein » comp l exl » compex2 ;
Somit ist die Anweisu ng ein» va l ;
gleichwertig zu operator»{cin , va l ) ;
Nach der Deklaration müssen Sie di ese Funktion nur noch global defin ieren: ist ream& oper ator »( i s t re am& is , myComple x& val) cou t « "Real - Tei 1 ". i s » val ._real : cou t « " Imag i narer Teil : ": i s» val . _ima ge ;
Das Überladen des Operators » ist aber nicht nur auf die Tastatur beschränkt, sondern lässt sich auch zum Einlesen aus einer Datei _umb iegen •. Hierbei müssen Sie lediglich stau des Streams »eint< einen Datei-Stream wie _ifstreamt< verwenden und mit eine r Datei verbinden: 11 Eing ab estream mi t Da t e i verb i nden
i f stream FileC i n : myComple x va l ; Fi leCin . ope n( "MyF i 1e . dat") ; 11 Daten f[l r Objekt aus Date i e i nlesen Fi leCin » va l :
Das dies funktioniert. liegt daran, dass _ein« und _ifstreamt< Instanze n der Basisklasse _istream« sind. Allerdings sei dies nur am Rande erwähnt, da bis jetzt noch nicht die grund legenden Streams von e++ behandelt wurden. Mehr dazu erfahren Sie ab Abschniu 7.2.
Bevor ich Ihnen das Beisp iel zum Einlesen einer Zahl vom Typ ~ myComplex t< demonstriere, soll noch der Ausgabeoperator « überlad en werden. Im Grunde können Sie hierbei aUes vom Eingabeoperator
»
gelesen übernehmen. Nur steh t
links vom Operator _cout« statt »cint<, und es ist ein Objekt der Klasse _ostream« statt "istreamt<. Sie müssen also statt ,.istreamt< für die Ei ngabe _oslreamt< für die Ausgabe verwenden. Somit sieht die Dekl aration und Definition der Operatorfunktion wi e fo lgt aus: 11 De kl a rat i on fr ien d os tream& oper at or «~I ostrea m& os . con s t myCom plex & va l ) ;
Operatoren überladen
I
4·5
11 Definition
ostream& operator «(ostream& os . const myComplex& os « ~al . _real « "+" « ~a l. _ i mage « ' \n ': retu rn os :
~al
) !
I
Hierzu das komplette Listing: 11 complex6 .cpp lIinclude
class myComplex pr i ~ate :
double real : double _im age : pub 1i c : myComplex( double vall-O . O. double valZ-O . O ) { real - vall : _image - va l Z: void print_Complex( ) { cout« real « "+" « _ima ge «
' \n ';
frlend os t ream& operator «( astream& os. canst lIIyComplex& val); frlend ls t ream& operator »(lstream& 15 , myComplex& val) ; I,
ostream& ope r ator «(os tream& os , co nst myComp l ex& val) { os« val. real « "+"« val. _lmage« ' \n ' ; return os; )
l st ream& ope r ator »(lstream& ls , myC Ollpl ex& val) { " ; ls» val. _ real; cou t « "Real-Tell cou t « "Imeogln6r er Te l l : H: 15 » val. _ 1r1eg e; )
int ma i n( void ) { my(omplex va l !, valZ , val3 ; cout « "Bitte eine komplexe Zahl eingeben\n" ; cln » va11; tout « val 1;
377
4
I
Obje ktorien tierte Programmierung
cout
« "Bitte zwei komplexe Zahl eingebe n\n' ;
c fn » v~ l2 » val 3; cou t « val2 « v~l3;
11 Dank Refe renz als RQckgabewe rt
return 0: Das Programm bei der Ausführung:
Bitte eine komp l exe Zahl ei ngeb en Rea l -Teil 1.1 Imag ; na rer Tei l 2. 2 1.1+2 .2 Bi tte zwei komplexe Zahl eingeben Real -Teil 3.3 Imagina rer Tei l 4.4 Real-Teil 5. 5 Imagina rer Tei l 6.6 3. 3+4 . 4 5. 5+6 . 6
4.5_8
O-Operator überladen
Auch der Funktionsoperator () lässt sich überladen. Damit können Sie ein Objekt der Klasse als Funkti on aufrufen. Daher wird auch von sogenann ten Fun klionsobjekten gesprochen, man kann es als eine bessere Alternative zu den Funktionszeigern bezeichnen. Den Operator () für Klassen zu überladen, ist in der Tat ein interessantes Feature, das auch in der STL (Standard Template Library) intensiv eingesetzt wird. Eine solche Funktion lässt sich praktisch folgendermaßen aufru fen:
Objekt{val) : 11 ode r auch Objekt.ope rator( )(val) ; Ich möchte den Funktionsoperator bei unserer Klasse »String« zum Einsatz kommen lassen, und zwar folgendermaßen:
5tr ; ng strin gl - "Adam und Eva' ; 5tr ; ng string2 - str ; ngl(S , 3) ; 5tr ; ng strin g3 - str i ngH9 , 3) ; Str i ng zahl - " 100 ":
long val - 12345 . gesamt ; gesamt - val + zahl(IO) :
378
Operatoren überladen
11 Hexadez i mal cout« zahl(16)« ' \n ': 11 Oktal cout « zahl(S) « ' \n ':
I
4·5
I
Mit der Verwendung von Str ing stringl - "Adam und Eva" : String str i ng2 - strin g1(5 , 3) :
soll an lIstring2 .. ein Subslring ab Position 5 von lIstring1.. mil 3 Zeichen Länge zugewiesen werden. Natürlich müssen diese Angaben in der Überladung überprüft werden. Außerdem soll noch eine Funktion implementiert werden, bei der ein Obj ekt der Klasse lI Scring '" in einen 1ong-Wert umgewandelt wird. Dabei soll es auch mögli ch sein, das Zahlensy stem anzugeben. Als Basis soll hierbei 16 für hexadezimal, 10 fIlr dezimal und S für das Oktalsystem möglich sein: St ring zahl - "100" : long ~al - 12345 . gesam t : gesamt - ~a l + zahl(lO} :
Damit wird es möglich, mit der Klasse lIString.. zu rechnen. Bei dem () -Operalor haben Sie viele Freiheiten. Entscheidend dafür, was Ihre Funktion machen soll, ist natürlich zunächst der Rückgabewert. der neben einer Klasse auch ein normaler Datentyp sein darf. und die Parameter. Hierzu das Beispiel des Operators ( ) mit der Klasse lIString .. : 11 St ring7.cpp llinclude us i ng namespace std :
class String [ pr i vate : cha r *bu ffer : uns i gned i nt len ; pub ! i C: 11 Konst r ukt oren Str i ng{ const char * s· " " ) [ len - strlen(s) : bu t t e r - new cha r [len+lJ : strcDY( buffer . s ) ;
379
4
I
Objektorien tierte Programm ierung
11 Destrukt or -St ring( ) ( delete [] bu ff er ; ) 11 Kapi erkons truk t ar - exp l i z i t nO t i 9 da 11 dyn ami scher Spe i cher berei ch verwendet wi rd St ring( const String & s ) I len - s . len ; bu f fer - new char [len+l] ; st rcpy( bu ff er , s . buffer ) ; 11 Zug r i f f sme thdoe char* get_St r ing() const I return buffe r; )
eons t Strlng operator( )(l nt pos. lnt count lf( (po s >-0) && (count< len) ) { Strlng tmp(*thl s) : eha r *ptr - tmp.buffer+pos : 11 Zef ehenkette abschlfeßen ptr{count) - '\0 ': return St rlng (ptr):
eons t (
)
11 Bel fal sc her Angabe
return Str l ng(buffer):
eo ns t 10ng ope rat or()(fnt base) const { 11 Hexadezlma l-1 6: Dezfmal-IO : Oktal-S 1t ( (base-16) 11 (base-I O) 11 (ba se-S l ) { unsfgned long zah l - s trto l( buffe r . O. base): return zahl: 11 Im fehlerhll
return 0;
operator ehar* () eons t ( return butter: I,
int main( void ) ( String st ringl - "Adam und Eva" ; String st ring2 - str1n gl(5. 3) ; String str ing3 - st r1n gl(9. 3); cout C( stringl (C ' \n ' : cout « str i ng2 « ' \ n';
Operatoren überladen
cout
«
I
4 ·5
st r in g3 C( ' \n ';
Str i ng zahl ~ "1 00" : l ong val - 12345 , gesamt : gesamt - val + lahl(lOJ; cout « gesamt « ' \n '; 11 Hexadezimal cout« zahl(l6J «
I
' \n ':
IIOk t al
cout« zahl(8J «
' \n ':
re t urn 0:
Das Programm bei der Ausführung: Adam und Eva
oe' ,,,
12445
256
64 Bei der folgenden Methode operato r char*(J const { return buffer;
handelt es sich nicht um die Überladung des () +Operators, sondern um einen Konvenieroperalor, mit dessen Hilfe es möglich wird, dass ein Aufruf wie cout
«
stringl
«
' \n ':
ohne Überladen des « -Operators möglich wird. Auf solche Ty penumwandlungen wird noch eingegangen. Hin weis Bei dem Operator () wird niemals eine Referenz zurückgegeben . Der Rückgabewert muss immer ein neues Objekt sein und darf niemals ein Verweis auf
ein bestehendes Objekt sein. 4 .5.9
new- und del ete-Operator überladen
Auch durch das Überladen der Operatoren new und de 1ete können Sie ein Programm weiterhin den selbst definierten Typen (Klassen) anpassen und eine effi zientere Speicherverwaltung einbauen. Damit können Sie ein Speichermanagement einsetzen, das »systemnäher« ist und somi t die Möglichkeiten der entsprechenden Plattform besser ausnutzt. Bei-
38'
[« )
4
I
Objektorientierte Programmierung
spielsweise können Sie die aus C bekannte Standardfunktionen ma 11 oe . rea 11 oc und fr ee verwenden, die wesentlich schneller sind als new bzw. delete , die in C++ zum Zuge kommen. Sie können hierbei den new-Operator klassenspezifisch überladen. Der Vorteil ist, dass die Größe der Objekte von vornherein bekannt sind, sodass das Programm auch wieder etwas schneller läuft, oder es lassen sich auch spezielle Algorithmen für die Speicherverwaltung in die klassenspezifische new-Methode implementieren . Außerdem können Sie hervorragend eine Art ,.Debug«-Version für Kontrollzwecke und Fehlersuche einsetzen , indem bei jedem Reservieren von Speicher eine Debug-Meldung den Vorgang mitp rotokolliert. Es stehen Ihnen zwei Mögli chkeiten zur Verfügung, den new- bzw. delete-Operator (gilt auch für new e] bzw. de 1ete[]) zu überladen: .. Global - Überladen Sie new bzw. de 1ete global. so sollten Sie wissen, dass Sie hiermit die beiden Operatoren new und delete , die im Namensraum std deklariert sind , überschreiben, sodass die Originale nicht mehr sichtbar sind. Überladen Sie also die beiden Operatoren global. so werden immer diese Versionen verwendet, wenn Basisdatentypen oder Objekte von Klassen Speicher anfordern . ..
Klassenspezifisch - verfugt eine Klasse über einen eigenen neu definierten new- bzw. (lel ete-Operator, so werden diese verwendet, wenn Speicher ((Ir ein neues Objekt angefordert wird. Dem Gültigke itsbereich entsprechend erhält auch eine lokale new- bzw. del ete-Methode den Verzug gegenüber einer eventuell vorhandenen globalen new- bzw. de 1ete-Funktion.
Um den new-Operator zu überladen, müssen Sie wissen , wie dieser arbeitet. Wenn Sie den Operator new() aufrufen, wird zunächst versucht , Speicherplatz für ein Objekt mit einer bestimmten Größe zu reservieren. Bei Erfolg liefert diese Funhion einen typenlosen Zeiger auf einen reservierten Speicherbereich zurück. Erst wenn der Speicher erfolgreich reserviert wurde, wird der Konstruktor für ein Objekt aufgerufen. Überlad e n von new
Das Überladen des Operators new wird genauso wie das Überladen anderer Operatoren realisiert, indem eine eigene Version definiert wird. Der grundlegende Prototyp sieht demnach wie folgt aus: vo i d* ope ra tor new ( size_t size ) ;
382
Operatoren überladen
Beim Aufruf dieser Operatorfunktion wird von der Größe "size« Speicher reserviert und ein typenloser Zeiger (vai d~) darauf zurückgegeben. Da Sie den Operator new selbst überladen. können Sie selbstverständlich auch zusätzliche Parameter verwenden, nur sollte mindestens ein Parameter vorhanden sein, der die Größe des zu reservieren den Speichers enthält. Überladen von delete
Wenn Sie new neu implementieren (überladen), dann solllen Sie auch de 1et e neu implementieren. de 1et e erwartet als Argument einen Zeiger auf den zuvor mit new alloziierten Speicher, der wieder freigegeben werden soll. Daher ist der Parameter ein typenloser Zeiger (voi d"). Hier der mögliche Prototyp zum Operator de 1ete : void ope r ator delete{ void * ) : Natürlich können Sie auch hier mehrere Parameter einfahren, sofern dies sinnvoll erscheint. Aber mindestens vorhanden sein sollte der type nlose Zeiger auf den mi t new alloziiertcn Speicherplatz. Überladen von new[] und delete[]
Beim Reservieren und Freigeben von Speicher für Arrays werden die Operatoren new(] und de 1ete[] überladen. Diese Operatoren können wie new und de 1ete entweder global oder klassenspezifisch überladen werden. Die Prototypen sind ähn lich: void * operator new [J( size_t s i ze ) : void ope rator dele t e []( void " ) : Hierbei entstehen häufig Missverständnisse, denn mit der Oberladung von new und delete für einzelne Objek te sollle man im Allgemeinen auch eine Version fur new(] und de 1ete[] fur Arrays implementieren. Nur dadurch ist es möglich, dass bei dynamisch erzeugten Arrays auch die selbst defi nierte Speicherverwaltung verwendet wird. Globale Überladung
Hierzu ein Beispiel mit der Klasse "String«, in dem die Operatoren new , de 1ete , new(] und de 1ete( ) global überladen werden. Da diese Überladung global definiert wurde, ist diese nicht auf Objekte der Klasse "String« beschränkt und kann somit für jeden beliebigen Typ verwendet werden. Die Operatoren new, new( 1. dele t e und dele t e(] , die in dem Listing verwendet werden, werden durch unsere selbst definierte Vers ion ausgeführt. Diese wurde der Einfachheit halber
I
4·5
I
4
I
Objektorien tierte Programmierung
so verändert, dass die ruten, aber systemnäheren und somit schnelleren (-Speicherreservierungsfu nktionen mallocC) und fr ee() verwendet we rden.
[»]
Hinweis Wenn Sie wi ssen wollen, wann welches new oder del ete aufgerufen wird, bauen Sie doch einfach eine Ausgabe-Zeile in die überladenen Operatorfunktionen mit ein, die einen klar aussagenden Text auf dem Bildschirm ausgibt: 11 String8 , c pp llinclude lIinclude llinclude lIinclude us i ng namespace std :
class Stri ng I private : char "bu ffer : uns i gned i nt len : pub 1i e : 11 Konst ruk taren Str i ng ( eonst ehar " s~") I len - st r len{s) : 11 Ve rwend et selbsdef1n1e r tes new( ) buf fe r - new ch~ r [l en+l ] ; strcpy( bu ffer . s ) : 11 Oes trukt or -String() I 11 Verwendet sel bst de f fn1e r tes delete[ ] dele t e [ l bu ff er; 11 Kop i e rkonstruk tor - explizit nOtig da 11 dynamische r Speieherbereieh verwe ndet wi rd
Str i ng( eon st St ring& s ) I Jen - s . le n : 11 Ve rw e ndet
~el b ~defin1e r te~
new[)
buf f er - new char (len+l ) ; strepy( bu ffer . s . bu f fer ) ; 11 Zugrif f smethode ehar * get_String{) const I r et urn bu ff er : 11 Def i nition der Ope r ator-übe rl adung operator char~() const I return buffer :
Operato ren überladen
I
4·5
I, 11 Oberladen von new - Globale Oberladung vold *operator new[]( slze_t slze ) [
vold* ptr .. mallot( slze ); 1f( ptr -- NULL) ( ta ut
«
"Speicherpl atzmangel J!! \n";
exlt(l): 11 Programmabb ru ch return ptr:
11 Überladen von delete[ ) - Globale Ober l adung yold operato r delete[]( yold *ptr ) ( 1f(
ptr 1- NULL) ( free( ptr );
ptr .. NUll; } }
11 Überladen von new - Globale Oberladung vold *operator new( slze_t sfze ) (
vold* ptr .. malloc( slze ); 1f(
ptr -- NULL) ( caut
«
"Spelcherpl atzmangell ! J \n";
exlt(I) ; 11 Programmabb r uch
return ptr:
11 Oberladen von delete - Globale Oberladung vold operato r delete( vold *ptr ) ( 1f( ptr 1- NULL) (
free( ptr );
ptr .. NUll: }
int ma i n( void ) r 11 Verwendet selbst def1niertes new Strlng* st r lngl .. new St r 1ng("Hal l o ~elt"); cout ee stringl ->ge t _String() ce ' \n ' : 11 Da Global au f f Or andere Typen 11 Verwendet selbsdef1nlertes new[]
I
4
I
Objektorien tierte Programmierung
lnt * ar ray - new lnt[5] : for( ; nt ; - 0 : ; < 5 : ;++ ) I array[;] - i : Verwendet selbs def lnlertes new l nt* val - new l nt ; * ~al - 9 : return 0: 11
Klassens pezifi sch e Überl adun g Natürlich lassen sich die Ope ratorfunktionen new, new[) , de 1ete und de 1ete[] auch nur für eine Klasse überladen. Hierbei werden diese Operatorfunktionen wie gewöhnliche Methoden der Klasse definiert. Sobald der Operator dann die Klasse als Operanden erhält. wird die selbst defmierte Methode zur Reservierung des Speicherbereichs aufgerufen.
[» l
Hinweis Die Klassenspezifischen Operatorfunktionen von new und de 1e te sind sta~ tische Methoden und können somit nur auf statische Elemente einer Klasse zugreifen und haben außerdem keinen t hi s-Zeiger auf das aktuelle Objekt. Hier besteh t die Gefahr, dass man in der überladenen new-Operatoren-Methode wieder new aufruft. um Speicher zu reservieren. Allerdings würde man damit praktisch die überladene Methode new erneut aufrufen und häue quasi eine Endlosrekursion. Da der globale new-Operator weiterhin zur Verfügung sieht , muss man diesen mit dem Scope-Operator ansprechen. Somit können Sie innerhalb einer Operator-Oberladungs-Methode mit : : new Ty p : auf de n globalen new-Operator zugreifen. Dasselbe gilt natürlich auch fü r new[ 1. delete und dele te[J .
4.6
Typenumwandlung für 1
,.Ty penumwandlung für Klassen .. hört sich zunächSl recht abenteuerlich an. wenn man sich vorstellt, man wandelt eine Klasse ,.Auto .. in ei ne Klasse "Mensch.. um . Aber es ist auch möglich. Klassen sowohl explizit als auch implizit umzuwandeln. Wie eine Klasse »umgewandelt .. wird , legt allerdings der Programmierer in der Klasse selbst fest. Als Umwandlungen können entweder verschiedene Klassen verwendet werden oder aber Umwandlungen zwischen Klassen und Basistypen. Dafür stehen Ihnen zwei Möglichkeiten mit einem Konvenierungskonstruktor und einer Konvenierungsfunktion zu r Verfügung.
386
Typenumwandlung für Klassen
I
4.6
Konvertierungskonstruktor
Hinter dem Begriff ,. Konvertierungskonstruktof« verbirgt sich nichts anderes als ein Konstruktor mit Parametern . Somit ist also jeder Konstruktor, den Sie bisher mit Parametern verwendet haben, ein Konvertierungskonstruktor - mit Ausnahme des Kopierkonstruktors. Der Begrjff Konvertierungskonstruktor ist anhand der Klasse ,.String« auch schnell erklärt. Beispielsweise sie ht der Konstruktor dieser Klasse folgenderma ßen aus :
class Strin g pr i vate : cha r *bu f f er : unsigne d int l en ; pub l i C: 11 Konst ruk to r Str1ng( const char* s_" n) I len - str l en(s) ; buf fer - new cha r [len+l) ; str cpy( buffer . s ) ; I,
Genau betrachtet erzeugt dieser Konstruktor aus einem C-String ein Objekt der Klasse _String«, und somit haben wir eine Typenumwandlung. Hierzu einige Aufrufmöglichkeiten, die u.a. auch den Konvertierungskonstruktor beinhalten: 11 St anda r dkonstruktor St ring stri ng l : II Konve rt ier ungskons tr uk t or char* .) Strin g St r i ng st ri ng 2( "Tes t -S t ri ng" ) : 11 Kop i erk onstruktor st r i ngl - st r in g2: 11 Implizite Konverti erung char* -> St r i ng str i ngl - "Noch e i n Test -St r ing ": 11 Explizi te Konverti erun g ( Kon vertieru ngskon struk t or ) st r ingl - String( "Te st 99" ) : // Ex plizite Konve rtie rung al s Cast-S chreibwe i se st r ingl - (Str i ngl "Mo re tes ts ":
Verwenden Sie den Konvertierungskonstruktor, wird immer ein temporäres Objekt erzeugt, das fur die Anweisung verwendet und anschli eßen d wieder zerstört wird.
38,
I
4
I
Objektorientierte Programmierung
Sie können also den Konvertierungskonstruktor für expHzite und impHzite Typenumwandlungen benutzen. Bezogen auf die Klasse "myComplex .. bedeutet dies: myComplex vall(1. 1. 2 . 2) . va 12 : va12 ~ vall + ( myComple x) 3 . 3 : va12 ~ vall + 3. 3 ;
11 Explizit 11 Implizit
1m Beispiel wird davon ausgegangen, dass es noch keinen Operator + gibt, der überladen wurde, sodass der implizite Ausdruck "val1 + 3.3 .. eigentlich einen Fehler ausgeben müsste, weil versucht wird, eine myComple x-Zahl mit einer doub 1e-Zahl zu addieren. Das dies dennoch funktionie n , liegt daran, dass der Compiler den Konvertierungskonstruktor heranzieht und versucht, aus der Zahl 3.3 ein temporäres myCompl ex-Objekt zu machen. Dadurch werden »vall« und das temporäre Objekt "myComplex(3.3)" addiert und dann wieder freigegeben. Selbstverständ lich wird neben dem selbst definienen Konverti erungskonstruktor gegebenenfalls auch noch die Standardkonvertierung vorgenommen. So ist zum Beispiel auch Folgendes möglich: myComplex vall{1 . 1. 2 . 2) . va 12 : int ival - 3 : 11 ival wird Konvertier t i nt - ) double - ) myComplex va12 - vall + lv al; Hier haben Sie denselben Vorgang wie gehabt. Nur dass zunächst der Typ; nt in einen doub 1e-Wert und dieser vom Konvenierungskonstruktor zu einem temporären myComp 1ex-Objekt umgewandelt wird.
4.6 .2
Konvertierungsfunktion
Mit einer Konvertierungsfu nktion können Sie das Objekt eine r Klasse in einen beliebigen Datenty p konvertieren. Dies ist eine Funktion, die festlegt, wie eine Konvenierung erfo lgen soll. Die Syntax einer solchen Konvertieru ngsfunktion hat folgende s Aussehen: ope rator z i eltyp l ) ; Eine solche Konvertierungs funktion haben Sie bereits bei der Klasse "String .. mit opera t or char*() (ons t retu r n bu ffer : ei ngesetzt. Durch diese Konvertieru ng wurde ein Objekt der Klasse "String" zu ei nem Zeiger auf die Anfangsadresse der Zei(henkette (der Eigenschaft buffe r) konveniert. Damit war es praktisch möglich. dass eine Ausgabe mit (out wie
388
Typenumwandlung für Klassen
I
4 .6
Str i ng stringl( "Tes t - St ring") ; cout « st r ingl ; ohne Pro bleme ausgefuhrt wurde, weil hier ..S[ri ng1« zuvor noch in den e ntsprechenden Typ umgewandel t wurde (Stri ng - > char *) . Damit ließe sich auch o hne Problem Folgendes verwenden: Str i ng stringl( "Test St ring") ; char ' ptr - stringl ; Natürlich könnten Sie hie rbei auch eine n ..String« in ei n int konvertieren, um zum Beispiel die Länge des Str ings zu ermitteln : 11 String9 . c pp lIinclude llinclude lIinclude lIinclude us i ng namespace std ;
class String ( pr i vate : cha r *buffer ; uns i gned int len ; pub 1 i c : 11 Konst r uktoren Str i ng{ co nst char * s .. ··) I len .. strlen{s) ; buf f e r - new char [len+l) ; strcpy( buffer . s ); 11 Destrukto r -String() ( delete (] buffer ; I 11 Zug r i f fsmethdoe cha r'" geCSt ri ng() cons t I return buffe r: I 11 Str 1ng -> 1nt ope r a t o r
un ~ ign e d
i n t () c ons t {
return l en; )
I,
int main( void ) ( String st r i ngl("Test -St ring " ) ; 1nt l aen ge - st rl ngl:
389
I
4
I
Objektorientierte Programmierung
cOut « "Anzahl Zeichen in st r in gl : " ret ur n 0;
«
laen ge
«
' \n ';
Wollen Sie zum Beispiel bei den komplexen Zahlen Folgend es realisieren myCom plex ~a l l(l.1. 2 , 2) : double ~ a12 - ~all : so sieht die Konvenierungsfunktion in der Klasse "myComplex« im pub 1i ( Bereich folgende rmaßen aus: ope rator dou bl e{) return _real ; Natürlich stehen Ihnen auch hier wieder alle Tore offen. Haben Sie zum Beispiel eine Schnittstelle fü r den Programmierer in der Klasse »myComplexK bereitgestellt und dieser versucht Folgendes auszuführen myComplex ~a l l(l.l . 2 . 2); char *val2 - va l l : können Sie in der Klasse »myComplex.. entweder eine Konvertierfunktion wie fo lgt einbauen operato r char"() [ cout « "myComplex - ) char* ni cht i mpleme ntiert\n "; oder eine entsprechende Fun ktion implementieren . Natürlich kann der Zieltyp auch aus mehreren Schlüsselwon en wie zum Beispiel unsig ned i nt bestehen. Bei der Verwendung von Konvertierfunktionen müssen Sie Folgendes beachten: ..
Eine Konvenierfunktion hat keinen Ergebnislyp, da dieser durch den Namen impliz it festgelegt wird. Ist beispielsweise der Name der Funktion "operator typ" so lautet der implizite Name "typ.. .
..
Die Fun ktion muss vom akwellen Objekt ein Objekt vom Ziel typ erzeugen und als Ergebnis zurückgeben.
.. Die Konvenierungsfunktion wird immer als Methode ohne Parameter definiert.
4.7
Vererbung (Abgeleitete Klassen)
Die Vererbung (oder auch das Ableiten von Klassen) ist ein sehr effizienter und häufig eingesetzter Mechanismus in der C++-Programmierung. Der Vorteil ist,
390
Vererbung (Abgelei te te Klassen)
dass hiermit bereilS existierende Klassen in neuen Klassen verwendet werden können. Eine so abgeleitete Klasse erbt dann die pub 1i c-Eigenschaften und Methoden der Basisklasse. Die abgeleitete Klasse wird dann gewöhnlich um weitere Eigenschaften und Methoden erweitert. Eine solche Vererbung von Klasse n wird gerne in der En twicklung von Klassenbibliotheken verwendet, wo gleichartige Methoden und Eigenschaft en einer Klasse benötigt werden, aber sich in getrennten Klassen befinden. Selbstverständlich können solche zusammengefassten Klassen noch weiter abgeleitet werden. Aber hierauf wird noch an einer anderen Stelle eingegangen. Der Hauptvorteil für den Program mierer besteht darin, dass dieser den Quellcode flir die Klasse nur einmal schreiben und ,.debuggen " muss. Klug eingesetzt können Sie solche Klassen immer wieder be i Ihren Projekten verwenden. Außerdem benötigt der Anwender einer Klasse keinen Quellcode und kann trotzdem mit einer Ableitung der Klasse eine vorhandene Klasse um weitere Eigenscharten und Methoden erweitern. Hierzu ist nur die Schn itlSlelle zur Basisklasse nötig. Dies wird u. a. in der GUI-Programmierung verwendet. Ein Objekt vom Typ einer abgeleiteten Klasse ist immer auch ein Objekt vom Typ der Basisklasse. Man sagt auch, dass die abgeleitete Klasse zur Basisklasse in einer Ist-Beziehung steht. Ein .. Auto", ist ei n ,.Fahrzeug« und ein .. Zug"' ist auch ein ,. Fahrzeug" (siehe Abbildung).
,.-
E;g.enochllften ur>d 1I~''''''meI_''''' Pab.n.ug
Fahrzeug (Basisktasse)
/
'"
Eigenoch~ten und KIa_nmetllOden def
,"-
Eigen""""IIen ...-.:j 1l1uHnm_d&r Kilo ....
r.b
r.b<""9
-
Z...tzklw
Kl.ossenmet _und _ Eigenoch~ten
KIassenm. _ ...-.:j _ Elgenochallen
11110...
Kilo ...
.."to
Auto Abbildung 4.3
.
~
ZLiQ
Ist-Beziehung der abg leiteten Klasse zur Basisklasse
39'
I
4 ·7
I
4
I
Objektorien tiert e Programm ierung
Das Gegenteil dieser ist-Beziehung ist die Hat-Beziehung, die entsteht, wenn eine Klasse ein Objekt der anderen Klasse als Element (Eigenschaft) besi tzt.
4.7.1
Anwendungsbeispiel (die Vorbereitung)
Als Beispiel soll eine einfache Versa nd fi rma verwendet werden, die ve rschiedene Gegenstände zum Verkauf anbietet. Die Ware selbst hat Eigenschaften wie die Bezeichnung. die Nummer. den Preis und die Anzahl der Waren. die noch im Lager vorhanden sind. Hierzu verwenden wir einfach eine Klasse mit dem Namen ~ Gegenstand ... Alle Eigenschaften und Methoden werden dabei gleich mit den Deklarationen und Definitionen in eine Datei geschrieben, um die Länge des Codes und der Erklärung nicht zu überstrapazieren. In der Praxis sollte man natü rli ch wieder di e Deklaration von der Defi nition trennen. Hierzu die Klasse »Gegenstand«, die in einer Headerdatei »gegenstand.h« zusammengefasst und auf das Nötigste beschränkt wurde: 11 gegensta nd.h
llinclude llinclude I/i fndef _G EGENSTAND_H_ lide fine _G EG ENSTAND_H_ using namespace std ; cl ass Gegenst and [ priva t e : char beze ic hnu ng(50) ; uns i gned i nt anzahl : uns i gned i nt nummer : 11 Preisan gabe in Cent uns i gned i nt prei s ; pub 1i C: // Konst ruktor Gegenstan d(const char * beze ichnung- · · . uns i gn ed i nt anza hl-O . unsi gncd int nummcr-O . unsigncd int pr cis-O
strncpy( this->bezei chnu ng , bezeichnung , 50 ) : t his ->anzahl - anza hl : t his -)nummer - numme r: th is-) preis - preis : I
11 Destrukt or
-Gegenst and {) { ) // Zugr if f smethoden
392
)f
Vererbung (Abgelei te te Klassen)
11 lesende r l ugri ff cons t char* get_beze i chnung() const l return beze i chnung ; I uns i gned int g et_anzahl{) const { retu r n anzahl ; I uns i gned int g et_nummer{) const { retu rn nummer : uns i gned int g et_pre i s{) co nst { return pr eis : ) 11 Schreibender Zugr i f f void se t _bezeichnung( const char * bez strncpy( bezei chnung . bez . 50 ) ; bezeic hnung [50 - 1] - 0 :
I
4·7
I
vold set anza hl { uns I gned i nt an zah l - anz : void se t nurrrner{ uns I gned i nt nurrrner - num : void se t_pre is ( uns I gned I nt pre is - pre : 11 Ausgabe a ller Eigenschaften void printe) const I cout « "Artikel « bezeichnung « ' \n ' : cout « "Anzahl « anzahl « ' \n ': cout « "N ummer « nummer « ' \n ': cout « " Preis « pre i s « ' \n ':
'"' ""' ,ce
I, lIendi f
Die Klasse »Gegenstand« können Sie jetzt auch ohne größeren Aufwand in einer Hauptfunktion testen: IImain . cp p lli ncl ude "gegens tand , h" int main ( vo id ) { Gegenstand ar t i kell : ar tikel1 . set_bezeichnung("Deo") : art i ke 11 . set_a nzah 1 (100) : artikell . set_n ummer(l) : arti ke11 . seCp re i s(399) ; artikell . prinU) ; return 0 ;
Mit der Klasse »Gegenstand« erhalten Sie schon mal die allgemeinen Daten, die eine Ware. die verkauft wird. haben kann. Natürlich lässt sich dies um weitere Eigenschaften erwei tern, Aber wenn die Gegenstände spezieller werden, benötigt man meistens weitere Eigenschaften . Verkauft die Versandfirma jetzt zum Beispiel auch Bücher, so werden weitere Eigenschaften wie de r Titel. der Autor. die Anzahl der Seiten usw. benötigt. Jetzt
393
4
I
Objektorien tierte Programm ierung
könnten Sie die Klasse ,.Gegenstand .. um diese Eigenschaften erweitern, oder Sie schreiben ei nfach ein e neue Klasse »Buch«, die dann auch die Eigenschaflen der Klasse ,.Gegenstand.. erhält (erbt) und verwenden kann. Damit bleibt die einfache Wiederverwendbarkeit der Klasse ,.Gegenstand« fü r weitere Gegenstände Ihres Versandhauses erhallen, und auch die Abstraktion der Daten ist leicht zu handhaben. Hierzu also zunächst die Klasse »Buch.. , ebenfalls in einer Datei (buch.h) und auf das Nötigste beschränkt: 11 buch .h lIinclude
cl ass Buch I priva t e : char t itel[50) : char autor[ 50) : uns i gned i nt se it en ; 11 us w. z ,B, Verlag , ISBN , etc
publ ; C: 11 Konst ruk to r Buch( const char· t itel - "" , const char * autor~ "', unsigned i nt seiten-O ) I st rncpy( thls·)tltel , titel , 50 ) ; this · )title[50 - I] - 0; strncpy( this ' )au t or , autor , 50 ) ; this ' )autor[50 - 1) - 0; t his ' )seiten - seiten ; 11 Oes trukt or -Buch ( ) 1I 11 lug r i f fsmethoden 11 Lesende r Zugri ff const char * geLt; tel () const 1 return t; tel ; I const char* get_autor() const 1 return auto r : I uns i gned int get_seiten{) const { retu rn seiten ; 11 Schreibender lugr i f f void set_titel ( const char* tit strncpy( titel. tit. 50 ) ; this · )title[50 - I] - 0;
394
Vererbung (Abgelei tete Klassen)
I
4 ·7
void se t_autor( const char* aut ) I st rncpy( autor . aut o 50 ) ; this->autor[50 - 1] - 0:
I
void set_seiten{ uns i gned i nt sei) 1 seiten - sei ; 1 11 Ausgabe der Eigenschafte n void printe) I « titel « ' \n ': cout « "Buch t i te l cout « "Au t or « au tor « ' \n ': cout « "Sei t en « sei ten « ' \n ': J,
lIendi f Auch diese Klasse können Sie ohne weiteres unabhängig von der Klasse »Gegenstand .. in der Praxis testen :
IImain . cp p lIinclude "buch . h" int main{ void ) { Buch buchl ; buchl . set_titel( "C++ von Abis Z") ; buchl . seLautor( "JÜ rgen Wolf ") ; buc hl. set_se i ten(lOOQ} ; buchl.p r int(} ; return 0; Bis jetzt haben Sie also im Grunde noch nichts anderes, als zwei voneinander unabhängige Klassen, die für ein Projekt verwendet werden können . Natürlich wurden diese beiden Klassen mit der Absicht erstellt, dass die Basisklasse »Gegenstand.. ist und die abgeleitete Klasse »Buch_. 4.7.2
Die Ableitung einer Klasse
Wollen Sie nun die Klasse »Buch .. von der (Basis-)Klasse _Gegenstand .. ableiten, so ist das seh r einfach. Sie müssen zunächst nur bei der Klasse »Huch.. eine Zeile ändern:
// buc h. h cl ass Buc h private :
pub1 1c Gegenstand I
1/
395
4
I
Objektorientierte Programmierung
pub 1 i C: }
11
,
Sie finden hier nach der Klasse ,.Buch ... einen Doppelpunk t, gefolgt von der publ i e-Ve rerbung (Zugriffsrechte) und der abschließenden Basisklasse (hier ,. Gegenstand ~).
Basisklasse
Eigenschaften und Klasservt1ethooen da, Klasse G·II·..· t ... d
Eigensch8ften und Klassenmethoden da, Klasse Abgeleitet von
G.I/.n.tan
der Klasse Gegenstand Zusätzliche Eigenschaften und Klassenmethodoo der .~~
Blich
Abbildung 4.4 Die Basisklasse und die abgeleitete Klasse
[»]
Hinweis Es soll nicht der Eindruck entstehen, dass die Basisklasse selbst keine abgeleitete Klassen sein kann. Es ist durchaus möglich. dass die Basisklasse auch eine abgele itete Klasse ist. Hierbei spricht man von einer indirekten Basisklasse. Damit lassen sich ganze Klassenhierarchien bilden.
Die Syntax sieht demnach wie folgt aus:
cl ass Klassenname : Zugr i ffs rechte Bas i sklasse I 11
Mit dieser Defin ition einer abgeleiteten Klasse legen Sie Folgendes fest: ..
Die Basisklasse. deren Eigenschaften und Methoden vererbt werden
396
Vererbung (Abgelei te te Klassen)
..
Die Zugriffsrec hte auf die Eigenschaften der Basisklasse
..
Die zusätzlichen Eigenschaften und Methoden, mi t denen die Basisklasse erweitert wird
I
4 ·7
I
public-Zugriffs rechte für eine abgeleitete Klasse Meistens wird beim Ablei ten von der Basisk lasse eine publ i c-Vererbung definiert. durch die alle öffendichen pu bl ic-Elemente der Basisklasse auch in der abgeleiteten Klasse zur Verfügung stehen. So können Sie mit Objekten der abgeleiteten Klasse auf Elemente der Basisklasse zugreifen. Im Fall der Klasse »Buch« können Sie alle publ ie-Elemente (hie r Methoden) der Klasse »Gegenstand« verwenden. Som it können Sie also alle pub 1i c-Elemente der Klasse »Gegenstand« über die abgeleitete Klasse »Buch« wie gehabt verwenden und noch erweitern. Hinweis Es ist nach wie vor nicht möglich, dass die abgeleitete Klasse auf die
pr i va te -El emente der Basisklasse direkt zugreift.
Erbschaft und Erweiterung Durch die Vererbung der Basisklasse "Gegenstand« an die Klasse »Buch« werden säm tliche Eigenschaften und Methoden vererb t: 11 buch . h
cl ass Buch private :
pub11c Gegenstand [
11
publiC: 11 ...
I,
Im Detail erbt die Klasse .. Buch« die Bezeichnung, die Nummer, die Anzahl und den Preis der Ware von der Klasse »Gegenstand«. Somit besitzt j edes Objekt vom Typ .. Buch« auch die Eigenschaften bezeichne r, nummer, anzahl und pre i s. Neu hinzu kommen außerdem bei de r Definition der abgeleiteten Klasse »Buch .. der Titel, der Autor und die Anzahl der Seiten , also die Eigenschaften ti te 1, au t or und sei ten. Somit hat ein Objekt vom Typ "Buch .. hier sechs Elemente (drei eigene und drei geerbte). Neben den Eigenschaften der Basisklasse .. Gegenstand_ werden auch sämtliche Methoden an die Klasse .. Buch_ weitervererbt. Dies sind neben dem Konstruktor die Zugriffsmethoden geLbezei chnung () , seLbeze i chnung () , geLanzahl ( ) usw. In der )(jasse ,. Buch_ wiederum wurden we itere neue Methoden definiert, die zu den bereits geerbten Methoden dazukommen.
397
[« J
4
I
Objektorientierte Programmierung
Besonde rs interessant erscheint in diesem Zusammenhang, dass beide Klassen eine Methode mit dem Namen "primO" implementien haben. Somit erbt die Klasse »Buch.. von der Klasse »Gege nstand .. ei ne Methode mit gleichem Namen. Man spricht in diesem Fall von einer Redefinition - die Methode »printO .. wird in der Klasse "Buch .. redefinien. Aber darauf wird noch nähe r eingegangen. Zugriff auf di e Elemente (d er Basisklasse)
Der Zugriff auf die Eigenschaften innerhalb der abgelei teten Klasse erfolgt über die Zugriffsmethoden . Dasselbe gilt fOr den Zugriff auf die Methoden der Basisklasse. Auch hierbei erfolgt der Zugriff auf die privaten Eigenschaften Ober die Zugriffsmethoden, sofern die Eigenschaften auch im privaten Bereich definiert wurden (was im Grunde immer zu empfehlen ist). SobaJd aber vom Zugriff innerhalb einer Methode die Rede ist. gibt es hierbei Unterschiede. Die Methode »primO" der Klasse "Buch .. sieht zum Beispiel wie fo lgt aus: 11 Ausga be der Eigenschaf ten
voi d Buch: : pri nt( ) I cout « "Buch t i te 1 cout « "Autor CDU t « ·Seiten
« « «
titel « ' \n ' ; autor « ' \n ' : sei ten « '\n' ;
Um noch die Eigenschaften rur die Klasse »Gegenstand .. mit auszugeben, können Sie nicht Folgendes einfugen: 11 Ausga be der Ei gensc haf ten '1o id Such : : prinH) \ cout « · Buc htite l « titel « ' \n '; cout « "Auto r « auto r « ' \n ': cout « · Seiten « seiten « ' \n '; 11 !!!! ! Feh l er !!!! ! - Eigenschaften s in d pr i va te « bezeichnung « • \n' : 11 co ut « "Arti kel « anzahl « '\n' ; cout « "Anzahl 11 « nummer « • \n' ; cout « "Hummer 11 « prei s « '\n' ; cout « " Prei s 11
Feh l e r Feh l er Feh l er Fehler
Hier bekommen Sie vom Compiler eine Fehlermeldung zurück, weil versucht wird, die Datenkapselung zu unterlaufen. Die El emente der Basisklasse "Gegenstan d.. sind nämlich alle als pr ; vate implemenLien. Daher kommen Sie zunächst nicht um die Zugriffsmethoden de r Klasse »Gegenstand .. _die im publ i c-Bereich der Klasse sind , herum:
398
Vererbung (Abgelei te te Klassen)
11 Ausgabe der Eigenschafte n woid Buch : :pr int() I « cout « " Bucht i tel « " Autor « cout « "Seiten « cout cou t « "Art1 k.el « cou t « " Anzahl « cout « " Mummer « « cout « "Pre1 s
I
4 ·7
titel « ' \n ' : autor « ' \n ' : se i ten « . \n ' : get_bezef chnu ng () « • \n ' : geCanzahl ( ) « • \n ' : geCnummer { ) « • \ n ' : get_pr e1 s ( ) « • \ n' ;
I
sicherlich stellen Sie sich die Frage. woher der Compiler wissen kann. dass die Methode .. gel_bezeichnungO.. oder ..gecanzahIO.. denn nu n eine Methode der eigenen (abgeleiteten) Klasse oder der Basisklasse ist. Der Compiler sucht zunächst in der eigenen (abgeleiteten) Klasse nach einer entsprechenden Methode bzw. nach einem entsprechenden Namen (wird auch als Name-Iookup bezeichnet). Findet er keinen entsprechenden Namen, sucht der CampHer in der Klassenh ierarchie eine Stufe höher, also in der Basisklasse. Handelt es sich um eine indirekte Basisklasse. wird so lange eine Stufe nach oben gegangen. bis eine Methode mit e iner entsprechenden Klasse gefunden wurde, oder das Programm lässt sich nicht übersetzten. In unserem Beispiel wird di e Methode "gecbezeichnungO« aus der Basisklasse .. Gegenstand .. verwendet. weil es in der abgelei teten Klasse .. Buch .. keine entsprechende Methode mit diesem Namen gibt. Würde es diesen geben. so würde tatsächlich die Methode der Klasse »Buch.. verwendet. auch wenn es eine gleichnamige Met.hode in der Klasse »Gegenstand« gibt. Hinweis Das es keine Probleme mit gleichnamigen Methoden gibt, liegt daran, dass die Methoden anhand der Signatur unterschieden werden und nicht durch de n Namen.
4.7.3
Redefinition von Klassenelementen
Rede finition bedeUlet. dass in der abgeleiteten Klasse eine Eigenschaft oder eine Methode mit demselben Namen nochmals neu definiert wird . obwohl diese bereits in der Basisklasse vorhanden ist. Wird also eine eigenschaft oder eine Methode aus der Basisklasse in der abgeleiteten Klasse erneut definiert, überdecken die neu definierten Elemente der abgeleiteten Klasse alle gleichnamigen Elemente der Basisklasse. Im Gru nde en tspricht dies dem Verhalten von globalen und lokalen Variablen, bei denen auch immer die lokalste Variable den »Zuschlag« erhält. Dies gi lt selbstverständlich auch für die überlad ung von Methoden, das he ißt, Sie können eine Methode der Basisklasse innerhalb der abgeleiteten Klasse auch mehrfach redeflnieren (und somit überladen).
399
[« )
4
I
Objektorientierte Programmierung
Um jetzt aus der abgelei teten KJasse auf die gleichnamigen Elemente der Basisklasse zuzugreifen, wird der Scope-Operator verwendel. In unserem Beispiel wurde in der Klasse ,.Buch" die Methode "prim{)« erneut redefini ert. Würden Sie diese Methode in der abgeleiteten Methode "primO" aufrufen, häue dies eine Endlosrekursion zur Folge, und das Programm ließe sich ohne Gewalt nicht mehr beenden. Es reicht also aus, die Basisklasse, den Bereichsoperator und die gleichnamige Funktion aufzurufen. Somit sieht unsere neue Version der Methode "printO" in der abgeleiteten Klasse »Buch« folgendermaßen aus:
vo i d BuC h:: print() I cou t « "B uchti te l «titel« ' \n '; cou t « "Autor « autor « ' \n '; cou t « 'Se iten « seiten « ' \n ' : 11 Ruft die Me t hode print de r Klasse Gegenstand auf Gegenstand::pr1nt(): Natürlich ist die Redefinition von Klassenelementen nicht auf Methoden beschränkt und lässt sich auch auf die Eigenschaften von Klassen anwenden. Allerdings ist dies in der Praxis sehen der Fall. Bisher wurde somit nur in der abgeleiteten Klasse _Buch" die Definition der Ableitung
cl ass Buch
publ1c Gegen stand I
I,
und die Methode _printO" in der Klasse _Buch" (buch.h) verändert. Damit lässt sich jetzt schon ein Objekl vom Typ »Buch« in einem Hauplprogramm verwenden:
IImain .cp p lI i ncl ude "gegenstand . h" lI i nclu de "bu ch . h" i nt main( void ) I ßuch buch1 : Buch ' buchPtr ; Gegenstand gegenstandH"Deo ". 10 0. 1234 . 5) ; buch1.seLtitel{ "C++ von Abis Z") ; buchl . se L autor{ "JDrgen Wol f") ; buchl . set_seitenf I098) ; buchl . set_bezeichnung( " 'T · Fac hbuch"); buchl . set_anzahl(2000J :
400
Vererbung (Abgelei tete Klassen)
bucht . set_nummer( 32654) ; 11 Alles Ausgeben buCh1.p r int() : cout « " \n\n" : 11 Nur den Tei 1 der Klasse Gegenstand ausgeben bucht . Gegenstand : : pr i nt{ ) : cout « " \n\n" : 11 Nur die Methode print de r Basisklasse 11 wi rd hie r aufgeru f en gegenstandl . pr i nt() ; 11 Auch mi t ei nern le i ger geht es cout « " \n\n" ; buchPt r - &buchl ; buchPt r - )p r int() : return 0:
C++ von Abis l JDrgen Wolf 1098 IT - Fachbuch 2000 32654
Ar tik el Anzahl Numme r Pre i s
IT-Fachbuch 20 00 32654
o
o Oeo
Ar ti kel Anzahl Numme r Pre ; s
100 1234 5
Buchtite l Au t or Seiten Ar tik el An zahl
C++ von Abis Z J[lrgen Wolf 1098 IT - Fachbuch 2000
4·7
I
Das Prog ramm bei der Ausführung : Buchtitel Autor Seiten Art i kel Anzahl Numme r Pre i s
I
401
4
I
Objektorientierte Programmierung
Nummer
3265 4
Pre ; s
o
Im Beispiel können Sie sehen, dass ein Aufruf mit
Gegenstand gegenstandl ; gegenstandl . pr int( ); nur die Methode »printO .. der Klasse »Gegenstand .. aufruft. Trotz der Redefin ition bleibt die Funktionalität der Basisklasse also erhalten. Der Aufruf von
BuCh buc h I : buchl . pri nt () : sorgt dafür, dass alle Eigenschaften inklusive der Basisklasse ausgegeben werden. Hierbei wi rd die redefinierte Melhode »printO" der Klasse ,.Buch _ aufgerufen . Will man trotzdem nur den Teil der Basisklasse _Gegenstand_ ausgeben. so muss nur der Scope-Operator dazu verwen det werden. wobei der Klassenname der Methode vorangestellt wird: 11 nur die Eigensc haften der Basisklasse ausgebe n buchl . Gegens t and ; : pri nt( ) ;
4.7 .4
Konstruktoren
Bevor Sie einen speziellen Konstruktor für die abgeleitete Klasse erstellen, müssen Sie zunächst wissen, wie die Daten der abgeleiteten Klasse und Basisklasse auf- und abgebaut werden. Wird ein Objekt vom Typ einer abgeleiteten Klasse angelegt, so erfolgt der Aufbau stets von oben nach unten (von der Klassenhierarchie her gesehen) oder auch von innen nach außen. Es wird also immer zuerst der Konstruktor der Basisklasse ausgeführt. gefolgt vo m Konstruktor der abgeleiteten Klasse. Der Abbau eines Objekts geSChieh t genau in der umgekehrten Reihenfolge. Zuerst wird der Destruktor der abgeleiteten Klasse aufgerufen und anschließend der DeslTuktor der Dasisklasse. Default-Konstruktor Den Default-Konstruktor stellt immer die abgeleitete Klasse zur Verfügung, da dieser auch alle Elemen te der Basisklasse enthält. Allerdings ist von der Verwendung des Default-Konstruktors bei abgeleiteten Klassen abzuraten.
402
Vererbung (Abgelei te te Klassen)
Bezogen auf die abgeleitete Klasse sehen:
~ B uch ~
I
4 ·7
könnte ein Konstruktor wie folg t aus-
Buch( const char * bezeichnung-· '. unsigned int anzahl-O . unsigned int numme r-O . unsigned int preis-O . const char * titel* ". const char * autor· "". unsig ned i nt seiten-O ) I 11 Elemente der Basisklasse set_ bezeic hnu ng(bezeichnung) ; seLanzahl (anzah l ) ; set_numme r (nummer) ; set_preis(p re is) ; 11 Elemente der abgele i teten Kl asse strncpy( t his·>tite l, tit el. 50 ) ; th i s-> t it e l[50 - 1] - 0; strncpy( this->au t or , autor , 50 ) ; t his ->autor(50 - 1) - 0; t his ->seiten - seiten :
I
Hier auch gleich der Grund, warum vom Default-Konstruktor abgera ten wird. In diesem Beispiel wird näm lich erst implizit der Default- Konstruktor fu r die Basisklasse »Gegenstand « aufgerufen und anschließend werden diese Eigenschaften mit den set-Zugriffsmethoden mit den entsprechenden Werten initialisiert. Also doppelte Arbeit - zunächst wird der Default-Konstruktor der Klasse ~ G egen stand" aufgerufen und ini tialls iert das Teilobjekt mit einem leeren String bzw_ mit dem Wert 0 (abhängig von den Eigenschaften), Und dann wi rd das Teilobjekt naChträglich wieder ve rändert. Abgesehen vom Zeitaufwan d Neben der verl orenen Zeit setzt das außerdem voraus, dass die Basisklasse einen Default-Konstruktor besitzt. Wenn anschließend die Teilobjekte mit ihren Anfangswerten initialisiert wurden. können erst die Eigenschaften der abgeleiteten Klasse mit Werten versehen werden. Basi si n itial is ierer In unserem Beispiel hat die Basisklasse »Gegenstand« einen eigenen Konstruktor, den man aufrufen kann und soHte, sodass die Elemen te der Basisklasse sofort mit den richtigen Werten initialisiert werden. Hierzu verwendet man in eH den Basisinitialisierer. Der neue Konstruktor der abgeleiteten Klasse »Buch« sieht so aus:
Konst ruktor Buch( const char ' titel - ·· . const char * autor- "". unsigned in t seiten- O ) I strncpy( this ->tite l. t itel. 50 ) :
11
403
4
I
Objektorien tierte Programm ierung
this -)ti t e1[ 50 - 1) - 0: strncpy( this -)autor , au t or , 50 J : this -)auto r [50 - 1) - 0: this -)seiten - se i ten :
11 Kons t rukto r mit Basisin it ialisie rer Buch( cons t char* bezeichnung, unsigned i nt anzahl , uns i gned i nt nummer , unsig ned int preis , cons t cha r* t itel - "", cons t char* autor - "", uns i gned i nt sei te n- O } :
Gegensta nd(beze1 c hnung, anzahl, n umme r , pre 1s)
st rncpy( this -)titel , titel . 50 ) ; this -)titel[5 0 - 1] - 0: strncpy( this -)a utor , au tor . 50 ) : this-)autor[50 - 1] = 0: this-)se i ten = seiten : Der Basisinitia lisierer wird bei der DefiniLio n am Ende der abgeleiteten Klasse, durch einen Doppelpunkt getren nt, angegeben und besteht aus dem Namen der Klasse und der Argumen ten liste mit den Werten, mit denen die Eigenschaften der Basisklasse initialisiert werden solle n.
[»]
Hinweis Beachten Sie bitte, dass Sie neben dem Konstruktor mit Basisinitialisierer auch noch den herkömmlichen Konstruktor der Klasse _Buch .. verwenden und erhalten .
Elementinitialisierer
Natürlich können Sie fur die Eigenschaften der abgeleiteten Klasse auch den Elementinitialisierer verwenden, den Sie in Abschnilt 4.4. 11 kennen gelernt haben, in diesem Fall auc h mit dem Basisinitialis ierer zusammen, um beide Teilobjekte zu initialisieren. Damit können Sie Basis- und Elementini tialisiercr, durch Komma getrennt, in einer Liste verwenden: 11 Konstr uk tor mit Basisinitial is i ere r
und Clementinitialis i erer Buch( const char· bezeichnung . unsigned uns i gned i nt nummer . unsig ned int const char '~ titel- "" . const char" uns i gned i nt seiten-O ) : Gegensta nd (beze i chnu ng. anzahl . 11
se1ten(se1ten ) {
strncpy( this -Hitel . t it el . 50) ; t his-)titel(50 - 1] - 0:
i nt anzahl . preis . autor- "". nummer . pr eis)
Vererbung (Abgelei te te Klassen)
I
4 ·7
st rnc py( th i s->auto r, autor . 50 ) ; this->autor[50 - 1] - 0 ;
4.7.5
Destruktoren
Für die abgeleitcte Klasse ist nur ein spezieller Dcstruktor nötig, wenn ein Vorgang rückgängig gemacht werden soll, wie das Freigeben eines dynamisch reservierten Speichers an den Heap. Der Destruktor der Basisklasse hingegen wird immer automatisch ausgeführt und muss nicht explizit aufgerufen werden.
4.7,6
Zugriffsrecht protected
Der Zugriff von den abgeleitClen Klassen auf private Elemente der Basisklassen ist weder mit Methoden oder friend -Funktionen möglich. Die einzige Möglichke it wäre, ein privates Element der Basisklasse in den pub 1i c-Bereich zu stellen, was aber nicht im Sinne der OOP ist. Somi t benötigt man einen Mechanismus, der zwischen private und public liegt. So eine Verfeinerung wurde mit dem Schlüsselwort protected eingeftlhrt. Die Zugriffsrech te von protected sind von außen nach wie vor dieselben wie bei pri va te , mit dem Unterschied, dass jetzt auch Methoden bzw. fri end-Funktionen einer abgeleiteten Klasse auf die protected -Elemente der Basisklasse zugre ifen kön nen. Ei n Bereich, der mit protected dek lariert wird, ist für abgeleitete Klassen (und natürlich die eigene Klasse) erreichbar - von außen hingegen lässt sich nach wie vor nichts machen (siehe Tabelle 4.3). S,hlusse-lwort
Eigene I
Abge-leltete Klasse
Außerhalb
pr1vate protected
Sichtbar
Nicht sichtbar
Nicht sichtbar
Sichtbar
Sichtbar
Nicht sichtbar
plJb!1 c
Sichtbar
Sichtbar
Sichtbar
Tabelle 4.3 ZlJgriffsschutz und dessen Sichtbarkeit
Auch wenn man mit dem Schlüsselwort protected Eigenschaften von der Basisklasse an die abgeleitete Klasse weitervererbt, sollte man Vorsicht wa lten lassen, weil man hierm it die Datenkapselung etwas lockert. Meistens wird nämlich eine solche protected-Deklaration nachträglich eingebaut oder wieder entfernt. Dabei kann es schnell passieren, dass die ein oder andere Methode der abgeleiteten Klasse nicht mehr funktioniert (gerade dann, wenn man das Schlüsselwort pro tee ted nachträglich entfernt). Hier ein Beispiel dazu:
I
4
I
Objektorien tiert e Programm ierung
lIinclude us i ng name space std : class Basi sk l asse I pr i va te : int int_private ;
prote cted: 1nt ln t _pro tec t ed ; pu b 1i c : i nt int_pub li c ; 11 Kons t r uktor Bas i sklasse{i nt vall - O. int va I 2- 0. i nt va13-01 I i nt_p r ivate - val! : i nt_p r otected - val2 : i nt _public - val3 :
c l ass Abgl e it eteKla sse pub lic Bas is klasse I pub 1i c : 11 Konst ruk to r Abgeleitete Klasse (in t vall~O. int vaI2-0 , int val3=Ol I 11 I!! Fehle r, da private I!! in t _priva t e - va ll ; 11 Ok , da prot e ct ed f nt _pro t ect ed - va12: 11 Ok , da public int_public - val3 : void rese L ints() { 11 I !! Fehler , da pr i vate I!! i nL pr ivate - 0: // Ok , da prote ct ed
1nt_protected - 0 ; // Ok , da publ i c i nL pub l ic - 0: }
,
Vererbung (public. private und protected)
Bisher haben wir nur die pu b li c-Schnittstell e verwendet, wenn w ir be i de r abgelei teten Klasse die Eigenschaften der Bas isklasse erben wo llte n. Allerd ings ist de r Zugriff auf die Basisklasse nicht auf eine pub 1i c-Ve rerbung beschrän kt. Die pub li c-Vererbung sah folgenderm aßen aus:
406
Vererbung (Abgeleitete Klassen)
I
4·7
cl ass Abgele i teteKlasse : pub l ic Basisklasse Bei einer pub I ; e-Vererbung werden ..
die publ ie -Elemente der Basisklasse in der abgeleiteten Klasse ebenfalls als publ i e-Elemente übernommen.
..
die proteeted-Elemente der Basisklasse in der abgeleiteten Klasse ebenfalls als pr otected übernommen und bleiben in der abgelei teten Klasse auch pro tected .
Eine Stufe darunter kann eine prot ected-Vererbung wie folgt vereinbart werden: class AbgeleiteteKlasse
pr otected Basisklasse
Bei einer solchen protected-Vererbung werden ..
die publ i c-Elemente der Basisklasse in der abgeleiteten Klasse als protectedElemente übernommen.
..
die proteete d-Elemente der Basisklasse in der abgeleiteten Klasse ebenfalls als pr oteeted übernommen und bleiben in der abgelei tete n Klasse auch protected .
Und zu guter Leut ist auch noch eine pr ; va te-Vererbung möglich: cl ass AbgeleiteteKlasse
private Basis kl asse
Bei einer pri va te-Vererbung werden ..
die publ i c- Elemente der Basisklasse in der abgeleiteten Klasse als pr i vateElemente übernommen.
.. die protee ted-Elemente der Basisklasse in der abgeleiteten Klasse als pr; va te-Elemente übernommen. Somit stehen bei pr ivate- und pro tected-Vererbungen in der abgeleiteten Klasse die öffentlichen Schnittstellen der Basisklasse nicht mehr zur Verfügung. Wenn Sie dennoch in die Verlegenheit kommen, einzelne Teile bei der abgeleiteten Klasse in der neuen öffentlichen Schnittstelle übernehmen zu müssen, können Sie dies mit einer Zugriffsdeklaration wie folgt machen: Basisklasse : : Element ; In der Praxis: class AbgeleiteteKlasse public: Basisklasse : : varl :
private Basisklasse (
I
4
I
Objekto rientie rte Programmie rung
protected : Bas i s kl asse ; ; var2 ;
" So steht das pu bl i c-Element ~var 1 « der Basisklasse auch fQr die abgeleitete Klasse zu r Verfügung und ist ebenfal ls pu bl ic . Dasselbe kann auch mit ei nem protec t ed-El ement (hier im Beispiel mi t »var2«) gemacht werden. Auch hierbei ist das pro tee t ed-Element in der abgelei teten Klasse anschließend protect ed. Die ZugrifTsdeklaration beschränkt sich auße rdem nicht nur auf die Eigenschaften der Basisklasse, sondern ist auch bei den Methoden gültig .
4.7 .7
Typenumwandlung abgeleiteter Klassen
Wenn ein Objekt einer abgeleiteten Klasse eine r Basisklasse zugewiesen wird, erfolgt implizit eine Typenumwandlung in den Typ der Basisklasse . So können Sie Objekte vom Ty p ,.Buch« an Objekte von Typ »Gegenstand« zuweisen (siehe Abbildung 4.5).
Eigenschatlen und Klassanmll\hodo.n d.... Klasse
_
E;geoschatlen und KJas ...n"",thocilln . ., Klasse
Zusätzliche Eig&nsc hahen und Klass.enmelhoden (!er Klasse Bue!>
Abbildung 4.5
Zuweisung der Klasse »Bucn« an Basisklasse »Gegenstand«
Wird ein Objekt einer abgeleiteten Klasse an ein Objekt der Basisklasse zugewiesen, werden (logischerweise) nur die Eigenschaften der Basisklasse zugewiesen . Die anderen Komponenten werden nicht berücksich tigt: IImainl . Cpp lIinclude "gegenstand .h" llinclude "buch . h" int milin( voin 1 I Gegenst and gegenstandl : Buch buchl( " Il - Fac hb uch ". 100 . 123 . O. "e++ von A bi s Z". " J . Wo lf" , 1000 ) ;
408
Vererbung (Abgeleitete Klassen)
I
4 ·7
11 Implizite Typenumwandlung
gegensta nd l - buchl : gegens tandl . pr ; nt( ) : re turn 0: Das Programm bei der AusfOhrung:
Art i ke 1 Anzahl Nummer Pre i s
IT- Fachbuch 100 123
o
Eine umgekehrte Zuweisung von einem Objekt der Basisklasse an ein Objekt der abgeleiteten Klasse wie beispielsweise
abgeleiteteKlasse - Basisk l asse - Fe hler buc h1 - gegenstandl :
11
ist nicht möglich. weil die Eigenschaften der abgeleiteten Klasse nicht zugeordnet werden können und undetlniert wären . Natürlich ist eine Zuweisung trotzdem möglich. Sie könnten zum Beispiel in der abgelei teten Klasse den Zuweisungsoperator entsprechend überladen. Es würde aber auch genügen, wenn ein Konstruktor der Basisklasse als Parameter eine Refe renz auf ein Basisobjekt hat. Die Typenumwandlung wird dann vom Konstruktor übernommen. Diese Ist-Beziehung zwischen der abgeleiteten Klasse und der Basisklasse gilt selbstverständlich auch für die Verwendung von Zeigern (bzw. BasiskJassenze igern) und Referenzen. So können Sie auch Basisklassenzeiger verwenden, die auf Objekte abgeleiteter Klassen verweisen. Wobei hier auch nur die öffentlichen Schnittstellen der BasiskJasse verwendet werden können. Es können also mit einem Basisklassenzeiger keine Methoden aufgerufen werden, die in der abgeleiteten Klasse (re)deflnien wurden. Ein Zeiger kann letztendlich auch nur auf etwas zeigen. dessen Typ er selbst repräsentiert. Dasselbe gilt analog beim Arbeiten mit Referenzen. Hierzu ein Beispiel:
IImain2 .cpp llinclude "gegenstilnd . h" lIinclude "buch . h" int main( void ) { 11 Basis klassenzeige r Gegensta nd* gegenstandPtr : Buch buc hU "IT-Fachbuch ". IOD , 123 . O. ·C++ von Abis Z", " J . Wol f", 1000 ) :
I
4
I
Objektorientierte Programm ierung
11 Adresse zuweisen gegenstandPtr - & buc hl : gegenstandPtr->pr i nt{ ) : cout « ' \ n' : 11 Refe r enz auf Basiso bje kte Gegenstand& gegenstand Ref - buchl ; gegenstandRef . print() ; return l) ;
Das Programm bei der Ausführung: Art i kel Anzahl Numme r Pre i s Art i kel Anzahl Numme r Pre i s
IT-Fachbuch
10 0 123
0 [T - Fachbuch
100 123
0
Explizite Typenumwandlung
Wollen Sie dennoch , dass alle Elemente bei der Zuweisung einer abgeleiteten Klasse an eine Basisklasse ausgegeben werden, können Sie dies mit einer expliziten Ty penumwand lung wie fo lgt erzwingen : 11 Basis k lassenze i ger gegenstandPtr : Buch buch l( " IT - Fachb uch" . 100 . 123 , 0 , ·C!! von Abis Z·, " J . Wolf ", 1000 ) .
Gegensta nd~
11 Ad ress e zuwei sen gegenstandPtr - & buchl : 11 Exp 1 i zi te Typenumwandl ung (( Buc h~ ) gegens ta nd Ptr )-> pr 1nt() ;
Die Ausgabe: Bucht i tel Autor Seiten Ar ti kel Anzahl Numme r Pre i s
4 0 '
C++ von Abis Z J . Wolf 10 00 !T - Fachbuch
100 123
0
Vererbung (Abgelei te te Klassen)
Beachlen Sie aber. dass eine explizite Ty penumwandl ung auch ihre Tücken haben kann. Denn falls hier »gegenstandPtr« nicht auf ein Objekt vom Typ _B uch« zeigt, so wird mi t de r Methode _prim« de r Klasse »Gegenstand« auf einen undefinierten Speicherbereich zugegriffen, der nich t zum Objekt gehört. 4 .7 .8
Klassenbibliotheken erweitern
Es wurde bereits erwähnt, dass der Vortei l beim Ableiten einer Klasse, neben der vereinfachten Datenabstraktion, darin besteht, eine bereitS vorhandene Klassenbibliothek. deren Quellcode Sie nicht benötigen, zu erweitern - was theoretisch auch die Standardbibliotheken mit einschließt. Wenn Sie eine Klassenbibliothek erweitern wollen, benötigen Sie den übersetzten Quellcode, der entweder in Form einer Objektdatei Cobjl.o) oder einer Bibliotheksdatei (.Iib/.a) vorliegt. und die entsprechende Headerdatei (.h) . Im Grunde haben Sie ja von der Standardbibliothek auch nicht mehr. Jetzt müssen Sie nur noch die Headerdatei in Ihren Quellcode einbinden und dem Linker die Objektdatei (. objl.o) oder Bibliotheksdatei (. lib/.a) mitteilen: II mei nP rojekt . cpp lIinclude "org i nal_klasse.h"
int main ( vaid ) [ 11 ...
Hier haben Sie die Headerdatei »orginal_klasse.h« miteingebunden . Im Beisp iel soll außerdem noch eine Objektdatei mit dem Namen »orginaLklasse.obj« vorhanden sei n. Diese linken Sie nun ebenfalls zum Program m hinzu . Sie wollen j etzt also die Originalklasse erweitern . Hierzu erstellen Sie zunächst eine eigene neue Headerdatei rur eine neue Klasse (im Beispiel soll der Name »meine_Klasse.ho< verwendet werden). Hierbei binden Sie die neue Headerdatei lO orginal_klasse.h" ein und deklarieren eine Ableitung der Originalklasse: 11 me i ne_Klasse.h lli fndef MEINEJLASSE_H lide fine MEINEJLASSE_H l i ncl ude "or gi nal _kl asse. h" cl ass meineK l asse : pub lfc org f nal Kl ass e 11 Dekla r at ionen }
,
lIendi f
411
I
4 ·7
I
4
I
Objektorien tierte Programm ierung
Jetzt erstellen Sie gewöhnlich noch eine weitere Quelldatei <.cpp) wo Sie die Definitionen der Methoden der neuen Klasse definieren. 11 me1ne_Kl asse.cpp
'lnc l ude me lne_k l asse.h " R
11 Definit i onen der Methode n
Jetzt müssen Sie in Ihrem eigenen Programm di e neue Headerdatei "meine_ Klasse. h.. mit einbinden und müssen selbstverständlich auch die Quellda tei ~ meine_ Klasse.c p p .. zum Projekt hinzufügen: 11 mei nProj ekt . cPP '1nc l ude me l ne_ kl asse.h " R
int main ( void ) I 11 ...
4.8
Polymorph ismus
Ein auf den ersten Blick seltsames Wort für die Informatik (Polymorphie = Vielgestaltigkeit), was sich aber schnell erklären lässt. In einem der vorangegangenen Abschniu (Abschniu4. 7.7, Beispiel ma i n2. cpp) wurde folgende r Code verwendet: 11 Basisklassenzeiger Gegenst and * gegensta ndP tr : Buch buch}( " Il -Fac hbu ch ". 100 . 123 . O. "eH von Abis Z" . " J. Wolf" . 1000 ) : 11 Adresse zuweisen gegens t an dPtr -& bucht : gegenstan dPtr->prl ntC );
In diesem Beispiel haben Sie die Basiskl asse _Gegenstand .. und die abgeleitete Klasse _Buch« verwendet. wobei beide Klassen die Methode "printO'" beinhalten. Und trotzdem wird in diesem Beispiel die print-Method e der Klasse »Gegensl
Polymorphismus
Natürlich kann eine solche »Gestalt.. nicht mehr statisch erfolgen. sondern sie wird dynamisch zur Laufzeit und nicht mehr zur Obersetzu ngszeit gebunden. Hierbei wird zunächst beim Aufruf einer solchen Methode vom System der Typ des Objektes untersucht. auf das die Met.hode angewandt werden soll. Abhängig vom Typ wird diese Methode mit »Gegenstand::printO« oder ,.B uch::printO" ausgewählt. Hierbei handelt es sich um eine dynamische Bindung. Solche dynamischen Bindungen werden mit vinuell en Methoden realisiert. 4.8.1
Statische bzw . dynamische Bindung
Seide Begriffe sin d nicht schwe r zu verstehen. aber es stellt sich die Frage. wie der Compiler das macht? Beim Aufruf gewöhnlicher Methoden steht zum Zeitpunkt der Übersetzung die Adresse der Methode fest. die aufgerufen werden soll. Die Adresse wird hie rbei fest im Maschinencode des Methodenaufrufs gespeiche rt. Bei einer statischen Bindung spricht man daher auch von einer frühen Bindung. Virtuelle Methoden werden über Zeiger bzw. Referenzen realisiert und wissen zur Laufzei t noch nicht, welche Methode ausgeführt werden soll . Hierbei können versch ied ene virtuelle Methoden aufgerufen werden. Welche Methode das ist, hängt immer davon ab, welches Objekt der Zeiger bzw. die Referenz adressiert. Der Compiler erzeugt einen Code. der erst zur Laufzeit gebunden wird. Diese dynamische Sindung bezeichnet man auch als späte Bi ndung. Eine solche dynamische Bindung lässt sich hervorragend verwenden, wenn man eine Klassenbibliothek erweitern will, deren Quellcode man ni cht besitzt, man kann soga r nachträgllch einen bereits übersetzten Code erwei tern. Dies wi rd gewöhnlich bei der Erweiterung kommerzieller Bibliotheken eingesetzt, von denen man meistens nur die Headerdateie n und die übersetzen Module (Objektdateien; .obj) besitzt. Von der kommerziellen Klasse muss anschließend nur noch die eigene Klasse abgeleitet werden, und neue virtuelle Methoden müssen dafür definiert werden . Dank der dynam ischen Bindung lassen sich so von den Methoden der Klassenbibliothek neue Methoden aufrufen. 4.8 .2
Virtuelle Methoden
Sie haben gelernt, dass ein »Such ..-Objekt ein »Gegenstand«-Objekt ist. Bisher haben Sie erfahren, dass ein »Buch«-Objekt die Eigenschaften und Methoden seiner Basis kl asse (»Gegenstand«) geerbt hat. Diese Ist- Beziehung ist aber noch nicht am Ende, wie Sie in Abschnitt 4.7.7 erfahren haben, wo eine abgeleitete Klasse an den Zeiger einer Basisklasse zugewiesen wurde. Beispielsweise ist auch Folgendes möglich:
413
I
4.8
I
4
I
Objektorien tiert e Programm ierung
Gegenst and * gegenstandl - new Buch ; Damit erzeugen Sie ein neues "Buch .. -Objekt auf dem Heap. Ungewöhnlich erscheint auch hier, dass der Zeiger vom Typ "Gegenstand .. ist. Aber das ist durchaus sinnvoll. da ein Buch ja auch ein Gegenstand isL über diesen Zeiger können Sie jetzt jede Methode der Klasse "Gegenstand .. aufrufen . wie Sie dies ja bereits mit folgendem Codeausschniu gesehen haben: 11 Bas i sklassenzei ger Gegenstand * gegenstandPtr ; Buch buchl( "IT -Fachbuch " , 100 , 123 . O. "C++ von Abis ZO , " J . Wol f " , 1000 ) ; 11 Adresse zuweis en gegenstandP t r - & buchl ; gegenstand Ptr- >p r int() ;
Der Nachteil war. dass so nur die Eigenschaften des Objekts vom Typ "Gegenstand ... ausgegeben wurden . Es wurde bereits eIWähnt. dass man hierbei theoretisch die Basisklasse um eine Eigenschaft (eine Referenz) eIWeitern könnte. die sich den Typ des Objekts merkt. Zusätzlich käme noch eine swi tc h-Abfrage hinzu. die dann die entsprechende Methode aufrufen wü rde, Allerdings häuen Sie zum einen das Problem, dass Sie bei jeder weiteren Ableitung den Code um eine ca se-Marke eIWeitern und somit das Programm auch neu übersetzen müssten, Außerdem bedeutet dies eine Menge weiterer Codezei len und Überprüfungen, was auch zusätzliche Rechenzeit fu r den Rechner bedeutet. Sie werden sich vielleicht gefragt haben. warum ich hierzu kein Beispiel erstellt und demonstriert habe. Das war nicht nötig. weil es hierfür die virtuellen Methoden gibt. Die Lösung fLir die Polymorphie (Vielgestaltigkeit) lautet also virtuelle Methoden . Dami t ist es praktisch möglich , dass eine in »Buch .. redefinierte Methode korrekt aufgerufen wi rd . Wenn ein Basisklassenzeiger "G ege n stand ~ auf ein Objekt der abgeleiteten Klasse "Buch... zeigt, soll es möglich sein, dass mit der Anweisung
gegenst andPtr ~ & buch1 : gegens t andPtr ->pr i ntc ) : alle Eigenschaften des Objekts (also die Eigenschaften von "Such..) ausgegeben werden. Auf der anderen Seite sollen bei einem Aufruf wie
gegensta ndPtr -& einGe genstand : gegens ta ndPtr ->pr i nt{ ) : nur die Eigenschaften des Objekts vom Typ »Gegenstand .. ausgegeben werden,
Polym orphismus
I
4.8
Hierzu müsse n Sie nur d ie em sprechende(n) Methode(n) mit d em schlüsselwort vi r t ua 1 deklarieren. Die Definitio n un tersche ide t sich n icht von der Definitio n der anderen Methoden . Hinweis
Konstruktoren können nicht als vi rtud 1 deklariert werden.
[
Hier noch mals die Klasse " Gegenstand ~ (gekü rzt) mit der virtuellen Methode "primO ~ :
11 gegenst and.h llinclude Uinclude lli f ndef _GEG ENSTANO_ H_ lIdefine _G EGENSTANO_H_ us i ng namespaee s t d ;
e l ass Gegenstand private :
pub l i e : 11 Virtuelle Methode v1 rtual vo1d pr i nt C) cons t I eout « "Arti ke l « bezeichnung « ' \n '; eout « "Anzah l « an zahl « ' \n '; « nummer « ' \n' ; eou t « "Nummer eou t « 'Pr e is «pr ei s « ' \n ';
I, lIendi f
J etzt noch die Klasse ~ pri nt O" beinhaltet:
,. Bu ch ~.
die ebenfalls eine redefinierte virtuelle Meth ode
11 bu ch .h lIincl ud e liinclude lIinelude "gegenstand . hO lIi f ndef _ BUCH_H_ lIdefine _ BU CH_H_
elass Buch : publ ie Gegensta nd I pr i volte :
415
I
4
I
Objektorientierte Programmierung
pub li C: 11 Implizit virt ue ll vi rtua 1 vo1 d pr1nt() const I
cout « "Buchtitel cout « "Autor cout « "Seiten Gegenstand : :p ri nt( ) :
« titel « ' \n ': « autor « ' \n ': « seiten « ' \n ':
I,
lIendi f
Jetzt haben Sie zwei Funktionen als virtuell deklarien. Diese Technik wird gewöhnlich dann verwendet, wenn eine abgelei tete Klasse eine eigene gleichnamige Methode zur Basisklasse definiert. Damit erreichen Sie, dass der Compiler jetzt veranlasst wird, immer die richtige Methode zum zugehörigen Objekt aufzurufen, Damit das funktioniert, ist es wichtig, dass das Objekt über einen Zeiger oder eine Referenz auf die Basisklasse angesprochen wird. Das ist de r Unterschied zu den herkömmlichen Methoden. Da Sie nicht erwarten können, dass der Programmierer immer zuerst einen Basisklassenzeiger auf ein Objekt wie folgt verwendet gegenstandPtr ~& buchl : gegens tandPtr - >pr i nt( ) :
können Si e alternativ auch eine globale Funktion schreiben. Damit stell en Sie sicher, dass die vinuelle Methode immer mit einem Basisklassenzeiger aufgerufen wird: void myP r int{ Gegenstand ~ 9 ) I g - )p r int{) :
Diese Funktion können Sie nun wie folgt aufrufen: myPr ; nt( &buchl ) ;
Natürlich können Sie auch diese Funktion mit einer Referenz als Parameter implementieren: vo i d myPrint{ Gegenstand & 9 ) [ g . p r int() :
416
Polymorphismus
I
4.8
Hierbei benötigt man beim Aufruf der Funktion natürlich kein en Adresseoperalor mehr:
myP rint( buch! ) ; Hierzu di e kompl ette Hauptfunktion mit ei nigen Beispielen, wie die virtu ellen Melhoden aufgerufen werden können: 11 main.c pp lIinclude "gegenstand . hO lIinclude "buch . h"
vo1d myPr1nt( Gegensta nd* 9 ) ( g-> pr1nt ( ) ;
int main( void ) 1 11 Basisklassenzeiger Gegenstand* gegenstandPtr : Gegens t and gegenstandI< "Roman ", 100 . 124 , 0 ) ; BuCh buchl( "!T - Fachbuch" . 100 , 123 , O. "CH von Abis Z" . "J . Wol f", 1000 ) ; gegenstand Ptr - & buchl ; gegenstandPtr->pr1nt() : cout « ' \n '; gegens t and Pt r - & gegens t andl ; gegenstandPtr -> pr1nt(); cou t « ' \n ': myPr1nt( &buchl ): cout « ' \n' ; myPr1nt( 'gegenstandl ): return 0; Das Programm bei der Ausführung:
Buchtit el Autor Seiten Art i kel Anzahl Nummer
C++ von Abis Z J . Wolf
1000 IT - Fachbuch 100 123
417
I
4
I
Objektorien tierte Programm ierung
Pre i s
0
Art i kel An zahl Numme r Pre is
Roma n 100 124 0
Ar tike l An zahl Numme r Pre i s
Roman 10 0 124 0
Bucht i tel Au t or Seiten Art ikel Anzahl Nu mmer Pre is
C++ von Abis Z J . Wolf 1000 IT ·Fa chbuch 100 123 0
An der Ausgabe des Programms können Sie feststellen, dass imme r die richtige vi rtuelle Methode aufgerufen wird.
4 .8.3
Virtuelle Methoden redefinieren
Die Verwendung der virtuellen Methoden kann auf den ersten Blick ziem lich komplex sein. Eine vi rtuelle Methode der BasiskJasse muss zunächst nich t in der abgeleite ten Klasse als virtuell redefiniert werde n. Die abgeleitete Klasse erbt sowieso die virtuelle(n) Methode(n). Ein einfaches Beispiel: 11 vituall.c pp llinclude us ; ng namespace std ;
class Basi sk la sse I pub 1i c : virtual vo i d f unkt1 o nl()
c:on~t
I
cou t « "Bas i sklasse :: f unk tionl()\n ";
c l ass abge le iteteK lasse : public Basisklasse I publ i C: void funktion! () cons t cout « "Abgeleitet : : fun kt ionl \n " :
418
Polymorphismus
I
4.8
I
iot main( ~oid ) 1 abgeleiteteKlasse abgelObje kt : Basisklasse* bas i sPtr : basisP t r - &abge l Objekt : // Zugriff Ober den Basis klassenzeiger basi sPt r - )funktionl() : 11 Zugriff Ober das Objekt der Klasse abge l eite t eKlass e abgelObj e kt . funk ti onl() : re t urn 0; Die Ausgabe des Programms: Abgel ei tet : : funkt i onl Abgel ei tet : : fu nkt i onl Oh ne das Schlüsselwort vi rtual vor der Methode ,.fun ktion10 .. in de r Basisklasse sähe die Ausgabe folgendermaßen aus: Basisklasse : : fu nktionl() Abgeleitet :: funktionl Auf diese Weise kann man festlegen. dass ,.einmal virtuell immer virtuell" bedeutet. Ei ne neue redefinierte Version ist auch wiede r virtuell. Zwar kann bei der redefinierten Version auch wieder das Schlüsselwort vi rtua 1 verwendet werden, es muss aber nicht verwendet werden. An ders herum hat aber Folgendes keinen Effekt: c l ass Basisklasse { publ i c ; void funktionl() const I cout « "Bas i sklasse ; ; funktionl()\n" ;
c l ass abgel e itete Klasse ; public Basisklass e pub 1 i c ; virtual void funktionl() const I cout« "Abgelei t e t;; funktionl\n ":
419
4
I
Objektorien tierte Programmierung
Nur weil Sie hier die Methode der abgeleiteten Klasse als virtuell deklariert haben, bedeutet dies nicht. dass die Basisklasse automatisch vinuell ist.
[»]
Hinweis Bitte beachten Sie, dass eine Redefinition kein Polymorphismus ist. Erst durch das Schlüsselwort vi rtual zeigt man an, dass die Entscheidung über die aufgerufene Methode zur l aufzeit auf Basis des aktuellen Objekts erfolgt. Wenn Sie das Schlüsselwort vi rtua 1 weglassen, wird aus Polymorphismus wieder eine einfache Redefinition.
[»1
Hinweis Außerdem sollte man nicht den Fehler machen, Redefinition und Überladung in einen Topf zu werfen, auch wenn beide Verfahren Ähnlich keiten aufweisen . Bei einer Überladung erzeugen Sie mehrere Methoden mit dem gleichen Namen, abe r mit einer unterschiedlichen Signatur. Bei einer Redefinition erzeugen Sie in der abgleiteten Klasse eine Methode mit gleichem Namen wie die Methode in der Basisklasse und mit der gleichen Signatur. Signatur
Dennoch wird nicht einfach vererbt. wenn etwas nicht zusammenpasst. Es genügt schließlich nicht, eine Methode in der Basisklasse als virtu ell zu kennzeichnen und eine weitere Methode in der abgeleiteten Klasse mi t demselben Namen zu deklarieren . Die redefinierte Methode in der abgeleiteten Klasse benötigt neben dem gleichen Namen auch dieselbe Signatur (Parameter) und auch den gleichen Rückgabewert wie die gleichnamige Methode der Basisklasse. Besitzt diese redefinierte virtuell e Methode eine andere Signatur, so wird lediglich eine weitere Methode mit gleichem Namen erzeugt, die allerdings nicht virtuell ist - es ist also kein Fehler, wenn eine Methode virtuell ist und in der abgeleiteten Klasse mit einer anderen Signatur redefiniert ist. Hierzu ein BeispieL das den hier beschriebenen Vorgang nochmals demonstriert: 11 virt ua 12 . cpp llinclude
class Basisklasse I pub 1 i c : void funktionl!) const I (out« "Basisklasse ::f unk t ionl{)\n ' ; vi rtual void funkt ion2() const I (out « "Basis klasse :: f unk t io n2{ )\n " ;
420
Polymorphismus
I
4.8
virtual vofd funktion3() const { cout « "Basfsklasse: :funktfon3()\n": }
I
I,
cl ass abgeleiteteKlasse : public Basisklasse ( pub 1i c : waid funktianl() canst cout« "A bgeleitet :: funktionl\n" : 11 Vi rtue11 vaid funktion2() const ( cout « "Abgeleitet : : funktion2\n "; 11 Nicht vi rt ll ell , da andere Si gnatur , wei 1 Parameter void funktfon3( double dwert-O.O ) cons t { cou t « " (" « dwert « ") Abgele1tet::funktion2\n";
I,
int main( woid ) ( abgeleiteteKl asse abgelObjekt : Bas i sklasse ~ basisPtr ; bas i sPt r - & abgelObjekt ; 11 Zugrif f über den Basis klassenze i ger bas i sPtr-)funktionl() ; 11 Basisklasse bas i sPtr-)funktion2() ; 11 Abgeleitete Klasse bas i sPtr -)funktion3() ; 11 Ba sisklasse 11 Zugrif f über das Objekt der Klasse abgeleiteteKlasse abgelObjekt , f ll nkt i onl() : abgelObjekt , f unkt i on2() : abgelObjekt , f unkt i on3() : 11 Zugriff Ober abgleitetes Objekt auf die Basisklasse abge 10bjekt .Bas i skl asse: : funkti on3( ) : re t urn 0:
Das Programm bei der Ausführung:
Bas i skl asse :: funkt i onl() Abgeleitet : : funkt i on2 Bas i sklasse : : funkt i on3() Abgeleitet : : funkt i onl
42'
4
I
Objektorien tiert e Programm ierung
Abgeleitet : : fu nkt i on2 (0) Abg elei tet : : f unktio n2 Bas i skl asse : : funkt i on3() Rückgabewert
Da erst zur La ufzeit aufkommt, welche virtu elle Methode ausgeführt wi rd , muss auch der Rückgabewert der verschiedenen virtuellen Methoden gleich sein . Eine Ausnahme gibt es, und zwar wenn eine virtuelle Funktion einen Zeiger bzw. eine Referenz auf die Basisklasse selbst zurückliefert, dann darf auch die neu definierte virtuelle Methode einen Zeiger bzw. eine Referenz auf eine von der Basisklasse abgeleitete Klasse zurückgeben. Verwenden Sie dann eine virtuelle Funktion über de n Basisklassenzeiger oder über eine Referenz auf die Bas isklasse, wird der Rückgabewert implizit in den Typ umgewandelt, den die Methode der Basisklasse hat. Das hört sich schlimmer an, als es ist, daher hierzu wieder ein Beispiel: 11 vir t ua13 .cpp llinclude
class Basis klasse I pu bl i C: v1rtual Bas1sklasse& funkt1 onl () I cout « "Bas i sk l asse ::f unk t io nl ( )\n" ; retu rn *t his : I,
c l ass abge l eiteteKlasse publiC Basisklasse I pu bl ; C: 11 vi r t uell abgele1teteKla ss e& fu nkt1onl () cout « "ab gelei t e t eKl ass e ; : funk t ionl{ )\ n" ; re t urn *th is : I,
int main( void ) I abgeleite t eKl asse abgelObjek t : Basisklasse* basis Ptr : basisP t r a& abgelObjek t : 11 Ze i ger bas1sPtr -)f unktl on l();
422
11
abgele i te t eKla sse
Polymorphismus
I
4.8
11 Re f erenz
Basisklasse& bas i sRef - abgelObjekt : ba s l sRe f.funktlonl (); // abgeleiteteKlasse
I
11 normal es Objekt
Basisklasse bas isVar - abgelObjekt ; basl sV ar .funktlonl (); /I Basisklasse return 0: Das Programm bei der Ausftihrung: abgel ei teteKl asse : : funkt i onl( ) abgel ei teteKl asse : : funk t i onl( ) Bas i sklasse : : funkt i onl() Zug riffs rec hte Natürlich kann man die virtuellen Methoden, wie andere Methoden auch, in pub 1i c, protected oder pr i va te· Bereiche aufteilen. Es gibt jedoch ei ne Besonderheit der virtuellen Methoden. Deklarieren Sie eine virtuelle Funktion in der abgeleileten Klasse als pr i vate , so ist diese Methode (wie sonst auch üblich) nicht über ein Objek t des entsprechenden Typs ansprechbar. Ober den Zugriff eines Basisklassenzeigers oder einer Referenz auf die Basisklasse. können Sie dennoch auf die private vi rtuelle Methode der Basisklasse zugreifen . Damit erzwingen Sie den Zugriff auf die private virtuell e Methode der abgeleiteten Klasse über einen Zeiger oder eine Referenz auf die Basisklasse. Hierbei stellt die Basisklasse die »offene« Schnittstelle für die private Methode der abgele iteten Klasse dar. Das Beispiel hierzu: /I vir t ua14.cpp llinclude (iostream) us i ng namespace std :
class Basisklasse I publ i C : vir tual void funk ti on}() const { cout (e "B asisklasse: : funktionl()\n"; I,
class abgeleiteteKlasse : public Bas i sklasse I priv ate: /I vir tue ll void f unktionl() const I
423
4
I
Objektorien tierte Programm ierung
cout
« "abgeleiteteKlasse : : funk tionl()\n" ;
I,
i nt main( ~oid ) I abgeleite t eKlasse abgelObjekt : Basisklasse~ basisPtr : bas i sPt r - & abge l0bjekt : 11 Zeige r bas i SPt r - )funkt i onl() ; 11 Referenz Bas i sklasse& ba sisRe f - abgel 0bjekt ; bas i sRef . funktion! () :
11 !!! Nicht mögl i ch , da pri~ate ! !! 1/ abgelObje kt . f unktion!() ;
return 0: Das Programm bei der Ausführung:
abgel ei teteK l asse : : funkt i onl () abgel ei teteK l asse : : funkt i onl () 4.8.4
Arbeitsweise von virtuellen Methoden
Bisher haben Sie die vi rtuellen Methoden ein fach eingesetzt. Man möchte aber auch ein wenig hinter die Kulissen von virtuellen Methoden schauen, um zu sehen. was bei einer solchen dy namischen Bindung intern abläuft. Wer viel mit C programmiert hat, wird es schon erahnen, dass hierbei Funktionszeiger am Werke si nd. Hierzu legt der Compiler fur jede Klasse, die mindestens eine virtuelle Methode besitzt, eine vi rtuelle Methodentabelle an (oder kurz VMT für Virtual Meth od rahle). Diese Tabelle ist im Grunde nur ein Array von Funktionszeigern, die die virtuellen Method en einer entsprechenden Klasse adressieren. Jede virtuelle Methoden erhält in dieser Tabelle einen Eintrag mit ihrer Adresse. Jede dieser virtuellen Methode in den Tabellen von Basis- und abgelei tet en Klassen besitzt dabei immer den gleichen Index. Um auf diese virtuelle Methodentabelle zuzugreifen, wird außerdem noch ei n Zeiger benötigt, ein sogenannter virtueller Methodenzeiger (VMT Zeiger) . Ein solcher Zeiger wird ebenfalls intern vom Compiler erzeugt und für jedes Objekt
424
Polymorphismus
I
4.8
zur Verfügu ng gestellt. Mit diesem versteckten Zeiger wird über die Verwendung eines Objekts auf die virtuelle Methode der entsprechen den Klasse zugegriffen. Für die dynamische Bindung wird zunächst im referenzierten Objekt der virtuelle Zeiger auf die virtuelle Tabelle (entsprechend der Klasse) ausgewertet und anschließen d wird aus der Tabelle die Adresse der virtuellen Methode verwen· det. Hierzu sollen nochmals unsere Klassen "Gegenstand .. und »Buch .. zum Einsatz kommen , die die virtuelle Methode ~printO" verwenden. Im Beispiel wurde außerdem in der Klasse ..Gegenstand" eine wei tere virtuelle Methode »inputO" hinzugefugt. die aber nicht in der abgeleiteten Klasse "Such .. redel'niert wird. Hier zunächs t die neue virtuelle Methode »inputO" der Klasse .. Gegenstand,,: 11 gegensta nd.h llinclude llinclude /}i f ndef _GEGENSTANO_H_ lide fine _GEGENSTANO_H_ us i ng namespace s t d ;
class Gegenstand priva t e : /I alles wie gehabt
pub 1 i c : /I alles wie gehabt
v1rtual vo1d 1nput () " . c1n.get1 1n e(beze1ch nung. 50) : cou t « "Art1 kel " . 0' 0 » anzahl: cou t « "Anzahl " . 0'0 » nummer: cou t « " Nummer " . 0' 0 » pre1s: cou t « "Pre1s 11 E1ngabepuffer leeren c1n.sy nc(): )
I,
lIendi f
Jetzt die Hauptfunktion: /I mai n.ep p flinel ude "gegenstand . h" llinelude "buch . h"
425
I
4
I
Objektorien tierte Programm ierung
int main( void ) I 11 Basisklassenzeige r Gegenstand~ gegenstandPtr : Gegenstand gegenstandl("Roman" . 100 . 124 . 0) . gegenstand2 : Buch buch!{ " IT - Fachbuch " . 100 . 123 . O. "e++ von Ab i s Z". " J . Wolf" . 1000 ) :
11 gegenstandl ausgeben mit Gegenstand : : print gegenstandPtr - & gegenstandl : gegenstandPtr - )p r int() : 11 gegenstand2 einlesen mit Gegenstand : : i nput gegenstandPtr - & gegenstand2 : gegenstandPtr-)input() ; 11 buchl ausgeben mit Buch : : print gegenstandPtr ~& buch! : gegenstandPtr-)pr i nt {) : cout«'\n' :
11 buchl ei nlesen mi t (!) Gegenstand :: input gegenstandPtr - )input() ; cout«'\n' : 11 ve r ande rt es bucht ausgeben mit Buch : : print gegenstandPtr - )pr i nt() ; return 0 :
Das Programm bei der Ausführung: Art i kel Anzahl Nummer Pre ; s Art ; kel Anzahl Nummer Pre ; s Bucht i tel Autor Seiten Ar ti kel
426
Roman 100
12' 0
Spielzeug
1 125 100 C++
,oe Abis Z
J . Wolf
1000 IT - Fachbu ch
Polymorphismus
Anzahl Nummer Pre i s
100 123 0
Ar ti kel Anzahl Nummer Pre i s
IT-Fachbuch (e++) 100 123
Buchtitel Autor Seiten Art i kel Anzahl Nummer Prei s
C++ '00 Abis Z J . Wolf
I
4.8
I
50
1000 IT-Fachbuch (e++> 100 123 50
Zu diesem Programmablauf soll die folgende Abbildung 4.6 den Vorgang zwischen den virtuellen Zeigern und Tabellen demonstrieren. In der Abbildung 4.6 können Sie jeweils die virtuelle Tabelle der Klasse . Gegenund . Bu ch ~ erkennen. Das Objekt _gege nstandh ruft (über einen Basisklassenzeiger) die virtuelle Melhode .print" aus der virtuellen Methodentabelle der Klasse .. Gegenstand" (Gegenstand : : pri nt) auf. Dasselbe gilt für das Objekt "gegenstand2 ~ , nur dass hierbei die virtuelle Methode . inputO" der Klasse »Gegenstand« (Gegens tand : : input) aufgerufen wird. Das Objekt . buch1« hingegen ruft die virtuelle Melhode »printO" der Klasse .. Buch .. (Buch ; ; pr; nt) aus der virtuellen Methodentabelle auf. Da es fur die Klasse _Buch _ keine eigene virtuelle input-Methode gibt, diese aber weitervererbt wurde, befindet sich in der virtuell en Methodentabelle der Klasse ,.Buch .. ebenfalls ein Eintrag für die Methode . inputO" der Klasse ,.Gegenstand_ (Gegenstand : ; ; nput) . stand ~
Bei beiden Klassen, . Gegenstand" und ,.Buch,,; sind zwei Methoden in der virtuellen Tabelle eingelragen, wobei die Einträge für die Methode ,.inputO_ dieselben sind. Nac hteile der dynami schen Bi ndung Natürlich gibt es in gewisser Hinsicht auch »Nachteil e" der dynamischen Bindung gegenüber der statischen Bindung. Zum einen kann sich die Laufzeit der Anwendung verschlechtern, weil statt einer direkten Adressierung der Methoden im Masch inencode zwei Zeiger dereferenziert werden müssen. Auf der anderen Seite ist die Dereferenzierung von zwei Zeigern immer noch schneller als die Verwendung der $W; tch-Statements und mehrerer case-Fallunterscheidungen .
427
4
ObJektofientlerte Programmierung
gegenstand1 Virlueller Zeiger
G.!l.n.tand bezeiclmong anzahl nummer preis
\
1\
Virtuelle Methodentebelle von der Klasse a.g.natand Funktionsze;ger Gegenstand: prinl FunkliO!lsze;ger Gegenstand::;nput
gegenstand2 Virlueller Zeiger
G.qa".tand bezeichnung anzahl nI.mmer preis
b
~
"' VirtuellElf Zeig&r
Oag .n. tand bezeichnung anzahl nummer preis
\
1\
Virtuelle Methoden1abe11e von der Klasse Bucb Funkhonsze;ger BlJCh :p!im Funktionszeiger Gegensland::input
Buch titel autor seiten
Abblldun g 4.6
Virtuelle Methodenzeiger und ,abellen
Einzig den etwas höhere n Speicherbedarf der Anwendung kann man nicht widerlegen. Dieser erhöhte Bedarf ergibt sich dadurch, dass die virtuellen Methodentabelten und die Zeiger ftir jedes einzelne Objekt Platz benötigen. Aber wenn man Anwendungen schreiben muss, die sehr schlan k sein sollen, ist CH sowieso nicht unbed ingt die erste Wahl. und man kann dann auch auf C zurückgreifen.
428
Polymorphismus
4.8 .5
I
4.8
Virtuelle Destruktoren bzw . Destruktoren abgeleiteter Klassen
Es wurde bereits erwähn t, wie der Vorgang bei den Destruktoren bei abgeleiteten Klassen vor sich gehl. Wird ein Objekt erzeugt, bei dem mehrere Konstruktoren aufgerufen wurden, so werden die Destruktoren in der umgekehrten Reihenfo lge ausgeführt. Bei Objekten abgeleiteter Klassen bedeutet dies, dass immer zue rst der Destruktor rur die abgeleitete Klasse aufgerufen wird und anschließend der Destruktor fü r die Basisklasse. Im Grunde müssen Sie hierbei fast nich ts machen, da der Compiler Ihnen diese Arbeit häufig abnimmt. Aber auch hier entstehen Probleme. wenn in der abgeleiteten Klasse ein Objekt dynamisch erzeugt wird. Hierzu sei fo lgendes Beispiel gegeben: 11 virtua15 . cPP lIinclude lIinclude us i ng namespace std :
class Basis kl ass e I pub li c : Basis klasse{) { cou t « "Konstruktor Basis kl asse \n " : - Bas i sklasse() I cou t « "Destru ktor Basiskl asse\n ": I,
cla ss abgel e iteteK lasse priva te ; char *Zkette :
public Basisklasse I
pub l i C: abgeleitete Klasse( const char ~ s t r - "" ) I cout « "Konstrukto r abge l eiteteKlasse\n ": Zkette - new char [strlen(str)~l] : strcpy( Zke tt e . str ) : -abgele i tet eK l as se() I cou t « "Destruktor abgeleiteteKlasse\n ": delete [J Zkette : I,
429
I
4
I
Objektorien tiert e Programm ierung
i nt main( vo i d ) [ Basiskl asse ' basis Pt r : basisPt r - new ab gelei t eteKlasse(~Ze i ehenkette") delete bas is Ptr : return 0:
:
Das Prog ramm bei der Ausführung:
Kon st ruk t or Bas i sklasse Konst ruk t or abgelei tete Kl ass e Destruktor Bas isklasse Hier fallt gleich auf. dass der Destruktor fü r die abgeleitete Klasse nicht aufgerufen wird. Das Problem ist, dass der Compiler das durch "basisPtr.. adressiene Objekt nicht kennt, weil "basisPtr .. ein Zeiger der Basisklasse ist. Daher wird auch der Destruktor fur die Basisklasse aufgerufen. Hätten Sie außerdem in diesem Beispiel keinen Destruktor der Klasse "abgeleiteteKlasse .. definien, so hätte dies keine problematischen Folgen , weil dann der Destruktor der Klasse "abgeleiteteKlasse« keine Arbei t ausfühn. Aber hier wurde der Destru ktor definiert und muss daher explizit aufgeru fen werden. Das Problem lässt sich ganz einfach dadurch lösen, dass man auch die Des truktoren virtuell deklariert. Im Beispiel müssen Sie daher nur den Destruktor der Basisklasse als virtuell dekla rieren und schon ist auch der Destruktor der abgeleiteten Klasse implizit vi rtuell.
class Basis klasse [ pub li c : Basis klasse{ ) [ cout « "Konstru ktor 8asis klasse\ n" ; vlrtual -Basi sk l asse () cou t « "Des tr uktor 8asiskla sse \n ": I,
elass abgeleitete KJasse : DubJic ßasisklasse I priva t e : char ' Zkette : pub 1i c : abgeleiteteKlasse( eonst char *st r - ". ) [ cout « "Konstruktor abgeleiteteK l asse\n ": Zket t e - new char (st r len{str)+l) :
430
Polymorphismus
I
4.8
strcpy( Zkette. str ) ; 11 Jetzt . implizit vir t uell -abgel eiteteKlasse() I cou t « "Destruktor abgeleiteteKlasse\n ": delete [) Zkette :
I
Nur das schlüsselwort vi rtua 1 in de r Basisklasse sorgt daftir. dass, wie bei den Methoden , jetzt auch der richtige Destru ktor aufgerufen wird. So sieh t die Ausführung des Programms »virtuaI5.cpp« nach dieser kleinen Änderung wie folgt aus:
Konstr ukto r Basisklasse Konstruktor abgeleiteteKlasse Des t r uktor abgeleiteteKlasse Destruktor Basiskl asse Hinweis Folgendes sollten Sie sich daher zur Regel machen . Wenn eine Basisklasse für andere Klassen dienen soll, sollte diese immer einen virtuellen Destrukto r haben. Selbst wenn die Basisklasse keinen eigenen Destruktor benötigt, sollten Sie zumindest einen virtuellen Dummy-Destruktor deklarieren. Virtuelle Ko nstruktoren ?
Im Gegensatz zu Destruktoren können Konstruktoren niemals virtuell sein. Ein Konstru ktor dient schließlich dazu, ein Objekt mit einem bestimmten Typ zu initialisieren. Der Typ ist dabei immer bekannt und daher statisch. Da die Objekte nur auf Veranlassung des Programms erzeugt werden. wird dabei auch der exakte Typ angegeben. 4 .8 .6
Polymorphismus und der Zuweisungsoperator
Wenn Sie einen Zuweisungsoperator als virtuell definieren wollen, sollten Sie vorsichtig sein, weil eine solche Zuweisun g ni cht immer polymorph ist. Dies ist schon deswegen nicht immer möglich, weil poly morphe Fun ktionen ja immer dieselbe Signatur (Argumenttyp und Rückgabewert) haben müssen. Ein Beispiel, was hiermit gemeint ist: 11 vi rtua16 . cpp Ifi nclude Ihnclude using namespace std :
431
[« )
4
I
Objektorien tiert e Programm ierung
cl ass Basisklasse I pub l i c : virtual Bas i s kla sse &operat or-( const retu rn *t his :
B~s1skl~sse
&bJ I
I,
publiC Basisklasse I cl ass abgeleiteteKlasse pub 1i C: i nt iwe rt; abgeleitete Kl asse() : iwer UOJ \ I abg e leitet e Kl asse &ope rat or- (cons t abgeiefteteKlasse &a) \ iwe r t - a . iwert : re t urn *this : I,
int main( void ) I Basisklasse *bas i sPtr ; abgeleiteteKlasse aObjektl , aObjekt2 : aObjektl , i wert - 1111 ; basisPtr =&aObjekt2 : II aObjekt2 mit dem Wert von aO bjektl belegen *bas isPtr - aObjektl ; II Ausgabe nicht wie erwar t et!!! cou t « aObjekt2 , i wer t « ' \n '; ret ur n 0: Anstau der Ausgabe des Werts ,.11 11 «, wie hier beabsichtigten, wird 0 ausgegeben. Das Problem Hegt darin, dass der Zuweisungsoperator in der abgeleiteten Klasse einen anderen Typ hat als der ZuweisungsoperalOr in der Basisklasse. Und daher stim mt die Signatu r der beiden Zuweisungsoperator-Methoden nicht oberein, und sie sind somit ohne jeden Bezug zueinander. Sie haben zwar mit ,.Basisklasse ::operator=(const Basisklasse&)« eine vinuelle Methode deflnien, aber diese wird in der Methode ,.abgeleiteteKlasse" nicht redefiniert. Würde die Methode in de r abgeleiteten Klasse folgenden Funktionskopf haben
abgeleiteteKlasse &o perator- (const Bas i sklasse &a ) dann hätten Sie einen echten polymorphen Zuweisu ngsoperator (ob dieser hier Si nn macht, sei dahin gestellt).
432
Polymorphismus
4 .8 .7
I
4.8
Rein virtuelle Methoden und abstrakte Basisklassen
Damit vinuell e Methoden über die Basisklassen-Schniustelle auch in der abgeleiteten Klasse zur VerfO.gung stehen, müsse n diese stets in der Basisklasse deklariert sein. Auch wenn in der Bas isklasse keine Aufgabe für d iese virtuelle Methode vorhanden ist, ist eine Deklaration einer leeren Du mmy-Methode notwendig, was zwar Speicherplatz verbraucht. abe r die einzige Möglichkeit ist, um die vinuellen Methoden in der abgeleiteten Klasse zu nutzten . Vinuelle Methoden verwende n zude m auch Speicherplatz in der vi n ue llen Tabell e. In C++ haben Sie die Möglichkeit, eine Methode als eine ,.rei n vinuell e« Methode zu d eklarieren. Eine solche Deklaration sieht wie fo lgt aus: cl ass Basisklasse 1 public : v1 r tual vo1d funkt1onO - 0 ;
I,
Durch die Deklaration der Methode und das Anhängen von - 0 erhält der Compiler die Anweisung, dass in di eser Klasse keine Definition der Methode vorha nden sein muss. In der virtuellen Tabelle wird daher rur ein Objekt dieser Klasse NUL L eingetragen. _Rein vinueJle« Methoden werden in der Praxis häufig auch als abstrakte Methoden beze ichnet. Solche abstrakten Methoden haben praktisch in de r Klasse, wo diese deklarien werden, keine Bedeutung, sie werden aber in ei ner abgeteileIen Klasse definiert: cl ass Des ktop [ publiC : Desktop() 1) vi rtual -Desktop() I ) vi rtua 1 long gecX() - 0 : vi rtual long gecY() - 0 : vir tual void Ze i chnen() - 0 : I,
In dieser Klasse _Desktop« werden mehrere virtuelle Methoden deklariert. Diese Klasse dient als Basisklasse zur Abteilung anderer Klassen , wie zum Beispiel ein einfaches Fenster mit Rollbalken , das au f dem Desktop angezeigt werden kann oder auch einer Nachrichtenbox. Ein kleiner Window-Manager eben. Abstrakte [
Natürlich sollte auch klar sein, dass von Klassen, die rein vi rtuelle Methoden en thalten, keine Obje kte erzeugt werden können. Das Objekt würde sonst versuchen , eine Methode aufzu rufen, die es gar nicht gibt. Bezoge n auf die Klasse Desktop ist Folgendes nicht möglich :
433
I
4
I
Objektorientierte Programmierung
Deskto p ein_Desktop ; 11 f alsch !! ! Solche Klassen, die mindestens eine rein virtuelle Methode enthalten und von denen kei n Objekt erzeugt werden kann, werden als abstrakte (Basis-)Klasse (oder auch abstrakte Datentypen) bezeichnet.
[»]
Hinweis Solche Klassen, die mindestens eine rein virtuelle Methode enthalten, von denen also kein Objekt erzeugt werd en kann, werden im Gegensatz zu den bisher verwendeten konkreten Klassen als abstrakte (Basis-)Klassen (oder auch abstrakte Datentypen) bezeichnet.
Abstrakte Klassen (bzw. auch abstrakte Datentypen) stellen in C++ immer ein Konzept und kein Objekt dar. Somit ist eine abstrakte Klasse immer die Basisklasse für andere Klassen. Sobald Sie also eine rein virtuelle Methode wie fol gt deklarieren vi rt ual vo i d funk t ion() - 0 :
signalisieren Sie hierm it: ...
Es kann kein Objekt dieser Klasse erzeugt werden.
..
Sie woll en einen abstrakten Typ erstellen , um eine gemeinsame Funktionali tät fü r mehrere (abgleiteIe) Klassen bereitzustellen.
..
Diese Methode soll auf jeden Fall redefiniert werden.
Auch wen n es hierbei kein Objek t einer abstra kten Klasse gibt, können Sie wieder einen entsprechenden Zeiger bzw. eine Referenz definieren und verwenden: Des kto p* deskPtr ; Mi th ilfe dieses Zeigers (bzw. dieser Referenz) kann dann wieder auf Objekte der abgeleiteten Klasse gezeigt werde n. Bevor auch die Erklärungen hier zu abstrakt werden, soll ein Beispiel zur Demonstration erstellt werden. Das Beispiel demonstriert eine geschlossene Hierarchie von Klassen. Zunächst erzeugen wir eine abstrakte Basisklasse . Desktopoc und leiten davon die Klasse ,.Fe nster ~ und ,.Box« ab. Hier nochmals die abstrakte Klasse . Desktop1< : cl ass Desktop ( publ i C:
Desk t op()
(I
v i rtual -Desk top() 11
v1rtual long get_X() - 0; v1rtual long geLY() - 0; v1rtual vofd Zefchnen() - 0; I,
434
Polymorphismus
Alle aus diese r abstrakten Basisklasse abgeleiteten Klassen erben jetzt die rei n virtuellen Methoden in der .. nackten« Form. Wenn Sie nicht wollen , dass die abgeleitete Klasse auch abstrakt ist, müssen Sie al!e drei Methoden redefini eren, um Objekte davon zu erzeugen. Wenn also eine abgeleitete Klasse ,.Fenster" von der Klasse ,.Desktop« erbt und ,.Desktop. wie hier drei rein virtuelle Methoden enthält, müssen Sie in .. Fenster" aller drei Methode redefinieren, oder - Fenster« bleibt ebenfa lls eine abstrakte Klasse . Die Bedeutung des folgenden Beispiels lässt sich schnell erklären. Auf einem ,.Desktop .. , der nicht als Obj ekt instantiiert werden darf, weil er ja schon existiert (hier: auf dem Bildschirm dargestellt wird), soll entweder ein einfaches »Fenster« oder eine ,. Boxo< (Nachrichten box> dargestellt werden. Um zu verhindern, dass nicht doch ein Objekt vom Typ ,.Desktop« erzeugt wird, erstellen wir diese Klasse so. dass sie nur als Schnittstelle für davon abgeleitete Klassen davon. Dies erledigt man mit einer abstrakten Basisklasse. In der abgeleiteten Klasse redefinieren Sie nun die Methoden "gecXO .. , »gecYO" und ,.Zeichnenü«. I! vi rtua 17 . cpp lIinclude /iinclude us i ng name space std ;
cl ass Deskt op ( publ i C: Desk top( ) (1 vi rtual -Desktop () ( 1 v1rtual l ong get_XC) - 0; v1rtual long geCY() - 0; v1rtual vo1d Ze1chnen() - 0:
cl ass Fen ster : publ i c Desktop ( priva t e : 10ng X· 10ng y :
pub l i c : fen~t e r {
l ong xlen , long yJen ) : x(xlen ) , y(ylen) (I
-Fens ter{ ) ( cout « ~ Fe nste r zerstö rt \ n": I l ong get_X() { return x : long get_Y() { return y; } vo1d Zeichnen() { cout « "Fenster (" « x « "-Ir" « y « " ) « "geze1chnet\n":
..
435
I
4.8
I
4
I
Objektorien tierte Programm ierung
cl ass Bo x: public Desktop { private : 10ng pos_x ; 10ng pOSJ ; pub 1i c ; Box( long x , long Y) : pOs_x(x) , POSJ(Y) {I -Box() { co ut « "Nachr i chten -Box zerst ört\n " ; long get_X() { r eturn pos~; } long get_Y() { r eturn posJ: } vold Zeichnen() { cou t « "N ac hr l chten-Box an Pos l tion (x: " « pos_x « "/y: " « pos-y « ") geze i chnet\ n" : I,
i nt main( void } { Desktop* desk Ptr l : Desktop * desk Ptr2 : deskPtrl - new Fenster(800 , 600) ; deskPtr2 - new Box(30 , 30) : deskPtrl -)Zeichnen() ; deskPtr2 -)Zeichnen() ; delete desk Ptrl : dele t e deskPtr2 : re t urn 0; Das Programm bei der Ausführung:
Fenster (800*600) gezeiChnet Nachrichten -Box an Position {x :30Iy :30J gezeichnet Fenste r zerstört Nachric hten -B ox zerstört Zusammengefa sst kann man sagen, dass abstrakte Klasse n eine polymorph e Schnittstelle ftlr abgeleitete Klassen darstellen, Damit lassen sich allgemeine Funktionalitäten zunächst als rein virtuelle Funktionen implementieren, die dann über Ze iger oder Referenzen auf die abstrakte Klasse aufgerufen werden. Wird in der abgeleiteten Klasse eine Methode aus der rein virtuellen Klasse redefinie rt, dann wird diese auch ausgeführt. Konstruktoren abstrakter Klassen
Abstrakte Klassen benötigen immer einen Konstruktor - auch wenn diese selbst kei ne Objekte erzeugen. Aber bei jedem Objekt einer abgeleiteten Klasse wird
436
Polymorphismus
I
4.8
auch der Konstruktor der Basisklasse aufge rufen. der für die lnitialisierung der Basiseigenschaften (falls vorhanden) zuständig ist. Zuweisungsoperator und Kopierkonstruktor abstrakter Klassen
Solange Sie keine dynamischen Elem ente in der abs trakte n Klasse verwenden. müssen Sie nich t explizit einen Kopierkonstruktor oder eine Zuweisung definieren und können die Standard vers ionen verwenden. die au tomatisch zur Verfü gung ges tellt werden. BefLnden sich allerdings dy namische Eigenschaften in der Basisklasse. so müssen Sie einen eigenen Kopierkonstruktor und eine Zuweisung definieren. Es ist nicht imm er nötig. in abgele iteten Klassen einen eigenen Kopierkonstruktor zu definieren. auch hier besteht erst Bedarf. sobald di e abgel eitete Klasse ein dynamisches Element besitzt (siehe auch Abschnitt 4.4.6)
4.8.8
Probleme mit der Vererbung und der dynamic_cast-Operator
Ein Problem. au f das vor allem die noch unerfahrenen Programmierer stoßen könnten. entsteht dann . wenn man eine Methode in der abgeleiteten Klasse hinzufügen möchte. diese aber überhaupt nicht zur Basisklasse passt. Beispielsweise wenn man in der abgeleiteten Klasse "Fenster« vom Usting !lvirtuaI7.cpp« fol gende Methode hinzufügt: elass Fenste r pub1 ie De s kto p I pr i va te : long x ; long y ; publiC: Fenste r { 10ng x l en , 10ng y1en ) ; x(xlen) . y(y l en) I) -Fenster ( } I cout « "Fenste r ze r s t Or t\n " ; I 10ng geLX() I return x : I 10ng geLY{) I return y : I vo i d Zeichnen{) I cout « "Fenster ( " « x « " *" « y « ") " « "geze i chnet\n" ;
vold Neuzelchnen( 10ng xlen, 10ng ylen ) { x-x len; y-ylen; 11 Neu zelchnen Zelchnen(); I,
437
I
4
I
Objektorientie rte Programmie rung
Wenn Sie hi er die Methode ,.Neuzeichnen .. mit dem Basisklassenzeiger venvenden, bekommen Sie eine Fehlermeldung vom Compiler, dass die Methode keine der Basisklasse (hier "Desktop,,) ist. Der Compiler kann also beim Auflösen der virtuellen Tabelle keinen solchen Eintrag finde n. Um es gleich zu sagen, dieses Beispiel entspricht einem schlecht überdachten Entwurf des Programmierers. Denn wenn man schon einen Zeiger auf eine Basisklasse verwenden will, um auf Objekte abgeleiteter Klassen zuzugreifen, will man dies gewöhnlich auch poly morph tun. Wie kann man auf diese Funktion zugreifen , ohne gleich das ganze Programm umzuschreiben? Zwar könnten Sie jetzt diese Methode in die Basisklasse schieben. Wenn Sie allerdings zur Basisklasse scho n eine ganze Sammlung abgeleiteter Klassen hinzugefügt haben, müssen Sie alle anderen Klassen gegebenenfalls an passen und testen. Außerdem wird dadurch die Basisklasse un nötig groß und die Wartung der abgeleiteten Klasse verkompliziert. Ein zweiter Weg wäre, den Zeiger der Basisklasse oder der Refere nz in einen Zeiger oder eine Referenz der abgele iteten Klasse umzuwandeln . Da wir hier eine dynamische Bindung haben, benötigen wir auch eine dy namische Umwandlung. Für diesen Zweck können Sie den dy n ami ccas t<)( )-Operator venvenden (siehe Abschnitt 3.7.2). Damit wird der Basisklassenzeiger ebenfalls erst zur Laufzeit überprüft. Wenn die Umwandlung erfolgreic h war, wird ein ei nwandfreier Zeiger der abgeleiteten Klasse zurückgegeben. Gibt es kein Objekt der abgeleiteten Klasse oder scheitert die Umwan dlung, wird 0 bzw. NUl l zurückgegeben. Soll also die Methode "Neuzeichnen .. in der abgeleiteten Klasse "Fenster.. ausgefuh rt werden, müssen Sie wie folgt vorgehen: i nt mai n ( void ) I
Oesktop* deskPtrl : Fenste r* fens t erP tr; deskPtrl - new Fenste r (800 . 600) : deskPtrl ->Ze i chnen() ; fenste r Pt r - dynamlc_castget_y() «"'n ":
438
Polymorphismus
I
4 .8
delete deskPtrl : re turn 0:
I
Das Programm bei der Ausführung : Fenster (800 *600) gezeichnet Fenster (640 *480) ge ze ichnet Neue Fen s tergr öBe : 640 x4 80 Fens ter zerstört Um an das ,.Fenster«-Objekt heranzukommen und die Methode ,.Neuzeichnen« aufzurufen, erzeugt man zunächst einen ,.Fenster«-Zeiger und nimmt eine Umwandlung mit dem dynamiccast-Operator vor. Ist der Rückgabewen dieser Umwandlung ungleich 0 bzw. ungleich NULL , können Sie auf die Methode ,.Neuzeichnen« zugreifen .
4 . 8 .9
Fallbeispiel: Verkettete Listen
Al les bisher Gelernte lässt sich nun hervorragend an einem Beispiel mit verketteten Liseen demonstrieren. Heute wird zwar keiner mehr ve rkettete Listen selbst implementiere n und auf einen der vielen Bibliotheken (z. B. STL) zurückgreifen. Aber wenn man sich mit C++ ernsthaft befassen muss, sind verkettete Listen pflicht. We r bereits Erfahru ng mit den verketteten Listen in C gemacht hat und meint er könne diesen Teil überfliegen, dem sei gesagt, dass die OOP-Implementierung der verkeu eten Listen nicht mehr viel mi t der prozeduralen Programmierung gemein hat. Wer noch nie etwas von verkettelen Listen gehön hat, bekomrntjeLZl eine kurze Erkläru ng, worum es sich dabei handelt. Nehmen Sie zunächst ein Array. In einem Array können Sie ei ne feste Anzahl von Elementen eines bes timmten Typs speichern. Manchmal ist es aber ein Hindernis, wenn man auf eine feste Größe fixiert ist.
Abbildung 4.7
Interne Ansicht eines Arrays
Auch Arrays lassen sich mit einen gewisse n Aufwand dynamisch implementieren, aber das Kopieren hat bei sehr großem Datenaufkommen (z. B. mehrere GB) wohl wenig Sinn und birgt zu dem einige Risiken. Wie machen das also die gm-
439
4
I
Objektorientierte Programmierung
ßen Datenbanken wie ,.MySQL« und Co .? Die Antwort gleich vorweg. große Datenbanken verwenden Bäume, was abe r auch eine Fo rm von Liste ist. Somit basiert zunächst alles auf der einfachsten Grundlage. den einfach verketteten listen:
f-Daten
f-Oaten
Abbi ldung 4.8
f--
[-
Daten
Daten
Einfach verkettete Listen
Bei den verketteten Listen handelt es sich um eine einfache Datenstruktur mit Elementen beliebigen Typs, die aneinander gehängt werden. Dadurch soll ge mäß OOP - eine Klasse erstell t werde n. die das Objekt der Daten darstellt und gleichzeitig auch auf das nächste Objekt vom seiben Typ zeigt. Sie erzeugen praktisch mit jeder Instanz ein neues Objekt und hängen d ieses in einer Liste an das andere. bei Bedarf auch sortiert. Bei einer einfach verketteten Liste beginnt man am Anfang der Liste und sucht nach einem bestimmten Knoten, wo das Element ei ngefügt werden soll. Dabe i durchläuft man die Liste Knoten für Knoten bis zum letzten Knoten. Neben einfach verketleten Listen gib t es natürlich noch weilere und komplexere Listenstrukturen, wobei die doppelt verketteten Listen und die binären Bäume ebenfalls zu den grundl ege nd en Datenstruktu ren gehören (Siehe Abbildung 4.9 und 4.10).
- --- -- -Daten
Abbi ldung 4.9
Abbildung 4.10
440
Oaten
Daten
Daten
I-
Doppelt verkettete Li sten
Binare Baume
-.
f--
Polymorphismus
I
4.8
Die einzelnen Klassen (I
Unabhängig von der verketteten liste benötigen Sie zu nächst ein e Klasse, in der Sie die Daten speichern wollen, die anschließend in der Kette aneinander gereiht werden sollen. In di esem Beispiel begnügen wir uns mit einer einfachen Klasse, die lediglich eine Eigenschaft "iwert« besitzt, die diese Klasse aufnehmen kann. Diese Klasse bekommt außerdem zwei Methoden - eine, mit der zwei Objekte von der Klasse .. Daten« miteinander verglichen we rden können, und eine Methode zum Ausgeben dieser Daten auf dem Bildschirm. Hier die kompleue Klasse: 11 da t e n.h I/ i nclude using names pace std : lI i fndef _DATEN_H_ IIde f i ne _DATEN_H_
cl ass Daten I priva t e : i nt iwe rt: pub l ; C : 11 Konst ru ktor Da t en( int iVa l ) : iwe r t(iVa l ) I 11 Zu Debug - bzw . Verstandniszwec ken ggf . entfernen cout « "Obje kt [Daten] erzeugt\n " : I 11 Des t rukto r
-Daten( ) 11 int verglei che n( const Da t en& ) : vo i d anzeig e n() const ; I, 11 Zum Vergleichen der Ei genschaften zweie r Objekte
int Daten : : vergleichen( const Daten& d ) 11 GrOBer if( i wert ) d . iwert retu r n 1: I : 11 Kleine r i f( iwe rt < d . iwert re tu rn - 1 ; I ; 11 Glei ch re t urn 0;
voi d Daten :: a nzei gen () cons t cout « iwert « . \n ': lIendi f
441
I
4
I
Objektorientierte Programmierung
Durch eine zusätzliche Klasse für die Daten können Sie schon erkennen, dass hier nicht die Daten verkettet werden, sondern die gleich erzeugten Knoten. In der prozeduralen Programmierung sind dies gewöhnlich die Daten, die aneinander gehangt werden. Für den oder die Knoten könnten wir je eine Klasse für den Anfang der Liste und einen für das Ende der Liste schreiben. Natürlich benötigen Sie auch einen Knoten, der die Daten verwaltet und behandelt. Damit wi r nicht alles in dreifacher Ausführung schreiben müssen, verwenden wir als Basisklasse einen abstrakten (rein virtuellen) Ty p als Knoten. Somit haben Sie zunächst vier Klassen für den Knoten, eine abstrakte und drei abgeleitete Klassen:
.. Knoten - Die abstrakte Basisklasse mit den Methoden »einfuegenO .. und »anzeigenO", die von alle n abgeleiteten Klassen ebenfalls redeflniert werden müssen.
.. AnfangsKnoun - Dieser Knoten nimmt keine Daten auf, sondern sorgt dafür, dass alle Daten, die kommen, nach diesem Knoten eingefugt werden. Vor diesem Knoten kommt nichts.
.. AllgemeinerKnoten - Der Knoten nimmt die eigentlichen Daten auf und fügt diese in die Kette an einer en tsprechenden Stelle ein. Hier ist auch die KernMethode "einfuegenO.. redefiniert, die ermittelt, wo dieser Knoten eingefügt wird.
.. EndKnoren - Der Knoten sorgt dafür, dass keine Daten an ih m vo rbeikommen und dient als Ende-Markierung. Kein AllgemeinerKnoun wi rd hinler diesem Knoten eingefügt. Somit ist die Klasse »Anfangs Knoten" und "EndKnoten« ein fester Knoten, der sich, einmal erzeugt, nicht mehr verändert. Zwischen diesen beiden Knoten wird ein Objekt der Klasse ,.AllgemeinerKnoten" sortiert eingefügt - und "AllgemeinerKnoten .. verwahet auch die Klasse »Daten« . Da di e Klasse ,. Knoten« selbst nur die abstrakte Basisklasse darstellt, soll diese hier vorgestellt werden. 11 Knoten i s t die abstrakte Basisklasse 11 Alle abgel e iL e Len Kla-s-sen müssen " ei n fuegen" 11 und "an zeigen" redefi n i e ren
cl ass Knoten I pub 1 i c : Knoten() I ) yi rtual - Knoten () { ) yi rtual Knoten* e i nfuegen{ Daten" d ) - 0 ; yi rtual yoid anze i gen() - 0 ; I,
442
Polymorphismus
Dem Ganzen setzen wir noch eine ,.Maske" auf und schreiben eine weitere Klasse ,. Liste". die die ganze Knoten·Geschichte vor dem Anwender dieser Klasse verbirgt. Natürlich benötigt diese Klasse mindestens ein Knoten-Element und die Melhoden . anzeigen()~ und ,.einfuegenO«, die allerdings nichts mit den gleichnamigen Methoden der Knoten-Klassen zu tun haben. Dennoch werden über das Knoten-Elemen t der Klasse ,.Liste« durch das Aufrufen der Methoden ,.einfuegenO ~ bzw. "anze igenO~ tatsächlich die eigemlichen Knoten-Methoden aufgerufen. Hier also zunächst die Klasse ,.Liste«:
Unabhangige Klasse Liste 11 cl ass Liste [ private : Anfa ngsKnoten *an f an g: publiC : 11 Bei Anlegen gle i ch e in Objekt "Anfangs kn ote n" erzeugen 11 der Ko nst r uktor von "Anf an gs Kno t en " erze ugt wiederum 11 ein Objekt "EndKnoten " . auf das dieser gleiCh zeigt . Liste() [ anfang " new AnfangsKnoten : I -L i ste() [ de l ete anfang : I 11 An die Methode ei nfuegen() von Knoten weiterle it en vo id ein f uegen { Daten · d ) I an f ang-)ei nfu egen(d) ; An die Met hode anze i gen() von Knot en weite r leiten voi d alles_anzeigen() ! anfang->anz eigen() ;
11
I,
Im Hauptprogramm werden Sie ausschließlich mit Objekten vom Typ ,.Liste" zu tun haben . Wenn Sie ein Objekt davon anlegen. erzeugt der Konstruktor auch ein Objekt vom Typ "AnfangsKno ten ~ (beim Beenden wird es vom Destruktor auch wieder abgebaut) . Um den Vorgang besser zu verstehen, wollen wir einen Tes tlauf machen . Daher zunächst die Hauptfunktion: 11 main . cpp lfinclude "d aten. h" lIinclude "lliste . h"
int main( void ) { Lis t e elemente : Daten ~ da te n; i nt i wert ;
443
I
4.8
I
4
I
Objektorien tierte Programm ierung
fore ; ; 1 f cout « "Wert eingeben (O - Ende) : " . 11 Falsc he Eingabe oder 0 i f( ( l( c;n» i we rtll 11 ;wert ~O ) break ; 11 En de da t en - new Daten(iwert) ; elemente . einf uegen(da t enl ; el emente.a l l es_anz ei gen ( ) ; re turn 0: Sie sehen hier keine Spur von den einzelnen Knoten. Sie finden nur die Klassen "Daten« und "Liste« wieder. Wenn also ein Objekt vom Typ "Liste'" erzeugt wurde. wird durch den Konstruktor ein Objekt vom Ty p "AnfangsKnoten« erzeugt. Hierzu die abgeleitete Klasse "Anfan gs Knoten«: 11 ---.-- Abgeleitete Klasse AnfangsKnoten -----_._----_._11 Knoten . der "nur " imme r auf das erste Element 11 der Liste zeig t
class An fang sKnoten : public Knoten priva t e : /I ... zeigt imme r auf das erste Eleme nt Knoten '" next : pub 1i C: 11 Kons truktor Anfangs Knoten( 1 11 ... gleich auch ei nen Endknoten erzeugen next - new End Knoten ; 11 Zu Debug - bzw . Ve rstandniszwecken ggf . entfernen cou t « "Objekt [An f angs Knote n] erzeugt\n· ; -Anfa ngsKnoten ( ) {I 11 Impli zit virtual Knoten* einf ueg en( Daten * d ) ; 11 Implizit virtual void anzeigen{) : I,
Knoten ' An f angsKno t en:: e i nf uegen{ Da ten * d 1 I 11 Am An f ang kommen keine Daten rein . daher an den 11 nachsten Knoten we i terreichen next - next ->einfuegen(dl : return thi s :
444
Polymo rphismus
void Anfan gs Knoten : : anzei gen() next-)a nzeigen() : Die Klasse »Anfan gsKnoten .. ruft zunächst auch wieder nur den Konstruktor auf. Der Konstruktor erzeugt hier ein Objekt vom Typ »EndKnoten«. auf den der next-Zeiger zunächst verweist. Somit zeigt das Objekt vom Typ ,.AnfangsKnoten« zum Start auf einen Typ vom Objekt ,. End Knoten«. Hier die Klasse ,.EndKnoten .. : Abgeleitete Klasse EndKnoten ------------Der EndKnoten di ent als End punkt der Liste class End Knoten : publ i c Knoten I pub 1i c : EndKnoten () 1 11 Zu Debu g' bzw . Ve rstandniszwecken ggf . entfernen cout « "Objekt (EndKnote n] erzeugt\n" ; 11 - --
11
-EndKnoten() I1 11 Impli z it vir tual Knoten * einf uegen( Daten * d ) : 11 Impli z it virtual vo i d anzeigen() I I: I, 11 Daten werden immer vor dem Ende ei ngefQgt
Knoten * EndKnoten : : ein f uegen( Daten* d ) I Al l gemeinerK noten * daten- new AllgemeinerKnoten(d . th i s) ; return date n; Den momen tanen Programmzustand kann man sich »bildlich .. folgenderm aßen vorstellen :
-, Ltn .
"a nfang
I Objekt Anhn!/. .:not.n
'ne"t
Abbildung 4.11
",,'"
/
.nd l<not .n
Momen tan er Program mzustand
445
I
4.8
I
4
I
Obje ktorien tie rt e Programm ie rung
Bis jetzt haben Sie als Benutzer noch keinen Wert eingegeben. Jetzt aber werden Sie dazu aufgeforden (wenn Sie das Hauptprogramm ausfUhren):
Objekt [EndKnoten] erzeugt Objekt [AnfangsKnoten] er ze ug t Wert einge ben (O- Ende) : 123 Objekt [Daten] erzeugt Objekt [AllgemeinerKnoten] erzeugt We rt eingeben (O- Ende) : Es wurde also der Wert _123 .. eingegeben. Kurz darauf werden die Daten und ein allgemeiner Knoten erzeugt und das neue El ement zur Liste hinzugefUgl. Das EinfUgen soll jetzt etwas genauer erläutert werden. Das Einfügen wird in der Hauptfun ktion wie fo lgt eingeleitet:
l i ste elemente ; Da ten · daten ; dat en - new Date n(123) ; elemente . einfuegen(daten) ; Es wird also zunächst die Methode .. Liste: :einfuegenO .. mi t dem Daten-Objekt aufgerufen. Lish/: :ain!ueg9l1 ()
Abbildung 4.12
Aufruf der ersten Methode aus dem Hauptprogramm
Diese Methode leitet die Verantwortung an den _AnfangsKnoten« weiter bzw. an die Methode "AnfangsKnoten::einfuegenO«. Auch der "AnfangsKnoten« gibt seine Arbeit an den Knoten weiter, auf den der Zeiger _next.. zeigt (was am Anfang noch der »EndKnoten« ist, auf den Sie beim Erzeugen eines »AnfangsKnotens« verwiesen haben (siehe Abbildung 4.13».
Knoten * AnfangsKnoten :: ein fu egen( Daten ~ d ) 11 Am Anfang kommen keine Daten rein . daher an den 11 nachsten Knoten weite r reichen next - next->e 1nfuegen( d) : retu r n this ; In der Methode .. EndKnoten::einfuegenO« wird das übernommene Objekt direkt vor dem "EndKnOlen« eingefü gt. Deshalb wird ein neues Objekt vom Typ "Allge-
Polymorphismus
meinerKnoten« erzeugt. Damit wi rd auch gleich der Konstruktor »Allgemei nerKnoten« mit den Daten und einem ne :< t-Zeiger mit der Adresse des übergebenen Knotens aufgerufen. Am Anfang ist dies die Adresse des .EndKnotens«, da dieser seinen eigenen thi s-Zeiger übergeben hat (Siehe Abbildung 4.14).
Abbildung 4 .13 Nach weiteren Delegationen
l
Li.t., ,.i n f".genO
I
I
I
Anfan5l.Knot.n, ,.inf".5I.nO
Objekl o.t. n
I
t ' dalen
I
I
BndKnot.n' ,.inf".g.n()
f---
Objekl Allg. m. in.rltnot.n
' 00.1
t
Objekt Bndltnot. n
Abbildung 4.14 Am Ende angekommen . Hierzu die Klasse lIAllgemeinerKnoten« :
Abgeleitete Klasse Al l gemeiner Knoten Di ese Klasse ist für die eigentliche Ver waltung der Daten 11 verantwortlich - im Beis piel wird zwar ei n Objekt vom Typ 11 "Daten· verwendet . aber mit Template {spateres Kapitell 11 kQnnen Sie hier noch eines drau f setzen und di e Listen I1 Klasse verallgeme i nern und somit ( f ast) unabhang i g von 11 den Daten machen cl ass AllgemeinerKnoten publiC Knoten I private : 11 11
447
I
4.8
I
4
I
Objektorientierte Programm ierung
Daten '" daten ; Knoten* next : pub 1i c : 11 Konst ru ktor Allgemeine rKno ten( Daten · d. Knoten· n ) : da t en( d) . nex t (n) I 11 Zu Debug" bzw . Versta ndn;szwecken gg f . ent f ernen cout « ·Objekt [Allgemei nerKnoten) e rzeugt \ n" : I
11 Des truktor
-A 11 gemei nerKn oten ( ) delete next : delete da t en : 11 Implizit vi rtual
Kno t en '" ei nf uegen { Daten'" d ) : 11 Implizit vi r tual
vo i d anzeige n{) :
11 Die wi chtigste n Methoden in diesem Prog ramm
Knoten'" AllgemeinerKnoten :: ein fuege n( Daten '" d ) I 11 Wir sortie r en aufwarts - kle iner We r t vor gr oBen We r t i nt r et - da ten - >vergleichen{ ~d ) : s witch( ret ) ( ca se 1: I 11 Neue Da te n vor den ak tuellen ei nordnen Allgemeine rK noten ' dKno ten new Allge me i nerKnoten ( d _ t hi s ) : return dKno ten : case-I : 11 größe r als das aktuelle Element 11 wei te r zum nachs t en Knoten next - next - >ein f uegen( d ) : return this : return this :
vo; d All gerne; nerKno t en : : anzei gene ) da t en·>anzeigen() : next - )anzei gen () :
448
Polymorphismus
Wenn das Objekt ..AllgemeinerKnoten« eneugt wurde. wird diese Adresse an den Zeiger "daten .. ubergeben. zugewiesen und als Wert von der Methode »Endknoten::einfuegenO« zurückgegeben (Abbildung 4.15):
Date n werden immer vor dem Ende eingefügt Knoten * EndKnoten : : einfuegen( Da t en >- d ) I Al l gemel nerKnoten* daten- new Allgeme in erKn ot en(d . this) : return daten;
11
I
Li.t., ,.infuegenO
I
I Obiekl
I
Anf.ngell;not.n, ,.infuege nO
I
I
t
Bndll;noten, ,elnfueg.nO
I
Daten
t
~
Objekl Allge •• inerKnoten
+ Objekl Bndll;noten
Abbildun g 4.15 ... geht es wieder zurück . Den zurückgegebenen Wert von »EndKnoten::einfuegenO ... erhält die Methode ,.AnfangsKnoten::einfuegenO .. , wo die Adresse des "AllgemeinenKnotentc dem Zeiger »nexttc von "AnfangsKnoten .. zugewiesen wurde:
Knoten * AnfangsKnoten : :einf uegen ( Daten ' d ) 1 11 Am Anfang kommen keine Da ten rein. daher an den 11 n~chsten Knoten weiterreichen next - next ->ein f uegen(d) : return t hi s : Der Rückgabewert von ,.AnfangsKnoten: :einfuegenO .. wiederum wird am Ende an das Objekt vom Typ "Liste ... zuruckgegeben, wo die Adresse nicht mehr benötigt und verwendet wird, da wir ja bereits von der Liste den Anfangs-Knoten haben (Abbildung 4.16):
449
I
4.8
I
4
Objektorientierte Programmierung
void Li s l e :: ein fu egen( Daten * d 1 ( an f ang->elnf uegen (dl ;
I
Liata •• alnfUegen(~ Objekt !Mten
t
I
[ Anfa ..... d:no t . ..... 1 .. fU ....... ll[
t
I
I
Bndltnot.n •• ei nfu.g. nO
r-
t Objekt Allgemeinerltnoten
• Objekt Endltnoten
Abbildung 4.16 ... bis die Adresse verworfen wird
Somit sieht diese verkettete Liste nach dem Einfügen von einem Objekt der Klasse "Daten« wie folgt aus:
Obj""t Lhta
,
'anhnw
Objekt
Objekt
.. nhngnnot.n
.. llll. . . l .. arJtDOta ..
'next
/
'next 'daten
/
Objekt Jtndltnota ..
I Objekt Date .. 1.erh123
Abbildung 4.17
450
Der erste eingefügte Knoten der verketteten Liste
Polymorphismus
Nachdem der erSte KnOlen eingefügt wurde, kann der nächste Knoten hinzukam· men. Da hierbei d ie Ausfü hrung w ieder ein wenig anders ist, soll der Vorgang nochmals an einem zweiten Objekt vom Typ »Daten« demonstriert werden: Objekt [End Knoten] erzeugt Objekt [AnfangsKnoten] erze ugt Wer t eingeben CO - Ende) : 123 Objekt [Daten] erzeugt Objekt [Allgemeine rKnot en] erzeugt Wert eingeben CO-Ende) : 12 Objekt [Daten] erzeugt Objekt [Al l gemeine rKnot en] erzeugt Jetzt soll das Objekt .Daten .. mit der iwe rt-Eigenschaft ,,12« in die verkenete Liste eingefügt werden. Hierbei finde n Sie zunächst w ieder die fo lgende bekannte Ausfiihrung vor: Liste elemente : Daten * daten : daten - new Daten(121 : elemen t e .e infuegen(datenl : Es wird wieder die Methode . Liste::einfuegenO« mit dem Daten-Objekt aufgerufen (Abbildung 4.18): 11 class Liste AnfangsK no t e n* an f ang ;
void Liste : : ein f uegen( Daten * d ) I anfang-)e1 nfuegen ( d);
I
Lis1e::einfuegoo()
I
Obj""'1 Daten iwert _12
Abbildun g 4.,8
_Oaten«-Objekt in die _liste« einfugen
Die Liste delegiert diese Arbeit wieder weiter an »AnfangsKnoten::einfuegen()«. Die Methode übergibt das neue Daten-Objekt an den Knoten, auf den . next.. momen tan zeigt: Knoten ~
AnfangsKnoten::einfuegen( Da ten' d ) [
11 Am An f ang ko mmen keine Daten rein , dahe r an de n
451
I
4.8
I
4
I
Objektorien tierte Programm ierung
11 nac hste n Kn oten weite r reichen next - next -> e1nfuegen(d); re t urn thi s;
Der »nex tO(-Zeiger vom Typ ,.Knoten ... verweist im Augenblick auf den Knoten mit dem -DatenO(-Objekt (iwert=123). Liat.a, ,ai .. fua ganO
I
Objekt Da t.an i .. art. a 12
,... fa .. g e r;nota .. , , a i .. fuagan ( )
Objekt Dat...
Knot an · .. a xt
i ..ert.123
Abbildung 4.'9
next-Zeiger _Knoten_ verweist auf Daten
Darauf folgt gleich der nächste Methodenaufruf ,.AlIgemeinerKnoten::einfuegenO"', class Allg eme i nerKno t en pr i vate : Daten * daten ; Knoten* ne xt :
pub 1 i c Knoten I
I, 11 Oi e wi cht i gsten Methode n i n dies em Progr amm
Knoten ' AllgemeinerKnoten : : einfuege n( Daten * d ) I 11 Wi r so r tieren au fwar ts - kleine r W ert vo r große n Wert - dat en->v erg le1chen ( 'd ) sw it ch( I ca se 0 , 11 Glei ch Grö Ber ca se L 1 11 11 Ne ue Daten vor den aktuellen ei no rdnen Allg emeinerKnoten * dKnoten oe . . . A11 geme i nerKnoten( d. t hi s ): retu rn dKnoten ;
,,' ,,'
ce'
ca se - 1:
452
,
11 . .
Klei ner
Polymorphismus
11 grOße r als das ak tue lle Elemen t 11 weiter zum nachsten Knoten next - next ->e in f uegen( d ) ; return this :
I
4.8
I
return this :
Hie rbei wird das »Daten«-Objekt (hier mit dem Zeiger »daten « (iwert=123» mi t der Methode »Dalen::vergleichenO« aufgerufen. Als Argument übergeben Sie dieser Melhode die Daten des neuen Objekts (iwert=12) : 11 Zum Vergleichen de r Eigenschaften zweier Objekte
i nt Dat en : : vergle i chen ( const Daten& d ) I i f ( iwe rt > d . i wer t ) ! r eturn 1: I : 11 .. . GrOßer if ( i we rt < d . i wert ) ! r eturn - 1: I , 11 ... Kleiner re tu r n 0: 11 Gleich
Da das neue >l Daten"-Objekt (d . i we rt) den Wert >1 12. hat und das bereits in der liste vorhandene »123 _ (i wert), gibt »Daten::vergleichenO" d en Wert 1 zurück also größer - woraufhin die swi tch-Anweisung von »AIJgemeinerKnolen:: einfuegenO« in den folgenden Abschnitt venweigt: Knoten* Allgeme i ne rKn ote n: : e i nfuegen( Da ten * d ) I 11 Wi r sort i eren aufwa rts - kle i ner We r t vor g r oßen We r t int ret - daten - >ve r gleichen( *d ) : switch( ret ) I case 0, 11 .. Gle i ch case 1: { 11 ... GrOBer 11 Neue Date n vor den aktue llen eino r dnen AllgemelnerKnoten* dKnoten new AllgemelnerKnoten( d. thls ); return dKnoten:
Jetzt wird für das neue >lDaten. -Objekt wieder ein »Allgemeine rKnoten« erzeugt. Dieser Kno ten (dKnote n) verweist jetzt auf das aktuelle »AllgemeinerKnoten«Objekt und gibt diese Adresse an den Aufrufer IIAnfangsKnoten::einfuegenO« zurück. Knoten * AnfangsKnoten :: e inf uegen{ Da ten * d ) I 1I Am Anfang kommen keine Da t en rein . dahe r an den 11 nac hsten Kno ten we i t er rei chen
453
4
I
Objektorientierte Programmierung
next - ne xt -)ein fu egen(dl ; ret urn thi s ; Jetzt haben Sie den neuen Knoten zur Liste hinzugefügt wodurch sich folgender ~b ildli c h e .. Zustand in der Liste ergibt:
Objekt Lh t e
,
' anfang
Objekt
ObjGkt
Objekt
Anhngn;notan
All gama i"ar~DOt e "
Allga .. dnerbot.n
'I>U;t
Abbildung
4.20
/
'"a"t 'da t en
/
' .. e"t ' daten
~
~
Objekt
Objllkt
Detan
Daten
iwart.12
iwert-12l
Neues Objekt
Objekt
/
Itnd);n oten
hin~ugefii gt
Ein Durchgang fehlt uns noch: Wenn ein neues Objekt dazwischen eingefügt werden soll (hier zwischen iwert=12 und iwert=123), zum Beispiel der Wert 50. Der Vorgang ist wie der eben beschriebene mit dem Wert ,.12", nur dass in der Methode ,.AllgemeinerKnoten:: ei nfuegenO" die Methode ,. Daten::vergleichenO" beim ersten Durchlauf den Wert - 1 zurückgibt, weil das ,.aktuelle .. Objekt (1 2) kleiner ist als das neue Objekt (50). Dadurch verzweigt die swi tc h-Falluntersche idung zu - 1, anstatt, wie im Du rchlauf zuvor, einen neuen ,.Al lgemeinenKnoten" zu erzeugen: 11
Die wichtigsten Methoden in diesem Programm
Knoten " AllgemeinerKnoten : : einfuegen( Daten ' d ) \
Wi r so rtieren au fw3 r ts - kleine r Wert vor großen Wert int ret - daten -)vergleichen( ' d ) : sw it ch( ret ) { ca se 0: 11 . . . Gleich ca se 1: 11 .. . Gr OBer 11 Neue Daten vo r den aktuellen einordnen Allgeme i nerKnoten* dKnoten new Allgemeine rKnoten( d . thi s ) :
11
454
Polymorphismus
I
4.8
re t u r n dKnoten : case -1: 11 ... Klei ner 11 gr öB er al s das a kt uelle Ele men t 11 weiter zum nachste n Kno t en next - next ->e1n fuegen { d ); return thls:
I
)
re t urn th i s : Hier werden die neuen Daten als Argument einfach erneut mit der Methode ,.AllgemeinerKnolen« (Rekursion) mit dem Objekt , auf das der Zeiger ,.next« hier verweist. aufgerufen. Dies wäre ein weiterer ,.AllgemeinerKnoten«, der auf ein ,.Date n«-Objekt mit dem Wert ,.123 « verweist. Ein weiterer Aufruf von ,.Date n::vergleichenO« steht bevor. Diesmal ist aber das ,.aktuelle« Objekt (123) größer als das neue Obj ekt (50), weshalb wied er ,.1« zurückgegeben wird. Somit wird jetzt ein neues Objekt der Klasse »Allgemeine rKnote n« erzeugt. mit den ,.Date n" ve rknüpft (wenn man das so sagen kann) und an den Aufrufer zurückgegeben: case 1: 11 .. . GrOBer 11 Neue Da ten vor den akt uellen e i nordnen Allge mei ne r Knoten * dKno t e n new Allg eme i nerKno t en( d . t hi s ) : return dKnoten;
Der Aufrufer war diesmal die Methode »AllgemeinerKnoten::einfuegenO .. selbst. Somit wird die Adresse des eingefügten Knote ns an das Objekt ,.AllgemeinerKnoten« weitergereicht, der das ,.Daten«-Objekt mit dem Wert 12 enthält: 11 ... Klei ner ca se -1 : 11 grOBer als das a ktue ll e Element 11 welter zum nachsten Knoten next - next->einf uegen{ d ) : r et urn th i s :
Das Obj ekt "AllgemeinerKnoten« mit den »Daten«, die de n Wert ,.12« enthalten. gibt seine eigene Adresse (t hi s) an den Aufrufer, hier die Methode »AnfangsKnoten::einfuegenO .. , und somit an den »next«-Zeiger zurück. Die Methode
455
4
I
Objektorientierte Programmierung
»AnfangsKnoten::einfuegenO« wiederum gibt ihre eigene Adresse an den Aufru· fer »Liste::einfuegenO« zurück - wo die Adresse _verworfen" wird. Sicherlich werden Sie sich fragen. warum man hier eine Adresse zurückgibt, nur um diese dann zu verwerfen? Ganz einfach. weil die Methode _einfuegenO« in Basisklasse "Knoten« deklarien wurde, und zwar mit dem Rückgabewen. Andere Redefinitionen, die von dieser Basisklasse abgeleitet sind. benötigen diesen Rück· gabewert an den Aufrufer. Würden Sie den Rückgabewen nur für die Methode ,.A nfangsKnOlen::einfuegenO_ ändern, ließe sich das Program m nicht mehr übersetzen. Das komple tte li sting
Zum Abschluss finden Sie hierzu noch mals das kompleue Listing. Die Headerdatei "dalen.h_, di e die Klasse »Daten_ beinhaltet, wurde ja bereits komplett abgedruckt. Es fehlt un s also nur noch die Headerdatei ,.lIiste.h«. die die abstrakte Basisklasse »Knoten« mit ihren abgelei teten Klassen »AnfangsKnoten«. ,.AUgemeinerKnOlen« und »EndKnoten « enthält . Außerdem ist auch die Klasse "Liste« enthalten: 11 lliste .h llinclude
class class class cl ass
Kno t en : An f angs Knot en : EndKnoten : Allgemeine r Knoten ;
11 Knoten ist die abs trakte Basisklasse 11 Alle abgeleiteten Klassen mü ssen "einfuegen " und "anzeigen "
redefinieren cl ass Knoten 1 pub 1 i c : Knoten( 1 11 virtual -Knoten() 11 vir t ual Knoten " ein f uegen( Daten * d ) - 0: virtual void anzeigen{) - 0: I, 11 ...... Abgeleitete Klasse Al lgeme i ne r Knoten .......... 11 Diese Klasse is t fO r die eigentliche Verwa ltun g de r Daten 11 ver antwor t l ich - im Beispie l wir d zw ar ein Objekt vom Ty p
456
Polymorphismus
"Daten " verwendet. aber mit Template (spateres Kapitel) 11 kOnnen Sie hier noch eines drau f se t zen und di e Listen I1 Klasse verallgemeinern und somit ( f ast) unabhangig von den 11 Da t en machen cl ass Allgemei nerKnoten publiC Knoten I priva t e : Da te n* daten ; Knoten "- next ; pub l i c ; 11 Konst ru ktor Allgemeine rKnoten( Daten" d. Knoten ' n ) : daten(d). ne~t(n) 11 Zu Deb ug - bzw . Yerstandniszwecken ggf. ent f ernen cout « "Obj ekt [AllgemeinerKnoten] erzeugt\n " ;
I
4.8
11
I
I
Destrukto r -A 11 gemei nerK noten ( ) delete ne~t ; delete da te n; 11
11 Implizit virtual Knoten* einfuegen( Dat en* d ); 11 Implizit virtual vo i d anzeige n() :
11 Die wichtigsten Methoden in diesem Prog ramm
Knoten* All gerne i nerKnoten: : ei nfuegen( Daten * d ) ( 11 Wir so r tieren au fwarts - kleiner Wert vor groBen Wert i nt ret - daten - )vergleic hen( *d ) ; switch( ret ) ( ca se 0 ; ca se 1; I 11 Neue Daten vor den akt uellen e i nordnen AllgerneinerKnoten * dKno ten new All gerne i nerKnoten( d. thi s ) ; return dKno ten : case - I : 11 größe r als das aktuelle Element - weite r zum 11 nachsten Knoten ne~t - ne~t-)einfuegen( d ) : return this : return th i s :
457
4
I
Objektorientierte Programmierung
voi d All gemei nerKnoten ; ; an zei gent ) da te n·>anz e ig en { ) ; next·>anzeige n() ; 11 ------ Abgeleitete Klasse EnÖKnoten ------------------11 Der End Knoten dien t i m Gru nd nur als Endp unk t der Li st e cl ass EndKnoten ; public Knoten 1 pub I i c ;
EndKnoten{) ( Zu Deb ug - bzw . Verst
11
I
-EndKnoten{) 11 11 Impl i zit vi rtual Knoten * e i nf uege n( Daten ~ d J ; 11 Impl i zit virtual voi d anzeig en () I I; I,
11 Daten werden imme r vor dem Ende ei ng efQg t Knoten * EnÖKno ten; ;e inf ue gen( Dat en"- d ) I Allgeme i nerKnoten * daten new Allg emeiner Knoten(d . t his} ; return daten ;
11 ------ Abge l eitete Klasse AnfangsKnoten -------------Knoten , der "n ur " immer auf das er ste Eleme nt der Liste zeigt cl ass AnfangsKnoten ; public Kno t en priva t e ; 11 " . ze igt immer auf das erste El ement Knoten* next : 11 11
publie: 11 Konstruktor
AnfangsKnoten( ) 11 ." gleich auch einen Endknoten erzeugen next - new EndK no t en ; 11 Zu Debug- bzw . Verstandniszwecken ggf , entfernen cou t « "Objekt [An fangs Knoten] erzeugt\n " ; -Anfang sKnote n(} 11
458
Polym orphismus
Implizit virtual Kno t en * einf uegen( Daten * d ) ; 11 Implizit virtual vo i d anzei gen{) ;
I
4.8
11
I
Knoten * An f an gsKnoten : : ei nfuege n( Daten * d ) I 11 Am Anfa ng kommen kei ne Daten re i n. da he r an den 11 nac hst en Knoten wei t e r reichen next - next -) e i nfuegenCd) : re t urn t hi s :
veid Anfang sKnete n: : anzeig en () next -)anzeigen() :
una bhangige Klasse Liste -------------- . . . -cl ass Liste I priva t e : AnfangsKnoten *an fang : pub 1i c : 11 Bei Anlegen gl ei ch ein Obje kt "Anfangs kn ot en " erzeugen 11 Der Konst r uktor von "Anfa ngs Kno t en " erze ug t wiederu m 11 ein Objekt "EndKnoten " . auf das dieser gleich zeig t. Lis t e ( ) I anfang - new Anfa ngs Kn oten ; I -L i ste() I de l ete anfang : I ve i d ei nfuegen{ Daten ~ d ) I an f ang-)einfueg en{d) ; 11 ------
vei d al l es_anzeigen{) I an fang-) anzeigen{) ; I,
lIendif Jetzt fehlt nur noch das Hauplprogramm: /I main .cpp llinclude "da ten. h" llinclude "lliste . h"
int main( void ) I Lis t e e lemente : Dat en'" da ten;
459
4
I
Objektorien tiert e Programm ierung
i nt iwert ; fort ; : ) I cout « "Wert eingeben (O- Ende) : " : 11 Falsc he Ei nga be oder 0 if( (Ucin » i we rt l 1 11 iwert - 0 1 break : 11 Ende da t en - new Daten(iwert) ; elemen t e. einfuegentdaten) ; elemente.alles_anzeigen( l : re t urn 0: Das Programm bei der Ausführung:
Objekt [EndKnoten] erzeugt Objekt [Anfa ngsKnoten] erzeugt Wert einge ben (O-Ende) : 10 Objekt [Da ten] erzeugt Objekt [Allgeme i nerKnoten] erzeugt Wert ei ngebe n (O- Ende) : 5 Objek t [Daten) er zeugt Objek t [A llgeme i nerKnoten] er zeugt Wert eingeben (O- Ende) : B Obj ek t [Daten) erzeugt Objek t [Allgeme i nerKnoten] erzeugt Wert einge ben (O- Ende) : 11 Objek t [ Da ten] erzeugt Objek t [Allgeme i nerKnoten] erzeugt Wert eingeben (O-Ende) : 0 5
8 10 11 Zusammenfassung
Dem ein oder anderen dürfte das Beispiel mit den verketteten Listen j etzt vielleicht recht viel abverlangt haben . Wer vielleicht verkettete Listen in einer prozeduralen Sprache wie C geschrieben hat, der wird beim OOP-Ansatz gestaunt haben und auch den Voneil von OOP erkennen. Anstatt hier Funktion für Funktion aufzurufen und eine Überprüfung nach der anderen vorzunehmen. wie di es in C üblich ist. wird die Verantwortung aufmehrere Klassen aufgeteilt. Jede Klasse hat ihren Verantwortungsbere ich, man verwendet hier einfach viele kleine Zahnräder statt eines großen.
Mehrfachvererbung
Ebenso werden die Daten von der eigentlichen Arbeit der Liste getrennt. Wenn es im Abschnitt um die Templates geht. können Sie eine solche Liste sogar unabhängig von den Daten erstellen, das heißt. die Liste können Sie immer wieder verwenden, egal wie die Daten aussehen. Bisher müssen Sie imme r noch ein paar Anpassungen vornehmen, sofern Sie andere Daten als die der Klasse ,.Daten .. verwenden wollen .
4.9
Mehrfachvererbung
Bisher haben Sie entweder Klassen neu erstellt. oder die Klassen wurden von der Basisklasse abgelei tet. Es ist aber in e++ auch möglich. eine neue Klasse von mehreren Basisklassen abzuleiten - die Mehrfachvererbung. Damit wird aus mehreren bereits existierenden Klassen ei ne neue Klasse gebildet. Die so neu definierte Klasse kann alle Eigenschaften und Methoden der bereits existierenden Klassen übernehmen. Eine mehrfach abgeleitete Klasse läsSt sich rech t einfach realisieren. Nehmen wir an. Sie haben ein Klasse ,.Brot« und eine Klasse ,.Wurst.. , dann können Sie diese beiden Klassen zum Beispiel in einer neuen Klasse ,.Wurstbrot« ablei ten. Die Syntax ist der einfachen Ableitung recht ähnlich: class Wu rstbrot public Brot. public Wurst 11 Weitere Ei genscha f ten und Methoden }
,
In diesem Fall besitzt ein Objekt vom Typ ,.Wurstb rot.. die Datenelemente der Klassen ,.Wurst« und "Brot...
Wurst
Brot
~/ Wurstbrot
Abbildung 4.21
Mehrfachvererbung
I
4·9
I
4
I
Objektorien tiert e Programm ierung
Was vererbt w ird. hängt auch hier wieder davon ab. ob Sie publ i c. protected ode r pr i vate verwenden. Das Prinzip bleibt so, wie Sie es be reits bei den einfachen abgeleiteten Klassen kennen gelernt haben. Geben Sie keines dieser drei schlüsselwörter an, so w ird automatisch pri vate dafür verwendet. In der Deklaration von ,.Wurstbrot« wurde zweimal pu b li c verwendet. wodurch in dieser Klasse alle Eigenschaften und Methoden von ,.Brot« und ,.Wurst« in der neuen Klasse ,.Wurstbrot« zur Verfügung stehen. d ie ebenfalls e ine öffentliche Schnittstelle anbietet. Hier das Beispiel der Klasse ,.Wurstbrot« in der Praxis: 11 meh rfachv ererbl . cpp llinclude
c l ass Brot ! protected : char brot[lOO) : uns i gned i nt gramm : pub 1i c : void Injtialisiere(const char * b- "" . uns i gned int g-O) I strncpy( br ot. b . 100 ) : brot[lOO - l) - 0: gramm - g : I,
un signed int gecgramm() const I ret ur n gramm : ) ; const char ' geCbrot() const I r et urn brot : ): I,
c l ass Wurst I protected : char wu r st[lOO] ; uns i gned i nt sche i ben ; pub 1i c : void l ni t ialis i ere(const char* w· " ". uns i gn ed in t s-O) I s tr ncpy( wurst . w. 100 ) ; wurst[100 1) - 0 ; scheiben - S : I,
un signed int geCscheiben{) const I ret ur n scheiben : I : const cha r* geCwurst() const I re tu r n wurst : ) : I, 11 Mehr f achvere r bung cla ss Wurstbr ot : pub11c Brot. pub11c Wurst I
Mehrfachvererbung
pub l i c : void In itAl 1e( cons t cha r* b. unsigned i nt g , cons t char* w. unsigned i nt s ) s trn c py ( brot , b . 100 ) : gramm - g : s trn cpy( wurs t. w. 10 0 ) : schei ben - s : \loi d geCAll() con st cou t « "G ra mm cou t « "Br ot cou t « "Sc he i be n cou t « "Wurst
« « « «
get_gra mm () « ' \ n' : get_brOt( ) « . \ n' : get_scheiben( ) « · \ n' : get_wur s t <) « ' \ n' :
I,
i nt main ( ) 1 Wur s t brot wb r ot; wbrot .l ni t Alle( "Vollko r nbr ot ", 100 , "Salami ", « wb ro t.g et_gramm ( ) « cou t « "Gramm "B rot cou t « « wb ro t. get_brot() « « "Sc hei ben « wb ro t. get_scheiben() cou t « « wbro t. ge t _wu rs t ( ) « "Wu rst cout « endl : cout 11 , .. oder al l e auf ei nma l wb rot.ge C All () : re t urn 0:
3) : · \n ' : ' \n ' : « ' \n ' : · \n ' :
Das Programm bei der Ausführung: Gra mm Brot Sche i ben Wurst Gramm Brot Scheiben Wurst
10 0 1/011kornbro t 3 Sa 1ami
10 0 Voll kornbrot 3 Sa 1am i
In der Praxis gestaltet sich die M ehrfachvererbung recht einfach . Gewöhnlich entstehen nur Probleme. wenn zwei Basiskl assen Elemente mit gleichem Namen haben. Da der Zugriff auf solche Elemen te nicht mehr eindeutig vom Compiler aufgelöst werden kann. müssen Sie hierzu den Scope-Operator verwenden,
I
4·9
I
4
I
Objektorientierte Programmierung
Wenn Sie beispielsweise in der Klasse Wurst die Methode lIgecscheibenO.. umändern in lOgec..grammO .. und die Eigenschaft »scheiben .. in .. gramm .. , dann haben Sie je eine Eigenschaft und eine Methode, die mit gleichen Namen in der Klasse .. Brot_ vorkommen. Im Beispiel müssen Sie daher allen Bezeichnern, die hier nicht ei ndeutig au fgelöst werden können, die Klasse mit dem Scope-Operator voranstell en: 11 meh rfac hv ererb2 . Cpp llinclude
elass Brot I pro t eeted : eha r brot[100] : uns i gned i nt gramm : pub l i e : void Initialis i ere(eonst ehar* b=" ". unsigned int g=O) I strnepy( brot. b . 100 ) : brot[lOO-ll - 0; gramm - g: I,
uns i gned i nt get_gramm() eonst I return gr amm : I : eons t ehar* geCbr ot () eonst I ret urn brot : I ; I,
(lass Wurst I proteeted ; ehar wu rst[lOOl; uns i gned i nt gramm ; pub l i e : void In iti alis i ere(eonst ehar* w-" · . uns igned int g-O) I st rncpy ( wu rst , w, 100 ) ; wurst[IOO-l] - 0 ; gramm - g : I,
uns i gned int geLgramm() eonst I return gra mm : I : eonst char* get_wurst() eonst I return wurst : I : I, 11 Mehrfachve r er bung el ass Wurstbrot public Brot , pU b)ie Wurst I pub 1i e : void In itAllef eonst eha r' b. uns igned i nt gl . eonst eha r ~ w. uns igned i nt g2 )
Mehrfachvererbung
s trn cpy( brot , b , 100 ) : Brot: :g ramm - gl; strncpy( w ~ rst . w. 100 ) ; Wurs t ::g ram~ - gZ; void get_A1H) cons t I cout « "Gr amm cou t « "Brot cou t « "Gramm cou t « "Wur s t
« « « «
Brot::get_gramm{) « ' \n ' ; geCbrot< ) « e nd l : Wur st::get_gramm{ ) « ' \n ' : ge t _wurst< ) « ' \n ' :
int main() 1 Wurstbrot wbr ot : wbrot.lnitAl1e( "Vollkorn brot ". 100 . "Salami" , 3) ; cout « "Gramm « wbrot.B r ot : : get_gramm() « ' \n ': cout« "Brot «wbrot.get_brot()« ' \n '; « wbrot.Wurst: :get_gramm() « ' \ n' : cout « "Gramm cout « "Wur st « wb rot.get_wurst() « ' \n '; cout « ' \ n ': // ... ode r alle auf einmal wb r ot . get_All() ; re turn 0:
4.9.1
Indirekte Basisklassen erben
Die Mehrfachvererbung lässt sich noch _tiefer« ausführen. So ist es möglich, eine Klasse von verschiedenen Klassen abzuleiten, die dieselbe Basisklasse besitzen eine mehrfache indirekte Basisklasse. Hierzu kann die neue Klasse _Wurstbrot« von der Klasse _Brot« und ,.WurSt« abgeleitet werde (wie bisher). Diese beiden Klassen wiederum sind von der Klasse "Supermarkt« abgelei tet. wo der entsprechende Gegens tand gekauft wurde (Siehe Abbildung 4.22). Somi t besitzt jetzt ein Objekt vom Typ _Wurstbrot« die Elemente der Klasse .. Supermarkt« zweimal. Natürlich bedeutet dies wiederum, dass der Aufruf von Eigenschaften und Methoden der Klasse .. Supermarkt« zweideutig ist, sodass ein Aufruf wie Objekt . Methode_von_Supermark t () ;
/I Fe hler!!!
I
4·9
I
4
I
Objektorientierte Programmierung
Su,p. r .... rkt
Sup.r.... rkt
i
r
Wur. t
Brot
~/ Wur.tbrot
Abbild ung 4.22 Mehrfache indirekte Basisklasse
zwangsläufig zu einem Fehler des Compilers führt, weil dieser wieder nicht weiß, ob die abgeleitete Methode vo n ,. B rot~ oder ,.Wurst. gemeint ist. Dieses Problem wird wieder mit dem Scope-Operator gelöst: Obj ek t . Bro t : : Methode_ von_Su perma rk t ( ) ; Obj ek t . Wu r st : : Met hode_vo n_Supe rma r k t ( ) ;
Hierzu wieder ein komplettes Usting: 11 meh rfachv ererb3 . Cpp llinclude us i ng namespace std :
C1BSS Superma rkt ( pro t ected: char ma r kt[ I DD] ; pub 1'1 c; Supermar kt(const char * 111"" " ) { s t r ncpy( markt . m. I OD ) : lIIa r kt[ IDO-I ] - 0; }
-Superma r kt() {} cons t cha r * ge t_ma r ktO const { ret urn mark t: }
Mehrfachvererbung
class Brot: pub l1 c Supe rmarkt 1 protected : char brot[IOO] : unsigned int gramm ; publ i c : Bra t { const char* b-"" , unsigned i nt g-O, const char* m- "" ) ; Supermarkt{m) 1 strncpy{ brot . b. 100 ) ; brat(lOO-l] - 0: gramm - g : I,
I,
class Wurst: publlc Supermarkt I pro t ec t ed : char wurst[IOO) : unsigned int gramm : pub 1i c : Wurst( const char * 101 -"" unsigned int g-O , const char * m) ; Su pe rmarkt(mJ I strncpy( wurst . 101 , 100 ) : wurst[100-1) ~ 0 : gramm - g : I,
-Wurst< J 1I unsigned int get_gramm() canst { return gramm ; I ; canst char'" geLwurst< ) const { return wu r st ; I ; I,
«
. \n ' ;
4·9
I
-Brot( J I1 unsigned int get_gramm{) canst 1 return gramm ; I ; const cha r* geCbrot{) const 1 return brot ; I ;
11 Mehrfachve r erbung cl ass Wu r stbrot : pub1ic Brot . pub1ic Wurst 1 pub 1i c : Wurstbrot{const char* b . unsigned int gl . const char* const char* w.un5igned int g2 . const char* Brot{b . g1. sll. Wurst(w . g2 , s2) I -Wurstbrot( ) 11 void get_All() canst « Brot ; : get_g ramm t ) « cout « "Gramm « get_brot( ) « . \n ' : co ut « "Brot « Brot: :geLmarkt() « cout « "Gekauft bei cout « "Gramm « Wurst :: get_gramm{)
I
sI . 52 ) :
. \n ' ; . \n ' :
4
I
Objektorien tiert e Programm ierung
cout cout
« «
«
« «
·Wurst "Gekauft bei
geLwu r st() « ' \n '; Wurst::get_ markt()
'\n ' :
int main() 1 Wurstbrot wbr otl · Vollkornbrot" , 100 , · Meier ·, · Salami ", 3, ·Eberhardt " J : tou t tout cou t
« « « « « « « « « « «
"Gramm ' \n ' ; "Brot "Gekauft be i
«
wbro t. Brot :: ge t _g r amm{J
« «
wbrot.get_brot{J « ' \n' ; wbrot.8rot::get_markt ( )
' \n ' ;
• Gr amm « wbro L Wu r s t : : ge t _g rammt ) cout ' \n ' : · Wurst «wbroLget_wurstl)« ' \n '; cout · Ge kauft bei « wbroLWurst: :getJlarkt { ) cout ' \n ' : ' \n ' : cout 11 ... ode r a lle auf einmal wbroLgeLA ll () : return 0 :
Das Programm bei der Ausführung: Gramm Brot Gekau ft bei Gramm Wurst Gekau ft bei Gramm Brot Gekau f t bei Gramm Wurst Gekau ft bei
100 Vollkornbro t Meier
10 Sol 1ami Eberhardt
100 Vo l lkornbrot Meier
10 Sa 1ami Eberhardt
Mehrfachvererbung
4 . 9 .2
I
4·9
Virtuelle indirekte Basisklassen erben
Manchmal ist es allerdings nicht wünschenswert, dass eine Klasse eine indirekte Basisklasse mehrfach erhält, wie dies im Abschniu zuvor gezeigt wurde. Warum sollten Sie fur die Klassen ,.Brot« und ,.Wurst« jeweils einen eigenen "Supermarkt .. angeben, wenn Sie beides im selben Geschäft kaufen können. In der Praxis ist es meis tens sinnvoller, wenn die indirekten Basisklassen nur einmal in der mehrfach abgeleiteten Klasse vorkommen. Dies wird mithilfe von virtuellen Basisklassen erreicht. Hierzu ist im Grunde (gegenüber der Version zuvor) nicht viel zu ve rändern. Bei der Deklaration der virtuellen Basisklassen muss nur das SchlGsselwort vi r tua 1 vor den Namen der Bas isklasse gesetzt werden: class Brot : pUbl i c v1rtual Superma r kt! /I
I,
cl ass Wurst : public v1rtual Supermarkt! /I
I,
Jetzt besitzt eine von ,.Wurst« oder "Brot« abgeleitete Klasse die Klasse markt« als virtuelle Basisklasse.
sup . .·... rkt
WUnt
Abbildung 4.23
Brot
Indirekte virtuelle Basisklaue . Supermarkt«
~Su per
I
4
I
Objektorien tierte Programm ierung
Beachten Sie alle rdings, dass sich die VinuaJi tät einer Klasse erst bei der Mehrfac h vererbung auswirkt bzw. Sinn macht. Objekte von "Wurst « ode r ,.Brot« haben nach wie vor ihren eigenen "Supermark t«, Ein Obje kt vom Typ ,.Wurs (brot« emhält diese virtuell e Basisklasse alle rdings n ur noch einmal. Somi t können die pub 1 i c-Eigenschaften und Methoden jetzt oh ne den Scope-Operator verwendet werden, da die von ,. Supermarkt« gee rbten Elemem e nur noch einmal im Speicher vorh anden sind, es gibt jetzt nur noch einen ,.Supermarkt«. Hierzu das Listing. das die indirekte virtuelle Basisklasse im Einsatz zeigt: 11 meh r fachvererb4 . cpp lIinclude us i ng namespace std ;
class Supermarkt ( protected : ehar ma rkt [lOO] ; publ i e; Supe r ma r kt(eonst ehar* m- "") s trn c py ( markt . m. 100 ) ; markt[lOO-l] - 0; -Supermarkt() 11 eonst eha r* get_mark t () eonst ( return mark t; I
class Bro t : publlc vlrtual Supermarkt I proteeted : ehar br ot[l OO] : uns i gned int g ramm ; pub l ; e : Brot( eonst ehar* b- "". unsig ned int g-O . eons t ehar * rn- "" ) : Supermarkt (m) I s t rnepy( brot. b . 100 J ; brot[lOO - l] - 0: gramm - g ; I,
-Brot() I1 unsigned int ge t _gramm() eonst I retu r n gramm : ) ; eonst ( har* ge t _brot() eonst 1 retur n br ot : I:
cl ass Wurst protected :
470
publ1c vlr tua l Su permarkt I
Mehrfachvererbung
char wu rst[lOOl : uns i gned i nt gramm : pub 1i c : Wurste const char " w- "". uns i gned i nt g-O . const char · m- "" J : Supermarkt{mJ I str ncpy( wu rs t, w, 100 ) ; wurst[100- 11 - 0; gramm - g; I, -Wurst() 11 un signed int get_gramm() const I return gramm : I : eonst cha r'" get_wurst() eonst I return wurst : I :
,
, ,
«
« «
• \ 0' :
« «
"Wu rst "Ge kauft bei
ge Lwurst( ) geCmarkt( )
« ' \n ' ; « . \n ' ;
i nt main() I Wurstbrot wbr oU "Voll kornb rot" . 100 . "Salami" . 20. "Eberhardt " ) : cout
« "C ramm
« cout «
. \n' ; "Brot "Gramm
« « ' \n ' : cout « "Wurst cout « "Ge kauft bei cout
cout
«
« wb rot . Brot :: get_grümm()
«
wbrot .get_brot()
«
. \n ' :
« wbrot .Wurst : :get_gramm() « wbrot .get_wurst() « « wbrot.get_ma r kt() «
4·9
I
11 Mehrfachvererbung el ass Wurstbrot publie Brot , pub l ie Wur st I pub 1i c : Wurs t bro U const ehar · b . unsigned i nt 91. const char · w. unsigned i nt 91. ) const char'" Brot(b . g1) , Wurs t(w . g2) , Supermarktes) -Wurs t brot () 11 void ge t _All() cons t « Brot : :get_gramm() « . \ n' : cout « "Gramm « ge t _brot( ) « . \n ' : cout « "Brot « Wurst : :get_gramm() cout « "Gramm
cout cout
I
. \n ' : . \n ' :
• \n ' ;
471
4
I
Objektorien tiert e Programm ierung
11 ... oder al l e au f einma l wb rot. ge L All () ; return 0;
Das Programm bei der Ausführung: Gra mm Brot Gra mm Wurs t Gekau ft be i
100 Vol lkornb rot 20 Sa 1am i Eberhardt
Gramm Bro t Gramm Wurst Gekauft be i
100 Vol l kornbrot 20 Sa 1ami Ebe r ha rdt
Reihenfolge bei der Erze ugung der Objekte
Die Erzeugung von Objekten bei der Mehrfachvererbung geschieht (gemäß der Abbildung 4.22 bzw. Abbildung 4.23) von oben nach unten. Beim Erzeugen eines Objekts werden also zunächst die Teilobjekte erzeuge die von der Basisklasse geerbt wurden. In Bezug auf die Konstruktoren, die immer als Erstes aufgerufen werden, gilt: ..
Erst werden die Konstruktoren der (indirekten) virtuellen Basisklassen ausgeführt (von oben nach unten).
..
Als Nächstes we rden die Konstruktoren der nicht virtuellen, direkten Basisklassen aufgerufen.
..
Am Ende wird dann der Konstruktor der »eigenen" Klasse aufgerufen.
Bezogen auf unse r Beispiel »mehrfachvererb4. cpp" wird beim Erzeugen eines »Wurslbrot«-übjekts zunächst der Konstruktor der indirekt virtuell en Basisklasse »Supermarkt" aufge rufen. Anschließend werden die beide n Konstruktoren der direkt virtuellen Basisklassen ..Wurst" und "Brot« aufgerufen . Am Ende erfolgt der Aufruf des Konstruktors der eigenen Klasse .. Wurstbrot«.
472
Mit Templates haben Sie eine Technik, mit der Sie Klassen und Funktionen unabhängig vom konkreten Datentyp erzeugen können. In diesem Kapitel erfahren Sie aUes zum Template-Konzep t von C++.
I
sn
5
Templates und
5.1
Funktions-Templates
Da e++ ei ne typengebundene Sprache ist, kommt es häufig vor, dass dieselbe Funktion mehrmals, nur mit um erschiedlich em Typ, implememiert. Wenn Sie zum Beispiel Algorithmen für das Sortieren oder Suchen von Arrays implementieren und hierbei eine Version für 10 ng und eine Tur f10at anbieten wollen, müssen Sie zwei Versionen der Funktio n anbi eten oder aber Funktions-Templates verwenden. Das Prinzip soll anhand des fo lgenden Beispiels demonstriert werden: 11 func _ temp1atel . cPP Ilinc1ude
10ng Big Num{ l ong n1 , 10ng n2 ) ; floa t Big Nu m (f10at nl , flo a t n2) ; void Swap{ 10 ng& n1 , 10ng& n2) ; void Swap{ f1oat& n1 , f10at& n2) ; i nt ma i n{) lang 1numl - 100 , 1num2 - 111 : f1 0a t fnu ml - 100 . 1. fnum2 - 111.1; cout cout
cout cout
« « « «
- Größ erer wer t Big Nu m{ 1 numl , ~ G röB erer W er t Big Nu m{ fnuml .
« « «
~ lnuml :
1 num2
«
~ Tausche
"
«
,.
(lang) 1 num2) « ( f1 oat) : fnu m2 ) «
1numl
«
"
~ \n ~
;
" ~ \n\n ~:
1 num2 :
~ \n ~:
Werte
(long)\n~
:
473
5
I
Temp!ates und STL
Swap( lnuml . lnum2 ) ; COut « " I num! : " « 1numl « 1num2 « " 'n" ;
« "
1num2 : "
"f numl : " « fnuml « " fnum2 : fnum2 « " 'n" ; cout "Tausche Werte (floatl\n " : Swap( fnuml . fnum2 ) : cout « "fn uml : " « f numl « " fnum2 : « f num2 « "\n " : return 0 ; cout
« « «
10ng BigNum( 10ng nl . long n2 ) I if( nl ) n2 ) I return nl ; I else i f ( nl ( n2 ) I r eturn n2 :
float BigNum( floa t nl . float n2 ) i f( nl ) n2 ) I return nl : I else i f ( nl ( n2 ) I r eturn n2 :
void SwapC 10n g& nl . 10ng& n2 ) [ 10ng tmp - n1: nl - n2 : n2 - tmp:
vo i d Swap( float & nl . floaU. n2 ) I float tmp - n1 : nl - n2 : n2 - tmp : Hier haben Sie zwei Versionen de r Funktion " B igNumO ~ . die den größeren von zwei 1ong- bzw. fl oat-W erte n zurückgibt. und di e Funktionen ,.SwapO- . die zwei 10ng- bzw. float -Werte untereinander austauschen. Gewöh nlich werden Sie auch noch ein e Version für short hinzufügen wollen. Allerdings haben Sie d ie Möglichkeit, Funktions-Templates zu verwenden. Funktions-Templates können Sie sich wie e ine Schablone vorstellen . sie sind ein e Vorlage gleichartiger Funktio nen. die sich vom Rü ckgabewen de r Funktion. vom Datentyp de r Parameter und/oder dem Datentyp de r lokalen Variablen d er Funktion unterscheiden. Außerdem muss (im Gegensatz zur Funktions·Überiadung)
474
Funktions-Templates
I
5_1
die aus einem Funhions-Temp[ates erzeugte Funktion d ie gleiche Anzahl an Parametern haben und auch die gleichen Anweisungen enthalten. Dadurch ergeben sich fur den Programmierer folgende Vorteile: ..
Durch das Funktions-Template wird der Code kürzer, da dieser nur einmal programmiert werden muss und dann für verschiedene Typen zur Verfilgun g stehL
..
Die Funktionen werden aus dem Template automatisch erzeugt. sobald diese verwendet werden.
..
Weniger Code und weniger Funktionen bedeuten auch weniger Aufwand beim Suchen nach Feh lern und beim Testen des Programms. Der Wartungsaufwand wird auch geringer.
5_1.1
Funktions-Templates definieren
Funktions-Tem plates werden mit dem Präfix
template (cl ass T> eingeleiteL Der Parameter "T ~ steht hier für den Typnamen, der in der gleich fo[genden Definition verwendet wird. Somit sieht die Defin ition der FunktionsTemplates "SigNumO.,; wie fo lgt aus:
templ ate T SigNum( T nl. T n2 ) I if ( nl > n2 ) { retu rn nl : ) else if( nl ( n2 ) { return n2: Verglichen mit der ursprünglichen Defi nition
long BigNum( long 01 . 10 ng n2 ) I i f ( nl > n2 ) { retur n nl : ) else ife nl < n2 ) I return 02 :
f loat Sig Num( float nl . float n2 ) i f ( nl > n2 ) { retur n nl : I else if( nl < n2 ) I r etu rn n2 : wurde "T_ verwendet statt eines Datentyps beim Riickgabewert und des Parameters. Das Gleiche ist auch bei lokalen Variablen möglich, wie sie bei der Fu nktion ,.SwapO- verwendet und benötigt werden. Die Funktions-Templates für "SwapO .. sehen dem nach wie fo lgt aus:
475
I
5
I
Te mp!ates und STL
template void Swap{ T& 1'1 1. T& 1'12) 1 T t mp - 1'1 1: 1'11 - 1'12 : 1'12 - tm p: Der Parametername >lT« in der Definition der Funktions-Templates wird wie ein
normal er Typname verwendet und muss nicht zwangsläufig ,.T« heißen. Hier können Sie beze ichnerübliche Namen verwenden - in der Praxis findet man aber häufig den Ty pnamen ,.T" vor. Hier das komplette Listing >l fun c template1.cpp" neu, jeut mit den Funktions-Templates: 11 f unc_tem plate2 .cpp llinclude us i ng namespace std :
templa t e T Big Nu m( T 1'11 . T 1'12 ) ; temp1a t e voi d Swa p( T& 1'11 . T& 1'1 2) : int main ( ) 1 10ng ln uml - 100 . lnum2 - 111 ; f l oa t fnuml - 100 . 1. fnum2 - 11 1. 1; cout cout
« « « «
"Gr OBerer Wer t Bi gNu m( 1numl . "GrOBerer Wert Bi gNu m( f numl .
,
" (lang) 1n um2) « "\1'1" ; (f1 oat ) : " f num2) « "\1'1\1'1" ;
"l numl : " « 1numl « " 1num2 ; « 1n um2 « "\n" ; co ut « "Ta usche Werte ( l ong)\n " ; $wap( 1numl . 1num2 ) ; cout « "l num! : " « 1num! « " 1num2 : « 1num2 « "\n "; cout
«
cout « "fnuml : " « fnuml « " fnum2 : « f num2 « "\n" : cout « "Tausc he Werte ( f10at)\ n"; Swa p( fnuml . f num2 ) ; cout « "fnum! ; " « fnum! « " fn um2 : « f num2 « "\n " : return 0;
476
Funktions-Templates
I
5 _1
template T Si gNum( T nl, T n2 ) 1 if ( nl > n2 ) 1 return nl : I else if( nl < n2 ) f ret urn n2 ;
I
template void Swa p( T& n1 . T& n2) 1 Ttmp - n1 : n1 - n2 : n2 - tmp: Mit der Definition eines Funktions-Templates wird noch lange keine korrekte Funktion erzeugt. Der Maschinencode wird erst erzeugt, wen n eine konkrete Funktion fur e inen bestimmten Typ benötigt wird. Wird zum Beispie l niemals ein Funktionsaufruf "SwapO" fur den Datentyp float gemach t, so wird auch keine solche Funktion als Maschinencode erze ugt. Man sagt, ein Funktions-Template wird instantiiert, also für einen bestimmten Typ generiert, wenn dieses Template zum ersten Mal aufgerufen wird. Das Ermitteln des entsprechenden Datentyps übernimmt dabei der Compiler: int iwertl - 100 . iwert2 - 200 ; Swap( iwertl . iwe r t2 ) ; 11 We r te tauschen Jetzt erzeugt der Compiler eine Funktion aus dem Funktions-Template "SwapO« für den Datentyp i nt. Der Compiler ersetzt im Maschin encode den Parameter ~T« durch das Template-Argument i nt. Wird erneut eine Funktion mi t den Argumenten int benötigt. so wird die berei ts generierte Funktion aufgerufen. Wird jetzt die Funktion ~ SwapO « mit float-Argumenten aufgerufen , dann wird ei ne weitere Funktion aus dem Funktions-Template für den Datenty p fl aat vom Compi ler generiert. Natürlich ist es auch erlaubt, solche Funktions-Templates als i n1 i ne zu deklarieren: templa t e I nl1ne vuill SWd lJ{ 1& Jll. T& 112) {
T
tmp - nl : nl - n2 : n2 - tm p:
Des Weiteren ist es möglich, dass in einem Funktions-Template ein weiteres Funktions-Template aufgerufen wird - man spricht dabei vom Verschachteln von Funktions-Templates:
477
5
I
Temp!ates und STL
11 f unc_template3 .cPP llinclude us i ng namespace std ;
template baal BigNum( T n1 . T n2 ) : temp1ate void Swap( T& n1 . T& n2) : int main() I lang lnum1 - 111 . lnum2 - 11 ; f10at fnuml - 11 . 1. fnum2 - 111 . 1; cout « 1n uml « cout « f numl «
« 1num2 « "'n "; « fnu m2 « "'n " ;
11 Tauschen . fal l s nötig
Swap( 1numl . 1num2 ) ; Swap( fnuml . fnum2 ) : cout « 1numl « cout « fnuml « return 0:
« 1num2 « "'n ": « fnu m2 « "'n ":
temp1ate bool B1gNum( T 01. T 02) if( nl > n2 ) { retu rn tr ue : else { return fa l se ; I
templa t e vo i d Swap( T& n1 . T& n2l I if( Sig Nu m( 0 1. 02)
l I
T tmp - nl : n1 - n2 : n2 - tmp :
Hier werden zwei Werte nur getauscht. wenn der linke Wert größer als der rechte Wert in der Argumentenliste ist. 5.1.2
Typenübereinstimmung
Beachten Sie, dass beim Auflöse n eines Templates vom Compiler niemals eine automatische Typen konvertierung vorgenommen wird. Das neue Fun ktionsTemplate muss immer so generiert werden. dass die Datemypen der Parameter
478
Funktions-Templates
I
5 _1
mit den Typen der Argumente übereinstimmen_ Selbst einfachste implizite Typenanpassungen von int nach long ftlhren hier zu einem Compilerfehler: int
i~al
- 100 ;
long l~al - 200 ; Swap( i~al . l~al l ; 11 Fehler ! ! !
I
Hier müsste Folgendes generiert werden: void Swap( int &. 10n g& ) ;
Aber bei der Definition des Funktions-Templates wurde mit ~T" nur ein Typ angegeben. Hi erbei können Sie entweder selbst eine Typenanpassung vornehmen, wie beispielsweise: Swa p( (long) i val . lv a ll ; 11 ... oder ... Swap( 100 . l ~ al ) ;
oder aber Sie ers tellen ein Funktions-Templale. das verschiedene Parameter aufnehmen ka nn (Siehe Abschnitt 5.1.5). 5 .1.3
Funktions-Templates über mehrere Module
Wollen Sie Funktions-Templat es über mehrere Module generieren, sollten Sie die Definition des Templates in eine Headerdatei stellen. damit das Template in allen Modu len zur Verfügung steht, wo die Headerdatei eingebunden wird. Der Grund dafur ist. dass die endgültige Funktion (Maschinencode) erst beim Aufruf einer durch das Funktions-Template vorgegebenen Funktion erstellt wird. Und dafür benötigt der Compiler den Code der Funktion. 5 .1.4
Spezialisierung von Funktions-Templates
In manchen Fällen wird eine Spezialisierung der Funktions-Templales nötig. Wollen Sie zum Beispiel mit einem Funktions-Template zwei Objekte einer Klasse tauschen, muss für diese Klasse der Kopierkonstruktor und die Zuweisung vorhanden sein. Hier liegt also die Spezialisierung nicht an dem Funktions-Template, sondern an der Klasse. Bei dem fo lgenden Beispiel muss man schon zweimal überlegen, ob es korrekt ist, wenn man Funktions-Templates auch aufC-Strings anwendet: 11 f unc _ tem plate4 . cpp llinclude us i ng names pace std ;
479
5
I
Te mp!a tes und STL
templa t e void Swap( T& n1 , T& n2 ) ; iot main() I 100g ln um1 - 111 . lnum2 - 11 : char* st rl - "ASCD" ; char* st r2 - " EFGH" ; cou t « l oum1 « " «loum2« "\n "; Swap( 1num1 . loum2 ) : cout « lnuml « " " « l num 2 « " \0 " : cout « st rl « " " « st r 2 « "\n "; Swap( st rl, st r 2 ) ; cout « st rl « " " « str 2 « "\n "; ret urn 0;
t emplate void Swap( T& n1 , T& n2) [ Ttmp - nl; n1 - n2 : n2 - tmp : Zugegeben, das Beispiel funktio niert noch. Anders sieht es allerdings mi t dem folgenden Beispiel aus: 11 func _temp1 ateS , CPP llincl ude using namespace s td ;
template T Big Objekt( T n1. T n2 ) ; i nt main() I long lnum1 - 111 . l num2 - 11 ; char* s t r l - "ABCD "; char* s tr 2 - "AAAA "; cou t « Bi gObjekt( l numl , lnu m2) « "\n " ; cout « B1g0bjekt(s t r l, str2) « "'n " ; return 0 :
template
T BigObje kt( T n1 , T n2 ) [ i f ( n1 > n2 ) [ return n1 : else if( n1 < n2 ) I ret urn n2 :
480
Funktions-Templates
I
5_1
Auch wenn es hier den Anschein hat, dass alles richtig abläuft, sollten Sie bedenken, dass lediglich die größere der beiden Adressen zurückgegeben wird, unter de nen die Strings gespeichert sind. Hier haben Sie nun die Möglichkei t, eine Spezialisierung einzubauen. DafLir müssen Sie theoretisch nur eine separat definierte Funktion überladen: 11 func_templa t e6 . cp p llinclude llinclude us i ng namespace s t d :
t empla te T BigO bjekt( T nl . T n2 ) : eons t eha r* B1g0bjekt( eonst eh a r* 51, eo nst ehar* s2) ; int mairl{) ( long lnuml - 111 . l num2 - 11 : char * strl - "ABCD" : char * str2 - "AAAA ": cout« BigObje ktrlnuml . ln um2 1 « "'n" : cout « BigObj ekt (s tr\ . s tr 2> « "'n ": return 0:
template T Bi gObje kt( T n1 , T n2 ) ! if ( n1 > n2 ) ! return nl : else if( nl < n2 ) I ret urn n2 :
cons t ehar* Blg0bje kt( eonst ehar* sI. eonst ehar * s2 ) ( 1f( strcllp( 51, s2) ) 0 ) { return 51; } else { return 52 ; } De r Reihenfolge wie der Com pil er nach einer Funktion sucht. garantiert Ihnen, dass die spezialisie rte Funktion stets vor dem Funktions-Template verwendet wird, wenn der entsprechende Typ (hier char *) deklariert wurde. Allerdings wirft dies bei umfangreiche ren Projekten, die über mehrere Module programmiert wurden, ein Problem auf. Wird nämlich die Spezialisierung in einem andere n Modul als das Funk tions-Template definiert, weiß der Compiler nicht, ob eine Deklaration einer Template- Instanz oder eine Spezialisierung vorliegt.
I
5
I
Te mp!ates und STL
Daher gibt es im neusten ANSI-Standard eine eigene Syntax , wie eine Spezialisierung zu definieren ist. Nat.ürllch bedeutet dies auch, dass dieser Vorgang noch nicht von allen Compilern unterstützt wird. Eine solche Spezialisierung beginnt mH dem Präfix: template
<>
Bezogen auf die Funktion "BigObjektO« sieht die Spezialisierung dieser Funktion wie folgt aus : 11 f unc_temp l ate7 . cpp If i nc1 ude lf i nc1ude using namespace std ;
template T BigObjekt( T nl , T n2 ) ; template <> const cha r* BigObjekt( const char* 51 , const char * s2) ; int main() I 10ng 1numl - 111 , 1num2 - 11 : cha r* strl - · ABCO" ; cha r* str2 - · AAAA" ; cout « BigObjek t( lnuml . l num2) « ·'no ; cout « BigObjek t( strl . str2) « "'n· ; return 0;
template T Bi gObje kt( T n1 . T n2 ) ( i f ( n1 > n2 ) I return n1 ; else if( n1 < n2 ) I retu rn n2;
templ ate <> cons t cha r* BigObjekt( const char* sI , const c har* s2 ) I if( st r cmp{ 51 , 52) > 0 ) I return 51 ; 1 el se I feLufn s2 ; 1
Diese Spezialisierung w ird in folgenden Fällen eingesetzt ..
Die gewöhnliche Methode über das Funklions-Template liefen kein vernünftiges Ergebnis .
..
Im Funktions-Template gibt es Anweisungen, die auf einen bestimmten Typ nicht ausgeftihn werden können (beispielsweise s trcmp( ) für Zahlen).
Funktions-Templates
5.1.5
I
5_1
Verschiedene Parameter
Funktions-Templates sind alle rdings nich t nur auf einen formalen Datenty p beschränkt, son dern können auch mit mehreren Typenparametern definiert werden:
func_tem plate8 cpp lIinclude llinclude us i ng namespace std : 11
templ ate void funktion( Tl n1. T2 n2 ); int main{) ! funktion{ funktio n{ funktion( funktio n{ retu rn 0;
100, 111 . 111 ) ; "Ha l lo Welt ", 12] ) ; 'p', ] , 14 ) ; 111.111 . 222 . 222 ) ;
template void f unktion( Tl n1 . T2 n2 ) ( co ut « nl « .. : .. « nZ « "'n" : Beide Parameter ,.Th und ,.Tb: in der Definition werden wie normale Typennamen verwendet. Natürlich ist es auch möglich, dass Sie hierbei zwei gleichwertige Argumente verwenden. Zum Beispiel ist ein Aufruf von
i nt ivall - 100 . i va12 - 200 ; funkt io n( i val 1. i va12 kein Fehler, auch wenn es unnötig erscheint, dass hier zwei Typparameter verwendet wurden. Es sollte außerdem nicht unerwähnt bleiben , dass Sie bei Funktions-Templates auch ,.gewöhnliche.. Parameler mil allen üblichen features verwenden könn en:
template void funktion ( T n1 , long nZ ) [ 11
I
5
I
Temp!ates und STL
5 .1.6
Explizite Template-Argumente
Die bisherige Ableitung der Funktions-Template war impliziL Das bedeu tet. ein Funktions-Template wurde mit einem bestimmten Typ inSlantiiert. wenn diese zum ersten Mal aufgerufen wurde_ Der Compiler ermittelte dann selbst den Typ für den (oder die) Parameter ,.T" anhand der Funktionsargumente. Sie haben aber auch zusätzlich die Möglichkeit. den oder die Tem plate-Argument(e) (gemäß dem ANSI-Standard) explizit anzugeben. Dabei werden die Template-Argumente in spitzen Klammern hinter dem Template-Namen eingesetzt. Hierzu soll nochmals das folgende Funk tions-Template verwendet werden:
t empla t e
Bi gObjekt( 100 . 111 . 111 ) ; Bi gObjekt< 111.111 , 222 . 222) ; Bi gObjekt( ' A', 67l ;
11 Fehler -> (long , do ub le) 11 Ok -) (double . double) 11 Fehler -> (char . int)
so würde de r Compiler das Listing nicht übersetzen, weil zweimal verschiedene Template-Argumente verwendet wurden, was in der Definition nic ht vereinbart war. Wollen Sie diese n Aufruf dennoch erzwi ngen, können Sie explizi te Template-Argumente verwenden . Mit
Bi gObjekt ( 100 . 111.111 ) ; erzwingen Sie zum Beispiel. dass ein Funktions-Template für den Datentyp f l Oilt generiert wird , egal. ob Sie hier einen anderen Datentyp als Argument verwendet haben . Natürlich funktioniert das auch mit mehreren Parametern . Hierzu ei n Listing, das die expliz iten Argumente rur Funktions-Templates in der Praxis demonstriere 11 func_tem plate9 . cPP lIinclude llinclude
t emplate T 8igObjektf T nl . T nZ ) : t empla t e vo id f unktion( Tl nl . TZ nZ ) ;
484
Klassen-Templales
I
5·2
int main() I 11 Funktion fOr floa t gene r ieren cou t « BigO bjekt< f loat>{ 10 0. 111 . 111 « "'n" : 11 Funktion fOr i nt generieren cou t « BigO bjekt< i nt>( 111 . 111 , 222 . 222 ) « "'n " : 11 Funktion fOr char gene rie r en cou t « BigObjekt{ ' A', 67) « "'n ": 11 Funktion fOr ein in t und e i n char generieren funktion{ ' A'. 65) : 11 Funk t ion fOr zwe i in t gene r i eren funktio n{!1 .1! , ' B' ) ; retu rn 0:
I
templa t e T Bi gObje kt( T nl , T n2 ) 1 if ( nl > nZ ) I return nl : else if( nl < n2 ) I ret urn n2 :
template void funktion( Tl n1. T2 n2 ) cout « nl « " : " « nZ « "'n ": Das Programm bei der Ausführung:
111.111 22Z C
65 11
A 66
Hin weis Beachten Sie, dass viele der hier beschriebenen Nutzungsmöglichkeiten
der Funktions-Templates erst bei den neueren Compilern (ab ca. 1999) zur Verfügung stehen.
5.2
Klassen-Templates
Die Templates, die Sie im letzten Abschnitt kennen gelernt haben, sind nicht nur auf Funktionen beschränkt, sondern könn en auch mit Klassen venvendet werden. Was zunächst recht suspekt erschei nt, wird relativ häu(1g verwendet, wenn es um die Entwicklung von Klassenbibliotheken geht. Recht populäre Beispiele von Klassen-Templates dürften wohl die STL-Bibliothek (siehe Abschni tt 5.3) und die Stream-Klassen sein, die alle standard mäßig als Tem plate implementiert sind.
[« )
5
I
Temp!ates und STL
Im Abschnitt über verketteten Listen (Abschnitt 4.8.9) wurde bereits erwähnt. dass die Implementierung nur auf Datenobje kt e angewandt werden konnte. rur die sie programmiert wurden. Im Beispiel waren dies Objekte der Klasse "Daten ... Würden Sie diese verkettete liste jetzt mit anderen Datenobjekten verwenden wollen. müssten Sie den Code zu den ve rketteten Listen anpassen. was in der Praxis meistens mehrere Zeilen Code sind. wie es das Beispiel zu den verketteten Listen demons triert. Mit den Klassen-Templates haben Sie nun die Möglich keit. die verke ttete Liste so umzuschreiben. dass Sie diese völlig unabhängig von den Datenobjekten verwende n kön nen . Hinweis In der Praxis werden Sie wohl kaum die verketteten listen in C++ selbst schreiben. Solche immer wiederkehrenden und häufig benötigten Aufgaben bietet Ihnen die STL-Bibliothek. aber auf der anderen Seite sind die verketteten listen immer so etwas wie ein Mittelstück vom Anfänger zum Profi. In Schulen und Universitäten werden die verketteten listen außerdem immer wieder gerne für Prüfungsaufgaben verwendet.
[» l
5.2.1
Definition
Wie schon die Funktion s-Te mplates beginnt ein Klassen-Template mit dem Präfix tempI a te und der anschließenden Klassendefinition: template class K lassen~ame I }
,
1/
Hier definieren Sie eine Klasse mit dem Namen Kl a ss e nNa me . Der Parameter "T.. steht für einen beliebigen Typ. Die Angaben ..T.. und ,.KlasseName .. werden in der Klassendefinition wie normale Typen verwendet. Die erste Verwirrung dürfte entstehen. weil hier von ..T.. und .. KlassenName .. die Rede ist. Die Unterscheidung ist wichtig im richtigen Geltungsbereich. So kan n innerhalb des GelLUngsbereichs einer Klasse ... KlassenName.. anstatt der Angabe .. KlassenName .. angegeben werden. Die Angabe . KlassenN ame.. ist der Date ntyp. und die Angabe ohne . T.. ist der Tem plate-Na me. Bezogen auf die verkette te Liste sieht das Template der Basisklasse ,.Knoten" fol gendermaßen aus :
templ ate class Knoten I pub li c : Knoten() II
Klassen-Templales
I
5·2
vi rtud 1 -Kno t en() 11 virtual Knoten* einfuegen( T* d ) - 0: virtual vo i d anzeigen() - 0: I,
Mit diesem Klassen -Template wird der Daten typ ,.Knotend>" definiert. Der Parameter "Tc< ist hierbei der Typ des Objekts, der in der Liste eingefligt we rden kann - was im vorherigen Beispiel (Abschn itt 4.8.9) zu den verkeueten Listen ein Objekt der Klasse "Daten« war (und theoretisch auch hier wieder sein kann). Es soll auch gleich erwähnt werden , dass es auch möglich ist. wie bei den Funktions-Templates, mehrere Typparameter zu definieren. Hier eine solche Definition mehrerer Typparameler: t empla t e cl ass KlassenName 1 11 I,
Hiermit haben Sie eine Klasse "KlassenNamed1 , T2>« defi niert. Die Parameter "T1 « und "T2« stehen für einen bestimmten Typ . 5.2.2
Methoden von Klassen-Templates definieren
Die Methoden eines Klassen-Templates mit dem Parameter ,.T" werden, wie die Klasse selbst, über den Typ ..T« parametrisiert. Die Definition einer solchen Mernode selbst stelh somit wiederum ein Funktions-Template dar. Die Definition außerhalb der Klassen-Templates hat gewöhnlich folgende Syntax: templ ate void KlassenName<1> : :methodenName( Parameter) { 11
Hiermit wurde die Methode »methodenName« der Klasse defin iert.
~ Kl assenName «